본문 바로가기

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

[밑바닥부터 시작하는 딥러닝1: Chapter 5] 오차역전파법

728x90



본 포스팅은 밑바닥부터 시작하는 딥러닝1을 토대로 공부한 내용을 정리하기 위한 포스팅입니다.
해당 도서에 나오는 Source Code 및 자료는 GitHub를 참조하여 진행하였습니다.
https://github.com/oreilly-japan/deep-learning-from-scratch

5.2 연쇄법칙


순전파는 계산 결과를 왼쪽에서 오른쪽으로 전달하는 것이다. 한편 역전파는 ‘국소적인 미분’을 순방향과는 반대인 오른쪽에서 왼쪽으로 전달한다. 또한, 이 ‘국소적 미분’을 전달하는 원리는 연쇄법칙(chain rule)에 따른 것이다.

그림 5-6 계산 그래프의 역전파

  [그림 5-6]과 같이 역전파의 계산 절차는 신호 E에 노드의 국소적 미분을 곱한 후 다음 노드로 전달하는 것이다. 여기에서 말하는 국소적 미분은 순전파 때의 y = f(x) 계산의 미분을 구한다는 것이며, 이는 x에 대한 y의 미분을 구한다는 뜻이다. 그리고 이 국소적인 미분을 상류에서 전달된 값에 곱해 앞쪽 노드로 전달하는 것이다.
이것이 역전파의 계산 순서인데, 이러한 방식을 따르면 목표로 하는 미분 값을 효율적으로 구할 수 있다는 것이 핵심이다.

여기서 연쇄법칙이란 합성 함수의 미분에 대한 성질이며, 다음과 같이 정의된다.

합성 함수의 미분은 합성 함수를 구성하는 각 함수의 미분의 곱으로 나타낼 수 있다.

연쇄 법칙(chain rule)

5.3 역전파


덧셈 노드의 역전파를 먼저 알아보자. z = x + y라는 식의 미분은 다음과 같이 해석적으로 계산할 수 있다.

[식 5.5]

[식 5.5]에서와 같이 x에 대한 z와 y에 대한 z는 모두 1이 된다. 이를 계산 그래프로는 [그림 5-9]처럼 그릴 수 있다.

[그림 5-9] 덧셈 노드의 역전파

[그림 5-9]와 같이 역전파 때는 상류에서 전해진 미분에 1을 곱하여 하류로 흘린다. 즉, 덧셈 노드의 역전파는 1을 곱하기만 할 뿐이므로 입력된 값을 그대로 다음 노드로 보내게 된다.

[그림 5-11] 덧셈 노드 역전파의 구체적인 예


구체적인 예를 하나 보면, 가령 ‘10 + 5 = 15’라는 계산이 있고, 상류에서 1.3이라는 값이 흐른다고 한다. 이를 계산 그래프로 그리면 [그림 5-11]처럼 된다.
덧셈 노드 역전파는 입력 신호를 다음 노드로 출력할 뿐이므로 [그림 5-11]처럼 1.3을 그대로 다음 노드로 전달한다.


이어서 곱셈 노드의 역전파를 알아본다. z = xy라는 식을 생각해보자. 이 식의 미분은 다음과 같다.

[식 5.6]

[식 5.6]에서 계산 그래프는 다음과 같이 그릴 수 있다.

[그림 5-12] 곱셈 노드의 역전파

곱셈 노드 역전파는 상류의 값에 순전파 때의 입력 신호들을 ‘서로 바꾼 값’을 곱해서 하류로 보낸다. 서로 바꾼 값이란 [그림 5-12]처럼 순전파 때 x였다면 역전파에서는 y, 순전파 때 y였다면 역전파에서는 x로 바꾼다는 의미이다.

그럼 구체적인 예를 하나 보자. 가령 ‘10 x 5 = 50'이라는 계산이 있고, 역전파 때 상류에서 1.3 값이 흘러온다고 하자. 이를 계산 그래프로 그리면 [그림5-13]처럼 된다.

[그림 5-13] 곱셈 노드 역전파의 구체적인 예

덧셈의 역전파는 순방향 입력 신호의 값은 필요하지 않았지만, 곱셈의 역전파는 순방향 입력 신호의 값이 필요하다. 그래서 곱셈 노드를 구현할 때는 순전파의 입력 신호를 변수에 저장해둔다.

5.4 단순한 계층 구현하기


