본문 바로가기
ML & DL/책 & 강의

[밑시딥 1] CHAPTER 5 오차역전파법

by 공부하는 무니 2023. 6. 18.
반응형

오차역전파법을 제대로 이해하는 두 가지 방법

1. 수식을 통한 것

2. 계산 그래프를 통한 것

5.1 계산 그래프

5.1.1 계산 그래프로 풀다

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

계산 그래프로 풀어본 문제 1의 답
계산 그래프로 풀어본 문제 1의 답: '사과의 개수'와 '소비세'를 변수로 취급해 원 밖에 표기

문제 2: 현빈 군은 슈퍼에서 사과를 2개, 귤을 3개 샀습니다. 사과는 1개에 100원, 귤은 1개 150원입니다. 소비세가 10%일 때 지급 금액을 구하세요.

계산 그래프로 풀어본 문제 2의 답

계산 그래프를 이용한 문제 진행 흐름

1. 계산 그래프를 구성한다.

2. 그래프에서 계산을 왼쪽에서 오른쪽으로 진행한다. -> 순전파(forward propagation)라고 한다. 

5.1.2 국소적 계산

국소적: '자신과 직접 관계된 작은 범위'

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

사과 2개를 포함해 여러 식품을 구입하는 예

계산 그래프는 국소적 계산에 집중한다. 국소적인 계산은 단순하지만, 그 결과를 전달함으로써 전체를 구성하는 복잡한 계산을 해낼 수 있다.

5.1.3 왜 계산 그래프로 푸는가?

계산 그래프의 이점

1. 국소적 계산: 전체가 아무리 복잡해도 각 노드에서는 단순한 계산에 집중하여 문제를 단순화할 수 있다.

2. 중간 결과를 모두 보관할 수 있다

3. (가장 큰 이유) 역전파를 통해 '미분'을 효율적으로 계산할 수 있다. (역전파에 대해서는 이후에 다시 설명)

역전파에 의한 미분 값의 전달

위 그림에서 역전파는 굵은 선으로 표시되었다. 이 결과로부터 '사과 가격에 대한 지불 금액의 미분'값은 2.2라고 할 수 있다. 사과가 1원 오르면 최종 금액은 2.2원 오른다!

5.2 연쇄법칙

역전파에서 '국소적인 미분'을 오른쪽에서 왼쪽으로 전달했다. 이 원리는 연쇄법칙(chain rule)을 따른 것이다. 

5.2.1 계산 그래프의 역전파

y = f(x)의 역전파

계산 그래프의 역전파 : 순방향과는 반대 방향으로 국소적 미분을 곱한다

국소적 미분이란 순전파 때의 x에 대한 y의 미분을 구한다는 뜻이다. 이 미분값을 상류에서 전달된 값(여기서는 E)에 곱해서 앞쪽 노드로 전달한다. 이런 방식을 따르면 목표로 하는 미분 값을 효율적으로 구할 수 있다. 아래에서 설명할 연쇄법칙 덕분에!

5.2.2 연쇄법칙이란?

연쇄법칙을 설명하려면 우선 합성 함수부터 설명해야 한다. 

함성 함수: 여러 함수로 구성된 함수. 예를 들어 z = (x + y)^2은 아래와 같이 두 개의 식으로 구성된다.

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

즉, 아래와 같다.

연쇄법칙을 써서 x에 대한 z의 편미분을 구해보자. 먼저 각각 국소적 미분(편미분)을 구한다.

그리고 각각을 곱하면 된다.

5.2.3 연쇄법칙과 계산 그래프

위에서 계산한 연괘법칙을 계산 그래프로 나타내보자.

순전파와는 반대 방향으로 국소적 미분을 곱하여 전달한다.

가장 왼쪽의 계산은 결국 x에 대한 z의 편미분이 되어서, 역전파가 하는 일은 연쇄법칙의 원리와 같게 된다.

계산 그래프의 역전파 결과에 따르면 x에 대한 z의 편미분은 2(x+y)가 된다.

5.3 역전파

5.3.1 덧셈 노드의 역전파

z = x + y 라는 식을 대상으로 역전파를 살펴보자. 우선 z = x + y의 미분은 다음과 같이 계산할 수 있다. 

계산 그래프로 그리면 아래와 같다.

덧셈 노드의 역전파: 왼쪽이 순전파, 오른쪽이 역전파다. 덧셈 노드의 역전파는 입력 값을 그대로 흘려보낸다.

즉, 덧셈 노드의 역전파는 1을 곱하기만 할 뿐이므로 입력된 값을 그대로 다음 노드로 보내게 된다.

구체적인 예를 들면 아래와 같다.

덧셈 노드 역전파의 구체적인 예

5.3.2 곱셈 노드의 역전파

