딥러닝으로 뉴스기사의 토픽을 분류해보자.

다중 분류 문제 : Multiple class classification

이전 포스팅에서 다룬 이진 분류 모델의 경우 예측할 수 있는 결과값이 긍정/부정 두가지 였습니다.
하지만 뉴스의 토픽(예를들어 시사/인터넷/연예/스포츠 등)을 예측하기 위해선 이진 분류로는 토픽을 나눌 수 없습니다.

이번 포스팅에서는 2개 이상의 클래스를 가진 경우 사용할 수 있는 다중 분류 문제를 해결해보도록 하겠습니다.

Dataset

저희는 로이터 데이터셋을 사용하도록 합니다.
해당 데이터셋은 46개의 토픽을 갖고 있으며 각 뉴스기사마다 하나의 토픽이 정해져있습니다.
즉, 단일 레이블 다중분류 문제로 볼 수 있습니다.

Download

1
2
from tensorflow.keras.datasets import reuters
(train_data, train_labels), (test_data, test_labels) = reuters.load_data(num_words=10000)

Dataset shape 확인

1
2
print("Train Datas Shape : {}".format(train_data.shape))
print("Train Labels Shape : {}".format(train_labels.shape))
Train Datas Shape : (8982,)
Train Labels Shape : (8982,)

데이터 확인

확인한대로 8982개의 훈련 데이터셋이 존재하며,
각 인덱스는 단어 인덱스의 리스트를 뜻 합니다.

1
2
3
4
display(train_data[0][:10])
display(train_labels)

display(test_labels)
[1, 2, 2, 8, 43, 10, 447, 5, 25, 207]



array([ 3,  4,  3, ..., 25,  3, 25])



array([ 3, 10,  1, ...,  3,  3, 24])

단어를 텍스트로 디코딩

1
2
3
4
word_index = reuters.get_word_index()
reverse_word_index = dict([(val, key) for (key, val) in word_index.items()])
decoded_news = ' '.join([reverse_word_index.get(i - 3, '?') for i in train_data[0]])
print(decoded_news)
? ? ? said as a result of its december acquisition of space co it expects earnings per share in 1987 of 1 15 to 1 30 dlrs per share up from 70 cts in 1986 the company said pretax net should rise to nine to 10 mln dlrs from six mln dlrs in 1986 and rental operation revenues to 19 to 22 mln dlrs from 12 5 mln dlrs it said cash flow per share this year should be 2 50 to three dlrs reuter 3

라벨 확인

샘플의 라벨은 토픽의 인덱스로써 0~45의 값을 가집니다.

1
train_labels[0]
3

데이터 변환

학습에 용이하도록 뉴스 기사와 라벨 데이터를 벡터로 변환시킵니다.
학습에 사용되는 데이터셋의 인풋 데이터는 해당 뉴스 기사의 들어가있는 단어 인덱스를 1.0 으로 변경시킵니다.

1
2
3
4
5
6
7
import numpy as np

def vectorize_sequences(sequences, dimension=10000):
results = np.zeros((len(sequences), dimension)) # 크기가 들어온 리스트 (단어개수, 전체단어개수)이고, 모든 원소가 0인 행렬을 생성
for i, sequence in enumerate(sequences):
results[i, sequence] = 1.
return results
1
2
3
4
5
6
7
8
9
10
11
12
13
from tensorflow.keras.utils import to_categorical

x_train = vectorize_sequences(train_data)
x_test = vectorize_sequences(test_data)

display(x_train.shape)
display(x_test.shape)

one_hot_train_labels = to_categorical(train_labels)
one_hot_test_labels = to_categorical(test_labels)

display(one_hot_train_labels.shape)
display(one_hot_test_labels.shape)
(8982, 10000)



(2246, 10000)



(8982, 46)



(2246, 46)

신경망 구성

단일 레이블 다중 분류를 위한 모델을 작성 해보도록 하겠습니다.
다만 레이어 구축 시 참고해야 할 부분이 있는데,
각 레이어를 통과 할 때 유닛의 수가 레이블 보다 적다면 가지고 있어야 할 정보가 많이 사라질 수 있습니다.

