기본기 다지기/CNN부터 Attention까지 구현

[밑시딥] RNN 구현하기

syveany 2024. 9. 23. 19:44

RNN 구현하기

참고문헌: 책 『밑바닥부터 시작하는 딥러닝2』 Chapter5. 순환 신경망(RNN)

~ 목차 ~
0. 서론- 왜 RNN을 구현하고 있는가

  1. RNN 개념
    1.1 CNN의 문제점
    1.2 RNN 설명
  2. RNN 구현
  3. Time RNN 구현(계층 전체)
  4. 소감

0. 서론- 왜 RNN을 구현하고 있는가

  • 저번 CNN 구현 포스트에서 이어지는 맥락으로, 기본기를 다지고 싶었다. 이번 RNN 구현 역시 딥러닝 기초 국룰 책인 『밑바닥부터 시작하는 딥러닝2』를 참고했다.
  • 한 번에 모든 걸 이해하려고 하면 역효과가 날 수 있으니 지금 이 순간 최선을 다해서 이해해고 넘가는 느낌으로 가고자 한다.

1. RNN 개념

1.1 CNN의 문제점

  • CNN은 신호가 한 방향으로만 전달되는 Feed Forward 유형의 신경망으로, 시계열 데이터를 다루지 못함
  • 이를 보완하기 위해서 RNN이 등장함
  • RNN을 사용하면 맥락이 아무리 길더라도 맥락의 정보를 기억할 수 있음

1.2 RNN 설명

  • R(Recurrent) N(Neural) N(Network)로, 순환하는 신경망이라는 뜻
    (오잉? 신경망이 순환한다는 게 무슨 소리지)

image.png

  • 그림과 같이 하나의 출력이 복제되어서 화살표 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.: 우와 다시 보니 더 이해가 간다.