Tensorflow Tutorial 4

 

영화 리뷰를 사용한 텍스트 분류

영화 리뷰를 사용한 텍스트 분류

  • 영화 리뷰들은 영문!
  • 영화 리뷰를 분석하여 긍정 평가와 부정 평가를 분류
  • 이진 분류의 문제
  • 인터넷 영화 데이터베이스에서 수집한 50,000개의 영화 리뷰를 담고있는 IMDB 데이터 세트을 사용
  • 훈련 세트 25,000개, 테스트 세트 25,000개이며 훈련 세트와 데이터 세트의 긍정과 부정 평가의 개수는 동일하다.

중요 모듈 import

import tensorflow as tf
from tensorflow import keras

import numpy as np

print(tf.__version__)
2.7.0

IMDB 데이터 세트 다운로드

  • 리뷰는 미리 전처리하여 정수 시퀀스로 변환되어 있음
  • 각 정수는 어휘 사전에 있는 특정 단어를 의미함
  • num_words=10000은 훈련 데이터에서 가장 많이 등장하는 상위 10,000개의 단어를 선택하도록 함
imdb = keras.datasets.imdb

(train_data, train_labels), (test_data, test_labels) = imdb.load_data(num_words=10000)
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/imdb.npz
17465344/17464789 [==============================] - 0s 0us/step
17473536/17464789 [==============================] - 0s 0us/step

데이터 탐색

  • 데이터 세트의 레이블은 0 또는 1
  • 0은 부정 평가, 1은 긍정 평가
print("훈련 샘플: {}, 레이블: {}".format(len(train_data), len(train_labels)))
훈련 샘플: 25000, 레이블: 25000
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]
  • 영화 리뷰들은 모두 길이가 다름
  • 신경망에서는 입력의 길이가 모두 같아야 하므로 나중에 길이에 대한 문제 처리
len(train_data[0]), len(train_data[1])
(218, 189)

정수를 단어로 다시 변환하기

# 단어와 정수 인덱스를 매핑한 딕셔너리
word_index = imdb.get_word_index()

# 처음 몇 개 인덱스는 사전에 정의되어 있습니다
# index 공간을 추가하여
word_index = {k:(v+3) for k,v in word_index.items()}
# 4개의 특수한 문자열을 추가한다.
word_index["<PAD>"] = 0
word_index["<START>"] = 1
word_index["<UNK>"] = 2  # unknown
word_index["<UNUSED>"] = 3

# 단어가 key, 정수가 값으로 되어 있던 것을 정수가 key, 단어가 값이 되도록 바꾼다.
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])
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/imdb_word_index.json
1646592/1641221 [==============================] - 0s 0us/step
1654784/1641221 [==============================] - 0s 0us/step
len(word_index)
88588
reverse_word_index[0]
'<PAD>'
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"

데이터 준비

  • 리뷰(정수 배열)는 신경망에 주입되기 전에 텐서로 변환되어야 한다. 변환 방법은
    • one-hot encoding: 해당하는 값은 1로 나머지 값들은 모두 0으로 체운 벡터 생성, 메모리를 많이 차지함
    • 정수 배열의 길이가 모두 같도록 패딩을 추가해 max_length * num_reviews 크기의 정수 텐서를 만든다.
  • 여기서는 두 번째 방법을 사용
train_data = keras.preprocessing.sequence.pad_sequences(train_data,
                                                        value=word_index["<PAD>"],
                                                        padding='post',    # 패딩 값을 뒤쪽에 붙임, 앞쪽에 붙이는 경우에는 'pre' 사용
                                                        maxlen=256)

test_data = keras.preprocessing.sequence.pad_sequences(test_data,
                                                       value=word_index["<PAD>"],
                                                       padding='post',
                                                       maxlen=256)
  • 변환된 길이 확인: 256으로 길이가 같아짐
  • 길이가 256보다 짧은 리뷰들은 빈 공간에 0으로 패딩
len(train_data[0]), len(train_data[1])
(256, 256)
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]

모델 구성

  • 두 가지 참고해야 할 사항
    • 모델에서 얼마나 많은 층을 사용할 것인가?
    • 각 층에서 얼마나 많은 은닉 유닛을 사용할 것인가?
# 입력 크기는 영화 리뷰 데이터셋에 적용된 어휘 사전의 크기입니다(10,000개의 단어)
vocab_size = 10000

