19 분 소요

본 글은 ‘시작하세요! 텐서플로 2.0 프로그래밍’ 을 바탕으로 작성되었습니다.

Chapter 7. RNN

순환 신경망 Recurrent Neural Network: RNN은 지금까지 살펴본 네트워크와는 입력을 받아들이고 처리하는 방식에서 큰 차이가 있습니다. 순환 신경망은 순서가 있는 데이터를 입력으로 받고 같은 네트워크를 이용해 변화하는 입력에 대한 출력을 얻어냅니다.

순환 신경망의 구조

지금까지 살펴본 딥러닝 네트워크의 구조는 출력 -> 네트워크 -> 입력의 단방향 구조입니다. 반면 순환 신경망은 입력을 받아서 출력을 반환하는 것은 동일하지만 되먹임 구조를 가지고 있습니다. 되먹임 구조는 어떤 레이어의 출력을 다시 입력으로 받는 것을 의미합니다.

image

Figure 7.1 순환 신경망의 구조

순환 신경망의 구조를 풀어보면 위와 같습니다. 입력이 변할 때 같은 네트워크를 사용해 각각 다른 출력을 내보내고 있습니다. 이때 중요한 점은 출력값이 다음 입력을 받을 때의 RNN 네트워크에도 동일하게 전달되고 있다는 것입니다. 처음에는 $X_1$을 입력으로 받아 출력 $Y_1$을 내보내게 되고, 그 다음에는 다시 $Y_1$을 입력 $X_2$로 받는 구조입니다. 이 과정에서 네트워크는 동일하게 사용됩니다.

image

Figure 7.2 순환 신경망의 다양한 형태

순환 신경망은 입력과 출력의 길이에 제한이 없습니다. 따라서 위와 같은 형태의 다양한 네트워크를 만들 수 있습니다.

지금까지 순환 신경망의 구조를 알아보았습니다. 이제 순환 신경망을 구성하는 각 레이어에 대해 알아보겠습니다.

주요 레이어 정리

SimpleRNN 레이어

순환 신경망의 가장 기초적인 레이어는 SimpleRNN 레이어입니다. 레이어의 구조는 다음과 같습니다.

image

Figure 7.3 SimpleRNN

$x_t$는 입력을 나타내고 $h_{t-1}, h_t$ 등은 SimpleRNN 레이어의 출력을 나타냅니다. $U$와 $W$는 입력과 출력에 곱해지는 가중치입니다. 단계 $t$에서의 SimpleRNN 출력은 다음 수식으로 나타낼 수 있습니다.

\[h_t = \tanh (U_{x_t} + Wh_{t-1})\]

활성화 함수로는 $\tanh$가 쓰입니다. $\tanh$는 실수 입력을 받아 -1에서 1 사이의 출력값을 반환하며, 다른 활성화 함수를 쓸 수도 있습니다. 지금까지 배운 레이어처럼 SimpleRNN 레이어도 단 한줄로 간단하게 생성할 수 있습니다.

SimpleRNN 레이어 생성 코드

units는 레이어에 존재하는 뉴런의 수이며, return_sequences는 출력으로 시퀀스 전체를 출력할지 여부를 나타내는 옵션으로 주로 여러 개의 RNN 레이어를 쌓을 때 쓰입니다. 그럼 간단한 예제를 살펴보겠습니다. 시퀀스를 구성하는 앞쪽 4개의 숫자가 주어졌을 때 그 다음에 올 숫자를 예측하는 간단한 시퀀스 예측 모델을 만들어보겠습니다.

예제 데이터 생성 코드

그 다음으로는 SimpleRNN 레이어를 사용한 네트워크를 정의합니다. 모델 구조는 지금까지 살펴본 시퀀셜 모델이고 출력을 위한 Dense 레이어가 뒤에 추가되어 있습니다.

시퀀스 예측 모델 정의

Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
simple_rnn_1 (SimpleRNN)     (None, 10)                120       
_________________________________________________________________
dense (Dense)                (None, 1)                 11        
=================================================================
Total params: 131
Trainable params: 131
Non-trainable params: 0
_________________________________________________________________

위에서 주목해야 할 점은 input_shape 입니다. 여기서 [4,1]은 각각 timesteps, input_dim을 나타냅니다. timesteps란 순환 신경망이 입력에 대해 계산을 반복하는 횟수이고 input_dim은 입력 벡터의 크기를 나타냅니다. X는 [1,4,1] 차원의 벡터이고 가장 첫 차원은 배치 차원이기 때문에 생략하고, 두 번째의 4는 타임스텝, 세 번째의 1은 input_dim이 됩니다.

시퀀스 예측 모델은 4 타임스텝에 걸쳐 입력을 받고 마지막 출력값을 다음 레이어로 반환합니다. 우리가 추가한 Dense 레이어에는 별도의 활성화함수가 없어서 바로 출력을 반환하게 됩니다. 그리고 이 출력과 실제값과의 차이가 평균 제곱 오차가 됩니다. 이제 훈련을 시켜보도록 하겠습니다.

네트워크 훈련 및 결과 확인

[[0.40528154]
 [0.5267162 ]
 [0.6284463 ]
 [0.7101967 ]
 [0.77369976]
 [0.8215594 ]]

얼추 비슷하게 예측하고 있는 것 같습니다. 이제 학습 과정에서 본 적이 없는 테스트 데이터를 넣어보도록 하겠습니다.

