2. 해외선물/2-2. 해외선물 알고리즘 연구

(해외선물 자동매매 알고리즘) (2) 역전파(backpropagation) 알고리즘 구현하기

봄이오네 2024. 2. 8. 08:04
반응형

 

목 차
1. 들어가며
2. 사전설명
   1) numpy 라이브러리 6가지 활용 형태
      ① numpy.random
      ② numpy.exp
      ③ numpy.dot
      ④ numpy.mean
      ⑤ numpy.mod
      ⑥ numpy.array
   2) 시그모이드 함수
   3) 행렬의 설명
3. 코드설명
4. 전체코드
5. 마치며

 
 

1. 들어가며

요즘에는 자동매매를 하지 않고, 딥러닝(Deep Learning) 쪽을 공부하고 있다. 딥러닝은 생소한 개념 및 코드 등 약간의 진입장벽이 있는 것 같다. 그래도 조금씩 감이 생기는것 같아, 좌절하지 않고 꾸준히 공부하고 있다.
 
순전파는 무엇이며, 역전파는 무엇일까? 2 + 3의 결과는 5(순전파)이다. 반대의 경우라면??? a+b=5일때 결과값(5)을 알고있으면 a, b는 각각 몇 일까? a, b를 구하는 과정(역전파)을 알아보는 것이다.
 
왠만하면 코드를 이해한후 설명하려는데, 편미분, 시그모이드 함수 등 필자에게 어려운 개념이 발목을 잡는 느낌이다. 어려운 개념은 이렇게 글을 써보면서 이해를 해보는 것도 좋아보인다. 중고등학교 때 영어단어 쓰면서 외우는 느낌이라고 할까? ㅎㅎㅎ
 
코드의 설명 및 출처는 아래와 같다. 원본코드는 최대한 수정하지 않았다. 아래의 역전파 설명은 "수학"적 개념을 설명해주고 있고, 다층 퍼셉트론 구현은 파이썬 코드를 설명해준다. 시간이 될 때 영상을 먼저 보도록 하자. (영상당 25분 정도)
 
※ 역전파 backpropagation 출처 : https://www.youtube.com/watch?v=DMCJ_GjBXwc
※ 다층 퍼셉트론 구현 코드 출처 : https://www.youtube.com/watch?v=fcoVlBIYD54
 


2. 사전설명

1) numpy 라이브러리 6가지 활용 형태

numpy 라이브러리에 대한 이해가 필요하다. 여기서는 6가지 정도가 쓰인다.
① numpy.random

  • 정의 : random 모듈을 이용하여 특정범위, 개수, 형태를 가진 숫자(배열)을 생성
  • 활용 : numpy.random.random(a, b)는 a~b 범위내에서 임의의 실수를 반환

② numpy.exp

  • 정의 : 지수함수(e^x)로 반환한다. numpy.exp(2)d이면 e^2(2제곱)을 말한다.
  • 활용 : numpy.exp(-x)는 e^(-x)승을 반환한다. (시그모이드 함수에서 활용한다)

③ numpy.dot

  • 정의 : dot 안에 있는 숫자(배열)을 곱한다.
  • 활용 : numpy.dot(a, b)는 a x b를 말한다.

④ numpy.mean

  • 정의 : numpy를 활용하여 평균을 구한다.
  • 활용 : numpy.mean(리스트 등)은 "리스트 등"의 평균을 구한다.

⑤ numpy.mod

  • 정의 : 나머지를 구한다
  • 활용 : numpy.mod(10, 3)의 그 결과값은 1(10/3)이다.

⑥ numpy.array

  • 정의 : 2~3차원 배열 등의 다차원 배열 자료구조로 바꾸어준다.
  • 활용 : numpy.array([[0,1,2], [4,5,6]])는 2 x 3 배열구조를 만든다.

 

2) 시그모이드 함수

