LSTM 구현하기
참고문헌: 책 『밑바닥부터 시작하는 딥러닝2』 Chapter6. 게이트가 추가된 RNN
~ 목차 ~
0. 서론- 왜 LSTM을 구현하고 있는가
- LSTM 개념
1.1 RNN의 문제점
1.2 기울기 폭발 대책- 기울기 클리핑
1.3 기울기 소실 대책- LSTM
1.3.1 LSTM 설명
1.3.2 LSTM 구조
1.3.3 LSTM은 어떻게 기울기 소실을 방지하는가 - LSTM 구현
- Time LSTM 구현(계층 전체)
- 소감
0. 서론- 왜 LSTM을 구현하고 있는가
- 이것 역시 연휴맞이 기본기 다지기의 일환이다. 기본기라고 하기엔 너무 기본적인 이론인가 싶지만.. 쌓아가야지 뭐
- 이번에도 국룰 책 『밑바닥부터 시작하는 딥러닝2』의 Chapter6. 게이트가 추가된 RNN을 참고했다.
1. LSTM 개념
1.1 RNN의 문제점
- 기울기 소실이나 폭발때문에 장기 의존 관계를 학습하기가 어려움
- RNN의 역전파에서 기울기는 , +, matmul 연산을 통과하게 되는데,
- : 값의 미분값은 0에서 1사이의 값으로, 노드를 통과하면 통과할수록 기울기는 작아짐
- +: 영향을 미치지 않음
- matmul: 초기값에 따라서 기울기가 시간에 비례해서 폭발하거나 소실됨
- 가중치 가 스칼라일 때, > 1이면 지수적으로 증가하고 < 1이면 지수적으로 감소함
- 하지만 가중치 가 행렬일 때, 행렬의 '특잇값' 즉 데이터가 얼마나 퍼져 있는지가 기울기 변화의 척도가 됨
- 주의할 점: 특잇값의 최댓값이 1보다 크면 지수적으로 증가하고, 1보다 작으면 지수적으로 감소할 확률이 높음. 반드시 그런 건 아님(그런가보다하고 일단 이유는 패스)
- RNN의 역전파에서 기울기는 , +, matmul 연산을 통과하게 되는데,
1.2 기울기 폭발 대책- 기울기 클리핑
- 문턱값 threshold를 설정한 뒤, 기울기의 L2 norm(=)이 threshold를 초과하면 기울기를 수정하는 방법
import numpy as np
dW1 = np.random.rand(3,3) * 10
dW2 = np.random.rand(3,3) * 10
grads = [dW1, dW2]
max_norm = 5.0
def clip_grads(grads, max_norm):
# L2 norm 계산
total_norm = 0
for grad in grads:
total_norm += np.sum(grad ** 2)
total_norm = np.sqrt(total_norm)
# 클리핑 비율 계산
rate = max_norm / (total_norm + 1e-6)
# rate가 1보다 작으면 기울기 클리핑 실행, 아니면 변경x
if rate < 1:
for grad in grads:
grad *= rate
1.3 기울기 소실 대책- LSTM
1.3.1 LSTM 설명
- 기울기 폭발은 기울기 클리핑 기법을 사용하면 해결되지만, 기울기 소실은 전체적인 모델 구조를 뜯어 고쳐야 함
- 이렇게 등장한 모델이 LSTM(Long Short-Term Memory, 단기기억을 긴 시간동안 지속할 수 있다는 뜻)임
- RNN과 비교해서 봤을 때 LSTM에는 c라는 기억셀이 추가됨
1.3.2 LSTM 구조
- o(output)게이트
- 기억의 몇%만 흘려보낼지를 조정
- f(forget)게이트
- 무엇을 잊을지를 결정
- g(새로운 기억셀)
- 새로 기억해야 할 정보 추가
- i(input)게이트
- g의 정보를 적절히 취사선택
- 최대한 이해해보려고 노력은 했다..
+참고) tanh vs sigmoid
- tanh의 출력값은 -1.0 ~ 1.0 / sigmoid의 출력값은 0.0 ~ 1.0임
- tanh는 그 안에 인코딩된 정보의 강약을 표시 / sigmoid는 데이터를 얼마만큼 통과시킬지 정하는 비율
-> 따라서 sigmoid는 게이트에서, tanh는 정보를 지니는 데이터에서 주로 사용됨
1.3.3 LSTM은 어떻게 기울기 소실을 방지하는가
- 기억셀 c의 역전파에 주목해보면 LSTM이 기울기 소실을 어떻게 방지하는지 알 수 있음
- 아래 그림을 보면 역전파 과정에서 c는 +와 x노드를 지나게 됨
- +: 기울기 변화에 영향x
- x: 이 노드는 행렬곱이 아니라 원소별곱(아마다르곱)임. RNN에서는 매번 같은 가중치 행렬을 사용해서 행렬곱을 하기 때문에 기울기 소실/폭발이 일어나지만, LSTM에서는 매번 새로운 게이트값을 이용해서 원소별곱을 계산하기 때문에 곱셈의 효과가 누적되지 않아서 기울기 소실이 일어나지 않는(어려운) 것임 (..더 파고들진 않겠음. 다음 기회에..)
- x노드의 계산은 forget게이트가 제어함. forget게이트가 잊어야 한다고 판단한 기억셀의 원소의 기울기는 작아지고, 잊으면 안 된다고 판단한 기억셀의 원소의 기울기는 그대로 전해지는 원리임
2. LSTM 구현
- 구현하기 전에 f, g, i, o 모두에 있는 아핀 변환()을 하나의 아핀변환으로 합쳐줄거임. 이렇게 하면
- 장점1. 계산속도가 빨라짐 (∵ 행렬 라이브러리는 일반적으로 큰 행렬을 한꺼번에 계산할 때 속도가 빠르기 때문)
- 장점2. 소스코드가 간결해짐 (∵ 가중치를 하나로 모아서 관리하기 때문)
class LSTM:
def __init__(self, Wx, Wh, b):
self.params = [Wx, Wh, b]
self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)]
self.cache = None
def forward(self, x, h_prev, c_prev):
Wx, Wh, b = self.params
N, H = h_prev.shape
# 4개의 아핀변환을 1개의 아핀변환으로 바꿔줌
A = np.matmul(x, Wx) + np.matmul(h_prev, Wh) + b
# 슬라이스해서 데이터 꺼냄
f = A[:, :H]
g = A[:, H:2*H]
i = A[:, 2*H:3*H]
o = A[:, 3*H:]
f = sigmoid(f)
g = np.tanh(g)
i = sigmoid(i)
o = sigmoid(o)
c_next = f * c_prev + g * i
h_next = o * np.tanh(c_next)
self.cache = (x, h_prev, c_prev, i, f, g, o, c_next)
return h_next, c_next.
3. Time LSTM 구현(계층 전체)
- 이제 계층 전체를 한번에 구현해볼거임
class TimeLSTM:
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
self.h, self.c = None, None
self.dh = None
self.stateful = stateful
def forward(self, xs):
Wx, Wh, b = self.params
N, T, D = xs.shape
H = Wh.shape[0]
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')
if not self.stateful or self.c is None:
self.c = np.zeros((N, H), dtype='f')
for t in range(T):
# 매 시간단계에서 새로운 LSTM 레이어 생성
layer = LSTM(*self.params)
# 현재 시간단계의 은닉상태와 셀상태 계산
self.h, self.c = layer.forward(xs[:,t,:], self.h, self.c) # xs[:,t,:]: t시점의 입력데이터
# 은닉상태 저장
hs[:,t,:] = self.h
# 레이어를 리스트에 저장(나중에 역전파시 사용됨)
self.layers.append(layer)
return hs
def backward(self, dhss):
Wx, Wh, b = self.params
N, T, H = dhs.shape
D = Wx.shape[0]
dxs = np.empty((N, T, D), dtype='f')
dh, dc = 0, 0
grads = [0,0,0]
for t in reversed(range(T)):
layer = self.layers[t]
dx, dh, dc = layer.backward(dhs[:,t,:] + dh, dc)
dxs[:,t,:] = dx
# 각 파라미터에 대한 기울기 업데이트
for i, grad in enumerate(layer.grads):
grads[i] += grad
# grads에 저장된 기울기를 self.grads에 복사
for i, grad in enumerate(grads):
self.grads[i][...] = grad
# 마지막 은닉상태에 대한 기울기 저장
self.dh = dh
return dxs
# 외부에서 은닉상태랑 셀상태를 직접 설정할 수 있게 하는 수
def set_state(self, h, c=None):
self.h, self.c = h,c
# 은닉상태랑 셀상태 초기화하는 함수
def reset_state(self):
self.h, self.c = None, None
4. 소감
- 처음에는 이게 무슨소리야 했던 LSTM의 f, g, i, o 구조들이 반복해서 읽으면 읽을수록 조금씩 가닥이 잡히는 게 느껴진다.
- 이렇게 밑바닥 기초부터 하나씩 잡아나가니 안정감이 느껴져서 좋다.
- 라이브러리만 무작정 사용해서 경험을 하는 게 아니라 시간 날 때마다 원리부터 차근차근 공부해 가야겠다.
- 파이팅
... - 24.10.05.: 그 사이에 발전했다는 걸 느낀다. 뿌듯하다.
'기본기 다지기 > CNN부터 Attention까지 구현' 카테고리의 다른 글
[기본이론] Attention 구현(2/3) (1) | 2024.09.27 |
---|---|
[기본이론] Attention 구현(1/3) (1) | 2024.09.26 |
[기본이론] seq2seq 구현 (0) | 2024.09.23 |
[밑시딥] RNN 구현하기 (0) | 2024.09.23 |
[밑시딥] CNN 구현하기 (0) | 2024.09.23 |