model = keras.Sequential()
model.add(keras.layers.Embedding(vocab_size, 16, input_shape=(None,)))
model.add(keras.layers.GlobalAveragePooling1D())
model.add(keras.layers.Dense(16, activation='relu'))
model.add(keras.layers.Dense(1, activation='sigmoid'))

model.summary()
2022-01-27 15:13:27.194661: I tensorflow/core/common_runtime/pluggable_device/pluggable_device_factory.cc:305] Could not identify NUMA node of platform GPU ID 0, defaulting to 0. Your kernel may not have been built with NUMA support.
2022-01-27 15:13:27.195448: I tensorflow/core/common_runtime/pluggable_device/pluggable_device_factory.cc:271] Created TensorFlow device (/job:localhost/replica:0/task:0/device:GPU:0 with 0 MB memory) -> physical PluggableDevice (device: 0, name: METAL, pci bus id: <undefined>)


Metal device set to: Apple M1
Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #
=================================================================
 embedding (Embedding)       (None, None, 16)          160000

 global_average_pooling1d (G  (None, 16)               0
 lobalAveragePooling1D)

 dense (Dense)               (None, 16)                272

 dense_1 (Dense)             (None, 1)                 17

=================================================================
Total params: 160,289
Trainable params: 160,289
Non-trainable params: 0
_________________________________________________________________
  • 각 층에 대한 설명
    • Embbeding:
      • 모델의 첫 번째 레이어로만 사용 가능
      • 정수로 인코딩된 배열을 입력받아 각 단어 인덱스에 해당하는 임베딩 벡터를 찾는다.
      • 모델이 훈련되면서 학습한다.
      • 출력 배열에 새로운 차원으로 추가된다.
      • 최종 출력값의 차원은 (batch, sequence, embedding)이다(25000, 256, 16).
    • GlobalAveragePooling1D:
      • sequence 차원에 대한 평균을 계산하여 각 샘플에 대한 고정된 길이의 출력 벡터를 반환한다.
      • 길이가 다른 입력을 다루는 가장 간단한 방법
    • Dense 1:
      • 16개의 유닛을 가진 완전 연결 층
      • 활성화 함수로 relu 사용
    • Dense 2:
      • 하나의 최종 출력을 갖는 완전 연결 층
      • 활성화 함수로 sigmoid 함수를 사용하여 0~1 사이의 실수값을 출력한다.
      • 이 출력 값은 확률 또는 신뢰도이다.

은닉 유닛

  • 두 개의 은닉 층이 존재(GlobalAveragePooling1D와 첫 번째 Dense)
  • 출력(유닛, 노드, 뉴런…)의 개수는 층이 가진 표현 공간의 차원
  • 모델에 많은 은닉 유닛과 층이 있다면 더 복잡한 표현을 학습할 수 있지만 과대적합에 빠지기 쉽다.

손실 함수와 옵티마이저

  • 이진 분류 모델이므로(최종 출력층에서 출력 유닛이 1개이고 활성화 함수가 sigmoid임) binary_crossentropy 손실 함수를 사용
  • 다른 손실 함수도 사용 가능하나 확률을 다루는데는 binary_crossentropy가 적합함
  • 정답인 타깃 분포와 예측 분포 사이의 거리
model.compile(optimizer='adam',
              loss='binary_crossentropy',
              metrics=['accuracy'])

검증 세트 만들기

  • 훈련 세트에서 10,000개를 나누어 검증 세트로 사용
x_val = train_data[:10000]
partial_x_train = train_data[10000:]

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

모델 훈련

  • 512 개의 샘플로 이루어진 미니 배치에서 40 에포크(epoch) 동안 훈련
  • 훈련하는 동안 위에서 만든 10,000 개의 검증 데이터 세트로 모니터링 수행
history = model.fit(partial_x_train,
                    partial_y_train,
                    epochs=40,
                    batch_size=512,
                    validation_data=(x_val, y_val),
                    verbose=1)
2022-01-27 16:26:10.669256: W tensorflow/core/platform/profile_utils/cpu_utils.cc:128] Failed to get CPU frequency: 0 Hz


Epoch 1/40


2022-01-27 16:26:10.928534: I tensorflow/core/grappler/optimizers/custom_graph_optimizer_registry.cc:112] Plugin optimizer for device_type GPU is enabled.


30/30 [==============================] - 7s 177ms/step - loss: 0.6913 - accuracy: 0.5999 - val_loss: 0.6889 - val_accuracy: 0.7090
Epoch 2/40


