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

TensorFlow.org에서 보기 구글 코랩(Colab)에서 실행하기 깃허브(GitHub) 소스 보기

이 노트북은 영화 리뷰(review) 텍스트를 긍정(positive) 또는 부정(negative)으로 분류합니다. 이 예제는 이진(binary)-또는 클래스(class)가 두 개인- 분류 문제입니다. 이진 분류는 머신러닝에서 중요하고 널리 사용됩니다.

여기에서는 인터넷 영화 데이터베이스(Internet Movie Database)에서 수집한 50,000개의 영화 리뷰 텍스트를 담은 IMDB 데이터셋을 사용하겠습니다. 25,000개 리뷰는 훈련용으로, 25,000개는 테스트용으로 나뉘어져 있습니다. 훈련 세트와 테스트 세트의 클래스는 균형이 잡혀 있습니다. 즉 긍정적인 리뷰와 부정적인 리뷰의 개수가 동일합니다.

이 노트북은 모델을 만들고 훈련하기 위해 텐서플로의 고수준 파이썬 API인 tf.keras를 사용합니다. tf.keras를 사용한 고급 텍스트 분류 튜토리얼은 MLCC 텍스트 분류 가이드를 참고하세요.

# keras.datasets.imdb is broken in 1.13 and 1.14, by np 1.16.3
!pip install -q tf_nightly
from __future__ import absolute_import, division, print_function, unicode_literals, unicode_literals
import tensorflow as tf
from tensorflow import keras

import numpy as np

print(tf.__version__)
1.14.1-dev20190524

IMDB 데이터셋 다운로드

IMDB 데이터셋은 텐서플로와 함께 제공됩니다. 리뷰(단어의 시퀀스(sequence))는 미리 전처리해서 정수 시퀀스로 변환되어 있습니다. 각 정수는 어휘 사전에 있는 특정 단어를 의미합니다.

다음 코드는 IMDB 데이터셋을 컴퓨터에 다운로드합니다(또는 이전에 다운로드 받았다면 캐시된 복사본을 사용합니다):

imdb = keras.datasets.imdb

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

매개변수 num_words=10000은 훈련 데이터에서 가장 많이 등장하는 상위 10,000개의 단어를 선택합니다. 데이터 크기를 적당하게 유지하기 위해 드물에 등장하는 단어는 제외하겠습니다.

데이터 탐색

잠시 데이터 형태를 알아 보죠. 이 데이터셋의 샘플은 전처리된 정수 배열입니다. 이 정수는 영화 리뷰에 나오는 단어를 나타냅니다. 레이블(label)은 정수 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)

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

정수를 다시 텍스트로 변환하는 방법이 있다면 유용할 것입니다. 여기에서는 정수와 문자열을 매핑한 딕셔너리(dictionary) 객체에 질의하는 헬퍼(helper) 함수를 만들겠습니다:

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

# 처음 몇 개 인덱스는 사전에 정의되어 있습니다
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])

이제 decode_review 함수를 사용해 첫 번째 리뷰 텍스트를 출력할 수 있습니다:

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)은 정수 배열을 0과 1로 이루어진 벡터로 변환합니다. 예를 들어 배열 [3, 5]을 인덱스 3과 5만 1이고 나머지는 모두 0인 10,000차원 벡터로 변환할 수 있습니다. 그다음 실수 벡터 데이터를 다룰 수 있는 층-Dense 층-을 신경망의 첫 번째 층으로 사용합니다. 이 방법은 num_words * num_reviews 크기의 행렬이 필요하기 때문에 메모리를 많이 사용합니다.
  • 다른 방법으로는, 정수 배열의 길이가 모두 같도록 패딩(padding)을 추가해 max_length * num_reviews 크기의 정수 텐서를 만듭니다. 이런 형태의 텐서를 다룰 수 있는 임베딩(embedding) 층을 신경망의 첫 번째 층으로 사용할 수 있습니다.

이 튜토리얼에서는 두 번째 방식을 사용하겠습니다.

