NIRVANA

[밑바닥부터 시작하는 딥러닝2] 6장. 게이트가 추가된 RNN 본문

AI

[밑바닥부터 시작하는 딥러닝2] 6장. 게이트가 추가된 RNN

녜잉 2024. 3. 20. 16:05

RNN의 문제점 

  • 앞 장에서 설명한 RNN은 시계열 데이터의 장기 의존 관계를 학습하기 어려움 
    • BPTT에서 기울기 소실 혹은 기울기 폭발이 발생하기 때문 

RNN 복습 

 

기울기 소실 또는 기울기 폭발 

Tom was watching TV in his room. Mary came into the room. Mary said hi to [ ? ] 

  • RNNLM이 위의 문제에 올바르게 답하기 위해서는 현재 맥락에서 톰이 TV를 보고 있음과 그 방에 마리가 들어옴이란 정보를 기억해둬야 함 
    • 정답 레이블이 톰임을 학습 할 때, 중요한 것은 RNN 계층의 존재 → RNN 계층이 과거 방향으로 '의미 있는 기울기'를 전달함으로써 시간 방향의 의존 관계를 학습하게 됨
  • 기울기는 학습해야 할 의미가 있는 정보가 들어 있고, 그것을 과거로전달함으로써 장기 의존 관계를 학습하게 됨 
    • 만약 기울기가 사그라들면, 가중치 매개변수는 전혀 갱신되지 않게 됨 → 장기 의존 관계를 학습할 수 없게 됨 
    • 현재의 단순한 RNN 계층에서는 시간을 거슬러 올라갈 수록, 기울기가 작아지거나(기울기 소실) 혹은 커지게(기울기 폭발)됨 

기울기 소실과 기울기 폭발의 원인 

  • 역전파로 전해지는 기울기는 차례대로 'tanh', '+', 'MatMul(행렬 곱)' 연산을 통과하게 됨
  • '+'의 역전파: 상류에서 전해지는 기울기를 그대로 하류에 흘려보냄 
  • 'tanh'의 역전파:  y = tanh(x)의 미분은 x가 0에서 멀어질 수록 점점 작아지게 됨 → 역전파에서 기울기는 tanh 노드를 지날 때 마다 값이 계속 작아지게 됨
  • MatMul(행렬곱)의 역전파: tanh를 제외하면, RNN 계층의 역전파 시 기울기는 행렬 곱 연산에 의해서만 변화하게 됨 
    • 행렬 곱셈에서는 매번 똑같은 가중치를 사용하게 됨
  • 같은 가중치를 T번 반복하여 곱했기 때문에 기울기의 크기가 지수적으로 증가하거나 감소하게 됨
    • 가중치가 스칼라일 경우, 가중치가 1보다 크면 지수적으로 증가하고 가중치가 1보다 작으면 지수적으로 감소함
    • 가중치가 행렬인 경우, 특잇값이 1보다 크면 지수적으로 증가하고, 가중치가 보다 작으면 지수적으로 감소함(대부분의 경우)
      • 행렬의 특잇값: 데이터가 얼마나 퍼져 있는 지를 나타내는 척도 

기울기 폭발 대책 

  • 기울기 클리핑 
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):
  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)

  if rate < 1:
    for grad in grads:
      grad *= rate

 

기울기 소실과 LSTM

LSTM의 인터페이스 

  • C - 기억 셀: LSTM 전용 기억 메커니즘 
    • 기억셀은 LSTM 계층 내에서만 완결 되고, 다른 계층으로는 출력하지 않음 

LSTM 계층 조립하기 

  • LSTM의 기억 셀에는 과거에서부터 시각 t까지 필요한 모든 정보가 저장 되어 있음 
    • 필요한 정보를 모두 간직한 기억을 바탕으로 외부 계층에 은닉 상태 h_t를 출력
  • 기억셀 c_t는 3개의 입력(c_t-1, h_t-1, x_t)로부터 연산을 수행하여 구하게 됨 
    • c_t를 사용하여 은닉 상태 h_t를 계산하고, h_t = tanh(c_t)로 기억 셀에 tanh함수를 적용한 값이 됨 
  • 게이트(in LSTM): 다음 단계로 흘려보낼 데이터의 양을 제어함 
    • 게이트의 열림 상태는 0.0 ~ 1.0 사이의 실수로 나타냄 

 

OUTPUT 게이트 

  • h_t, 즉 tanh(c_t)는 '그것이 다음 시각의 은닉 상태에서 얼마나 중요한가'를 조정하는 역할을 담당함
  • 해당 게이트는 다음 은닉 상태의 h_t의 출력을 담당하는 게이트이므로 output 게이트(출력 게이트)라고 함 
  •  ouput게이트의 열림 상태는 입력x_t와 이전 상태h_t-1로부터 구함

O = σ(x_t W_x^(o) + h_t-1 W_h^(o) + b^(o) )

 

  • 위의 공식에서 도출 된 O와 tanh(c_t)의 원소 별 곱을 h_t로 출력하게 됨 

 h_t = O (아다마르 곱) tanh(c_t)

 

