본문 바로가기

TIL/밑바닥부터 시작하는 딥러닝

계산그래프

1. 계산 그래프(Computatuinal graph)

  • 계산 그래프는 계산 과정을 그래프로 나타낸 것
  • 그래프 자료구조로 복수의 노드(node)와 간선(edge)로 표현됨

1.1. 계산 그래프로 풀다.

  • 문제 1 : 슈퍼에서 1개에 100원인 사과 2개를 샀습니다. 이때 지불 금액을 구하세요. 단, 소비세가 10% 부과됩니다.

  • 그림과 같이 처음에 사과의 100원인 'x 2' 노드로 흐르고, 200원이 되어 다음 노드로 전달된다.
  • 이제 200원이 'x 1.1' 노드를 거쳐 220원이 된다.
  • 따라서 이 계산 그래프에 따르면 최종 답은 220원

계산 그래프를 이용한 문제풀이의 흐름

  1. 계산 그래프를 구성한다.
  2. 그래프에서 계산을 왼쪽 -> 오른쪽으로 진행한다.
    • 계산을 왼쪽 -> 오른쪽으로 진행하는 단계를 순전파(forward propagation 라고 한다.
    • 반대로 오른쪽 -> 왼쪽으로 진행하는 단계를 역전파(backward propagation 라고 한다.
      • 역전파는 이후에 미분을 계산할 때 중요한 역할을 한다.

1.2. 국소적 계산

  • 계산 그래프의 특징은 _국소적 계산_을 전파함으로써 최종 결과를 얻는다는 점이다.

  • 국소적 계산은 결국 전체에서 어떤 일이 벌어지든 상관없이 자신과 관계된 정보만으로 결과를 출력할 수 있다는 것이다.

    • 가령 사과와 그 외의 물품 값을 더하는 계산 * 4000(물품 값) + 200(사과 값) -> 4200 * 은 4000이라는 숫자가 어떻게 계산되었느냐와는 상관없이, 단지 두 숫자를 더하면 된다는 뜻이다.
    • 이런 특징으로 계산 그래프에서 각 노드는 자신과 관련한 계산 외에는 아무것도 신경 쓸 게 없다.

    역전파

    • 그림과 같이 역전파는 순전파와는 반대 방향의 화살표(굵은 선)로 그린다.
    • 이 전파는 '국소적 미분'을 전달하고 그 미분 값은 화살표의 아래에 적는다.
    • 이 예에서 역전파는 왼쪽에서 * 1 -> 1.1 -> 2.2 * 순으로 미분 값을 전달한다.
      • 사과 가격에 대한 지불 금액의 미분 = 2.2
    • 이처럼 계산 그래프의 이점은 순전파와 역전파를 활용해 각 변수의 미분을 효율적으로 구할 수 있다는 것이다.

이처럼 계산 그래프는 국소적 계산에 집중한다.
  • 전체 계산이 아무리 복잡하더라도 각 단계에서 하는 일은 해당 노드의 _국소적 계산_이다.
  • 국소적 계산은 단순하지만, 그 결과를 전달함으로써 전체를 구성하는 복잡한 계산을 해낼 수 있다.

2. 연쇄법칙

  • 역전파에서 국소적인 미분*을 전달하는 원리는 *연쇄법칙(chain rule) 에 따른 것이다.
계산 그래프의 역전파

  • 다음 그림은 $y = f(x)$ 라는 계산의 역전파이다.
  • 그림과 같이 역전파의 계산 절차는 신호 E에 노드의 국소적 미분($ \frac{\partial y}{\partial x} $) 을 곱한 후 다음 노드로 전달하는 것이다.
  • 여기서의 국소적 미분은 순전파 때의 $y = f(x)$ 계산의 미분을 구한다는 것이며, 이는 x에 대한 y의 미분($ \frac{\partial y}{\partial x} $)을 구한다는 뜻이다.
  • 가령 $y = f(x) = x^2$ 라면 $ \frac{\partial y}{\partial x} = 2x $가 된다.

2.1. 합성 함수

연쇄법칙을 설명하려면 먼저 합성 함수에 대해 알아야 한다.

  • 합성 함수란 여러 함수로 구성된 함수이다.
  • 예를 들어 $ z = (x + y)^2 $ 라는 식은 다음과 같은 두 개의 식으로 구성된다.
    $$
    z = t^2
    $$
    $$
    t = x + y
    $$
  • 연쇄법칙은 합성 함수의 미분에 대한 성질이며, 다음과 같이 정의된다.
    • 합성 함수의 미분은 합성 함수를 구성하는 각 함수의 미분의 곱으로 나타낼 수 있다.
  • 예를 들면, $ \frac{\partial z}{\partial x} $ (x에 대한 z의 미분)은 $ \frac{\partial z}{\partial t} $ (t에 대한 z의 미분)과 $ \frac{\partial t}{\partial x} $ (x에 대한 t의 미분)의 곱으로 나타낼 수 있다.

$$
\frac{\partial z}{\partial x} = \frac{\partial z}{\partial t}\frac{\partial t}{\partial x}
$$

그럼 연쇄법칙을 써서 위 식의 미분 $\frac{\partial z}{\partial x}$을 구해보자.

  • 가장 먼저 $z = t^2$ , $t = x + y$ 의 국소적 미분(편미분)을 구한다.

$$
\frac{\partial z}{\partial t} = 2t
$$

$$
\frac{\partial t}{\partial x} = 1
$$

  • $\frac{\partial z}{\partial t}$ 는 $2t$이고, $\frac{\partial t}{\partial x}$는 $1$ 이다.
  • 최종적으로 구하고 싶은 $\frac{\partial z}{\partial x}$는 두 미분을 곱해 계산한다.
    $$
    \frac{\partial z}{\partial x} = \frac{\partial z}{\partial t} \cdot \frac{\partial t}{\partial x} = 2t \cdot 1 = 2(x + y)
    $$

위의 연쇄법칙을 계산 그래프로 나타내보자.

  • 순전파와는 반대방향으로 국소적 미분을 곱하여 전달한다.
  • 역전파 계산 그래프의 예

덧셈 노드의 순전파, 역전파

  • 덧셈 노드의 역전파는 입력 값을 그대로 흘려보낸다.

곱셈 노드의 순전파, 역전파

  • 곱셈 노드의 역전파는 순전파의 입력 신호가 필요하다. 따라서 곱셈 노드 구현시에는 순전파의 입력 신호를 변수에 저장해둔다.
  • 곱셈 노드의 역전파는 상류의 값에 순전파 때의 입력 신호들을 '서로 바꾼 값'을 곱해서 하류로 보낸다.
    • '서로 바꾼 값'이란 순전파 때 x 였다면 역전파에서는 y, 순전파 때 y 였다면 역전파에서는 x로 바꾼다는 뜻이다.
  • 예를 들어, '10 x 5 = 50'이라는 계산이 있고, 역전파 때 상류에서 1.3 값이 흘러온다고 하면 다음 그림과 같이 된다.
  • 입력 신호를 바꾼 값을 곱하여 하나는 1.3 x 5 = 6.5, 다른 하나는 1.3 x 10 = 13 이 된다.

3. 계층 구현하기

  • 계산 그래프의 곱셈 노드를 'MulLayer', 덧셈 노드를 'AddLayer'로 구현해보자
  • 모든 계층은 forward() 와 backword()라는 공통의 메서드를 갖는다.
  • forward()는 순전파, backword()은 역전파를 처리

3.1. 곱셈 계층

class MulLayer:
    def __init__(self):
        self.x = None
        self.y = None

    def forward(self, x, y):
        self.x = x
        self.y = y                
        out = x * y

        return out

    def backward(self, dout):
        dx = dout * self.y  # x와 y를 바꾼다.
        dy = dout * self.x

        return dx, dy


apple = 100
apple_num = 2
tax = 1.1

mul_apple_layer = MulLayer()
mul_tax_layer = MulLayer()

# forward
apple_price = mul_apple_layer.forward(apple, apple_num)
price = mul_tax_layer.forward(apple_price, tax)

# backward
dprice = 1
dapple_price, dtax = mul_tax_layer.backward(dprice)
dapple, dapple_num = mul_apple_layer.backward(dapple_price)

print("price:", int(price))
print("dApple:", dapple)
print("dApple_num:", int(dapple_num))
print("dTax:", dtax)
"""
출력
price: 220
dApple: 2.2
dApple_num: 110
dTax: 200
"""

3.2. 덧셈 계층

class AddLayer:
    def __init__(self):
        pass

    def forward(self, x, y):
        out = x + y

        return out

    def backward(self, dout):
        dx = dout * 1
        dy = dout * 1

        return dx, dy
  • 덧셈 계층에서는 초기화가 필요 없으니 init()에서는 아무 일도 하지 않는다.
apple = 100
apple_num = 2
orange = 150
orange_num = 3
tax = 1.1

# layer
mul_apple_layer = MulLayer()
mul_orange_layer = MulLayer()
add_apple_orange_layer = AddLayer()
mul_tax_layer = MulLayer()

# forward
apple_price = mul_apple_layer.forward(apple, apple_num)  # (1)
orange_price = mul_orange_layer.forward(orange, orange_num)  # (2)
all_price = add_apple_orange_layer.forward(apple_price, orange_price)  # (3)
price = mul_tax_layer.forward(all_price, tax)  # (4)

# backward
dprice = 1
dall_price, dtax = mul_tax_layer.backward(dprice)  # (4)
dapple_price, dorange_price = add_apple_orange_layer.backward(dall_price)  # (3)
dorange, dorange_num = mul_orange_layer.backward(dorange_price)  # (2)
dapple, dapple_num = mul_apple_layer.backward(dapple_price)  # (1)

print("price:", int(price))
print("dApple:", dapple)
print("dApple_num:", int(dapple_num))
print("dOrange:", dorange)
print("dOrange_num:", int(dorange_num))
print("dTax:", dtax)
"""
출력
price: 715
dApple: 2.2
dApple_num: 110
dOrange: 3.3000000000000003
dOrange_num: 165
dTax: 650
"""

'TIL > 밑바닥부터 시작하는 딥러닝' 카테고리의 다른 글

계산그래프 신경망에 적용하기(ReLU, Sigmoid)  (0) 2024.06.28
학습 알고리즘  (1) 2024.06.18
경사법  (1) 2024.06.16
손실 함수  (1) 2024.06.15
소프트맥스 함수  (0) 2024.06.15