학습되지 않은 시퀀스에 대한 예측 결과

[[0.8564883]]
[[0.26754123]]

1.0을 예측해야 하는 데이터의 출력으로는 0.856, 0.3을 예측해야 하는 데이터의 출력으로는 0.3454를 내놓았습니다. 아쉽지만 학습이 진행되었다는 점은 확인할 수 있었습니다.

SimpleRNN 레이어는 순환 신경망의 가장 간단한 형태입니다. 실제로는 이 레이어의 단점을 개선한 LSTM 레이어와 GRU 레이어가 많이 쓰입니다.

LSTM 레이어

SimpleRNN 레이어의 치명적인 단점은 입력 데이터가 길어질수록 학습 능력이 떨어진다는 점입니다. 이를 장기의존성 Long-Term Dependency 문제라고 하며 입력 데이터와 출력 사이의 길이가 멀어질수록 연관 관계가 적어집니다. 현재와 과거의 시점이 너무 멀어지면 문제를 풀기 힘들어지는 것입니다.

장기 의존성 문제를 해결하기 위한 구조로 LSTM Long Short Term Memory가 제안됐습니다. LSTM은 RNN에 비해 복잡한 구조를 가지고 있는데 가장 큰 특징은 출력 외에 LSTM 셀 사이에서만 공유되는 셀 상태 cell state를 가지고 있다는 것입니다. LSTM을 그림으로 나타내면 다음과 같습니다.

image

Figure 7.4 LSTM

여기서 $C_{t-1}$과 $C_t$가 바로 셀 상태를 나타내는 기호입니다. LSTM에서는 RNN과 다르게 $h_t$뿐만 아니라 $C_t$도 같이 전달되고 있습니다. 이처럼 타임스텝을 가로지르며 셀 상태가 보존되어 장기의존성 문제를 일부분 완화시킨 것이 LSTM의 핵심 아이디어입니다.

LSTM 레이어는 활성화 함수로 $\tanh$ 외에 시그모이드 함수도 쓰였습니다. 시그모이드 함수는 항상 0에서 1 범위의 출력을 냅니다. 이러한 출력의 특성 때문에 정보가 통과하는 게이트의 역할을 하게 됩니다. 출력이 0이면 입력한 정보를 하나도 통과시키지 않는 것이고 1이면 그대로 통과시키게 됩니다.

타임스텝 $t$에서의 LSTM의 출력은 다음과 같습니다.

\[i_t = \text{sigmoid}(x_tU^i + h_{t-1}W^t) \\ f_t = \text{sigmoid}(x_tU^f + h_{t-1}W^f) \\ o_t = \text{sigmoid}(x_tU^o + h_{t-1}W^o) \\ \widetilde{C_{t}} = \tanh(x_tU^{\widetilde{C_{t}}} + h_{t-1}W^{\widetilde{C_{t}}}) \\\]

$U$와 $W$는 SimpleRNN과 마찬가지로 입력과 출력에 곱해지는 가중치이며 $i_t, f_t, o_t$는 각각 타임스텝 $t$에서의 Input, Forget, Output 게이트를 통과한 출력을 의미합니다. $\widetilde{C_t}$는 SimpleRNN에서도 존재하던 $x_t$와 $h_{t-1}$을 각각 $U$와 $W$로 곱한 뒤에 $\tanh$ 활성화 함수를 취한 값으로, 셀 상태 $C_t$가 되기 전의 출력값입니다. 이제 가장 중요한 부분들을 살펴보겠습니다. 앞의 게이트에서 계산한 결과에 의해 다음 두 값이 결정됩니다.

\[C_t = f_t \times C_{t-1} + i_t \times \widetilde{C_t}\]

$C_t$의 경우 셀 상태는 Forget 게이트의 출력에 의해 이전 타임스텝의 셀 상태를 얼만큼 남길지가 결정되고 새로 입력된 Input 게이트의 출력과 $\widetilde{C_t}$를 곱한 값을 더해서 다음 타임스텝의 셀 상태를 만듭니다.

\[h_t = \tanh(C_t) \times o_t\]

LSTM의 출력인 $h_t$는 윗줄에서 계산된 셀 상태에 $\tanh$ 활성화함수를 취한 값을 Output 게이트의 출력에 곱합니다.

이제 예제 코드를 살펴보겠습니다. LSTM을 처음 제안한 논문에서는 실험 여섯개를 제시했는데 그 중 하나가 곱셉 문제 Multiplication problem 입니다. 이 문제는 말 그대로 실수에 대해 곱셈을 하는 문제로, 고려해야 할 실수의 범위가 100개고 그중에서 마킹된 두 개의 숫자만 곱해야 합니다.

곱셉 문제 데이터 생성

랜덤한 숫자 100개를 만든 뒤 2개를 선택하여 원-핫 인코딩 벡터를 만들고, 마킹 인덱스와 랜덤한 숫자를 같이 X에 저장하고 인덱스가 1인 값들을 곱해 Y에 저장합니다. 첫 원소를 출력해보면 마킹 인덱스가 1인 값이 2개만 존재하는 것을 확인할 수 있으며, Y값은 두 값을 곱한 값입니다.

LSTM 레이어를 이용한 곱셉 문제 모델 정의

