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

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

by Majestyblue 2023. 9. 20.

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

 

39.[RNN기초] RNN 순전파 구현(이론)

이전시간에 자연어 데이터를 이용하기 위해 문장을 의미 있는 단위로 구별(토크나이징)하고 구별한 토큰을 컴퓨터가 이해할 수 있는 데이터로 변환(워드 임베딩)을 실시하였다. 그리고 데이터

toyourlight.tistory.com

 

일단 데이터 전처리와 입력값, 목표값을 생성하자.

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]]

 

 

 

다음 시간에는 역전파에서 규칙성을 이용하여 일반화한 코드를 작성해 보도록 하겠다.