계산그래프
계산 그래프는 계산 과정을 노드와 엣지로 표현하는 그래프를 말한다.
알파벳A를 예시로 계산그래프를 만들어보자.

이런 식으로 오른쪽으로 흘러가며, 총액을 계산하는 계산 그래프이다.
총액은 210달러가 될 것이다.
연산의 종류만 남기고 피연산자는 원 밖으로 빼보자.

이렇게 빼면, 이제 다른 주식들도 더 추가 할 수 있게 된다.
여기에 스페이스X도 추가해보자.

이렇게 덧셈과 곱셈을 엮어서 그래프를 작성 할 수도 있다.
이렇게 왼쪽에서 오른쪽으로 흘러가는 전파를 순전파라고 한다.
당연히 오른쪽에서 왼쪽으로 거슬러가는 전파는 역전파라고 한다.
이렇게 계산 그래프를 사용하는 이유는 증가량을 보기 위해서다.

결국 알파벳 A는 금액에 비해서 2.1배 증가했다.
만약 알파벳 A의 주당 가격이 200달러였다면, 총액은 200 * 2.1 = 420이 되었을 것이다.
결국 한 지점에서의 값이 얼마나 증가했는지를 찾기 위해서 이 계산 그래프를 사용한다.
연쇄법칙
역전파에서는 국소적 미분을 오른쪽에서 왼쪽으로 전달하며, 노드가 지날수록 연쇄법칙을 따른다.
합성함수를 떠올려보자.
다음과 같은 식이 있다면

이거는 x + y = k로 치환한다면, 다음과 같이 나타낼 수 있다.


z를 미분하면

다음과 같은 식이 나오게 된다.
(x+y) 앞에 있던 곱하기 2를 앞으로 더 빼두면 다음과 같이 나누어서 역전파를 구할 수 있게 된다.

역전파
- 덧셈 노드의 역전파
z = x + y만 먼저 역전파를 살펴보면
우선 저 식의 미분은


덧셈노드의 역전파는 그냥 입력값을 그대로 흘려보낸다.
생각해보면 단순히 더하기만 하기 때문에, 해당 값에 변화율이 생기거나 하지는 않을 것 같다.
그렇기에 덧셈이 역전파를 그림으로 나타내면 다음과 같다.

이런 순전파가 있다면, 역전파는 아래와 같다.

그냥 역전파로 들어온 값을 그대로 흘려주면 된다.
- 곱셈 노드의 역전파
z = xy라는 식에서 각각을 x, y로 미분해보면 아래와 같이 나온다.


x의 변화량은 y, y의 변화량은 x가 나오게 되는데 결국 x에 y를 곱하고 y에 x를 곱하기에 다음과 같이 값이 나오게 되는 거 같다.
이를 그림으로 나타내면


위와 같이 나오게 되며, 만약 100 x 2 = 200을 예시로 들어보면

이렇게 상대방의 값을 변화율로 곱해주면 될 것 이다.
단순한 계층 구현
곱셈 노드, 덧셈 노드를 한 번 파이썬을 현해보자
- 곱셈 노드
class MultiLayer:
def __init__(
self
):
self.x = None
self.y = None
def forward(
self,
x: float,
y: float,
) -> float:
self.x = x
self.y = y
out = x * y
return out
def backward(
self,
value: float,
) -> tuple:
dx = value * self.x
dy = value * self.y
return dx, dy
순전파 때에는 x와 y를 곱해주고, 역전파에는 역전파로 넘어온 값에 상대방의 값을 곱해서 보내준다.
알파벳 주식으로 예시 코드를 만들어보면
alpha_a = 100
num = 2
fee = 1.05
multi_alpha_layer = MultiLayer()
multi_fee_layer = MultiLayer()
stock_price = multi_alpha_layer.forward(alpha_a, num)
total_price = multi_fee_layer.forward(stock_price, fee)
print(total_price)

- 덧셈 노드
덧셈 노드는 훨씬 쉽다.
class PlusLayer:
def forward(
self,
x: float,
y: float,
) -> float:
out = x + y
return out
def backward(
self,
value: float,
) -> tuple:
dx = value
dy = value
return dx, dy
당연히 덧셈노드와 곱셈 노드를 섞어서 계산 그래프를 작성 할 수도 있다.
활성화 함수 계층 구현하기
활성화 함수에 사용되는 ReLU, sigmoid등을 노드로 넣어보자.
- ReLU 노드
ReLU함수의 수식은 다음과 같다.

이 식을 미분하면