학습 시 파라미터를 줄이기 위해서나 노이즈를 줄이기 위해서 유닛을 줄이는 경우도 있지만,
데이터가 가지고 있어야 할 필수 데이터를 잃어버릴 수 있다는 점도 참고하여 네트워크를 구성해야 합니다.

신경망 네트워크 구축

로이터 데이터셋의 단어수를 10,000개로 제한해두었으니,
신경망의 입력 차원수도 10,000으로 설정합니다.

1
2
3
4
5
6
7
8
9
from tensorflow.keras import models
from tensorflow.keras import layers

model = models.Sequential()
model.add(layers.Dense(64, activation='relu', input_shape=(10000, )))
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dense(46, activation='softmax'))

model.summary()
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_3 (Dense)              (None, 64)                640064    
_________________________________________________________________
dense_4 (Dense)              (None, 64)                4160      
_________________________________________________________________
dense_5 (Dense)              (None, 46)                2990      
=================================================================
Total params: 647,214
Trainable params: 647,214
Non-trainable params: 0
_________________________________________________________________

이진분류와 비슷하게 마지막 Dense 레이어의 아웃풋 벡터 개수는(46개) 예측하기 위한 클래스의 개수와 동일합니다.
다른점은 이진 분류에서는 활성함수로 sigmoid를 사용한 반면 여기서는 softmax를 이용했는데,
softmax는 각 클래스별로 해당 클래스 일 확률을 표시하도록 만들어집니다.

모델 컴파일

다중 분류에서 손실함수로써는 categorical_crossentropy를 주로 사용합니다.
옵티마이저는 가장 빠르고 효과가 좋다고 알려진 adam 옵티마이저를 사용하도록 설정합니다.

1
2
3
4
5
model.compile(
optimizer='adam',
loss='categorical_crossentropy',
metrics=['accuracy'],
)

검증 데이터셋 구성

훈련용 데이터셋에서 Validation으로 사용할 데이터셋을 분리시킵니다.

1
2
3
4
5
x_val = x_train[:1000]
partial_x_train = x_train[1000:]

y_val = one_hot_train_labels[:1000]
partial_y_train = one_hot_train_labels[1000:]

모델 학습

1
2
3
4
5
6
7
history = model.fit(
partial_x_train,
partial_y_train,
epochs=20,
batch_size=512,
validation_data=(x_val, y_val)
)
Train on 7982 samples, validate on 1000 samples
Epoch 1/20
7982/7982 [==============================] - 1s 152us/step - loss: 3.3032 - acc: 0.4328 - val_loss: 2.6156 - val_acc: 0.5320
Epoch 2/20
7982/7982 [==============================] - 1s 78us/step - loss: 2.0615 - acc: 0.6076 - val_loss: 1.6695 - val_acc: 0.6460
Epoch 3/20
7982/7982 [==============================] - 1s 78us/step - loss: 1.3844 - acc: 0.7051 - val_loss: 1.3219 - val_acc: 0.7100
Epoch 4/20
7982/7982 [==============================] - 1s 77us/step - loss: 1.0796 - acc: 0.7669 - val_loss: 1.1773 - val_acc: 0.7520
Epoch 5/20
7982/7982 [==============================] - 1s 79us/step - loss: 0.8673 - acc: 0.8132 - val_loss: 1.0768 - val_acc: 0.7790
Epoch 6/20
7982/7982 [==============================] - 1s 78us/step - loss: 0.6909 - acc: 0.8515 - val_loss: 0.9995 - val_acc: 0.7910
Epoch 7/20
7982/7982 [==============================] - 1s 78us/step - loss: 0.5410 - acc: 0.8880 - val_loss: 0.9467 - val_acc: 0.8010
Epoch 8/20
7982/7982 [==============================] - 1s 78us/step - loss: 0.4177 - acc: 0.9137 - val_loss: 0.9069 - val_acc: 0.8190
Epoch 9/20
7982/7982 [==============================] - 1s 78us/step - loss: 0.3253 - acc: 0.9300 - val_loss: 0.8855 - val_acc: 0.8160
Epoch 10/20
7982/7982 [==============================] - 1s 79us/step - loss: 0.2593 - acc: 0.9432 - val_loss: 0.8973 - val_acc: 0.8090
Epoch 11/20
7982/7982 [==============================] - 1s 80us/step - loss: 0.2123 - acc: 0.9503 - val_loss: 0.8912 - val_acc: 0.8210
Epoch 12/20
7982/7982 [==============================] - 1s 78us/step - loss: 0.1792 - acc: 0.9549 - val_loss: 0.9012 - val_acc: 0.8230
Epoch 13/20
7982/7982 [==============================] - 1s 78us/step - loss: 0.1594 - acc: 0.9565 - val_loss: 0.9194 - val_acc: 0.8170
Epoch 14/20
7982/7982 [==============================] - 1s 78us/step - loss: 0.1410 - acc: 0.9573 - val_loss: 0.9531 - val_acc: 0.8150
Epoch 15/20
7982/7982 [==============================] - 1s 79us/step - loss: 0.1294 - acc: 0.9578 - val_loss: 0.9501 - val_acc: 0.8160
Epoch 16/20
7982/7982 [==============================] - 1s 78us/step - loss: 0.1192 - acc: 0.9603 - val_loss: 0.9757 - val_acc: 0.8130
Epoch 17/20
7982/7982 [==============================] - 1s 77us/step - loss: 0.1146 - acc: 0.9592 - val_loss: 0.9701 - val_acc: 0.8100
Epoch 18/20
7982/7982 [==============================] - 1s 79us/step - loss: 0.1058 - acc: 0.9600 - val_loss: 0.9883 - val_acc: 0.8080
Epoch 19/20
7982/7982 [==============================] - 1s 80us/step - loss: 0.1055 - acc: 0.9624 - val_loss: 1.0214 - val_acc: 0.8130
Epoch 20/20
7982/7982 [==============================] - 1s 81us/step - loss: 0.0979 - acc: 0.9622 - val_loss: 0.9970 - val_acc: 0.8150

