자연어처리(NLP) 17일차 (로이터 뉴스 분류하기)

정민수
15 min readJun 20, 2019

--

2019.06.20

출처 : https://wikidocs.net/book/2155

핵심키워드

  • LSTM

로이터 뉴스 분류하기(Reuters News Classification)

이번 챕터에서는 케라스에서 제공하는 로이터 뉴스 데이터를 이용하여 LSTM을 이용한 텍스트 분류를 진행해보도록 한다. 로이터 뉴스 데이터는 총 11,258개의 뉴스 기사가 46개의 카테고리로 나누어지는 테스트 데이터다. 우선 데이터가 어떻게 구성되어있는지에 대해서 알아보고, 실습을 진행한다.

1. 로이터 뉴스 데이터에 대한 이해

from keras.datasets import reuters
(X_train, y_train), (X_test, y_test) = reuters.load_data(num_words=None, test_split=0.2)

object arrays cannot be loaded when allow_pickle=False 라는 경고가 뜰 수 있다. numpy 1.16.3 버전에서는 np.load()파라미터에 allow_pickle이 추가되어 케라스 데이터를 불러오는데 오류가 생기는 건데, reuters.py 에서 np.load(path, allow_pickle=True)를 바꿔줘도 로드가 안돼서 numpy를 1.16.1 버전으로 다운 그레이드 했다.

pip uninstall numpy
pip install --upgrade numpy==1.16.1

numpy가 바뀐 버전.

np.__version__
'1.16.1'

먼저 케라스 데이터셋으로부터 로이터 뉴스 데이터를 다운로드하고, 훈련 데이터와 테스트 데이터를 나눈다.

여기서 num_words는 이 데이터에서 등장 빈도 순위로 몇 번재에 해당하는 단어까지를 갖고 올 것인지 조절한다. 예를 들어서 저기에 100이란 값을 넣으면, 등장 빈도 순위가 1~100에 해당하는 단어만 갖고 오게 된다. 모든 단어를 사용하고자 한다면 None으로 설정한다. 정확하게 무슨 의미인지 이해가 안간다면, 아래에서 훈련 데이터를 출력할 때 보면 이해할 수 있다.

test_split은 테스트 데이터를 전체 데이터 중 몇 퍼센트를 사용할 것인지를 의미한다. 우리는 전체 데이터 중 20%를 테스트 데이터로 사용할 것이므로, 0.2로 설정한다.

print('훈련 데이터: {}'.format(len(X_train)))
print('테스트 데이터: {}'.format(len(X_test)))
num_classes = max(y_train)+1
print('카테고리: {}'.format(num_classes))
훈련 데이터: 8982
테스트 데이터: 2246
카테고리: 46

y_train은 0부터 시작하는 숫자들로 카테고리 라벨을 부여하므로, 가장 큰 수에 +1을 하여 출력하면 카테고리가 총 몇 개인지를 알 수 있다. 훈련 데이터는 8,982개, 테스트 데이터는 2,246개, 카테고리는 46개인 것을 확인할 수 있다.

print(X_train[0])
print(y_train[0])
[1, 27595, 28842, 8, 43, 10, 447, 5, 25, 207, 270, 5, 3095, 111, 16, 369, 186, 90, 67, 7, 89, 5, 19, 102, 6, 19, 124, 15, 90, 67, 84, 22, 482, 26, 7, 48, 4, 49, 8, 864, 39, 209, 154, 6, 151, 6, 83, 11, 15, 22, 155, 11, 15, 7, 48, 9, 4579, 1005, 504, 6, 258, 6, 272, 11, 15, 22, 134, 44, 11, 15, 16, 8, 197, 1245, 90, 67, 52, 29, 209, 30, 32, 132, 6, 109, 15, 17, 12]3