영화 리뷰의 길이가 같아야 하므로 pad_sequences 함수를 사용해 길이를 맞추겠습니다:

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)

샘플의 길이를 확인해 보죠:

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]

모델 구성

신경망은 층(layer)을 쌓아서 만듭니다. 이 구조에서는 두 가지를 결정해야 합니다:

  • 모델에서 얼마나 많은 층을 사용할 것인가?
  • 각 층에서 얼마나 많은 은닉 유닛(hidden unit)을 사용할 것인가?

이 예제의 입력 데이터는 단어 인덱스의 배열입니다. 예측할 레이블은 0 또는 1입니다. 이 문제에 맞는 모델을 구성해 보죠:

# 입력 크기는 영화 리뷰 데이터셋에 적용된 어휘 사전의 크기입니다(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=tf.nn.relu))
model.add(keras.layers.Dense(1, activation=tf.nn.sigmoid))

model.summary()
WARNING: Logging before flag parsing goes to stderr.
W0524 23:30:34.825410 140608263632640 deprecation.py:506] From /home/kbuilder/.local/lib/python3.5/site-packages/tensorflow/python/keras/initializers.py:119: calling RandomUniform.__init__ (from tensorflow.python.ops.init_ops) with dtype is deprecated and will be removed in a future version.
Instructions for updating:
Call initializer instance with the dtype argument instead of passing it to the constructor

Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding (Embedding)        (None, None, 16)          160000    
_________________________________________________________________
global_average_pooling1d (Gl (None, 16)                0         
_________________________________________________________________
dense (Dense)                (None, 16)                272       
_________________________________________________________________
dense_1 (Dense)              (None, 1)                 17        
=================================================================
Total params: 160,289
Trainable params: 160,289
Non-trainable params: 0
_________________________________________________________________

층을 순서대로 쌓아 분류기(classifier)를 만듭니다:

  1. 첫 번째 층은 Embedding 층입니다. 이 층은 정수로 인코딩된 단어를 입력 받고 각 단어 인덱스에 해당하는 임베딩 벡터를 찾습니다. 이 벡터는 모델이 훈련되면서 학습됩니다. 이 벡터는 출력 배열에 새로운 차원으로 추가됩니다. 최종 차원은 (batch, sequence, embedding)이 됩니다.
  2. 그다음 GlobalAveragePooling1D 층은 sequence 차원에 대해 평균을 계산하여 각 샘플에 대해 고정된 길이의 출력 벡터를 반환합니다. 이는 길이가 다른 입력을 다루는 가장 간단한 방법입니다.
  3. 이 고정 길이의 출력 벡터는 16개의 은닉 유닛을 가진 완전 연결(fully-connected) 층(Dense)을 거칩니다.
  4. 마지막 층은 하나의 출력 노드(node)를 가진 완전 연결 층입니다. sigmoid 활성화 함수를 사용하여 0과 1 사이의 실수를 출력합니다. 이 값은 확률 또는 신뢰도를 나타냅니다.

은닉 유닛

위 모델에는 입력과 출력 사이에 두 개의 중간 또는 "은닉" 층이 있습니다. 출력(유닛 또는 노드, 뉴런)의 개수는 층이 가진 표현 공간(representational space)의 차원이 됩니다. 다른 말로 하면, 내부 표현을 학습할 때 허용되는 네트워크 자유도의 양입니다.

모델에 많은 은닉 유닛(고차원의 표현 공간)과 층이 있다면 네트워크는 더 복잡한 표현을 학습할 수 있습니다. 하지만 네트워크의 계산 비용이 많이 들고 원치않는 패턴을 학습할 수도 있습니다. 이런 표현은 훈련 데이터의 성능을 향상시키지만 테스트 데이터에서는 그렇지 못합니다. 이를 과대적합(overfitting)이라고 부릅니다. 나중에 이에 대해 알아 보겠습니다.

손실 함수와 옵티마이저

모델이 훈련하려면 손실 함수(loss function)과 옵티마이저(optimizer)가 필요합니다. 이 예제는 이진 분류 문제이고 모델이 확률을 출력하므로(출력층의 유닛이 하나이고 sigmoid 활성화 함수를 사용합니다), binary_crossentropy 손실 함수를 사용하겠습니다.

다른 손실 함수를 선택할 수 없는 것은 아닙니다. 예를 들어 mean_squared_error를 선택할 수 있습니다. 하지만 일반적으로 binary_crossentropy가 확률을 다루는데 적합합니다. 이 함수는 확률 분포 간의 거리를 측정합니다. 여기에서는 정답인 타깃 분포와 예측 분포 사이의 거리입니다.

나중에 회귀(regression) 문제(예를 들어 주택 가격을 예측하는 문제)에 대해 살펴 볼 때 평균 제곱 오차(mean squared error) 손실 함수를 어떻게 사용하는지 알아 보겠습니다.

이제 모델이 사용할 옵티마이저와 손실 함수를 설정해 보죠:

model.compile(optimizer=tf.train.AdamOptimizer(),
              loss='binary_crossentropy',
              metrics=['acc'])
W0524 23:30:34.935649 140608263632640 deprecation.py:323] From /home/kbuilder/.local/lib/python3.5/site-packages/tensorflow/python/ops/nn_impl.py:180: add_dispatch_support.<locals>.wrapper (from tensorflow.python.ops.array_ops) is deprecated and will be removed in a future version.
Instructions for updating:
Use tf.where in 2.0, which has the same broadcast rule as np.where

