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

6-1. 학습 관련 기술들

by 코드포휴먼 2020. 5. 11.

6. 학습 관련 기술들

6장에서는 신경망 학습의 핵심 개념을 다룬다.

가중치 매개변수의 최적값을 탐색하는 최적화 방법, 가중치 매개변수 초깃값, 하이퍼파라미터 설정 방법 등이다. 

오버피팅 대응책인 가중치 감소와 드롭아웃 등의 정규화 방법과 배치 정규화도 짧게 알아본다.

 

6.1. 매개변수 갱신

신경망 학습의 목적은 손실 함수의 값을 가능한 한 낮추는 매개변수를 찾는 것이다.

이는 곧 매개변수의 최적값을 찾는 문제이며, 이 문제를 푸는 것을 최적화(optimization)라 한다.

신경망 최적화는 굉장히 어려운 문제다.

매개변수 공간은 매우 넓고 복잡해서 최적의 솔루션은 쉽게 찾을 수 없다. 

심층 신경망에서는 매개변수의 수가 엄청나게 많아져서 더 어렵다.

 

지금까지 최적의 매개변수 값을 찾는 단서로 매개변수의 기울기(미분)를 이용했다.

매개변수의 기울기를 구해서, 기울어진 방향으로 매개변수 값을 갱신하는 일을 반복하여 최적값에 다가갔다.

이것이 확률적 경사 하강법(SGD)이란 단순한 방법인데, SGD의 단점과 다른 최적화 기법을 알아보겠다.

 

 

6.1.1. 모험가 이야기

최적화를 해야하는 상황을 모험가 이야기에 비유해본다.

색다른 모험가가 있다. 광활한 메마른 산맥을 여행하면서 날마다 깊은 골짜기를 찾아 발걸음을 옮긴다. 그는 전설에 나오는 세상에서 가장 깊고 낮은 골짜기, '깊은 곳'을 찾아가려 한다. 그것이 그의 여행 목적이다. 게다가 그는 엄격한 '제약' 2개로 자신을 옭아맸다. 하나는 지도를 보지 않을 것, 또 하나는 눈가리개를 쓰는 것이다. 지도도 없고 보이지도 않으니 가장 낮은 골짜기가 광대한 땅 어디에 있는지 알 도리가 없다. 그런 혹독한 조건에서 이 모험가는 어떻게 '깊은 곳'을 찾을 수 있을까? 어떻게 걸음을 옮겨야 효율적으로 '깊은 곳'을 찾아낼 수 있을까?

 

최적의 매개변수를 탐색하기 위해 중요한 단서가 되는 것이 땅의 '기울기'다.

모험가는 주위 경치는 볼 수 없지만 지금 서 있는 땅의 기울기는 알 수 있다. 

그래서 지금 서 있는 장소에서 가장 크게 기울어진 방향으로 가자는 것이 SGD 전략이다.

 

 

6.1.2. 확률적 경사하강법(SGD)

SGD를 복습한다. 수식으로는 다음과 같이 쓸 수 있다.

식에서 보듯 SGD는 기울어진 방향으로 일정 거리만 가겠다는 단순한 방법이다.

 

SGD를 파이썬 클래스로 구현해본다.

class SGD:
    def __init__(self, lr=0.01):
        self.lr = lr
        
    def update(self, params, grads):
        for key in params.keys():
            params[key] -= self.lr * grads[key]

 

초기화 때 받는 인수인 lr은 learning rate(학습률)를 뜻한다.

이 학습률을 인스턴스 변수로 유지한다. 

update(params, grads) 메서드는 SGD 과정에서 반복해서 불린다. 

인수인 params와 grads는 지금까지의 신경망 구현처럼 딕셔너리 변수다.

params['W1'], grads['W1'] 등과 같이 각각 가중치 매개변수와 기울기를 저장하고 있다.

 

SGD 클래스를 사용하면 신경망 매개변수의 진행을 아래처럼 수행할 수 있다(실제론 동작 안하는 코드).