forget 게이트 

  • 이전 시각의 기억 셀, 즉 c_t-1의 기억 중에서 불필요한 기억을 잊게 해주는 게이트를 의미 

f = σ(x_t W_x^(f) + h_t-1 W_h^(f) + b^(f)

 

새로운 기억 셀

  • 불필요한 정보를 잊은 후, 필요한 정보를 기억할 추가적인 기억 셀이 필요함 
    • 이때, tanh연산을 다시 하게됨 → '게이트'가 아님에 주의, 새로운 정보를 기억 셀에 추가하기 위함임 
      • 활성화 함수로 시그모이드 함수가 아닌 tanh 함수가 사용되는 이유 

g = tanh(x_t W_x^(g) + h_t-1 W_h^(g) + b^(g)

  • 위의 식의 g가 이전 시각의 기억셀인 c_t-1에 더해짐으로써 새로운 기억 c_t가 생기게 되는 것임 

input 게이트

  • g의 각 원소가 새로 추가되는 정보로써의 가치가 얼마나 큰지를 판단하는 게이트 
    • 새 정보를 무비판적으로 수용하지 않고, 적절히 취사선택할 수 있도록 하는 것이 게이트의 역할이 됨
      • 또 다른 관점에서는, input 게이트를 통해 가중된 정보가 새로 추가되는 셈 

i = σ(x_t W_x^(i) + h_t-1 W_h^(i) + b^(i) ) 

 

LSTM 기울기의 흐름 

  • LSTM의 역전파, 특히 기억 셀의 역전파에서는 '+' 노드와 'x' 노드 만을 지나게 됨 
    • '+' 노드: 상류에서 전해지는 기울기를 그대로 흘리는 역할
    • 'x' 노드: 행렬 곱이 아닌, 원소별 곱(아마다르 곱)을 계산을 진행 
      • 매 시각 다른 게이트 값을 사용하여 원소별 곱을 계산, 곱셈의 효과가 누적되지 않아 기울기 소실이 발생하지 않는(혹은 어려워지는) 것 
  • forget 게이트가 'x' 노드의 계산을 제어, 잊어야 하는 기억 셀 원소에 대해서는 기울기가 작아지고, 기억해야 하는 기억 셀 원소에 대해서는 기울기가 약화되지 않은 채로 과거 방향에 전해짐 

 

LSTM 구현 

  • LSTM에서 수행하는 계산 목록 

 

  • 아핀 변환: 행렬 변환과 평행 이동(평향)을 결합한 상태, 즉 xW_x + hW_h + b를 의미) 
    • 4개의 가중치 혹은 편향을 하나로 모아 아핀 변환을 1회 실시 → 계산 속도가 빨라지게 됨

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.layer = None
    slef.h, self.c = None, None
    self.dh = None
    sefl.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):
      layer = LSTM(*self.params)
      self.h, self.c = layer.forward(xs[:, t, :], self.h, self.c)
      hs[:, t, :] = self.h

      self.layers.append(layer)

    return hs 

  
  def backward(self, dhs):
    Wx, Wh, b = self.params
    N, Tn, 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 , grads in enumerate(layer.grads):
        grads[i] += grad
    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

 

LSTM을 사용한 언어 모델 

///코드 추가 /// 

RNNLM 추가 개선 

LSTM  계층 다층화 

  • RNNLM으로 정확한 모델을 만들고자 한다면, 많은 경우 LSTM 계층을 깊게 쌓는 방법을 사용할 수 있음 
    • LSTM의 계층이 올라갈 수록 더 복잡한 패턴을 학습 할 수 있게 됨 

드롭아웃에 의한 과적합 억제

  • 층을 깊게 쌓게 되면, 과적합 문제를 야기할 수 있고, RNN은 일반적인 피드포워드 신경망 보다 쉽게 과적합을 일으키게 됨 
  • 과적합 억제 방법
    • 훈련 데이터의 양 늘리기
    • 모델 복잡도 줄이기
    • 정규화 
    • 드롭아웃
      • 훈련 시, 계층 내의 뉴런 몇 개를 무작위로 무시하고 학습하는 방법
  • 드롭 아웃을 시계열 방향에 넣어버리면, 학습 시 시간 흐름에 따라 정보가 사라질 수도 있음 
  • 드롭 아웃을 깊이 방향으로 삽입하면 학습 시(좌우 방향)에도 정보를 잃지 않게 됨
    • 시간축과는 독립적으로 깊이 방향(상하 방향)에만 영향을 주게 됨

 

가중치 공유

  • 언어 모델을 개선하는 아주 간단한 방법 중 하나, Embedding 계층의 가중치와 Affine 계층의 가중치를 연결하는 기법
    • 두 계층이 가중치를 공유함으로써 학습하는 매개변수 수가 크게 줄어들게 되고, 정확도도 향상되게 됨 
  • 어휘 수가 V, LSTM의 은닉 상태의 차원 수가 H일 때 
    • Embedding 계층의 가중치 형상: VxH
    • Affine 계층의 가중치 형상: HxV
    • 가중치 공유를 적용하려면, 임베딩 계층의 가중치를 전치하여 아핀 계층의 가중치로 설정하면 됨