훈련 데이터 X_train 중 첫번째 훈련 데이터 X_train[0]에는 숫자들이 들어있다. 텍스트 데이터가 아니라서 의아할 수 있는데, 현재 이 데이터는 토크나이제이션과 단어의 등장 횟수를 세는 것과 단어에 인덱스를 부여하는 전처리가 끝난 상태라고 이해하면 된다.

이 데이터는 단어들이 몇 번 등장하는 지의 빈도에 따라서 인덱스를 부여했다. 1이라는 숫자는 이 단어가 이 데이터에서 등장 빈도가 1등이라는 뜻이다. 27,595라는 숫자는 이 단어가 데이터에서 27,595번째로 빈도수가 높은 단어라는 뜻이다. 즉, 실제로 빈도가 굉장히 낮은 단어라는 뜻이다. 앞서 num_words에다가 None을 부여했는데, 만약 num_words에 1,000을 넣었다면 빈도수 순위가 1,000 이하의 숫자만 등장할 것ㄷ이므로 27.595와 28,842 같은 1000이상의 숫자에 해당되는 단어는 갖고 오지도 않고, 출력되지도 않는다.

훈련 데이터 y에서 첫번째 훈련 데이터인 y_train[0]에는 3이라는 값이 들어있다. 이 숫자는 첫번째 훈련 데이터가 46개의 카테고리 중 3에 해당하는 카테고리임을 의미한다. 방금본 것은 8,982개의 훈련 데이터 중 첫번째 데이터만 확인한 것이다. 이번에는 8,982개의 각 X_train의 길이가 대체적으로 어떤 크기를 가지는지 그래프를 통해 확인해본다.

import matplotlib.pyplot as plt
% matplotlib inline
plt.hist([len(s) for s in X_train],bins=50)
plt.xlabel('length of Data')
plt.ylabel('number of Data')
plt.show()

각 데이터의 길이는 다르며, 대체적으로 길이가 100~200대가 많은 것을 알 수 있다. X_train에 들어있는 숫자들이 각자 어떤 단어들을 나타내고 있는지 확인해본다.

word_index = reuters.get_word_index()
print(word_index)
{'mdbl': 10996, 'fawc': 16260, 'degussa': 12089, 'woods': 8803, 'hanging': 13796, 'localized': 20672, 'sation': 20673, 'chanthaburi': 20675, 'refunding': 10997, 'hermann': 8804, 'passsengers': 20676,
...
'jung': 30978, "may's": 10065, 'rotting': 16258, 'pods': 10995, 'emery': 2849, 'northerly': 30979, 'onomichi': 16259}

reuters.get_word_index는 각 단어와 그 단어에 부여된 인덱스를 리턴한다. 이를 출력해보면, 위와 같은 결과가 나온다.

무수히 많은 단어가 등장하기 때문에 출력 결과는 중간에 생략했다. 어떤 단어에 어떤 인덱스가 부여되었는지를 알 수는 있는데 이렇게 보는 것은 매우 불편하다. 좀 더 쉽게 확인하기 위해서 숫자로부터 단어를 바로 알 수 있도록 해보겠다.

index_to_word = {}
for key, value in word_index.items():
index_to_word[value] = key
print(index_to_word){10996: 'mdbl', 16260: 'fawc', 12089: 'degussa', 8803: 'woods', 13796: 'hanging', 20672: 'localized', 20673: 'sation', 20675: 'chanthaburi', 10997: 'refunding', 8804: 'hermann', 20676: 'passsengers',
...
30978: 'jung', 10065: "may's", 16258: 'rotting', 10995: 'pods', 2849: 'emery', 30979: 'northerly', 16259: 'onomichi'}

이제 index_to_word[]에다가 인덱스를 입력하면 단어를 확인할 수 있다.

print(index_to_word[28842])nondiscriminatory

28,842란 인덱스를 가진단어는 nondiscriminatory임을 확인할 수 있다. 잘 쓰이지 않는 단어라서 이 데이터에서는 등장 빈도 순위로 따지면 28,842등이라는 뜻이다.