network = TwoLayerNet(...)
optimizer = SGD()

for i in range(10000):
    ...
    x_batch, t_batch = get_mini_batch(...)   # 미니배치
    grads = network.gradient(x_batch, t_batch)
    params = network.params
    optimizer.update(params, grads)
    ...

optimizer는 '최적화를 행하는 자'라는 뜻으로, 여기선 SGD가 그 역할을 한다.

매개변수 갱신은 optimizer가 책임지고 수행하므로 optimizer에 매개변수와 기울기 정보만 넘기면 된다.

이처럼 최적화를 담당하는 클래스를 분리해서 구현하면 기능을 모듈화하기 좋다. 

 

 

6.1.3. SGD의 단점

SGD는 단순하고 구현하기 쉽지만, 문제에 따라서는 비효율적일 때가 있다.

다음 함수의 최솟값을 구하는 문제를 생각해보겠다.

이 함수는 아래의 그림 왼쪽처럼 '밥그릇'을 x축 방향으로 늘인 듯한 모습이고, 실제 그 등고선은 오른쪽과 같이 x축 방향으로 늘인 타원으로 되어있다.

 

함수의 기울기를 그려보면 다음처럼 된다. 

이 기울기는 y축 방향은 크고 x축 방향은 작다는 것이 특징이다. 

y축 방향은 가파른데 x축 방향은 완만하다. 

주의할 점은 함수 식에서 최솟값이 되는 장소는 (x, y) = (0, 0)이지만, [그림 6-2]에서 보여주는 기울기 대부분은 (0, 0) 방향을 가리키지 않는다는 것이다.

 

이제 [그림 6-1]의 함수에 SGD를 적용해본다.

탐색을 시작하는 장소 (초깃값)는 (x, y) = (-7.0, 2.0)으로 하겠다.

결과는 아래처럼 된다.

그림 6-3) SGD에 의한 최적화 갱신 경로 : 최솟값인 (0,0)까지 지그재그로 이동하니 비효율적이다.

SGD는 [그림 6-3]처럼 심하게 굽이진 움직임을 보여준다. 상당히 비효율적이다.

즉, SGD의 단점은 비등방성(anisotropy) 함수(방향에 따라 성질, 즉, 여기에서는 기울기가 달라지는 함수)에서는 탐색 경로가 비효율적이다.

이럴 때는 SGD 같이 무작정 기울어진 방향으로 진행하는 단순한 방식보다 더 영리한 방법이 필요하다.

또한 SGD가 지그재그로 탐색하는 근본 원인은 기울어진 방향이 본래의 최솟값과 다른 방향을 가리켜서라는 점도 생각해봐야 한다.

 

이제부터 SGD의 단점을 개선해주는 모멘텀, AdaGrad, Adam 세 방법을 소개한다.

 

 

6.1.4. 모멘텀

모멘텀(Momentum)은 '운동량'을 뜻하는 단어로, 물리와 관계 있다.

수식은 다음과 같이 쓸 수 있다.

첫 번째 식은 기울기 방향으로 힘을 받아 물체가 가속된다는 물리 법칙을 나타낸다.

 

모멘텀은 아래 그림처럼 공이 그릇의 바닥을 구르는 듯한 움직임을 보여준다.

[식 6.3]의 αv 항은 물체가 아무런 힘을 받지 않을 때 서서히 하강시키는 역할을 한다.

물리에서의 지면 마찰이나 공기 저항에 해당한다.

(α는 0.9 등의 값으로 설정한다.)

 

다음은 모멘텀의 구현이다.

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

import numpy as np

class Momentum:
    def __init__(self, lr=0.01, momentum=0.9):
        self.lr = lr
        self.momentum = momentum
        self.v = None
        
    def update(self, params, grads):
        if self.v is None:
            self.v = {}
            for key, val in params.items():                                
                self.v[key] = np.zeros_like(val)
                
        for key in params.keys():
            self.v[key] = self.momentum*self.v[key] - self.lr*grads[key] 
            params[key] += self.v[key]