2022-01-27 16:26:17.492867: I tensorflow/core/grappler/optimizers/custom_graph_optimizer_registry.cc:112] Plugin optimizer for device_type GPU is enabled.


30/30 [==============================] - 5s 175ms/step - loss: 0.6847 - accuracy: 0.6822 - val_loss: 0.6801 - val_accuracy: 0.7376
Epoch 3/40
30/30 [==============================] - 5s 178ms/step - loss: 0.6710 - accuracy: 0.7535 - val_loss: 0.6630 - val_accuracy: 0.7535
Epoch 4/40
30/30 [==============================] - 5s 173ms/step - loss: 0.6470 - accuracy: 0.7719 - val_loss: 0.6358 - val_accuracy: 0.7690
Epoch 5/40
30/30 [==============================] - 5s 173ms/step - loss: 0.6116 - accuracy: 0.7915 - val_loss: 0.5984 - val_accuracy: 0.7813
Epoch 6/40
30/30 [==============================] - 5s 173ms/step - loss: 0.5671 - accuracy: 0.8062 - val_loss: 0.5555 - val_accuracy: 0.8003
Epoch 7/40
30/30 [==============================] - 5s 176ms/step - loss: 0.5183 - accuracy: 0.8269 - val_loss: 0.5111 - val_accuracy: 0.8167
Epoch 8/40
30/30 [==============================] - 5s 170ms/step - loss: 0.4700 - accuracy: 0.8453 - val_loss: 0.4689 - val_accuracy: 0.8323
Epoch 9/40
30/30 [==============================] - 5s 176ms/step - loss: 0.4250 - accuracy: 0.8614 - val_loss: 0.4319 - val_accuracy: 0.8428
Epoch 10/40
30/30 [==============================] - 5s 166ms/step - loss: 0.3857 - accuracy: 0.8727 - val_loss: 0.4012 - val_accuracy: 0.8522
Epoch 11/40
30/30 [==============================] - 5s 167ms/step - loss: 0.3523 - accuracy: 0.8819 - val_loss: 0.3756 - val_accuracy: 0.8593
Epoch 12/40
30/30 [==============================] - 5s 173ms/step - loss: 0.3244 - accuracy: 0.8897 - val_loss: 0.3561 - val_accuracy: 0.8643
Epoch 13/40
30/30 [==============================] - 5s 170ms/step - loss: 0.3015 - accuracy: 0.8959 - val_loss: 0.3405 - val_accuracy: 0.8702
Epoch 14/40
30/30 [==============================] - 5s 169ms/step - loss: 0.2816 - accuracy: 0.9012 - val_loss: 0.3286 - val_accuracy: 0.8731
Epoch 15/40
30/30 [==============================] - 5s 173ms/step - loss: 0.2643 - accuracy: 0.9061 - val_loss: 0.3190 - val_accuracy: 0.8753
Epoch 16/40
30/30 [==============================] - 5s 175ms/step - loss: 0.2492 - accuracy: 0.9117 - val_loss: 0.3108 - val_accuracy: 0.8763
Epoch 17/40
30/30 [==============================] - 5s 172ms/step - loss: 0.2350 - accuracy: 0.9173 - val_loss: 0.3042 - val_accuracy: 0.8804
Epoch 18/40
30/30 [==============================] - 5s 158ms/step - loss: 0.2231 - accuracy: 0.9225 - val_loss: 0.2995 - val_accuracy: 0.8796
Epoch 19/40
30/30 [==============================] - 5s 169ms/step - loss: 0.2120 - accuracy: 0.9256 - val_loss: 0.2952 - val_accuracy: 0.8826
Epoch 20/40
30/30 [==============================] - 5s 168ms/step - loss: 0.2012 - accuracy: 0.9307 - val_loss: 0.2921 - val_accuracy: 0.8836
Epoch 21/40
30/30 [==============================] - 5s 164ms/step - loss: 0.1916 - accuracy: 0.9351 - val_loss: 0.2895 - val_accuracy: 0.8845
Epoch 22/40
30/30 [==============================] - 5s 163ms/step - loss: 0.1829 - accuracy: 0.9389 - val_loss: 0.2889 - val_accuracy: 0.8839
Epoch 23/40
30/30 [==============================] - 4s 150ms/step - loss: 0.1745 - accuracy: 0.9439 - val_loss: 0.2870 - val_accuracy: 0.8842
Epoch 24/40
30/30 [==============================] - 5s 168ms/step - loss: 0.1669 - accuracy: 0.9460 - val_loss: 0.2874 - val_accuracy: 0.8840
Epoch 25/40
30/30 [==============================] - 5s 167ms/step - loss: 0.1593 - accuracy: 0.9497 - val_loss: 0.2859 - val_accuracy: 0.8845
Epoch 26/40
30/30 [==============================] - 5s 165ms/step - loss: 0.1523 - accuracy: 0.9524 - val_loss: 0.2860 - val_accuracy: 0.8858
Epoch 27/40
30/30 [==============================] - 5s 165ms/step - loss: 0.1455 - accuracy: 0.9545 - val_loss: 0.2867 - val_accuracy: 0.8860
Epoch 28/40
30/30 [==============================] - 5s 163ms/step - loss: 0.1398 - accuracy: 0.9577 - val_loss: 0.2878 - val_accuracy: 0.8860
Epoch 29/40
30/30 [==============================] - 5s 167ms/step - loss: 0.1334 - accuracy: 0.9601 - val_loss: 0.2894 - val_accuracy: 0.8862
Epoch 30/40
30/30 [==============================] - 5s 164ms/step - loss: 0.1288 - accuracy: 0.9613 - val_loss: 0.2910 - val_accuracy: 0.8855
Epoch 31/40
30/30 [==============================] - 5s 167ms/step - loss: 0.1228 - accuracy: 0.9643 - val_loss: 0.2932 - val_accuracy: 0.8853
Epoch 32/40
30/30 [==============================] - 5s 155ms/step - loss: 0.1173 - accuracy: 0.9665 - val_loss: 0.2953 - val_accuracy: 0.8859
Epoch 33/40
30/30 [==============================] - 5s 161ms/step - loss: 0.1129 - accuracy: 0.9680 - val_loss: 0.2989 - val_accuracy: 0.8831
Epoch 34/40
30/30 [==============================] - 5s 160ms/step - loss: 0.1082 - accuracy: 0.9691 - val_loss: 0.3014 - val_accuracy: 0.8836
Epoch 35/40
30/30 [==============================] - 4s 147ms/step - loss: 0.1034 - accuracy: 0.9711 - val_loss: 0.3030 - val_accuracy: 0.8846
Epoch 36/40
30/30 [==============================] - 5s 161ms/step - loss: 0.0994 - accuracy: 0.9731 - val_loss: 0.3067 - val_accuracy: 0.8839
Epoch 37/40
30/30 [==============================] - 5s 161ms/step - loss: 0.0952 - accuracy: 0.9746 - val_loss: 0.3096 - val_accuracy: 0.8839
Epoch 38/40
30/30 [==============================] - 5s 160ms/step - loss: 0.0914 - accuracy: 0.9755 - val_loss: 0.3154 - val_accuracy: 0.8819
Epoch 39/40
30/30 [==============================] - 5s 154ms/step - loss: 0.0882 - accuracy: 0.9770 - val_loss: 0.3167 - val_accuracy: 0.8823
Epoch 40/40
30/30 [==============================] - 5s 158ms/step - loss: 0.0841 - accuracy: 0.9792 - val_loss: 0.3208 - val_accuracy: 0.8806