z = xy라는 식을 생각해보자. 미분은 다음과 같다.

계산 그래프는 다음과 같다.

곱셈 노드의 역전파: 왼쪽이 순전파, 오른쪽이 역전파다.

곱셈 노드 역전파는 상류의 값에 순전파 때의 입력 신호들을 서로 바꾼 값을 곱해서 하류로 보낸다. 

구체적인 예를 들면 아래와 같다.

곱셈 노드 역전파의 구체적인 예

덧셈의 역전파에서는 상류의 값을 그대로 흘려보내서 순방향 입력 신호의 값은 필요하지 않았지만, 곱셈의 역전파에서는 순방향 입력 신호의 값이 필요하다. 따라서 곱셈 노드를 구현할 때는 순전파의 입력 신호를 변수에 저장해두어야 한다.

5.3.3 사과 쇼핑의 예

사과 쇼핑 예를 다시 보자. 사과의 가격, 사과의 개수, 소비세라는 세 변수가 최종 금액에 어떻게 영향을 주는지를 풀고자 한다. 이를 계산 그래프의 역전파를 사용해서 풀면 아래와 같다. 

사과 쇼핑의 예

결과를 보면, 사과 가격의 미분은 2.2, 사과 개수의 미분은 110, 소비세의 미분은 200이다. 이는 소비세와 사과 가격이 같은 양만큼 오르면 최종 금액에는 소비세가 200의 크기로, 사과 가격이 2.2 크기로 영향을 준다고 해석할 수 있다. 단 이 예에서 소비세와 사과 가격은 단위가 다르니 주의해야 한다.(소비세 1은 100%, 사과 가격 1은 1원)

5.4 단순한 계층 구현하기

위에서 본 '사과 쇼핑' 예를 파이썬으로 구현하자. 계산 그래프의 곱셈 노드를 'MulLayer', 덧셈 노드를 'AddLayer'라는 이름으로 구현한다.

5.4.1 곱셈 계층

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

곱셈 계층은 Mullayer라는 이름의 클래스로 다음과 같이 구현할 수 있다.

layer_naive.py

이 Mullayer를 사용해서 앞에서 본 '사과 쇼핑'을 구현해보자. 계산 그래프로 다시 살펴보면 아래와 같다.

사과 2개 구입
buy_apple.py
buy_apple.py 실행 결과

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

buy_apple.py
buy_apple.py 실행 결과

backward()호출 순서는 forward()와는 반대이다.

5.4.2 덧셈 계층

layer_naive.py

__init__()에서 초기화할 변수가 따로 없으니(backward때 기존 순전파 결과를 저장해야 할 필요가 없으니) pass한다.

 

위 덧셈 계층과 곱셈 계층을 사용하여 사과 2개와 귤 3개를 사는 아래 상황을 구현해보자.

사과 2개와 귤 3개 구입
buy_apple_orange.py
buy_apple_orange.py 실행결과

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

계산 그래프를 신경망에 적용해보자. 신경망을 구성하는 층을 각각의 클래스로 구현한다. 

5.5.1 ReLU 계층

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

위에서 x에 대한 y의 미분은 아래와 같다.

순전파 때의 입력 x가 0보다 크면, 역전파는 상류의 값을 그대로 흘려보내고, 순전파 때 x가 0이하면 역전파때는 하류로 신호를 보내지 않는다(0을 보낸다). 계산 그래프로는 아래와 같이 나타낼 수 있다.

ReLU 계층의 계산 그래프
layers.py

순전파 때의 값이 0 이하면 역전파 때의 값은 0이 되어야 하므로, mask 에 순전파 때 입력값이 0 이하인지 체크하여 저장하고, 역전파 때 mask를 활용한다. 

5.5.2 Sigmoid 계층

시그모이드 함수 식은 다음과 같았다.

위 식을 계산 그래프로 그려보면 다음과 같다.

Sigmoid 계층의 계산 그래프(순전파)

이제 역전파를 알아보자. 

1 단계

'/'노드, 즉 y = 1/x 를 미분하면 다음과 같다. 

역전파 때는 상류에서 흘러온 값에 -y^2을 곱해서 하류로 전달한다. 계산 그래프는 아래와 같다.

2단계

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

3단계

'exp'노드는 y = exp(x) 연산을 수행하고 미분은 아래와 같다.

계산 그래프에서는 상류값에 순전파때의 출력(여기에서는 exp(-x))을 곱해서 하류로 전파한다.

4단계

'x' 노드는 순전파 때의 값을 '서로 바꿔' 곱한다. 

Sigmoid 계층의 계산 그래프

위와 같이 시그모이드 계층의 역전파를 계산 그래프로 그려보았다. 계산 그래프의 중간 과정을 모두 묶으면 아래처럼 단순한 'sigmoid'노드 하나로 대체할 수 있다.