Model: "sequential_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
lstm (LSTM)                  (None, 100, 30)           3960      
_________________________________________________________________
lstm_1 (LSTM)                (None, 30)                7320      
_________________________________________________________________
dense_1 (Dense)              (None, 1)                 31        
=================================================================
Total params: 11,311
Trainable params: 11,311
Non-trainable params: 0
_________________________________________________________________

LSTM 레이어를 겹치기 위해 첫 번째 LSTM 레이어에서 return_sequences=True로 설정했습니다. 레이어의 출력을 다음 레이어로 그대로 넘겨주게 됩니다. 네트워크의 구조는 다음과 같습니다.

image

Figure 7.5 2 layer LSTM

첫 번째 레이어는 모든 출력을 다음 레이어로 넘기기 때문에 두 번째 레이어도 각 타임스텝에 대해 아래쪽과 옆에서 오는 양방향의 입력을 정상적으로 받을 수 있습니다. 두번째 레이어는 return_sequences 인수가 지정돼 있지 않기 때문에 기본값인 False가 돼서 마지막 계산값만 출력으로 넘기고 마지막의 Dense 레이어의 출력과 정답과의 평균 제곱 오차를 비교하고 이 오차를 줄이는 방향으로 네트워크를 학습시키게 됩니다. 그러면 이 네트워크를 학습시켜보겠습니다.

LSTM 네트워크 학습

Epoch 1/100
64/64 [==============================] - 5s 15ms/step - loss: 0.0502 - val_loss: 0.0507
Epoch 2/100
64/64 [==============================] - 1s 8ms/step - loss: 0.0492 - val_loss: 0.0508
Epoch 3/100
64/64 [==============================] - 1s 9ms/step - loss: 0.0497 - val_loss: 0.0509
...
Epoch 98/100
64/64 [==============================] - 1s 9ms/step - loss: 6.7805e-04 - val_loss: 0.0011
Epoch 99/100
64/64 [==============================] - 1s 9ms/step - loss: 8.6041e-04 - val_loss: 0.0019
Epoch 100/100
64/64 [==============================] - 1s 9ms/step - loss: 7.9022e-04 - val_loss: 0.0010

직접 같이 해보진 않았지만 이 네트워크를 LSTM 대신 SimpleRNN으로 구성할 경우 마지막 에포크에서 loss는 0.025, val_loss는 0.075 정도가 나오게 됩니다. 비교해보면 훨씬 개선된 결과인 것 같습니다. 그래프로 확인해보겠습니다.

LSTM 네트워크 학습 결과 확인

image

매우 가파르게 줄어들어 0에 가까워지며 val_loss 역시 계속해서 감소하는 경향을 보입니다. RNN의 경우 과적합이 발생해 val_loss가 오히려 증가하게 되는데, 문제점을 해결했다는 점을 알 수 있습니다. 테스트 데이터를 넣어 값을 얼마나 정확하게 예측하는지 보겠습니다.

테스트 데이터에 대한 예측 정확도 확인

14/14 [==============================] - 0s 5ms/step - loss: 0.0011
0.01768231448188979 	 0.02641555 	diff: 0.008733234736764843
0.018781752671543545 	 0.023027666 	diff: 0.004245913241561466
0.03620508578216625 	 0.035315864 	diff: 0.0008892219940383542
0.5890630373116781 	 0.5634142 	diff: 0.025648821270113142
0.2580453246499286 	 0.22772479 	diff: 0.03032053407680846
correctness: 83.86363636363636 %

테스트 데이터에 대한 loss는 0에 가까이 나옵니다. 정답의 기준은 오차가 0.04를 넘는지를 기준으로 하였는데 SimpleRNN의 경우 약 9%가 나오므로 이 task에 대해 LSTM이 RNN보다 훨씬 좋은 성능을 보이고 있다는 점을 알 수 있습니다.

GRU 레이어

GRU Gated Recurrent Unit 레이어는 LSTM 레이어와 비슷한 역할을 하지만 구조가 더 간단합니다. 간단한 구조 덕분에 계산상의 이점이 있고, 경우에 따라 LSTM 레이어보다 더 좋은 성능을 보이기도 합니다.

image

Figure 7.6 GRU

LSTM과의 가장 큰 차이점은 셀 상태가 보이지 않는다는 것입니다. 셀 상태가 없는 대신 $h_t$가 비슷한 역할을 합니다. LSTM 레이어보다 시그모이드 함수가 하나 적게 쓰였는데, 게이트의 수가 하나 줄었다는 것을 의미합니다. 수식으로 나타내면 다음과 같습니다.

\[z_t = \text{sigmoid}(x_tU^z + h_{t-1}W^z) \\ r_t = \text{sigmoid}(x_tU^r + h_{t-1}W^r) \\ \widetilde{h_t} = \tanh\left(x_tU^{\widetilde{h}} + (h_{t-1} \times r^t)W^{\widetilde{h}}\right) \\ h_t = (1-z_t)\times h_{t-1} + z_t \times \widetilde{h_t}\]

