공부/밑바닥부터 시작하는 딥러닝

[밑시딥2] Chapter 6. 게이트가 추가된 RNN

셩잇님 2024. 7. 29. 23:40
반응형

 

 

본 포스팅은 '밑바닥부터 시작하는 딥러닝2'를 읽고 공부 및 학습 내용을 정리한 글입니다. 언제든지 다시 참고할 수 있도록, 지식 공유보단 개인적인 복습을 목적으로 포스팅하였습니다.

 

 

6.1 RNN의 문제점

 RNN은 장기 의존 관계를 학습하기 어렵다. 왜냐하면 BPTT에서 기울기 소실 혹은 폭발이 일어나기 때문이다.

 

6.1.1 RNN 복습

 RNN 계층은 시계열 데이터 xt를 입력하면 ht를 출력하는데 이 ht는 RNN의 은닉 상태라고 하며 과거 정보를 저장한다. 또한 RNN 특징으로는 바로 이전 시각의 은닉 상태를 이용하는 것이다.

 

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

 

 

 RNNLM이 위 문제에서 올바르게 대답하려면 현재 맥락에서 "Tom이 방에서 TV를 보고 있음"과 "그 방에 Mary가 들어옴"이란 정보를 기억해야 한다.

 

 

 정답 레이블이 Tom일 경우 과거 방향으로 기울기를 전달할텐데, 이 기울기는 학습해야 할 의미가 있는 정보와 그것을 과거로 전달함으로써 장기 의존 관계를 학습을 의미한다. 하지만 기울기가 중간에 사라지면 가중치 매개변수는 갱신이 되지 않는데, 현재의 RNN 계층은 시간을 거슬러 올라갈수록 기울기가 작아지거나, 커지는 둘 중 하나의 운명을 가지고 있다.

 

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

 

 

 길이가 T인 시계열 데이터를 가정해 T번재 정답 레이블로부터 전해지는 기울기가 어떻게 변하는지 보자. 

 

 

 그림 6-6은 y=tann(x)의 대한 미분이다. 역전파에서는 기울기가 tanh 노드를 지날 때마다 값은 계속 작아진다는 뜻이다. 

 

6.1.4 기울기 폭발 대책

 

이러한 기울기 폭발과 소실에 대한 대처 방법으로는 기울기 클리핑이라는 방법이 있다. 이는 매우 단순하며 전통적인 기법이다.

 

6.2 기울기 소실과 LSTM

 이러한 문제를 해결하기 위해서는 RNN 계층의 아키텍처를 근본부터 뜯어고쳐야 한다. 이 문제를 해결하기 위해 '게이트'의 기능을 추가할 것이다. 

 

6.2.1 LSTM의 인터페이스

 

 

LSTM 계층은 기억셀 c가 추가 된 것이다. c는 데이터를 자기 자신으로만(LSTM 계층 내에서만) 주고 받으며, 다른 계층으로 '출력'하지 않는다.

 

6.2.2 LSTM 계층 조립하기

 

 

이제부터는 기존의 계산 h와 x를 하는 것이 아닌 기억셀 c가 새롭게 추가되어 연산한다. 

 

6.2.3 output 게이트

 

 

 output 게이트와 아다마르의 곱을 통해 출력을 계산한다.

 

6.2.4 forget 게이트

 

 

기엑 셀에 '무엇을 잊을까'를 명시해야 한다. 이또한 forget 게이트를 통해 처리한다. 즉 불필요한 기억을 지우는 것이다.

 

6.2.5 새로운 기억셀

 

 

 forget 게이트에서 기억을 삭제해주었으니 해당 절에서는 새로 기억해야 할 정보를 기억셀에 추가하는 내용을 추가한다.

 

6.2.6 input 게이트

 

 

 input 게이트는 추가된 정보가 가치가 얼마나 큰지를 판단한다.

 

6.2.7 LSTM의 기울기 흐름

 

 

 

6.3 LSTM 구현

6.3.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.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):
      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, 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
      
    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

 

6.4 LSTM을 사용한 언어 모델

 

