딥러닝독학! (3) 학습 관련 기술들 - 밑바닥부터시작하는딥러닝

 

 

 

 

시간이 급한 관계로 책의 내용들을 다 보지는 못하지만, 이렇게라도 필요한 부분들을 공부하니 확실히 전보다는 이해도가 높아진 것 같다. 그래도 아직 배워야할게 너무많다. 밑바닥부터시작하는 딥러닝 책이 1,2권 있는데 1권에선 총 8개의 챕터가 있고 이제 두 챕터(2,3)를 끝냈다. 이제 6챕터를 보려고 하고 뒤에 7,8챕터를 빨리 끝내서 2권으로 넘어가려한다.

2권에서는 앞부분은 건너뛰고 3챕터(word2vec)부터 시작할 것이다. 

 

이번장에서 다룰 내용
최적화 방법, 가중치 매개변수 초기값, 하이퍼파라미터 설정 방법, 오버피팅의 대응책

 

1. 매개변수 갱신

신경망학습의 목적은 손실함수의 값을 최소화 하는 매개변수(최적 매개변수)를 찾는 것이다. 

but 신경망 최적화는 굉장히 어렵다 (매개변수 고간이 매우 넓고 복잡하기 때문)

매개변수의 기울기를 구해, 기울어진 방향으로 매개변수 값을 갱신하는 일을 반복하면 최적의 매개변수로 다가갈 수 있고, 이를 확률적 경사 하강법 (SGD)라 한다.  단순하지만, SGD보다 똑똑한 방법도 있다. 

 

1.1 확률적 경사하강법 (SGD)

$\mathbf{W}$ 갱신할 가중치
${\partial L} \over {\partial \mathbf{W}}$ W에대한 손실함수의 기울기
$\eta$ 학습률 (미리 정해서 사용)
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*grad[key]

 

optimizer는 '최적화를 행하는 자'라는 뜻의 단어이며 SGD가 그 역할을 할 수 있다. optimzer는 매개변수 갱신을 책임지고 수행하기 때문에 매개변수와 기울기 정보만 넘겨주면 된다.

 

1.2 SGD의 단점

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

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

그래프와 그 등고선

이 함수의 기울기를 그려보면 다음과 같다.

위 식이 최소가 되는 장소는 (x,y) = (0,0)이지만, 그림을 보면 기울기 대부분은 (0,0) 방향을 가리키지 않는다. 그러면 이 함수에 SGD를 적용하면 어떻게 될까?

최솟값에 다다를 때까지 지그재그로 이동하여 비효율적인 움직임을 갖는다. 이 원인은 기울어진 방향이 최솟값과 다른 방향을 갖기 때문이다. 

 

1.3 모멘텀

모멘텀은 '운동량'을 뜻하는 단어로 다음과 같이 쓸 수 있다.

SGD와 비슷한데 $\mathbf{v}$라는 변수가 새로 나온다. 이는 물리에서 말하는 속도에 해당함.

class Momentum:

    """모멘텀 SGD"""

    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]

즉, 기울기 방향으로 힘을 받아 물체가 가속된다는 물리 법칙을 나타낸다.

$\boldsymbol{\alpha} \mathbf{v}$항은 물체가 아무런 힘을 받지 않을때 서서히 하강시키는 역할을 한다. 

(0.9등의 값으로 설정)

이 모멘텀을 이용해서 최적화 문제를 풀어보면 결과는 다음과 같다.

SGD와 비교하면 지그재그 정도가 덜하다. 전체적으로는 SGD보다 x축방향으로 빠르게 다가갈 수 있어서 지그재그 움직임을 줄일 수 있다.

 

1.4 AdaGrad

신경망 학습에선 학습률값이 중요하다. 너무 작으면 학습시간이 길어지고, 너무 크면 발산한다.

이 학습률을 정하는 효과적 기술로 학습률 감소가 있는데, 학습을 진행하면서 학습률을 점차 줄여가는 방법이다.

AdaGrad는 '각각의' 매개변수에 '맞춤형' 값을 만들어준다.

여기서는 h라는 변수가 새로 등장한다. h는 기존 기울기 값을 제곱하여 계속 더해준다. ($\bigodot$는 행렬의 원소별 곱셈)

그리고 매개변수를 갱신할 때  $1 \over \sqrt{h}$ 을 곱해 학습률을 조정한다. (학습률이 낮아짐)

