이 글은 [유원준, 상준]의 [딥러닝을 이용한 자영어 처리 입문]에서 가져온 내용이며, [크리에이티브 커먼즈 저작자표시-비영리-동일조건변경허락 2.0 대한민국 라이선스](https://creativecommons.org/licenses/by-nc-sa/2.0/kr/)에 따라 사용되었습니다.
https://wikidocs.net/48558
08. 순환 신경망(Recurrent Neural Network)
앞서 배운 피드 포워드 신경망은 입력의 길이가 고정되어 있어 자연어 처리를 위한 신경망으로는 한계가 있었습니다. 결국 다양한 길이의 입력 시퀀스를 처리할 수 있는 인공 신경망이 …
wikidocs.net
08. 순환 신경망(Recurrent Neural Network)
- 피드 포워드 신경망은 입력의 길이가 고정되어 있어 자연어 처리를 위한 신경망으로는 한계가 있음 => 다양한 길이의 입력 시퀀스를 처리할 수 있는 인공신경망이 필요. => 대표적인 인공 신경망이 바로 순환 신경망(REcurrent Neural Network, RNN)
- 바닐라 RNN, 이를 개선한 LSTM, GRU에 대해 학습해볼 예정.
08-01 순환 신경망 (Recurrent Neural Network, RNN)
- 입력과 출력을 시퀀스 단위로 처리하는 시퀀스 모델.
1. 순환 신경망(Recurrent Neural Network, RNN)
- 은닉층의 노드에서 활성화 함수를 통해 나온 결과값을 출력층 방향으로도 보내면서, 다시 은닉층 노드의 다음 계산의 입력으로 보내는 특징을 가짐.
- RNN에서 은닉층에서 활성화 함수를 통해 결과를 내보내는 역할을 하는 노드를 셀(cell)이라 함. 이 셀은 이전의 값을 기억하려고 하는 일종의 메모리 역할을 수행하므로 이를 메모리 셀 또는 RNN 셀이라고 표현.
- 은닉층의 메모리 셀은 각각의 시점(time step)에서 바로 이전 시점에서의 은닉층의 메모리 셀에서 나온 값을 자신의 입력으로 사용하는 재귀적 활동을 함.
- 현재시점을 t라고 하면, 메모리 셀이 출력층 방향 또는 다음 시점인 t+1의 자신에게 보내는 값을 은닉 상태(hidden state)라고 함.
- 피드 포워드 신경망에서는 뉴런이라는 단어를 사용했지만 RNN에서 뉴런보다는 입력층과 출력층에서는 각각 입력벡터와 출력벡터, 은닉층에서는 은닉 상태라는 표현을 주로 사용함.

- RNN은 입력과 출력의 길이를 다르게 설계할 수 있음. RNN 셀의 각 시점의 입,출력의 단위는 사용자가 정의하기 나름이지만 가장 보편적인 단위는 '단어 벡터'.

예를 들어 하나의 입력에 대해서 여러 개의 출력을 의미하는 일 대 다 (one-to-many) 구조의 모델은 하나의 이미지 입력에 대해서 사진의 제목을 출력하는 이미지 캡셔닝(Image Captioning) 작업에 사용할 수 있음. 사진의 제목은 단어들의 나열이므로 시퀀스 출력임.

- 또한 단어 시퀀스에 대해서 하나의 출력을 하는 다 대 일 (many-to-one) 구조의 모델은 입력 문서가 긍정적인지 부정적인지 판별하는 감성 분류(sentiment classification), 또는 메일이 정상 메일인지 스팸 메일인지 판별하는 스팸 메일 분류 (spam detection) 등에 사용할 수 있음.

- 다 대 다 (many-to-many) 구조의 모델의 경우에는 사용자가 문장을 입력하면 대답 문장을 출력하는 챗봇과 입력 문장으로부터 번역된 문장을 출력하는 번역기, 또는 '태깅 작업' 챕터에서 배우는 개체명 인식이나 품사 태깅과 같은 작업에 속함.

- 현재 시점 t에서의 은닉 상태값을 ht라고 정의. 은닉층의 메모리 셀은 ht 계산을 위해 총 두 개의 가중치를 가짐. 하나는 입력층을 위한 가중치 Wx이고, 하나는 이전 시점 t-1의 은닉상태값인 ht-1을 위한 가중치 Wh임.

- 식으로 표현하면 다음과 같음
- ht를 계산하기 위한 활성화 함수로는 주로 하이퍼볼릭탄젠트 함수(tanh)가 사용됨.
2. 케라스(Keras)로 RNN 구현하기
- 코드:
from tensorflow.keras.layers import SimpleRNN
model.add(SimpleRNN(hidden_units))
# 추가 인자를 사용할 때
model.add(SimpleRNN(hidden_units, input_shape=(timesteps, input_dim)))
# 다른 표기
model.add(SimpleRNN(hidden_units, input_length=M, input_dim=N))
- hidden_units = 은닉 상태의 크기를 정의. 메모리 셀이 다음 시점의 메모리 셀과 출력층으로 보내는 값의 크기(output_dim)와도 동일. RNN의 용량을 늘린다고 보면 되며, 중소형 모델의 경우 보통 128,256,512,1024 등의 값을 가짐
- timesteps = 입력 시퀀스의길이 (input_length)라고 표현하기도 함. 시점의 수
- input_dim = 입력의 크기
- RNN층은 (batch_size, timesteps, input_dim) 크기의 3D 텐서를 입력으로 받음. batch_size는 한 번에 학습하는 데이터의 개수를 말함.
- RNN 층은 사용자의 설정에 따라 두 가지 종류의 출력을 내보냄. 메모리 셀의 최종 시점의 은닉 상태만을 리턴하고자 한다면 (batch_size, output_dim) 크기의 2D 텐서를 리턴. 하지만 메모리 셀의 각 시점(time step)의 은닉 상태값들을 모아서 전체 시퀀스를 리턴하고자 한다면 (batch_size, timesteps, output_dim) 크기의 3D 텐더를 리턴 => 이는 RNN 층의 return_Sequences 매개변수에 True를 입력하면 설정 가능. output_dime은 앞서 코드에서 정의한 hidden_units의 값으로 설정.
-모델 내부적으로 출력 결과를 어떻게 정의하는지 코드 확인:
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import SimplrRNN
model = Sequential()
model.add(SimpleRNN(3, input_shape(2,10)))
# model.add(SimpleRNN(3, input_length=2, input_dim=10))와 동일
model.summary()
- 출력 :
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
simple_rnn_1 (SimpleRNN) (None, 3) 42
=================================================================
Total params: 42
Trainable params: 42
Non-trainable params: 0
_________________________________________________________________
- 출력값이 (batch_size, output_dim)의 크기의 2D 텐서일때, output_dim은 hidden_units의 값인 3이 됨. 이 경우 batch_size를 현 단계에서 알 수 없으므로 (None, 3)이 됨.
-이번에는 batch_size를 미리 정의해보자
model = Sequential()
model.add(SimplrRNN(3, batch_input_shape=(8,2,10)))
model.summary()
- 출력:
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
simple_rnn_2 (SimpleRNN) (8, 3) 42
=================================================================
Total params: 42
Trainable params: 42
Non-trainable params: 0
_________________________________________________________________
- batch_size를 8로 기재하면 출력의 크기가 (8,3)이 됨.
- 이번에는 return_sequences 매개변수에 True를 기재하여 출력값으로 (batch_size, timesteps, output_dim) 크기의 3D 텐서를 리턴하도록 만들어 보자.
- 코드:
model = sequential()
model.add(SimpleRNN(3, batch_input_shape=(8,2,10), return_sequences = True))
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
simple_rnn_3 (SimpleRNN) (8, 2, 3) 42
=================================================================
Total params: 42
Trainable params: 42
Non-trainable params: 0
_________________________________________________________________
- 출력의 크기가 (8,2,3)이 됨.
3. 파이썬으로 RNN 구현하기
- numpy로 RNN 층 구현하기
ht = tanh(WxXt + Whht-1+b)
- t 시점의 은닉상태 = hidden_state_t, 입력데이터의 길이 = input_length (=시점의 수 timesteps), t시점의 입력값 = input_t라고 선언.
- 각 메모리 셀은 각 시점마다 input_t와 hidden_state_t(이전 상태의 은닉 상태)를 입력으로 활성화 함수인 하이퍼볼릭탄젠트 함수를 통해 현 시점의 hidden_state_t를 계산
- 아래 코드는 이해를 돕기 위해 (timesteps, input_dim) 크기의 2D 텐서를 입력으로 받았다고 가정했으나, 실제로 케라스에서는 (batch_size, timesteps, input_dim)의 크기의 3D 텐서를 입력으로 받는 것을 기억하자
- timestpes는 시점의 수 => 자연어 처리에서는 보통 문장의 길이.
- input_dim은 입력의 차원 => 자연어처리에서는 보통 단어 벡터의 차원
- hidden_units는 은닉 상태의 크기로 메모리 셀의 용량. 초기 은닉 상태는 0의 값을 가지는 벡터로 초기화함
- 코드 :
import numpy as np
timesteps = 10
input_dim = 4
hidden_units = 8
#입력에 해당되는 2D 텐서
inputs = np.random.random((timesteps, input_dim))
# 초기 은닉 상태는 0(벡터)로 초기화
hidden_state_t = np.zeros((hidden_units,))
print('초기 은닉 상태 :', hidden_state_t)
- 출력:
초기 은닉 상태 : [0. 0. 0. 0. 0. 0. 0. 0.]
- 은닉 상태의 크기를 8로 정했으니, 8의 차원을 가지는 0의 값으로 구성된 벡터가 출력됨.
- 가중치와 편향을 각 크기에 맞게 정의하고 크기를 출력해보자
Wx = np.random.random((hidden_units, input_dim)) # (8,4) 크기의 2D 텐서 생성, 입력에 대한 가중치
Wh = np.random.random((hidden_units, hidden_units)) # (8,8) 크기의 2D 텐서 생성. 은닉 상태에 대한 가중치
b = np.random.random((hidden_units,)) # (8, ) 크기의 1D 텐서 생성. 이 값은 편향
- Wx는 (은닉 상태의 크기 X 입력의 차원), Wh는 (은닉 상태의 크기 X 은닉 상태의 크기), b는 (은닉 상태의 크기)의 크기를 가짐.
- RNN 층을 동작시켜보자
total_hidden_states = []
# 각 시점 별 입력값
# Wx * Xt + Wh * Ht-1 + b(bias)
output_t = np.tanh(np.dot(Wx, input_t) + np.dot(Wh, hidden_state_t) + b)
# 각 시점 t별 메모리 셀의 출력의 크기는 (timestep t, output_dim)
# 각 시점의 은닉 상태의 값을 계속해서 누적
total_hidden_states.append(list(output_t))
hidden_state_t = output_t
# 출력 시 값을 깔끔하게 해주는 용도
total_hiddne_states = np.stack(total_hidden_states, axis = 0)
# (timesteps, output_dim)
print('모든 시점의 은닉 상태 : ')
print(total_hidden_states)
- 출력:
모든 시점의 은닉 상태 :
[[0.85575076 0.71627213 0.87703694 0.83938496 0.81045543 0.86482715 0.76387233 0.60007514]
[0.99982366 0.99985897 0.99928638 0.99989791 0.99998252 0.99977656 0.99997677 0.9998397 ]
[0.99997583 0.99996057 0.99972541 0.99997993 0.99998684 0.99954936 0.99997638 0.99993143]
[0.99997782 0.99996494 0.99966651 0.99997989 0.99999115 0.99980087 0.99999107 0.9999622 ]
[0.99997231 0.99996091 0.99976218 0.99998483 0.9999955 0.99989239 0.99999339 0.99997324]
[0.99997082 0.99998754 0.99962158 0.99996278 0.99999331 0.99978731 0.99998831 0.99993414]
[0.99997427 0.99998367 0.99978331 0.99998173 0.99999579 0.99983689 0.99999058 0.99995531]
[0.99992591 0.99996115 0.99941212 0.99991593 0.999986 0.99966571 0.99995842 0.99987795]
[0.99997139 0.99997192 0.99960794 0.99996751 0.99998795 0.9996674 0.99998177 0.99993016]
[0.99997659 0.99998915 0.99985392 0.99998726 0.99999773 0.99988295 0.99999316 0.99996326]]
4. 깊은 순환 신경망(Deep Recurrent Neural Network)
- 은닉층이 2개 이상
- 은닉층을 2개 추가하는 경우 코드:
model = Sequential()
model.add(SimppeRNN(hidden_units, input_length=10 input_dim=5, return_sequences = True))
model.add(SimpleRNN(hidden_units, return_sequences = True))
- 첫번째 은닉층은 다음 은닉층이 존재하므로 return_sequences=True를 설정하여 모든 시점에 대해서 은닉 상태 값을 다음 은닉층으로 보내주고 있음
5. 양방향 순환 신경망(Bidirectional Recurrent Neural Network)
- 시점 t에서의 출려값을 예측할 때 이전 시점의 입력뿐만 아니라, 이후 시점의 입력 또한 예측에 기여할 수 있다는 아이디어에 기반함.
- ex. '운동을 열심히 하는 것은 [ ]을 늘리는 데 효과적이다'의 빈칸을 예측하기 위해 앞문장인 '운동을 열심히 하는 것은'만으로 빈칸을 예측하기는 어렵다.
- 기본적으로 두 개의 메모리 셀을 사용함. 첫번째 메모리 셀은 앞 시점의 은닉 상태(Forward States)를 전달받아 현재의 은닉 상태를 계산. 두번째 메모리 뒤 시점의 은닉 상태(Backward States)를 전달받아 현재의 은닉 상태를 계산.
- 코드:
from tensorflow.keras.layers import Bidireactinoal
timesteps = 10
input_dim = 5
model = Sequential()
model.add(Bidireactional(SimpleRNN(hidden_units, return_Sequences = True), input_shape=(timesteps, input_dim)))
- 양방향도 다수의 은닉층을 가질 수 있음
* 내가 생각한 개념 정리)
timesteps : 단어 개수
input_dim : 단어 길이
hidden_units : 은닉노드 (hidden units와 출력층의 차원이 같음)
batch_size : 데이터 개수
08-02 장단기 메모리 (Long Short-Term Memory, LSTM)
- 08-01에서 배운 RNN을 가장 단순한 형태의 RNN이라고 하여 바닐라 RNN이라고 함. (케라스에서 SimpleRNN)
- 바닐라 RNN의 한계를 극복하기 위해 다양한 RNN의 변형이 나옴 -> LSTM도 그 중 하나.
1. 바닐라 RNN의 한계
- 비교적 짧은 시퀀스(sequence)에 대해서만 효과를 보이는 단점이 있음. 바닐라 RNN의 시점이 길어질 수록 앞의 정보가 뒤로 충분히 전달되지 못하는 현상이 발생 => 장기 의존성 문제
2. 바닐라 RNN 내부 열어보기
- xt와 ht-1이라는 두 개의 입력이 각각의 가중치가 곱해져서 메모리 셀의 입력이 됨. 이를 하이퍼볼릭탄젠츠 함수의 입력으로 사용하고 이 값은 은닉층의 출력인 은닉 상태가 됨.
3. LSTM(Long Short-Term Memory)
- 은닉층의 메모리 셀에 입력 게이트, 망각 게이트, 출력 게이트를 추가하여 불필요한 기억을 지우고, 기억해야 할 것들을 정함 => 은닉 상태를 계산하는 식이 전통적인 RNN보다 조금 더 복잡해지고 셀 상태(cell state)라는 값을 추가함 => 긴 시퀀스의 입력을 처리하는데 탁월한 성능을 보임.
- 셀 상태 또한 이전에 배운 은닉 상태처럼 이전 시점의 셀 상태가 다음 시점의 셀 상태를 구하기 위한 입력으로서 사용됨. 은닉 상태의 값과 셀 상태의 값을 구하기 위해서 새로 추가된 3개의 게이트 사용 => 삭제 게이트, 입력 게이트, 출력 게이트. 게이트 모두 시그모이드 함수가 존재 => 0~1 사이의 값이 나오게 되는데 이 값들을 가지고 게이트를 조절.
- xt와 각 게이트에서 사용되는 네개의 가중치가 존재, ht-1과 각게이트에서 사용되는 4개의 가중치가 있고, 각 게이트에서 사용되는 4개의 편향이 있음.
(1) 입력 게이트
- 현재 정보를 기억하기 위한 게이트. 우선 현재 시점 t의 x값과 입력 게이트로 이어지는 가중치 Wx를 곱한 값과 이전 시점 t-1의 은닉 상태와 입력 게이트로 이어지는 가중치 Wh를 곱한 값을 더하여 시그모이드 함수를 지남 => 그 값을 i_t라고 함.
- 그리고 현재 시점 t의 x값과입력 게이트로 이어지는 가중치 Wxg를 곱한 값과 이전 시점 t-1의 은닉 상태가 입력 게이트로 이어지는 가중치 Whg를 곱한 값을 더하여 하이퍼볼릭탄젠트 함수를 지남 => 이를 gt라고 함.
- 시그모이드 함수를 지나 0과 1 사이의 값을 가지는 i_t와 하이퍼볼릭탄젠트 함수를 지나 -1과 1 사이의 값을 가지는 gt. 이 두 개의 값을 가지고 이번에 선택된 기억할 정보의 양을 정하게 됨.
(2) 삭제 게이트
- 기억을 삭제하기 위한 게이트.
- 현재 시점 t의 x값과 이전 시점 t-1의 은닉 상태가 시그모이드 함수를 지나게 되면서 0과 1 사이의 값이 나오는데, 이 값이 곧 삭제 과정을 거친 정보의 양.
- 0에 가까울 수록 정보가 많이 삭제된 것이고, 1에 가까울수록 정보를 온전히 기억한 것. 이를 가지고 셀 상태를 구하게 됨.
(3) 셀 상태
- 셀 상태 Ct를 구하는 방법을 알아보자. (삭제 게이트에서 일부 기억을 잃은 상태)
- 입력 게이트에서 구한 i_t, gt 두 개의 값에 대해서 원소별 곱을 진행. => 같은 크기의 두 행렬이 있을 때 같은 위치의 성분끼리 곱하는 것을 말함. 식으로는 ○ 이라고 표현. 이것이 이번에 선택된 기억할 값.
- 입력 게이트에서 선택된 기억을 삭제 게이트의 결과값과 더하기. 이 값을 현재 시점 t의 셀 상태라고 하며, 이 값은 다음 시점 t+1 시점의 LSTM 셀로 넘겨짐.
- 삭제 게이트의 출력값인 ft가 0이 되면, 이전 시점의 셀 상태의 값인 Ct-1은 현재 시점의 셀 상태의 값을 결정하기 위한 영향력이 0이 되면서, 오직 입력 게이트의 결과만이 현재 시점의 셀 상태의 값 Ct를 결정할 수 있음 => 삭제 게이트가 완전히 닫히고 입력 게이트를 연 상태를 의미.
- 입력 게이트의 i_t 값을 0이라고 한다면, 현재 시점의 셀 상태의 값 Ct는 오직 이전 시점의 셀 상태의 값 Ct-1의 값에만 의존 => 이는 입력 게이트를 완전히 닫고 삭제 게이트만을 연 상태를 의미.
- 결과적으로 삭제 게이트는 이전 시점의 입력을 얼마나 반영할지의 의미이고, 입력 게이트는 현재 시점의 입력을 얼마나 반영할지를 결정
(4) 출력 게이트와 은닉 상태
- 현재 시점의 t의 x값과 이전 시점 t-1의 은닉 상태가 시그모이드 함수를 지난 값.
- 해당 값은 현재 시점 t의 은닉 상태를 결정하는 일에 쓰임. 셀 상태의 값이 하이퍼볼릭탄젠트 함수를 지나 -1과 1 사이의 값이 되고, 해당 값은 출력 게이트 값과 연산되면서 , 값이 걸러지는 효과가 발생하여 은닉 상태가 됨. 은닉 상태의 값은 또한 출력층으로도 향함.
08-03 게이트 순환 유닛 (Gated Recurrent Unit, GRU)
- LSTM의 장기 의존성 문제에 대한 해결책을 유지하면서, 은닉 상태를 업데이트하는 계산을 줄임.
- 즉, GPU 성능은 LSTM과 유사하면서 복잡한 LSTM 구조를 간단화
1. GRU (Gated Recurrent Unit)
- 업데이트 게이트와 리셋 게이트 두 가지 게이트만 존재.
- LSTM과 비교하여 어떤 것이 성능이 낫다라고 단정지을 수 없지만, 경험적으로 데이터 양이 적을 때는 매개 변수의 양이 적은 GRU가 조금 더 낫고, 데이터 양이 더 많으면 LSTM이 더 낫다고도 함.
2. 케라스에서의 GRU
- 코드:
model.add(GRU(hidden_size, input_shape=(timesteps, input_dim)))
08-04 케라스의 SimpleRNN과 LSTM 이해하기
1. 임의의 입력 생성하기
- 코드:
import numpy as np
import tensorflow as tf
from tensorflow.keras.layers import SimpleRNN, LSTM, Bidirectional
# 임의의 입력 생성
train_X = [[0.1, 4.2, 1.5, 1.1, 2.8], [1.0, 3.1, 2.5, 0.7, 1.1], [0.3, 2.1, 1.5, 2.1, 0.1], [2.2, 1.4, 0.5, 0.9, 1.1]]
print(np.shape(train_X))
- 출력:
(4, 5)
- 벡터의 차원은 5이고, 문장의 길이가 4인 경우를 가정한 입력 => 4번의 시점(timesteps), 각 시점마다 5차원의 단어 벡터가 입력으로 사용
- RNN은 2D 텐서가 아니라 3D 텐서를 입력으로 받으므로 2D 텐서를 3D 텐서로 변경 => 배치 크기 1을 추가하기
- 코드 :
train_X = [[[0.1, 4.2, 1.5, 1.1, 2.8], [1.0, 3.1, 2.5, 0.7, 1.1], [0.3, 2.1, 1.5, 2.1, 0.1], [2.2, 1.4, 0.5, 0.9, 1.1]]]
train_X = np.array(train_X, dtype=np.float32)
print(train_X.shape)
- 출력:
(1, 4, 5)
- (batch_size, timesteps, input_dim)에 해당되는 (1,4,5)의 크기를 가지는 3D 텐서 생성. batch_size는 한 번에 rNN이 학습하는 데이터의 양을 의미하지만, 여기서는 샘플이 하나밖에 없으므로 batch_size는 1이다.
2. SimpleRNN 이해하기
- 대표적인 인자로 return_sequences와 return_state가 있음. 기본값은 둘 다 False. 은닉 상태의 크기를 3으로 지정한 출력값을 확인해보자
- 코드:
rnn = SimpleRNN(3)
# rnn = SimpleRNN(3, return_sequence=False, return_state = False)와 동일
hidden_state = rnn(train_X)
print('hidden state : {}, shape : {}'.format(hidden_state, hidden_state.shape))
- 출력:
hidden state : [[-0.866719 0.95010996 -0.99262357]], shape: (1, 3)
- (1,3) 크기의 텐서가 출력되는데, 이는 마지막 시점의 은닉 상태. 은닉 상태의 크기를 3으로 지정했음을 주목하자. return_sequences가 False인 경우에는 마지막 시점의 은닉 상태만 출력.
- return_sequences=True일때 코드:
rnn = SimpleRNN(3, return_sequences = True)
hidden_states = rnn(train_X)
print('hidden state : {}, shape {}'.format(hidden_state, hidden_state.shape))
- 출력 :
hidden states : [[[ 0.92948604 -0.9985648 0.98355013]
[ 0.89172053 -0.9984244 0.191779 ]
[ 0.6681082 -0.96070355 0.6493537 ]
[ 0.95280755 -0.98054564 0.7224146 ]]], shape: (1, 4, 3)
- (1,4,3) 크기의 텐서가 출력됨. 입력 데이터는 (1,4,5) 크기를 가지는 3D 텐서였고, 그 중 4가 시점에 해당하는 값이므로 모든 시점에 대해서 은닉 상태의 값을 출력하여 (1,4,3) 크기의 텐서를 출력.
- return_state가 True일 경우에는 return_sequences에 상관없이 마지막 시점의 은닉 상태를 출력.
- 코드 :
rnn = SimpleRNN(3, return_sequences = True, return_state = True)
hidden_states, last_state = rnn(train_X)
print('hidden state : {}, shape : {}'.format(hidden_states, hidden_states.shape))
print('last hidden state : {}, shape : {}'.format(last_state, last_states.shape))
- 출력 :
hidden states : [[[ 0.29839835 -0.99608386 0.2994854 ]
[ 0.9160876 0.01154806 0.86181474]
[-0.20252597 -0.9270214 0.9696659 ]
[-0.5144398 -0.5037417 0.96605766]]], shape: (1, 4, 3)
last hidden state : [[-0.5144398 -0.5037417 0.96605766]], shape: (1, 3)
- 첫번째 출력은 return_sequences = True로 인한 출력으로 모든 시점의 은닉 상태
- 두번째 출력은 return_state = True로 인한 출력으로 마지막 시점의 은닉 상태.
- 이번에는 return_sequences가 False, return_state =True인 경우 코드 :
rnn = SimpleRNN(3, return_sequences = False, return_state = True)
hidden_state, last_state = rnn(train_X)
print('hidden state : {}, shape : {}'.format(hidden_state, hidden_state.shape))
print('last hidden state : {}, shape: {}'.format(last_state, last_state.shape))
- 출력:
hidden state : [[0.07532981 0.97772664 0.97351676]], shape: (1, 3)
last hidden state : [[0.07532981 0.97772664 0.97351676]], shape: (1, 3)
- 두 개의 출력 모두 마지막 시점의 은닉 상태를 출력하게 됨
3. LSTM 이해하기
- 실제로 SimpleRNN이 사용되는 경우는 거의 없고, LSTM이나 GRU를 주로 사용. 그 중 LSTM을 사용할 경우를 보자.
- 코드 :
lstm = LSTM(3, return_sequences = False, return_state = True)
hidden_state, last_state, last_cell_state = lstm(train_X)
print('hidden state : {}, shape : {}'.format(hidden_state, hidden_state.shape))
print('last hidden state : {}, shape: {}'.format(last_state, last_state.shape))
print('last cell state : {}, shape : {}'.format(last_cell_state, last_cell_state.shape))
- 출력:
hidden state : [[-0.00263056 0.20051427 -0.22501363]], shape: (1, 3)
last hidden state : [[-0.00263056 0.20051427 -0.22501363]], shape: (1, 3)
last cell state : [[-0.04346419 0.44769213 -0.2644241 ]], shape: (1, 3)
- SimpleRNN과 달리 세 개의 결과를 반환. return_sequences가 False이므로 우선 첫번째 결과는 마지막 시점의 은닉 상태.
- SimpleRNN과 다른 점은 return_state를 True로 둔 경우에는 마지막 시점의 은닉 상태뿐만 아니라 셀 상태까지 반환한다는 점.
- return_sequences = True 코드:
lstm = LSTM(3, return_sequences=True, return_state=True)
hidden_states, last_hidden_state, last_cell_state = lstm(train_X)
print('hidden states : {}, shape: {}'.format(hidden_states, hidden_states.shape))
print('last hidden state : {}, shape: {}'.format(last_hidden_state, last_hidden_state.shape))
print('last cell state : {}, shape: {}'.format(last_cell_state, last_cell_state.shape))
- 출력:
hidden states : [[[ 0.1383949 0.01107763 -0.00315794]
[ 0.0859854 0.03685492 -0.01836833]
[-0.02512104 0.12305924 -0.0891041 ]
[-0.27381724 0.05733536 -0.04240693]]], shape: (1, 4, 3)
last hidden state : [[-0.27381724 0.05733536 -0.04240693]], shape: (1, 3)
last cell state : [[-0.39230722 1.5474017 -0.6344505 ]], shape: (1, 3)
- 두번째 출력값은 마지막 은닉 상태, 세번째 출력값은 셀 상태인 것은 변함없지만, return_state=True이므로 첫번째 출력값은 모든 시점의 은닉 상태가 출력
4. Bidirectional(LSTM) 이해하기
- 양방향 LSTM의 출력값을 확인해보자. return_sequences가 True인 경우와 False인 경우에 대해서 은닉 상태의 값이 어떻게 바뀌는지 직접 비교하기 위해 이번에는 출력되는 은닉 상태의 값을 고정.
- 코드 :
k_init = tf.keras.initializers.Constant(value = 0.1)
b_init = tf.keras.initializers.Constant(value=0)
r_init = tf.keras.initializers.Constant(value = 0.1)
- return_sequences가 False이고, return_State가 True인 경우 코드 :
bilstm = Bidirectional(LSTM(3, return_sequences = False, return_state = True, kernel_initializer=k_init,
bias_initializer = b_nit, recurrent_initializer = r_init)
hidden_states, forward_h, forward_c, backward_h, backward_c = bilstm(train_X)
print('hidden states : {}, shape: {}'.format(hidden_states, hidden_states.shape))
print('forward state : {}, shape: {}'.format(forward_h, forward_h.shape))
print('backward state : {}, shape: {}'.format(backward_h, backward_h.shape))
- 출력:
hidden states : [[0.6303139 0.6303139 0.6303139 0.70387346 0.70387346 0.70387346]], shape: (1, 6)
forward state : [[0.6303139 0.6303139 0.6303139]], shape: (1, 3)
backward state : [[0.70387346 0.70387346 0.70387346]], shape: (1, 3)
- return_state = True인 경우에는 정방향 LSTM의 은닉 상태와 셀 상태, 역방향 LSTM의 은닉 상태와 셀 상태 4가지를 반환하기 때문에 은닉상태까지 총 5가지의 값을 반환.
- 첫 번째 출력값의 크기가 (1,6)인 것에 주목해보자. 이는 return_sequences가 False인 경우 정방향 LSTM의 마지막 시점의 은닉 상태와 역방향 LSTM의 첫번째 시점의 은닉 상태가 연결된 채 반환되기 때문.
- return_state가 True인 경우에 반환한 은닉 상태의 값인 forward_h와 backward_h는 각각 정방향 LSTM의 마지막 시점의 은닉 상태와 역방향 LSTM의 첫번째 시점의 은닉 상태값. 이 두 값을 연결한 값이 hidden_states에 출력되는 값.
-정방향 LSTM의 마지막 시점의 은닉 상태값과 역방향 LSTM의 첫번째 은닉 상태값을 기억하자.
정방향 LSTM의 마지막 시점의 은닉 상태값 : [0.6303139 0.6303139 0.6303139]
역방향 LSTM의 첫번째 시점의 은닉 상태값 : [0.70387346 0.70387346 0.70387346]
- 현재 은닉 상태의 값을 고정시켜두었기 때문에 return_sequences를 True로 할 경우 출력이 어떻게 변하는지 비교가 가능함. 코드 :
bilstm = Bidirectional(LSTM(3, return_sequences=True, return_state=True, \
kernel_initializer=k_init, bias_initializer=b_init, recurrent_initializer=r_init))
hidden_states, forward_h, forward_c, backward_h, backward_c = bilstm(train_X)
print('hidden states : {}, shape: {}'.format(hidden_states, hidden_states.shape))
print('forward state : {}, shape: {}'.format(forward_h, forward_h.shape))
print('backward state : {}, shape: {}'.format(backward_h, backward_h.shape))
-출력:
hidden states : [[[0.3590648 0.3590648 0.3590648 0.70387346 0.70387346 0.70387346]
[0.5511133 0.5511133 0.5511133 0.5886358 0.5886358 0.5886358 ]
[0.5911575 0.5911575 0.5911575 0.39516988 0.39516988 0.39516988]
[0.6303139 0.6303139 0.6303139 0.21942243 0.21942243 0.21942243]]], shape: (1, 4, 6)
forward state : [[0.6303139 0.6303139 0.6303139]], shape: (1, 3)
backward state : [[0.70387346 0.70387346 0.70387346]], shape: (1, 3)
- hidden states의 출력값에선는 이제 모든 시점의 은닉 상태가 출력됨. 역방향 LSTM의 첫번째 시점의 은닉상태는 더이상 정방향 LSTM의 마지막 시점의 은닉상태와 연결됨.
8장도 길어서 (2)편으로 이어서 써야 할듯..! 완벽히 이해되지는 않았지만 그래도 계속 하다보면 이해되지 않을까..😅

'공부 > 자연어 처리' 카테고리의 다른 글
| [딥러닝을 이용한 자연어 처리 입문] Chapter 8. 순환 신경망 (Recurrent Neural Network) (2) (1) | 2025.04.02 |
|---|---|
| [딥러닝을 이용한 자연어 처리 입문] Chapter 7. 딥 러닝(Deep Learning) 개요 (2) (0) | 2025.03.21 |
| [딥러닝을 이용한 자연어 처리 입문] Chapter 7. 딥 러닝(Deep Learning) 개요 (1) (0) | 2025.03.04 |
| [딥러닝을 이용한 자연어 처리 입문] Chapter 9. 워드 임베딩 (Word Embedding) (2) | 2024.08.06 |
| [딥러닝을 이용한 자연어 처리 입문] Chapter 6. 머신 러닝 개요 (2) | 2024.07.26 |