인스턴스 변수 v가 물체의 속도다.

v는 초기화 때는 아무 값도 담지 않고, 대신 update()가 처음 호출될 때 매개변수와 같은 구조의 데이터를 딕셔너리 변수로 저장한다. 

나머지 부분은 [식 6.3]과 [식 6.4]를 간단히 코드로 옮긴 것이다. 

 

이제 모멘텀을 사용해서 [식 6.2]의 최적화 문제를 풀어본다. 결과는 아래처럼 된다.

모멘텀에 의한 최적화 갱신 경로

모멘텀의 갱신 경로는 공이 그릇 바닥을 구르듯 움직인다. SGD와 비교하면 지그재그 정도가 덜하다. 

이는 x축의 힘은 아주 작지만 방향은 변하지 않아서 한 방향으로 일정하게 가속하기 때문이다.

거꾸로 y축의 힘은 크지만 위아래로 번갈아 받아서 상충하여 y축 방향의 속도는 안정적이지 않다. 

전체적으로는 SGD보다 x축 방향으로 빠르게 다가가 지그재그 움직임이 줄어든다.

 

 

6.1.5. AdaGrad

신경망 학습에서는 학습률(𝜂) 값이 중요하다.

너무 작으면 학습 시간이 너무 길어지고, 너무 크면 발산하여 학습이 제대로 이뤄지지 않는다.

이 학습률을 정하는 효과적 기술로 학습률 감소(learning rate decay)가 있다.

이는 학습을 진행하면서 학습률을 점차 줄여가는 방법이다. 

 

학습률을 서서히 낮추는 가장 간단한 방법은 매개변수 '전체'의 학습률 값을 일괄적으로 낮추는 것이다.

이를 더욱 발전시킨 것이 AdaGrad이다. AdaGrad는 '각각의' 매개변수에 '맞춤형' 값을 만들어준다.

AdaGrad는 개별 매개변수에 적응적으로(adaptive) 학습률을 조정하면서 학습을 진행한다.

갱신 방법은 수식으로 다음과 같다.

매개변수의 원소 중에서 많이 움직인(크게 갱신된) 원소는 학습률이 낮아진다는 뜻인데, 다시 말해 학습률 감소가 매개변수의 원소마다 다르게 적용됨을 뜻한다.

더보기

AdaGrad는 과거의 기울기를 제곱하여 계속 더해간다. 그래서 학습을 진행할수록 갱신 강도가 약해진다. 실제로 무한히 계속 학습한다면 어느 순간 갱신량이 0이 되어 전혀 갱신되지 않게 된다. 이 문제를 개선한 기법으로서 RMSProp이라는 방법이 있다. RMSProp은 과거의 모든 기울기를 균일하게 더해가는 것이 아니라, 먼 과거의 기울기는 서서히 잊고 새로운 기울기 정보를 크게 반영한다. 이를 지수이동평균(Exponential Moving Average, EMA)이라 하여, 과거 기울기의 반영 규모를 기하급수적으로 감소시킨다.

 

AdaGrad의 구현을 보겠다.

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

class AdaGrad:
    def __init__(self, lr=0.01):
        self.lr = lr
        self.h = None
        
    def update(self, params, grads):
        if self.h is None:
            self.h = {}
            for key, val in params.items():
                self.h[key] = np.zeros_like(val)
            
        for key in params.keys():
            self.h[key] += grads[key] * grads[key]
            params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)

마지막 1e-7이라는 작은 값을 더하는 부분에 주의한다.

이 작은 값은 self.h[key]에 0이 담겨있다 해도 0으로 나누는 사태를 막아준다.

대부분의 딥러닝 프레임워크에서는 이 값도 인수로 설정할 수 있다.

 

AdaGrad를 사용하여 [식 6.2]의 최적화문제를 풀어보겠다. 결과는 다음과 같다.

AdaGrad에 의한 최적화 갱신 경로

최솟값을 향해 효율적으로 움직이는 것을 알 수 있다.

