반응형


본 포스팅에서는 케라스(tf.keras)를 사용하여 텍스트로 작성된 리뷰를 긍정적 또는 부정적 영화 리뷰로 분류합니다.

클래스가 두 개인 바이너리 분류(binary classification) 입니다.



본 포스팅은 다음 텐서플로우 문서를 참고하여 작성하였습니다.


Text classification with movie reviews

https://www.tensorflow.org/tutorials/keras/basic_text_classification



텐서플로우 1.12를 기준으로 설명합니다.


import tensorflow as tf

print(tf.__version__)


1.12.0



최초작성 2019. 1. 17



IMDB  데이터셋 다운로드

Internet Movie Database(https://www.imdb.com/)에서 가져온 50,000개의 영화 리뷰를 포함하고 있는 IMDB 데이터 셋을 사용합니다.


텐서플로우를 사용하여 다음처럼 IMDB 데이터셋을 가져올 수 있습니다.

아규먼트 num_words=10000은 데이터셋에 있는 리뷰에서 많이 나타나는 상위 10,000개의 단어만 사용하겠다는 의미입니다.


imdb = keras.datasets.imdb

(train_data, train_labels), (test_data, test_labels) = imdb.load_data(num_words=10000)



데이터셋을 둘로 나누어 25,000개의 리뷰는 학습을 위해 사용하고 25,000개는 학습된 결과를 검증하기 위해 사용합니다.

각각 훈련 데이터셋(train_data, train_labels), 테스트 데이터셋(test_data, test_labels)이라고 부릅니다.

훈련 데이터셋과 테스트 데이터셋의 크기가 같은 것은 동일한 개수의 긍정적인 리뷰와 부정적 리뷰를 포함하고 있기 때문입니다.


print("Train datas: {}, labels: {}".format(len(train_data), len(train_labels)))
print("Test datas: {}, labels: {}".format(len(test_data), len(test_labels)))


Train datas: 25000, labels: 25000

Test datas: 25000, labels: 25000



입력으로 사용되는 train_data와 test_data는 영화 리뷰 텍스트에 있는 단어를 정수로 변환한 데이터입니다.

각 정수는 사전에서 등록되어 있는 특정 단어를 나타냅니다.

train_data의 첫번째 데이터를 출력해보면 다음처럼 정수를 포함한 배열이 출력됩니다.


print(train_data[0])


[1, 14, 22, 16, 43, 530, 973, 1622, 1385, 65, 458, 4468, 66, 3941, 4, 173, 36, 256, 5, 25, 100, 43, 838, 112, 50, 670, 2, 9, 35, 480, 284, 5, 150, 4, 172, 112, 167, 2, 336, 385, 39, 4, 172, 4536, 1111, 17, 546, 38, 13, 447, 4, 192, 50, 16, 6, 147, 2025, 19, 14, 22, 4, 1920, 4613, 469, 4, 22, 71, 87, 12, 16, 43, 530, 38, 76, 15, 13, 1247, 4, 22, 17, 515, 17, 12, 16, 626, 18, 2, 5, 62, 386, 12, 8, 316, 8, 106, 5, 4, 2223, 5244, 16, 480, 66, 3785, 33, 4, 130, 12, 16, 38, 619, 5, 25, 124, 51, 36, 135, 48, 25, 1415, 33, 6, 22, 12, 215, 28, 77, 52, 5, 14, 407, 16, 82, 2, 8, 4, 107, 117, 5952, 15, 256, 4, 2, 7, 3766, 5, 723, 36, 71, 43, 530, 476, 26, 400, 317, 46, 7, 4, 2, 1029, 13, 104, 88, 4, 381, 15, 297, 98, 32, 2071, 56, 26, 141, 6, 194, 7486, 18, 4, 226, 22, 21, 134, 476, 26, 480, 5, 144, 30, 5535, 18, 51, 36, 28, 224, 92, 25, 104, 4, 226, 65, 16, 38, 1334, 88, 12, 16, 283, 5, 16, 4472, 113, 103, 32, 15, 16, 5345, 19, 178, 32]




다음 코드를 사용하여  train_data의 첫번째 리뷰에 있는 정수들을 단어로 변환해 볼 수 있습니다.


from tensorflow import keras


imdb = keras.datasets.imdb

(train_data, train_labels), (test_data, test_labels) = imdb.load_data(num_words=10000)


# A dictionary mapping words to an integer index
word_index = imdb.get_word_index()

# The first indices are reserved
word_index = {k:(v+3) for k,v in word_index.items()}
word_index["<PAD>"] = 0
word_index["<START>"] = 1
word_index["<UNK>"] = 2  # unknown
word_index["<UNUSED>"] = 3

reverse_word_index = dict([(value, key) for (key, value) in word_index.items()])

def decode_review(text):
   return ' '.join([reverse_word_index.get(i, '?') for i in text])

print(decode_review(train_data[0]))


<START> this film was just brilliant casting location scenery story direction everyone's really suited the part they played and you could just imagine being there robert <UNK> is an amazing actor and now the same being director <UNK> father came from the same scottish island as myself so i loved the fact there was a real connection with this film the witty remarks throughout the film were great it was just brilliant so much that i bought the film as soon as it was released for <UNK> and would recommend it to everyone to watch and the fly fishing was amazing really cried at the end it was so sad and you know what they say if you cry at a film it must have been good and this definitely was also <UNK> to the two little boy's that played the <UNK> of norman and paul they were just brilliant children are often left out of the <UNK> list i think because the stars that play them all grown up are such a big profile for the whole film but these children are amazing and should be praised for what they have done don't you think the whole story was so lovely because it was true and was someone's life after all that was shared with us all




라벨로 사용되는 train_labels와 test_labels은 입력을 부정적인 리뷰와 부정적인 리뷰로 분류해 놓은 것입니다.

0 또는 1의 값을 갖습니다. 0은 부정적인 리뷰, 1은 긍정적인 리뷰를 의미합니다.




데이터셋에 있는 리뷰 텍스트의 길이(단어 개수)는 모두 다릅니다.  다음은 첫번째 리뷰와 두번째 리뷰의 길이를 출력해본 결과입니다.

뉴럴 네트워크의 입력으로 사용하려면  모두 같은 길이가 되도록 조정해줘야 합니다.


print(len(train_data[0]), len(train_data[1]))


(218, 189)




데이터 준비하기


pad_sequences 함수를 사용하여 모든 배열의 크기가  256으로 같아지도록 값이 0인 원소를 적절히 추가해줍니다.


train_data = keras.preprocessing.sequence.pad_sequences(train_data,
                                                       value=word_index["<PAD>"],
                                                       padding='post',
                                                       maxlen=256)

test_data = keras.preprocessing.sequence.pad_sequences(test_data,
                                                      value=word_index["<PAD>"],
                                                      padding='post',
                                                      maxlen=256)



이제 모든 리뷰 텍스트 길이가 256이 됩니다.


print(len(train_data[0]), len(train_data[1]))


256 256



출력해보면 배열 크기를 맞추기위해 0을 채운 것을 볼 수 있습니다.


print(train_data[0])


[   1 14   22 16 43  530 973 1622 1385   65 458 4468 66 3941

   4 173   36 256  5 25 100   43 838 112 50  670 2 9

  35 480  284 5 150    4 172 112 167   2 336 385 39 4

 172 4536 1111   17 546 38 13  447 4 192 50  16 6 147

2025   19 14  22 4 1920 4613  469 4 22 71  87 12 16

  43 530   38 76 15   13 1247 4 22   17 515 17 12 16

 626   18 2    5 62 386   12 8 316  8 106 5 4 2223

5244   16 480  66 3785 33    4 130 12 16   38 619 5 25

 124   51 36  135 48  25 1415 33    6 22 12 215   28 77

  52   5 14  407 16   82 2 8    4 107 117 5952   15 256

   4 2    7 3766 5  723 36 71   43 530 476 26  400 317

  46   7 4    2 1029 13  104 88 4  381 15 297 98   32

2071   56 26 141    6 194 7486 18    4 226 22 21 134  476

  26 480    5 144 30 5535   18 51 36 28 224   92 25 104

   4 226   65 16 38 1334   88 12 16 283 5   16 4472 113

 103   32 15   16 5345 19  178 32 0   0 0 0 0   0

   0 0    0 0 0    0 0 0 0    0 0 0 0 0

   0 0    0 0 0    0 0 0 0    0 0 0 0 0

   0 0    0 0]




..


..

모델 생성하기

뉴럴 네트워크를 생성하기 위해 레이어를 쌓으려면 몇개의 레이어를 사용할지, 각 레이어를 위해 몇개의 뉴런을 사용할지를 결정해야 합니다.


다음처럼 4개의 레이어로 구성되는 모델을 생성합니다.


# input shape is the vocabulary count used for the movie reviews (10,000 words)
vocab_size = 10000

model = keras.Sequential()
model.add(keras.layers.Embedding(vocab_size, 16))
model.add(keras.layers.GlobalAveragePooling1D())
model.add(keras.layers.Dense(16, activation=tf.nn.relu))
model.add(keras.layers.Dense(1, activation=tf.nn.sigmoid))

model.summary()



model.add(keras.layers.Embedding(vocab_size, 16))

첫번째 레이어는 임베딩(Embedding) 레이어입니다.

첫번째 아규먼트인 vocab_size는 사전의 크기 10,000이며(앞에서 상위 단어 10,000개만 사용하도록 했습니다.)  두번째 아규먼트인 16은 생성되는 임베딩 벡터의 크기입니다.

10,000개 의 단어에 대한 크기 16의 임베딩 벡터를 10,000개 만듭니다.


차원이 추가되어 넘파이 배열의 shape는 (25000, 256, 16) 이 됩니다.

25,000개의 리뷰 텍스트가 있으며, 각 리뷰는 256개의 단어로 구성되고, 각 단어는 16개의 원소로 구성된 벡터로 표현됩니다.



layer 0 output (25000, 256, 16)

# 첫번째 리뷰의 임베딩 결과를 출력해보면 크기 16인 256개의 임베딩 벡터가 출력됩니다. 똑같은 단어는 같은 임베딩 벡터입니다.

[[-4.51784134e-02  8.43267515e-03 2.41203420e-02  4.95403446e-02

  2.92846598e-02 -1.98755395e-02  5.22672012e-03 3.98325063e-02

 -4.06740978e-03  1.85608603e-02 2.21599676e-02  4.39548381e-02

  3.50121409e-03  3.62660550e-02 -2.22286936e-02  4.82255332e-02]

               . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

  3.30433361e-02 -3.33103910e-02  1.49245970e-02 3.34262848e-03]

[-4.08503413e-02 -1.63457543e-03 -2.30084546e-02 -1.88462492e-02

  1.23437643e-02  1.17091164e-02 2.51974910e-03 -2.21333392e-02

 -3.31593677e-03  3.02489139e-02 2.21424177e-03 -4.39639576e-02

  3.30433361e-02 -3.33103910e-02  1.49245970e-02 3.34262848e-03]]