$r_t$는 reset 게이트, $z_t$는 Update 게이트를 통과한 출력입니다. Reset 게이트를 통과한 출력 $r_t$는 이전 타임스텝의 출력인 $h_t$에 곱해지기 때문에 정보를 얼마나 남길지를 결정하는 정도라고 생각할 수 있습니다. Update 게이트의 출력 $z_t$는 LSTM의 Input과 Forget 게이트의 출력의 역할을 동시에 수행한다고 볼 수 있습니다. 위 수식의 마지막 줄에서 $\tanh$을 통과한 $\widetilde{h_t}$와 이전 타임스텝의 출력인 $h_{t-1}$은 $z_t$ 값에 따라 최종 출력에서 각각 어느 정도의 비율을 점유할지 결정되기 때문입니다.

코드는 LSTM과 거의 같습니다. LSTM만 GRU로 바꿔주면 됩니다. 직접 해보고 결과를 비교해봅시다.

임베딩 레이어

임베딩 레이어 Embedding Layer는 자연어를 수치화된 정보로 바꾸기 위한 레이어를 의미합니다. 자연어는 시간의 흐름에 따라 정보가 연속적으로 이어지는 시퀀스 데이터입니다. 영어는 문자(character), 한글은 문자를 넘어 자소 단위로도 쪼갤 수 있습니다. 혹은 더 큰 단위인 단어로 쪼개기도 합니다.

자연어를 구성하는 단위에 정수 인덱스를 저장하는 방법도 있으며, 원-핫 인코딩 방식을 사용하게 됩니다. 하지만 사용하는 메모리의 양에 비해 너무 적은 정보량을 표현하고 인덱스에 저장된 단어의 수가 많아질수록 메모리의 양이 더욱 늘어나게 됩니다. 반면 임베딩 레이어는 한정된 길이의 벡터로 자연어의 구성 단위를 표현할 수 있습니다.

임베딩 레이어에 대한 개념은 어렵지 않습니다. 하지만 방법에는 여러가지가 있습니다. 대표적으로 Word2Vec, GloVe, FastText, ELMo 등이 있습니다. 또 미리 훈련된 임베딩 레이어의 가중치를 불러와서 사용하면 학습 시간을 절약할 수도 있습니다. 여기서는 랜덤한 값에서 시작해서 가중치를 점점 적합한 값으로 학습시켜나가는 방법을 사용하겠습니다.

긍정, 부정 감성 분석

감성 분석 감성 분석은 입력된 자연어 안의 주관적 의견, 감정 등을 찾아내는 문제입니다. 이 가운데 극성 polarity 감성 분석은 문장의 긍정/부정이나 긍정/중립/부정을 분류합니다. 리뷰 데이터에는 양이 많고 별점을 함께 달기 때문에 쉽게 적용할 수 있습니다. 여기서는 네이버에서 발표했던 영화 리뷰 데이터를 사용해 긍정/부정 감성 분석을 해보겠습니다. 훈련 데이터 15만개, 테스트 데이터 5만개 총 20만개의 리뷰 데이터가 있으며 각 데이터는 별점이 포함되어 있습니다. 리뷰 중 별점이 1-4인 10만개를 부정적, 9-10인 10만개를 긍정적인 리뷰로 보겠습니다.

데이터 다운로드

