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

44.[RNN기초] RNN(many to many) 순전파 구현(이론, 실습)

by Majestyblue 2023. 9. 23.

이전 시간까지 주어진 문장이 문법에 맞으면 1, 아니면 0으로 예측하는 RNN 기반 인공신경망을 제작하고 훈련하였다. as soon as로 배치되었을 때 1이고 as as soon, soon as as 이라면 0으로 두었다.
 
이번엔 가장 간단한 생성형 RNN을 접근해 보자. 바로 many to many 문제이다.

The Unreasonable Effectiveness of Recurrent Neural Networks (karpathy.github.io)

 
아주 쉽게 접근하는 방법으로 'hello' 문제를 생각해 볼 수 있다. 한글자를 입력하면 그 다음 글자를 예측하는 문제이다. 
즉 아래와 같다.
1. h 입력 → e 출력
2. e 입력 → l 출력
3. l 입력 → l 출력
4. l 입력 → o 출력
 
지금까지 배운 DNN으로 해결할 수 있지 않을까? 생각이 들 수 있는데 나중에 보여주겠지만 잘 되지 않는다. 예를 들어 3번과 4번에서 같은 l이지만 l을 출력 해야 하는 경우와 o를 출력해야 하는 경우가 다르다. 이를 해결하기 위해 그 이전에 어떤 것이 입력 되었는지 정보가 필요하다. 
 
다시 말해 l이 입력 되었을 때 그 전에 e가 입력 되었다면 출력은 l이 되어야 하고, l이 입력될 때 그 이전에 l이 입력되었다면 출력은 o가 되어야 한다. 따라서 DNN은 이 문제를 해결할 수 없다.
 
이전시간에 진행했던 코드와 상당히 겹치기 때문에 이론과 코드 작성을 동시에 진행하겠다.
 

1. 전처리

h, e, l, o에 대해 원 핫 인코딩을 실시한다.

import numpy as np
np.random.seed(230912)
dict_hello = {'h':np.array([1, 0, 0, 0]), \
              'e':np.array([0, 1, 0, 0]), \
              'l':np.array([0, 0, 1, 0]), \
              'o':np.array([0, 0, 0, 1]), \
             }
print(dict_hello)

출력결과
{'h': array([1, 0, 0, 0]), 'e': array([0, 1, 0, 0]), 'l': array([0, 0, 1, 0]), 'o': array([0, 0, 0, 1])}
 
 
입력 데이터 input_RNN을 설정한다. 토큰은 h, e, l, o 4개이므로 sequence length는 4,  h → e → l → l 순으로 입력되므로 time step은 4이다. 

# input 값을 설정
inputs = np.array([])
for text in "hell":
  for key, value in dict_hello.items():
    if key == text:
     if inputs.size == 0:
       inputs = np.concatenate([inputs, value])
     else:
       inputs = np.vstack([inputs, value])

input_RNN = np.reshape(inputs, (1, 4 ,-1 ))
print('inputs shape :', input_RNN.shape)
print(input_RNN)

 
 
출력 데이터 targets를 설정한다. 토큰은 h, e, l, o 4개이므로 sequence length는 4, e → l → l → o 순으로 출력되므로 time step은 4이다. 

# target 값을 설정
target = np.array([])
for text in "ello":
  for key, value in dict_hello.items():
    if key == text:
     if target.size == 0:
       target = np.concatenate([target, value])
     else:
       target = np.vstack([target, value])

targets = np.reshape(target, (1, 4, -1))
print('target shape :', targets.shape)
print(targets)

 
 
 

2. 변수(Hyper parameters) 설정

  • time step 은 4이다. (h → e → l → l 순으로 입력, e → l → l → o 순으로 출력)
  • sequence length는 4이다. (h, e, l, o)
  • hidden node는 설정하기 나름인데 이전 예제와 같이 3으로 설정하겠다.
  • output_feature는 target의 sequence length와 같아야 하므로 4이다.

이를 코드로 작성하면 아래와 같다. 이전 코드와 거의 같다.

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

Wxh = np.random.randn(sequence_length, hidden_node) # (s.l(4), 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(4))
By = np.random.randn(1, 1) # (1, 1)

 
 
 
 

3. 순전파 구현

순전파 코드는 이전과 조금 달라지는데 모식도를 살펴보고 어떻게 수정해야 하는지 관찰하자. fc 분류기에서의 활성화 함수는 softmax이다.

 
 
매 순간마다 sequence를 리턴하게 되었으므로 아래와 같은 수정사항이 필요하다.

  • many to one에서는 pred`, pred`의 경우 1개만 출력되었으므로 list에 담지 않고 그대로 리턴하였는데 매 순간마다 sequence를 리턴하므로 pred`와 pred을 담을 list가 필요하다.
  • for sequence in data 문에서 pred`과 pred 연산은 밖에 있었다. rnn 셀을 time step 만큼 순회한 후 fc 분류기 연산을 해야 하기 때문이다. 그러나 매 순간 sequence를 리턴하므로 pred`와 pred 연산은 for문 안에 있어야 한다.
  • fc 분류기에서의 활성화 함수를 softmax로 수정해준다.

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

def rnn_cell(data):
  h = np.zeros((1, hidden_node)) #(1, 3) h_-1 상태이며 사실 없으므로 0이다. 반복문을 위해 넣어줌.
  # 역전파 계산을 위해 만든 list들
  ht_i_list = []
  ht_list = []
  pred_i_list = [] # many to many에서 필요함.
  pred_list = [] # many to many에서 필요함.
  ht_list.append(h) # 규칙적인 역전파를 위해 h_-1 상태를 입력함.
  count = 0
  for sequence in data:
    x = np.reshape(sequence, (1, -1))
    h_i = np.dot(x, Wxh) + np.dot(h, Whh) + Bh
    h = np.tanh(h_i)
    pred_i = np.dot(h, Wy) + By # many to many에서 필요함.
    pred = np.exp(pred_i)/np.sum(np.exp(pred_i)) # many to many에서 필요함.

    ht_i_list.append(h_i)
    ht_list.append(h)
    pred_i_list.append(pred_i)
    pred_list.append(pred)
    if count == 0:
      total_pred = pred.copy()
    else:
      total_pred = np.concatenate([total_pred, pred], axis=0)
    count = count + 1
  return total_pred, pred_list, pred_i_list, ht_list, ht_i_list

predics, pred_list, pred_i_list, ht_list, ht_i_list = rnn_cell(input_RNN[0])
print(predics)
print(pred_list)
print(pred_i_list)
print(ht_list)
print(ht_i_list)

 
 
 
 

4. 오차 함수 구현

오차함수는 교차 엔트로피 오차(Cross Entropy Error, CEE)이다.

# 오차 계산
def CEE_loss(y_hat, y):
  loss =  np.sum(-y*np.log(y_hat))
  return loss

losses = CEE_loss(predics, targets[0])
print(losses)

출력결과
12.062061261168791
 
 
 
다음시간에는 역전파를 구현해 보도록 하겠다.