IMDB 긍정 여부 분류 (RNN, Embedding)
IMDB 영화 리뷰 긍정 여부 예측 모델 학습
총 25000개의 샘플이 존재하며, 각 샘플은 영화 리뷰 한 건을 의미합니다.
데이터셋은 이미 정수로 인코딩되어 있고, 정수값은 단어의 빈도수를 나타냅니다.
-
imdb.load_data()의 인자로 num_words : 단어의 등장 빈도 순위로 몇 등까지 사용할 것인지를 의미합니다.
num_words를 10000으로 설정하여 단어의 등장 빈도 순위가 10000을 넘는 단어는 보이지 않게 해서 데이터셋을
좀 더 간단하게 표현하기 위해 사용합니다.
즉, 이 데이터에 사용할 단어 사전에 10000개의 단어를 사용합니다.
from tensorflow.keras.datasets import imdb
import matplotlib.pyplot as plt
import numpy as np
num_word = 500
(X_train, y_train), (X_test, y_test) = imdb.load_data(num_words=num_word)
X_train.shape, y_train.shape
((25000,), (25000,))
X_train의 샘플을 확인해보면 각 단어의 빈도 순위가 정수형으로 나타나 있는걸 확인할 수 있습니다.
print(X_train[0])
[1, 14, 22, 16, 43, 2, 2, 2, 2, 65, 458, 2, 66, 2, 4, 173, 36, 256, 5, 25, 100, 43, 2, 112, 50, 2, 2, 9, 35, 480, 284, 5, 150, 4, 172, 112, 167, 2, 336, 385, 39, 4, 172, 2, 2, 17, 2, 38, 13, 447, 4, 192, 50, 16, 6, 147, 2, 19, 14, 22, 4, 2, 2, 469, 4, 22, 71, 87, 12, 16, 43, 2, 38, 76, 15, 13, 2, 4, 22, 17, 2, 17, 12, 16, 2, 18, 2, 5, 62, 386, 12, 8, 316, 8, 106, 5, 4, 2, 2, 16, 480, 66, 2, 33, 4, 130, 12, 16, 38, 2, 5, 25, 124, 51, 36, 135, 48, 25, 2, 33, 6, 22, 12, 215, 28, 77, 52, 5, 14, 407, 16, 82, 2, 8, 4, 107, 117, 2, 15, 256, 4, 2, 7, 2, 5, 2, 36, 71, 43, 2, 476, 26, 400, 317, 46, 7, 4, 2, 2, 13, 104, 88, 4, 381, 15, 297, 98, 32, 2, 56, 26, 141, 6, 194, 2, 18, 4, 226, 22, 21, 134, 476, 26, 480, 5, 144, 30, 2, 18, 51, 36, 28, 224, 92, 25, 104, 4, 226, 65, 16, 38, 2, 88, 12, 16, 283, 5, 16, 2, 113, 103, 32, 15, 16, 2, 19, 178, 32]
이 데이터 셋은 리뷰의 긍정여부를 나타내기 때문에 y값은 0과 1 두개의 값만 가지는 것을 확인할 수 있습니다.
print(np.unique(y_train))
[0 1]
이 데이터는 토큰화와 정수 인코딩이라는 텍스트 전처리가 완료된 상태입니다.
다음에 시퀀스 패딩을 사용하기 위해 적절한 리뷰의 길이 즉, 토큰의 길이와 그에 따른 빈도 수를 확인해봅니다.
대체적으로 1000이하의 길이를 가지며, 100 ~ 500정도의 길이를 가진 데이터가 많은 것을 확인할 수 있습니다.
from sklearn.model_selection import train_test_split
X_train, X_val, y_train, y_val = train_test_split(X_train, y_train,
test_size=0.2, random_state=42)
length = np.array([len(x) for x in X_train])
print(f"X_train의 평균 : {np.mean(length)}\nX_train의 중간값 : {np.median(length)}")
plt.hist(length)
plt.xlabel('length')
plt.ylabel('frequency')
plt.show()
X_train의 평균 : 239.00925 X_train의 중간값 : 178.0
시퀀스 패딩
-
서로 다른 개수의 단어로 이루어진 문장을 같은 길이로 만들어주기 위해 패딩을 사용합니다.
-
패딩을 사용하기 위해서 tensorflow.keras.preprocessing.sequence 모듈의 pad_sequences함수를 사용합니다.
-
pad_sequences 함수는 숫자 0을 이용해서 같은 길이의 시퀀스로 변환합니다.
padding 파라미터
-
‘post’ : 시퀀스의 뒤에 패딩이 채워집니다. 디폴트는 ‘pre’입니다.
-
maxlen 파라미터
- 시퀀스의 최대 길이를 제한합니다.
-
truncating 파라미터
-
최대 길이를 넘는 시퀀스를 잘라낼 위치를 지정합니다.
-
‘post’로 지정하면 뒷부분을 잘라냅니다.
-
pad_sequences()의 인자 maxlen을 통해 패딩할 길이를 설정합니다.
위에서 데이터의 토큰 길이를 시각화해보고 100이 적절하다고 생각하여 maxlen을 100으로 설정합니다.
from tensorflow.keras.preprocessing.sequence import pad_sequences
max_len = 100
X_train_seq = pad_sequences(X_train, maxlen=max_len)
X_val_seq = pad_sequences(X_val, maxlen=max_len)
X_train_seq.shape, X_val_seq.shape
((20000, 100), (5000, 100))
원 핫 인코딩
단어 집합의 크길르 벡터 차원으로 만들어줍니다.
표현하고 싶은 단어의 인덱스에 1의 값을 부여하고, 나머지는 0을 부여하는 벡터 표현 방식입니다.
from tensorflow import keras
X_train_seq = keras.utils.to_categorical(X_train_seq)
X_val_seq = keras.utils.to_categorical(X_val_seq)
X_train_seq.shape, X_val_seq.shape
((20000, 100, 500), (5000, 100, 500))
순환 신경망 모델 만들기
model = keras.Sequential()
model.add(keras.layers.SimpleRNN(8, input_shape=(100, 500)))
model.add(keras.layers.Dense(1, activation='sigmoid'))
단순 순환 신경망 하나로 이루어진 간단한 모델입니다.
은닉층 크기는 8, 입력 크기는 원 핫 인코딩을 끝낸 벡터의 차원을 입력해서 SimpleRNN을 사용합니다.
RNN층 다음에 Dense층을 사용할 때는 합성곱층과 달리 Flatten층을 사용할 필요가 없습니다.
Dense층은 긍정/부정 두가지 레이블을 판별하므로 sigmoid를 사용했습니다.
model.summary()
Model: "sequential" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= simple_rnn (SimpleRNN) (None, 8) 4072 dense (Dense) (None, 1) 9 ================================================================= Total params: 4,081 Trainable params: 4,081 Non-trainable params: 0 _________________________________________________________________
순환신경망의 출력 벡터는 입력 벡터의 크기와 동일하고, 8개의 뉴런과 500개의 원핫 인코딩 입력이 완전 연결되므로 500 x 8,
은닉 상태에서 8개의 뉴런이 완전 연결되도록 순환하므로 8 x 8, 8개의 절편이 존재하므로 +8이 되어 4072개가 됩니다.
Dense층에서 8 x 1 + 1이므로 9개가 되어 총 4081개의 파라미터를 확인 할 수 있습니다.
rmsprop = keras.optimizers.RMSprop(learning_rate=1e-4)
model.compile(optimizer=rmsprop,
loss='binary_crossentropy',
metrics=['acc'])
checkpoint = keras.callbacks.ModelCheckpoint('./models/imdb-simplernn-model.h5')
early_stopping = keras.callbacks.EarlyStopping(patience=3,
restore_best_weights=True)
history = model.fit(X_train_seq, y_train, epochs=100, batch_size=64,
validation_data=(X_val_seq, y_val),
callbacks=[checkpoint, early_stopping])
loss = history.history['loss']
val_loss = history.history['val_loss']
acc = history.history['acc']
val_acc = history.history['val_acc']
epochs = range(1, len(loss) + 1)
fig = plt.figure(figsize=(10, 5))
ax1 = fig.add_subplot(1, 2, 1)
ax1.plot(epochs, loss, color='blue', label='train_loss')
ax1.plot(epochs, val_loss, color='orange', label='val_loss')
ax1.set_title('train and val loss')
ax1.set_xlabel('epochs')
ax1.set_ylabel('loss')
ax1.legend()
ax2 = fig.add_subplot(1, 2, 2)
ax2.plot(epochs, acc, color='green', label='train_acc')
ax2.plot(epochs, val_acc, color='red', label='val_acc')
ax2.set_title('train and val acc')
ax2.set_xlabel('epochs')
ax2.set_ylabel('acc')
ax2.legend()
model.evaluate(X_val_seq, y_val)
157/157 [==============================] - 2s 13ms/step - loss: 0.4702 - acc: 0.7810
[0.47018691897392273, 0.781000018119812]
단순히 SimpleRNN 하나의 층만을 사용해서 약 0.78 정도의 정확도를 얻을 수 있었습니다.
결과를 시각화해보면 40번째 epoch에서 과적합이 심해져 조기 종료되었음을 확인 할 수 있습니다.
원 핫 인코딩을 사용해서 입력 데이터를 준비한다면, 단어 사전을 유연하게 늘리기가 어렵습니다.
단어 사전이 증가할수록 또는 토큰의 개수가 늘어날수록 벡터 차원의 개수가 매우 많이 늘어나기 때문입니다.
또한, 원 핫 인코딩은 하나의 벡터 원소만 1이고 나머진 0으로 채워지기 때문에 각 토큰 사이의 관련성을 알기 어렵습니다.
이에 대비해서 임베딩 방법이 존재합니다.
임베딩은 토큰들을 지정된 갯수의 실수 벡터 즉, 밀집 벡터로 변환해줍니다.
원 핫 인코딩에 비해 단어 사이에 의미 있는 정보를 얻을 수 있는 방법입니다.
임베딩을 사용하여 두번째 모델을 만들어 보겠습니다.
Embedding()
-
단어를 밀집 벡터로 만드는 작업을 워드 임베딩(word embedding)이라고 합니다.
-
원-핫 인코딩과 상대적으로 저차원을 가지며 모든 원소의 값이 실수입니다.
-
첫번째 인자 : 단어 사전의 크기(총 단어의 개수)
-
두번째 인자 : 임베딩 벡터의 출력 차원(결과로 나오는 임베딩 벡터의 크기)
만약, embedding층과 연결된 층이 순환신경망이면 사용하지 않습니다.
- input_length : time step의 길이(입력 시퀀스의 길이)
만약, 다음에 Flatten층이 오게 되면 반드시 input_length를 명시해주어야 합니다.
-
model2 = keras.Sequential()
model2.add(keras.layers.Embedding(500, 16, input_length=100))
model2.add(keras.layers.SimpleRNN(8))
model2.add(keras.layers.Dense(1, activation='sigmoid'))
model2.summary()
Model: "sequential_1" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= embedding (Embedding) (None, 100, 16) 8000 simple_rnn_1 (SimpleRNN) (None, 8) 200 dense_1 (Dense) (None, 1) 9 ================================================================= Total params: 8,209 Trainable params: 8,209 Non-trainable params: 0 _________________________________________________________________
500개의 입력데이터가 Embedding층을 통과하면 16개의 벡터 크기로 출력됩니다.
그러므로 SimpleRNN층에서 처리하는 벡터의 갯수가 훨씬 줄어들게 됩니다.
input_length를 100으로 지정해서 입력 토큰 즉, time step의 갯수를 지정해줍니다.
summary의 결과입니다.
Embedding층에서 500개의 입력 데이터와 16개의 뉴런이 완전 연결되어
100 x 16개의 파라미터가 사용되고, 16개의 출력 벡터가 나옵니다.
SimpleRNN층에서 16개의 입력 벡터와 8개의 뉴런이 완전 연결되어 16 x 8,
다시 순환하여 8 x 8, 8개의 절편을 포함하여 총 200개의 파라미터가 사용되고,
마지막 Dense층에서 파라미터의 개수가 8 x 1 + 1이 됨을 확인할 수 있습니다.
max_len = 100
X_train_seq = pad_sequences(X_train, maxlen=max_len)
X_val_seq = pad_sequences(X_val, maxlen=max_len)
checkpoint = keras.callbacks.ModelCheckpoint('./models/imdb-embedding-simplernn-model.h5')
early_stopping = keras.callbacks.EarlyStopping(patience=3,
restore_best_weights=True)
model2.compile(optimizer=rmsprop,
loss='binary_crossentropy',
metrics=['acc'])
history2 = model2.fit(X_train_seq, y_train, epochs=100, batch_size=64, verbose=0,
validation_data=(X_val_seq, y_val),
callbacks=[checkpoint, early_stopping])
model2.evaluate(X_val_seq, y_val)
157/157 [==============================] - 0s 3ms/step - loss: 0.4535 - acc: 0.7884
[0.4534836411476135, 0.7883999943733215]
loss2 = history2.history['loss']
val_loss2 = history2.history['val_loss']
loss = history2.history['loss']
val_loss = history2.history['val_loss']
epochs = range(1, len(loss) + 1)
fig = plt.figure(figsize=(10, 5))
ax1 = fig.add_subplot(1, 2, 1)
ax1.plot(epochs, loss, color='blue', label='train_loss')
ax1.plot(epochs, val_loss, color='orange', label='val_loss')
ax1.set_title('Model1 loss')
ax1.set_xlabel('epochs')
ax1.set_ylabel('loss')
ax1.legend()
ax2 = fig.add_subplot(1, 2, 2)
ax2.plot(epochs, loss2, color='green', label='train_acc')
ax2.plot(epochs, val_loss2, color='red', label='val_acc')
ax2.set_title('Model2 loss')
ax2.set_xlabel('epochs')
ax2.set_ylabel('loss')
ax2.legend()
<matplotlib.legend.Legend at 0x7fd1d3ea7730>
위에서 데이터를 간소화하기 위해 단어의 갯수를 500개로 제한하고, 패딩의 길이를 100으로 설정했습니다.
임베딩의 효과를 더 보기 위해 이번엔 단어의 갯수를 10000개로 늘리고 패딩의 길이를 500으로 설정하여
모델을 구성해보겠습니다.
# 데이터 불러오기, 단어 사전의 길이를 10000개로 지정
num_word = 10000
(X_train, y_train), (X_test, y_test) = imdb.load_data(num_words=num_word)
# 시퀀스 패딩하기
X_train_seq = pad_sequences(X_train, maxlen=500)
X_test_seq = pad_sequences(X_test, maxlen=500)
# 검증 셋 분리
X_train_seq, X_val_seq, y_train, y_val = train_test_split(X_train_seq, y_train)
X_train_seq.shape, X_val_seq.shape
((18750, 500), (6250, 500))
# 모델 정의 및 컴파일
model = keras.Sequential()
model.add(keras.layers.Embedding(10000, 16, input_length=500))
model.add(keras.layers.SimpleRNN(8))
model.add(keras.layers.Dense(1, activation='sigmoid'))
model.compile(optimizer=rmsprop,
loss='binary_crossentropy',
metrics=['acc'])
# 콜백, 모델 학습
checkpoint = keras.callbacks.ModelCheckpoint('./models/imdb10000-simplernn-embedding.h5')
early_stopping = keras.callbacks.EarlyStopping(patience=3, restore_best_weights=True)
history = model.fit(X_train_seq, y_train, epochs=100, batch_size=64, verbose=0,
validation_data=(X_val_seq, y_val),
callbacks=[checkpoint, early_stopping])
# 손실과 정확도 시각화
loss = history.history['loss']
val_loss = history.history['val_loss']
acc = history.history['acc']
val_acc = history.history['val_acc']
epochs = range(1, len(loss) + 1)
fig = plt.figure(figsize=(10, 5))
ax1 = fig.add_subplot(1, 2, 1)
ax1.plot(epochs, loss, color='blue', label='train_loss')
ax1.plot(epochs, val_loss, color='orange', label='val_loss')
ax1.set_title('train and val loss')
ax1.set_xlabel('epochs')
ax1.set_ylabel('loss')
ax1.legend()
ax2 = fig.add_subplot(1, 2, 2)
ax2.plot(epochs, acc, color='green', label='train_acc')
ax2.plot(epochs, val_acc, color='red', label='val_acc')
ax2.set_title('train and val acc')
ax2.set_xlabel('epochs')
ax2.set_ylabel('acc')
ax2.legend()
<matplotlib.legend.Legend at 0x7fd32a4448b0>
# 모델 평가
model.evaluate(X_test_seq, y_test)
782/782 [==============================] - 9s 11ms/step - loss: 0.3963 - acc: 0.8402
[0.39633744955062866, 0.8401600122451782]
모델 학습 과정에서 손실과 정확도를 시각화해보면 검증 셋에서 약 0.84정도의 정확도를 얻었습니다.
epoch가 약 30에서 과적합이 심해져 조기 종료 되었음을 확인 할 수 있습니다.
테스트 데이터에 모델을 적용한 결과 약 0.84의 정확도의 나쁘지 않은 결과를 얻을 수 있었습니다.
댓글남기기