본문 바로가기
Study/책『밑바닥부터 시작하는 딥러닝』

5-2. 오차역전파법

by 코드포휴먼 2020. 4. 9.

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

계산 그래프를 신경망에 적용하기 위해 신경망 계층을 클래스 하나로 구현한다.

우선 활성화 함수인 ReLU와 Sigmoid 계층을 구현한다.

 

5.5.1. ReLU 계층

활성화 함수로 사용되는 ReLU 수식은 아래와 같다.

x에 대한 y의 미분은 다음처럼 구한다.

 

순전파 때의 입력인 x가 0보다 크면 역전파는 상류의 값을 그대로 하류로 흘린다.

순전파 때 x가 0 이하면 역전파 때는 하류로 신호를 보내지 않는다(0을 보낸다).

계산 그래프로는 아래처럼 그린다. 

ReLU 계층의 계산 그래프

 

ReLU 계층을 구현할 건데, 신경망 계층의 forward()와 backward() 함수는 넘파이 배열을 인수로 받는다고 가정한다.

ReLU 계층을 구현한 코드는 common/layers.py에 있다. 

(깃허브https://github.com/WegraLee/deep-learning-from-scratch 에서 common/layers.py파일.)

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로 유지한다.

아래는 예시다.

x = np.array( [[1.0, -0.5], [-2.0, 3.0]] )
print(x)
'''
[[ 1.  -0.5]
 [-2.   3. ]]
'''

mask = (x <= 0)
print(mask)
'''
[[False  True]
 [ True False]]
'''

### True(x <= 0) 인 원소를 뽑아보기 
out = x.copy()
print(out[mask]) 
# [-0.5 -2. ]

### True(x <= 0) 인 원소를 0으로 만들기
out[mask] = 0
print(out[mask])
# [0. 0.]

순전파 때의 입력값이 0 이하면 역전파 때의 값은 0이 돼야 한다.

그래서 역전파 때는 순전파 때 만들어둔 mask를 써서 mask의 원소가 True인 곳에는 상류에서 전파된 dout을 0으로 설정한다.

 

 

5.5.2. Sigmoid 계층

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

 

위 식을 계산 그래프로 그리면 아래처럼 된다.

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

그림에서는 'x'와 '+' 노드 말고도 'exp'와 '/' 노드가 새로 등장했다. 

'exp' 노드는 y = exp(x) 계산을 수행하고 '/' 노드는 y = 1/x 계산을 수행한다. 

노드의 입력 1+exp(-x) 전체를 x 하나로 치환하여 이해한다.

 

시그모이드 함수식의 계산은 국소적 계산의 전파로 이뤄진다. 

역전파에서는 오른쪽에서 왼쪽의 흐름을 한 단계씩 짚어보겠다.

 

1단계

'/' 노드, 즉 y = 1/x을 미분하면 다음 식이 된다. (분수 미분과정)

역전파 때는 상류에서 흘러온 값에 순전파의 출력을 제곱한 후 마이너스를 붙인 값을 곱해서 하류로 전달한다.

 

2단계

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

 

3단계

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

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

 

4단계

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

Sigmoid 계층의 계산 그래프

왼쪽 하단의 역전파 최종 출력이 하류 노드로 전파된다. 이 값은 순전파의 입력 x와 출력 y만으로 계산할 수 있다.

따라서 계산 그래프의 중간 과정을 모두 묶어 단순한 'sigmoid' 노드 하나로 대체할 수 있다. 

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

간소화 버전은 역전파 과정의 중간 계산들을 생략할 수 있어 더 효율적인 계산이다.

노드를 그룹화하여 Sigmoid 계층의 세세한 내용을 노출하지 않고 입력과 출력에만 집중할 수 있다. 

또, 다음처럼 정리해서 쓸 수 있다.

 

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

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

 

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())을 사용했다.

(3.3. 다차원 배열의 계산 참고)

 

예를 들어 파이썬으로 다음과 같이 구현했다. 

import numpy as np

# 행렬의 곱 (넘파이에서 np.dot())
X = np.random.rand(2)   # 입력
W = np.random.rand(2,3)   # 가중치
B = np.random.rand(3)   # 편향

X.shape   # (2,)
W.shape   # (2, 3)
B.shape   # (3,)

