본문 바로가기
Computer Science/Deep Learning

[비전공자용] [Python] 모멘텀, AdaGrad, Adam 최적화기법

by 롱일스 2020. 7. 9.
반응형

 

이번 포스트에서는 모멘텀, AdaGrd, Adam 최적화 기법에 대해 상세히 알아볼 겁니다.

 

 

1. 모멘텀 Momentum

모멘텀은 운동량을 뜻하는 단어로, 신경망에서의 모멘텀 기법은 아래 수식과 같이 표현할 수 있습니다.

모멘텀의 속도 갱신 수식
모멘텀 가중치 갱신 수식

SGD에서와 마찬가지로
W
는 갱신할 가중치 매개변수, 
L은 손실함수를 나타내고 
η 
는 학습률 learning rate,
∂L/∂W은 에 대한 손실함수의 기울기를 나타냅니다. 

SGD와 달리 변수 v가 등장하는데 물리에서 운동량을 나타내는 식은 p = mv, 질량 m, 속도 v이므로
위 수식에서도 v는 속도를 의미합니다.
매개변수 α를 v에 곱해서 αv 항은 물체가 아무 힘도 받지 않을 때도 서서히 하강시키는 역할을 하게 됩니다. 물리에서의 마찰력이라고 생각하면 편할 것 같습니다. 하강시키기 위해 α를 0.9등의 값으로 설정합니다.

모멘텀을 파이썬 코드로 구현하면 다음과 같습니다.

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):
	# update()가 처음 호출될 때 v에 매개변수와 같은 구조의 데이터를 딕셔너리 변수로 저장
        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]

이전에 살펴보았던 SGD와 최적화 성능차이를 알아보기 위해 SGD 포스트에서 다룬 문제에 적용해본다. 문제는 아래 링크에서 확인할 수 있다.

2020/07/08 - [Computer Science/Deep Learning] - 확률적 경사 하강법 SGD 의 단점

 

확률적 경사 하강법 SGD 의 단점

SGD란? 이전 포스트에서는 최적의 매개변수 값을 찾는 단서로 매개변수의 기울기(미분)을 이용했습니다. 이 방법을 확률적 경사 하강법, 즉 Stochastic Gradient Decent (SGD) 라고 합니다. SGD의 수식은 아�

huangdi.tistory.com

 모멘텀을 이용해서 최적화를 했을 시 최적해 (0, 0)을 찾는 경로는 다음과 같다.

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

SGD와 비교했을 때 더 효율적인 경로로 최적해를 찾는 것을 확인할 수 있다. 이 변화의 원인은 x 축 방향으로 일정하게 가속되고 방향의 변화가 아주 작게 일어나기 때문이다.
 

2. AdaGrad

다음으로 AdaGrad 최적화 기법을 살펴보겠다.
이 기법은 각각의 매개변수에 적응적으로 Adaptive 학습률 Learning rate을 조정하며 학습을 진행한다.

신경망 학습에서는 학습률(η ) 값이 굉장히 중요하다.
값이 너무 작으면 학습 시간이 너무 길어지고, 반대로 너무 크면 발산하여 학습이 제대로 이뤄지지 않는다.

학습률에 따른 최적화 과정 비교, (좌)학습률이 큰 경우, (우)학습률이 작은 경우

현재 위치 (파란점)에서 (0, 0)인 최솟점(주황점)으로 최적화할 때 학습률이 너무 큰 경우와 작은 경우 어떤 특징이 발생하는 지 알아보겠다. 

왼쪽 그래프의 경우에는 학습률이 커서 매개변수가 기울기 방향대로 크게 변해서 3번만에 최솟점 근처에 도달하지만,
너무 크게 변해서 최솟점을 지나치게 된다. 따라서 다시 최솟점으로 가기 위해 그래프의 왼쪽 부분에서 다시 기울기 방향으로 변화하지만 변화율이 커서 역시나 최솟점에 도달하지 못하고 근처에만 도달 가능하다.

오른쪽 그래프의 경우에는 학습률이 작아 7번만에 최솟점에 도달할 수 있지만 왼쪽 그래프의 경우에 비해 2배정도 시간이 오래 걸린다.