model.add(keras.layers.GlobalAveragePooling1D())

GlobalAveragePooling1D 레이어는 고정된 크기(여기에선 16)의 출력 벡터를 리턴합니다.

입력으로 shape가 (25000, 256, 16)인 배열이 사용됩니다. 두번째 차원(리뷰당 단어개수 256개) 방향으로 평균을 구하여 shape가 (25000, 16)인 배열을 생성합니다.  

입력으로 사용되는 리뷰에 포함된 단어 개수가 변경되더라도 같은 크기의 벡터로 처리할 수 있게 됩니다.



layer 1 output (25000, 16)

[-0.00634271  0.00400057 -0.00282727  0.00280815 0.00067278 0.00357414

-0.00044041 -0.005514    0.00050653 0.00501671 -0.00345321 -0.01061774

 0.00476874  0.00029571 0.00374537  0.00056968]




model.add(keras.layers.Dense(16, activation=tf.nn.relu))

뉴런이 16개인 fully-connected layer(Dense)에 입력 벡터를 통과시킵니다.

크기 16인 벡터가 출력됩니다.



layer 2 output (25000, 16)

[0.00242995 0.         0.0011177 0.00598094 0.00098548 0.00424779

0.         0. 0.         0.00465069 0.00174703 0.

0.         0. 0.         0. ]




