(Recurrent Neural Network) '몇 번이나 반복해서 일어나는 일' 즉, 순환하는 신경망이라는 뜻이다.
forward NN이나 CNN과 달리 RNN는 데이터의 순차구조로 인식하기 위해 데이터를 시간 순서대로 하나씩 입력 받는다.
시간적 공간적 순서 관계가 있는 데이터를 '순차 데이터'라고 부르는데 시공간의 순서에 관한 context를 가지고 있다.
forward NN이나 CNN과 달리 RNN는 데이터의 순차구조로 인식하기 위해 데이터를 시간 순서대로 하나씩 입력 받는다.
일반적으로 RNN은 은닉 계층으로 이루어지며 은닉 계층은 여러 계층이 될 수 있으나 일반적으로 은닉 계층을 깊게 쌓아도 성능이 크게 향상 하지 않아 한 두 계층만 쌓는다.
이전의 어떤 정보가 추가적으로 왔다라는 의미이다.
이전 메모리+ 현재 입력을 함께 고려하는 구조
즉, 한참 전의 메모리도 같이 고려해 출력하게 됨.
이렇게 RNN의 체인처럼 이어지는 성질은 바로 sequence나 list로 이어지는 것을 알려준다.
이런 데이터를 다루기에 최적화된 구조의 neural network인 것이다.
ref)https://dgkim5360.tistory.com/entry/understanding-long-short-term-memory-lstm-kr
은닉 계층은 '이전 상태, 새로운 입력'을 받아서 현재 상태를 매핑한다.
이 식을 전개해 보면.
입력은 시퀀스인데 출력은 시퀀스가 아니다 -> many to one
입,출력이 시퀀스다 -> many to many
입력은 시퀀스가 아니지만 출력은 시퀀스다 -> one to many
입력을 양쪽으로 살펴보는 방식.
상대적인 순서를 따지기 때문에 양방향으로 살펴보고 판단.
예를 들어, 기계 번역을 할 떄 문장을 양방향으로 보는 것이 효과가 좋다.
방식)
입력 데이터를 순방향 계층과 역방향 계층에 모두 입력한다.
순방향 계층과 역방향 계층의 출력을 출력 계층에 입력되며,
출력 계층에서는 두 결과를 합쳐서 예측한다.
기존 바닐라 RNN같은 경우 시계열 데이터의 장기 의존 관계를 학습하기 어려운데, BPTT(BackPropagation Through Time)에서 'Gradient Vanishing&Exploding이 문제가 된다.
마지막 단계 역전파
세번째 단계 역전파
단계별 역전파
은닉 계층부터는 순차적으로 실행된다.
이를 RNNLM를 예를 든다면,
Tom was watching TV in his room. Mary came into the room. Mary said hi to ( ? )
빈칸에 들어갈 단어는 'TOM'이 된다. 이 답의 정보는 RNN 계층의 은닉 상태에 인코딩해 보관해두기때문에 가능하다.
그럼 정답 레이블이 'TOM'이 주어졌을 때, BPTT로 수행함에 따라 이 시점으로부터 과거 방향으로 기울기를 전달하게 된다.
기본 바닐라 RNN은 최적화하기 어렵고 성능적 한계가 존재. 이것을 극복하기 위해 나온 것이 'LSTM', 'GRU'와 같은 셀 구조를 갖는 순환 신경망이 등장
문제점)
바닐라 RNN은 시간이 지나면서 입력데이터의 영향이 점점 사라지는 '장기 의존성 문제'와 '기울기 소실과 폭발'이 쉽게 일어나는 구조적 문제를 가짐.
장기의존성
컨텍스트 범위가 넓을 때 멀리 떨어진 입력에 대한 의존성이 있음에도 불구하고 입력의 영향이 점점 사라지는 현상. 오래된 입력 정보가 사라져 정확한 예측 힘듬.
기울기 소실과 폭발
(은닉계층)
기본 순환 신경망은 가중치 행렬 W가 반복적으로 곱해지는 구조여서 기울기 소실과 폭발이 쉽게 일어난다.
g: 기울기, v: 임계치
즉, 가파른 절벽으 만나 기울기가 급격히 커져 엉뚱한 방향으로 흘러가는 것을 막는다.
ResNet처럼 기울기가 소실되지 않는 구조로 바꾼 것이 'LSTM'
인터페이스를 보면 c(cell)라는 경로가 있고, 이것이 기억 매커니즘이다.
이 셀은 LSTM 계층 내에서만 주고 받고, 출력은 은닉 상태 벡터 h뿐이다.
LSTM에는 기억 셀 ct가 있다. c_t에서 시각 t에서의 LSTM의 기억이 저장돼 있는데, 과거로부터 시각 t까지의 필요한 모든 정보가 저장돼 있다고 가정한다. 그리고 필요한 정보를 모두 간직한 이 기억을 바탕으로, 외부 계층에 은닉 상태 h_t를 출력한다. 이때 출력하는 h_t는 아래 그림과 같이 기억 셀의 값을 tanh함수로 변환한 값이다.
Gate>
댐에서 물을 흘려보내는 것처럼 다음 단계로 얼마나 흘려보낼지를 조절한다.
정리 step by step
gru는 lstm의 장점을 유지하면서 게이트 구조로 단순하게 만든 순환 신경망이다.
GRU는 게이트 메커니즘이 적용된 RNN 프레임워크의 일종, LSTM에 영감을 받았고, 더 간략한 구조를 가지고 있다.
Reset Gate
과거의 정보를 적당히 리셋시키는게 목적, sigmoid를 출력으로 이용해(0,1)값을 이전 은닉층에 곱해준다.
직전 시점의 은닉층의 값과 현시점의 정보에 가중치를 곱하여 얻을 수 있다.
Update Gate
Update gage는 LSTM의 forget gate와 input gate를 합쳐놓은 느낌으로 과거와 현재의 정보의 최신화 비율을 결정한다.
Candidate
현 시점의 정보 후보군을 계산하는 단계이다. 핵심은 과거 은닉층의 정보를 그래도 이용하지 않고 리셋 게이트의 결과를 곱하여 이용해준다.
은닉층 계산
마지막, update gate결과와 candidate 결과를 결합하여 현 시점의 은닉층을 계산하는 단계, sigmoid 함수의 결과는 현시점 결과의 정보의 양을 결정하고, 1-sigmoid함수의 결과는 과거 시점의 정보 양을 결정한다.
정리
LSTM과 구조상 큰 차이도 없고, 분석 결과도 큰 차이가 없는 것으로 알려져있다. 주제별로 LSTM, GRU가 좋다는 의미, 하지만 GRU가 학습할 가중치가 적다는 것은 확실한 이점이 있다.
(LSTM의 1/4)
(GRU는 LSTM에 비해 셀 구조가 단순해지고 연산량은 줄었지만 성능은 LSTM과 비슷하다.?)
Ref)https://yjjo.tistory.com/18
기울기 소실과 기울기 폭발의 원인을 보자면
역전파로 전해지는 기울기는 차례로 'tanh', '+', 'MatMul(행렬곱)' 연산을 통과하게 된다.
(+는 그냥 흘려보내므로 나머지 두 연산의 의미를 보자.)
<역전파 시 MatMul에서 기울기 변화를 보자>
-> 기울기 dh는 시간 크기에 비례하여 지수적으로 증가한다. 이것이 '기울기 폭발' 이다.
이것은 오버플로을 일으켜 NaN(Not a Number)같은 값을 발생시켜 학습을 제대로 시킬 수 없게 된다.
'''
Wh = np.random.randn(H, H)x0.5
'''
-> 이번엔 처음과 반대로 지수적으로 감소하는 것을 볼 수 있다. 이것이 '기울기 소실' 이다.
이것은 일정 수준 이하로 작아지면 가중치 매개변수가 더 이상 갱신되지 않아, 장기 의존 관계를 학습할 수 없다.
그래서 '기울기 폭발' 해결책으로.
--
LSTM 구현
(클래스)
초기화 인수는 가중치 매개변수인 Wx와 Wh, 그리고 편향 b
Time LSTM 구현
class TimeRNN:
def __init__(self, Wx, Wh, b, stateful=False):
self.params = [Wx, Wh, b]
self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)]
self.layers = None # RNN 계층을 리스트로 저장
self.h, self.dh = None, None
self.stateful = stateful
def set_state(self, h):
'''hidden state(h)를 설정하는 메서드'''
self.h = h
def reset_state(self):
'''hidden state(h)를 초기화하는 메서드'''
self.h = None
def forward(self, xs):
Wx, Wh, b = self.params
N, T, D = xs.shape # N(batch), T(time steps), D(input size)
D, H = Wx.shape
self.layers = []
hs = np.empty((N, T, H), dtype='f')
if not self.stateful or self.h is None:
self.h = np.zeros((N, H), dtype='f')
for t in range(T):
layer = RNN(*self.params)
self.h = layer.forward(xs[:, t, :], self.h)
hs[:, t, :] = self.h
self.layers.append(layer)
return hs
def backward(self, dhs):
Wx, Wh, b = self.params
N, T, H = dhs.shape
D, H = Wx.shape
dxs = np.empty((N, T, D), dtype='f')
dh = 0
grads = [0, 0, 0]
for t in reversed(range(T)):
layer = self.layers[t]
dx, dh = layer.backward(dhs[:, t, :] + dh) # 합산된 기울기
dxs[:, t, :] = dx
for i, grad in enumerate(layer.grads):
grads[i] += grad
for i, grad in enumerate(grads):
self.grads[i][...] = grad
self.dh = dh
return dxs
RNNNL 추가 개선 작업
1) 계층 다층화 작업
-> 기존 LSTM계층을 1단으로 쌓아 사용했는데, 층을 겹겹히 2, 3층으로 쌓아 모델의 정확도를 기대할 수 있다.
2) 드롭아웃에 의한 과적합 억제
층을 깊게 쌓아 성능을 높이는 장점이 있지만 자칫 오버피팅을 불러일으킨다.
이에 대한 대책은 지금도 연구되는 주제다.
(흔한 방법으로 '데이터양 증가', '모델 복잡도 단순화'가 있다. 그 외에 '정규화'도 있음.)
(뉴런을 무작위로 선택하여 무시)
그럼 어디에 드랍아웃을 어디에 적용해야 하는가?
시계열 방향으로 삽입하는 것은 좋지 않다.?
그 이유는 RNN에서 시계열 방향으로 드랍아웃을 학습 시에 넣어버리면 흐르는 시간에 비례해 노이즈가 축적되어 정보가 사라질 수 있다.
대신 드랍아웃 계층을 위아래로 삽입하는 방법을 생각한다.
이렇게 하면 시간 방향(좌우 방향)으로 진행해도 정보 손실이 없다.(시간축과는 독립적으로 상하 방향에난 영향을 주게 된다.)
하지만, 이를 해결할 대안으로 나온 것이 '변형 드롭아웃'이다.
이 '변형 드롭아웃'은 깊이 방향, 시간 방향에도 이용할 수 있어 언어 모델의 정화도를 더욱 더 향상시킬 수 있다.
(*마스크= 데이터의 '통과/차단'을 결정하는 바이너리 형태의 무작위 패턴을 말한다.)
진행 방향에 변화를 준 RNN이다.
배가 너무 [ ] 빵을 먹었다. 라는 예문이 있다면, 빈칸에 들어갈 말은 쉽게 유추가 가능하다.
그말은 아마도 빵을 먹었다 는 말 때문이다. 만약 순방향이었다면 '배가 불러 빵을 먹었다.'도 될 수 있다. 이를 해결하기 위한 것이 양방향 RNN이다.
텐서플로우에서 적용시키려면 사용하고자 하는 레이어를 tf.keras.layers.Bidirectional()로 감싸주면 된다.
ex)tf.keras.layers.Bidirectional(
tf.keras.layers.SimpleRNN(units=64, use_bias=False, return_sequences=True)
)
쉽게 말해 RNN은 y값이 확 변하는데, LSTM은 세개의 게이트를 통해 변화를 점진적으로 준다.
예)ResNet (Residual block)
단순한 RNN의 학습에서는 기울기 소실과 기울기 폭발이 문제가 된다.
기울기 폭발에는 기울기 클리핑, 기울기 소실에는 게이트가 추가된 (LSTM이나 GRU)이 효과적
LSTM에는 input, forget, output게이트 등 3개의 게이트가 있다.
게이트에는 전용 가중치가 있으며, 시그모이드 함수를 사용하여 0.0~1.0 사이의 실수를 출력한다.
언어 모델 개선에는 LSTM 계층 다층화, 드롭아웃, 가중치 공유 등의 기법이 효과적이다.
RNN의 정규화는 중요한 주제이며, 드롭아웃 기반의 다양한 기법이 제안되고 있다.
<참고-밑씨딥2_chapter6>
Long Short-Term Memory
기울기 소실 문제를 해결하기 위해 고안된 RNN 레이어
LSTM은 RNN이 진화한 형태이다. RNN을 길게 이으면 훈련 단계는 네트워크를 통해 역전파되는 경사도를 매우 작거나 크게 만들어서 가중치를 0이나 무한대로 만들 수 있다.
딥러닝 네트워크는 backpropergation을 통해 학습을 하는데, 입력값이 길수록 입력된 단어들의 미분 값이 매우 작아지거나 커진다. 너무 작아지는 것을 Vanishing Gradient, 너무 커지면 Exploding Gradient라 한다. LSTM은 gradient vanishing에 강하다.
이 문제를 완화하기 위해서 LSTM에서는 각 단계에 두 개의 출력을 둔다. 하나는 모델의 실제 출력이고 다른 하나는 해당 단계의 내부 상태로 메모리라고 한다.
-기본적인 바닐라(standard)RNN 구조
-LSTM
좀 더 자세히
체인과 같은 구조를 가지고 있지만, 각 반복 모듈은 다른 구조를 가지고 있다. 단순한 neural network layer 한 층 대신에, 4개의 layer가 특별한 방식으로 서로 정보를 주고 받도록 되어 있다.
-Step by step
결국 목적은 내가 현재 입력과 이전 출력가지고 cell state에 값을 집어 넣고 어떻게 밖으로 출력하지를 결정하는 것이다.
실제로 사용할 땐 뉴럴 네트워크 고려할 필요없이
입력과 출력을 정해주고 초기화만 잘 시켜주면 텐서플로우가 알아서 해준다.
가중치 공유
가중치 공유는 Embedding 계층의 가중치와 Affine 계층의 가중치를 연결하는 기법이다. 두 계층이 가중치를 공유함으로써 학습하는 매개변수 수가 크게 줄어드는 동시에 정확도도 향상되는 일석이조의 기술이다.
개선된 RNNLM 구현
정리- 개선점 3가지
1) LSTM 계층의 다층화
2) 드롭아웃 사용(깊이 방향으로)
3) 가중치 공유
이 세 가지 개선점이 들어간 BettherRnnlm 클래스
'''
import sys
sys.path.append('..')
from common.time_layers import
from common.np import # import numpy as np
from common.base_model import BaseModel
class BetterRnnlm(BaseModel):
LSTM 계층을 2개 사용하고 각 층에 드롭아웃을 적용한 모델이다.
아래 [1]에서 제안한 모델을 기초로 하였고, [2]와 [3]의 가중치 공유(weight tying)를 적용했다.
[1] Recurrent Neural Network Regularization (https://arxiv.org/abs/1409.2329)
[2] Using the Output Embedding to Improve Language Models (https://arxiv.org/abs/1608.05859)
[3] Tying Word Vectors and Word Classifiers (https://arxiv.org/pdf/1611.01462.pdf)
'''
def __init__(self, vocab_size=10000, wordvec_size=650,
hidden_size=650, dropout_ratio=0.5):
V, D, H = vocab_size, wordvec_size, hidden_size
rn = np.random.randn
embed_W = (rn(V, D) / 100).astype('f')
lstm_Wx1 = (rn(D, 4 * H) / np.sqrt(D)).astype('f')
lstm_Wh1 = (rn(H, 4 * H) / np.sqrt(H)).astype('f')
lstm_b1 = np.zeros(4 * H).astype('f')
lstm_Wx2 = (rn(H, 4 * H) / np.sqrt(H)).astype('f')
lstm_Wh2 = (rn(H, 4 * H) / np.sqrt(H)).astype('f')
lstm_b2 = np.zeros(4 * H).astype('f')
affine_b = np.zeros(V).astype('f')
# 이 부분이 세 가지 개선 요소가 적용됨!
self.layers = [
TimeEmbedding(embed_W),
TimeDropout(dropout_ratio),
TimeLSTM(lstm_Wx1, lstm_Wh1, lstm_b1, stateful=True),
TimeDropout(dropout_ratio),
TimeLSTM(lstm_Wx2, lstm_Wh2, lstm_b2, stateful=True),
TimeDropout(dropout_ratio),
TimeAffine(embed_W.T, affine_b) # weight tying!!
]
self.loss_layer = TimeSoftmaxWithLoss()
self.lstm_layers = [self.layers[2], self.layers[4]]
self.drop_layers = [self.layers[1], self.layers[3], self.layers[5]]
self.params, self.grads = [], []
for layer in self.layers:
self.params += layer.params
self.grads += layer.grads
def predict(self, xs, train_flg=False):
for layer in self.drop_layers:
layer.train_flg = train_flg
for layer in self.layers:
xs = layer.forward(xs)
return xs
def forward(self, xs, ts, train_flg=True):
score = self.predict(xs, train_flg)
loss = self.loss_layer.forward(score, ts)
return loss
def backward(self, dout=1):
dout = self.loss_layer.backward(dout)
for layer in reversed(self.layers):
dout = layer.backward(dout)
return dout
def reset_state(self):
for layer in self.lstm_layers:
layer.reset_state()