검증 세트 만들기

모델을 훈련할 때 모델이 만난 적 없는 데이터에서 정확도를 확인하는 것이 좋습니다. 원본 훈련 데이터에서 10,000개의 샘플을 떼어내어 검증 세트(validation set)를 만들겠습니다. (왜 테스트 세트를 사용하지 않을까요? 훈련 데이터만을 사용하여 모델을 개발하고 튜닝하는 것이 목표입니다. 그다음 테스트 세트를 사용해서 딱 한 번만 정확도를 평가합니다).

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

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

모델 훈련

이 모델을 512개의 샘플로 이루어진 미니배치(mini-batch)에서 40번의 에포크(epoch) 동안 훈련합니다. x_trainy_train 텐서에 있는 모든 샘플에 대해 40번 반복한다는 뜻입니다. 훈련하는 동안 10,000개의 검증 세트에서 모델의 손실과 정확도를 모니터링합니다:

history = model.fit(partial_x_train,
                    partial_y_train,
                    epochs=40,
                    batch_size=512,
                    validation_data=(x_val, y_val),
                    verbose=1)
Train on 15000 samples, validate on 10000 samples
Epoch 1/40
15000/15000 [==============================] - 1s 50us/sample - loss: 0.6918 - acc: 0.5781 - val_loss: 0.6898 - val_acc: 0.6336
Epoch 2/40
15000/15000 [==============================] - 1s 37us/sample - loss: 0.6860 - acc: 0.6889 - val_loss: 0.6823 - val_acc: 0.7169
Epoch 3/40
15000/15000 [==============================] - 1s 36us/sample - loss: 0.6746 - acc: 0.7415 - val_loss: 0.6684 - val_acc: 0.7564
Epoch 4/40
15000/15000 [==============================] - 1s 38us/sample - loss: 0.6550 - acc: 0.7539 - val_loss: 0.6458 - val_acc: 0.7678
Epoch 5/40
15000/15000 [==============================] - 1s 39us/sample - loss: 0.6253 - acc: 0.7895 - val_loss: 0.6145 - val_acc: 0.7860
Epoch 6/40
15000/15000 [==============================] - 1s 38us/sample - loss: 0.5865 - acc: 0.8112 - val_loss: 0.5767 - val_acc: 0.7990
Epoch 7/40
15000/15000 [==============================] - 1s 39us/sample - loss: 0.5419 - acc: 0.8263 - val_loss: 0.5343 - val_acc: 0.8179
Epoch 8/40
15000/15000 [==============================] - 1s 38us/sample - loss: 0.4958 - acc: 0.8440 - val_loss: 0.4938 - val_acc: 0.8302
Epoch 9/40
15000/15000 [==============================] - 1s 38us/sample - loss: 0.4518 - acc: 0.8579 - val_loss: 0.4561 - val_acc: 0.8416
Epoch 10/40
15000/15000 [==============================] - 1s 38us/sample - loss: 0.4120 - acc: 0.8701 - val_loss: 0.4243 - val_acc: 0.8498
Epoch 11/40
15000/15000 [==============================] - 1s 38us/sample - loss: 0.3778 - acc: 0.8788 - val_loss: 0.3975 - val_acc: 0.8585
Epoch 12/40
15000/15000 [==============================] - 1s 39us/sample - loss: 0.3488 - acc: 0.8881 - val_loss: 0.3769 - val_acc: 0.8599
Epoch 13/40
15000/15000 [==============================] - 1s 38us/sample - loss: 0.3249 - acc: 0.8925 - val_loss: 0.3583 - val_acc: 0.8657
Epoch 14/40
15000/15000 [==============================] - 1s 38us/sample - loss: 0.3036 - acc: 0.8987 - val_loss: 0.3448 - val_acc: 0.8694
Epoch 15/40
15000/15000 [==============================] - 1s 40us/sample - loss: 0.2859 - acc: 0.9028 - val_loss: 0.3333 - val_acc: 0.8731
Epoch 16/40
15000/15000 [==============================] - 1s 40us/sample - loss: 0.2700 - acc: 0.9077 - val_loss: 0.3239 - val_acc: 0.8747
Epoch 17/40
15000/15000 [==============================] - 1s 39us/sample - loss: 0.2555 - acc: 0.9129 - val_loss: 0.3161 - val_acc: 0.8769
Epoch 18/40
15000/15000 [==============================] - 1s 41us/sample - loss: 0.2427 - acc: 0.9174 - val_loss: 0.3092 - val_acc: 0.8795
Epoch 19/40
15000/15000 [==============================] - 1s 41us/sample - loss: 0.2312 - acc: 0.9203 - val_loss: 0.3036 - val_acc: 0.8812
Epoch 20/40
15000/15000 [==============================] - 1s 40us/sample - loss: 0.2208 - acc: 0.9243 - val_loss: 0.2997 - val_acc: 0.8807
Epoch 21/40
15000/15000 [==============================] - 1s 40us/sample - loss: 0.2105 - acc: 0.9287 - val_loss: 0.2964 - val_acc: 0.8815
Epoch 22/40
15000/15000 [==============================] - 1s 39us/sample - loss: 0.2017 - acc: 0.9306 - val_loss: 0.2928 - val_acc: 0.8833
Epoch 23/40
15000/15000 [==============================] - 1s 40us/sample - loss: 0.1928 - acc: 0.9347 - val_loss: 0.2913 - val_acc: 0.8833
Epoch 24/40
15000/15000 [==============================] - 1s 39us/sample - loss: 0.1852 - acc: 0.9382 - val_loss: 0.2892 - val_acc: 0.8846
Epoch 25/40
15000/15000 [==============================] - 1s 39us/sample - loss: 0.1774 - acc: 0.9431 - val_loss: 0.2871 - val_acc: 0.8846
Epoch 26/40
15000/15000 [==============================] - 1s 38us/sample - loss: 0.1704 - acc: 0.9451 - val_loss: 0.2872 - val_acc: 0.8847
Epoch 27/40
15000/15000 [==============================] - 1s 39us/sample - loss: 0.1638 - acc: 0.9475 - val_loss: 0.2862 - val_acc: 0.8846
Epoch 28/40
15000/15000 [==============================] - 1s 38us/sample - loss: 0.1574 - acc: 0.9509 - val_loss: 0.2861 - val_acc: 0.8852
Epoch 29/40
15000/15000 [==============================] - 1s 39us/sample - loss: 0.1520 - acc: 0.9532 - val_loss: 0.2870 - val_acc: 0.8842
Epoch 30/40
15000/15000 [==============================] - 1s 38us/sample - loss: 0.1462 - acc: 0.9545 - val_loss: 0.2862 - val_acc: 0.8861
Epoch 31/40
15000/15000 [==============================] - 1s 39us/sample - loss: 0.1403 - acc: 0.9585 - val_loss: 0.2867 - val_acc: 0.8860
Epoch 32/40
15000/15000 [==============================] - 1s 39us/sample - loss: 0.1350 - acc: 0.9601 - val_loss: 0.2877 - val_acc: 0.8864
Epoch 33/40
15000/15000 [==============================] - 1s 41us/sample - loss: 0.1299 - acc: 0.9617 - val_loss: 0.2894 - val_acc: 0.8858
Epoch 34/40
15000/15000 [==============================] - 1s 39us/sample - loss: 0.1254 - acc: 0.9639 - val_loss: 0.2910 - val_acc: 0.8865
Epoch 35/40
15000/15000 [==============================] - 1s 39us/sample - loss: 0.1213 - acc: 0.9638 - val_loss: 0.2927 - val_acc: 0.8858
Epoch 36/40
15000/15000 [==============================] - 1s 39us/sample - loss: 0.1166 - acc: 0.9671 - val_loss: 0.2943 - val_acc: 0.8862
Epoch 37/40
15000/15000 [==============================] - 1s 38us/sample - loss: 0.1122 - acc: 0.9687 - val_loss: 0.2965 - val_acc: 0.8860
Epoch 38/40
15000/15000 [==============================] - 1s 38us/sample - loss: 0.1081 - acc: 0.9701 - val_loss: 0.2995 - val_acc: 0.8836
Epoch 39/40
15000/15000 [==============================] - 1s 39us/sample - loss: 0.1049 - acc: 0.9706 - val_loss: 0.3023 - val_acc: 0.8827
Epoch 40/40
15000/15000 [==============================] - 1s 39us/sample - loss: 0.1008 - acc: 0.9730 - val_loss: 0.3046 - val_acc: 0.8844