model.add(keras.layers.Dense(1, activation=tf.nn.sigmoid))

마지막 층은 하나의 노드로 구성된 출력 레이어에  fully-connected하게 연결됩니다.


본 포스팅의 예제는 바이너리 분류이기 때문에 모델의 출력은 확률입니다. 그래서 마지막 레이어는 하나의 노드를 가지며 시그모이드 함수를 활성화 함수로 사용합니다.

시그모이드 함수는 출력이 0 ~ 1사이의 실수값을 갖도록 합니다.



layer 3 output (25000, 1)

[0.5010359]





손실 함수와 옵티마이저 설정

모델을 학습시키려면 손실 함수(Loss function)와 옵티마이저(optimizer)가 필요합니다.


옵티마이저로는 Adam 알고리즘을 구현한 AdamOptimizer를 사용합니다.

경사 하강법을 사용하는 GradientDescentOptimizer보다 성능이 좋다고 합니다.


손실함수로  binary_crossentropy를 사용합니다. binary_crossentropy는 확률 분포간의 거리를 측정합니다.

여기에선 예측된 분포와 정답(=라벨) 분포간의 거리를 측정합니다.


모델의 성능을 평가하기 위해 전체 데이터셋에서 올바르게 분류된 이미지 비율을 표시하는 정확도(accuracy)를 사용합니다.