class AdaGrad:

    """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라는 작은 값을 더해주는데 이는 0을 방지하기 위함이다. 

AdaGrad를 사용해서 최적화문제를 풀어보면 결과는 다음과 같다.

최솟값을 향해 효율적으로 움직인다. y축방향은 처음에는 크게 움직이지만, 갱신 정도도 큰 폭으로 작아지도록 조정된다. 따라서 y축방향으로 갱신 강도가 약해지고 지그재그 움직이 줄어든것.

 

1.5 Adam

공이 구르는 듯한 모멘텀과, 매개변수의 원소바다 갱신 정도를 조정했던 AdaGrad, 이 둘을 융합하면? Adam이다!

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

class Adam:

    """Adam (http://arxiv.org/abs/1412.6980v8)"""

    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)

이를 이용하여 최적화문제를 풀어보면 다음과 같다.

패턴은 모멘텀과 비슷하나, 좌우 흔들림이 적다. 학습의 갱신강도를 적응적으로 조정했기 때문이다.

 

 

1.6 어떤 방법을 이용할까?

사용한 기법에 따라 갱신경로가 다르다. 위의 예제에선 AdaGrad가 가장 나아 보이지만, 문제에 따라 달라진다.

또, 하이퍼파라미터를 어떻게 설정햐느냐도 결과를 좌우한다.

지금 많은 연구에서는 SGD를 사용하고 있다. 요즘엔 Adam도 많이 사용된다. 

데청캠 교수님 피셜 : 잘 모르겠으면 Adam을 써라!

 

2. 가중치의  초깃값

신경망 학습에선 가중치의 초깃값이 특히 중요하다. 초깃값이 학습의 성패를 가르는 일도 자주 있다.

초깃값을 0으로 하면 모든 가중치의 값이 똑같이 갱신되기 때문에 가중치를 여러개 갖는 의미를 사라지게 한다. 

 

2.1 은닉층의 활성화값 분포

가중치의 초깃값에 따라 은닉층 활성화값이 바뀌게 된다.

활성화값들이 0과 1에 치우쳐 분포될 경우 기울기 소실이 발생할 수 있고,

특정 값에 치우치게 될 경우엔 표현력을 제한한다는 관점에서 문제가 생긴다.뉴런 100개가 거의 같은 값을 출력한다면 1개짜리와 다를 게 없다는 뜻이다.

각층의 활성화 값은 적당히 분포되어야 한다. Xavier 초깃값은 가중치 초기값으로 권장되며 일반적인 딥러닝 프레임워크들이 표준적으로 이용하고 있다. 앞 계층의 노드가 $n$개라면 가중치초기값의 표준편차가 $1 \over {\sqrt n}$가 된다. 

 

가중치의 초깃값으로 'Xavier 초깃값을 이용할 때의 각 층의 활성화값 분포는 다음과 같다. 층이 5개가 있으며, 각 층의 뉴런은 100개씩이다. 입력 데이터로서 1000개의 데이터를 정규분포로 무작위 생성하여 5층 신경망에 흘린다.

층이 깊어지면서 형태가 다소 일그러지지만 넓게 분포됨을 볼 수 있다. 오른쪽으로 갈수록 일그러지는 현상은 sigmoid 함수 대 신 tanh함수를 이용하면 개선된다.활성화 함수용으로는 원점에서 대칭인 함수가 바람직하다고 알려져 있다.

 

2.2 RELU를 사용할 때 가중치 초깃값

Xavier 초깃값은 활성화 함수가 선형임을 전제로 한다. 반면 RELU를 이용할 때는 이에 특화된 초깃값을 이용하기를 권장하는데 이 특화된 초깃값을 He초깃값이라 한다.

 

He초깃값은 앞 계층의 노드가 $n$개 일때, 표준편차가 $\sqrt{2\over n}$인 정규분포를 사용한다. 

 

3. 바른 학습을 위해

3.1 오버피팅

오버피팅이란 신경망이 훈련 데이터에만 지나치게 정으되어 그 외 데이터에는 제대로 대응하지 못하는 사태를 말한다.

주로 다음의 두 경우에서 일어난다.

  1. 매개변수가 많고 표현력이 높은 모델 (데이터양을 늘려주면 해결)
  2. 훈련데이터가 적음

 

3.2 가중치 감소

