Date:     Updated:

카테고리:

태그: , , ,


💡 ‘혼자 공부하는 머신러닝 딥러닝(박해선 저)’ 책을 읽고 공부한 내용을 요약한 페이지입니다.
책에 나오는 코드를 그대로 쓰지 않고, 코드와 파라미터를 변형하고 조정해가며 다른 결과를 출력하며 공부했습니다.


09 텍스트를 위한 인공 신경망

09-1 순차 데이터와 순환 신경망

순차 데이터(sequential data)텍스트시계열 데이터(time series data)와 같이 순서에 의미가 있는 데이터를 말한다. 순차 데이터의 순서는 뒤섞이면 안된다. 즉 순차 데이터를 다룰 때는 이전에 입력한 데이터를 기억하는 기능이 필요하다.

완전 연결 신경망이나 합성곱 신경망은 이런 기억 장치가 없다. 이렇게 데이터의 흐름이 앞으로만 전달되는 신경망을 피드포워드 신경망(FFNN)이라고 한다.

반면 이전에 처리한 샘플을 재사용하는 신경망을 순환 신경망(recurrent neural network, RNN)이라고 한다. 이는 완전 연결 신경망에 순환하는 고리 하나만 추가한 형상이다.

A,B,C를 순차적으로 처리하는 순환 신경망이 있다고 가정하자. 첫번째 샘플 A를 처리하고 난 출력 OA가 다시 뉴런으로 돌아간다. 그 다음 샘플 B를 처리할 땐 OA와 B가 함께 처리되어 OB가 만들어진다. 이 때 OB엔 A의 정보가 일정량 포함되어 있다. 그 다음 샘플 C를 처리할 땐 OB가 함께 쓰인다. 그렇게 만들어진 OC에는 B와 A의 정보가 포함되어있다. 물론 직전에 쓰인 B가 A보단 정보량이 많다.

이 때 샘플을 처리하는 한 단계를 타임스텝(time step)이라고 한다. 타임스텝이 오래될수록 순환되는 정보는 희미해진다.

순환 신경망에서는 층을 셀(cell)이라고 부른다. 셀의 출력은 은닉 상태(hidden state)라고 부른다.

은닉층의 활성화함수로는 하이퍼볼릭 탄젠트 함수가 사용된다. 시그모이드 함수와 유사하지만, -1~1의 범위를 갖는다.

파란색이 시그모이드, 빨간색이 하이퍼볼릭 탄젠트 함수.

순환 신경망의 뉴런은 가중치가 하나 더 있는데, 바로 이전 타임스텝의 은닉 상태에 곱해지는 가중치이다.

순환층은 기본적으로 마지막 타임스텝의 은닉 상태만 출력으로 내보낸다.

합성곱 신경망과 다른 점은 마지막 셀의 출력이 1차원이기 때문에 굳이 Flatten 클래스로 펼칠 필요가 없고, 셀의 출력을 그대로 밀집층에 사용할 수 있다.

09-1 핵심 키워드

  • 순차 데이터: 텍스트나 시계열 데이터와 같이 순서에 의미가 있는 데이터
  • 순환 신경망: 순차 데이터에 잘 맞는 인공 신경망의 한 종류.
  • 셀: 순환 신경망에서 순환층을 일컫는 말.
  • 은닉 상태: 순환 신경망에서 셀의 출력을 일컫는 말.


09-2 RNN으로 IMDB 리뷰 분류하기

IMDB 리뷰 데이터셋은 유명한 인터넷 영화 데이터베이스인 imdb.com에서 수집한 리뷰를 감상평에 따라 긍정과 부정으로 분류해 놓은 데이터셋이다.

텍스트 자체를 신경망에 전달하는 것이 아니라, 단어마다 고유한 정수를 부여한다. 이 때 0은 패딩, 1은 문장의 시작, 2는 어휘 사전에 없는 토큰으로 미리 부여가 되어있다. 이렇게 분리된 단어를 토큰(token)이라고 부른다. 하나의 샘플은 여러 개의 토큰으로 이루어져 있고, 1개의 토큰이 하나의 타임스텝에 해당한다.

from tensorflow.keras.datasets import imdb
(train_input, train_target), (test_input, test_target) = imdb.load_data(num_words=500)
#num_words매개변수를 500으로 하여 자주 등장하는 단어 500개만 사용