model.compile(optimizer=tf.train.AdamOptimizer(),
             loss='binary_crossentropy',
             metrics=['accuracy'])




검증 데이터셋 생성

모델이 학습되는 동안 모델의 정확성을 체크하기 위해 테스트 데이터셋 대신에 검증 데이터셋을 사용합니다.

(학습되는 동안에는 훈련 데이터셋만 사용하고 나중에 모델의 정확도를 평가할때에만 테스트 셋을 사용합니다. )


다음 코드처럼 훈련 데이터셋을 둘로 나누어 일부를 검증 데이터셋(x_val, y_val )으로 사용하도록 합니다.

훈련 데이터 셋으로는 15,000개, 검증 데이터 셋으로는 10,000개를 사용하게 됩니다.  


x_val = train_data[:10000]
partial_x_train = train_data[10000:]

y_val = train_labels[:10000]
partial_y_train = train_labels[10000:]




모델 학습 시키기

훈련 데이터셋(partial_x_train, partial_y_train)을 512개의 작은 미니배치(mini-batches)로 분할하여 입력으로 사용합니다.

512개씩 나누어 전체 훈련 데이터셋을 모델 학습에 사용하는 것을 40번 반복하도록 합니다.

모델이 학습되는 동안 모델의 손실(loss)과 정확도(acc)를 계산하기 위해 검증 데이터셋(x_val, y_val)을 사용합니다


history = model.fit(partial_x_train,
                   partial_y_train,
                   epochs=40,
                   batch_size=512,
                   validation_data=(x_val, y_val),
                   verbose=1)



학습이 되는 동안 손실(loss)과 정확도(acc)가 출력됩니다.



Epoch 1/40


 512/15000 [>.............................] - ETA: 15s - loss: 0.6932 - acc: 0.4922

1536/15000 [==>...........................] - ETA: 5s - loss: 0.6931 - acc: 0.5065

2560/15000 [====>.........................] - ETA: 3s - loss: 0.6930 - acc: 0.5145

3584/15000 [======>.......................] - ETA: 2s - loss: 0.6929 - acc: 0.5215

4608/15000 [========>.....................] - ETA: 1s - loss: 0.6928 - acc: 0.5193

5632/15000 [==========>...................] - ETA: 1s - loss: 0.6927 - acc: 0.5126

6656/15000 [============>.................] - ETA: 1s - loss: 0.6926 - acc: 0.5110

7680/15000 [==============>...............] - ETA: 0s - loss: 0.6924 - acc: 0.5117

8704/15000 [================>.............] - ETA: 0s - loss: 0.6923 - acc: 0.5124

9728/15000 [==================>...........] - ETA: 0s - loss: 0.6921 - acc: 0.5126

10752/15000 [====================>.........] - ETA: 0s - loss: 0.6920 - acc: 0.5149

11776/15000 [======================>.......] - ETA: 0s - loss: 0.6919 - acc: 0.5145

12800/15000 [========================>.....] - ETA: 0s - loss: 0.6916 - acc: 0.5192

13824/15000 [==========================>...] - ETA: 0s - loss: 0.6915 - acc: 0.5197

14848/15000 [============================>.] - ETA: 0s - loss: 0.6913 - acc: 0.5203

15000/15000 [==============================] - 2s 108us/step - loss: 0.6913 - acc: 0.5213 - val_loss: 0.6888 -

val_acc: 0.5731

Epoch 2/40


15000/15000 [==============================] - 1s 66us/step - loss: 0.6835 - acc: 0.6941 - val_loss: 0.6780 -