모델 평가

모델의 성능을 확인해 보죠. 두 개의 값이 반환됩니다. 손실(오차를 나타내는 숫자이므로 낮을수록 좋습니다)과 정확도입니다.

results = model.evaluate(test_data, test_labels)

print(results)
25000/25000 [==============================] - 1s 34us/sample - loss: 0.3250 - acc: 0.8726
[0.3250268449497223, 0.87256]

이 예제는 매우 단순한 방식을 사용하므로 87% 정도의 정확도를 달성했습니다. 고급 방법을 사용한 모델은 95%에 가까운 정확도를 얻습니다.

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

model.fit()History 객체를 반환합니다. 여기에는 훈련하는 동안 일어난 모든 정보가 담긴 딕셔너리(dictionary)가 들어 있습니다:

history_dict = history.history
history_dict.keys()
dict_keys(['loss', 'acc', 'val_loss', 'val_acc'])

네 개의 항목이 있습니다. 훈련과 검증 단계에서 모니터링하는 지표들입니다. 훈련 손실과 검증 손실을 그래프로 그려 보고, 훈련 정확도와 검증 정확도도 그래프로 그려서 비교해 보겠습니다:

import matplotlib.pyplot as plt

acc = history_dict['acc']
val_acc = history_dict['val_acc']
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()
<Figure size 640x480 with 1 Axes>
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번째 에포크 이후가 최적점인 것 같습니다. 이는 과대적합 때문입니다. 이전에 본 적 없는 데이터보다 훈련 데이터에서 더 잘 동작합니다. 이 지점부터는 모델이 과도하게 최적화되어 테스트 데이터에서 일반화되기 어려운 훈련 데이터의 특정 표현을 학습합니다.

여기에서는 과대적합을 막기 위해 단순히 20번째 에포크 근처에서 훈련을 멈출 수 있습니다. 나중에 콜백(callback)을 사용하여 자동으로 이렇게 하는 방법을 배워 보겠습니다.

#@title MIT License
#
# Copyright (c) 2017 François Chollet
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.