# 뉴런의 가중치 합
Y = np.dot(X, W) + B

여기서 X, W, B는 다차원 배열이다. 뉴런의 가중치 합은 Y = np.dot(X, W) + B 처럼 계산한다.

이 Y를 활성화 함수로 변환해 다음 층으로 전파하는 것이 신경망 순전파의 흐름이었다. 

더보기

신경망의 순전파 때 수행하는 행렬의 곱은 기하학에서 어파인 변환(affine transformation)이라고 한다.

그래서 어파인 변환을 수행하는 처리를 'Affine 계층'이라는 이름으로 구현한다.

 

앞에서 수행한 계산(행렬의 곱과 편향의 합)을 계산 그래프로 그려보겠다.

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

또한 각 변수의 이름 위에 그 변수의 형상도 표기한다. 

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

X, W, B가 행렬(다차원 배열)이라는 점에 주의한다.

지금까지의 계산 그래프는 노드 사이에 '스칼라값'이 흘렀는데, 이 예에서는 '행렬'이 흐르고 있다.

 

행렬을 사용한 역전파도 행렬의 원소마다 전개해보면 스칼라값을 사용한 지금까지의 계산 그래프와 같은 순서로 생각할 수 있다.

전개해보면 다음 식이 도출된다.

최종적으로 L이라는 값을 출력하는 큰 계산 그래프를 가정할 때, 상류에서 'Y에 대한 L의 미분값'이 흐른다.

 

전치행렬은 W의 (i, j) 위치의 원소를 (j, i) 위치로 바꾼 것을 말한다. 형상이 서로 바뀐다.

수식으로는 다음과 같이 쓸 수 있다.

 

식을 바탕으로 계산 그래프의 역전파를 구해본다.

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

X와 X에 대한 미분은 같은 형상이고, W와 W에 대한 미분도 같은 형상이다. 

 

X와 X에 대한 미분의 형상이 같다는 것은 다음 식을 보면 명확해진다. 

행렬의 곱에서는 대응하는 차원의 원소 수를 일치시켜야 하므로 형상에 주의하자.

 

행렬의 대응하는 차원의 원소 수가 일치하도록 곱을 조립하여 구할 수 있다.

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

 

 

5.6.2. 배치용 Affine 계층

지금까지의 Affine 계층은 입력 데이터로 X 하나만을 고려한 것이었다. 

이번 절에서는 데이터 N개를 묶어 순전파하는 경우, 즉 배치용 Affine 계층을 생각해보겠다.

묶은 데이터를 배치(Batch)라고 부른다.

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

X의 형상이 (N, 2)가 된 것이 다르다. 

형상에 주의하면 역전파 때 역시 X에 대한 미분, W에 대한 미분을 이전과 같이 도출할 수 있다.

 

편향을 더할 때도 주의해야 한다. 

순전파 때의 편향 덧셈은 X·W에 대한 편향이 각 데이터에 더해진다. 

예를 들어 N=2(데이터 2개)로 한 경우, 편향은 그 두 데이터 각각(각각의 계산 결과)에 더해진다.

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

print(X_dot_W)
'''
[[ 0  0  0]
 [10 10 10]]
'''
print(X_dot_W + B)
'''
[[ 1  2  3]
 [11 12 13]]
'''

 

순전파의 편향 덧셈은 각각의 데이터에 더해진다. 

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

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

dB = np.sum(dY, axis=0)
print(dB)
# [5 7 9]

이 예에서는 데이터가 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 계층의 출력은 아래처럼 된다.

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

Softmax 계층은 입력 값을 정규화(출력의 합이 1이 되도록 변형)하여 출력한다.

또한 손글씨 숫자는 가짓수가 10개(10클래스 분류)이므로 Softmax 계층의 입력은 10개가 된다.

더보기

신경망에서 수행하는 작업은 학습추론 두 가지다. 추론할 떄는 일반적으로 Softmax 계층을 사용하지 않는다.

예컨데 위의 신경망 그림에서 추론할 때는 마지막 Affine 계층의 출력을 인식 결과로 이용한다.

또한 신경망에서 정규화하지 않는 출력결과(Softmax 앞의 Affine 계층의 출력)를 점수(Score)라고 한다.