이번 절에서는 계산 그래프를 파이썬으로 구현한다. 그래프의 곱셈 노드를 ‘MulLayer', 덧셈 노드를 ’ AddLayer'라는 이름으로 구현한다.

모든 계층은 forward()와 backward()라는 공통의 메서드를 갖도록 구현할 것이다. forward()는 순전파, backward()은 역전파를 처리한다.

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
        dy = dout * self.x
        
        return dx, dy


forward()에서는 x와 y를 인수로 받고 두 값을 곱해서 반환한다. 반면 backward()에서는 상류에서 넘어온 미분(dout)에 순전파 때의 갑승ㄹ ‘서로 바꿔’ 곱한 후 하류로 흘러보낸다.

이 MulLayer를 사용해서 [그림 5-16]을 구현해보도록 하자.

[그림 5-16]

MulLayer를 사용하여 [그림 5-16]의 순전파를 다음과 같이 구현할 수 있다.

apple = 100
apple_num = 2
tax = 1.1

#계층들
mul_apple_layer = MulLayer()
mul_tax_layer = MulLayer()

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

print(price)


또, 각 변수에 대한 미분은 backward()에서 구할 수 있다.

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

print(dapple, dapple_num, dtax)


이어서 덧셈 노드인 덧셈 계층을 구현해보자. 덧셈 계층은 다음과 같이 구현할 수 있다.

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__()에서는 아무 일도 하지 않는다.

이상의 덧셈 계층과 곱셈 계층을 사용하여 사과 2개와 귤 3개를 사는 [그림 5-17]의 상황을 구현해보자.

[그림 5-17]

apple = 100
apple_num = 2
orange = 150
orange_num = 3
tax = 1.1


#계층들
mul_apple_layer = MulLayer()
mul_orange_layer = MulLayer()
add_apple_orange_layer = AddLayer()
mul_tax_layer = MulLayer()

#순전파
apple_price = mul_apple_layer.forward(apple, apple_num)
orange_price = mul_orange_layer.forward(orange, orange_num)
all_price = add_apple_orange_layer.forward(apple_price, orange_price)
price = mul_tax_layer.forward(all_price, tax)

#역전파
dprice = 1
dall_price, dtax = mul_tax_layer.backward(dprice)
dapple_price, dorange_price = add_apple_orange_layer.backward(dall_price)
dapple, dapple_num = mul_apple_layer.backward(dapple_price)
dorange, dorange_num = mul_orange_layer.backward(dorange_price)

print(price)
print(dapple_num, dorange_num, dapple_price, dorange_price, dtax)


이처럼 계산 그래프에서의 계층은 쉽게 구현할 수 있으며, 이를 사용해 복잡한 미분도 계산할 수 있다. 다음 절에서는 신경망에 사용하는 계층을 구현해보자.


5.5 활성화 함수 계층 구현하기


계산 그래프를 신경망에 적용해보자. 여기에서는 신경망을 구성하는 층 각각을 클래스 하나로 구현한다. 우선은 활성화 함수인 ReLU와 Sigmoid 계층을 구현한다.

5.5.1 ReLU 계층

활성화 함수로 사용되는 ReLU의 수식은 다음과 같다.

[식 5.7]

[식 5.7]에서 x에 대한 y의 미분은 [식 5.8]처럼 구한다.

[식 5.8]


[식 5.8]에서와 같이 순전파 때의 입력인 x가 0보다 크면 역전파는 상류의 값을 그대로 하류로 흘린다. 반면, 순전파 때 x가 0 이하면 역전파 때는 하류로 신호를 보내지 않는다. 계산 그래프로는 [그림 5-18]처럼 그릴 수 있다.

[그림 5-18] ReLU 계층의 계산 그래프

이제 이 ReLU 계층을 구현해보도록 한다. 신경망 계층의 forward()와 backward() 함수는 넘파이 배열을 인수로 받는다고 가정한다.

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


Relu 클래스는 mask라는 인스턴스 변수를 가진다. mask는 True/False로 구성된 넘파이 배열로, 순전파의 입력인 x의 원소 값이 0 이하인 인덱스는 True, 그 외(0보다 큰 원소)는 False로 유지한다. [그림 5-18]과 같이 순전파 때의 입력 값이 0 이하면 역전파 때의 값은 0이 돼야 하므로, 순전파 때 만들어둔 mask를 역전파 때 써서 mask의 원소가 True인 곳에는 상류에서 전파된 dout을 0으로 설정한다.

