NIRVANA

[밑바닥부터 시작하는 딥러닝2] 5장. 순환 신경망(RNN) 본문

AI

[밑바닥부터 시작하는 딥러닝2] 5장. 순환 신경망(RNN)

녜잉 2024. 3. 18. 12:15

확률과 언어 모델

언어 모델

  • 단어 나열에 확률을 부여 → 특정한 단어의 시퀀스에 대해, 그 시퀀스가 일어날 가능성이 어느 정도인지를 확률로 평가하는 것
  • 기계 번역, 음성인식, 새로운 문장 생성 등에 사용 가능 

 

CBOW 모델을 언어 모델로? 

  • 맥락의 크기가 고정되게 될 경우, 정해진 맥락보다 왼쪽에 있든 단어들의 정보는 무시 되어진다는 문제가 존재한다. 

→ RNN을 사용하여 문제 해결 가능 

 

  • RNN은 맥락이 아무리 길더라도 그 맥락 정보를 기억하는 메커니즘을 가지고 있음
    • RNN을 사용하면 아무리 긴 시계열 데이터라도 대응 가능 

 

RNN이란 

순환하는 신경망 

  • 순환: 어느 한 지점에서 시작한 것이, 시간을 지나 다시 원래 장소로돌아오는 것, 그리고 이 과정을 반복하는 것
    • 순환을 하기 위해서는 '닫힌 경로'가 필요함 
    • 즉, 닫힌 경로 혹은 순환하는 경로가 존재해야 데이터가 같은 장소를 반복하여 왕래할 수 있고, 데이터가 순환하면서 정보가 끊임 없이 갱신하게 됨 
  • RNN은 순환하는 경로(닫힌 경로)가 존재하고, 해당 경로를 따라 데이터가 끊임 없이 순환됨 → 과거의 정보를 기억하는 동시에 최신 데이터로의 갱신이 가능해짐 

 

순환 구조 펼치기 

  • 각 시각의 RNN 계층은 그 계층으로의 입력과 1개 전의 RNN 계청으로부터의 출력을 받음 → 두 정보를 바탕으로 현 시각의 출력을 계산 

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

  • RNN에는 가중치가 2개 존재함 
    • W_x: 입력 x를 출력 h로 변환하기 위한 가중치
    • W_h: 1개의 RNN 출력을 다음 시각의 출력으로 변환하기 위한 가중치 
    • b: 편향
    • h_t-1, X_t:  행벡터 
  • 행렬 곱을 계산하고, 그 합을 tanh함수를 이용하여 변환한 결과가 시각 t의 출력 h_t가 됨 
    • h_t는 다른 계층을 향해 위쪽으로 출력되는 동시에, 다음 시각의 RNN 계층(자기 자신)을 향해 오른쪽으로도 출력됨 
  • 현재의 출력(h_t)는 한 시각 이전 출력(h_t-1)에 기초해 계산됨 
    • 즉, RNN은  h라는 상태를 가지고 있고, 위의 식의 형식으로 갱신됨 
    • RNN 계층 = 상태를 가지는 계층, 메모리가 있는 계층 

 

BPTT

  • 시간 방향으로 펼친 신경망의 오차역전파
  • 시계열 데이터의 시간 크기가 커지는 것이 비례하여 BPTT가 소비하는 컴퓨팅 자원이 증가한다는 문제가 발생 
  • 시간 크기가 커지면 역전파 시 기울기가 불안정해진다는 문제 발생 

Truncated BPTT

  • 시간축 방향으로 너무 길어진 신경망을 적당한 지점에서 잘라내어 작은 신경망 여러 개로 만들고, 작은 신경망에서 오차 역전파를 수행하는 것 
  • 신경망의 연결을 끊되, '역전파'의 연결만 끊고 순전파의 연결은 그대로 유지해야 함 
    • 역전파의 연결을 잘라버리면, 그보다 미래의 데이터에 대해서는 생각할 필요가 없어짐
      • 각각의 블록 단위로, 미래의 블록과는 독립적으로 오차역전파법을 완결 시킬 수 O
    • 순전파의 연결은 끊어지면 안됨, 즉 데이터는 순서대로 입력 되어져야 함 

 

