저번시간에 RNN 역전파를 BPTT(Backpropagation Through Time)로 구현하였다. 이번 시간은 이를 코드로 구현해 보자. 40차시에서 진행했던 코드를 참고하자.(https://toyourlight.tistory.com/67)
일단 데이터 전처리와 입력값, 목표값을 생성하자.
import numpy as np
from itertools import *
np.random.seed(230907)
dataset = ["as", "soon", "as"]
datalist = list(set(permutations(dataset, 3)))
# permutations는 중복이 허용됨. 중복을 제거하기 위해 set 함수를 이용함
print(datalist)
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)
target = np.array([[0],
[0],
[1]])
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 = target.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)
1. 순전파 수정
역전파를 위해 time step 에 따라 순전파에 의해 출력된 결과들을 담는 리스트가 필요하다. 따라서 순전파 함수를 조금 수정해야 한다. 이전시간과 마찬가지로 ' → _i 로 표기했음을 기억하자. (예시 h` → h_i)
- pred = fc → sigmoid 출력값
- pred_i(pred') = fc 출력값
- ht_list(h list) = rnn → tanh 출력값을 순차적으로 저장
- ht_i_list(h` list) = 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))
h_i = np.dot(x, Wxh) + np.dot(h, Whh) + Bh
h = np.tanh(h_i)
ht_i_list.append(h_i)
ht_list.append(h)
pred_i = np.dot(h, Wy) + By
pred = 1/(1+np.exp(-pred_i))
return pred, pred_i, ht_list, ht_i_list,
pred, pred_i, ht_list, hi_i_list = rnn_cell(input_RNN[0])
print(pred)
print(pred_i)
print(hi_i_list) # [h0_i, h1_i, h2_i] 순서
print(ht_list) # [h-1, h0, h1, h2] 순서
2. fc 분류기 역전파
fc 분류기의 역전파를 실시하여 오차에 대한 가중치 Wy, By의 기울기인 ∂E / ∂Wy , ∂E / ∂By 를 구해보자.
먼저 기울기를 계속 누적할 변수를 선언하자.
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_list, hi_i_list = rnn_cell(X)
# hi_i_list는 [h0_i, h1_i, h2_i] 순서
# ht_list는 [h-1, h0, h1, h2] 순서
# x는 [x_0, x_1, x_2] 순서대로 구성됨.
loss = BCEE_loss(pred, Y)
Wy와 By를 계산하기 위해 아래 그림과 같이 ∂E / ∂pred'를 구해보자.
E는 BCEE(Binary Cross Entropy Error, 이진 교차 엔트로피 오차) 이고 pred는 시그모이드(σ) 연산이다.
따라서 기울기는 아래와 같이 구할 수 있다.
위 수학적 식을 코드로 구현하면 아래와 같다.
dE_dpred = -1*( (Y / pred) - ( (1-Y) / (1-pred) ) ) # (1, o.f(1))
dpred_dpred_i = ( 1/(1+np.exp(-pred_i)) ) * ( 1 - 1/(1+np.exp(-pred_i)) ) # (1, o.f(1))
dE_dpred_i = dE_dpred * dpred_dpred_i # (1, o.f(1))*(1, o.f(1)) = (1, o.f(1))
앞에서 구했던 ∂E / ∂pred'을 이용하여 ∂E / ∂Wy와 ∂E / ∂By를 본격적으로 구해보자.
- ∂pred` / ∂Wy는 h_2의 전치(Transpose)이다. h_2은 ht_list[3] 에 있다.
- ∂pred` / ∂By는 pred` 형렬 형태의 요소가 모두 1인 행렬이다. 항등원이므로 계산에서 빠져도 된다.
위 과정을 코드로 나타내면 아래와 같다.
# fc 분류기에 대한 기울기 구하기
dpred_i_dWy = np.transpose(ht_list[3], (1, 0)) # (h.n(3), 1)
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)
3. time step 2(=h_1, x_2 입력)에서의 가중치 기울기 구하기
time step 2에서의 가중치 기울기 ∂E / ∂Wxh , ∂E / ∂Whh, ∂E / ∂Bh 를 구해보자.
아래와 같이 기울기 흐름을 관찰하자.
- fc 분류기에서 사용했던 ∂E / ∂pred`의 기울기를 이어받는다.
- pred`에 대한 입력 h_2의 기울기는 Wy의 전치(transpose)이다.
- h_2에 대한 h`_2의 기울기는 하이퍼볼릭 탄센트(tanh) 함수의 미분(1-(tanh x)^(2))이다.
- 위 기울기를 이용하여 ∂E / ∂h`_2 기울기를 정의한다.
코드는 아래와 같다
# time steps = 2(3번째)에 대한 rnn 셀에 대한 역전파
dpred_i_dh2 = np.transpose(Wy, (1, 0)) # (o.f(1), h.n(3))
dE_dh2 = np.dot(dE_dpred_i, dpred_i_dh2) #(1, o.f(1)) × (o.f(1), h.n(3)) = (1, h.n(3))
dh2_dh2_i = (1-np.power(np.tanh(hi_i_list[2]), 2)) # (1. h.n(3))
dE_dh2_i = dE_dh2 * dh2_dh2_i # (1, h.n(3))
위에서 구한 ∂E / ∂h'_2을 이용하여 ∂E / ∂Wxh, ∂E / ∂Whh와 ∂E / ∂Bh 를 구해보자.
- ∂h`_2 / ∂Wxh는 x_2의 전치(Transpose)이다. x_2은 X[2] 에 있다.
- ∂h`_2 / ∂Whh는 h_1의 전치(Transpose)이다. h_1은 ht_list[2] 에 있다.
- ∂h`_2 / ∂By는 h`_2 형렬 형태의 요소가 모두 1인 행렬이다. 항등원이므로 계산에서 빠져도 된다.
코드는 아래와 같다
# dE_dWxh 구하기
dh2_i_dWxh = np.transpose(np.reshape(X[2], (1, -1)), (1, 0)) # (s.l(2), 1)
# X를 그냥 가지고 오면 1차원 배열이여서 2차원으로 바꾸어주어야 함
dE_dWxh = np.dot(dh2_i_dWxh, dE_dh2_i) # (s.l(2), 1) × (1, h.n(3)) = (s.l(2), h.n(3))
# dE_dWhh 구하기
dh2_i_dWhh = np.transpose(ht_list[2], (1, 0)) # (h.n(3), 1)
dE_dWhh = np.dot(dh2_i_dWhh, dE_dh2_i) # (h.n(3), 1) × (1, h.n(3)) = (h.n(3), h.n(3))
# dE_dBh 구하기
dE_dBh = np.sum(dE_dh2_i, keepdims=True) # (1, h.n(3)) -> (1, 1)
# 기울기 누적
dE_dWxh_list += dE_dWxh
dE_dWhh_list += dE_dWhh
dE_dBh_list += dE_dBh
4. time step 1(=h_0, x_1 입력)에서의 가중치 기울기 구하기
time step 1에서의 가중치 기울기 ∂E / ∂Wxh , ∂E / ∂Whh, ∂E / ∂Bh 를 구해보자.
아래와 같이 기울기 흐름을 관찰하자.
- time step 2 에서 사용했던 ∂E / ∂h`_2의 기울기를 이어받는다.
- h`_2에 대한 입력 h_1의 기울기는 Whh의 전치(transpose)이다.
- h_1에 대한 h`_1의 기울기는 하이퍼볼릭 탄센트(tanh) 함수의 미분(1-(tanh x)^(2))이다.
- 위 기울기를 이용하여 ∂E / ∂h`_1 기울기를 정의한다.
코드는 아래와 같다
dh2_i_dh1 = np.transpose(Whh, (1, 0)) # (h.n(3), h.n(3))
dE_dh1 = np.dot(dE_dh2_i, dh2_i_dh1) #(1, h.n(3)) × (h.n(3), h.n(3)) = (1, h.n(3))
dh1_dh1_i = (1-np.power(np.tanh(hi_i_list[1]), 2)) # (1. h.n(3))
dE_dh1_i = dE_dh1 * dh1_dh1_i # (1, h.n(3))
위에서 구한 ∂E / ∂h'_1을 이용하여 ∂E / ∂Wxh, ∂E / ∂Whh와 ∂E / ∂Bh 를 구해보자.
- ∂h`_1 / ∂Wxh는 x_1의 전치(Transpose)이다. x_1은 X[1] 에 있다.
- ∂h`_1 / ∂Whh는 h_0의 전치(Transpose)이다. h_0은 ht_list[1] 에 있다.
- ∂h`_1 / ∂By는 h`_1 형렬 형태의 요소가 모두 1인 행렬이다. 항등원이므로 계산에서 빠져도 된다.
코드는 아래와 같다
# dE_dWxh 구하기
dh1_i_dWxh = np.transpose(np.reshape(X[1], (1, -1)), (1, 0)) # (s.l(2), 1)
# X를 그냥 가지고 오면 1차원 배열이여서 2차원으로 바꾸어주어야 함
dE_dWxh = np.dot(dh1_i_dWxh, dE_dh1_i) # (s.l(2), 1) × (1, h.n(3)) = (s.l(2), h.n(3))
# dE_dWhh 구하기
dh1_i_dWhh = np.transpose(ht_list[1], (1, 0)) # (h.n(3), 1)
dE_dWhh = np.dot(dh1_i_dWhh, dE_dh1_i) # (h.n(3), 1) × (1, h.n(3)) = (h.n(3), h.n(3))
# dE_dBh 구하기
dE_dBh = np.sum(dE_dh1_i, keepdims=True) # (1, h.n(3)) -> (1, 1)
# 기울기 누적
dE_dWxh_list += dE_dWxh
dE_dWhh_list += dE_dWhh
dE_dBh_list += dE_dBh
5. time step 0(= x_0 입력)에서의 가중치 기울기 구하기
time step 0에서의 가중치 기울기 ∂E / ∂Wxh , ∂E / ∂Whh, ∂E / ∂Bh 를 구해보자.
아래와 같이 기울기 흐름을 관찰하자.
- time step 1 에서 사용했던 ∂E / ∂h`_1의 기울기를 이어받는다.
- h`_1에 대한 입력 h_0의 기울기는 Whh의 전치(transpose)이다.
- h_0에 대한 h`_0의 기울기는 하이퍼볼릭 탄센트(tanh) 함수의 미분(1-(tanh x)^(2))이다.
- 위 기울기를 이용하여 ∂E / ∂h`_1 기울기를 정의한다.
코드는 아래와 같다
# time steps = 0(1번째)에 대한 rnn 셀에 대한 역전파
dh1_i_dh0 = np.transpose(Whh, (1, 0)) # (h.n(3), h.n(3))
dE_dh0 = np.dot(dE_dh1_i, dh1_i_dh0) #(1, h.n(3)) × (h.n(3), h.n(3)) = (1, h.n(3))
dh0_dh0_i = (1-np.power(np.tanh(hi_i_list[0]), 2)) # (1. h.n(3))
dE_dh0_i = dE_dh0 * dh0_dh0_i # (1, h.n(3))
위에서 구한 ∂E / ∂h'_0을 이용하여 ∂E / ∂Wxh, ∂E / ∂Whh와 ∂E / ∂Bh 를 구해보자.
- ∂h`_0 / ∂Wxh는 x_0의 전치(Transpose)이다. x_0은 X[0] 에 있다.
- ∂h`_0 / ∂Whh는 h_-1의 전치(Transpose)이다. h_-1은 ht_list[0] 에 있다.
→ 어차피 h_-1은 0이므로 기울기 값은 0이 된다. - ∂h`_0 / ∂By는 h`_0 형렬 형태의 요소가 모두 1인 행렬이다. 항등원이므로 계산에서 빠져도 된다.
코드는 아래와 같다
# dE_dWxh 구하기
dh0_i_dWxh = np.transpose(np.reshape(X[0], (1, -1)), (1, 0)) # (s.l(2), 1)
# X를 그냥 가지고 오면 1차원 배열이여서 2차원으로 바꾸어주어야 함
dE_dWxh = np.dot(dh0_i_dWxh, dE_dh0_i) # (s.l(2), 1) × (1, h.n(3)) = (s.l(2), h.n(3))
# dE_dWhh 구하기
dh0_i_dWhh = np.transpose(ht_list[0], (1, 0)) # (h.n(3), 1)
dE_dWhh = np.dot(dh0_i_dWhh, dE_dh0_i) # (h.n(3), 1) × (1, h.n(3)) = (h.n(3), h.n(3))
# dE_dBh 구하기
dE_dBh = np.sum(dE_dh0_i, keepdims=True) # (1, h.n(3)) -> (1, 1)
# 기울기 누적
dE_dWxh_list += dE_dWxh
dE_dWhh_list += dE_dWhh
dE_dBh_list += dE_dBh
6. loss gradient 함수 정의 및 훈련 결과 탐구하기
위 내용을 정리하여 loss gradeint라는 역전파 시행 함수를 만들 수 있다.
# 한 개의 데이터에 대한 역전파 실행
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_list, hi_i_list = rnn_cell(X)
# hi_i_list는 [h0_i, h1_i, h2_i] 순서
# ht_list는 [h-1, h0, h1, h2] 순서
# x는 [x_0, x_1, x_2] 순서대로 구성됨.
loss = BCEE_loss(pred, Y)
#역전파
dE_dpred = -1*( (Y / pred) - ( (1-Y) / (1-pred) ) ) # (1, o.f(1))
dpred_dpred_i = ( 1/(1+np.exp(-pred_i)) ) * ( 1 - 1/(1+np.exp(-pred_i)) ) # (1, o.f(1))
dE_dpred_i = dE_dpred * dpred_dpred_i # (1, o.f(1))*(1, o.f(1)) = (1, o.f(1))
# fc 분류기에 대한 기울기 구하기
dpred_i_dWy = np.transpose(ht_list[3], (1, 0)) # (h.n(3), 1)
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)
# time steps = 2(3번째)에 대한 rnn 셀에 대한 역전파
dpred_i_dh2 = np.transpose(Wy, (1, 0)) # (o.f(1), h.n(3))
dE_dh2 = np.dot(dE_dpred_i, dpred_i_dh2) #(1, o.f(1)) × (o.f(1), h.n(3)) = (1, h.n(3))
dh2_dh2_i = (1-np.power(np.tanh(hi_i_list[2]), 2)) # (1. h.n(3))
dE_dh2_i = dE_dh2 * dh2_dh2_i # (1, h.n(3))
# dE_dWxh 구하기
dh2_i_dWxh = np.transpose(np.reshape(X[2], (1, -1)), (1, 0)) # (s.l(2), 1)
# X를 그냥 가지고 오면 1차원 배열이여서 2차원으로 바꾸어주어야 함
dE_dWxh = np.dot(dh2_i_dWxh, dE_dh2_i) # (s.l(2), 1) × (1, h.n(3)) = (s.l(2), h.n(3))
# dE_dWhh 구하기
dh2_i_dWhh = np.transpose(ht_list[2], (1, 0)) # (h.n(3), 1)
dE_dWhh = np.dot(dh2_i_dWhh, dE_dh2_i) # (h.n(3), 1) × (1, h.n(3)) = (h.n(3), h.n(3))
# dE_dBh 구하기
dE_dBh = np.sum(dE_dh2_i, keepdims=True) # (1, h.n(3)) -> (1, 1)
# 기울기 누적
dE_dWxh_list += dE_dWxh
dE_dWhh_list += dE_dWhh
dE_dBh_list += dE_dBh
# time steps = 1(2번째)에 대한 rnn 셀에 대한 역전파
dh2_i_dh1 = np.transpose(Whh, (1, 0)) # (h.n(3), h.n(3))
dE_dh1 = np.dot(dE_dh2_i, dh2_i_dh1) #(1, h.n(3)) × (h.n(3), h.n(3)) = (1, h.n(3))
dh1_dh1_i = (1-np.power(np.tanh(hi_i_list[1]), 2)) # (1. h.n(3))
dE_dh1_i = dE_dh1 * dh1_dh1_i # (1, h.n(3))
# dE_dWxh 구하기
dh1_i_dWxh = np.transpose(np.reshape(X[1], (1, -1)), (1, 0)) # (s.l(2), 1)
# X를 그냥 가지고 오면 1차원 배열이여서 2차원으로 바꾸어주어야 함
dE_dWxh = np.dot(dh1_i_dWxh, dE_dh1_i) # (s.l(2), 1) × (1, h.n(3)) = (s.l(2), h.n(3))
# dE_dWhh 구하기
dh1_i_dWhh = np.transpose(ht_list[1], (1, 0)) # (h.n(3), 1)
dE_dWhh = np.dot(dh1_i_dWhh, dE_dh1_i) # (h.n(3), 1) × (1, h.n(3)) = (h.n(3), h.n(3))
# dE_dBh 구하기
dE_dBh = np.sum(dE_dh1_i, keepdims=True) # (1, h.n(3)) -> (1, 1)
# 기울기 누적
dE_dWxh_list += dE_dWxh
dE_dWhh_list += dE_dWhh
dE_dBh_list += dE_dBh
# time steps = 0(1번째)에 대한 rnn 셀에 대한 역전파
dh1_i_dh0 = np.transpose(Whh, (1, 0)) # (h.n(3), h.n(3))
dE_dh0 = np.dot(dE_dh1_i, dh1_i_dh0) #(1, h.n(3)) × (h.n(3), h.n(3)) = (1, h.n(3))
dh0_dh0_i = (1-np.power(np.tanh(hi_i_list[0]), 2)) # (1. h.n(3))
dE_dh0_i = dE_dh0 * dh0_dh0_i # (1, h.n(3))
# dE_dWxh 구하기
dh0_i_dWxh = np.transpose(np.reshape(X[0], (1, -1)), (1, 0)) # (s.l(2), 1)
# X를 그냥 가지고 오면 1차원 배열이여서 2차원으로 바꾸어주어야 함
dE_dWxh = np.dot(dh0_i_dWxh, dE_dh0_i) # (s.l(2), 1) × (1, h.n(3)) = (s.l(2), h.n(3))
# dE_dWhh 구하기
dh0_i_dWhh = np.transpose(ht_list[0], (1, 0)) # (h.n(3), 1)
dE_dWhh = np.dot(dh0_i_dWhh, dE_dh0_i) # (h.n(3), 1) × (1, h.n(3)) = (h.n(3), h.n(3))
# dE_dBh 구하기
dE_dBh = np.sum(dE_dh0_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], target[0]))
이 값을 이용하여 input_RNN[0]인 'soon as as' → [ [ [ 0. 1 ], [ 1. 0. ], [ 1. 0. ] ] ]에 대해 훈련해 보자.
# 훈련 전 예측값
pred, _, _, _ = rnn_cell(input_RNN[0])
print('before pred', pred)
# 경사하강법 적용
learning_rate = 0.1
for i in range(100):
dL_dWy, dL_dBy, dL_dWxh, dL_dWhh, dL_dBh = loss_gradient(input_RNN[0], target[0])
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, _, _, _ = rnn_cell(input_RNN[0])
print(i, BCEE_loss(pred, target[0]))
출력결과
before pred [[0.30956668]]
0 0.2523950249966755
10 0.08017118888651976
20 0.055263087553286315
30 0.04370874812674875
40 0.036677568962188774
50 0.03183015118774915
60 0.028237434828740842
70 0.02544493198737741
80 0.023199725879232608
90 0.021348123128252128
뭔가가 수렴하는 듯 결과가 나온다. 훈련 후 결과를 확인해 보자. soon as as라면 모델 값은 0이 나와야 한다.
# 훈련 후 예측값
pred, _, _, _ = rnn_cell(input_RNN[0])
print('after pred', pred)
출력결과
after pred [[0.01973779]]
다음 시간에는 역전파에서 규칙성을 이용하여 일반화한 코드를 작성해 보도록 하겠다.
'파이썬 프로그래밍 > Numpy 딥러닝' 카테고리의 다른 글
44.[RNN기초] RNN(many to many) 순전파 구현(이론, 실습) (0) | 2023.09.23 |
---|---|
43.[RNN기초] RNN(many to one) 역전파 구현(실습2) (0) | 2023.09.22 |
41.[RNN기초] RNN(many to one) 역전파 구현(이론) (0) | 2023.09.19 |
40.[RNN기초] RNN(many to one) 순전파 구현(실습) (0) | 2023.09.13 |
39.[RNN기초] RNN(many to one) 순전파 구현(이론) (0) | 2023.09.12 |