5.5.2 Sigmoid 계층


다음은 시그모이드 함수이다. 시그모이드 함수는 다음 식을 의미하는 함수이다.

[식 5.9]


[식 5.9]를 계산 그래프로 그리면 [그림 5-19]처럼 된다.

[그림 5-19] Sigmoid 계층의 계산 그래프


[그림 5-19]에서는 ‘exp'와 '/' 노드가 새롭게 등장했다. 'exp' 노드는 y = exp(x) 계산을 수행하고 ‘/’ 노드는 y = 1/x 계산을 수행한다. 이제 역전파의 흐름을 오른쪽에서 왼쪽으로 한 단계식 짚어보도록 한다.

1단계

’/‘ 노드. 즉, y = 1/x을 미분하면 다음 식이 된다.

[식 5.10]

[식 5.10]에 따르면 역전파 때는 상류에서 흘러온 값에 -y^2을 곱해서 하류로 전달한다. 계산 그래프에서는 다음과 같다.


2단계

'+' 노드는 상류의 값을 여과 없이 하류로 내보내는 게 전부이다.

3단계

‘exp' 노드는 y = exp(x) 연산을 수행하며, 그 미분은 다음과 같다.

[식 5.11]

계산 그래프에서는 상류의 값에 순전파 때의 출력을 곱해 하류로 전파한다.

4단계

‘x'노드는 순전파 때의 값을 ’서로 바꿔‘ 곱한다. 이 예에서는 -1을 곱하면 된다.

이상으로 Sigmoid 계층의 역전파를 계산 그래프로 완성했다. 여기에서 역전파의 최종 출력을 순전파의 입력 x와 출력 y만으로 계산할 수 있다는 것을 눈치챌 수 있다. 그래서 계산 그래프의 중간 과정을 모두 묶어 [그림 5-21]처럼 단순한 ‘sigmoid’ 노드 하나로 대체할 수 있다.

[그림 5-21] Sigmoid 계층의 계산 그래프(간소화 버전)

간소화 버전은 역전파 과정의 중간 계산들을 생략할 수 있어 더 효율적인 계산이라 말할 수 있다. 또, 노드를 그룹화하여 Sigmoid 계층의 세세한 내용을 노출하지 않고 입력과 출력에만 집중할 수 있다. 또한 역전파의 최종 출력은 다음처럼 정리해서 쓸 수 있다.

[식 5.12]

이처럼 Sigmoid 계층의 역전파는 순전파의 출력(y)만으로 계산할 수 있다.

[그림 5-22]

Sigmoid 계층을 파이썬으로 구현한 결과는 다음과 같다.

class Sigmoid:
    def __init__(self):
        self.out = None
        
    def forward(self, x):
        out = 1 / (1 + np.exp(-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-24] Affine 계층의 계산 그래프

[그림 5-24]의 X, W, B는 행렬(다차원 배열)이다. 지금까지의 덧셈, 곱셈, exp, / 연산들에서는 ‘스칼라값’이 노드 사이에 흘렀던 반면에, 이 예에서는 ‘행렬’이 흐르고 잇는 것이다. 행렬을 사용한 역전파도 행렬의 원소마다 전개해보면 다음 식이 도출된다.

[식 5.13]

[식 5.13]에서 행렬의 T는 전치행렬을 의미한다. 전치행렬은 행렬의 (i, j) 위치의 원소를 (j, i) 위치로 바꾼 것을 말한다. [식 5.13]을 바탕으로 계산 그래프의 역전파를 구해보면 [그림 5-25]처럼 된다.

[그림 5-25] Affine 계층의 역전파

[그림 5-25]의 계산 그래프에서는 각 변수의 형상에 집중해야 한다. 특히 X와 X에 대한 L의 미분은 같은 형상이고, W와 W에 대한 L의 미분도 같은 형상이라는 것을 확인할 수 있다.

행렬의 곱에서는 대응하는 차원의 원소 수를 일치시켜야 한다. 따라서 Y에 대한 L의 미분 형상이 (1x3)이고 W의 형상이 (2x3)일 때, X에 대한 L의 미분 형상인 (1x2)를 만들려면 Y에 대한 L의 미분에 W의 전치 행렬(3x2)을 곱해야 한다는 것을 유도할 수 있다. [그림 5-26]을 보면 이해하기 쉽다.

[그림 5-26]


지금까지 설명한 Affine 계층은 입력 데이터로 X 하나만을 고려한 것이었다. 이번에는 데이터 N개를 배치하여 순전파하는 경우, 즉 배치용 Affine 계층을 확인해보자.

[그림 5-27] 배치용 Affine 계층의 계산 그래프

기존과 다른 부분은 입력인 X의 형상이 (Nx2)가 된 것 뿐이다. 그러나 편향 부분에서 주의해야 한다. 순전파 때의 편향 덧셈은 X x W에 대한 편향이 각 데이터에 더해진다. 밑의 코드를 통해 확인해볼 수 있다.

X_dot_W = np.array([[0, 0, 0], [10, 10, 10]])
B = np.array([1, 2, 3])

print(X_dot_W + B)


그래서 역전파 때는 각 데이터의 역전파 값이 편향의 원소에 모여야 한다. 코드로는 다음과 같다.

dY = np.array([[1, 2, 3], [4, 5, 6]])

dB = np.sum(dY, axis = 0)
print(dB)


이 예에서는 데이터가 2개 (N = 2)라고 가정한다. 편향의 역전파는 그 두 데이터에 대한 미분을 데이터마다 더해서 구한다. 그래서 np.sum()에서 0번째 축에 대해서 (axis = 0)의 총합을 구하는 것이다.

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, dout):
        dx = np.dot(dout, self.W.T)
        self.dW = np.dot(self.x.T, dout)
        self.db = np.sum(dout, axis = 0)
        
        return dx


