이번에는 이전 튜토리얼과 동일하게 영화 리뷰(review) 텍스트를 긍정(positive) 또는 부정(negative)으로 분류할 것입니다. 마찬가지로 이진(binary)(클래스(class)가 두 개인) 분류 문제입니다.
그러나 이전 튜토리얼과는 다르게 이번 튜토리얼에서는 텍스트 데이터에 사전 전처리를 적용하여 모델 학습에 활용합니다.
다시 한번 데이터에 대해 소개하자면, 인터넷 영화 데이터베이스(Internet Movie Database)에서 수집한 50,000개의 영화 리뷰 텍스트를 담은 IMDB 데이터셋을 사용하며, 25,000개 리뷰는 훈련용으로, 25,000개는 테스트용으로 나누어져 있습니다. 또한, 긍정적인 리뷰와 부정적인 리뷰의 개수가 동일하기 때문에 훈련 세트와 테스트 세트의 클래스는 균형이 잡혀 있다고 할 수 있습니다.
# 필요한 라이브러리 임포트
import warnings
warnings.simplefilter('ignore')
import tensorflow as tf
from tensorflow import keras
import numpy as np
print(tf.__version__)
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)))
리뷰 텍스트는 어휘 사전의 특정 단어를 나타내는 정수로 변환되어 있습니다. 첫 번째 리뷰를 확인해 봅시다:
print(train_data[0])
영화 리뷰들은 각각 길이가 다릅니다. 다음 코드는 첫 번째 리뷰와 두 번째 리뷰에서 단어의 개수를 출력합니다. 신경망의 입력은 길이가 같아야 하기 때문에 신경망 입력을 위하여 튜토리얼의 후반부에서 이 문제를 해결하겠습니다.
len(train_data[0]), len(train_data[1])
정수를 다시 텍스트로 변환하는 방법이 있다면 유용할 것입니다. 여기에서는 정수와 문자열을 매핑한 딕셔너리(dictionary) 객체를 사용하여 단어로 다시 변환하는 함수를 만들어 리뷰를 확인해보도록 하겠습니다:
# 단어와 정수 인덱스를 매핑한 딕셔너리
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])
리뷰(정수 배열)는 신경망에 주입하기 전에 텐서로 변환되어야 합니다. 정수 배열을 텐서로 변환하는 방법에는 다음과 같은 방법이 있습니다:
num_words * num_reviews
크기의 행렬이 필요하기 때문에 많은 메모리를 사용합니다.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)
0번째와 1번째 텍스트 샘플의 길이를 확인해 봅시다:
len(train_data[0]), len(train_data[1])
(패딩된) 첫 번째 리뷰 내용을 확인합니다:
print(train_data[0])
신경망은 층(layer)을 쌓아서 만듭니다. 이 단계에서는 다음 두 가지를 결정해야 합니다:
이 예제의 입력 데이터는 단어 인덱스의 배열입니다. 예측할 레이블은 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='relu'))
model.add(keras.layers.Dense(1, activation='sigmoid'))
model.summary()
층을 순서대로 쌓아 분류기(classifier)를 만듭니다:
첫 번째 층은 Embedding
층입니다. 이 층은 정수로 인코딩된 단어를 입력 받고 각 단어 인덱스에 해당하는 임베딩 벡터를 찾습니다. 이 벡터는 모델이 훈련되면서 학습되며 출력 배열에 새로운 차원으로 추가됩니다. 따라서 최종 차원은 (batch, sequence, embedding)
이 됩니다.
그 다음 GlobalAveragePooling1D
층은 sequence
차원에 대해 평균을 계산하여 각 샘플에 대해 고정된 길이의 출력 벡터를 반환합니다. 이는 길이가 다른 입력을 다루는 가장 간단한 방법이기 때문에 만약 입력으로 사용되는 리뷰에 포함된 단어 개수가 변경되더라도 같은 크기의 벡터로 처리할 수 있습니다.
이 고정 길이의 출력 벡터는 16개의 은닉 유닛을 가진 완전 연결(fully-connected) 층(Dense
)을 거칩니다.
마지막 층은 하나의 출력 노드(node)를 가진 완전 연결 층입니다. sigmoid
활성화 함수를 사용하여 0과 1 사이의 실수를 출력합니다. 이 값은 각 리뷰 데이터가 긍정 리뷰일 확률 또는 신뢰도를 나타냅니다.
위 모델에는 입력과 출력 사이에 두 개의 중간 또는 은닉 층이 있습니다. 출력(유닛 또는 노드, 뉴런)의 개수는 층이 가진 표현 공간(representational space)의 차원이 됩니다. 다른 말로 하면, 내부 표현을 학습할 때 허용되는 네트워크 자유도의 양입니다.
모델에 많은 은닉 유닛(고차원의 표현 공간)과 층이 있다면 네트워크는 더 복잡한 표현을 학습할 수 있습니다. 하지만 이러한 고차원의 표현 공간과 층은 네트워크의 계산 비용이 많이 들고 원치않는 패턴을 학습할 가능성도 있습니다.
이런 표현은 훈련 데이터의 성능을 향상시키지만 테스트 데이터에서는 그렇지 못합니다. 이를 과대적합(overfitting)이라고 부릅니다.
모델이 훈련하려면 손실 함수(loss function) 과 옵티마이저(optimizer) 가 필요합니다. 이 예제는 클래스가 두 개인 이진 분류 문제이고 모델이 확률을 출력하므로(출력층의 유닛이 하나이고 sigmoid
활성화 함수를 사용하기 때문에), binary_crossentropy
손실 함수를 사용하겠습니다.
물론 다른 손실 함수를 선택할 수 없는 것은 아닙니다. 예를 들어 mean_squared_error
를 선택할 수 있습니다. 하지만 일반적으로 binary_crossentropy
가 확률을 다루는데 적합합니다. 이 함수는 확률 분포 간의 거리를 측정합니다. 여기에서는 정답인 타깃 분포와 예측 분포 사이의 거리입니다.
이제 모델이 사용할 옵티마이저와 손실 함수를 설정하겠습니다:
model.compile(optimizer='adam',
loss='binary_crossentropy',
metrics=['accuracy'])
모델을 훈련할 때 모델이 만난 적 없는 데이터에서 정확도를 확인하는 것이 좋습니다. 원본 훈련 데이터에서 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_train
과 y_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)
모델의 성능을 확인해 봅시다. 손실(오차를 나타내는 숫자이므로 낮을수록 좋습니다) 과 정확도 두 개의 값이 반환됩니다.
results = model.evaluate(test_data, test_labels, verbose=2)
print(results)
학습 결과 87% 정도의 정확도를 달성했습니다.
model.fit()
은 History
객체를 반환합니다. 여기에는 훈련하는 동안 일어난 모든 정보가 담긴 딕셔너리(dictionary)가 들어 있습니다:
history_dict = history.history
history_dict.keys()
훈련과 검증 단계에서 모니터링하는 지표인 네 개 항목이 있습니다. 훈련 손실과 검증 손실을 그래프로 그려 보고, 훈련 정확도와 검증 정확도도 그래프로 그려서 비교해 보겠습니다:
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()
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()
이 그래프에서 점선은 훈련 손실과 훈련 정확도를 나타내고, 실선은 검증 손실과 검증 정확도를 나타냅니다.
훈련 손실은 에포크마다 감소하고 훈련 정확도는 증가한다는 점을 주목합시다. 이는 경사 하강법 최적화를 사용할 때 볼 수 있는 현상으로 매 반복마다 최적화 대상의 값을 최소화합니다.
하지만 검증 손실과 검증 정확도 그래프는 그렇지 못합니다. 약 20번째 에포크 이후가 최적점인 것 같습니다. 이는 이전에 본 적 없는 데이터보다 훈련 데이터에서 더 잘 동작하는 현상인 과대적합 때문입니다. 이 지점부터는 모델이 과도하게 최적화되어 테스트 데이터에서 일반화되기 어려운 훈련 데이터의 특정 표현을 학습합니다.
#@title Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#@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.