즉, 신경망 추론에서 답을 하나만 내는 경우에는 가장 높은 점수만 알면 되니 Softmax 계층은 필요 없다는 것이다.

반면, 신경망을 학습할 때는 Softmax 계층이 필요하다.  

 

이제 소프트맥스 계층을 구현할 것이다.

손실 함수인 교차 엔트로피 오차도 포함하여 'Softmax-with-Loss 계층'이라는 이름으로 구현한다.

Softmax-with-Loss 계층의 계산 그래프

다소 복잡한데 여기에선 결과만 제시한다.

(도출 과정은 책의 부록 A에 설명되어 있고, 시간이 나면 추가 포스팅을 하겠다.)

 

위의 계산 그래프는 아래처럼 간소화할 수 있다.

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

계산 그래프에서 소프트맥스 함수는 'Softmax' 계층으로, 교차 엔트로피 오차는 'Cross Entropy Error' 계층으로 표기했다.

여기에선 3클래스 분류를 가정하고 이전 계층에서 3개의 입력(점수)를 받는다.

그림과 같이 Softmax 계층은 입력(a1, a2, a3)를 정규화하여 (y1, y2, y3)를 출력한다.

Cross Entropy Error 계층은 Softmax의 출력 (y1, y2, y3)와 정답 레이블 (t1, t2, t3)를 받고, 이 데이터들로부터 손실 L을 출력한다.

 

윗 그림에서 주목할 것은 역전파의 결과다.

Softmax 계층의 역전파(y1-t1, y2-t2, y3-t3)라는 말끔한 결과를 내놓고 있다.

(y1,y2,y3)는 Softmax 계층의 출력이고, (t1, t2, t3)는 정답 레이블이므로 (y1-t1, y2-t2, y3-t3)는 Softmax 계층의 출력과 정답 레이블의 차분인 것이다.

신경망의 역전파에서는 이 차이인 오차가 앞 계층에 전해지는 것이다. 이는 신경망 학습의 중요한 성질이다.

 

그런데 신경망 학습의 목적신경망의 출력(Softmax의 출력)이 정답 레이블과 가까워지도록 가중치 매개변수의 값을 조정하는 것이었다.

그래서 신경망의 출력과 정답 레이블의 오차를 효율적으로 앞 계층에 전달해야 한다.

앞의 (y1-t1, y2-t2, y3-t3)라는 결과는 바로 Softmax 계층의 출력과 정답 레이블의 차이로, 신경망의 현재 출력과 정답 레이블의 오차를 있는 그대로 드러내는 것이다.

더보기

'소프트맥스 함수'의 손실 함수로 '교차 엔트로피 오차'를 사용하니 역전파가 (y1-t1, y2-t2, y3-t3)로 말끔히 떨어진다.

이런 말끔함은 우연이 아니라 교차 엔트로피 오차라는 함수고 그렇게 설계되었기 때문이다.

또 회귀의 출력층에서 사용하는 '항등 함수'의 손실 함수로 '오차제곱합'(3.5. 출력층 설계하기 참고)을 이용하는 이유도 이와 같다.

즉, '항등 함수'의 손실 함수로 '오차제곱합'을 사용하면 역전파의 결과가 (y1-t1, y2-t2, y3-t3)로 말끔히 떨어진다.

 

구체적인 예를 보겠다. 

가령 정답 레이블이 (0, 1, 0)일 때 Softmax 계층이 (0.3, 0.2, 0.5)를 출력했다고 가정한다.

정답 레이블을 보면 정답의 인덱스는 1이다.

그런데 출력에서는 이때의 확률이 겨우 0.2 (20%)라서, 이 시점의 신경망은 제대로 인식하지 못하고 있다.

이 경우 Softmax 계층의 역전파는 (0.3, -0.8, 0.5)라는 커다란 오차를 전파한다.  

굘과적으로 Softmax 계층의 앞 계층들은 그 큰오차로부터 큰 걔달음을 얻게 된다.

 

이번에는 꽤 정확히 인식하고 있는 신경망을 예로 보겠다.

정답 레이블이 똑같이 (0, 1, 0)일 때 Softmax 계층이 (0.01, 0.99, 0)을 출력한 경우다.