y축 방향은 기울기가 커서 처음에는 크게 움직이지만, 그 큰 움직임에 비례해 갱신 정도도 큰 폭으로 작아지도록 조정된다.

그래서 y축 방향으로 갱신 강도가 빠르게 약해지고, 지그재그 움직임이 줄어든다.

 

 

6.1.6. Adam

모멘텀과 AdaGrad 두 기법을 융합한 기법이 Adam이다. 

두 방법의 이점을 조합했다면 매개변수 공간을 효율적으로 탐색해줄 것으로 기대해도 좋다.

또, 하이퍼파라미터의 '편향 보정'이 진행된다는 점도 Adam의 특징이다. 

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

class Adam:
    def __init__(self, lr=0.001, beta1=0.9, beta2=0.999):
        self.lr = lr
        self.beta1 = beta1
        self.beta2 = beta2
        self.iter = 0
        self.m = None
        self.v = None
        
    def update(self, params, grads):
        if self.m is None:
            self.m, self.v = {}, {}
            for key, val in params.items():
                self.m[key] = np.zeros_like(val)
                self.v[key] = np.zeros_like(val)
        
        self.iter += 1
        lr_t  = self.lr * np.sqrt(1.0 - self.beta2**self.iter) / (1.0 - self.beta1**self.iter)         
        
        for key in params.keys():
            #self.m[key] = self.beta1*self.m[key] + (1-self.beta1)*grads[key]
            #self.v[key] = self.beta2*self.v[key] + (1-self.beta2)*(grads[key]**2)
            self.m[key] += (1 - self.beta1) * (grads[key] - self.m[key])
            self.v[key] += (1 - self.beta2) * (grads[key]**2 - self.v[key])
            
            params[key] -= lr_t * self.m[key] / (np.sqrt(self.v[key]) + 1e-7)
            
            #unbias_m += (1 - self.beta1) * (grads[key] - self.m[key]) # correct bias
            #unbisa_b += (1 - self.beta2) * (grads[key]*grads[key] - self.v[key]) # correct bias
            #params[key] += self.lr * unbias_m / (np.sqrt(unbisa_b) + 1e-7)

여기에서 코드는 참고용으로 깊게 파진 않겠다.

 

Adam으로 최적화 문제를 푼 결과는 아래와 같다.

Adam에 의한 최적화 갱신 경로

Adam 갱신 과정도 그릇 바닥을 구르듯 움직인다.

모멘텀과 비슷한 패턴인데, 모멘텀 때보다 공의 좌우 흔들림이 적다.

이는 학습의 갱신 강도를 적응적으로 조정해서 얻는 혜택이다.

 

 

6.1.7. 어느 갱신 방법을 이용할 것인가?

4개의 매개변수 갱신 방법을 보았다.

이번 절에서는 네 기법의 결과를 비교해보겠다.