5.6.3 Softmax-with-Loss 계층

마지막으로 출력층에서 사용하는 소프트맥스 함수에 관해 알아보자. 소프트맥스 함수는 입력 값을 정규화하여 출력한다. 예를 들어 손글씨 숫자 인식에서의 Softmax 계층의 출력은 [그림 5-28]처럼 된다.

[그림 5-28]

[그림 5-28]과 같이 Softmax 계층은 입력 값을 정규화(출력의 합이 1이 되도록 변형)하여 출력한다. 또한, 손글씨 숫자는 가짓수가 10개이므로 Softmax 계층의 입력은 10개가 된다.

신경망에서 수행하는 작업은 학습과 추론 두 가지이다. 일반적으로 신경망을 추론할 때는 마지막 Affine 계층의 출력을 인식 결과로 이용한다. 반면에 신경망을 학습할 때는 Softmax 계층이 필요하다.

소프트맥스 계층을 손실 함수인 교차 엔트로피 오차도 포함하여 ‘Softmax-with-Loss 계층’이라는 이름으로 구현한다. 계산 그래프는 다음과 같다.

[그림 5-29] Softmax-with-Loss 계층의 계산 그래프

Softmax-with-Loss 계층은 다소 복잡하다. [그림 5-29]의 계산 그래프는 [그림 5-30]처럼 간소화할 수 있다.

[그림 5-30]

소프트맥스 함수는 'Softmax'계층으로, 교차 엔트로피 오차(CEE)는 ‘Cross Entropy Error' 계층으로 표기했다. 그림과 같이 Softmax 계층은 입력(a1, a2, a3)를 정규화하여 (y1, y2, y3)를 출력한다. CEE 계층은 Sofmtax의 출력(y1, y2, y3)와 정답 레이블(t1, t2, t3)를 받고, 이 데이터들로부터 손실 L을 출력한다.

[그림 5-30]에서 주목할 부분은 역전파의 결과이다. Softmax 계층의 역전파는 (y1-t1, y2-t2, y3-t3)라는 결과를 내놓고 있다. Softmax 계층의 출력과 정답 레이블의 차분인 것이다. 이는 신경망 학습의 중요한 성질이다.

신경망 학습의 궁극적인 목적은 Softmax의 출력이 정답 레이블과 가까워지도록 가중치 매개변수를 조정하는 것이었다. 그래서 신경망의 출력과 정답 레이블의 오차를 앞 계층에 전달해야 하는 것이다. 역전파 결과인 (y1-t1, y2-t2, y3-t3)라는 결과는 바로 신경망의 현재 출력과 정답 레이블의 오차를 있는 그대로 드러내는 것이다.

Softmax-with-Loss 계층을 구현한 코드는 다음과 같다.