이 경우 Softmax 계층의 역전파가 보내는 오차는 비교적 작은 (0.01, -0.01, 0)이다.

이번에는 앞 계층으로 전달된 오차가 작으므로 학습하는 정도도 작아진다.

 

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]
        dx = (self.y - self.t) / batch_size
        
        return dx

여기서는 3.5.2. 소프트맥스 함수 구현 시 주의점4.2.4. (배치용) 교차 엔트로피 오차 구현하기에서 구현한 함수인 softmax()와 cross_entropy_error()를 이용했다.

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

 

 

5.7. 오차역전파법 구현하기

지금까지 구현한 계층을 조합해서 신경망을 구축해보겠다.

 

5.7.1. 신경망 학습의 전체 그림

다음은 신경망 학습의 순서다.

4.5.에서 본 순서다.

전제)

신경망에는 적응 가능한 가중치와 편향이 있고, 이 가중치와 편향을 훈련 데이터에 적응하도록 조정하는 과정을 '학습'이라 한다.

신경망 학습은 4단계로 수행한다.

1단계 - 미니배치)

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

2단계 - 기울기 산출)

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

3단계 - 매개변수 갱신)

가중치 매개변수를 기울기 방향으로 아주 조금 생신한다.

4단계 - 반복)

1~3단계를 반복한다.

오차역전파법이 등장하는 단계는 두 번째인 '기울기 산출'이다.

앞 자에서는 이 기울기를 구하기 위해 수치 미분을 사용했다.

수치 미분은 구현하긴 쉽지만 계산이 오래걸렸다.

오차역전파법을 이용하면 느린 수치 미분과 달리 기울기를 효율적으로 빠르게 구할 수 있다.

 

 

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

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

이 클래스의 인스턴스 변수와 메서드를 정리한 표를 살펴보겠다.

 

<TwoLayerNet 클래스의 인스턴스 변수>

인스턴스 변수 설명
params

딕셔너리 변수로, 신경망의 매개변수를 보관

params['W1']은 1번째 층의 가중치, params['b1']은 1번째 층의 편향

params['W2']은 2번째 층의 가중치, params['b2']은 2번째 층의 편향

layers

순서가 있는 딕셔너리 변수로, 신경망의 계층을 보관

layers['Affine1'], layers['Relu1'], layers['Affine2']와 같이 각 계층을 순서대로 유지 

lastLayers

신경망의 마지막 계층

이 예에서는 SoftmaxWithLoss 계층

 

<TwoLayerNet 클래스의 메서드>

메서드 설명
__init__(self, input_size, hidden_size, output_size, weight_init_std)

초기화를 수행한다.

인수는 앞에서부터 입력층의 뉴런 수, 은닉층의 뉴런 수, 출력층의 뉴런 수, 가중치 초기화 시 정규분포의 스케일

predict(self, x)

예측(추론)을 수행한다.

인수 x는 이미지 데이터

loss(self, x, t)

손실 함수의 값을 구한다.

인수 x는 이미지 데이터, t는 정답 레이블

accuracy(self, x, t) 정확도를 구한다.
numerical_gradient(self, x, t) 가중치 매개변수의 기울기를 수치 미분 방식으로 구한다(앞 장과 같음).
gradient(self, x, t) 가중치 매개변수의 기울기를 오차역전파법으로 구한다.

 

이 클래스의 구현은 좀 길지만 4.5. 학습 알고리즘 구현하기와 공통되는 부분이 많다.

계층을 사용한다는 점이 크게 다르다.

계층을 사용함으로써 인식 결과를 얻는 처리(predict())와 기울기를 구하는 처리(gradient()) 계층의 전파만으로 동작이 이루어진다. 

