RNN 구현하기
참고문헌: 책 『밑바닥부터 시작하는 딥러닝2』 Chapter5. 순환 신경망(RNN)
~ 목차 ~
0. 서론- 왜 RNN을 구현하고 있는가
- RNN 개념
1.1 CNN의 문제점
1.2 RNN 설명 - RNN 구현
- Time RNN 구현(계층 전체)
- 소감
0. 서론- 왜 RNN을 구현하고 있는가
- 저번 CNN 구현 포스트에서 이어지는 맥락으로, 기본기를 다지고 싶었다. 이번 RNN 구현 역시 딥러닝 기초 국룰 책인 『밑바닥부터 시작하는 딥러닝2』를 참고했다.
- 한 번에 모든 걸 이해하려고 하면 역효과가 날 수 있으니 지금 이 순간 최선을 다해서 이해해고 넘가는 느낌으로 가고자 한다.
1. RNN 개념
1.1 CNN의 문제점
- CNN은 신호가 한 방향으로만 전달되는 Feed Forward 유형의 신경망으로, 시계열 데이터를 다루지 못함
- 이를 보완하기 위해서 RNN이 등장함
- RNN을 사용하면 맥락이 아무리 길더라도 맥락의 정보를 기억할 수 있음
1.2 RNN 설명
- R(Recurrent) N(Neural) N(Network)로, 순환하는 신경망이라는 뜻
(오잉? 신경망이 순환한다는 게 무슨 소리지)
- 그림과 같이 하나의 출력이 복제되어서 화살표 2개로 갈라짐. 하나는 자기 자신한테 입력되고, 하나는 출력됨
- 순환하기 위해서는 닫힌 경로가 필요함. 순환 경로를 따라서 데이터가 순환하면서 과거의 정보를 기억할 수 있게됨
- [그림5-8]에 있는 RNN 모두가 1개의 계층을 이루고 있는 것임
- RNN의 순전파 계산식은 아래와 같음(밑에 코드 구현할 때 나와서 첨부했음)
$$
h_t = \tanh(W_h h_{t-1} + W_x x_t + b_h)
$$
2. RNN 구현
class RNN:
def __init__(self, Wx, Wh, b):
# 인수로 받은 가중치 2개와 편향 1개를 parmas에 저장
self.params = [Wx, Wh, b]
# 각 매개변수에 대응하는 형태로 기울기를 초기화한 후 grads에 저장
self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)]
# 역전파 계산할 때 사용하는 중간데이터를 담을 cach를 None으로 초기화
self.cache = None
def forward(self, x, h_prev): # 아래로부터의 입력x, 왼쪽으로부터의 입력 h_prev를 받음
Wx, Wh, b = self.params
# RNN 순전파 계산식
t = np.matmul(h_prev, Wh) + np.matmul(x, Wx) + b
h_next = np.tanh(t)
# 순전파 단계에서 계산된 중간데이터를 저장해서 나중에 역전파 단계에서 활용할 수 있게 함
self.cache = (x, h_prev, h_next)
return h_next
def backward(self, dh_next):
Wx, Wh, b = self.params
x, h_prev, h_next = self.cache
# 은닉상태 t의 기울기 계산
# dh_next를 tanh함수의 미분값에 곱해서 다음 시점에서 전파된 기울기를 현재 시점에 반영
dt = dh_next * (1 - h_next ** 2) # d/dh tanh(h) = 1 - tanh(h) ** 2 이므로 (1 - h_next ** 2)는 tanh함수의 미분값을 의미
# 편향 b의 기울기 계산
db = np.sum(dt, axis=0) # 편향은 각 샘플마다 동일하기 때문에 시간축을 따라서 모든 기울기를 합산하면 됨
# 가중치 Wh의 기울기 계산
dWh = np.matmul(h_prev.T, dt)
# 이전 은닉상태 h_prev의 기울기 계산
dh_prev = np.matmul(dt, Wh.T)
# 가중치 Wx의 기울기 계산
dWx = np.matmul(dt, Wh.T)
# 입력 x의 기울기 계산
dx = np.matmul(dt, Wx.T)
# 계산된 각 기울기 dWx, dWh, db를 grads리스트에 저장
# 나중에 가중치 업데이트 때 사용될 값을 저장하는 거임
self.grads[0][...] = dWx # [...]: 배열이 몇 차원이든 상관없이 전체 배열을 선택할 수 있음
self.grads[1][...] = dWh
self.grads[2][...] = db
# 입력 x와 이전 입력상태 h_prev에 대한 기울기 반환
return dx, dh_prev
- 아래 그림이랑 같이 보니 코드 이해에 아주 조금 도움이 되었음
3. Time RNN 구현(계층 전체)
- 이 책에서는 T개의 RNN으로 구성된 한 계층을 임의로 Time RNN이라고 부르고 있음
- Time RNN 계층은 은닉 상태를 인스턴스 변수 h로 보관해서 은닉 상태를 다음 블록에 인계할 수 있음. 이때 stateful 인수를 사용함
- stateful=True: '상태가 있다'. 은닉상태가 시퀀스간에 전달됨
- stateful=False: '상태가 없다'. 은닉상태가 시퀀스간에 전달되지 않고 독립적으로 처리됨. 은닉상태를 영행렬(모든 요소가 0인 행렬)로 초기화함
- 긴 시퀀스를 처리해야 하거나, 데이터가 끊어진 상태로 입력되더라도 연속적인 정보를 유지해야 할 때 stateful=True를 사용하면 됨
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
# 은닉상태 h, 역전파 시 사용될 은닉상태의 기울기 dh
self.h, self.dh = None, None
self.stateful = stateful
# 은닉상태 h를 설정하는 함수
def set_state(self, h):
self.h = h
# 은닉상태를 초기화하는 함수
def reset_state(self):
self.h = None
def forward(self, xs): # xs: 입력 시퀀스 데이터
Wx, Wh, b = self.params # Wx: 입력에 대한 가중치, Wh: 은닉상태에 대한 가중치
N, T, D = xs.shape
D, H = Wx.shape # D: 입력차원, H: 은닉상태차원
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):
# 각 시간 단계마다 RNN 레이어가 생성되어 입력데이터를 처리함
layer = RNN(*self.params)
# 현재시점의 입력 xt와 이전 시점의 은닉상태 ht-1를 사용해서 새로운 은닉상태 ht를 계산함
self.h = layer.forward(xs[:,t,:], self.h)
# 현재 시점의 은닉상태 ht를 저장
hs[:,T,:] = self.h
# 각 시간단계의 RNN레이어를 self.layers에 저장함
# 이렇게 하면 나중에 역전파 할 때 각 시간단계에서 계산된 정보를 활용할 수 있음
self.layers.append(layer)
# 은닉상태들의 배열 hs를 반환
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)):
# 순전파 시 저장된 각 시간단계의 RNN레이어를 가져옴
layer = self.layers[t]
# 현재 시점의 기울기 dhs와 다음 시간단계에서 전해진 기울기 dh를 더해서 역전파함
# 이 과정에서 시간축을 따라 기울기가 누적됨
dx, dh = layer.backward(dhs[:, t, :] + dh) # 합산된 기울기
# 각 시간단계에서 입력에 대한 기울기 dx를 저장함
dxs[:,t,:] = dx
# 각 시간단계에서의 RNN레이어에서 계산된 기울기를 누적함
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
4. 소감
- 코드 하나하나 이해해보려고 했지만 역시나 어렵다.
- 그래도 큰 흐름은 머릿속에 잡힌 것 같아서 좋다.
- 파이팅
.. - 24.10.05.: 우와 다시 보니 더 이해가 간다.
'기본기 다지기 > CNN부터 Attention까지 구현' 카테고리의 다른 글
[기본이론] Attention 구현(2/3) (1) | 2024.09.27 |
---|---|
[기본이론] Attention 구현(1/3) (1) | 2024.09.26 |
[기본이론] seq2seq 구현 (0) | 2024.09.23 |
[밑시딥] LSTM 구현하기 (0) | 2024.09.23 |
[밑시딥] CNN 구현하기 (0) | 2024.09.23 |