print(len(train_input[0])) #첫번째 리뷰의 길이
#결과값
218

print(train_target[:20]) #타깃 20개 출력
#결과값
[1 0 0 1 0 0 1 0 1 0 1 0 0 0 0 0 1 1 0 1] #0은 부정, 1은 긍정

#검증세트 분리
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)

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 #평균 단어 수는 239개, 중간값은 178개.

평균값이 239, 중간값이 178인 것으로 보아 이 리뷰 길이 데이터는 한쪽에 치우쳤을 것이다. 히스토그램을 확인해보자.

대부분이 300 미만인 것을 확인할 수 있다.

리뷰의 길이가 100 미만인 것만 사용해도 충분할 것으로 보인다. 길이가 100을 넘으면 100에 맞추어 잘라내고, 100보다 짧으면 토큰 0으로 패딩을 해준다. 케라스는 시퀀스 데이터의 길이를 맞추는 pad_sequences()함수를 제공한다.

from tensorflow.keras.preprocessing.sequence import pad_sequences
train_seq = pad_sequences(train_input, maxlen=100) #maxlen매개변수에 원하는 길이를 지정
val_seq = pad_sequences(val_input, maxlen=100) #검증세트의 길이도 100으로

print(train_seq.shape)
#결과값
(20000, 100)  #토큰 100개의 샘플이 20,000개 있다.

시퀀스의 마지막에 있는 단어가 셀의 은닉상태에 가장 큰 영향을 미치므로 마지막에 패딩을 추가하는 것은 선호되지 않는다. 때문에 자동으로 패딩은 앞에 위치한다.

이제 순환 신경망을 만들어보자. 케라스는 simpleRNN 클래스를 제공한다.

from tensorflow import keras
model = keras.Sequential()
model.add(keras.layers.SimpleRNN(8,input_shape=(100,500))) 
#Dense나 Conv2D 클래스 대신 SimpleRNN 사용
#activation의 기본값은 tanh이다.
model.add(keras.layers.Dense(1, activation='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
_________________________________________________________________

또한 토큰에 부여된 정수가 크기의 의미를 지녀선 안 된다. 100이 부여된 단어가 5가 부여된 단어보다 20배 중요한 것이 아니다. 따라서 정숫값에 있는 크기 속성을 없애고 각 정수를 고유하게 표현하기 위해 원-핫인코딩을 해준다.

#원핫인코딩
train_oh = keras.utils.to_categorical(train_seq)
val_oh = keras.utils.to_categorical(val_seq)
#순환 신경망 훈련
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])

#결과
Epoch 36/100 #36번째 에포크에서 중단
313/313  15s 47ms/step - loss: 0.4060 - accuracy: 0.8189 - val_loss: 0.4488 - val_accuracy: 0.7944

하지만 이 작업에서 원-핫 인코딩으로 변환하면 입력 데이터가 매우 커진다. 토큰 1개가 500차원으로 늘어났기 때문이다.

때문에 원-핫인코딩보다도 단어 임베딩(word embedding)을 즐겨 사용한다. 단어 임베딩은 각 단어를 고정된 크기의 실수 벡터로 바꿔 준다. 이는 원-핫 인코딩보다 훨씬 의미 있는 값으로 채워져 있기에 더 좋은 성능을 낼 수 있다.

model2=keras.Sequential()
model2.add(keras.layers.Embedding(500, 16, input_length=100))
#첫번째 매개변수는 어휘 사전의 크기
#두번째 매개변수는 임베딩 벡터의 크기. 원-핫인코딩은 500개인데, 여기선 16개밖에 안 된다
#세번째 매개변수는 입력 시퀀스의 길이.
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
_________________________________________________________________
#단어임베딩 순환신경망 훈련
rmsprop = keras.optimizers.RMSprop(learning_rate=1e-4)
model2.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 = model2.fit(train_seq, train_target, epochs=100, batch_size=64, validation_data=(val_seq, val_target), callbacks=[checkpoint_cb, early_stopping_cb])

#결과
Epoch 40/100
313/313 - 8s 24ms/step - loss: 0.3829 - accuracy: 0.8396 - val_loss: 0.4562 - val_accuracy: 0.7870