Truncated BPTT의 미니 배치 학습

  • 데이터 제공 방법면에서 몇 가지 주의해야 함 
    • 데이터를 순서대로 제공하기
    • 미니배치별로 데이터를 제공하는 시작 위치 옮기기 
  • 미니배치 학습을 수행하므로, 구체적인 배치 방식을 고려하여 데이터를 순서대로 입력해야 함 
    • 데이터를 주는 시작 위치를 각 미니배치의 시작 위치로 '옮겨줘야' 함 
    • 각 미니배치의 시작 위치를 오프셋으로 옮겨준 후, 순서대로 제공함 → 데이터를 순서대로 입력하다, 끝에 도달하면 다시 처음부터 입력하도록 함 

 

RNN 구현 

  • (우리가 구현할 ) 신경망은 길이가 T인 시계열 데이터를 받고, 각 시각의 은닉 상태를 T개 출력함
  • 신경망을 하나의 계층으로 구현할 경우, 상하 방향의 입력과 출력을 각각 하나로 묶으면 옆으로 늘어선 일련의 계층을 하나의 계층으로 간주할 수 있음 
    • (X_0, x_1, .... , X_t-1)을 묶은 XS를 입력하면 (h_0, h_1, ..., h_t-1)을 묶은 hs를 출력하는 단일 계층으로 볼 수 있음
    • RNN 계층: TimeRNN 계층 내에서 한 단계의 작업을 수행하는 계층
    • TimeRNN 계층: T개 단계 분의 작업을 한꺼번에 처리하는 계층 

 

RNN 계층 구현 

  • RNN 순전파 공식

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

  •  데이터는 미니배치로 모아서 처리 되므로 x(와h_t)에는 각 샘플 데이터를 행 방향에 저장

순전파 

class RNN:
  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, h_prev):
      Wx, Wh, b = self.params
      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
  • RNN 계층의 순전파에서 일어나는 계산은 행렬의 곱인 MatMul과 덧셈인 +, tanh로 구성됨
  • 역전파의 경우, 순전파와 달리 반대 방향으로 각 연산자의 역전파를 수행하면 됨 

역전파 

  def backward(self, dh_next):
      Wx, Wh, b = self.params
      x, h_prev, h_next = self.cache

      dt = dh_next * (1 - h_next **2)
      db = np.sum(dt, axis = 0)
      dWh = np.matmul(h_prev.T, dt)
      dh_prev = np.matmjl(dt, Wh.T)
      dWx = np.matmul(x.T, dt)
      dx = np.matmul(dt, Wx,T)

      self.grads[0][...] = dWx
      self.grads[1][...] = dWh
      self.grads[2][...] = db

      return dx, dh_prev

 

TimeRNN 계층 구현 

  • TimeRNN 계층은 T개의 RNN 계층으로 구성됨 
    • RNN 계층 T개를 연결한 신경망 
    • RNN 계층의 은닉 상태 h를 인스턴스 변수로 유지하고 이를 은닉 상태를 인계 받는 용도로 이용함 
  • RNN 계층의 은닉 상태를 TimeRNN 계층에서 관리하면 은닉 상태를 '인계'하는 작업을 생각하지 않아도 됨 
    • 책에서는 은닉 상태를 인계받을지에 대한 결정 여부를 stateful 인수로 조정 