val_acc: 0.7441

Epoch 3/40


15000/15000 [==============================] - 1s 67us/step - loss: 0.6672 - acc: 0.7605 - val_loss: 0.6585 - val_acc: 0.7512

Epoch 4/40


….. ….. ….. ….. ….. ….. ….. ….. ….. ….. ….. ….. ….. ….. ….. ….. ….. ….. ….. ….. ….. ….. ….. ….. ….. ….. ….. ….. …..



Epoch 39/40


15000/15000 [==============================] - 1s 63us/step - loss: 0.0938 - acc: 0.9757 - val_loss: 0.3087 - val_acc: 0.8834

Epoch 40/40


15000/15000 [==============================] - 1s 66us/step - loss: 0.0900 - acc: 0.9771 - val_loss: 0.3117 -

val_acc: 0.8826





모델 성능 평가하기

테스트 셋으로 평가한 결과 정확도는  0.87입니다.


results = model.evaluate(test_data, test_labels)

print(results)



  32/25000 [..............................] - ETA: 1s

1216/25000 [>.............................] - ETA: 1s

2112/25000 [=>............................] - ETA: 1s

………………………………………………………………………...

23104/25000 [==========================>...] - ETA: 0s

23840/25000 [===========================>..] - ETA: 0s

24864/25000 [============================>.] - ETA: 0s

25000/25000 [==============================] - 1s 51us/step

[0.3225130436515808, 0.87356]





정확도와 손실 그래프 그리기

model.fit() 메소드는 학습되는 동안 측정된 정확도, 손실등을 리턴합니다.


history = model.fit(partial_x_train,
                   partial_y_train,
                   epochs=40,
                   batch_size=512,
                   validation_data=(x_val, y_val),
                   verbose=1)



4개의 엔트리로 구성됩니다.  각각 학습 정확도, 검증 정확도, 학습 손실, 검증 손실을 의미합니다.


history_dict = history.history
history_dict.keys()


dict_keys(['acc', 'val_acc', 'loss', 'val_loss'])



다음 코드를 추가하여 정확도와 손실을 그래프로 그려볼 수 있습니다.


import matplotlib.pyplot as plt

history_dict = history.history

history_dict.keys()


acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']

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

# "bo" is for "blue dot"
plt.plot(epochs, loss, 'bo', label='Training loss')
# b is for "solid blue line"
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()

plt.show()


plt.clf()   # clear figure
acc_values = history_dict['acc']
val_acc_values = history_dict['val_acc']

plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.legend()

plt.show()




그래프에서 점선은 학습 손실과 학습 정확도,  실선은 검증 손실과 검증 정확도를 나타냅니다.

반복 횟수가 증가함에 따라 학습 손실은 감소하고 정확도는 상승합니다.


검증 손실과 정확도의 경우에는 상황이 다릅니다. 20번 반복한 후 부터는 변화가 없습니다.

학습시 사용하는 데이터셋을 입력으로 사용했을때보다  검증에 사용하는 데이터셋을 입력으로 사용했을때 더 결과가 안좋은 것을 오버피팅(overfitting)이라고 합니다.   

학습용 데이터셋에 최적화되어 있어서 새로운 데이터셋을 입력으로 사용시 모델의 성능이 떨어지는 겁니다.




예측하기

학습된 모델에 테스트 데이터셋을 입력으로 사용하여 예측을 해봅니다.


predictions = model.predict(test_data)
print(predictions[0:10].T)

predict_class=model.predict_classes(test_data)
print(predict_class[0:10].T)



예측된 값 중 10개만 출력해보았습니다. 첫번째 줄은 뉴럴 네트워크의 출력이며 두번째 줄은 어느 클래스에 속하는지 판정한 결과입니다.  


[[0.09011856 0.99828243 0.7261485  0.7149344 0.99290705 0.92161864 0.9481936  0.02148814 0.96879655 0.9984517 ]]

[[0 1 1 1 1 1 1 0 1 1]]





마지막 업데이트 2019. 1. 17




반응형

문제 발생시 지나치지 마시고 댓글 남겨주시면 가능한 빨리 답장드립니다.

도움이 되셨다면 토스아이디로 후원해주세요.
https://toss.me/momo2024


제가 쓴 책도 한번 검토해보세요 ^^

+ Recent posts