본문 바로가기
파이썬 프로그래밍/Numpy 딥러닝

43.[RNN기초] RNN(many to one) 역전파 구현(실습2)

by Majestyblue 2023. 9. 22.

저번시간에 아래와 같이 RNN 역전파를 이용하여 문장에 의미가 있으면 1, 없으면 0을 구별하는 RNN 역전파를 구현하였다. 코드를 잘 보면 알겠지만 반복성에 의한 규칙이 존재한다.

https://toyourlight.tistory.com/70

 

42.[RNN기초] RNN 역전파 구현(실습)

저번시간에 RNN 역전파를 BPTT(Backpropagation Through Time)로 구현하였다. 이번 시간은 이를 코드로 구현해 보자. 40차시에서 진행했던 코드를 참고하자.(https://toyourlight.tistory.com/67) 39.[RNN기초] RNN 순전파

toyourlight.tistory.com

 

왜 그렇냐면 RNN 자체가 이전 값을 받아오는 '재귀성'을 가지고 있기 때문에 역전파도 재귀성을 가진다. 이러한 규칙성을 이용하여 역전파 코드를 반복문으로 정리해 보자.

 

 

 

 

1. 임의의 time step의 t에 대한 순전파 모식도

아래 그림은 임의의 time step t에 대한 순전파를 나타낸 것이다. t 다음에 fc 분류기인지, RNN 인지에 따라 기울기 값을 조금 다르게 정의해야 한다.

 

 

우리의 목적은 오차(E)에 대한 Wxh, Whh, Bh 기울기의 규칙성을 발견하는 것이다. 

 

 

 

2. 기울기 전달받기

가. h_t 다음 fc 분류기인 경우

만약 h_t  다음이 fc 분류기라면, fc에 대한 기울기를 이어 받아야 한다. 과정은 아래와 같다.

 

따라서 오차에 대한 RNN 셀(h`_t)의 기울기 ∂E / ∂h`_t 는 아래와 같다.

 

 

 

 

 

 

 

나. h_t 다음 h_(t+1)인 경우 경우

만약 h_t  다음이 h`_(t+1)인 RNN cell이라면 h`_(t+1)에  기울기를 이어 받아야 한다. 과정은 아래와 같다.

 

따라서 오차에 대한 RNN 셀(h`_t)의 기울기 ∂E / ∂h`_t 는 아래와 같다.

∂E / ∂h`_(t+1)에서 ∂E / ∂h`_t으로 기울기가 흘러가는 과정을 잘 보면 재귀 과정임을 알 수 있다.

 

 

∂E / ∂h`_t를 왜 이렇게 애를 쓰면서 구해야 할까? 바로 RNN 셀의 가중치 기울기를 구할 수 있기 때문이다.

 

 

 

 

 

 

3. 규칙성 발견하기

아래 순전파를 보면서 다음 차례에 대한 규칙성을 확인하자.

  • time step은 0, 1, 2(마지막)으로 구성되어 있다.
  • time step 2인 마지막 time step에서의 다음 입력은 fc 분류기이다.
  • time step 0, 1에서의 다음 입력은 RNN셀이다.

 

 

 

이전 역전파 과정에서 볼 수 있듯이, 순전파 과정에서 pred, pred`, h`t_i, ht를 리턴하였고 pred, pred`은 1개의 값으로, h`t_i, ht는 리스트로 구성되었으며 각 리스트는 아래와 같이 구성되어 있었다.

h`t _i list = [h0_i, h1_i, h2_i] → 3개 요소

ht list = [h-1, h0, h1, h2] → 4개 요소(역전파 규칙성을 위해 h-1을 대입함)

 

역전파는 그림과 같이 거꾸로 들어가야 한다.

 

 

 

 

 

따라서 리스트의 마지막 부터 접근해야 한다. 이를 코드로 구현해 보자

아래와 같이 파이썬에서 reversed를 사용하면 인덱스를 거꾸로 접근할 수 있다.

 

 

 

 

time step 2에서의 다음 상태는 fc 분류기이고 그 외의 time step 1, 0일 때의 다음 상태는 RNN 셀이므로 이를 구별하기 위해 조건문을 아래와 같이 사용해 볼 수 있다.

 

 

 

따라서 아래와 같이 의사코드를 작성해 볼 수 있다.

 

 

 

 

위 과정을 코드로 나타내면 아래와 같다. 코드 표기법에 주의하면서 살펴보자.

 

 

  for t  in reversed(range(time_steps)):
    # time steps = 2 일 때 기울기 정의
    if t == time_steps -1: # ht_plus1_i가 pred`인 경우, time step 마지막에는 분류기에서 기울기를 받는다.
      dht_plus1_i_dht = np.transpose(Wy, (1, 0)) # (o.f(1), h.n(3))
      dE_dht_plus1_i = dE_dpred_i
    # time steps = 1, 0 일 때 기울기 정의
    else:
      dht_plus1_i_dht = np.transpose(Whh, (1, 0))
      dE_dht_plus1_i = dE_dht_i # 재귀과정, 새로운 time step이 시작되면 이전 과정은 t+1이 되므로 plus1 해줘야 한다.

    dE_dht = np.dot(dE_dht_plus1_i, dht_plus1_i_dht) #(1, o.f(1)) × (o.f(1), h.n(3)) = (1, h.n(3))
    dht_dht_i = (1-np.power(np.tanh(ht_i_list[t]), 2)) # (1. h.n(3))
    dE_dht_i = dE_dht * dht_dht_i # (1, h.n(3))

 

 

 

4. RNN 셀의 가중치 Wxh, Whh, Bh 기울기 구하기

∂E / ∂h`_t를 구했다면 우리가 목표로 했던 RNN 셀의 가중치 Wxh, Whh, Bh에 대한 기울기를 구할 수 있다.

 

 

이를 코드로 나타내면 아래와 같다.

    # dE_dWxh 구하기
    dht_i_dWxh = np.transpose(np.reshape(X[t], (1, -1)), (1, 0)) # (s.l(2), 1)
    # X를 그냥 가지고 오면 1차원 배열이여서 2차원으로 바꾸어주어야 함
    dE_dWxh = np.dot(dht_i_dWxh, dE_dht_i) # (s.l(2), 1) × (1, h.n(3)) = (s.l(2), h.n(3))

    # dE_dWhh 구하기
    dht_i_dWhh = np.transpose(ht_list[t], (1, 0)) # (h.n(3), 1)
    dE_dWhh = np.dot(dht_i_dWhh, dE_dht_i) # (h.n(3), 1) × (1, h.n(3)) = (h.n(3), h.n(3))

    # dE_dBh 구하기
    dE_dBh = np.sum(dE_dht_i, keepdims=True) # (1, h.n(3)) -> (1, 1)

 

x = [x_0, x_1, x_2]

h`t _i list = [h0_i, h1_i, h2_i] 

ht list = [h-1, h0, h1, h2] 

임을 기억하자. ht list는 h-1 상태를 담았기 때문에 같은 인덱스를 접근하더라도 time step이 1 낮은 값에 접근한다. 따라서 인덱스를 그대로 접근하더라도 기울기를 구할 수 있다.

 

예를 들어 t=1 이라면 아래와 같이 접근할 수 있다.

X[1] = x_1

h't_i list[1] = h1_i

ht list[1] = h0

 

 

5. 실전 예제 도전

아래와 같이 "as", "soon", "as"를 조합하여 [('soon', 'as', 'as'), ('as', 'as', 'soon'), ('as', 'soon', 'as')] 데이터를 만들어 보자. 

 

import numpy as np
from itertools import *
np.random.seed(230907)

dataset = ["as", "soon", "as"]
datalist = list(set(permutations(dataset, 3)))
# permutations는 중복이 허용됨. 중복을 제거하기 위해
print(datalist)

[('soon', 'as', 'as'), ('as', 'as', 'soon'), ('as', 'soon', 'as')]

 

 

 

아래와 같이 (batch, time steps, sequence length)인 (3, 3, 2)의 데이터가 생성된다.

목표값은 as soon as 일 때 1이고 (의미가 있으므로) 나머지는 0이다.

def parallel_prep(x):
  count = 0
  for sentence in x:
    prep = np.array([])
    for word in sentence:
      if word == 'as':
        word = np.array([1, 0])
      elif word == 'soon':
        word = np.array([0, 1])
      if prep.size == 0:
        prep = np.concatenate([prep, word])
      else:
        prep = np.vstack([prep, word])
    if count == 0:
      total_prep = prep.copy()
    else:
      total_prep = np.vstack([total_prep, prep])
    count = count + 1
  total_prep = np.reshape(total_prep, (-1, 3, 2)) # (batch, time steps, sequence length)
  return total_prep

input_RNN = parallel_prep(datalist)
print(input_RNN)
print(input_RNN.shape)

targets = np.array([[0],
                   [0],
                   [1]])

[ [ [0. 1.]

    [1. 0.]

    [1. 0.] ]

  [ [1. 0.]

    [1. 0.]

    [0. 1.] ]

  [ [1. 0.]

    [0. 1.]

    [1. 0.] ] ]

(3, 3, 2)

 

 

 

변수, 가중치를 생성한다.

time_steps = input_RNN.shape[1] # t.s(3)
sequence_length = input_RNN.shape[2] # s.l(2)
hidden_node = 3 # 내가 설정해야 하는 것, h.n(3)
output_feature = targets.shape[1] # o.f(1)

Wxh = np.random.randn(sequence_length, hidden_node) # (s.l(2), h.n(3))
Whh = np.random.randn(hidden_node, hidden_node) # (h.n(3), h.n(3))
Bh = np.random.randn(1, 1) # (1, 1)

Wy = np.random.randn(hidden_node, output_feature) # (h.n(3), o.f(1))
By = np.random.randn(1, 1) # (1, 1)

 

 

RNN 셀을 구현한다.

# RNN 셀을 위한 for문을 설정해 보자. 1개씩 요소로 접근해야 한다.
# 1개씩 접근한 값을 rnn 연산을 해 보자.
# 마지막 출력에 linear 연산을 걸고 출력하자.
# 역전파를 위해 ht_i, ht를 리스트로 걸어서 리턴하자
def rnn_cell(data):
  h = np.zeros((1, hidden_node)) #(1, 3) h_-1 상태이며 사실 없으므로 0이다. 반복문을 위해 넣어줌.
  ht_i_list = []
  ht_list = []
  ht_list.append(h) # 규칙적인 역전파를 위해 h_-1 상태를 입력함.
  for sequence in data:
    x = np.reshape(sequence, (1, -1))
    hi = np.dot(x, Wxh) + np.dot(h, Whh) + Bh
    h = np.tanh(hi)
   
    ht_i_list.append(hi)
    ht_list.append(h)

  pred_i = np.dot(h, Wy) + By
  pred = 1/(1+np.exp(-pred_i))
  return pred, pred_i, ht_i_list, ht_list

pred, _, _, _ = rnn_cell(input_RNN[0])
print(pred)

[[0.57238818]]

 

 

 

순전파 함수를 구현한다.

def forward(dataset):
  count = 0
  for data in dataset:
    pred, _, _, _ = rnn_cell(data)
    if count == 0:
      total_pred = pred.copy()
    else:
      total_pred = np.concatenate([total_pred, pred], axis=0)
    count = count + 1
  return total_pred

predics = forward(input_RNN)
print(predics)

[ [0.57238818]

  [0.30956668]

  [0.36887959] ]

 

 

 

 

오차 함수를 구현한다.

# 첫 번째 데이터에 대한 오차 계산
def BCEE_loss(y_hat, y):
  loss = np.sum(-y*np.log(y_hat) - (1-y)*np.log(1-y_hat))
  return loss

losses = BCEE_loss(predics, targets)
print(losses)

2.217260343518136

 

 

 

 

 

위에서 진행한 역전파 함수를 정리한다. 역전파는 1개의 데이터에 대해 실시한다.

# 한 개의 데이터에 대한 역전파 실행
def loss_gradient(X, Y):

  dE_dWxh_list = np.zeros_like(Wxh) # (s.l(2), h.n(3)) 기울기 누적 변수
  dE_dWhh_list = np.zeros_like(Whh) # (h.n(3), h.n(3)) 기울기 누적 변수
  dE_dBh_list = np.zeros_like(Bh) # (1, 1) 기울기 누적 변수

  #순전파
  pred, pred_i, ht_i_list, ht_list = rnn_cell(X)
  # hi_i_list는 [h0_i, h1_i, h2_i] 순서
  # ht_list는 [h-1, h0, h1, h2] 순서
  loss = BCEE_loss(pred, Y)

  #역전파
  dE_dsig = -1*( (Y / pred) - ( (1-Y) / (1-pred) ) ) # (1, o.f(1))
  dsig_dpred_i = ( 1/(1+np.exp(-pred_i)) ) * ( 1 - 1/(1+np.exp(-pred_i)) ) # (1, o.f(1))
  dE_dpred_i = dE_dsig * dsig_dpred_i # (1, o.f(1))*(1, o.f(1)) = (1, o.f(1))

  # fc 분류기에 대한 기울기 구하기 실시
  dpred_i_dWy = np.transpose(ht_list[time_steps], (1, 0)) # (h.n(3), 1), ht_list에서 맨 마지막 요소가 fc에 들어가므로 따로 꺼내야 함.
  dE_dWy = np.dot(dpred_i_dWy, dE_dpred_i) # (h.n(3), 1) × (1, o.f(1)) = (h.n(3), o.f(1))
  dE_dBy = dE_dpred_i # (1, 1)

  for t  in reversed(range(time_steps)):
    # time steps = 2 일 때 기울기 정의
    if t == time_steps -1: # ht_plus1_i가 pred`인 경우, time step 마지막에는 분류기에서 기울기를 받는다.
      dht_plus1_i_dht= np.transpose(Wy, (1, 0)) # (o.f(1), h.n(3))
      dE_dht_plus1_i = dE_dpred_i
    # time steps = 1, 0 일 때 기울기 정의
    else:
      dht_plus1_i_dht = np.transpose(Whh, (1, 0))
      dE_dht_plus1_i = dE_dht_i # 재귀과정, 새로운 time step이 시작되면 이전 과정은 t+1이 되므로 plus1 해줘야 한다.

    dE_dht = np.dot(dE_dht_plus1_i, dht_plus1_i_dht) #(1, o.f(1)) × (o.f(1), h.n(3)) = (1, h.n(3))
    dht_dht_i = (1-np.power(np.tanh(ht_i_list[t]), 2)) # (1. h.n(3))
    dE_dht_i = dE_dht * dht_dht_i # (1, h.n(3))

    # dE_dWxh 구하기
    dht_i_dWxh = np.transpose(np.reshape(X[t], (1, -1)), (1, 0)) # (s.l(2), 1)
    # X를 그냥 가지고 오면 1차원 배열이여서 2차원으로 바꾸어주어야 함
    dE_dWxh = np.dot(dht_i_dWxh, dE_dht_i) # (s.l(2), 1) × (1, h.n(3)) = (s.l(2), h.n(3))

    # dE_dWhh 구하기
    dht_i_dWhh = np.transpose(ht_list[t], (1, 0)) # (h.n(3), 1)
    dE_dWhh = np.dot(dht_i_dWhh, dE_dht_i) # (h.n(3), 1) × (1, h.n(3)) = (h.n(3), h.n(3))

    # dE_dBh 구하기
    dE_dBh = np.sum(dE_dht_i, keepdims=True) # (1, h.n(3)) -> (1, 1)

    # 기울기 누적
    dE_dWxh_list += dE_dWxh
    dE_dWhh_list += dE_dWhh
    dE_dBh_list += dE_dBh

  return dE_dWy, dE_dBy, dE_dWxh_list, dE_dWhh_list, dE_dBh_list

print(loss_gradient(input_RNN[0], targets[0]))

 

 

 

전체 데이터에 대한 역전파를 실시하기 위해 for문과 zip을 이용하여 역전파를 실시한다.

# 전체 데이터에 대하여 실행해 보자
def backward(X, Y):
  dE_dWy_list = np.zeros_like(Wy) # (h.n(3), o.f(1)) 기울기 누적 변수
  dE_dBy_list = np.zeros_like(By) # (1, 1) 기울기 누적 변수
  dE_dWxh_list = np.zeros_like(Wxh) # (s.l(2), h.n(3)) 기울기 누적 변수
  dE_dWhh_list = np.zeros_like(Whh) # (h.n(3), h.n(3)) 기울기 누적 변수
  dE_dBh_list = np.zeros_like(Bh) # (1, 1) 기울기 누적 변수

  for data, target in zip(X, Y):
    dE_dWy, dE_dBy, dE_dWxh, dE_dWhh, dE_dBh = loss_gradient(data, target)
    dE_dWy_list += dE_dWy
    dE_dBy_list += dE_dBy
    dE_dWxh_list += dE_dWxh
    dE_dWhh_list += dE_dWhh
    dE_dBh_list += dE_dBh

  return dE_dWy_list, dE_dBy_list, dE_dWxh_list, dE_dWhh_list, dE_dBh_list

gradients = backward(input_RNN, targets)
print(gradients)

 

 

 

 

경사하강법을 적용하여 훈련한다. 오차가 점점 줄어드는 것을 확인할 수 있다.

# 훈련 전 예측값
pred = forward(input_RNN)
print('before pred', pred)

# 경사하강법 적용
learning_rate = 0.1
for i in range(100):
    dL_dWy, dL_dBy, dL_dWxh, dL_dWhh, dL_dBh = backward(input_RNN, targets)
    Wy = Wy + -1*learning_rate * dL_dWy
    By = By + -1*learning_rate * dL_dBy
    Wxh = Wxh + -1*learning_rate * dL_dWxh
    Whh = Whh + -1*learning_rate * dL_dWhh
    Bh = Bh + -1*learning_rate * dL_dBh
    if i % 10 == 0:
      pred = forward(input_RNN)
      print(i, BCEE_loss(pred, targets))

before pred

[ [0.57238818]

  [0.30956668]

  [0.36887959] ]

0 1.5205523782457608

10 0.45650644260590134

20 0.2895924059369971

30 0.21387102930075602

40 0.1699660680082089

50 0.14111890475219838

60 0.12062425197085415

70 0.10525581974395332

80 0.0932653443724302

90 0.08362239065405241

 

 

 

 

훈련 후 예측값을 확인하자. 순서대로 0, 0, 1이 나오면 성공이다.

# 훈련 후 예측값
pred = forward(input_RNN)
print('after pred', pred)

after pred

[ [0.0110038 ]

  [0.02597423]

  [0.96172285] ]

 

 

 

 

지금까지 as, soon 단어를 이용하여 3단어로 이루어진 문장을 만들고 문장이 문법에 맞으면 1, 틀리면 0을 예측하는 many to one을 구현하였다. 다음 시간에는 many to many 예제를 탐구해 보자.