학습률의 이런 특성 때문에 보통 처음에는 학습률이 크게 잡다가 학습률을 점차 줄여서 학습한다.

여기서 학습률을 서서히 낮추는 가장 간단한 방법은 매개변수 '전체'의 학습률 값을 일괄적으로 낮추는 것이다. 이를 더욱 발전시킨 것이 바로 AdaGrad이다. AdaGrad는 개별 매개변수에 맞춤형 값을 만들어준다.

AdaGrad의 갱신 방법을 수식으로 나타내면 다음과 같다.

변수 h 갱신 수식
AdaGrad 가중치 갱신 수식

⨀ 기호는 행렬의 원소별 곱셈을 의미합니다.
h는 기존 기울기 값을 제곱하여 계속 더해줍니다.
매개변수 W를 갱신할 때는 1/√h 를 곱해서 학습률을 조정합니다.

이렇게 매개변수 갱신 수식을 정하면 매개변수의 원소 중에서 크게 갱신된(기울기 변화가 큰) 변수는, 즉 ||∂L/∂W || 가 큰 변수는 h 값이 크게 증가하고 η x 1/√h가 감소하면서 학습률이 감소한다고 볼 수 있습니다.
따라서 학습률 감소가 매개변수의 원소마다 다르게 적용된다는 것을 의미합니다.

AdaGrad를 파이썬 코드로 구현하면 다음과 같습니다.

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)
            # 0으로 나누는 일이 없도록 1e-7을 더해줍니다. 이 값은 임의로 지정 가능합니다.

AdaGrad를 이용해서 위에서 푼 최적화 문제를 풀어보면 다음과 같은 결과를 보입니다.

AdaGrad에 의한 최적화 갱신 경로

y축 방향으로 처음 기울기가 큰 탓에 갱신 강도가 빨랐다가 점점 약해지면서 작은 움직임으로 최솟값에 도달하는 것을 확인할 수 있습니다.

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

RMSProp을 파이썬 코드로 구현하면 다음과 같습니다.

class RMSprop:

    def __init__(self, lr=0.01, decay_rate = 0.99):
        self.lr = lr
        self.decay_rate = decay_rate
        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] *= self.decay_rate
            self.h[key] += (1 - self.decay_rate) * grads[key] * grads[key]
            params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)

 

3. Adam

모멘텀과 AdaGrad 두 기법의 이점을 융합하면 어떨까? 하는 생각에서 시작한 기법이 Adam 입니다. 모멘텀의 공이 그릇 바닥을 구르는 듯한 움직임을 보이는 점과 AdaGrad에서 매개변수의 원소마다 적응적으로 갱신 정도를 조정하는 특성을 융합한 기법입니다. 즉, 매개변수 공간을 효율적으로 탐색해주며, 하이퍼파라미터의 '편향 보정'이 진행된다는 점이 Adam의 특징입니다.

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)

Adam의 이론은 생각보다 복잡하여 여기까지 설명하고 동일한 최적화 문제를 풀어보면 다음과 같은 결과를 보이는 걸 확인할 수 있습니다.

Adam에 의한 최적화 갱신 경로

모멘텀과 비슷하게 바닥을 구르듯 움직이지만 모멘텀 방법과 비교해서 공의 좌우 흔들림이 적은 것을 확인할 수 있습니다. 학습의 갱신 강도를 적응적으로 조정해서 얻었기 때문입니다.

Adam은 하이퍼파라미터를 3개 설정합니다. 하나는 지금까지의 학습률 α, 나머지 두 개는 일차 모멘텀용 계수β1과 이차 모멘텀용 계수 β2 입니다. 논문에 따르면 기본 설정값은 β1 은 0.9, β2 는 0.999이며, 이 값이면 많은 경우에 좋은 결과를 얻을 수 잇습니다.

 

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

지금까지 살펴본 최적화 기법 4가지의 성능을 비교하기 위해 아래 파이썬 코드를 통해 매개변수 갱신 경로를 살펴보려 합니다.

# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 부모 디렉터리의 파일을 가져올 수 있도록 설정
import numpy as np
import matplotlib.pyplot as plt
from collections import OrderedDict
from common.optimizer import *


