혼자 공부하는 머신러닝+딥러닝 책을 바탕으로 공부한 내용입니다.
CH9 텍스트를 위한 인공 신경망 ①②
케라스 API로 순환 신경망 구현하기
텐서플로를 사용해 순환 신경망을 만들어 영화 리뷰 데이터셋에 적용해 리뷰를 긍정과 부정으로 분류해보자
▶ 09-1장
순환 신경망의 개념을 설명한 단원이라 키워드만 정리하고 넘어가겠습니다
- 순차 데이터
- 텍스트나 시계열 데이터와 같이 순서에 의미가 있는 데이터
- 대표적으로 글, 대화, 일자별 날씨, 일자별 판매 실적 등을 예로 들 수 있음
- 순환 신경망
- 순차 데이터에 잘 맞는 인공 신경망의 한 종류
- 순차 데이터를 처리하기 위해 고안된 순환층을 1개 이상 사용한 신경망
- 순환 신경망에서는 종종 순환층을 셀이라 부름 - 하나의 셀은 여러 개의 뉴런으로 구성
- 순환 신경망에서는 셀의 출력을 은닉 상태라고 부름 - 은닉 상태는 다음 층으로 전달될 뿐만 아니라 셀이 다음 타임스텝의 데이터를 처리할 때 재사용됨
▶ IMDB 리뷰 데이터셋
IMDB 리뷰 데이터셋은 유명한 인터넷 영화 데이터베이스에서 수집한 리뷰를 감상평에 따라 긍정과 부정으로 분류해 놓은 데이터셋이다.
총 50,000개의 샘플로 이루어져 있고 훈련 데이터와 테스트 데이터에 각각 25,000개씩 나눠져있다.
- 말뭉치 - 자연어 처리에서 사용하는 훈련 데이터셋을 일컫는다.
- 토큰 - 텍스트에서 공백으로 구분되는 문자열을 말한다. 종종 소문자로 변환하고 구둣점은 삭제함
먼저, 데이터셋을 불러오자
from tensorflow.keras.datasets import imdb
(train_input, train_target), (test_input, test_target) = imdb.load_data(
num_words=500)
# 크기 확인
print(train_input.shape, test_input.shape) #(25000,) (25000,)
- 전제 데이터셋에 가장 자주 등장하는 단어 500개만 불러왔다.
print(len(train_input[0])) #218
print(len(train_input[1])) #189
첫 번째 리뷰의 길이는 218, 두 번째 리뷰의 길이는 189로 리뷰마다 길이가 다른 것을 확인할 수 있다.
첫 번째 리뷰에 담긴 내용을 출력해보면 다음과 같다
print(train_input[0])
- 어휘 사전에는 앞에서 지정했듯이 500개의 단어만 들어가 있으므로, 어휘 사전에 없는 단어는 모두 2로 표시되어 나타난다.
이번에는 타깃 데이터를 출력해보자
print(train_target[:20])
리뷰가 0(부정)인지 1(긍정)인지 분류하는 이진 분류 문제이다.
# 훈련, 검증 set
from sklearn.model_selection import train_test_split
train_input, val_input, train_target, val_target = train_test_split(
train_input, train_target, test_size=0.2, random_state=42)
훈련 세트에서 20%를 검증 세트로 떼어주었다.
이제 훈련 세트에 대해 확인해보자. 먼저, 각 리뷰의 길이를 확인해볼 것이다.
import numpy as np
lengths = np.array([len(x) for x in train_input])
print(np.mean(lengths), np.median(lengths)) #239.00925 178.0
# 리뷰 길이
import matplotlib.pyplot as plt
plt.hist(lengths)
plt.xlabel('length')
plt.ylabel('frequency')
plt.show()
- 리뷰의 평균 단어 개수는 239개, 중간값은 178개로 리뷰 길이가 한쪽으로 치우친 것을 알 수 있다.
- 리뷰가 대부분 짧기 때문에 우리는 100개의 단어만 사용할 것이다.
- 리뷰들의 길이를 100으로 맞추기 위해서는 패딩을 사용하면 된다.
# 길이를 100으로 맞추기
from tensorflow.keras.preprocessing.sequence import pad_sequences
train_seq = pad_sequences(train_input, maxlen=100)
print(train_seq.shape) #(20000, 100)
- maxlen에 원하는 길이를 지정하면 이보다 길 때는 잘라내고 짧은 경우에는 0으로 패딩한다.
train_seq의 첫 번째 샘플을 출력해보면 다음과 같다.
print(train_seq[0])
- 샘플의 앞뒤에 패딩값 0이 없는 것으로 보아 100보다 긴 길이를 잘라낸 것으로 보인다.
앞부분, 뒷부분 중 어느쪽을 잘라냈는지 확인하기 위해 train_input의 끝 부분을 확인해보자
print(train_input[0][-10:])
- 끝부분이 일치하는 것을 보아 pad_sequences() 함수는 maxlen보다 긴 시퀀스의 앞부분을 자른다는 것을 알 수 있다.
- 뒷부분을 자르고 싶다면 truncating 매개변수의 값을 post로 바꾸면 된다.
print(train_seq[5])
- 이 샘플의 길이는 100이 안되어 앞부분을 0으로 패딩한 것을 확인할 수 있다.
검증 세트의 길이도 100으로 맞춰 훈련 세트와 검증 세트 준비를 마치자!
# 검증 세트 길이도 100으로 맞추기
val_seq = pad_sequences(val_input, maxlen=100)
▶ 순환 신경망 만들기
IMDB 리뷰 분류 문제는 이진 분류이므로 마지막 출력층은 1개의 뉴런을 가지고 시그모이드 활성화 함수를 사용해야 한다.
또, 이 정수들 사이에는 어떠한 관련이 없기 때문에 정숫값에 있는 크기 속성을 없애고 각 정수를 고유하게 표현하는 방법인 원-핫 인코딩을 사용할 것이다.
- 원-핫 인코딩
- 어떤 클래스에 해당하는 원소만 1이고 나머지는 모두 0인 벡터
- 정수로 변환된 토큰을 원-핫 인코딩으로 변환하려면 어휘 사전 크기의 벡터가 만들어짐
from tensorflow import keras
model = keras.Sequential()
model.add(keras.layers.SimpleRNN(8, input_shape=(100, 500)))
model.add(keras.layers.Dense(1, activation='sigmoid'))
우리가 위에서 지정했기 때문에 훈련 데이터에 포함될 수 있는 정숫값의 범위는 0에서 499까지이다.
따라서 이 범위를 원-핫 인코딩으로 표현하려면 배열의 길이가 500이어야 한다.
train_seq을 원-핫 인코딩으로 변환하여 train_oh 배열을 만들고 크기를 출력해보면
# train_seq 원핫 인코딩
train_oh = keras.utils.to_categorical(train_seq)
print(train_oh.shape) #(20000, 100, 500)
- 정수 하나마다 모두 500차원의 배열로 변경되었으므로 크기가 (20000, 100, 500)로 바뀌었다.
train_oh의 첫 번째 샘플의 첫 번째 토큰 10이 잘 인코딩 되었는지 출력해보면
print(train_oh[0][0][:12]) #[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0.]
print(np.sum(train_oh[0][0])) #1.0
- 11번째 원소만 1이고 나머지는 0으로 원-핫 인코딩이 잘 되었다.
이제, val_seq도 원-핫 인코딩으로 바꾸고 모델의 구조를 출력해보자
# val_seq 원핫 인코딩
val_oh = keras.utils.to_categorical(val_seq)
model.summary()
- 이전에 만들었던 완전 연결 신경망에서 바뀐 것은 Dense 층 대신 SimpleRNN 층을 사용하고 입력 데이터의 차원을 원-핫 인코딩으로 바꿔준 것이다
▶ 순환 신경망 훈련하기
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',
save_best_only=True)
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])
- 이 훈련은 39번째 에포크에서 조기 종료 되었다.
- 검증 세트에 대한 정확도는 약 80% 정도이다.
훈련 손실과 검증 손실을 그래프로 그려서 살펴보자
# 훈련 손실, 검증 손실
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.xlabel('epoch')
plt.ylabel('loss')
plt.legend(['train', 'val'])
plt.show()
- 훈련 손실은 꾸준히 감소하고 있지만 검증 손실은 대략 20번째 에포크에서 감소가 둔해지고 있는 것으로 보아 적절한 에포크에서 훈련을 멈춘 것으로 보인다.
우리가 지금까지 사용했던 원-핫 인코딩의 단점은 입력 데이터가 매우 커진다는 것이다.
print(train_seq.nbytes, train_oh.nbytes) #8000000 4000000000
- 토큰 1개를 500차원으로 늘렸기 때문에 대략 500배가 커진다.
다음으로는 순환 신경망에 사용하는 더 좋은 단어 표현 방법을 사용해보자
▶ 단어 임베딩을 사용하기
- 단어 임베딩
- 정수로 변환된 토큰을 비교적 작은 크기의 실수 밀집 벡터로 변환하는 것
- 이런 밀집 벡터는 단어 사이의 관계를 표현할 수 있기 때문에 자연어 처리에서 좋은 성능을 발휘함
- 단어 임베딩의 장점은 입력으로 정수 데이터를 받는다는 것
입력으로 정수 데이터를 받기 때문에 train_oh가 아니라 train_seq를 사용할 수 있어 메모리를 훨씬 효율적으로 사용할 수 있다.
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()
- 두 번째 매개변수 16은 임베딩 벡터의 크기이다. - 원핫 인코딩보다 훨씬 작은 크기의 벡터를 사용
- 샘플의 길이를 앞에서 100으로 맞췄기 때문에 input_length=100이다.
이제 모델을 훈련해 출력 결과를 보면
# 모델 훈련
rmsprop = keras.optimizers.RMSprop(learning_rate=1e-4)
model2.compile(optimizer=rmsprop, loss='binary_crossentropy',
metrics=['accuracy'])
checkpoint_cb = keras.callbacks.ModelCheckpoint('best-embedding-model.h5',
save_best_only=True)
early_stopping_cb = keras.callbacks.EarlyStopping(patience=3,
restore_best_weights=True)
history = model2.fit(train_seq, train_target, epochs=100, batch_size=64,
validation_data=(val_seq, val_target),
callbacks=[checkpoint_cb, early_stopping_cb])
- 순환층의 가중치 개수는 훨씬 작고 훈련 세트 크기도 훨씬 줄어들었다.
마지막으로 훈련 손실과 검증 손실의 그래프를 출력해보면
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.xlabel('epoch')
plt.ylabel('loss')
plt.legend(['train', 'val'])
plt.show()
- 검증 손실이 더 감소되지 않아 훈련이 적절하게 조기 종료 된 것을 확인할 수 있다.
- 이에 비해 훈련 손실은 계속 감소하는데 이를 개선할 방법에 대해서는 다음 장에서 알아보자
▶ 진행한 순서
- 첫번째는 입력 데이터를 원-핫 인코딩으로 변환하여 순환층에 직접 주입해봤다.
- 두번째는 정수 시퀀스를 그대로 사용하기 위해 모델 처음에 Embedding 층을 추가했다.
- 단어 임베딩은 단어마다 실수로 이루어진 밀집 벡터를 학습하기 때문에 단어를 풍부하게 표현할 수 있다.
'Python > ML & DL 공부' 카테고리의 다른 글
[DL] 09-3 LSTM과 GRU 셀 (0) | 2022.04.07 |
---|---|
[DL] 08-3 합성곱 신경망의 시각화 (0) | 2022.04.06 |
[DL] 08-2 합성곱 신경망을 사용한 이미지 분류 (+ 08-1) (0) | 2022.04.06 |
[DL] 07-3 신경망 모델 훈련 (0) | 2022.03.27 |
[DL] 07-2 심층 신경망 (0) | 2022.03.23 |