훈련의 정확도와 손실 시각화

이전 이진분류 포스팅에서 사용했던 함수를 그대로 끌어와 사용하도록 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import matplotlib.pyplot as plt

def show_graph(history):
history_dict = history.history
accuracy = history_dict['acc']
val_accuracy = history_dict['val_acc']
loss = history_dict['loss']
val_loss = history_dict['val_loss']

epochs = range(1, len(loss) + 1)

plt.figure(figsize=(16, 1))

plt.subplot(121)
plt.subplots_adjust(top=2)
plt.plot(epochs, accuracy, 'ro', label='Training accuracy')
plt.plot(epochs, val_accuracy, 'r', label='Validation accuracy')
plt.title('Trainging and validation accuracy')
plt.xlabel('Epochs')
plt.ylabel('Accuracy and Loss')

plt.legend(loc='upper center', bbox_to_anchor=(0.5, -0.1),
fancybox=True, shadow=True, ncol=5)
# plt.legend(bbox_to_anchor=(1, -0.1))

plt.subplot(122)
plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend(loc='upper center', bbox_to_anchor=(0.5, -0.1),
fancybox=True, shadow=True, ncol=5)
# plt.legend(bbox_to_anchor=(1, 0))

plt.show()
1
show_graph(history)

png

그래프를 보면 대략 8~9번째 에폭부터 과대적합이 시작되는걸 알 수 있습니다.

새로운 데이터 예측해보기

모델을 이용해 각 뉴스기사에 대한 토픽을 예측해보도록 합니다.
softmax를 사용하여 각 뉴스별로 46개의 토픽에 해당하는 확률을 출력합니다.
각 클래스 별 확률을 모두 더하면 1.0(100%)가 됩니다.

1
2
predictions = model.predict(x_test)
display(predictions.shape)
(2246, 46)

predictions는 테스트 데이터셋의 개수에 맞게 2246개의 결과가 들어있습니다.
각 결과안에는 46개 클래스에 해당하는 확률값이 들어가있습니다.

1
np.sum(predictions[0])
1.0

각 46개의 모든 원소의 값을 모두 더하면 1.0(100%)가 된걸 확인할 수 있습니다.

1
2
display(np.argmax(predictions[0]))
display(predictions[0][3])
3



0.96262544

predictions[0]의 3번째 인덱스가 가장 큰 값을 가진걸 확인하였고,
해당 인덱스의 값을 확인하였더니 0.96262544(96.262544%)로 모델이 예측한걸 확인할 수 있습니다.