모델 평가

results = model.evaluate(test_data,  test_labels, verbose=2)

print(results)
782/782 - 2s - loss: 0.3435 - accuracy: 0.8713 - 2s/epoch - 2ms/step
[0.3435123562812805, 0.8713200688362122]

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

  • model.fit()은 History 객체를 반환
history_dict = history.history
history_dict.keys()
dict_keys(['loss', 'accuracy', 'val_loss', 'val_accuracy'])
import matplotlib.pyplot as plt

acc = history_dict['accuracy']
val_acc = history_dict['val_accuracy']
loss = history_dict['loss']
val_loss = history_dict['val_loss']

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

# "bo"는 "파란색 점"입니다
plt.plot(epochs, loss, 'bo', label='Training loss')
# b는 "파란 실선"입니다
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()

png

plt.clf()   # 그림을 초기화합니다

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()

png

테스트 시에는 손실은 지속적으로 감소하고 훈련 정화도는 지속적으로 증가하고 있다. 하지만 검증의 경우 손실은 20 에포크, 정확도는 15 에포크 언저리에서 변화가 없다. 검증 데이터에서 성능이 좋지 않으므로 과대적합 되어 있다고 볼 수 있다. 이런 경우 과대 적합을 막기 위해 20 에포크 언저리에서 훈련을 멈추는 방법이 있다.