class Rnnlm:
  def __init__(self, vocab_size = 10000, wordvec_size = 100, hidden_size = 100):
    V, D, H = vocab_size, wordvec_size, hidden_size
    rn = np.random.randn

    # 가중치 초기화
    embed_W = (rn(V, D) / 100).astype('f')
    lstm_Wx = (rn(D, 4 * H) / np.sqrt(D)).astype('f') # i, g, f, o
    lstm_Wh = (rn(H, 4 * H) / np.sqrt(H)).astype('f') # i, g, f, o
    lstm_b = np.zeros(4 * H).astype('f')
    affine_W = (rn(H, V) / np.sqrt(H)).astype('f')
    affine_b = np.zeros(V).astype('f')

    # 계층 생성
    self.layers = [
        TimeEmbedding(embed_W),
        TimeLSTM(lstm_Wx, lstm_Wh, lstm_b, stateful=True),
        TimeAffine(affine_W, affine_b)
    ]
    self.loss_layer = TimeSoftmaxWithLoss()
    self.lstm_layer = self.layers[1]

    # 모든 가중치와 기울기를 리스트에 모은다.
    self.params, self.grads = [], []
    for layer in self.layers:
      self.params += layer.params
      self.grads += layer.grads
    
  def predict(self, xs):
    for layer in self.layers:
      xs = layer.forward(xs)
    return xs
  
  def forward(self, xs, ts):
    score = self.predict(xs)
    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):
    self.lstm_layer.reset_state()

  def save_params(self, file_name ='Rnnlm.pkl'):
    with open(file_name, 'wb') as f:
      pickle.dump(self.params, f)
    
  def load_params(self. file_name='Rnnlm.pkl'):
    with open(file_name, 'rb') as f:
      self.params = pickle.load(f)

 

 

6.5 RNNLM 추가 개선

6.5.1 LSTM 계층 다양화

 

 

 

 RNNLM의 성능을 높이기 위해 LSTM 계층을 깊게 쌓아 효과를 볼 수 있다. 위 그림은 LSTM 계층을 2 개 쌓은 모습이다. 이렇게 LSTM 계층을 몇 층 쌓아가며 더 복잡한 패턴을 학습할 수 있게 된다.

 

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

 

LSTM 계층을 다층화하면 복잡한 의존관계를 학습한 모델을 만들 수 있으나, 이는 종종 과적합(overfitting)을 일으킨다. 이를 해결하기 위홰 정규화나 드롭 아웃을 사용한다.

 

 

6.5.3 가중치 공유

 

가중치를 공유함으로써 매개 변수의 수를 줄이고 정보의 일관성을 유지할 수 있다.

 

6.5.4 개선된 RNNLM 구현

 

class BetterRnnlm(BaseModel):
  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 1층
    lstm_Wx1 = (rn(D, 4 * H) / np.sqrt(D)).astype('f') # i, g, f, o
    lstm_Wh1 = (rn(H, 4 * H) / np.sqrt(H)).astype('f') # i, g, f, o
    lstm_b1 = np.zeros(4 * H).astype('f')
    # LSTM 2층
    lstm_Wx2 = (rn(D, 4 * H) / np.sqrt(D)).astype('f') # i, g, f, o
    lstm_Wh2 = (rn(H, 4 * H) / np.sqrt(H)).astype('f') # i, g, f, o
    lstm_b2 = np.zeros(4 * H).astype('f')
    # affine_W = (rn(H, V) / np.sqrt(H)).astype('f') -> Embedding 가중치 공유 예정. 
    affine_b = np.zeros(V).astype('f')

    # 3가지 개선!
    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) # 가중치 공유!
    ]
    self.loss_layer = TimeSoftmaxWithLoss()
    self.lstm_layers = [self.layers[2], self.layers[4]]
    self.dropout_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.dropout_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()

 

6.5.5 첨단 연구로

 

6.6 정리

 

단순한 RNN의 학습에서는 기울기 소실과 기울기 폭발이 문제가 된다.
기울기 폭발에는 기울기 클리핑, 기울기 소실에는 게이트가 추가된 RNN(LSTM, GRU etc)이 효과적이다.
LSTM에는 input, forget, output gate 등 총 3개의 게이트가 있다.
게이트에는 전용 가중치가 있으며, 시그모이드 함수를 사용해 0.0 ~ 1.0 사이의 실수를 출력한다.
언어 모델 개선에는 LSTM 계층 다층화, 드롭아웃, 가중치 공유 등의 기법이 효과적이다. 
RNN의 정규화는 중요한 주제이며, 드롭아웃 기반의 다양한 기법이 제안되고 있다. 

 

 

반응형