초기화 메서드 

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 #다수의 RNN 계층을 리스트로 저장하는 용도 
	
    #순전파 메서드를 불렀을 때, 마지막 RNN 계층의 은닉 상태를 저장
    #역전파 메서드를 불렀을 때, 하나 앞 블록의 은닉 상태의 기울기 저장 
    self.h, self.dh = None, None
    self.stateful = stateful 

  def set_state(self, h):
    self.h = h

  def reset_state(self):
    self.h = None

 

  • '상태가 있다'는 TimeRNN 계층이 은닉 상태를 유지한다는 뜻, 즉 아무리 긴 시계열이 있어도 TimeRNN 계층의 순전파를 끊지 않고 전파한다는 의미
  • '상태가 없다'는 은닉 상태를 영행렬로 초기화함 

순전파

 def forward(self, xs):
    Wx, Wh, b  = self.params
    N, T, D = xs.shape
    D, H = Wx.shape

    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):
        layer = RNN(*self.params)
        self.h = layer.forward(xs[:, t, :], self.h)
        hs[:, t, :] = self.h
        self.layers.append(layer)

      return hs

 

역전파

  • 역전파에서는 상류(출력쪽 층)에서부터 전해지는 기울기를 dhs로 쓰고, 하류로 보내는 기울기를 dxs로 씀
  • RNN 계층의 순전파에서 출력이 2개로 분기되는 지점이 존재 → 순전파시 분기했을 경우, 역저파에서는 각 기울기가 합산 되어 전해짐 
    • 역전파 시, RNN 계층에서는 합산된 기울기 (dh_t + dh_next)가 입력됨 
  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)):
      layer = self.layers[t]
      dx, dh = layer.backward(dhs[:, t, :] + dh)
      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

 

시계열 데이터 처리 계층 구현

RNNLM의 전체 그림 

RNNLM의 전체 구성

  •  Embeddimg 계층: 단어 ID를 단어의 분산 표현(단어 벡터)로 변환 
  • RNN계층: 임베딩 계층의 분산 표현을 입력 받아 은닉 상태를 다음층으로 출력함(위쪽)과 동시에, 다음 시각의 RNN 계층으로 출력 (오른쪽)
  • Affine 계층
  • Softmax 계층 

RNNLM의 특징 

  •  RNN계층은 "you say"라는 맥락을 기억하고 있음 
    • "you say"라는 과거의 정보를 응집된 은닉 상태 벡터로 저장해두고, 더 위의 Affine 계층이나 다음 시각의 RNN 계층에 전달하는 역할을 맡음 
  • RNNLM은 지금까지 입력된 단어를 기억하고, 이를 바탕으로 다음에 출현할 단어를 예측함 
    • RNN 계층이 과거에서 현재로 데이터를 계속 흘려보내줌으로써 과거의 정보를 인코딩해 저장할 수 있는 것 

 

RNNLM 학습과 평가 

RNNLM 구현 

  • 4개의 Time 계층을 쌓은 SimpleRnnlm 클래스 구현 
import sys
sys.path.append('..')
import numpy as np
from common.time_layers import 

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

    #가중치 초기화
    embed_W = (rn(V, D) / 100).astype('f')
    rnn_Wx = (rn(D,H)/ np.sqrt(D)).astype('f')
    rnn_Wh = (rn(H, H) / np.sqrt(H)).astype('f')
    rnn_b = np.zeros(H).astype('f')
    affine_W = (rn(H, V) / np.sqrt(H)).astype('f')
    affine_b = np.zeros(V).astype('f')

    #계층 생성
    self.layers = [
        TimeEmbeddig(embed_W), 
        TimeRNN(rnn_Wx, rnn_Wh, rnn_b, stateful=True),
        TimeAffine(affine_W, affine_b)
    ]

    self.loss_layer = TimeSoftmaxWithLoss()
    self.rnn_layer = self.layers[1]

    #모든 가중치와 기울기를 리스트에 모으기 
    self.params, self.grads = [], []

    for layer in self.layer:
      self.params += layer.params
      self.grads += layer.grads

 

  • forward(), backward(), reset_state() 메서드 구현 
