프로필사진

IT Anthology/encyclopedia

[밑러닝] 신경망에서의 최적화(optimization), 옵티마이저(optimizer) 정복하기

다각 2020. 4. 13. 18:54

* 이 글은 <밑바닥부터 시작하는 딥러닝2 (저자: 사이토 고키)> 책을 읽으며 정리한 글입니다.
* 나중에라도 제가 참고하기 위해 정리해 두었으며, 모든 내용을 적은 것이 아닌,
필요하다고 생각되는 부분만 추려서 정리한 것임을 미리 밝힙니다.


목차

  1. 확률적 경사 하강법(SGD, Stochastic Gradient Descent)
  2. 모멘텀(Momentum)
  3. AdaGrad(Adaptive Gradient)
  4. Adam

신경망 학습의 목적은 손실 함수의 값을 낮추는 매개변수를 찾는 데에 있었다. 매개변수의 최적값을 찾아가는 방법은 여러가지가 있을 수가 있는데, 이렇게 최적값을 빠르게 찾아가는 문제를 푸는 것을 '최적화(optimization)'라고 한다.

 

우리는 앞선 활동을 통해 매개변수의 기울기를 구하고, 기울어진 방향으로 매개변수 값을 갱신하는 일을 반복하며 최적의 값에 다가가는 방식을 취했다. 이것을 확률적 경사 하강법(SGD)이라고 하는데, 이 이외에도 최적값을 찾아가는 방법은 많다. 이번 포스팅에서는 그 방법들에 대해 논해보기로 하자.

 


1. 확률적 경사 하강법(SGD, Stochastic Gradient Descent)

SGD는 기울어진 방향으로 일정 거리를 이동한다. 매개변수 갱신을 나타내는 식은 다음과 같다.
$$
W \gets W - \eta \frac {\partial L}{\partial W}
$$
이것을 파이썬으로 구현하면 다음과 같다.

weight[i] += -lr * gradient

다량의 정보를 한꺼번에 처리할 수 있도록 코드를 재구성하면 다음과 같다. (아래에서 paramsgrads는 딕셔너리 객체이다)

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

SGD는 직관적이지만, 비효율적일 때가 있다. SGD의 단점을 극명하게 보여줄 수 있는 다음과 같은 함수를 생각해보자.
$$
f(x, y) = \frac {1}{20}x^2+y^2
$$
이를 그래프로 그려보면 다음과 같다.

위의 함수에 대해 기울기 벡터를 그려보면 다음과 같다.

함수 $f(x, y)$에서 최솟값이 되는 장소는 $(x, y) = (0, 0)$이다. 하지만 위의 기울기 벡터는 $(0, 0)$ 방향을 명시적으로 가리키지는 못하고 있다. 실제로 SGD를 이용하여 최솟값을 찾아내는 과정을 시각화해보면 다음과 같다.

까만 선은 $f(x,y)$의 등고선을 표현한 것이고, 빨간 점은 갱신되며 최솟값을 찾아나가는 경로를 나타낸 것이다. 위의 그래프는 50번 갱신했는데, 그 이하로는 최솟값인 $(0,0)$에 제대로 다가가지 못한다. 또한 최솟값을 향해 똑바로 나아가지 못하고 지그재그를 그리며 방황한다. 이는 매우 비효율적인 탐색 방법이다. 그렇다면 최솟값을 찾아갈 수 있는, 더 효율적인 다른 방식은 뭐가 있을까?

 

2. 모멘텀(Momentum)