def f(x, y):
    return x**2 / 20.0 + y**2


def df(x, y):
    return x / 10.0, 2.0*y

init_pos = (-7.0, 2.0)
params = {}
params['x'], params['y'] = init_pos[0], init_pos[1]
grads = {}
grads['x'], grads['y'] = 0, 0


optimizers = OrderedDict()
optimizers["SGD"] = SGD(lr=0.95)
optimizers["Momentum"] = Momentum(lr=0.1)
optimizers["AdaGrad"] = AdaGrad(lr=1.5)
optimizers["Adam"] = Adam(lr=0.3)

idx = 1

for key in optimizers:
    optimizer = optimizers[key]
    x_history = []
    y_history = []
    params['x'], params['y'] = init_pos[0], init_pos[1]
    
    for i in range(30):
        x_history.append(params['x'])
        y_history.append(params['y'])
        
        grads['x'], grads['y'] = df(params['x'], params['y'])
        optimizer.update(params, grads)
    

    x = np.arange(-10, 10, 0.01)
    y = np.arange(-5, 5, 0.01)
    
    X, Y = np.meshgrid(x, y) 
    Z = f(X, Y)
    
    # 외곽선 단순화
    mask = Z > 7
    Z[mask] = 0
    
    # 그래프 그리기
    plt.subplot(2, 2, idx)
    idx += 1
    plt.plot(x_history, y_history, '-', color="red")
    plt.contour(X, Y, Z)
    plt.ylim(-10, 10)
    plt.xlim(-10, 10)
    plt.plot(0, 0, '+')
    #colorbar()
    #spring()
    plt.title(key)
    plt.xlabel("x")
    plt.ylabel("y")
    
plt.show()

 그래프를 그려보면 다음과 같은 결과를 얻을 수 있습니다.

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

위의 그림만 봐서는 AdaGrad가 가장 성능이 좋은 것 같지만 풀어야 할 문제에 따라, 또 학습률 등의 하이퍼파라미터를 어떻게 설정하느냐에 따라서도 결과가 바뀝니다.

모든 문제에서 항상 뛰어난 기법이라는 것은 아직 없고, 각각의 장단점이 있습니다. 

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

손글씨 숫자 인식을 대상으로 지금까지 설명한 네 기법을 비교해봅시다. 각 방법의 학습 진도가 얼마나 다른지 아래 파이썬 코드를 이용해서 그래프를 그려보겠습니다.

# coding: utf-8
import os
import sys
sys.path.append(os.pardir)  # 부모 디렉터리의 파일을 가져올 수 있도록 설정
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
from common.util import smooth_curve
from common.multi_layer_net import MultiLayerNet
from common.optimizer import *


# 0. MNIST 데이터 읽기==========
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)

train_size = x_train.shape[0]
batch_size = 128
max_iterations = 2000


# 1. 실험용 설정==========
optimizers = {}
optimizers['SGD'] = SGD()
optimizers['Momentum'] = Momentum()
optimizers['AdaGrad'] = AdaGrad()
optimizers['Adam'] = Adam()
#optimizers['RMSprop'] = RMSprop()

networks = {}
train_loss = {}
for key in optimizers.keys():
    networks[key] = MultiLayerNet(
        input_size=784, hidden_size_list=[100, 100, 100, 100],
        output_size=10)
    train_loss[key] = []    


# 2. 훈련 시작==========
for i in range(max_iterations):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]
    
    for key in optimizers.keys():
        grads = networks[key].gradient(x_batch, t_batch)
        optimizers[key].update(networks[key].params, grads)
    
        loss = networks[key].loss(x_batch, t_batch)
        train_loss[key].append(loss)
    
    if i % 100 == 0:
        print( "===========" + "iteration:" + str(i) + "===========")
        for key in optimizers.keys():
            loss = networks[key].loss(x_batch, t_batch)
            print(key + ":" + str(loss))


# 3. 그래프 그리기==========
markers = {"SGD": "o", "Momentum": "x", "AdaGrad": "s", "Adam": "D"}
x = np.arange(max_iterations)
for key in optimizers.keys():
    plt.plot(x, smooth_curve(train_loss[key]), marker=markers[key], markevery=100, label=key)