오버피팅 억제용으로 예로부터 만이 이용해온 방법 중 가중치 감소가 있다.

학습 과정에 서 큰 가중치에 대해서는 그에 상응하는 큰 페널티를 부과하여 오버피팅을 억제하는 방법이다. (원래 오버피팅이 가중치 매개변수의 값이 커서 발생하는 경우가 많다)

 

신경망 학습의 목적은 손실함수의 값을 줄이는 것이때, 가중치의 제곱 법칙(L2법칙) 을 손실 함수에 더한다면 가중치가 커지는 것을 억제할 수 있다. 

가중치 $\mathbf{W}$는 ${1\over2} \lambda \mathbf{W}^2$이 되고 이를 손실함수에 더해준다. 여기에서 $\lambda$(람다)는 정규화의 세기를 조절하는 하이퍼파라미터이다. 

 

이전에는 오버피팅되었던 실험에 대해 $\lambda$=0.1로 가중치 감소를 적용했을 때, 훈련/실험 정확도의 차이가 줄었고 훈련 데이터에 대한 정확도도 100%에 도달하지 못했다. (도달하면 오버피팅)

 

3.3 드롭아웃

신경망 모델이 복잡해지면 가중치 감소만로는 대응하기 어려워진다. 이럴 때는 흔히 드롭아웃 기법을 이용한다.

드롭아웃은 뉴런을 임의로 삭제하면서 학습하는 방법이다. 훈련 때 은닉층의 뉴런을 무작위로 골라 삭제한다.

class Dropout:
  def __init__(self, dropout_ratio = 0.5):
    self.dropout_ratio = dropout_ratio
    self.mask = None

  def forward(self, x, train_fig = True): #train_fig : 훈련상태인가?
    if train_fig:
      self.mask = np.random.rand(*x.shape) > self.dropout_ratio
      return x*self.mask
    else:
      return x*(1.0-self.dropout_ratio)

  def backward(self,dout):
    return dout*self.mask

순전파 때마다 self.mask에 삭제할 뉴런을 False로 표시한다

self.mask는 x와 형상이 같은 배열을 무작위로 생성하고, 그 값이 dropout_ratio보다 큰 원소만 True로 설정한다.

 

드롭아웃을 이용하면 표현력을 높이면서도 오버피팅을 억제할 수 있다. 드롭아웃이 학습 때 뉴런을 무작위로 삭제하는 행위를 매번 다른 모듈을 학습시키는 것으로 해석할 수 있는 '앙상블 학습' 기법도 있다. 개별적으로 학습시킨 여러 모델의 출력을 평균 내어 추론하는 방식이다. 

 

3.4 적절한 하이퍼 파라미터 찾기

앞으로 하이퍼파라미터를 다양한 값으로 설정하고 검증할 텐데, 여기서 주의할 점은 하이퍼파라미터의 성능을 평가할 때는 시험 데이터를 사용해선 안 된다는 것이다.  하이퍼파라미터값이 시험데이터에 오버피팅되기 때문이다. 하이퍼파라미터를 조정할 때는 하이퍼파라미터 전용 확인 데이터가 필요하고 이를 일반적으로 검증데이터라고 부른다. 하이퍼파라미터의 적절성을 평가한다.

훈련 데이터 매개변수 학습
검증 데이터 하이퍼파라미터 성능 평가
시험 데이터 신경망의 범용 성능 평가

 

3.5 하이퍼파라미터 최적화

핵심은 하이퍼파라미터의 '최적 값'이 존재하는 범위를 조금씩 줄여나가는 것!

그러기 위해서는 대략적인 범위 설정 ▷ 범위내에서 무작위로 샘플링 ▷ 그 값으로 정확도 평가 하는 작업을 반복한다.

# 하이퍼파라미터 무작위 추출 코드
weight_decay = 10**np.random.uniform(-8, -4) #로그스케일 값으로 랜덤초기화
lr = 10**np.random.uniform(-6,-2)

범위도 '대략적으로 지정'하는 것이 효과적이다. 

 

4. 정리

매개변수 갱신 방법 : SGD , 모멘텀, Adam, AdaGrad

가중치 초깃값을 정하는 방법은 올바른 학습을 위해 중요!

선형적이다? Xavier, Relu에는? He

오버피팅 억제 : 가중치감소, 드롭아웃 (+앙상블)

하이퍼파라미터 => 범위를 점차 좁히면서 최적값 찾기