위와 같다.
x가 0보다 크면, 역전파는 상류의 값을 그대로 하류로 보낸다.
x가 0보다 작으면, 역전파는 하류에 신호를 보내지 않는다.
class ReluNode:
def __init__(
self,
):
self.mask = None
def forward(
self,
x,
):
self.mask = (x <= 0)
out = x.copy()
out[self.mask] = 0
return out
def backward(
self,
value,
):
value[self.mask] = 0
dx = value
return dx
순전파 때 입력이 0 밑인 친구들은 그냥 0으로 보내주면 된다.
- sigmoid 노드
시그모이드 수식은 다음과 같았다.

이거를 계산 그래프로 쭉 펼쳐보자.

이렇게 쭉 펼치고 이거를 하나씩 역전파로 돌아가보자.
- Step1

해당 식을 미분하면 다음과 같다.

여기서 저 위의 식을 다시 대입해보면

그렇기에 y = 1/x의 경우에 상위 노드에서 흘러온 값을 제곱해서 -해주면 된다.
지금까지 구한 부분을 넣어보면 다음과 같다.

- Step2
이번에 만난 노드는 단순히 +이기 때문에 위에서처럼 그냥 흘려주면 된다.

- Step3

이 값의 미분값은 편하게도

이 값이다.
여기에서는 순전파로 -x가 들어오기 때문에 그 값을 그냥 넣어서 하위 노드로 보내주면 된다.

- Step4
여기도 단순히 곱하기 노드이기에 -1만 곱해주면 된다.

이렇게 들어가기 때문에 결국 중간의 노드들은 신경 쓸 필요 없이

이거만 보면 되는 것이다.
근데 또 여기서도 값을 바꿔보려고 시도하면
y에 대입을 하면

이렇게까지 식이 정리되게 된다.
이제 이거를 파이썬 함수로 만들어보자.
import numpy as np
class SigmoidNode:
def __init__(
self,
):
self.out = None
def forward(
self,
x,
):
out = 1 / (1 + np.exp(-x))
self.out = out
return out
def backward(
self,
value,
):
dx = value * (1.0 - self.out) * self.out
return dx
y 값이 역전파에 필요하기에 순전파 때 저장해두었다가 사용한다.
Affine/Softmax 계층 구현하기
행렬의 계산을 계산 그래프로 그려보자.

이제 여기서 X, Y의 변화율을 구하는 방법을 생각해보자.



a x b = z를 생각해보면, z1은 다음과 같은 방법으로 구해진다.

a1은 z1, z2, z3에 모두 곱해진다.
따라서 a1의 변화율은 다음과 같다.

이를 행렬 연산으로 한 번에 나타내면 결국


이렇게 상위 노드에서 온 값에 전치행렬을 맞춰서 곱해주면 구할 수 있게 된다.
이제 python으로 Affine 노드를 구현해보자.
class Affine:
def __init__(
self,
W,
b,
):
self.W = W
self.b = b
self.x = None
self.dW = None
self.db = None
def forward(
self,
x,
):
self.x = x
out = np.dot(x, self.W) + self.b
return out
def backward(
self,
value,
):
dx = np.dot(value, self.W.T)
self.dW = np.dot(self.x.T, value)
self.db = np.sum(value, axis=0)
return dx
- softmax-with-loss
softmax 계층은 입력 값을 정규화하여 출력했다.
교차 엔트로피 오차는 정답인 인덱스의 확률만 구해서 그 값을 로그 취하고 -하는 것이었다.
이 2개를 엮은 계층의 역전파는
그냥 간단하게 y-t라고 한다.
예시를 들어보면 softmax가 (0.1, 0.2, 0.7)을 출력했고, 정답 레이블의 인덱스는 2라고 한다면 (0, 0, 1)이 될 것이다.
그러면 역전파는 (0.1, 0.2, -0.3)이 되는 것이다.
파이썬으로 만들어본다면
class SoftmaxWithLossNode:
def __init__(
self,
):
self.loss = None
self.y = None
self.x = None
def forward(
self,
x,
t,
):
self.t = t
self.y = softmax(x)
self.loss = cross_entropy_error(self.y, self.t)
return self.loss
def backward(
self,
value = 0,
):
batch_size = self.t.shape[0]
dx = (self.y - self.t) / batch_size
return dx
오차역전파법 구현하기
이제 지금까지 했던 것들을 바탕으로 귀여운 신경망을 만들어보자.
step1
훈련 데이터 중 무작위로 가져오고, 그 값들의 손실 함수 값을 측정한다.
이 손실 함수의 값을 줄이는 것이 목표이다.
step2
각 가중치 매개변수의 기울기를 구한다.
이 기울기는 손실 함수의 값을 줄이는 방향을 제시한다.
step3
가중치 매개변수를 기울기 방형으로 조금씩 갱신한다.
step4
손실함수 값이 계속 줄어들도록 앞 단계를 반복한다.