모멘텀은 SGD와 비슷한 경사하강법이지만 가던 방향 그대로 조금 더 등을 떠밀어주는 방식이다. 식은 다음과 같다.
$$
v \gets \alpha v - \eta \frac {\partial L}{\partial W},\ \text{where}\ \alpha\ \text{is momentum} \\[3mm]
W \gets W + v
$$
보통 $\alpha$는 0.9로 설정한다. '가던 방향 그대로 조금 더 등을 떠민다'를 파악하기 위해 예시를 들어보자. 제일 처음의 기울기($\frac {\partial L}{\partial W}$가 7이고 두번째 기울기가 -5인 상태에서 $\eta$는 0.1이라고 해보자.

첫번째 v는 (0.9)(0)-(0.1)(7) = -0.7이며, 두번째 v는 (0.9)(-0.7)-(0.1)(-5) = -0.13이다. 이처럼 첫번째와 두번째의 기울기 방향(+, -)이 다르더라도, 이전의 이동 값을 일정 비율 반영시키므로 관성의 효과를 낼 수 있다. 그럼 이렇게 관성의 효과를 내는 게 뭐가 좋을까? 그래프는 우리의 생각만큼 매끈하지 않고, 울퉁불퉁하기 때문이다. 작은 요철에 민감하게 반응해서 튀어나가면 제대로 길을 찾아나설 수 없다. 즉, 조금 더 큰 경향을 찾을 수 있도록 해주는 것이다.

모멘텀을 이용해서 경로를 그려보면, SGD처럼 경사하강법을 이용하긴 하지만 SGD만큼 경박하게 방황하지는 않는다.

모멘텀의 파이썬 구현은 다음과 같다.

v = momentum*v - lr * gradient
weight[i] += v

확장성 있게 코드를 재구성하면 다음과 같다.

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]

 

3. AdaGrad(Adaptive Gradient)

한편, 학습률(learning rate) 자체를 조정하는 방법도 있다. 학습률이 너무 작으면 학습 시간이 너무 길어지고, 너무 크면 발산하여 학습이 제대로 이뤄지지 않는다. '적당한' 학습률이 중요한데, 이 '적당한'을 어떻게 정하냐는 것이다. 그래서 AdaGrad 방법은 많이 변화하는 변수는 학습률을 작게 하고 꼼짝 않는 변수는 학습률을 크게 한다. AdaGrad는 특히 word2vec이나 GloVe에서 유용하다. (출처: Beomsu Kim's Blog) 수식은 다음과 같다.
$$
h \gets h + \frac {\partial L}{\partial W} \odot \frac {\partial L}{\partial W} \\[3mm]
W \gets W - \eta \frac {1}{\sqrt{h}} \frac {\partial L}{\partial W}
$$
$\odot$이 행렬의 원소별 곱셈을 의미할 때, h는 바로 전 단계의 기울기 값을 제곱해서 더해주는 것이라고 이해할 수 있다.
AdaGrad를 파이썬으로 구현하면 다음과 같다.

h += gradient**2
weight[i] -= lr * ( gradient / np.sqrt(h) )

확장성 있게 구현하면 다음과 같다.

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 * grad[key] / (np.squrd(self.h[key]) + 1e-7)

마지막에 1e-7이라는 아주 작은 값을 더했는데 0으로 나눠지는 상황을 방지하기 위해서이다.
AdaGrad를 사용해서 최적화 문제를 풀어보면 다음과 같다.

처음에는 성큼성큼 가지만 그에 비례해서 갱신되는 정도도 작아지고, 나중에는 촘촘하게 최솟값을 찾아나서는 것을 확인할 수 있다.

 

4. Adam

모멘텀은 관성을, AdaGrad는 학습률 조정을 이용했다. 이 두가지를 융합한 것이 Adam이다. (물론 이것은 직관적인 설명이며, 자세한 설명은 논문을 참고하자)
Adam으로 최적화해보면 다음과 같다.

 


 

정리
지금까지 네 개의 옵티마이저에 대해 알아봤다. SGD는 단순한 경사하강법이며, 모멘텀은 너무 작은 변동에 학습의 방향이 휙휙 꺾이지 않도록 관성을 붙여주는 옵티마이저였다. 또한 AdaGrad는 학습률 자체를 건드리며 Adam은 모멘텀과 AdaGrad의 방식을 혼합했다.

 

지금까지 배운 옵티마이저들의 갱신 경로는 다음과 같다. 비교를 위해 전부 30번 갱신을 시도했다.

옵티마이저의 성능은 풀고자하는 문제의 성격에 따라, (학습률같은) 하이퍼파라미터에 따라 많이 달라진다. 따라서 위의 그림만 보고 무엇이 좋은 옵티마이저인지 성급하게 결론내리면 안 된다.