데이터 레이블 인코딩 방식 변경하여 학습하기

one hot 인코딩이 아닌 정수형으로 토픽을 예측하도록 레이블 인코딩을 사용하도록 변경해봅니다.
손실함수를 변경해주면 되는데,
categorical_crossentropy는 범주형 인코딩일 시 사용하는 손실함수이고,
정수형을 사용할 때에는 sparse_categorical_crossentropy를 사용합니다.

1
2
3
4
5
6
7
8
y_train = np.array(train_labels)
y_test = np.array(test_labels)

x_val = x_train[:1000]
partial_x_train = x_train[1000:]

y_val = y_train[:1000]
partial_y_train = y_train[1000:]
1
2
3
4
5
6
model = models.Sequential()
model.add(layers.Dense(64, activation='relu', input_shape=(10000, )))
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dense(46, activation='softmax'))

model.summary()
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_6 (Dense)              (None, 64)                640064    
_________________________________________________________________
dense_7 (Dense)              (None, 64)                4160      
_________________________________________________________________
dense_8 (Dense)              (None, 46)                2990      
=================================================================
Total params: 647,214
Trainable params: 647,214
Non-trainable params: 0
_________________________________________________________________
1
2
3
4
5
model.compile(
optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['acc']
)
1
2
3
4
5
6
7
history = model.fit(
partial_x_train,
partial_y_train,
epochs=9,
batch_size=512,
validation_data=(x_val, y_val)
)
Train on 7982 samples, validate on 1000 samples
Epoch 1/9
7982/7982 [==============================] - 1s 116us/step - loss: 3.3313 - acc: 0.3983 - val_loss: 2.5661 - val_acc: 0.5770
Epoch 2/9
7982/7982 [==============================] - 1s 78us/step - loss: 2.0006 - acc: 0.6272 - val_loss: 1.6576 - val_acc: 0.6580
Epoch 3/9
7982/7982 [==============================] - 1s 78us/step - loss: 1.3389 - acc: 0.7134 - val_loss: 1.2907 - val_acc: 0.7090
Epoch 4/9
7982/7982 [==============================] - 1s 78us/step - loss: 1.0256 - acc: 0.7762 - val_loss: 1.1484 - val_acc: 0.7630
Epoch 5/9
7982/7982 [==============================] - 1s 80us/step - loss: 0.8077 - acc: 0.8339 - val_loss: 1.0422 - val_acc: 0.7870
Epoch 6/9
7982/7982 [==============================] - 1s 76us/step - loss: 0.6357 - acc: 0.8716 - val_loss: 0.9641 - val_acc: 0.8050
Epoch 7/9
7982/7982 [==============================] - 1s 78us/step - loss: 0.4960 - acc: 0.8990 - val_loss: 0.9275 - val_acc: 0.8050
Epoch 8/9
7982/7982 [==============================] - 1s 78us/step - loss: 0.3909 - acc: 0.9197 - val_loss: 0.8983 - val_acc: 0.8100
Epoch 9/9
7982/7982 [==============================] - 1s 78us/step - loss: 0.3116 - acc: 0.9346 - val_loss: 0.8843 - val_acc: 0.8190
1
show_graph(history)

png

예측하는 방법은 아까와 동일하고,
출력된 레이블 또한 softmax처럼 각 클래스 별 확률이 나오게 됩니다.
9번의 에폭에서 과대적합 되던걸 확인하여 이번 학습은 최대 에폭을 9로 설정하여 학습을 진행한 내용의 그래프입니다.

추가 개선사항

중간 레이어의 유닛수가 너무 작게 되면 데이터에 대한 손실이 발생할 수 있지만,
데이터를 압축하여 노이즈를 줄이는 효과를 얻을 수도 있고, 반대로 유닛수를 크게 한다면,
해당 레이블을 표현하기 위한 데이터를 더 넣을 수 있게 된다고 볼 수도 있습니다.
이러한 내용을 잘 숙지하여 레이어 구성을 변경해가며 테스트를 진행해봅시다.

Powered by Hexo and Hexo-theme-hiker

Copyright © 2019 - 2019 Commit once a day All Rights Reserved.

UV : | PV :