[혼자공부하는 머신러닝 + 딥러닝] 22~23_순환 신경망
21. 순차 데이터와 순환 신경망
1. 순차 데이터
- 순차데이터는 텍스트나 시계열 데이터와 같이 순서에 의미가 있는 데이터를 말한다.
- 지금까지 살펴본 데이터들은 순서와 상관이 없는 데이터였다. 하지만 이번 시간에 사용하는 데이터인 댓글, 즉 텍스트 데이터는 단어의 순서가 중요한 순차 데이터이다. 이런 데이터는 순서를 유지하며 신경망에 주입해야 한다. 단어의 순서를 마구 섞어서 주입하면 안된다.
- 따라서 순차 데이터를 다룰 때는 이전에 입력한 데이터를 기억하는 기능이 필요하다. 완전 연결 신경망이나 합성곱 신경망은 이런 기억장치가 존재하지 않는다. 완전 연결 신경망이나 합성곱 신경망 같은 데이터의 흐름이 앞으로만 전달되는 신경망을 피드포워드 신경망이라 한다.
- 이전에 처리한 샘플을 다음 샘플을 처리하는데 사용하기 위해서는 데이터의 흐름이 순환해야 한다.
2. 순환 신경망
- 순환 신경망은 일반적인 완전 연결 신경망과 동일한 형태를 가지며, 기존 신경망에 순환하는 고리 하나만 추가하면 된다.
- 순환 신경망에서는 이전에 사용한 샘플에 대한 정보를 가지고 있다. 이런 샘플을 처리하는 단계를 타임스랩이라 한다. 순환 신경망은 이전 타임스텝의 샘플을 기억하지만 타임스탭이 오래되면 순환되는 정보가 희미해진다.
-
순환 신경망에서 층을 셀이라 부른다. 한 셀에는 여러 개의 뉴런이 존재하지만 완전 연결 신경망과 달리 뉴런을 모두 표시하지 않고 하나의 셀로만 층을 표현한다. 또한 셀의 출력을 은닉상태라 부른다.
- 일반적으로 순환 신경망에서 은닉층의 활성화 함수로는 하이퍼볼릭 탄젠트 함수가 사용된다. tanh 함수는 시그모이드 함수와 달리 -1에서 1사이의 범위를 가진다.(시그모이드 함수는 0에서 1사이)
22. 순환 신경망으로 IMDB 리뷰 분류하기
- 대표적인 순환 신경망 문제인 IMDB 리뷰 데이터셋을 사용해 가장 간단한 순환 신경망 모델을 훈련해보도록 하자.
- 해당 데이터셋을 두 가지 방법으로 변형하여 순환 신경망에 주입해보도록 한다. 하나는 원-핫 인코딩, 다른 하나는 단어 임베딩이다.
- 먼저 데이터 셋을 적재해 보도록 하자.
1. IMDB 리뷰 데이터셋
- IMDB 리뷰 데이터는 유명한 영화 데이터베이스인 imdb.com에서 수집한 리뷰를 감상평에 따라 긍정과 부정으로 분류해 놓은 데이터셋이다.
- 텍스트 자체를 신경망에 전달하지 않는다. 컴퓨터는 오로지 숫자 데이터만 처리하기 때문이다. 단어를 숫자 데이터로 변환하는 일반적인 방법은 등장하는 단어에 고유한 정수를 부여하는 것이다. 단어를 분리해서 각 원소별로 숫자를 매칭한다. 이를 토큰이라 한다. 하나의 샘플은 여러개의 분리된 토큰으로 이루어져 있고 1개의 토큰이 하나의 타임스탬프에 해당한다.
- 토큰에 할당하는 정수 중에 몇 개는 특정한 용도로 예약되어 있는 경우가 많다. 예를 들어 0은 패딩. 1은 문장의 시작, 2는 어휘 사전에 없는 토큰을 나타낸다. (어휘사전은 훈련 세트에서 고유한 단어를 뽑아 만든 목록을 말한다.)
- 이제 실제 데이터를 불러와 보도록 하자. 전체 데이터셋에서 가장 자주 등장하는 단어 500개만 추려서 사용해보도록 하자.
from tensorflow.keras.datasets import imdb
from sklearn.model_selection import train_test_split
import numpy as np
import matplotlib.pyplot as plt
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow import keras
(train_input, train_target), (test_input, test_target) = imdb.load_data(num_words=500)
print(train_input.shape, test_input.shape)
(25000,) (25000,)
- 훈련 세트와 테스트 세트 각각 25000개의 샘플로 이루어져 있다. 배열이 1차원으로 이루어져있다. IMDB 리뷰 텍스트는 길이가 제각각입니다. 따라서 고정 크기의 2차원 배열에 담기 보다는 리뷰마다 별도의 파이썬 리스트로 담아야 메모리를 효율적으로 사용할 수 있다.
- 첫 번째 리뷰의 길이를 출력해 보도록 하자.
print(len(train_input[0]))
print(len(train_input[1]))
218
189
- 첫 번째 리뷰의 길이는 218개의 토큰으로 이루어져 있다. 두 번쨰 길이는 189개의 토큰으로 이루어져 있다,
- 하나의 리뷰가 하나의 샘플이 된다. 첫 번쨰 리뷰에 담긴 내용들을 출력해 보자.
print(train_input[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]
- 이미 IMDB의 리뷰 데이터는 정수로 변환이 되어있다. num_words를 500으로 지정했기 때문에 어휘 사전에는 500개의 단어만 들어가 있다. 어휘사전에 들어있지 않은 단어는 모두 2로 표시되어있다.
- 이번에는 타깃 데이터를 출력해보자.
print(train_target[:20])
[1 0 0 1 0 0 1 0 1 0 1 0 0 0 0 0 1 1 0 1]
- 우리가 해결할 문제는 리뷰가 긍정적인지 부정적인지 판단하는 것이다. 그렇다면 이진 분류 문제로 접근할 수 있다. 그래서 타깃값이 부정이면 0 긍정이면 1로 나누어진다.
- 본격적으로 다루기 전에 훈련 세트를 검증세트와 분류를 한다.
train_input, val_input, train_target, val_target = train_test_split(train_input, train_target,
test_size= 0.2, random_state=42)
- 데이터를 훈련 세트와 검증 세트로 분류했다.
- 이제 훈련 세트에 대해 몇 가지 조사를 수행해보자.
- 각 리뷰의 길이를 계싼해 넘파이 배열에 담는다. 이를 이용해 평균적인 리뷰의 길이와 가장 짧은 리뷰의 길이 그리고 가장 긴 리뷰의 길이를 확인하고자 하기 위함이다.
lengths = np.array([len(x) for x in train_input])
print(np.mean(lengths), np.median(lengths))
239.00925 178.0
- 리뷰의 평균 단어 개수는 239개이며 중간값이 178이다. 이를 통해 알 수 있는것은 길이 데이터는 한쪽에 치우친 분포를 보일 것 같다. lengths 배열을 히스토그램으로 표현해보자.
plt.hist(lengths)
plt.xlabel('lengths')
plt.ylabel('frequency')
plt.show()
- 대부분이 길이 300미만에 분포하고 있다. 평균이 중간값보다 높은 이유는 맨 오른쪽에 아주 큰 데이터가 존재하기 때문입니다.
- 리뷰는 대부분 짧기 떄문에 이번 시간에는 중간값보다 훨씬 짧은 100개의 단어만 사용하도록 한다. 100개보다 작은 길이의 리뷰도 존재하기 떄문에 패딩이 필요하다. 보통 패딩을 나타내는 토큰으로는 0을 사용한다.
- 케라스는 시퀀스 데이터의 길이를 맞추는 pad_sequences() 함수를 제공한다. 이 함수를 이용해 길이를 100으로 맞추도록 하자.
train_seq = pad_sequences(train_input, maxlen=100)
print(train_seq.shape)
(20000, 100)
- 기존의 train_input은 파이썬 리스트 배열이였다. 하지만 pad_sequences()를 이용해 길이를 100으로 맞추고 나서 (20000, 100)의 2차원 배열이 되었다.
print(train_seq[0])
[ 10 4 20 9 2 364 352 5 45 6 2 2 33 269 8 2 142 2
5 2 17 73 17 204 5 2 19 55 2 2 92 66 104 14 20 93
76 2 151 33 4 58 12 188 2 151 12 215 69 224 142 73 237 6
2 7 2 2 188 2 103 14 31 10 10 451 7 2 5 2 80 91
2 30 2 34 14 20 151 50 26 131 49 2 84 46 50 37 80 79
6 2 46 7 14 20 10 10 470 158]
- 해당 데이터를 살펴보면 0이 존재하지 않는다. 이는 첫 번쨰 샘플의 길이가 100보다 길다는 것을 의미한다.
- pad_sequences() 함수는 기본적으로 maxlen보다 긴 시퀀스의 앞부분을 자른다. 통상 시퀀스의 앞 부분보다 뒷부분이 더 유용할 것이라 기대하기 떄문이다. 만약 뒷 부분을 자르고 싶다면 pad_sequences()의 truncating 매개변수의 기본값을 ‘pre’가 아닌 ‘post’로 바꾸면 된다.
- 6번째의 샘플을 살펴보면 0이 존재한다. 이는 샘플이 길이가 100이 안된다는 것을 의미한다. 패딩 토큰은 시퀀스의 뒷부분이 아니라 앞부분에 추가한다. 시퀀스 마지막에 있는 단어가 셀의 은닉 상태에 가장 큰 영향을 미치기 때문에 일반적으로 뒷부분에 추가하는것을 선호하지 않는다.
- 검증 세트의 길이도 맞춰주도록 한다.
val_seq = pad_sequences(val_input, maxlen=100)
- 이제 모델 생성을 위한 훈련 세트와 검증 세트 준비를 마쳤다. 이어서 모델을 생성해보자.
2. 순환 신경망 만들기
- 케라스는 여러 종류의 순환층 클래스를 제공한다. 그중 가장 간단한 것은 SimpleRNN 클래스이다. IMDB 데이터는 이진 분류 문제이기 떄문에 마지막 출력층은 1개의 뉴런을 가지고 시그모이드 활성화 함수를 사용해야 한다.
model = keras.Sequential()
model.add(keras.layers.SimpleRNN(8, input_shape=(100, 500)))
model.add(keras.layers.Dense(1, activation='sigmoid'))
2021-10-13 15:27:41.821063: I tensorflow/core/platform/cpu_feature_guard.cc:142] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations: AVX2 FMA
To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.
- 입력 차원을 SimpleRNN을 이용했다. 입력차원을 (100, 500)으로 설정했다. 여기서 첫 번쨰 차원은 길이 제한을 100으로 했기 때문에 똑같이 사용한 것이고, 두 번쨰 차원의 500은 imdb.load_data()함수에서 500개의 단어만 사용하도록 지정했기 때문에 고유한 단어는 모두 500개이다. 따라서 원-핫 인코딩을 수행하기 위해서는 배열의 길이가 500이여야 한다. 또한 활성 함수를 기본값인 하이퍼볼릭 탄젠트 함수를 사용했다.
- 원-핫 인코딩을 수행해보자.
train_oh = keras.utils.to_categorical(train_seq)
print(train_oh.shape)
(20000, 100, 500)
- 정수 하나마다 모두 500차원의 배열로 변경되었기 때문에 (20000, 100)크기의 train_seq가 (20000, 100, 500)이 되었다. 샘플 데이터의 크기가 1차원정수(100,) 배열에서 2차원 배열(100, 500)로 바뀌어야 하기 때문에 input_shape 매개변수 값을 (100, 500)으로 지정한 것이다.
print(train_oh[0][0][:12])
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0.]
- 원-핫 인코딩을 수행하면 한개 숫자만 뺴고 전부 0으로 바뀐다. 같은 방식으로 검증 세트도 원-핫 인코딩을 진행해 준다.
val_oh = keras.utils.to_categorical(val_seq)
- 이제 훈련에 사용할 훈련 세트가 모두 준비되었다. 모델 구조를 살펴보자.
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
_________________________________________________________________
3. 순환 신경망 훈련하기
- 순환 신경망의 훈련은 완전 연결 신경망이나 합성곱 신경망과 크게 다르지 않다.
- 해당 예제에서는 기본 RMSprop의 학습률 0.001을 사용하지 않기 위해 별도의 RMSprop 객체를 만들어 학습률을 0.0001로 지정했다. 그 다음 에포크 횟수를 100으로 늘리고 배치 크기는 64개로 설정했다. 체크포인트와 조기종료를 설정해 에포크가 전부 수행되지 않도록 했다.
rmsprop = keras.optimizers.RMSprop(learning_rate=1e-4)
model.compile(optimizer='RMSprop', loss='binary_crossentropy', metrics=['accuracy'])
checkpoint_cb = keras.callbacks.ModelCheckpoint('best-simplernn-model.h5')
early_stopping_cb = keras.callbacks.EarlyStopping(patience=3, restore_best_weights=True)
history = model.fit(train_oh, train_target, epochs=100, batch_size=64, validation_data=(val_oh, val_target),
callbacks=[checkpoint_cb, early_stopping_cb])
2021-10-13 15:27:45.403541: I tensorflow/compiler/mlir/mlir_graph_optimization_pass.cc:185] None of the MLIR Optimization Passes are enabled (registered 2)
Epoch 1/100
313/313 [==============================] - 17s 49ms/step - loss: 0.6345 - accuracy: 0.6460 - val_loss: 0.5659 - val_accuracy: 0.7304
Epoch 2/100
313/313 [==============================] - 15s 48ms/step - loss: 0.5318 - accuracy: 0.7508 - val_loss: 0.4987 - val_accuracy: 0.7656
Epoch 3/100
313/313 [==============================] - 15s 49ms/step - loss: 0.4764 - accuracy: 0.7807 - val_loss: 0.4914 - val_accuracy: 0.7642
Epoch 4/100
313/313 [==============================] - 15s 48ms/step - loss: 0.4515 - accuracy: 0.7966 - val_loss: 0.4602 - val_accuracy: 0.7846
Epoch 5/100
313/313 [==============================] - 15s 48ms/step - loss: 0.4400 - accuracy: 0.8009 - val_loss: 0.4613 - val_accuracy: 0.7852
Epoch 6/100
313/313 [==============================] - 15s 48ms/step - loss: 0.4306 - accuracy: 0.8039 - val_loss: 0.4597 - val_accuracy: 0.7820
Epoch 7/100
313/313 [==============================] - 15s 48ms/step - loss: 0.4238 - accuracy: 0.8061 - val_loss: 0.4647 - val_accuracy: 0.7870
Epoch 8/100
313/313 [==============================] - 15s 48ms/step - loss: 0.4208 - accuracy: 0.8091 - val_loss: 0.4728 - val_accuracy: 0.7844
Epoch 9/100
313/313 [==============================] - 15s 48ms/step - loss: 0.4172 - accuracy: 0.8102 - val_loss: 0.4693 - val_accuracy: 0.7770
- 이 훈련은 9번째 에포크에서 조기종료 되었다. 검증 세트에 대한 정확도는 81%로 나타났다.
- 이를 훈련 손실과 검증 손실을 그래프를 통해 확인해보자.
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.xlabel('epoch')
plt.ylabel('loss')
plt.legend(['train', 'val'])
plt.show()
- 지속적인 감소추세를 보이다가 3번째 에포크부터 감소추세가 둔화되는 모습을 보인다.
- 입력데이터를 원-핫 인코딩으로 변환하는 과정을 거쳤다. 원-핫 인코딩의 단점은 입력 데이터가 엄청 커진다는 것이다. 실제로 train_seq 배열과 train_oh 배열의 nbyte속성을 출력하여 크기를 확인해보면 엄청 커진 것을 알 수 있다.
print(train_seq.nbytes, train_oh.nbytes)
8000000 4000000000
- 대략 500배가 커진다. 훈련 데이터가 더 큰 경우에는 더 크게 크기가 커져 문제가 될 수 있다. 그렇다면 다른 방법이 있을 것이다.
4. 단어 임베딩을 사용하기
- 순환 신경망에서 텍스트를 처리할 때 즐겨 사용하는 방법은 단어 임베딩이다. 단어 임베딩은 각 단어를 고정된 크기의 실수 벡터로 바꾸어 준다. 단어 임베딩으로 만들어진 벡터는 원-핫 인코딩된 벡터보다 훨씬 의미 있는 값으로 채워져 있기 떄문에 자연어 처리에 더 좋은 성능을 내는 경우가 많다.
- 단어 임베딩의 장점은 입력으로 정수 데이터를 받는다는 것이다. 이를 통해 메모리를 더 효율적으로 사용할 수 있다.
- Embedding 클래스를 SimpleRNN 층 앞에 추가한 두 번쨰 순환 신경망을 만들어보자.
model = keras.Sequential()
model.add(keras.layers.Embedding(500, 16, input_length=100))
model.add(keras.layers.SimpleRNN(8))
model.add(keras.layers.Dense(1, activation='sigmoid'))
- Embedding 클래스의 첫 번쨰 매개변수는 어휘 사전의 크기를 나타내고, 두 번쨰 매개변수는 임베팅 벡터의 크기를 나타낸다. 세 번쨰 매개변수 input_shape는 입력 시퀀스의 길이를 말한다.
- 나머지는 동일하다. 구조를 한번 살펴보고, 모델을 훈련시켜 결과를 확인해 보자.
model.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
_________________________________________________________________
rmsprop = keras.optimizers.RMSprop(learning_rate=1e-4)
model.compile(optimizer='RMSprop', loss='binary_crossentropy', metrics=['accuracy'])
checkpoint_cb = keras.callbacks.ModelCheckpoint('best-embedding-model.h5')
early_stopping_cb = keras.callbacks.EarlyStopping(patience=3, restore_best_weights=True)
history = model.fit(train_seq, train_target, epochs=100, batch_size=64,
validation_data=(val_seq, val_target),
callbacks=[checkpoint_cb, early_stopping_cb])
Epoch 1/100
313/313 [==============================] - 11s 30ms/step - loss: 0.6213 - accuracy: 0.6428 - val_loss: 0.5872 - val_accuracy: 0.6910
Epoch 2/100
313/313 [==============================] - 9s 28ms/step - loss: 0.5078 - accuracy: 0.7615 - val_loss: 0.5984 - val_accuracy: 0.6900
Epoch 3/100
313/313 [==============================] - 9s 29ms/step - loss: 0.4669 - accuracy: 0.7891 - val_loss: 0.5247 - val_accuracy: 0.7496
Epoch 4/100
313/313 [==============================] - 9s 28ms/step - loss: 0.4431 - accuracy: 0.7984 - val_loss: 0.5121 - val_accuracy: 0.7492
Epoch 5/100
313/313 [==============================] - 9s 28ms/step - loss: 0.4269 - accuracy: 0.8077 - val_loss: 0.4578 - val_accuracy: 0.7896
Epoch 6/100
313/313 [==============================] - 9s 29ms/step - loss: 0.4216 - accuracy: 0.8099 - val_loss: 0.4903 - val_accuracy: 0.7892
Epoch 7/100
313/313 [==============================] - 9s 29ms/step - loss: 0.4133 - accuracy: 0.8130 - val_loss: 0.4611 - val_accuracy: 0.7818
Epoch 8/100
313/313 [==============================] - 9s 29ms/step - loss: 0.4074 - accuracy: 0.8191 - val_loss: 0.4578 - val_accuracy: 0.7942
Epoch 9/100
313/313 [==============================] - 9s 29ms/step - loss: 0.3977 - accuracy: 0.8256 - val_loss: 0.4673 - val_accuracy: 0.7760
Epoch 10/100
313/313 [==============================] - 9s 28ms/step - loss: 0.3915 - accuracy: 0.8284 - val_loss: 0.4568 - val_accuracy: 0.7950
Epoch 11/100
313/313 [==============================] - 9s 29ms/step - loss: 0.3856 - accuracy: 0.8282 - val_loss: 0.4751 - val_accuracy: 0.7928
Epoch 12/100
313/313 [==============================] - 9s 30ms/step - loss: 0.3834 - accuracy: 0.8299 - val_loss: 0.4754 - val_accuracy: 0.7684
Epoch 13/100
313/313 [==============================] - 9s 29ms/step - loss: 0.3796 - accuracy: 0.8357 - val_loss: 0.4711 - val_accuracy: 0.7820
- 결과를 살펴보면 원-핫 인코딩을 사용한 모델보다 조금 더 나은성능을 냈고 순환층의 가중치 개수도 줄고 훈련세트 크기도 훨씬 줄어들었다.
- 손실 그래프를 출력해보자.
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.xlabel('epoch')
plt.ylabel('loss')
plt.legend(['train', 'val'])
plt.show()
- 검증 손실이 감소되지 않아 훈련이 조기종료 되었음을 알 수 있다. 이에 비해 훈련 손실을 계속 감소한다. 다음시간에는 이를 개선할 수 있는 방법에 대해 알아보도록 하겠다.
Leave a comment