(깃허브https://github.com/WegraLee/deep-learning-from-scratch 에서 ch06/optimizer_compare_naive.py파일 결과.)

최적화 기법 비교 : SGD, 모멘텀, AdaGrad, Adam

사용한 기법에 따라 갱신 경로가 다르다. 

AdaGrad가 가장 나아보이지만 풀어야 할 문제가 무엇이냐에 따라 달라진다. 

학습률 등의 하이퍼파라미터를 어떻게 설정하느냐에 따라서도 결과가 바뀐다. 

기법마다 장단이 있지만 SGD가 많은 연구에서 사용되고 있고 Adam도 많이 사용된다.

 

 

6.1.8. MNIST 데이터셋으로 본 갱신 방법 비교

손글자 숫자 인식을 대상으로 네 기법을 비교해본다.

각 방법의 학습 진도가 얼마나 다른지를 그릴 수 있다.

(깃허브https://github.com/WegraLee/deep-learning-from-scratch 에서 ch06/optimizer_compare_mnist.py파일 결과.)

MNIST 데이터셋에 대한 학습 진도 비교

이 실험은 각 층이 100개의 뉴런으로 구선된 5층 신경망에서 ReLU를 활성화 함수로 사용해 측정했다.

결과를 보면 SGD의 학습 진도가 가장 느리다. 

 

 

6.2. 가중치의 초깃값

가중치 초깃값 설정은 신경망 학습의 성패를 가른다. 권장 초깃값에 대해 설명한다.

 

6.2.1. 초깃값을 0으로 하면?

오버피팅을 억제해 범용 성능을 높이는 테크닉인 가중치 감소(weight decay) 기법을 소개한다.

가중치 감소는 간단히 말해 가중치 매개변수의 값이 작아지도록 학습하는 방법이다.

가중치 값을 작게 하여 오버피팅이 일어나지 않게 하는 것이다.

 

가중치를 작게 만들고 싶으면 초깃값도 최대한 작은 값에서 시작하는 것이 정공법이다.

지금까지 가중치의 초깃값은 0.01*np.random.randn(10,100)처럼 정규분포에서 생성되는 값을 0.01배 한 작은 값(표준편차가 0.01인 정규분포)을 사용했다. 

 

초깃값을 모두 0으로 하면 (정확히는 가중치를 균일한 값으로 설정하면) 안 된다. 

오차역전파법에서 모든 가중치 값이 똑같이 갱신되기 때문이다. 

예를 들어 2층 신경망에서 첫 번째와 두 번째 층의 가중치가 0이라고 가정하겠다.

그럼 순전파 때는 입력층의 가중치가 0이기 때문에 두 번째 층의 뉴런에 모두 같은 값이 전달된다.

두 번째 층의 모든 뉴런에 같은 값이 입력된다는 것은 역전파 때 두 번째 층의 가중치가 모두 똑같이 갱신된다는 것이다.

그래서 가중치들은 같은 초깃값에서 시작하고 갱신을 거쳐도 여전히 같은 값을 유지하는 것이다.

이는 가중치를 여러 개 갖는 의미를 사라지게 한다.

이를 막으려면 초깃값을 무작위로 설정해야 한다.

 

 

6.2.2. 은닉층의 활성화값 분포

은닉층의 활성화값(활성화 함수의 출력 데이터)의 분포를 관찰하면 중요한 정보를 얻을 수 있다.

가중치의 초깃값에 따라 은닉층 활성화값들이 어떻게 변화하는지 실험해보겠다.

구체적으로는 활성화 함수로 시그모이드 함수를 사용하는 5층 신경망에 무작위로 생성한 입력 데이터를 흘리며 각 층의 활성화값 분포를 히스토그램으로 그려본다.

 

실험을 위한 전체 소스 코드는 깃허브에 있고, 본문에서는 필요한 부분만 가져와서 이해한다.

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

import numpy as np
import matplotlib.pyplot as plt

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

x = np.random.randn(1000, 100)  # 1000개의 데이터
node_num = 100  # 각 은닉층의 노드(뉴런) 수
hidden_layer_size = 5  # 은닉층이 5개
activations = {}  # 이곳에 활성화 결과를 저장

for i in range(hidden_layer_size):
    if i != 0:
        x = activations[i-1]

    w = np.random.randn(node_num, node_num) * 1
    a = np.dot(x, w)
    z = sigmoid(a)
    activations[i] = z

층이 5개가 있으며, 각 층의 뉴런은 100개씩이다.

입력 데이터로서 1000개의 데이터를 정규분포로 무작위로 생성하여 이 5층 신경망에 흘린다.

활성화 함수로는 시그모이드 함수를 이용했고, 각 층의 활성화 결과를 activations 변수에 저장한다. 이 코드에서는 가중치의 분포에 주의한다.

이번에는 표준편차가 1인 정규분포를 이용했는데, 이 분포된 정도(표준편차)를 바꿔가며 활성화값들의 분포가 어떻게 변화하는지 관찰하는 것이 이 실험의 목적이다.

 

그럼 activations에 저장된 각 층의 활성화값 데이터를 히스토그램으로 그려본다.

for i, a in activations.items():
    plt.subplot(1, len(activations), i+1)
    plt.title(str(i+1) + "-layer")
    if i != 0: 
        plt.yticks([], [])
    plt.hist(a.flatten(), 30, range=(0,1))
plt.show()

가중치를 표준편차가 1인 정규분포로 초기화할 때의 각 층의 활성화값 분포

각 층의 활성화값들이 0과 1에 분포한다

여기에서 사용한 시그모이드 함수는 그 출력이 0에 가까워지자(또는 1에 가까워지자) 그 미분은 0에 다가간다.

그래서 데이터가 0과 1에 치우쳐 분포하게 되면 역전파의 기울기 값이 점점 작아지다가 사라진다.

이것이 기울기 소실 문제(gradient vanishing)다.

층을 깊에 하는 딥러닝에서는 기울기 소실이 더 심각한 문제가 될 수 있다.

 

이번에는 가중치의 표준편차를 0.01로 바꿔 같은 실험을 반복해본다.

앞의 코드에서 가중치 초깃값 설정 부분을 다음과 같이 바꾸면 된다.

# w = np.random.randn(node_num, node_num) * 1
w = np.random.randn(node_num, node_num) * 0.01

 

표준편차를 0.01로 한 정규분포의 경우 각 층의 활성화값 분포는 아래처럼 된다.

가중치를 표준편차가 0.01인 정규분포로 초기화할 때의 각 층의 활성화값 분포

이번에는 0.5 부근에 집중되었다. 앞의 예처럼 0과 1로 치우치진 않았으니 기울기 소실문제는 없다.

그러나 활성화값들이 치우쳤다는 것은 표현력 관점에서는 큰 문제가 있다.

이 상황에서는 다수의 뉴런이 거의 같은 값을 출력하고 있으니 뉴런이 여러 개인 의미가 없어진다.

그래서 활성화값들이 치우치면 표현력을 제한한다는 관점에서 문제가 된다.

 

이어서 사비에르 글로로트(Xavier Glorot)와 요수아 벤지오(Yoshua Bengio)의 논문에서 군장하는 가중치 초깃값인, 일명 Xavier 초깃값을 써보겠다.

현재 Xavier 초깃값은 일반적인 딥러닝 프레임워크들이 표준적으로 이용하고 있다.

 

예를 들어 카페(Caffe) 프레임워크는 가중치 초깃값을 설정할 때 인수로 xavier를 지정할 수 있다.

이 논문은 각 층의 활성화값들을 광범위하게 분포시킬 목적으로 가중치의 적절한 분포를 찾고자 했다.

그리고 이끌어낸 결론은 다음과 같다.

Xavier 초깃값 : 초깃값의 표준편차는  1/n의 제곱근  이 되도록 설정(n은 앞 층의 노드 수)

 

Xavier 초깃값을 사용하면 앞 층에 노드가 많을수록 대상 노드의 초깃값으로 설정하는 가중치가 좁게 퍼진다.

이제 Xavier 초깃값을 써서 실험해본다.

코드에서는 가중치 소깃값 설정 부분을 다음과 같이 고쳐주기만 하면 된다.

(모든 층의 노드 수가 100개라고 단순화)

시비에르의 논문은 앞 층의 입력 노드 수 외에 다음 층의 출력 노드 수도 고려한 설정값을 제안한다. 다만 카페 등의 프레임워크는 앞 층의 입력 노드만으로 계산하도록 단순화했다.

node_num = 100   # 앞 층의 노드 수

# w = np.random.randn(node_num, node_num) * 0.01
w = np.random.randn(node_num, node_num) * np.sqrt(1.0 / node_num)

가중치의 초깃값으로 'Xavier 초깃값'을 이용할 떄의 각 층의 활성화값 분포

층이 깊어지면서 형태가 다소 일그러지지만 앞에서 본 방식보다는 훨씬 넓게 분포된다.

각 층에 흐르는 데이터는 적당히 퍼져 있으므로, 시그모이드 함수의 표현력도 제한받지 않고 학습이 효율적으로 이뤄질 것으로 기대된다.

 

 

6.2.3. ReLU를 사용할 때의 가중치 초깃값

Xavier 초깃값은 활성화 함수가 선형인 것을 전제로 이끈 결과다.

sigmoid 함수와 tanh 함수는 좌우 대칭이라 중앙 부근이 선형인 함수로 볼 수 있다. 그래서 Xavier 초깃값이 적당하다.

반면 ReLU를 이용할 땐 ReLU에 특화된 초깃값을 이용하라고 권장한다.

이 특화된 초깃값을 찾아낸 카이밍 히(Kaiming He)의 이름을 따 He 초깃값이라 한다.

ReLU는 음의 영역이 0이라서 더 넓게 분포시키기 위해 2배의 계수가 필요하다고 해석 가능하다.

 

활성화 함수로 ReLU를 이용한 경우의 활성화값 분포를 보겠다.

표준편차가 0.01인 정규분포(std=0.01), Xavier 초깃값, ReLU 전용 He 초깃값일 때의 실험 결과를 차례로 보여준다.

활성화 함수로 ReLU를 사용한 경우의 가중치 초깃값에 따른 활성화값 분포 변화

  1. std=0.01일 때의 각 층의 활성화값들은 아주 작은 값들이다.
    (각 층의 활성화값 분포의 평균은 1층부터 차례로 0.0396, 0.00290, 1.32e-5, 9.46e-7이다.)
    신경망에 아주 작은 데이터가 흐른다는 것은 역전파 때 가중치의 기울기 역시 작아진다는 뜻이다.
    이는 중대한 문제이며, 실제로도 학습이 거의 이뤄지지 않을 것이다.
  2. Xavier 초깃값 결과를 보면 이쪽은 층이 깊어지면서 치우침이 조금씩 커진다.
    실제로 층이 깊어지면 활성화값들의 치우침도 커지고, 학습할 때 '기울기 소실'을 일으킨다.
  3. He 초깃값은 모든 층에서 균일하게 분포됐다.
    층이 깊어져도 분포가 균일하게 유지되므로 역전파 때도 적절한 값이 나올 것이다.

 

실험 결과를 바탕으로, 현재의 모범 사례는 다음과 같다.

활성화 함수 가중치 초깃값
ReLU He 초깃값
S자 모양 곡선 (sigmoid, tanh 등) Xavier 초깃값

 

 

6.2.4. MNIST 데이터셋으로 본 가중치 초깃값 비교

실제 데이터를 갖고 가중치의 초깃값 설정이 신경망 학습에 얼마나 영향을 주느니 보겠다.

세 경우(std=0.01, Xavier 초깃값, He 초깃값) 모두 실험해본다.

(깃허브https://github.com/WegraLee/deep-learning-from-scratch 에서 ch06/weight_init_compare.py파일의 실행결과)

MNIST 데이터셋으로 살펴본 '가중치 초깃값'에 따른 비교

이 실험은 층별 뉴런 수가 100개인 5층 신경망에서 활성화 함수로 ReLU를 사용했다. 

  1. std=0.01일 때는 학습이 전혀 이뤄지지 않는다.
    앞의 활성화값의 분포에서 본 것처럼 순전파 때 너무 작은 값(0 근처로 밀집한 데이터)이 흐르기 때문이다.
    그로 인해 역전파 때의 기울기도 작아져 가중치가 거의 갱신되지 않는다.
  2. Xavier 초깃값은 학습이 순조롭게 이뤄진다.
  3. He 초깃값도 학습이 순조롭게 이뤄진다. 학습 진도가 더 빠르다.

이처럼 가중치의 초깃값에 따라 신경망 학습의 성패가 갈리는 경우가 많다. 

 

 

6.3. 배치 정규화

앞 절에서는 각 층의 활성화값 분포를 보며, 가중치 초깃값을 적절히 설정하면 각 층의 활성화값 분포가 적당히 퍼지면서 학습이 원활하게 수행됨을 배웠다.

각 층이 활성화를 적당히 퍼뜨리도록 강제해보면 어떨까. 배치 정규화(Batch Normalization)가 그런 방식이다.

 

 

6.3.1. 배치 정규화 알고리즘

배치 정규화는 2015년에 제안된 기법이지만 많은 연구자와 기술자가 즐겨 사용한다.

배치 정규화가 주목받는 이유는 다음과 같다.

  • 학습 빨리 진행할 수 있다(학습 속도 개선).
  • 초깃값에 크게 의존하지 않는다(골치 아픈 초깃값 선택 부담을 줄인다).
  • 오버피팅 억제한다(드롭아웃 등의 필요성 감소).

배치 정규화의 기본 아이디어는 각 층에서의 활성화값이 적당히 분포되도록 조정하는 것이다.

그래서 아래처럼 데이터 분표를 정규화하는 '배치 정규화(Batch Norm) 계층'을 신경망에 삽입한다.

배치 정규화를 사용한 신경망의 예

 

배치 정규화는 이름처럼 학습 시 미니배치를 단위로 정규화한다.

구체적으로는 데이터 분포가 평균 0, 분산 1이 되도록 정규화한다. 수식은 다음과 같다.

입력데이터인 미니배치 B에 대해 평균과 분산을 구한다.

그리고 입력 데이터를 평균 0, 분산 1이 되게(적절한 분포가 되게) 정규화한다.

ε 기호(epsilon, 엡실론)은 작은 값(ex. 10e-7)으로서, 0으로 나누는 사태를 예방하는 역할이다.

마지막 수식의 변환 처리를 활성화 함수의 앞(혹은 뒤)에 삽입함으로써 데이터 분포가 덜 치우치게 할 수 있다.

 

또, 배치 정규화 계층마다 이 정규화된 데이터에 고유한 확대(scale)와 이동(shift) 변환을 수행한다.

수식으로는 다음과 같다.

확대 값과 이동 값은 학습하면서 적합한 값으로 조정해간다.

 

배치 정규화 알고리즘은 신경망에서 순전파 때 적용된다.

이를 계산 그래프로는 다음처럼 그릴 수 있다.

배치 정규화의 계산 그래프

배치 정규화의 역전파 유도는 다소 복잡하여 설명은 생략한다.

 

 

6.3.2. 배치 정규화의 효과

배치 정규화 계층을 사용한 실험을 해본다.

MNIST 데이터셋을 사용하여 배치 정규화 계층을 사용할 때와 사용하지 않을 때의 학습 진도 변화를 보겠다.

(깃허브https://github.com/WegraLee/deep-learning-from-scratch 에서 ch06/batch_norm_test.py파일의 실행결과)

배치 정규화의 효과 : 배치 정규화가 학습 속도를 높인다.

배치 정규화가 학습을 빨리 진전시키고 있다.

 

계속해서 초깃값 분포를 다양하게 줘가며 학습 진행이 어떻게 달라지는지 보겠다.

다음은 가중치 초깃값의 표준편차를 다양하게 바꿔가며 학습 경과를 관찰한 그래프다.

실선이 배치 정규화를 사용한 경우, 점선이 사용하지 않은 경우 : 가중치 초깃값의 표준편차는 각 그래프 위에 표기

거의 모든 경우에서 배치 정규화를 사용할 떄의 학습 진도가 빠른 것으로 나타난다.

실제로 배치 정규화를 이용하지 않는 경우엔 초깃값이 잘 분포되어 있지 않으면 학습이 전혀 진행되지 않는다.

 

지금까지 본 것처럼 배치 정규화를 사용하면 학습이 빨라지며, 가중치 초깃값에 크게 의존하지 않아도 된다.

 

 

6.4. 바른 학습을 위해 부터는 다음 포스팅에 이어 작성한다.


<참고 문헌>

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

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

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

댓글