Downloading data from [https://raw.githubusercontent.com/e9t/nsmc/master/ratings_train.txt](https://raw.githubusercontent.com/e9t/nsmc/master/ratings_train.txt)
14630912/14628807 [==============================] - 2s 0us/step
14639104/14628807 [==============================] - 2s 0us/step
Downloading data from [https://raw.githubusercontent.com/e9t/nsmc/master/ratings_test.txt](https://raw.githubusercontent.com/e9t/nsmc/master/ratings_test.txt)
4898816/4893335 [==============================] - 0s 0us/step
4907008/4893335 [==============================] - 0s 0us/step

다운로드를 한 뒤 데이터를 메모리에 불러와야 합니다. 데이터를 불러오고 데이터가 어떻게 생겼는지 간단하게 확인해보겠습니다.

데이터 로드 및 확인

Length of text: 6937271 characters
Length of text: 2318260 characters

id	document	label
9976970	아 더빙.. 진짜 짜증나네요 목소리	0
3819312	흠...포스터보고 초딩영화줄....오버연기조차 가볍지 않구나	1
10265843	너무재밓었다그래서보는것을추천한다	0
9045019	교도소 이야기구먼 ..솔직히 재미는 없다..평점 조정	0
6483659	사이몬페그의 익살스런 연기가 돋보였던 영화!스파이더맨에서 늙어보이기만 했던 커스틴 던스트가 너무나도 이뻐보였다	1
5403919	막 걸음마 뗀 3세부터 초등학교 1학년생인 8살용영화.ㅋㅋㅋ...별반개도 아까움.	0
7797314	원작의

데이터의 각 행은 탭 문자(\t)로 구분돼 있습니다. id는 데이터의 고유한 인덱스이고, document는 리뷰 내용입니다. label은 긍정/부정을 나타내는 값으로 0은 부정, 1은 긍정입니다. 이제 학습을 위해 훈련 데이터와 테스트 데이터로 만들어 보겠습니다. 우선 간단한 라벨부터 처리하겠습니다.

학습을 위한 정답 데이터(Y) 만들기

(150000, 1) (50000, 1)
[[0]
 [1]
 [0]
 [0]
 [1]]

첫 번째 줄과 두 번째 줄은 먼저 각 텍스트를 \n로 분리한 다음 헤더에 해당하는 부분을 제외한 나머지에 대해 각 행을 처리합니다. 각 행은 탭 문자 \t로 나눠진 후에 3번째 원소를 정수로 변환해서 저장합니다. 직접 확인해보면 라벨이 잘 들어있음을 확인할 수 있습니다.

그 다음으로는 입력으로 쓸 자연어를 토큰화 Tokenization 하고 정제 Cleaning 해야 합니다. 토큰화란 자연어를 작은 단위로 나누는 것으로 여기서는 단어를 사용할 것이기 때문에 띄워쓰기를 기준으로 나눕니다. 정제란 원하지 않는 입력이나 불필요한 기호를 제거하는 것입니다.

훈련 데이터의 입력(X) 정제

['아', '더빙', '진짜', '짜증나네요', '목소리']
['흠', '포스터보고', '초딩영화줄', '오버연기조차', '가볍지', '않구나']
['너무재밓었다그래서보는것을추천한다']
['교도소', '이야기구먼', '솔직히', '재미는', '없다', '평점', '조정']
['사이몬페그의', '익살스런', '연기가', '돋보였던', '영화', '!', '스파이더맨에서', '늙어보이기만', '했던', '커스틴', '던스트가', '너무나도', '이뻐보였다']

라이브러리 re는 정규표현식 라이브러리입니다. re_sub() 안에서 세 번째 인수인 string에서 첫 번째 인수에 해당하는 내용을 찾아 두 번째 인수로 교체해주게 됩니다. 정규표현식에 대한 더 자세한 내용은 더 찾아보시면 좋을거 같습니다. 이렇게 정제한 결과 구두점(.) 같은 기호가 삭제되고 단어 단위로 나눠진 데이터가 생긴 것을 확인할 수 있습니다.

우리는 이 데이터를 네트워크에 입력해야 합니다. 이를 위해선 데이터의 크기(문장의 길이(가 동일해야 합니다. 적당한 길이의 문장이 어느 정도인지 확인하고, 긴 문장은 줄이고 짧은 문장은 공백을 의미하는 패딩 padding을 채워넣겠습니다. 이를 위해서 문장의 길이를 그래프로 그려보겠습니다.

각 문장의 단어 길이 확인

image

그래프의 Y축은 문장의 단어 개수입니다. 15만 개의 문장 중에서 대부분이 40단어 이하로 구성돼 있습니다. 특히 25 단어 이하인 문장의 수는 142,587개로 전체의 95% 입니다. 따라서 기준이 되는 문장의 길이를 임의로 25단어로 잡도록 하겠습니다.

또 각 단어의 최대 길이도 조정해줘야합니다. 앞에서부터 5글자 정도로 자르더라도 단어가 가진 의미는 어느정도 보존되기 때문에 여러 개의 단어에 분산될 수 있는 의미를 하나로 모을 수 있습니다. 원래는 어간 추출 Stemming 등의 기법을 사용하지만 여기서는 길이만 줄여보도록 하겠습니다.

단어 정제 및 문장 길이 줄임

['아', '더빙', '진짜', '짜증나네요', '목소리']
['흠', '포스터보고', '초딩영화줄', '오버연기조', '가볍지', '않구나']
['너무재밓었']
['교도소', '이야기구먼', '솔직히', '재미는', '없다', '평점', '조정']
['사이몬페그', '익살스런', '연기가', '돋보였던', '영화', '!', '스파이더맨', '늙어보이기', '했던', '커스틴', '던스트가', '너무나도', '이뻐보였다']

이제 앞에서 설명한 작업 중 짧은 문장의 같은 길이의 문장(25단어)으로 바꾸기 위한 패딩을 넣겠습니다. tf.keras에서 패딩을 위해 pad_sequences라는 함수를 제공합니다. 또 모든 단어를 사용하지 않고 출현 빈도가 가장 높은 일부 단어만 사용하기 위해 Tokenizer도 사용했습니다.

Tokenizerpad_sequences를 이용한 문장 전처리

[[   25   884     8  5795  1111     0     0     0     0     0     0     0
      0     0     0     0     0     0     0     0     0     0     0     0
      0]
 [  588  5796  6697     0     0     0     0     0     0     0     0     0
      0     0     0     0     0     0     0     0     0     0     0     0
      0]
 [    0     0     0     0     0     0     0     0     0     0     0     0
      0     0     0     0     0     0     0     0     0     0     0     0
      0]
 [   71   346    31    35 10468     0     0     0     0     0     0     0
      0     0     0     0     0     0     0     0     0     0     0     0
      0]
 [  106  5338     4     2  2169   869   573     0     0     0     0     0
      0     0     0     0     0     0     0     0     0     0     0     0
      0]]

Tokenizer는 데이터에 출현하는 모든 단어의 개수를 세고 빈도 수로 정렬해서 num_words에 지정된 만큼만 숫자로 반환하고 나머지는 0으로 반환합니다. tokenizer.fit_on_texts(sentences)Tokenizer에 데이터를 실제로 입력합니다. 이 과정을 거친 뒤 tokenizer.texts_to_sequences(sentences)는 문장을 입력받아 숫자를 반환합니다. 마지막으로 pad_sequences()는 입력된 데이터에 패딩을 더합니다.

출력에서 전처리된 문장을 확인할 수 있습니다. “아”는 25, “더빙”은 884 등의 숫자로 바뀌었습니다. 문장에서 사용하지 않는 부분은 0으로 넣어 입력 길이인 25를 맞춰줬습니다. padding 인수에는 문장의 앞에 패딩을 넣는 pre와 문장의 뒤에 패딩을 넣는 post가 있습니다.

이제 실제 네트워크를 정의하고 학습시켜보겠습니다. 먼저 임베딩 레이어와 LSTM 레이어를 연결한 뒤 마지막에 Dense 레이어의 소프트맥스 활성화함수를 사용해 긍정/부정을 분류하는 네트워크를 정의해보겠습니다.

감성 분석 모델 정의

Model: "sequential_2"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding (Embedding)        (None, 25, 300)           6000000   
_________________________________________________________________
lstm_2 (LSTM)                (None, 50)                70200     
_________________________________________________________________
dense_2 (Dense)              (None, 2)                 102       
=================================================================
Total params: 6,070,302
Trainable params: 6,070,302
Non-trainable params: 0

임베딩 레이어는 시퀀셜 모델의 첫 번째 레이어이기 때문에 입력 형태에 대한 정의가 필요합니다. input_length 인수를 25로 지정해서 각 문장에 들어있는 25개의 단어를 길이 300의 임베딩 벡터로 변환합니다. 네트워크의 losssparse_categorical_crossentropy를 사용했습니다. 이에 대해선 앞에 CNN에서 다뤘었습니다. 이제 네트워크를 학습시켜보겠습니다.

감성 분석 모델 학습

Epoch 1/5
938/938 [==============================] - 8s 8ms/step - loss: 0.4365 - accuracy: 0.7825 - val_loss: 0.3790 - val_accuracy: 0.8229
Epoch 2/5
938/938 [==============================] - 7s 7ms/step - loss: 0.3249 - accuracy: 0.8470 - val_loss: 0.3878 - val_accuracy: 0.8185
Epoch 3/5
938/938 [==============================] - 7s 7ms/step - loss: 0.2708 - accuracy: 0.8690 - val_loss: 0.4201 - val_accuracy: 0.8180
Epoch 4/5
938/938 [==============================] - 7s 7ms/step - loss: 0.2268 - accuracy: 0.8895 - val_loss: 0.4752 - val_accuracy: 0.8148
Epoch 5/5
938/938 [==============================] - 7s 7ms/step - loss: 0.1896 - accuracy: 0.9069 - val_loss: 0.5461 - val_accuracy: 0.8111

데이터가 많기 때문에 batch_size는 128로 설정했고, 5에포크만 학습시켰습니다. 학습 과정에서 loss는 꾸준히 감소하지만 val_loss는 점점 증가하는 것을 확인할 수 있습니다. 이는 네트워크가 과적합되고 있음을 의미합니다.

감성 분석 모델의 학습 결과

image

val_loss는 점점 증가하고 val_accuracy는 점점 감소하고 있습니다. 네트워크가 과적합되는 것으로 보입니다. 과적합의 이유는 임베딩 레이어를 랜덤한 값에서부터 시작해서 학습시키기 때문에 각 단어를 나타내는 벡터의 품질이 좋지 않기 때문입니다. 임베딩 레이어를 별도로 학습시켜서 네트워크에 불러와 사용하는 등의 방법으로 개선할 수 있습니다.

학습된 네트워크가 테스트 데이터는 어떻게 평가하는지 살펴보겠습니다. 테스트 데이터도 앞선 과정과 같이 변환시키고 model.evaluate()로 평가해 보겠습니다. 여기서 Tokenizer는 학습 데이터와 같은 것을 사용한다는 점을 유의해야 합니다.

테스트 데이터 평가

[0.5657506585121155, 0.801360011100769]

테스트 데이터의 정확도는 80%로 나왔습니다. 이는 검증 데이터와 비슷한 값입니다. 임의의 문장에 대한 감성 분석은 어떤지도 확인해보기 위해 하나의 문장을 잘라서 앞에서부터 차례대로 입력해보겠습니다.

임의의 문장에 대한 감성 분석 결과 확인

['재미있을']
[0.56276757 0.43723238]
['재미있을', '줄']
[0.52503574 0.47496432]
['재미있을', '줄', '알았는데']
[0.50102687 0.4989731 ]
['재미있을', '줄', '알았는데', '완전']
[0.61140907 0.388591  ]
['재미있을', '줄', '알았는데', '완전', '실망했다.']
[0.61140907 0.388591  ]
['재미있을', '줄', '알았는데', '완전', '실망했다.', '너무']
[0.70096964 0.29903036]
['재미있을', '줄', '알았는데', '완전', '실망했다.', '너무', '졸리고']
[0.9799427  0.02005735]
['재미있을', '줄', '알았는데', '완전', '실망했다.', '너무', '졸리고', '돈이']
[0.99790716 0.00209286]
['재미있을', '줄', '알았는데', '완전', '실망했다.', '너무', '졸리고', '돈이', '아까웠다.']
[0.99790716 0.00209286]

부정적인 어휘가 나오면서 점점 더 부정적이라고 예측할 확률이 높아지고 있다는 점을 확인할 수 있습니다.

자연어 생성

단어 단위 생성

이번에는 한글 원본 텍스트를 자소 단위와 단어 단위로 나눠서 순환 신경망으로 생성해 보겠습니다. 이번에 사용할 데이터는 조선왕조실록 국문 번역본입니다.

조선왕조실록 데이터 파일 다운로드

Downloading data from [http://bit.ly/2Mc3SOV](http://bit.ly/2Mc3SOV)
62013440/62012502 [==============================] - 2s 0us/step
62021632/62012502 [==============================] - 2s 0us/step

데이터 파일은 62MB로 꽤 크기가 큽니다. 이 데이터를 메모리에 불러온 다음 글자 수와 데이터의 첫 부분을 확인해보겠습니다.

데이터 로드 및 확인

Length of text: 26265493 characters

태조 이성계 선대의 가계. 목조 이안사가 전주에서 삼척·의주를 거쳐 알동에 정착하다 
태조 강헌 지인 계운 성문 신무 대왕(太祖康獻至仁啓運聖文神武大王)의 성은 이씨(李氏)요, 휘

한자가 많은 부분을 차지하지만, 단어 기반의 생성을 위해 한자와 한자가 들어간 괄호는 생략하겠습니다.

훈련 데이터 입력 정제

['태조', '이성계', '선대의', '가계', '목조', '이안사가', '전주에서', '삼척', '의주를', '거쳐', '알동에', '정착하다', '\n', '태조', '강헌', '지인', '계운', '성문', '신무', '대왕']

조선왕조실력에는 영문 텍스트가 없기 때문에 영문 관련 처리는 하지 않았습니다. 그리고 한자와 괄호는 삭제했습니다. 또 개행 문자(\n)의 보존을 위해 텍스트를 먼저 개행 문자로 나눈 뒤 다시 합칠 때 개행 문자를 추가했습니다. 다음으로는 단어를 토큰화하는 것입니다. 여기서는 직접 토큰화 하겠습니다. 단어의 수가 너무 많고, 모든 단어를 사용할 것이기 때문에 Tokenizer는 불필요한 시간을 쓰게 됩니다.

단어 토큰화

332640 unique words
{
  '\n':   0,
  '!' :   1,
  ',' :   2,
  '000명으로':   3,
  '001':   4,
  '002':   5,
  '003':   6,
  '004':   7,
  '005':   8,
  '006':   9,
  ...
}
index of UNK: 332639

텍스트에 들어간 각 단어가 중복되지 않는 리스트를 만들고, 텍스크에 존재하지 않는 토큰을 나타내는 UNK를 넣었습니다. 이제 학습을 위한 데이터를 만들어보겠습니다. 여기서는 기존의 방식이 아닌 tf.data.Dataset을 이용합니다. 간단한 코드로 데이터 섞기, 배치 수 만큼 자르기, 다른 데이터에 매핑하기 등을 빠르게 수행할 수 있습니다.

기본 데이터 세트 만들기

['태조' '이성계' '선대의' '가계' '목조' '이안사가' '전주에서' '삼척' '의주를' '거쳐' '알동에' '정착하다'
 '\n' '태조' '강헌' '지인' '계운' '성문' '신무' '대왕' '의' '성은' '이씨' '요' ',' '휘']
[299305 229634 161443  17430 111029 230292 251081 155087 225462  29027
 190295 256129      0 299305  25624 273553  36147 163996 180466  84413
 224182 164549 230248 210912      2 330313]

여기서는 seq_length를 25로 설정해 25개의 단어가 주어졌을 때 다음 단어를 예측하도록 데이터를 만들겠습니다. Dataset에 쓰이는 batch() 함수는 Dataset에서 한번에 반환하는 데이터의 숫자를 지정합니다. 여기서는 seq_length+1을 지정하여 처음 25개 단어와 그 뒤에 오는 정답이 될 1단어를 합쳐서 반환하도록 합니다. 또 drop_remainder=True로 남는 부분은 버리도록 했습니다. 이제 이렇게 만들어진 Dataset으로 새로운 Dataset을 만들어 보겠습니다.

학습 데이터세트 만들기

['태조' '이성계' '선대의' '가계' '목조' '이안사가' '전주에서' '삼척' '의주를' '거쳐' '알동에' '정착하다'
 '\n' '태조' '강헌' '지인' '계운' '성문' '신무' '대왕' '의' '성은' '이씨' '요' ',']
[299305 229634 161443  17430 111029 230292 251081 155087 225462  29027
 190295 256129      0 299305  25624 273553  36147 163996 180466  84413
 224182 164549 230248 210912      2]
휘
330313

먼저 split_input_target(chunk)라는 함수를 정의하여 26개의 단어를 25개, 1개로 잘라주고 map() 함수를 사용해 새로운 Dataset를 만들었습니다. 그 후 데이터를 섞고 batch size를 다시 설정했습니다. 빠른 학습을 위해 128개의 데이터를 학습하고, 데이터를 섞을 때의 BUFFER_SIZE는 10,000으로 했습니다. tf.data는 한번에 모든 데이터를 섞지 않고 버퍼에 일정한 양의 데이터를 올려놓고 섞습니다. 이제 생성 모델을 정의해보겠습니다.

단어 단위 생성 모델 정의

Model: "sequential_3"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding_1 (Embedding)      (None, 25, 100)           33264000  
_________________________________________________________________
lstm_3 (LSTM)                (None, 25, 100)           80400     
_________________________________________________________________
dropout (Dropout)            (None, 25, 100)           0         
_________________________________________________________________
lstm_4 (LSTM)                (None, 100)               80400     
_________________________________________________________________
dense_3 (Dense)              (None, 332640)            33596640  
=================================================================
Total params: 67,021,440
Trainable params: 67,021,440
Non-trainable params: 0
_________________________________________________________________

임베딩 레이어와 LSTM 레이어로 구성돼 있는 것은 같습니다. LSTM은 2층으로 쌓여 있고 중간에 드롭아웃 레이어를 배치했습니다. 마지막에는 Dense레이어를 배치해서 소프트맥스로 주어진 입력에 대해 어떤 단어를 선택해야 하는지 고릅니다.

단어 단위 생성 모델 학습

Epoch 1/50
533/533 [==============================] - 103s 192ms/step - loss: 8.3514 - accuracy: 0.0738 -

 태조 이성계 선대의 가계 목조 이안사가 전주에서 삼척 의주를 거쳐 알동에 정착하다  , 그 그 그 그 그 그 그 그 그 그 그 그 그 그 그 그 그 그 그 그 그 그 그 , , , , , , , , , , 그 그 그 그 그 그 그 그 그 그 그 그 그 그 그 그 그 그 그 그 그 그 그 그 , , , , , , , , , , 그 그 그 그 그 그 그 그 그 그 그 그 그 그 그 그 그 그 그 그 그 그 그 그 , , , , , , , ,

...

Epoch 50/50
533/533 [==============================] - 102s 192ms/step - loss: 0.1695 - accuracy: 0.978822s - loss: 0.1747 -  - ETA: 19s - loss: 0.17 - ETA: 15s - loss: 0.172 - ETA: 12s  - ETA: 3s - ETA: 0s - loss: 0.1696 - accuracy: 

 태조 이성계 선대의 가계 목조 이안사가 전주에서 삼척 의주를 거쳐 알동에 정착하다  옛일을 좌천 사관 의 아우 혁파 을 선포 하는 물건을 와서 주게 하여 사사로이 당하여 주게 하는 자가 합하여 도로 중한 자를 합하여 길을 끊게 하여 제 금령을 임하여 임하여 임하여 달려 있더라도 진실로 먼 인구 에 전보 할 수 아닌 때를 만들 아니하고 경은 경은 왕비 441 이고 총명 골육을 인재 으로 길이 하지만 , 신은 경 이니 , 본도 이니 흠축 지중추원사 지중추원사 지중추원사 박원형 우부승지 이극감 우찬성 우찬성 우찬성 홍경손 무송군 무송군 것은 한 뒤에 각각 각각 관 을 남김없이 난 126 영원히 조선 214 천하의 쇄환 라고 1 흠준 였는데 \? 홍응이 평천하 나 모든 사람의

먼저 모델을 학습시키면서 모델의 생성 결과물을 확인하기 위해 testmodel 이라는 이름으로 콜백 함수를 정의했습니다. 임의의 문장을 입력한 다음 뒤에서부터 seg_length만큼의 단어를 선택합니다. 그 후 문장의 단어를 인덱스 토큰으로 바꾸고 사전에 등록되어 있지 않을 경우 ‘UNK’로 바꿉니다. 그 다음 패딩을 넣어줬습니다. 출력 단어는 test_sentence의 끝 부분에 저장되어 다음 스텝의 입력으로 활용됩니다. 이렇게 정의된 콜백 함수는 testmodelcb라는 이름으로 저장되어 model.fit()callbacks 인수로 포함됩니다.

학습을 시킬 때도 위에서 정의한 Dataset을 활용합니다. repeat() 함수를 통해서 데이터를 끊임없이 반환해야 합니다. 데이터의 시작과 끝을 알 수 없기 때문에 에포크에 데이터를 얼마나 학습시킬지를 steps_per_epoch 인수로 지정해야 합니다.

학습 결과를 보면 처음에는 의미없는 단어가 반복되지만 점점 문맥이 연결되는 단어가 배열되도록 학습되고 있습니다. 더 많이 학습시킬수록 더 자연스러운 결과를 얻게 될 것입니다. 이제 임의의 문장을 넣어 학습이 잘 되는지 확인해보겠습니다.

임의의 문장을 사용한 생성 결과 확인

동헌에 나가 공무를 본 후 활 십오 순을 쏘았다 일본국에서 주머니 분격하여 힘쓸 에 은밀히 관등 하는데 하라 고 하므로 , 전자에 우리들은 이만주 전에는 여산 많아 이석정 4월로 3등 사목 가 하직하니 , 친정을 해당 사용을 정지하고 , 어가 가 국문 하였다 대신을 올리게 한다 만약 친히 아뢰기를 , 
 창하기를 , 전하는 몸을 바로 한다 만약 이를 가지고 알지 못하고 반드시 물을 것이니 , 어찌 먼저 물을 있지 않으니 , 만약 매 29일 경희전 수가 없다 
 하였다 강맹경이 평안도 관찰사에게 유시하기를 , 
 호패 가 이미 먼저 대신하게 하여 능히 능히 능히 능히 복 을 많이 보이고 , 만일 도망하여 묻는 일을 더하여 논죄 하여 돌아와서

전체적인 문장의 의미가 잘 통하지는 않지만 부분 부분에서는 자연스럽게 연결되는 단어들이 보입니다.

댓글남기기