09-2 핵심 키워드

  • 토큰: 텍스트에서 공백으로 구분되는 문자열
  • 원-핫 인코딩: 어떤 클래스에 해당하는 원소만 1이고 나머지는 모두 0인 벡터
  • 단어 임베딩: 정수로 변환된 토큰을 비교적 작은 크기의 실수 밀집 벡터로 변환하는 방법

09-2 핵심 패키지와 함수

  • TensorFlow
    • pad_sequences(): 시퀀스 길이를 맞추기 위해 패딩을 추가
      • maxlen매개변수로 원하는 시퀀스 길이 지정
      • padding매개변수는 패딩을 추가할 위치를 지정. 기본값인 ‘pre’는 시퀀스 앞에 패딩을 추가
    • to_categorical(): 정수 시퀀스를 원-핫 인코딩으로 변환
    • SimpleRNN: 케라스의 기본 순환층 클래스
      • 첫번째 매개변수에 뉴런의 개수 지정
      • activation매개변수에 활성화함수 지정. 기본값은 tanh
      • dropout매개변수에 입력에 대한 드롭아웃 비율 지정
    • Embedding: 단어 임베딩을 위한 클래스
      • 첫번째 매개변수에 어휘 사전의 크기 지정
      • 두번째 매개변수에 출력할 밀집 벡터의 크기 지정
      • input_length매개변수에 입력 시퀀스의 길이 지정


09-3 LSTM

LSTM은 Long Short-Term Memory의 약자로, 단기 기억을 오래 기억하기 위해 고안된 셀이다.

LSTM에는 은닉상태 말고도 셀 상태라고 부르는 값이 있다. 이 셀 상태는 다음 층으로 전달되지 않고 LSTM 셀에서 순환만 되는 값이다.

LSTM의 대략적 구조

삭제 게이트는 셀 상태에 있는 정보를 제거하는 역할을 하고 입력 게이트는 새로운 정보를 셀 상태에 추가하며 출력 게이트를 통해서 이 셀 상태가 다음 은닉 상태로 출력된다.

이제 LSTM 신경망을 훈련해보자

from tensorflow.keras.datasets import imdb
from sklearn.model_selection import train_test_split
(train_input, train_target), (test_input, test_target) = imdb.load_data(num_words=500)
train_input, val_input, train_target, val_target = train_test_split(train_input, train_target, test_size=0.2, random_state=42)

from tensorflow.keras.preprocessing.sequence import pad_sequences
#길이를 100으로 맞추어 패딩
train_seq = pad_sequences(train_input, maxlen=100)
val_seq = pad_sequences(val_input, maxlen=100)

순환층에서 SimpleRNN 클래스를 LSTM 클래스로 바꿔주기만 하면 된다.

#순환층 만들기 
from tensorflow import keras
model = keras.Sequential()
model.add(keras.layers.Embedding(500,16,input_length=100))
model.add(keras.layers.LSTM(8))
model.add(keras.layers.Dense(1,activation='sigmoid'))
Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 embedding (Embedding)       (None, 100, 16)           8000      
                                                                 
 lstm (LSTM)                 (None, 8)                 800       
                                                                 
 dense (Dense)               (None, 1)                 9         
                                                                 
=================================================================
Total params: 8,809
Trainable params: 8,809
Non-trainable params: 0
_________________________________________________________________

SimpleRNN 클래스의 모델 파라미터 개수는 200개였다. LSTM 셀에는 작은 셀이 4개 있으므로 정확히 4배 늘어 모델 파라미터 수가 800개이다.

이제 모델을 컴파일하고 훈련한 다음 훈련손실과 검증 손실 그래프를 그려보자.

rmsprop = keras.optimizers.RMSprop(learning_rate=1e-4)
model.compile(optimizer = rmsprop, loss='binary_crossentropy', metrics=['accuracy'])
checkpoint_cb = keras.callbacks.ModelCheckpoint('best-lstm-model.h5', save_best_only=True)
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 27/100
313/313 - 12s 38ms/step - loss: 0.4164 - accuracy: 0.8139 - val_loss: 0.4396 - val_accuracy: 0.8024
import matplotlib.pyplot as plt
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.xlabel('epoch')
plt.ylabel('loss')
plt.legend(['train','val'])
plt.show()

기본 순환층보다 LSTM이 과대적합을 잘 억제하면서 훈련을 잘 수행한 것으로 보인다.