Sigmoid 계층의 계산 그래프(간소화 버전)

시그모이드 역전파의 최종 출력값은 아래처럼 절이해서 쓸 수 있다.

이렇게 정리하면 sigmoid 계층의 역전파는 순전파의 출력(y)만으로 계산할 수 있다.

Sigmoid 계층의 계산 그래프: 순전파의 출력 y만으로 역전파를 계산할 수 있다.

sigmoid 계층을 파이썬으로 구현해보자.

layers.py

순전파의 출력(y)을 변수 out에 저장해두었다가 역전파 계산 때 그 값을 사용한다.

5.6 Affine/Softmax 계층 구현하기

5.6.1 Affine 계층

행렬의 곱 계산은 대응하는 차원의 원소 수를 일치시켜야 했다.

행렬의 곱에서는 대응하는 차원의 원소 수를 일치시킨다.

신경망의 순전파 때 수행하는 행렬의 곱을 기하학에서는 어파인 변환이라고 한다. 따라서 이 책에서는 어파인 변환을 수행하는 처리를 'Affine 계층'이라는 이름으로 구현한다.

곱을 계산하는 노드를 'dot'이라고 하면 np.dot(X, W) + B 계산은 아래 그림처럼 그려진다. 

Affine 계층의 계산 그래프: 변수가 행렬임에 주의. 각 변수의 형상을 변수명 위에 표기했다.

위는 비교적 단순한 계산 그래프이다. 단 X, W, B가 행렬(다차원 배열)이라는 것에 주의하자. 지금까지의 계산 그래프는 노드 사이에 '스칼라값'이 흘럿는데 반해, 이 예에서는 '행렬'이 흐르고 있는 것이다.

이제 위 그림의 역전파에 대해 생각해보자. 행렬을 사용한 역전파도 스칼라값을 사용한 지금까지의 계산 그래프와 같은 순서로 생각할 수 있다. 전개해보면 아래와 같다.

T는 전치행렬을 뜻한다. 전치행렬은 (i, j) 위치의 원소를 (j, i) 위치로 바꾼 것을 말한다. 수식으로는 아래와 같이 쓸 수 있다.

계산 그래프의 역전파를 구해보자.

Affine 계층의 역전파: 변수가 다차원 배열임에 주의. 역전파에서의 변수 형상은 해당 변수명 아래에 표기했다.

각 변수의 형상에 주의해서 사펴보자. 순전파때와의 같은 형상이다.

행렬 곱('dot' 노드)의 역전파는 행렬의 대응하는 차원의 원소 수가 일치하도록 곱을 조립하여 구할 수 있다.

5.6.2 배치용 Affine 계층

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

배치용 Affine 계층의 계산 그래프

기존과 다른 부분은 X의 형상이 (N, 2)가 된 것 뿐이다. 

편향은 순전파때 N개의 데이터 각각에 더해진다. 

반대로 역전파때는 값이 편향의 원소에 모인다. (shape를 유지해야 하기 때문에)

Affine 계층의 구현은 다음과 같다.

layer.py

5.6.3 Softmax-with-Loss 계층

마지막으로 출력층에서 사용하는 소프트맥스 함수에 대해 알아보자. 소프트맥스는 입력 값을 정규화하여 출력한다. 손글씨 숫자 인식에서 Softmax의 출력을 예로 들면 아래와 같다.

입력 이미지가 Affine 계층과 ReLU 계층을 통과하며 변환되고, 마지막 Softmax 계층에 의해서 10개의 입력이 정규화된다. 이 그림에서는 숫자 '0'의 점수는 5.3이며, 이것이 Softmax 계층에 의해서 0.008(0.8%)로 변환된다. 또 '2'의 점수는 10.1에서 0.991(99.1%)로 변환된다.

위와 같이 Softmax 계층은 입력 값을 정규화(출력의 합이 1이 되도록 변형)하여 출력한다. 신경망에서 정규화하지 않는 출력 결과는 점수(score)라고 한다.

일반적으로 추론할 때는 Softmax를 사용하지 않는다. 답을 하나만 내는 경우에는 가장 높은 점수만 알면 되기 때문이다. 반면에 신경망을 학습할 때는 Softmax 계층이 필요하다. 

-> 왜? 학습할때는 정규화된 출력값이어야 공정하기 때문..! 

 

이제 소프트맥스 계층을 구현할 텐데, 손실함수인 교차 엔트로피 오차도 포함하여 'Softmax-with-Loss 계층' 이라는 이름으로 구현해보자.

Softmax-with-Loss 계층의 계산 그래프
간소화한 Softmax-with-Loss 계층의 계산 그래프

