1986 오차역전파
import numpy as np
import matplotlib.pyplot as plt
import torch1986 오차역전파¶
신경망 학습에서 우리가 구해야 하는 것은 손실에 대한 경사입니다. 이 목표는 변하지 않지만, 경사를 구하는 방식은 크게 두 가지로 나뉩니다. 하나는 수치 미분입니다. 미분의 정의에 따라 입력을 아주 조금 바꾸어 보고 출력의 변화량을 직접 측정하는 방식입니다. 그러나 수치 미분은 신경망의 구성이 조금만 복잡해져도 재계산해야 하는 연산의 조합이 폭발적으로 증가합니다. 다른 하나는 해석적으로 미분을 구하는 방식입니다. 그런데 전체 손실 함수를 모든 매개변수에 대해 한꺼번에 해석적으로 미분하는 것은 매우 어렵고, 신경망의 구성이 바뀔 때마다 그 결과도 달라지기 때문에 처음에는 수치 미분을 고려하게 됩니다. 그렇다면 다시 해석적인 방식으로 돌아올 방법은 없을까요?
두 개 이상의 연산 또는 함수가 이어져 구성된 것을 합성 함수라고 합니다. 손실 함수는 여러 연산이 합성되어 있으므로 합성 함수입니다. 합성 함수의 미분을 해석적으로 구하는 효과적인 방법으로 연쇄 법칙(chain rule)이 있습니다. 연쇄 법칙은 17세기에 이미 미분의 기법으로 알려져 있던 정리로, 합성 함수를 구성하는 각 함수의 미분의 곱으로 전체 미분을 구할 수 있다는 것입니다. 합성 함수를 한꺼번에 미분하기는 어렵지만, 그 함수를 국소적인 연산으로 분해한 다음 각 국소 연산의 미분을 구하는 것은 상대적으로 훨씬 쉽습니다. 이렇게 구한 국소적 미분의 결과를 모두 곱하면 합성 함수 전체의 미분이 됩니다.
연쇄 법칙을 손실에 대한 학습 매개변수의 경사를 구하는 데 활용하는 것을 오차 역전파라고 합니다. 신경망 학습 최적화의 수단으로 연쇄 법칙을 적용하는 것이라고 할 수 있습니다. 오차 역전파와 수치 미분은 같은 목표를 서로 다른 방식으로 달성합니다. 수치 미분은 조합의 경로가 폭발적으로 늘어나면서 연산이 어려워지는 데 비해, 오차 역전파는 경사 산출을 위해 손실을 한 번 계산하는 것만으로 모든 매개변수에 대한 경사를 구합니다. 그래서 매개변수의 개수가 늘어나도 연산 횟수의 증가가 매우 완만하며, 오늘날의 신경망 학습에서 오차 역전파를 사용하지 않는 경우는 사실상 없습니다.
학습 최적화의 방법으로 연쇄 법칙을 활용하는 논의는 이미 1960년대부터 있었습니다. 미분 계산이라는 측면에서 17세기의 연쇄 법칙은 잘 알려져 있었기 때문입니다. 이러한 방법론이 정리되어 교재에 실리며 널리 알려진 시점이 1986년입니다. 그래서 오차 역전파의 발명 시점을 흔히 1986년으로 이야기하고, 이 시점을 신경망의 기초 이론이 정립된 때로 보기도 합니다. 오차 역전파를 활용하지 않는 신경망은 실제적으로 학습이 불가능하기 때문에, 오차 역전파는 신경망의 가장 핵심적인 개념이자 기법 중 하나입니다.
class 더하기:
def __init__(self):
self.x = None
self.y = None
def __call__(self, x, y):
"""순전파"""
self.x = x
self.y = y
z = x + y
return z
def backward(self, dz):
"""역전파"""
dx = dz * 1
dy = dz * 1
return dx, dy
class 곱하기:
def __init__(self):
self.x = None
self.y = None
def __call__(self, x, y):
"""순전파"""
self.x = x
self.y = y
z = x * y
return z
def backward(self, dz):
"""역전파"""
dx = dz * self.y
dy = dz * self.x
return dx, dy
class Sum:
def __init__(self):
self.input_shape = None
self.axis = None
def __call__(self, xs, axis=None):
"""순전파"""
self.input_shape = xs.shape
self.axis = axis
z = np.sum(xs, axis=axis)
return z
def backward(self, dz):
"""역전파"""
# dz를 배열로 변환 (스칼라 처리 포함)
dz = np.asarray(dz)
# 출력 형상 계산
if self.axis is not None:
if isinstance(self.axis, int):
output_shape = tuple(dim for i, dim in enumerate(self.input_shape) if i != self.axis)
else: # 다중 축 처리
reduced_shape = list(self.input_shape)
for ax in sorted(self.axis):
reduced_shape[ax] = 1
output_shape = tuple(dim for dim in reduced_shape if dim != 1)
else:
# axis=None (전체 합산)
output_shape = ()
# dz의 형상 검증
if dz.shape != output_shape and dz.shape != ():
raise ValueError(f"dz의 크기 {dz.shape}가 출력 형상 {output_shape}와 일치하지 않습니다.")
# dz를 입력 형상으로 확장
if self.axis is not None:
dz_broadcasted = np.expand_dims(dz, axis=self.axis)
else:
dz_broadcasted = dz
dxs = dz_broadcasted * np.ones(self.input_shape)
return dxs
# 구현 확인
x = np.array([[1, 2, 3], [4, 5, 6]])
sum = Sum()
# 전체 합산 (axis=None)
assert sum(x) == 21
assert np.all(sum.backward(0.1) == 0.1 * np.ones_like(x))
# 축=0
assert np.all(sum(x, axis=0) == [5, 7, 9])
assert np.all(sum.backward([0.1, 0.2, 0.3]) == [[0.1, 0.2, 0.3], [0.1, 0.2, 0.3]])
# 축=1
assert np.all(sum(x, axis=1) == [6, 15])
assert np.all(sum.backward([0.1, 0.2]) == [[0.1, 0.1, 0.1], [0.2, 0.2, 0.2]])
# 다중 축 (axis=(0, 1))
assert sum(x, axis=(0, 1)) == 21
assert np.all(sum.backward(0.1) == 0.1 * np.ones_like(x))
x = torch.tensor(x.astype('float32'), requires_grad=True)
y = torch.sum(x)
print(y)
y.backward(torch.ones_like(y))
print(x.grad)
x.grad.zero_()
y = torch.sum(x, axis=0)
y.backward(torch.tensor([0.1, 0.2, 0.3], dtype=torch.float32))
print(x.grad.numpy().round(1))
x.grad.zero_()
y = torch.sum(x, axis=1)
y.backward(torch.tensor([0.1, 0.2], dtype=torch.float32))
print(x.grad.numpy().round(1))
tensor(21., grad_fn=<SumBackward0>)
tensor([[1., 1., 1.],
[1., 1., 1.]])
[[0.1 0.2 0.3]
[0.1 0.2 0.3]]
[[0.1 0.1 0.1]
[0.2 0.2 0.2]]
1계산 그래프와 순전파¶
연쇄 법칙을 사용하려면 합성 함수를 국소적인 연산으로 나누어 표현하는 것이 도움이 됩니다. 수식으로 표현할 수도 있지만, 각 연산의 흐름을 계산 그래프로 시각화하면 연산별로 중간 결과가 어떻게 변하는지 살펴보기 편리하고 연쇄 법칙을 이해하는 데도 도움이 됩니다. 계산 그래프는 각 연산을 노드로 두고, 데이터가 전달되는 흐름을 화살표로 표시합니다. 입력으로부터 출력을 향해 값이 전달되는 과정을 순전파라고 합니다. 신경망 역시 임의의 합성 함수이고 값이 입력에서 출력을 향해 전달되므로 같은 용어를 사용합니다.
구체적인 예로 사과와 귤의 단가와 수량으로 전체 가격을 계산하는 공식을 생각해 보겠습니다.
이 공식은 더하기와 곱하기 두 종류, 네 개의 연산으로 구성된 합성 함수입니다. 무척 간단하지만 합성 함수이므로 연쇄 법칙의 효과를 그대로 논의할 수 있고, 복잡한 함수도 결국은 국소 연산의 조합이므로 분석 방법은 동일합니다. 과일의 가격은 단가와 수량의 곱이고, 여러 종류를 산다면 각 가격을 더해 합계를 낸 뒤 부가세를 곱해 최종 가격을 결정합니다. 마지막 노드에서 출력이 발생하면 순전파가 완료됩니다. 이때 입력 중 하나라도 값이 바뀌면 최종 출력도 바뀝니다. 그런데 중간 결과를 따로 저장해 두지 않았다면, 입력 하나만 바뀌어도 순전파 전체를 처음부터 다시 계산해야 합니다.
아래에서는 곱하기 연산만으로 이루어진 더 단순한 예로, 사과 가격에 부가세를 곱해 최종 가격을 구하는 순전파를 따라가 봅니다.
사과와 귤의 단가·수량으로 가격을 계산하는 합성 함수의 계산 그래프(순전파). 곱하기·더하기 네 개의 국소 연산으로 입력에서 출력을 향해 값이 전달됩니다.
곱1 = 곱하기()
곱2 = 곱하기()사과단가, 사과수량, 부가세 = 100, 2, 1.1사과가격 = 곱1.forward(사과단가, 사과수량)
사과가격가격 = 곱2.forward(사과가격, 부가세)
가격2역전파와 국소 미분¶
계산 그래프는 전체 합성 함수를 국소적 연산으로 표현하는 것이 특징입니다. 어떤 국소 연산을 라 하고 그 입력을 , 출력을 라고 하겠습니다. 이 출력 는 최종 합성 함수의 출력 의 입장에서 보면 중간 출력입니다. 우리가 알고 싶은 것은 국소 연산의 입력 가 변할 때 최종 출력 가 얼마나 변하는가, 즉 입니다. 와 는 간접적으로 연결되어 있지만, 연쇄 법칙에 따르면 국소 연산의 입력과 출력의 변화량 관계인 를 구하고 여기에 상류에서 전달되어 온 미분을 곱하면 전체 미분을 구할 수 있습니다. 이렇게 미분은 출력에서 입력을 향해, 즉 순전파와 반대 방향으로 전달되므로 역전파라고 합니다.
더하기 역전파. 더하기 연산은 두 입력 , 를 받아 단일 출력 를 냅니다. 각 변수에 대한 국소 미분은 , 입니다. 따라서 상류에서 전달된 미분 에 각각 1을 곱하게 되어, 더하기 연산은 상류의 역전파를 그대로 양쪽으로 흘려보냅니다.
곱하기 역전파. 곱하기 연산 의 국소 미분은 , 입니다. 가 1만큼 변할 때 는 만큼 변하고, 가 1만큼 변할 때 는 만큼 변하기 때문입니다. 따라서 상류에서 전달받은 미분에 순전파 시점의 두 입력값을 서로 바꾸어 곱해 주면 각 입력에 대한 역전파가 됩니다. 이 때문에 곱하기 연산은 순전파 시점의 입력값을 기억해 두어야 합니다.
국소 연산 f의 입력 x와 최종 출력 z는 간접적으로 연결됩니다. 연쇄 법칙은 국소 미분 dy/dx에 상류 미분을 곱해 dz/dx를 구하게 해 주며, 미분은 순전파와 반대 방향(z→x)으로 전달됩니다.
d사과가격, d부가세 = 곱2.backward(1)
d사과가격, d부가세d사과단가, d사과수량 = 곱1.backward(d사과가격)
d사과단가, d사과수량순전파만 사용하면 입력이 바뀔 때마다 전체를 다시 계산해야 하고, 조합의 가짓수가 많아질수록 재계산 횟수가 폭발적으로 증가합니다. 역전파는 이 재계산을 없애 줍니다. 역전파는 순전파와 반대 방향으로 진행하여 출력에서 입력을 향해 내려갑니다. 출력에서는 자기 자신에 대한 변화량이므로 언제나 1에서 시작합니다. 곱하기 연산에서는 순전파 시점의 입력을 서로 바꾸어 곱하고, 더하기 연산에서는 전달받은 값을 그대로 흘려보내며 내려가면 입력까지 역전파가 도달합니다.
이렇게 구해진 값은 곧 입력의 변화량에 대한 최종 출력의 변화량입니다. 앞의 사과 예에서 사과단가의 역전파가 2.2라면 사과단가가 1 변할 때 가격이 2.2만큼 변한다는 뜻이고, 사과수량의 역전파가 110이라면 사과수량이 1개 변할 때 가격이 110만큼 변합니다. 실제로 사과수량을 2개에서 3개로 바꾸면 가격은 715원에서 110을 더한 825원이 되어 순전파로 재계산한 결과와 일치합니다. 다른 점은 더 이상 재계산이 필요 없다는 것입니다. 입력과 최종 출력의 변화량 관계가 역전파로 이미 결정되어 있으므로, 입력이 변할 때마다 출력이 어떻게 바뀌는지 곧바로 알 수 있습니다. 이것이 역전파의 가치입니다.
같은 계산 그래프의 역전파. 출력에서 1로 시작하여, 곱하기는 순전파 입력을 서로 바꿔 곱하고 더하기는 값을 그대로 흘려보냅니다. 사과단가 2.2, 사과수량 110처럼 입력별 경사가 한 번에 결정됩니다(수량 2→3이면 가격 715→825).
3계산 그래프로 본 시그모이드¶
덧셈·곱셈 같은 기본 연산을 계산 그래프의 노드로 두고 연쇄적으로 미분을 전달하면, 시그모이드처럼 여러 연산이 합성된 함수의 미분도 그래프의 역전파로 구할 수 있습니다.
class 더하기:
def __call__(self, x, y):
return x + y
def backward(self, dout):
"""역전파
dout: 상류에서 전해지는 미분값
"""
dx = dout * 1
dy = dout * 1
return dx, dy
class 곱하기:
def __init__(self):
self.x = None
self.y = None
def __call__(self, x, y):
self.x = x
self.y = y
return x * y
def backward(self, dout):
"""역전파
dout: 상류에서 전해지는 미분값
"""
dx = dout * self.y
dy = dout * self.x
return dx, dy
class Inverse:
def __init__(self):
self.x = None
def __call__(self, x):
self.x = x
return 1 / x
def backward(self, dout):
dx = dout * (-1 / self.x ** 2)
return dx
class 나누기:
def __init__(self):
self.곱하기 = 곱하기()
self.역수 = Inverse()
def __call__(self, x, y):
x = self.역수(x)
out = self.곱하기(x, y)
return out
def backward(self, dout):
dx, dy = self.곱하기.backward(dout)
dx = self.역수.backward(dx)
return dx, dy
class Exponetial:
def __init__(self):
self.out = None
def __call__(self, x):
self.out = np.exp(x)
return self.out
def backward(self, dout):
dx = dout * self.out
return dx시그모이드는 2010년 무렵까지 신경망 은닉층의 기본 활성화 함수로 쓰였고, 지금은 주로 ReLU로 대체되었습니다. 시그모이드 역시 여러 연산이 합성된 함수이므로 역전파 분석을 위해 계산 그래프로 표현하고 각 연산에서 전달되는 중간값을 기억해 두는 것이 좋습니다. 복잡한 합성 함수는 출력에서부터 거꾸로 내려오며 분해하는 것이 편리합니다. 최종 출력 직전의 연산은 나누기이고, 그 입력 를 만든 것은 더하기 연산입니다. 더하기의 입력 중 1은 상수이므로 역전파에 관심이 없고, 를 만든 것은 지수 함수이며, 그 입력 를 만든 것은 와 -1을 곱한 곱하기 연산입니다. 여기서 -1도 상수이므로 생략하면, 입력 에 도달하여 분해가 완료됩니다. 이제 각 국소 연산의 미분을 구해 곱하며 내려가면 시그모이드의 역전파가 됩니다.
나누기 역전파. 나누기(역수) 연산 의 국소 미분은 입니다. 이는 출력 를 이용해 으로 정리할 수 있는데, 이렇게 하면 나누기 연산을 한 번 줄일 수 있어 더 적은 연산으로 역전파를 수행할 수 있습니다. 결국 나누기 연산은 순전파의 출력을 기억해 두었다가, 역전파 시점에서 상류의 미분에 을 곱해 하류로 전달합니다.
지수 함수 역전파. 지수 함수 의 미분은 출력과 동일한 입니다. 그래서 순전파의 출력값을 기억해 두었다가 상류에서 전달받은 미분에 곱해 주기만 하면 됩니다.
class Sigmoid:
def __init__(self):
# y = 1 / (1 + exp(-x))
self.mul = 곱하기()
self.exp = Exponetial()
self.add = 더하기()
self.inv = Inverse()
def __call__(self, x):
x = self.mul(x, -1) # -x
x = self.exp(x) # exp(-x)
x = self.add(x, 1) # 1 + exp(-x)
y = self.inv(x) # 1 / (1 + exp(-x))
return y
def backward(self, dout):
dx = self.inv.backward(dout)
dx, _ = self.add.backward(dx)
dx = self.exp.backward(dx)
dx, _ = self.mul.backward(dx)
return dx
sigmoid = Sigmoid()
x = 0.0
y = sigmoid(x)
dydx = sigmoid.backward(1.0)
# 순전파 검증
assert np.isclose(y, 1 / (1 + np.exp(-x)))
# 해석적 미분과 역전파가 일치하는지 확인
assert np.isclose(dydx, y * (1 - y))신경망 역전파 구현
4신경망의 역전파¶
신경망의 구성 자체가 합성 함수입니다. 여러 연산을 거쳐 최종 출력이 발생하는데, 예측을 수행할 때와 달리 학습 시점에서는 손실이 최종 출력입니다. 신경망은 하나 이상의 은닉층으로 구성되며, 각 은닉층은 완전연결 연산을 거친 뒤 비선형 활성화를 적용해 다음 계층으로 출력을 전달합니다. 출력층에서는 과제에 따라 처리가 달라집니다. 회귀에서는 마지막 출력을 그대로 손실 계산에 사용하며 주로 평균제곱오차를 손실 함수로 씁니다. 분류에서는 소프트맥스를 거쳐 확률로 표현한 값을 교차 엔트로피 오차로 처리해 손실을 계산합니다. 아무리 복잡한 합성 함수도 결국은 국소 연산으로 나누어지고 그 국소 미분의 곱으로 전체 미분을 구할 수 있으므로, 신경망을 구성하는 각 연산 단위에 대해 역전파 분석을 수행한 뒤 이어 붙이면 신경망 전체 손실의 경사를 구할 수 있습니다. 이렇게 구한 경사를 경사 하강에 활용해 최적화를 수행합니다.
은닉층의 활성화로는 시그모이드 외에 ReLU도 널리 쓰입니다. 앞서 언급했듯 오늘날에는 시그모이드를 대체한 최신 활성화 함수로, 입력이 음수이면 0으로, 양수이면 입력값을 그대로 내보냅니다. 시그모이드보다 연산이 훨씬 간단하고 국소 미분도 단순합니다. 순전파 시점에 입력이 양수였던 경우에는 역전파에서 전달받은 미분을 그대로 흘려보내고, 입력이 음수였던 경우에는 전달받은 미분이 무엇이든 0으로 막습니다. 아래에서는 본문 신경망에 시그모이드 활성화를 사용하지만, 활성화 단위마다 이렇게 독립적으로 역전파를 분석할 수 있다는 점은 동일합니다.
class Sigmoid:
def __init__(self):
self.y = None
def __call__(self, x):
return self.forward(x)
def forward(self, x):
y = 1 / (1 + np.exp(-x))
self.y = y
return y
def backward(self, dout):
dx = dout * self.y * (1 - self.y)
return dx완전연결 계층 역전파. 완전연결 계층의 출력은 입력과 가중치 행렬의 내적에 편향을 더해 발생하며, 이를 수학적으로 어파인 변환(affine transformation)이라고 합니다. 어파인 변환은 내적과 더하기 두 개의 국소 연산으로 구성되고, 계산 그래프에서는 더하기가 마지막 연산으로 계층의 출력을 냅니다. 더하기 연산의 입력은 '입력과 가중치의 내적 결과’와 '편향’입니다. 더하기는 상류의 미분을 그대로 내려보내므로 여기서 편향에 대한 경사가 결정되고, 내적이 받는 역전파 미분은 완전연결 계층이 받은 역전파 미분과 같습니다.
내적 연산의 역전파는 입력의 전치를 상류 미분과 원래와 반대 순서로 내적하는 것을 국소 연산으로 합니다. 이를 통해 가중치에 대한 경사 가 산출되고, 실제로 하류로 전달되는 역전파는 이전 입력 에 대한 것입니다. 가중치와 편향의 역전파는 더 이상 전달할 이전 연산이 없지만, 그것이 바로 우리가 최적화를 위해 구하고자 하는 경사 정보입니다. 이로써 완전연결 계층의 역전파 분석이 완료됩니다.
완전연결 계층의 어파인 변환은 내적과 더하기 두 국소 연산으로 구성됩니다. 더하기에서 편향 경사 db가, 내적에서 가중치 경사 dW가 결정되고, 입력 x에 대한 역전파가 하류로 전달됩니다.
class 완전연결:
def __init__(self, 입력수, 출력수, 활성화=None):
self.W = np.random.randn(입력수, 출력수)
self.b = np.zeros(출력수)
self.activation = 활성화
self.x = None
self.dW = None
self.db = None
def __call__(self, x):
return self.forward(x)
def forward(self, x):
self.x = x
z = np.dot(x, self.W) + self.b
if self.activation:
return self.activation(z)
return z
def backward(self, dout):
if self.activation:
dout = self.activation.backward(dout)
self.dW = np.dot(self.x.T, dout)
# 배치 단위 연산 시, 미분값을 모든 표본에 대해 더합니다.
self.db = np.sum(dout, axis=0)
dx = np.dot(dout, self.W.T)
return dx소프트맥스 교차 엔트로피 역전파. 분류 출력은 마지막에 소프트맥스로 예측 확률을 발생시키고, 이 예측을 교차 엔트로피 오차로 처리해 손실을 계산합니다. 이 손실이 신경망 학습 전체의 최종 출력이므로, 그 역전파는 언제나 1입니다. 최종 출력과 국소 연산의 중간 출력이 동일하기 때문입니다.
교차 엔트로피 오차도 합성 함수이지만, 복잡한 합성 함수도 분석이 가능하다는 점을 이미 보았으므로 결론만 보겠습니다. 교차 엔트로피 오차의 역전파는 정답을 예측값으로 나눈 값에 -1을 곱한 것이고, 이 역전파를 받은 소프트맥스의 역전파는 결론적으로 예측값에서 정답을 뺀 것입니다. 소프트맥스와 교차 엔트로피는 각각 독립적인 연산이지만, 소프트맥스로 처리한 값은 주로 교차 엔트로피로 이어 처리하므로 두 연산을 묶어 분석하는 것이 편리합니다. 큰 틀에서 보면 소프트맥스 교차 엔트로피의 역전파는 '예측값에서 정답을 뺀 것’이라는 깔끔한 결론에 도달합니다.
분류 출력은 소프트맥스로 예측 확률을 내고 교차 엔트로피 오차로 손실을 계산합니다. 손실의 역전파는 1에서 시작하며, 소프트맥스·교차 엔트로피를 묶어 보면 역전파는 '예측값 − 정답’이 됩니다.
def softmax(z):
if z.ndim == 1:
z = z.reshape(1, -1)
exp_z = np.exp(z - np.max(z, axis=1).reshape(-1, 1))
return exp_z / np.sum(exp_z, axis=1).reshape(-1, 1)
def 교차엔트로피오차(y, y_pred):
delta = 1e-7
배치크기 = y.shape[0]
return -np.sum(y * np.log(y_pred + delta)) / 배치크기
class CrossEntropy:
def __init__(self, from_logits=False):
self.y = None
self.from_logits = from_logits
self.proba = None
def __call__(self, z, y):
return self.forward(z, y)
def forward(self, outputs, y):
self.y = y
# z -> softmax -> proba
if self.from_logits:
self.proba = outputs
else:
self.proba = softmax(outputs)
# CEE(y, proba)
손실 = 교차엔트로피오차(y, self.proba)
return 손실
def backward(self, dout=1):
배치크기 = len(self.y)
dz = self.proba - self.y
return dz / 배치크기class 역전파신경망:
def __init__(self, 손실함수):
self.layers = []
self.loss_func = 손실함수
def add(self, layer):
self.layers.append(layer)
def __call__(self, x):
"""순전파"""
outputs = x
for layer in self.layers:
outputs = layer(outputs)
return outputs # z_last
def 손실산출(self, x, y):
outputs = self(x)
손실 = self.loss_func(outputs, y)
return 손실
def fit(self, x, y, 배치크기, 에폭수, 학습률):
에폭당_배치수 = len(x) // 배치크기
학습횟수 = 에폭당_배치수 * 에폭수
print(f'배치크기={배치크기}, 에폭수={에폭수}, 학습횟수={학습횟수}({에폭당_배치수}/에폭)')
손실변화 = []
for 학습 in range(학습횟수):
# 1. 미니 배치
표본수 = S = len(x)
배치색인 = np.random.choice(표본수, 배치크기)
x_batch = x[배치색인]
y_batch = y[배치색인]
# 2. 경사 산출 (역전파)
# 1) 순전파
손실 = self.손실산출(x_batch, y_batch)
손실변화.append(손실)
# 2) 역전파
dout = self.loss_func.backward(1)
for layer in reversed(self.layers):
dout = layer.backward(dout)
# 3. 매개변수 갱신 (경사 하강)
for layer in self.layers:
if isinstance(layer, 완전연결):
layer.W -= layer.dW * 학습률
layer.b -= layer.db * 학습률
if 학습 == 0 or (학습 + 1) % 100 == 0:
print(f'[학습 {학습 + 1}] Loss: {손실:.3f}')
return 손실변화import pickle
파일경로 = '../data/mnist/mnist_ndarray.pkl'
with open(파일경로, 'rb') as 파일:
mnist_data = pickle.load(파일)
(train_images, train_labels), (test_images, test_labels) = mnist_data
print(train_images.shape, test_images.shape)
assert train_images.shape[1:] == test_images.shape[1:]
assert len(train_images) == len(train_labels)
assert len(test_images) == len(test_labels)def 전처리(images):
X = images.reshape(-1, 28 * 28)
X = X.astype('float32')
X /= 255
return X
X_train = 전처리(train_images)
X_test = 전처리(test_images)
print(X_train.shape, X_test.shape)
print(X_train.max(), X_test.max())
Y_train = np.eye(10)[train_labels]
Y_train.shapeFC = 완전연결
model = 역전파신경망(손실함수=CrossEntropy())
model.add(FC(784, 50, Sigmoid()))
# model.add(Sigmoid())
model.add(FC(50, 100, Sigmoid()))
# model.add(Sigmoid())
model.add(FC(100, 10))
outputs = model(X_test)
print(f'훈련 전 성능: {np.mean(test_labels == np.argmax(outputs, 1)):.2%}')
손실변화 = model.fit(X_train, Y_train, 배치크기=100, 에폭수=10, 학습률=1.0)
outputs = model(X_test)
print(outputs.shape)
예측 = np.argmax(outputs, axis=1)
Y_test = np.eye(10)[test_labels]
손실 = CrossEntropy()(outputs, Y_test)
정확도 = np.mean(test_labels == 예측)
print(f'[시험] 손실: {손실:.3f}, 정확도: {정확도:.2%}')# 에폭마다 손실 표시
에폭별_손실 = 손실변화[::600]
plt.plot(에폭별_손실, 'go--')
plt.show()