(깃허브https://github.com/WegraLee/deep-learning-from-scratch 에서 ch05/two_layer_net.py파일.)

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

__init__ 함수계층 생성 부분, predict() 함수 부분, gradient 함수 순전파 부분과 역전파 부분을 집중해서 본다.

특히 신경망의 계층을 OrderedDict에 보관하는 점이 중요하다. 

OrderedDict은 순서가 있는 딕셔너리이므로, 딕셔너리에 추가한 순서를 기억한다.

그래서 순전파 때는 추가한 순서대로 각 계층의 forward() 메서드를 호출하기만 하면 처리가 완료된다.

마찬가지로 역전파 때는 계층을 반대 순서로 호출하기만 하면 된다.

Affine 계층과 ReLU 계층이 각자의 내부에서 순전파와 역전파를 제대로 처리하고 있다. 

따라서 여기에서는 계층을 올바른 순서로 연결한 다음 순서대로(혹은 역순으로) 호출해주면 끝이다.

 

이처럼 신경망의 구성 요소를 '계층'으로 구현한 덕분에 신경망을 쉽게 구축할 수 있다.

계층으로 모듈화해서 구현하면 깊은 신경망을 만들 때 필요한 만큼 계층을 추가하면 되어 단순해진다.

 

 

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

<기울기를 구하는 방법>에는 두 가지가 있었다.

  1. 수치 미분을 써서 구하는 방법
  2. 해석적으로 수식을 풀어 구하는 방법

후자인 해석적 방법은 오차역전파법을 이용하여 매개변수가 많아도 효율적으로 계산할 수 있었다.

수치미분은 느리지만 구현하기 쉽고, 오차역전파법을 정확히 구현했는지 확인하기 위해 필요하다.

이처럼 두 방식으로 구한 기울기가 일치함을 확인하는 작업을 기울기 확인(gradient check)이라고 한다.

 

코드는 다음과 같다.

앞에서 사용한 TwoLayerNet 클래스를 사용한다.

(깃허브https://github.com/WegraLee/deep-learning-from-scratch 에서 ch05/gradient_check.py파일.)

import sys, os
sys.path.append(os.pardir)  # 부모 디렉터리의 파일을 가져올 수 있도록 설정
import numpy as np
from dataset.mnist import load_mnist

# 데이터 읽기
(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))
    

'''
실행 결과
W1:3.9756560738559423e-10
b1:2.249611513083401e-09
W2:5.064956754067138e-09
b2:1.402397101085695e-07
'''

가장 먼저 MNIST 데이터셋을 읽는다.

그리고 훈련 데이터 일부를 수치 미분으로 구한 기울기오차역전파법으로 구한 기울기오차를 확인한다.

여기에선 각 가중치 매개변수의 차이의 절댓값을 구하고, 이를 평균한 값이 오차가 된다.

실행 결과에 의하면 수치 미분과 오차역전파법으로 구한 기울기의 차이가 매우 작다는 걸 알 수 있다.

가력 1번째 층의 편향 오차는 9.7e-13(0.00000000000097)이다. 

이로써 오차역전파법으로 구한 기울기도 실수 없는 구현이 된다.

 

 

5.7.4. 오차역전파법을 사용한 학습 구현하기

마지막으로 오차역전파법을 사용한 신경망 학습을 구현해보겠다.

기울기를 오차역전파법으로 구한다는 점만 다르다.

여기서도 앞에서 사용한 TwoLayerNet 클래스를 사용한다.

(깃허브https://github.com/WegraLee/deep-learning-from-scratch 에서 ch05/train_neuralnet.py파일.)

import sys, os
sys.path.append(os.pardir)
import numpy as np
from dataset.mnist import load_mnist

# 데이터 읽기
(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장에서는 계산 과정을 시각적으로 보여주는 계산 그래프를 알아봤다.

계산 그래프를 이용하여 신경망의 동작과 오차역전파법을 설명하고, 그 처리 과정을 계층 단위로 구현했다.

ReLU 계층, Softmax-with-Loss 계층, Affine 계층, Softmax 계층 등이었다.

모든 계층에서 forward와 backward라는 메서드를 구현한다.

동작을 계층으로 모듈화하고 조합하여 원하는 신경망을 쉽게 만들 수 있었다.

 


<참고 문헌>

사이토 고키(2019), 『밑바닥부터 시작하는 딥러닝』, 한빛미디어, pp.165-187.

'Study > 책『밑바닥부터 시작하는 딥러닝』' 카테고리의 다른 글

6-2. 학습 관련 기술들  (0) 2020.05.12
6-1. 학습 관련 기술들  (0) 2020.05.11
5-1. 오차역전파법  (0) 2020.03.31
4-2. 신경망 학습  (0) 2020.03.25
4-1. 신경망 학습  (0) 2020.03.16

댓글