위에서 주목해야 할 부분은 역전파의 결과이다. Softmax 계층의 역전파는 (y1-t1, y2-t2, y3-t3)라는 말끔한 결과를 내놓고 있다. 소프트맥스 계층의 추력에서 정답 레이블을 뺀 값이다. 

신경망 학습의 목적은 신경망의 출럭(Softmax의 출력)이 정답 레이블과 가까워지도록 가중치 매개변수를 조정하는 것이었다. 따라서 말이 된다!

소프트맥스 함수의 손실 함수로 교차 엔트로피 오차를 사용하니 말끔하게 떨어졌는데, 이건 우연이 아니라 교차 엔트로피 오차가 그렇게 설계 되었기 때문이다. 또한 회귀의 출력층에서 사용하는 항등 함수의 손실함수로 오차제곱합을 이용하는 이유도 같다. 항등 함수의 손실 함수로 오차 제곱 합을 사용하면 역전파의 결과가 (y1-t1, y2-t2, y3-t3)로 말끔하게 떨어진다.

 

Softmax-with-Loss 계층을 코드로 구현해보자.

layers.py

backward부분 하나하나 뜯어보았다.

마지막에 batch_size로 나눠주는 이유는 순전파때 N개의 배치만큼 더했으니, 역전파때 나눠서 데이터 1개당 오차를 앞 계층으로 전파하기 위함이다. 

5.7 오차역전파법 구현하기

위에서 구현한 계층을 조합하면 레고 블록을 조합하듯 신경망을 구축할 수 있다. 

5.7.1 신경망 학습의 전체 그림

신경망 학습의 순서 복습

전제
신경망에는 적응 가능한 가중치와 편향이 있다.
이 가중치와 편향을 훈련 데이터에 적응하도록 조정하는 과정을 '학습'이라고 한다.
1단계 - 미니배치
훈련 데이터 중 일부를 무작위로 가져온다. 이렇게 선별한 데이터를 미니배치라고 하며, 그 미니배치의 손실 함수 값을 줄이는 것이 우리의 목표이다.
2단계 - 기울기 산출
미니배치의 손실 함수 값을 줄이기 위해 각 가중치 매개변수의 기울기를 구한다. 기울기는 손실함수의 값을 가장 작게 하는 방향을 제시한다.
3단계 - 매개변수 갱신
가중치 매개변수를 기울기 방향으로 아주 조금 갱신한다.
4단계 - 반복
1~3단계를 반복한다.

오차역전파법이 등장하는 단계는 두 번째인 '기울기 산출' 단계이다. 앞에서 이 기울기를 구하기 위해 수치 미분을 사용했다. 그러나 수치 미분은 계산이 오래걸렸다. 오차역전파법을 이용하면 기울기를 효과적이고 빠르게 구할 수 있다.

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

2층 신경망을 TwoLayerNet으로 구현해보자.

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

신경망의 계층을 OrderedDict에 보관하는 점이 중요하다. 순전파 때는 추가한 순서대로 각 계층의 forward()메서드를 호출하기만 하면 처리가 완료된다. 마찬가지로 역전파때는 계층을 반대 순서로 호출하기만 하면 된다. 

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

기울기를 구하는 방법을 두 가지로 설명했다.

1. 수치 미분 -> 느리다. 그러나 구현하기 쉽고 버그가 거의 없다.

2. 해석적으로 수식을 풀어서 구하는 방법 -> 오차역전파법을 이용하여 매개변수가 많아도 효율적으로 계산할 수 있었다. 그러나 구현하기 복잡해서 종종 실수를 한다.

따라서 수치 미분은 오차역전파법을 정확히 구현했는지 확인하기 위해 필요하다.

기울기 확인(gradient check): 두 방식으로 구한 기울기가 일치함을 확인하는 작업

gradient_check.py
gradient_check.py 실행 결과

수치 미분과 오차역전파법으로 구한 기울기의 차이가 매우 작다고 말해준다. 수치 미분과 오차역전파의 오차가 0이 되는 일은 드물다. 컴퓨터가 할수 있는 계산의 정밀도가 유한하기 때문이다.(32비트 부동소수점)

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)

train_neuralnet.py 실행 결과

5.8 정리

  • 계산 그래프를 이용하면 계산 과정을 시각적으로 파악할 수 있다.
  • 계산 그래프의 노드는 국소적 계산으로 구성된다. 국소적 계산을 조합해 전체 계산을 구성한다.
  • 계산 그래프의 순전파는 통상의 계산을 수행한다. 한편, 계산 그래프의 역전파로는 각 노드의 미분을 구할 수 있다.
  • 신경망의 구성 요소를 계층으로 구현하여 기울기를 효율적으로 계산할 수 있다(오차역전파법).
  • 수치 미분과 오차역전파법의 결과를 비교하면 오차역전파법의 구현에 잘못이 없는지 확인할 수 있다(기울기 확인).
반응형

댓글