시그모이드 함수의 개념 및 활용 이유는 제대로 설명하기 힘들다.ㅠㅠ 개념부터 파악하고 설명하면 좋겠지만, 필자의 능력 밖의 일인 것 같다. 위키백과(시그모이드 함수)에 나와있는 설명을 참고하자.
 

그림2. 시그모이드 함수

 
시그모이드 함수의 형태이다. (출처 : 위키백과의 시그모이드 함수 설명)
 

3) 행렬의 설명

행렬에서 가로줄은 행, 세로줄은 열이다. 1행 2열은 [[1,2]]이고, 2행 3열은 [[1,2,3], [4,5,6]]이다. 여기서 행렬간 곱은 1x2 및 2x1의 결과는 1x1 배열이 된다. 또한 1x2 및 2x2을 곱한 결과는 1x2 배열이 된다.
 


3. 코드설명

유튜버 신박AI님의 코드이다. 출처는 아래과 같다. 필자는 시그모이드 함수의 개념/활용이유 및 기타 편미분 개념, 체인룰 등은 제대로 설명하지 못한다. 4단계(순전파-손실계산-역전파-학습)의 과정을 알아보자.
 
    ※ 코드 출처 : https://www.youtube.com/watch?v=fcoVlBIYD54
 

그림3. 순전파, 역전파 화면(출처:유튜버 신박AI님의 영상에서 발췌)

 
< 그림3 >은 순전파 및 역전파에 대한 내용이다. x1 → z1 → z3 흐름이 순전파이고, z3 → z1 → x1이 역전파의 흐름이다.
 

그림4-1. 가중치 및 순전파, MSE 손실계산의 내용

 
1줄 : 넘파이 함수를 임포트한다.
4줄 : Shin_park_ai 클래스를 선언한다.
5줄~14줄 : 변수를 초기화한다.
5줄~7줄 : 90줄에서 입력받을 변수를 전역변수로 만든다. 클래스 내에서 self를 붙이면 전역변수가 된다.
10줄~14줄 : 가중치를 초기화한다. self.w1_2_3_4는 2x2 배열이고, self.w5_6은 2x1 배열이다.
 
16줄~17줄 : 시그모이드 함수를 선언하고, return으로 결과값을 반환한다.
 
19줄~25줄 : 순전파 함수를 정의한다. 47줄, 70줄~82줄에서 forward 함수를 실행할 때 실행될 순전파 함수를 정의한다.
21줄 : 47줄, 70~82줄에서 입력받은 변수(x) 및 12줄의 self.w1_2_3_4(2x2배열)를 곱한다. 입력받은 변수 x는 1x2 배열이고, self.w1_2_3_4는 2x2배열이므로, 결과값은 1x2 배열로 출력된다.
22줄 : 21줄에서 구한 self.z1_2를 16줄의 시그모이드 함수에 넣어서 16줄의 함수를 실행한다.
23줄 : 22줄에서 구한 self.h(1x2배열)와 14줄의 self.w5_6(2x1배열)을 곱한다. 결과값은 1x1의 배열이다.
24줄 : 시그모이드 함수를 실행한다.
 
27줄~29줄 : y실측값과 y예측치를 평균제곱오차를 활용하여 결과값을 얻는다. (MSE 평균오차활용)
 

그림4-2. 역전파 및 학습을 설명

 
31줄~40줄 : 역전파를 설명한다.
31줄 : 53줄에서 입력받은 변수를 넣은 역전파 함수를 정의한다.
※ 여기서 체인룰은 A/D = (A/B) * (B/C) * (C/D)를 말한다. 우리가 구하고 싶은 값은 A/D인데, A/D를 구할 수 없을 때, 이미  알고있는 A/B, B/C, C/D를 곱해서 구한다는 내용이다. 어렵다. @@
 