plt.xlabel("iterations")
plt.ylabel("loss")
plt.ylim(0, 1)
plt.legend()
plt.show()

 결과를 보면 다음과 같습니다.

===========iteration:0===========
SGD:2.447420847905626
Momentum:2.3197923968359344
AdaGrad:2.0980615141436965
Adam:2.2011322971158362
===========iteration:100===========
SGD:1.3249325881288563
Momentum:0.38946898206638736
AdaGrad:0.2105794727475605
Adam:0.28237109181213815
===========iteration:200===========
SGD:0.6580867094015548
Momentum:0.3557810802072136
AdaGrad:0.15096724968324698
Adam:0.23724710331215215
===========iteration:300===========
SGD:0.393982358957206
Momentum:0.15894346203267087
AdaGrad:0.07672423091566927
Adam:0.14317231671017733
===========iteration:400===========
SGD:0.5161563727192546
Momentum:0.3393333735017402
AdaGrad:0.13194060018061252
Adam:0.195137919222847
===========iteration:500===========
SGD:0.3833750623656824
Momentum:0.16306564391837475
AdaGrad:0.08884928013676202
Adam:0.11343385638827881
===========iteration:600===========
SGD:0.34176790509089827
Momentum:0.09719061411602228
AdaGrad:0.03906751744512798
Adam:0.04082942764442893
===========iteration:700===========
SGD:0.29840091311406414
Momentum:0.1419262783935838
AdaGrad:0.10012770425571171
Adam:0.08018232438479811
===========iteration:800===========
SGD:0.3244132517077296
Momentum:0.11684795212548157
AdaGrad:0.08067072705504441
Adam:0.08433595402408414
===========iteration:900===========
SGD:0.28593377412424187
Momentum:0.11912490943652476
AdaGrad:0.10515927887804662
Adam:0.1499946122602371
===========iteration:1000===========
SGD:0.26094097548277095
Momentum:0.059588405854194004
AdaGrad:0.02703930622353266
Adam:0.03918501797500247
===========iteration:1100===========
SGD:0.16422475213418547
Momentum:0.05885625843114389
AdaGrad:0.029154422733466436
Adam:0.027261535189829645
===========iteration:1200===========
SGD:0.2340737740714617
Momentum:0.1117301709762567
AdaGrad:0.07874474027630193
Adam:0.11834940430910366
===========iteration:1300===========
SGD:0.17978674562241445
Momentum:0.07148249975448391
AdaGrad:0.030458989604083823
Adam:0.019399144707531183
===========iteration:1400===========
SGD:0.2884505482343188
Momentum:0.07872948299169524
AdaGrad:0.06716379943375238
Adam:0.05835984357235065
===========iteration:1500===========
SGD:0.23673094466019728
Momentum:0.11732340303686863
AdaGrad:0.08570528824598067
Adam:0.09272510588029333
===========iteration:1600===========
SGD:0.17263068522797426
Momentum:0.06486991586792401
AdaGrad:0.027882826604388237
Adam:0.02001170947080948
===========iteration:1700===========
SGD:0.218500459866564
Momentum:0.04437062868846875
AdaGrad:0.02137316195854536
Adam:0.025025909656406362
===========iteration:1800===========
SGD:0.14737692201280622
Momentum:0.027846833487301808
AdaGrad:0.017847226586023213
Adam:0.02176945038898675
===========iteration:1900===========
SGD:0.2601712754500385
Momentum:0.07246107350395825
AdaGrad:0.05628025930437791
Adam:0.040544298532501355

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

위 결과를 보면, SGD의 학습 진도가 가장 느리고 나머지 세 기법의 진도는 비슷하지만 AdaGrad가 조금 더 빠르다는 걸 알 수 있습니다. 다시 한 번 강조하지만 하이퍼파라미터인 학습률과 신경망의 구조에 따라 결과가 달라질 수 있다는 것입니다. 다만 일반적으로 SGD보다 다른 세 기법이 빠르게 학습하고, 때로는 최종 정확도도 높게 나타납니다.

 

[출처] Deep Learning from Scratch, ゼロ から作る

728x90
반응형