print(index_to_word[1])the

반대로 제일 많이 등장하는 단어는 the임을 알 수 있다.

보통 불용어로 분류되는 the가 이 데이터에서도 어김없이 등장 빈도수로 1위를 차지했다.

print(' '.join([index_to_word[X] for X in X_train[0]]))

이를 이용해서 X_train[0]가 어떤 단어들로 구성되어있는지를 확인해보자.

the wattie nondiscriminatory mln loss for plc said at only ended said commonwealth could 1 traders now april 0 a after said from 1985 and from foreign 000 april 0 prices its account year a but in this mln home an states earlier and rise and revs vs 000 its 16 vs 000 a but 3 psbr oils several and shareholders and dividend vs 000 its all 4 vs 000 1 mln agreed largely april 0 are 2 states will billion total and against 000 pct dlrs

지금까지 로이터 뉴스 데이터가 어떤 구성을 갖고 있는지에 대해서 알아봤다. 이제 해당 데이터를 가지고 텍스트 분류를 수행해보자.

2. LSTM으로 로이터 뉴스 분류하기

텍스트 분류를 LSTM을 통해서 수행해본다.

from keras.datasets import reuters
from keras.models import Sequential
from keras.layers import Dense, LSTM, Embedding
from keras.preprocessing import sequence
from keras.utils import np_utils
# 코드 수행을 위해 필요한 패키지를 가져옴(X_train, y_train), (X_test, y_test) = reuters.load_data(num_words=1000, test_split=0.2)# 우선 학습에서는 등장 빈도가 1,000번째까지의 단어들만 사용하도록 한다.X_train = sequence.pad_sequences(X_train, maxlen=100)
X_test = sequence.pad_sequences(X_test, maxlen=100)

각 훈련 데이터는 길이가 서로 다르다. 각 기사는 갖고 있는 단어의 수가 제각각이다. 모델의 입력으로 사용하려면 모든 훈련 데이터의 길이를 동일하게 맞춘다. pad_sequences()를 사용하여 maxlen의 값으로 100을 줬는데, 이는 모든 훈련 데이터의 길이. 즉, 단어 수를 100으로 맞춘다는 뜻이다.

훈련 데이터에는 분명히 단어의 수가 100개가 넘는 경우도 있을 것이고, 100개가 안되는 경우도 있을 것이다. 그렇기 때문에 단어의 개수가 100개보다 많으면 100개만 선택하고 나머지는 제거하며, 100개보다 부족한 경우에는 부족한 부분을 0으로 처리한다.

y_train = np_utils.to_categorical(y_train)
y_test = np_utils.to_categorical(y_test)

이제 y의 훈련 데이터와 테스트 데이터에 원-핫 인코딩을 수행한다.

model = Sequential()
model.add(Embedding(1000, 120))
model.add(LSTM(120))
model.add(Dense(46, activation='softmax'))

이제 LSTM 모델을 만들어보자. 우선 Embedding()을 사용하여 임베딩 층(Embedding layer)을 만들어야 하는데, Embedding()은 최소 두 개의 인자를 받는다. 첫번째 인자는 단어 집합의 크기이며, 두번째 인자는 임베딩 차원이 된다. 결과적으로 Embedding()은 120차원을 가지는 임베딩 벡터를 1,000개 생성하는 역할을 한다.

이제 120의 차원을 가지는 임베딩 벡터들을 LSTM 모델에다가 넣는다. LSTM의 인자로는 메모리 셀의 은닉 상태의 크기(hidden_size)를 입력한다.

46개의 카테고리를 분류해야 하므로, 출력층에서는 46개의 뉴런을 사용한다. 또한 출력층의 활성화 함수로 소프트맥스 함수를 사용한다. 소프트맥스 함수는 각 입력에 대해서 46개의 확률 분포를 만들어낸다.

model.compile(loss='categorical_crossentropy', optimizer='adam',
metrics=['accuracy'])