33줄 : 29줄의 평균제곱오차(MSE)를 편미분한 값이다. 설명하기 어렵다. 외울 수 밖에 없다. ㅠㅠ
34줄 : 시그모이드 함수로 구해진 값에 저장된 오차값이다.
35줄 : 22줄에서 구한 값이다.
36줄 : 체인룰을 통해 구한값이다. 결과값은 1x2 배열이다.
37줄 : 경사하강법 가중치 업데이트 공식이다. (새로운 연결강도 값 = 현체결강도 - (∂C/∂W5 x 학습률)
40줄 : 새로운 가중치를 구한다. 행렬 모양을 맞추기 위해 .T를 써준다. 어렵다. ㅠㅠ
 
42줄~56줄 : 학습을 하는 함수를 정의한다.
44줄 : 66줄에서 1,000번을 실행하는 반복횟수를 구한다.
45줄 : 60줄에서 입력받은 x_train은 학습을 위한 데이터이며, [0,0], [0,1], [1,0], [1,1]의 4가지이며, 4가지의 데이터를 100번을 랜덤하게 돌린다. 60줄의 100은 100번을 반복하라는 이야기이다. 즉, 45줄의 len(x_train)은 100이다.
47줄 : 60줄에서 랜덤으로 들어오는 i번째 x_train 데이터에 대해 순전파를 구하여 y_pred에 넣는다.
50줄 : mse를 통해 오차값을 구한다.
53줄 : 31줄에 x_train[i], y_train[i], y_pred(y예측값), learning_rate(학습률)을 31줄에 넣어서 역전파를 실행한다.(가중치를 업데이트 하기 위함)
55줄~56줄 : i 값을 100으로 나눈 나머지 값이 0일때, 출력하라.
 

그림4-3. 학습할 데이터를 정의한다.

 
 
58줄 : 입력 및 출력할 값을 정의한다.
60줄 : x_train 값을 정의한다. 총 4가지 패턴이며, 랜덤으로 100개씩 2쌍의 데이터를 생성한다.
   * 4가지 패턴 : [0,0], [0,1], [1,0], [1, 1]
62줄 : [0,0]혹은 [1,1]이 입력되면 0으로 출력되고, [0,1]혹은 [1,0]이 입력되면 1로 출력된다.
66줄 : 42줄의 train 함수에 4가지 변수(x_train, y_train, epoch=1000, learning_rate=0.1)을 넣어서 실행한다.
 
69줄~83줄 : 4가지 데이터([0,0], [0,1], [1,0], [1,1])를 넣어서 모델값을 예측한다.
 
90줄 : 입력한 층은 2개, 은닉층은 2개, 결과값은 1개를 넣어 다층페셉트론을 선언한다. 어렵다. @@
92줄 : 58줄의 함수를 실행한다.
 

그림4-4. 반복횟수에 따른 오차값 및 예측값이다.

 


4. 전체코드

원본코드는 유튜버 신박AI님의 https://www.youtube.com/watch?v=fcoVlBIYD54이다.
 

더보기
import numpy as np

# class MLP:
class Shin_park_ai:
    def __init__(self, input_size, hidden_size, output_size):  # input_size 2개, hidden_size 2개, output_size 1개인 다층(3층) 신경망을 만드려고 함
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.output_size = output_size

        # 가중치 초기화
        # self.w1_2_3_4 = np.random.random((self, input_size, hidden_size, output_size))    # 가중치를 랜덤하게 선언할 수도 있음
        self.w1_2_3_4 = [[1, 10], [1, 10]]  # 이 코드에서는 가중치를 fixing해서 선언하였음
        # self.w5_6 = np.random.random((self, input_size, hidden_size, output_size))
        self.w5_6 = [[-40], [40]]

    def sigmoid(self, x):
        return 1 / (1 + np.exp(-x))  # np.exp(x)는 지수함수(e^x)로 변환됨. 예를 들어 np.exp(10)은 e^10승인다. 여기서 np.exp(-x)이므로 e^(-x)승을 나타낸다.

    def forward(self, x):  # 순전파       # x는 입력값(input)
        # propagate inputs through the network                          #
        self.z1_2 = np.dot(x, self.w1_2_3_4)    # 1 x 2배열로 출력됨      # 12줄의 self.w1_2_3_4는 2x2 배열이므로, 입력받는 x 값도 1x2 혹은 2x2 배열로 입력되어야 한다.
        self.h = self.sigmoid(self.z1_2)        # 1 x 2배열로 출력됨      # z3를 구하기 위한 은닉층의 output이다. z3 = h1w5 + h2w6 (h1,h2는 은닉층이고, w5,w6는 z3를 구하기 위한 은닉층의 가중치이다)
        self.z3 = np.dot(self.h, self.w5_6)     # 1 x 1배열로 출력됨      # 14줄의 self.w5_6은 2x1배열형태이므로, self.h(1x2배열) x self.w5_6(2x1배열)의 결과는 1x1 배열로 출력된다.
        self.o = self.sigmoid(self.z3)          # 1 x 1배열로 출력됨
        return self.o

    def mse_loss(self, y_true, y_pred):
        # MSE 손실계산
        return np.mean((y_true - y_pred) ** 2)  # 평균제곱오차 (Mean Squared Error)

    def backward(self, x, y, y_pred, learning_rate):  # 역전파 알고리즘
        # 체인룰 계산
        dc_do1 = -2 * (y - y_pred)                              # 27줄~29줄의 MSE 손실계산의 합산(1/n∑(y_true - y_pred) ** 2)은 C = np.mean((y_true - y_pred) ** 2)으로 쓸 수 있고, 손실함수 C = (y - o1)^2로 바꾸어 쓸 수 있다. 손실함수 C를 o1에 관해 편미분해주면 ∂C/∂o1 = -2(y-o1)^(2-1)로 계산된다.
        do1_dz3 = y_pred * (1 - y_pred)                         # 33줄의 o1은 시그모이드 함수(19줄~25줄)를 통해 구해진 output(결과값)에 저장된 오차값이다.
        dz3_dw5_6 = self.h                                      # 22줄에서 구한다. (은닉층의 output을 구한다)
        dc_dw5_6 = dc_do1 * do1_dz3 * dz3_dw5_6                 # 1 x 2배열이 출력된다.     # 체인률을 적용한다.(3개의 편미분한 값을 각각 곱한다)
        self.w5_6 = self.w5_6 + learning_rate * - dc_dw5_6.T    # 2 x 1배열이 출력된다.     # 경사하강법 가중치 업데이트 공식 : 새 연결강도 = (현 연결강도) + (-∂C/∂W5 x 학습률)
        dc_dw1_2_3_4 = dc_do1 * do1_dz3 * np.dot(self.w5_6 * (self.h * (1 - self.h)).T, x)  # 손실함수 C의 변화량을 구한다. (변형된 가중치를 넣어준다)     # .T는 배열이 2차원 배열이니, 차원을 맞혀주는 기능(transfer)을 한다. 즉, 행렬을 계산하기 위해 차원을 맞추어준다.

        self.w1_2_3_4 = self.w1_2_3_4 + learning_rate * - dc_dw1_2_3_4.T  # 행렬 모양을 맞추기 위해 .T를 써주어서, 새로운 가중치를 구한다. (2층부터 1층까지 가중치를 업데이트 해준다) (순전파 1층 → 2층 → 3층, 역전파 3층 → 2층 → 1층이다.)

    def train(self, x_train, y_train, epochs, learning_rate):  # x_train(학습을 하는 데이터 모음, 00,01,10,11의 4가지 데이터), y_train(결과값이며 0, 1이다)
        # for epoch in range(epoch):
        for epoch in range(epochs):
            for i in range(len(x_train)):
                # forward pass
                y_pred = self.forward([x_train[i]])

                # compute and rint loss
                loss = self.mse_loss([y_train[i]], y_pred)

                # backward pass
                self.backward([x_train[i]], [y_train[i]], y_pred, learning_rate)

            if np.mod(epoch, 100) == 0:
                print('epoch=', epoch, 'loss=', loss)

    def m_func_start(self):
        # 데이터 생성
        x_train = np.random.randint(0, 2, (100, 2))             # 100개 쌍(200개)의 데이터를 생성한다. (x, y)형태로, 0과 1로 이루어진다.
        # print(x_train[1])                                     # 4가지([0,0], [1,0],[0,1],[1,1])를 랜덤으로 출력          # 여기서는 출력형태는 "array[1,0]" 출력된다.
        y_train = (x_train[:, 0] != x_train[:, 1]).astype(int)  # 서로 XOR 게이트의 output이다.
        # print(y_train[1])                                     # 여기서는 1이 출력된다.    # 출력형태는 2가지(0, 1) 둘중의 하나로 출력              # [0,0] 혹은 [1,1]은 0으로 출력, [1,0]혹은 [0,1]은 1로 출력

        # 모델 학습
        m_test.train(x_train, y_train, epochs=1000, learning_rate=0.1)  # 트레이닝을 한다.

        ### 테스트 값으로 모델값 예측 (트레이닝(65줄) 후 모델값을 예측한다)
        test_input = np.array([[0, 0]])                           # [[0 0]]이 들어가면 0에 가까운 값이 나와야한다.
        predicted_output = m_test.forward(test_input)
        print("predicted_output:", test_input, predicted_output)  # [[0 0]] [[0.01185694]]

        test_input = np.array([[1, 0]])                           # [[0 1]]이 들어가면 1에 가까운 값이 나와야한다.
        predicted_output = m_test.forward(test_input)
        print("predicted_output:", test_input, predicted_output)  # [[1 0]] [[0.97098306]]

        test_input = np.array([[0, 1]])                           # [[1 0]]이 들어가면 1에 가까운 값이 나와야한다.
        predicted_output = m_test.forward(test_input)
        print("predicted_output:", test_input, predicted_output)  # [[0 1]] [[0.97212242]]

        test_input = np.array([[1, 1]])                           # [[1 1]]이 들어가면 0에 가까운 값이 나와야한다.
        predicted_output = m_test.forward(test_input)
        print("predicted_output:", test_input, predicted_output)  # [[1 1]] [[0.03973796]]


if __name__ == "__main__":
    # m_test = MLP

    # 다층페셉트론 선언
    m_test = Shin_park_ai(input_size=2, hidden_size=2, output_size=1)

    m_test.m_func_start()

 


5. 마치며

훌륭한 코드를 접하게 되면, 경외감이 들 때가 있다. 수학적 개념이 동반된 어렵다는 생각이 많이 들지만... 그래도 역전파 코드가 어떻게 이루어지는 궁금했는데, 실제 역전파 구성코드를 보니 생각보다 간단해서 약간 당황스럽다. 시그모이드 함수, MSE 오차 및 편미분 등 이해하려면 상당한 시간이 필요할 것 같다. 이해가 안되면... 외울 수 밖에 없다. ㅠㅠ
 
필자가 설명을 하다가 이해가 안되는 부분이 많다. 결국 유튜버 신박AI님의 영상을 참고해야 될 것이다. 차분하게 설명을 해주시는게 정말 마음에 든다. 시간이 될 때마다, 영상을 보며 이해를 하려고 한다. ㅎㅎㅎ
 
역전파는 구했으니, MCTS 구조를 조금은 이해할 수 있을거 같긴 한데... 이또한 많은 고비가 있을 것 같다. 해외선물 수익은 쉽게 주지 않을 것이다. 인내심을 가지고, 꾸준히 코드를 공부해서 반드시 수익을 내도록 하자! ^^
 
 

반응형