해당 포스팅은 '밑바닥부터 시작하는 딥러닝1'을 공부하고 정리, 요약한 글입니다. 모든 내용은 해당 도서를 기준으로 합니다.
◼️ 5.1 계산그래프
계산 그래프는 계산 과정을 그래프로 나타낸 것이다. 그래프는 그래프 자료 구조로 복수의 노드와 에지로 표현된다.
◾ 5.1.1 계산 그래프로 풀다
계산 그래프는 계산 과정을 노드와 화살표로 표현한다. 노드는 원으로 표기하고 원 안에 연산 내용을 적는다.
그림 5-1에서는 x2와 x1.1을 각각 하나의 연산으로 취급해 원 안에 표기했지만 곱셈인 x만 연산으로 생각할 수도 있다. 그렇게 되면 그림 5-2처럼 2와 1.1은 각각 사과의 개수와 소비세 변수가 되어 원 밖에 표기하게 된다.
계산 그래프는 왼쪽에서 오른쪽으로 계산을 진행하며 이를 순전파라고 한다.
반대 방향의 전파는 역전파라고 하며 이후에 미분을 계산할 때 중요한 역할을 하게 된다.
◼️ 5.2 연쇄법칙
◾ 5.2.1 계산 그래프의 역전파
그림 5-6과 같이 역전파의 계산 절차는 신호 E에 노드의 국소적 미분을 곱한 후 다음 노드로 전달하는 것이다. 국소적 미분은 순전파 때의 y=f(x) 계산의 미분을 구한다는 것이며 x에 대한 y의 미분을 구한다느 ㄴ뜻이다.
이 국소적인 미분을 상류에서 전달된 값에 곱해 앞쪽 노드로 전달하는 것이다.
◾ 5.2.2 연쇄법칙이란
합성 함수란 여러 함수로 구성된 함수이다.
연쇄 법칙은 합성 함수의 미분에 대한 성질이며 다음과 같이 정의된다.
합성 함수의 미분은 합성 함수를 구성하는 각 함수의 미분의 곱으로 나타낼 수 있다.
위 과정들은 연쇄 법칙을 나태낸 것이다.
◾ 5.2.3 연쇄법칙과 계산 그래프
연쇄법칙 계산을 계산 그래프로 나타내면 다음과 같다
역전파의 계산 절차에서는 노드로 들어온 입력 신호에 그 노드의 국소적 미분(편미분)을 곱한 후 다음 노드로 전달된다.
즉, 역전파가 하는 일은 연쇄 법칙의 원리와 같다는 것이다.
그림 5-7에 식 5.3의 결과를 대입하면 그림 5-8이 된다.
◼️ 5.3 역전파
◾ 5.3.1 덧셈 노드의 역전파
덧셈 노드의 역전파는 1을 곱하기만 할 뿐 입력된 값을 그대로 다음 노드로 보내게 된다.
최종 출력으로 가는 계산의 중간게 덧셈 노드가 존재한다. 역전파에서는 국소적 미분이 가장 오른쪽의 출력에서 시작하여 노드를 타고 역방향으로 전파 된다.
◾ 5.3.2 곱셈 노드의 역전파
곱셈 노드 역전파는 상류의 값에 순전파 때의 입력 신호들을 서로 바꾼 값을 곱해서 하류로 보낸다.
그림 5-12처럼 순전파 때 x였다면 역전파에서는 y, 순전파 때 y였다면 역전파에서는 x로 바꾼다는 의미이다.
곱셈의 역전파는 순방향 입력 신호의 값이 필요하다. 그래서 곱셈 노드를 구현할 때는 순전파의 입력 신호를 변수에 저장 해 둔다.
◼️ 5.4 단순한 계층 구현하기
◾ 5.4.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
forward()에서는 x와 y를 인수로 받고 두 값을 굽해서 반환한다. backward()에서는 상류에서 넘어온 미분(dout)에 순전파 때의 값을 서로 바꿔 곱한 후 하류로 흘린다.
◾ 5.4.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
forward()에서는 입력받은 두 인수 x,y를 더해서 반환한다. backward()에서는 상류에서 내려온 미분을 그대로 하류로 흘린다.
◼️ 5.5 활성화 함수 계층 구현하기
◾ 5.5.1 ReLU 계층
ReLU의 수식은 식 5.7과 같다.
이 식의 x에 대한 y의 미분은 식 5.8처럼 구하게 된다.
식 5.8과 같이 순전파일 때 입력인 x가 0보다 크면 역전파는 상류의 값을 그대로 하류로 흘린다.
순전파 때 x가 0 이하이면 역전파 때는 하류로 신호를 보내지 않고 0을 보낸다.
class Relu:
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, dout):
dout[self.mask] = 0
dx = dout
return dx
◾ 5.5.2 Sigmoid 계층
시그모이드 함수는 다음 식을 의미하는 함수이다.
그림 5-19에서는 x와 + 노드 말고도 exp와 / 노드가 새롭게 등장했다. exp노드는 y=exp(x) 계산을 수행하고 / 노드는 계산을 수행한다.
/ 노드, y=1/x를 미분하면 다음 식이 된다.
+ 노드는 상류의 값을 여과없이 하류로 내보낸다.
exp 노드는 y=exp(x)연산을 수행하며 미분은 식 5.11과 같다
x노드는 순전파 때의 값을 서로 바꿔 곱한다.
그림 5-20의 계산 그래프의 중간 과정을 모두 묶어 sigmoid 노드 하나로 대체할 수 있다.
간소화 버전은 역전파 과정의 중간 계산들을 생략할 수 있어 더 효율적인 계산이라 말할 수 있다. 또, sigmoid 계층의 세세한 내용을 노출하지 않고 입력과 출력에만 집중 할 수 있다.
이처럼 sigmoid 계층의 역전파는 순전파의 출력(y)만으로 계산할 수 있다.
class Sigmoid:
def __init__(self):
self.out = None
def forward(self, x):
out = sigmoid(x)
self.out = out
return out
def backward(self, dout):
dx = dout * (1.0 - self.out) * self.out
return dx
이 구현에서는 순전파의 출력을 인스턴스 변수 out에 보관했다가 역전파 계산 때 그 값을 사용한다.
◼️ 5.6 Affine/Softmax 계층 구현하기
◾5.6.1 Affine 계층
신경망 순전파에서는 가중치 신호의 총합을 계산하기 때문에 행렬의 곱 np.dot()을 사용했다.
이러한 행렬의 곱은 기하학에서는 Affine 변환이라고 한다. 그래서 이 책에서는 어파인 변환을 수행하는 처리를 Affine 계층이라는 이름으로 구현한다.
◾ 5.6.2 배치용 Affine 계층
데이터 N개를 묶어 순전파 하는 경우인 배치용 Affine 계층은 아래 그림과 같다.
순전파의 편향 덧셈은 각각의 데이터에 더해지며 역전파 때는 각 데이터의 역전파 값이 편향의 원소에 모여야 한다.
편향의 역전파는 그 두 데치터에 대한 미분을 데이터마다 더해서 구한다.
class Affine:
def __init__(self, W, b):
self.W = W
self.b = b
self.x = None
self.original_x_shape = None
# 가중치와 편향 매개변수의 미분
self.dW = None
self.db = None
def forward(self, x):
# 텐서 대응
self.original_x_shape = x.shape
x = x.reshape(x.shape[0], -1)
self.x = x
out = np.dot(self.x, self.W) + self.b
return out
def backward(self, dout):
dx = np.dot(dout, self.W.T)
self.dW = np.dot(self.x.T, dout)
self.db = np.sum(dout, axis=0)
dx = dx.reshape(*self.original_x_shape) # 입력 데이터 모양 변경(텐서 대응)
return dx
◾ 5.6.3 Softmax-with-Loss 계층
소프트맥스 함수는 입력 값을 정규화하여 출력한다.
그림 5-28과 같이 softmax 계층은 입력 값을 정규화(출력의 합이 1이 되도록 변형)하여 출력한다.
손실 함수인 교차 엔트로피 오차도 포함하여 softmax-with-loss 계층이라는 이름으로 구현한다.
간소화한 softmax-with-loss 계층의 계산 그래프는 아래와 같다.
class SoftmaxWithLoss:
def __init__(self):
self.loss = None # 손실함수
self.y = None # softmax의 출력
self.t = 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, dout=1):
batch_size = self.t.shape[0]
if self.t.size == self.y.size: # 정답 레이블이 원-핫 인코딩 형태일 때
dx = (self.y - self.t) / batch_size
else:
dx = self.y.copy()
dx[np.arange(batch_size), self.t] -= 1
dx = dx / batch_size
return dx
◼️ 5.7 오차역전파법 구현하기
◾ 5.7.2 오차역전파법을 적용한 신경망 구현하기
4.5장과 다른 부분은 계층을 사용한다는 것이다. 계층을 사용함으로써 인식 결과를 얻는 처리와 기울기를 구하는 처리 계층의 전파만으로 동작이 이루어 지게 된다.
import sys, os
sys.path.append(os.pardir) # 부모 디렉터리의 파일을 가져올 수 있도록 설정
import numpy as np
from common.layers import *
from common.gradient import numerical_gradient
from collections import OrderedDict
class TwoLayerNet:
def __init__(self, input_size, hidden_size, output_size, weight_init_std = 0.01):
# 가중치 초기화
self.params = {}
self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
self.params['b1'] = np.zeros(hidden_size)
self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)
self.params['b2'] = np.zeros(output_size)
# 계층 생성
self.layers = OrderedDict()
self.layers['Affine1'] = Affine(self.params['W1'], self.params['b1'])
self.layers['Relu1'] = Relu()
self.layers['Affine2'] = Affine(self.params['W2'], self.params['b2'])
self.lastLayer = SoftmaxWithLoss()
def predict(self, x):
for layer in self.layers.values():
x = layer.forward(x)
return x
# x : 입력 데이터, t : 정답 레이블
def loss(self, x, t):
y = self.predict(x)
return self.lastLayer.forward(y, t)
def accuracy(self, x, t):
y = self.predict(x)
y = np.argmax(y, axis=1)
if t.ndim != 1 : t = np.argmax(t, axis=1)
accuracy = np.sum(y == t) / float(x.shape[0])
return accuracy
# x : 입력 데이터, t : 정답 레이블
def numerical_gradient(self, x, t):
loss_W = lambda W: self.loss(x, t)
grads = {}
grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
grads['b2'] = numerical_gradient(loss_W, self.params['b2'])
return grads
def gradient(self, x, t):
# forward
self.loss(x, t)
# backward
dout = 1
dout = self.lastLayer.backward(dout)
layers = list(self.layers.values())
layers.reverse()
for layer in layers:
dout = layer.backward(dout)
# 결과 저장
grads = {}
grads['W1'], grads['b1'] = self.layers['Affine1'].dW, self.layers['Affine1'].db
grads['W2'], grads['b2'] = self.layers['Affine2'].dW, self.layers['Affine2'].db
return grads
◾ 5.7.3 오차역전파법으로 구한 기울기 검증하기
기울기를 구하는 방법으로는 수치 미분을 써서 구하는 방법과 오차역전파법을 이용하는 방법이 있다.
비교적 구현하기 쉬운 수치 미분에 비해 오차역전파법은 구현하기 복잡해서 종종 실수를 하기 때문에 수치 미분은 느리지만 오차역전파법을 정확히 구현했는지 확인하기 위해 필요하다.
두 방식으로 구한 기울기가 일치하는지 확인하는 작업을 기울기 확인이라고 한다.
import sys, os
import numpy as np
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet
# 데이터 로드
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
x_batch = x_train[:3]
t_batch = t_train[:3]
grad_numerical = network.numerical_gradient(x_batch, t_batch)
grad_backprop = network.gradient(x_batch, t_batch)
# 각 가중치의 절대 오차의 평균을 구함.
for key in grad_numerical.keys():
diff = np.average( np.abs(grad_backprop[key] - grad_numerical[key]) )
print(key + ":" + str(diff))
결과가 아주 작은 값이 나왔다면 수치 미분과 오차 역전파법으로 구한 기울기의 차이가 매우 작다고 말해주는 것이다.
◾ 5.7.4 오차역전파법을 사용한 학습 구현하기
오차역전파법을 사용한 신경망 학습을 구현하면 아래와 같다.
import sys, os
sys.path.append(os.pardir)
import numpy as np
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet
# 데이터 읽기
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
iters_num = 10000
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1
train_loss_list = []
train_acc_list = []
test_acc_list = []
iter_per_epoch = max(train_size / batch_size, 1)
for i in range(iters_num):
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
# 기울기 계산
#grad = network.numerical_gradient(x_batch, t_batch) # 수치 미분 방식
grad = network.gradient(x_batch, t_batch) # 오차역전파법 방식(훨씬 빠르다)
# 갱신
for key in ('W1', 'b1', 'W2', 'b2'):
network.params[key] -= learning_rate * grad[key]
loss = network.loss(x_batch, t_batch)
train_loss_list.append(loss)
if i % iter_per_epoch == 0:
train_acc = network.accuracy(x_train, t_train)
test_acc = network.accuracy(x_test, t_test)
train_acc_list.append(train_acc)
test_acc_list.append(test_acc)
print(train_acc, test_acc)
◼️ 정리
계산 그래프를 이용하면 계산 과정을 시각적으로 파악할 수 있다.
계산 그래프의 노드는 국소적 계산으로 구성되며 국소적 계산을 조합해 전체 계산으로 구성한다.
계산 그래프의 순전파는 통상의 계산을 수행한다. 계산 그래프의 역전파로는 각 노드의 미분을 구할 수 있다.
신경망의 구성 요소를 계층으로 구현하여 기울기를 효율적으로 계산할 수 있다(오차역전파법).
수치 미분과 오차역전파법의 결과를 비교하면 오차역전파법의 구현에 잘못이 없는지 확인할 수 있다.