class SoftmaxWithLoss:
    def __init__(self):
        self.loss = None
        self.y = None
        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]
        dx = (self.y - self.t) / batch_size
        
        return dx


역전파 때는 전파하는 값을 배치의 수(batch_size)로 나눠서 데이터 1개당 오차를 앞 계층으로 전파하는 점에 주의해야 한다.

5.7 오차역전파법 구현하기


신경망 학습의 전체 순서는 다음과 같다.

전체
신경망에는 적응 가능한 가중치와 편향이 있고, 이 가중치와 편향을 훈련 데이터에 적응하도록 조정하는 과정을 ‘학습’이라 한다. 신경망 학습은 다음과 같이 4단계로 수행한다.

1단계 - 미니배치
훈련 데이터 중 일부를 무작위로 가져온다. 이렇게 선별한 데이터를 미니배치라 하며, 그 미니배치의 손실 함수의 값을 줄이는 것이 목표이다.

2단계 - 기울기 산출
미니배치의 손실 함수 값을 줄이기 위해 각 가중치 매개변수의 기울기를 구한다. 기울기는 손실 함수의 값을 가장 작게 하는 방향을 제시한다.

3단계 - 매개변수 갱신
가중치 매개변수를 기울기 방향으로 아주 조금 갱신한다.

4단계 - 반복
1~3단계를 반복한다.

지금까지 공부한 오차역전파법은 두 번째 단계인 ‘기울기 산출’에 사용된다. 지금까지는 기울기를 구하기 위해 수치 미분을 사용하였다. 그런데 수치 미분은 구현하기는 쉽지만 계산이 오래 걸린다는 단점이 존재한다. 오차역전파법을 이용하면 느린 수치 미분과는 달리 기울기를 효율적이고 빠르게 구할 수 있다.

5.7.2 오차역전파법을 적용한 신경망 구현하기


여기에서는 2층 신경망을 TwoLayerNet 클래스로 구현한다.

# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 부모 디렉터리의 파일을 가져올 수 있도록 설정
import numpy as np
from layers import *
from 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


신경망의 계층을 OrderedDict에 보관하는 점에 집중하자. OrderedDict은 순서가 있는 딕셔너리이다. 그래서 순전파 때는 추가한 순서대로 각 계층의 forward() 메서드를 호출하기만 하면 처리가 완료된다. 마찬가지로 역전파 때는 계층을 반대 순서로 호출하기만 하면 된다. Affine 계층과 ReLU 계층이 각자의 내부에서 순전파와 역전파 전파를 제대로 처리하고 있으니, 그냥 계층을 올바른 순서로 연결한 다음 순서대로(혹은 역순으로) 호출해주면 된다.

5.7.3 오차역전파법으로 구한 기울기 검증하기


지금까지 기울기를 구하는 방법을 두 가지 공부했다. 하나는 수치 미분을 써서 구하는 방법, 또 하나는 해석적으로 수식을 풀어 구하는 방법이다. 수치 미분의 이점은 구현하기 쉽다는 점이다. 그래서 구현에 버그가 숨어 있기 어렵다. 반면, 오차역전파법은 구현하기 복잡해서 종종 버그가 나타난다. 따라서 구현하기 쉬운 수치 미분의 결과와 오차역전파법의 결과를 비교하여 오차역전파법을 제대로 구현했는지 검증하곤 한다. 이처럼 두 방식으로 기울기가 일치함을 확인하는 작접을 기울기 확인(gradient check)이라고 한다.

# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 부모 디렉터리의 파일을 가져올 수 있도록 설정
import numpy as np
from 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 오차역전파법을 사용한 학습 구현하기

마지막으로 오차역전파법을 사용한 신경망 학습을 구현해보자. 기울기를 오차역전파법으로 구한다는 점에 주목하자.

# coding: utf-8
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)


5.8 정리


이번 장에서는 계산 과정을 시각적으로 보여주는 방법인 계산 그래프를 배웠다. 계산 그래프를 이용하여 신경망의 동작과 오차역전파법을 설명하고, 그 처리 과정을 계층이라는 단위로 구현했다. 계층은 ReLu, Softmax-with-Loss, Affine, Softmax 등이 있으며, 모든 계층에서 forward와 backward라는 메서드를 구현한다. forward 메서드는 데이터를 순방향으로 전파하고, backward 메서드는 역방향으로 전파함으로써 가중치 매개변수의 기울기를 효율적으로 구할 수 있다.

728x90