모델을 컴파일한다. 이 경우 다중 클래스 분류(Multi-Class Classification) 문제이므로 손실 함수는 categorical_crossentropy를 사용한다. categorical_crossentropy는 모델의 예측값과 실제값에 대해서 두 확률 분포 사이의 거리를 최소화하도록 훈련한다.

history = model.fit(X_train, y_train, batch_size=100,
epochs=20, validation_data=(X_test, y_test))

모델을 실행한다. validation_data로 X_test와 y_test를 사용한다. val_loss가 줄어들다가 증가하는 상황이 오면 과적합(Overfitting)으로 판단하기 위함이다.

Epoch 1/20
8982/8982 [==============================] - 14s 2ms/step - loss: 2.5879 - acc: 0.3458 - val_loss: 2.2982 - val_acc: 0.3838
Epoch 2/20
8982/8982 [==============================] - 13s 1ms/step - loss: 2.0912 - acc: 0.4791 - val_loss: 2.0017 - val_acc: 0.5071
...
Epoch 19/20
8982/8982 [==============================] - 13s 1ms/step - loss: 0.7500 - acc: 0.8075 - val_loss: 1.2094 - val_acc: 0.7053
Epoch 20/20
8982/8982 [==============================] - 13s 1ms/step - loss: 0.7380 - acc: 0.8075 - val_loss: 1.1968 - val_acc: 0.7102

테스트 데이터에 대한 정확도를 출력한다.

print('\n 테스트 정확도 : %.4f' % (model.evaluate(X_test, y_test)[1]))
2246/2246 [==============================] - 1s 533us/step

테스트 정확도 : 0.7102

70%의 정확도를 얻었다. X_test와 y_test를 검증 데이터로 사용하고, 테스트 데이터로 재사용하는 것을 의아하다고 느낄 수 있다. 앞서 훈련 데이터, 검증 데이터, 테스트 데이터는 서로 다른 데이터를 사용해야한다고 언급하였기 때문이다.

이는 model.fit()의 특성과 관계가 있다. model.fit()에서 validation_data는 실제 훈련에는 반영되지 않과 과적합을 판단하기 위한 용도로만 사용된다. 즉, validation_data는 기계 학습에 반영하지 않고 오직 정확도의 결과로만 보여준다. 그러므로 validation_data에 X_test, y_test를 사용하더라도 기계는 아직 이 데이터로 학습한 적이 없는 상태다. 그러므로 테스트 정확도를 확인하는 용도로 사용해도 무방하다.

사실 validation_data에 테스트 데이터인 X_test와 y_test를 사용한 경우에는 결국 훈련 과정에서의 마지막 에포크에서의 val_acc가 테스트 정확도와 동일함을 알 수 있다. 즉, 굳이 model.evaluate()를 사용해서 테스트 정확도를 측정할 필요가 없다. 이는 앞서 스팸 메일 분류하기 실습에서 validation_data 대신 validation_split=0.2를 사용하여 검증 데이터와 테스트 데이터를 다른 것을 사용했을 때와는 다른 결과이다.

epochs = range(1, len(history.history['acc'])+1)
plt.plot(epochs, history.history['loss'])
plt.plot(epochs, history.history['val_loss'])
plt.title('model loss')
plt.ylabel('loss')
plt.xlabel('epoch')
plt.legend(['train', 'test'], loc='upper left')
plt.show()

이번 실습에서는 훈련 데이터와 테스트 데이터에 대해서 같이 정확도를 확인하면서 훈련 데이터에 대해서 훈련하였으므로, 이를 비교하여 그래프로 시각화해본다.

아직은 테스트 데이터의 오차가 줄어들고 있는 것을 확인할 수 있다. 하지만 테스트 데이터와 훈련 데이터의 오차의 차이가 벌어지고 있는 것은 과적합의 신호일 수 있으므로 주의해야 한다.

--

--

정민수
정민수

No responses yet