def forward(self, xs, tx):
  for layer in self.layers:
    xs = layer.forward(xs)
  loss = self.loss_layer_forward(xs, tx)
  return loss

def backward(self, dout=1):
  dout = self.loss_layer.backward(dout)
  for layer in reversed(self.layer):
    dout = layer.backward(dout)
  
  return 

def reset_state(self):
  self.rnn_layer.reset_state()

 

언어 모델의 평가

  • 언어 모델은 주어진 과거 단어(정보)로부터 다음에 출현할 단어의 확률 분포를 출력
  • 언어 모델의 예측 성능을 평가하는 척도로 "퍼플렉서티"를 자주 사용함 
    • 퍼플렉서티: 확률의 역수

ex) "You say goodbye and I say hello." 가 있을 때, 모델1에 "you"를 입력 했을 경우 say의 확률이 0.8이고 실제 정답이 say일 경우 퍼플렉서티는 이 확률의 역수 즉 1/0.8 = 1.25가 됨

→ 퍼플렉서티는 작을 수록 좋음(작을 수록 정답에 가까움) 

  • 퍼플렉서티 값은 분기 수를 사용하여 직관적 해석이 가능함 
    • 분기 수: 다음에 취할 수 있는 선택 사항 
    • 즉, 예측한 분기수가 1.25라는 것은 다음에 출현할 수 있는 단어의 후보를 1개 정도로 좁혔다는 것이고, 나쁜 모델에서는 후보가 아직 5개나 있다는 의미 

 

RNNLM의 학습 코드 

import sys
sys.path.append('..')
import matplotlib.pyplot as plt
import numpy as np
from common.optimizer import SGD
from dataset import ptb
from simple_rnnlm import SimpleRnnlm

#학습 파라미터 설정
batch_size = 10
wordvec_size = 100
hidden_size = 100
time_size = 5
lr =0.1
max_epoch = 100

#학습 데이터 읽기(전체 중 1000개만)
corpus, word_to_id, id_to_word = ptb.load_data('train')
corpus_size = 1000
corpus = corpus[:corpus_size]
vocab_size = int(max(corpus)+1)

xs = corpus[:-1] #입력
ts = corpus[1:] #출력(정답 레이블)
data_size = len(xs)
print("말뭉치의 크기: %d, 어휘 수 : %d" % (corpus_size, vocab_size))

#학습 시 사용하는 변수 
max_iters = data_size // (batch_size * time_size)
time_idx = 0
total_loss = 0
loss_count = 0
ppl_list = []

#모델 생성 
model = SimpleRnnlm(vocab_size, wordvec_size, hidden_size)
optimizer = SGD(lr)

#각 미니배치에서 샘플 읽기 시작 위치를 계산
jump = (corpus_size-1 ) // batch_size
offsets = [i * jump for i in range(batch_size)]

for epoch in range(max_epoch):
  for iter in range(max_iters):
    
    #미니배치 획득
    batch_x = np.empty((batch_size, time_size), dtype='i')
    batch_t = np.empty((batch_size, time_size), dtype='i')

    for t in range(time_size):
      for i, offset in enumerate(offset):
        batch_x[i, t] = xs[(offset + time_idx) % data_size]
        batch_t[i, t] = ts[(offset + time_idx) % data_size]
      time_idx +=1

    #기울기를 구하여 매개변수 갱신 
    loss = model.forward(batch_x, batch_t)
    model.backward()
    optimizer.update(model.params, model.grads)
    total_loss += loss
    loss_count +=1

  ppl = np.exp(total_loss / loss_count)
  print('| 에폭 %d | 퍼플렉서티 %.2f' % (epoch+1, ppl))

  ppl_list.append(float(ppl))
  total_loss, loss_count = 0, 0

 

1. 미니 배치를 '순차적'으로 만들어

2. 모델의 순전파와 역전파를 구하고

3. 옵티마이저로 가중치를 갱신하고

4. 퍼플렉서티를 구함