본문 바로가기
파이썬 프로그래밍/Numpy 딥러닝

21. 경사하강법의 개선 - Adam

by Majestyblue 2022. 2. 5.

저번 시간에 단순경사하강법을 개선한 Momentum과 RMSprop를 알아보았다. 

Momentum은 기울기를 변화시키는 방법으로 지역 최소점에 빠지지 않게 도와주었고 RMSprop는 학습률을 변화시키는 방법으로 진동을 줄여 빠른 시간내에 수렴할 수 있도록 도와주었다.

 

이 둘을 합치면 더 좋은 성능을 낼 수 있지 않을까? 

 

Adam은 Momentum과 RMSprop를 합친 최적화 방법이다. 즉 Momentum의 직진성과 RMSprop의 감쇠가 합쳐진 것으로 생각해 볼 수 있다. 수식을 보자

 

 

 

Adam 방법은 위의 수식에서 볼 수 있듯이 Momentum과 RMSprop를 합친 경사하강법이다. 

 

 

그런데 이 식을 그대로 사용하다간 작은 문제가 생긴다.

바로 초기값 v = 0, s = 0이 문제다. 초기값에 따라 지수가중이동평균이 큰 영향을 받기 때문이다. 처음에 지수가중이동평균을 몇개 더하지 않을 때 0의 항이 큰 영향을 미치고 가중치 업데이트가 제대로 일어나지 않을 수 있다. 따라서 초기값 0 항목 영향력을 줄여야 한다. (reference : https://stats.stackexchange.com/questions/232741/why-is-it-important-to-include-a-bias-correction-term-for-the-adam-optimizer-for)

 

초기값이 0의 영향력을 줄이기 위해 지수가중이동평균의 '가중평균'으로 구해준다. (이해가 안간다면 지수가중이동평균(Exponentially Weighted Moving Average)-1 포스트를 읽고 오길 바란다.) 이렇게 하면 초기값 0의 영향력을 줄이면서도 지수가중이동평균을 계속 구해줄 때 초기값 0의 영향력은 감쇠되어 매우 작아진다.

 

가중평균 식

 

 

지수가중이동평균의 가중평균을 구해보자.

먼저 지수가중이동평균을 구해본다. 여기서 g는 기울기를 나타낸다.

 

 

 

가중평균 식에 따라 분자는 전체 항을 더하고, 분모는 가중치만을 더한다. 그렇다면 식은 아래와 같다.

 

 

 

 

 

분모항 중 시그마 베타 값은 첫항이 β^(t-1)이고 끝항이 1이며 공비가 β^(-1)인 등비수열의 합이다. 일반적인 등비수열 합 공식으로 계산하면 틀리게 될 가능성도 높다(내가 그랬거든). 등비수열 합 공식을 만들 때 사용하던 방법을 이용하여 합을 구하여야 한다. (수학에서 공식 암기를 하면 안되는 이유)

 

 

 

 

 

따라서 지수가중이동평균의 가중평균식은 다음과 같다.

 

 

 

 

 

아래 그래프는 데이터에 대한 지수가중이동평균 그래프(파란색)지수가중이동평균의 가중평균 그래프(초록색)을 나타낸 것이다. 확실히 0에 대한 영향력을 줄이기 때문에 첫 항에 대해 조금 더 자세한 값을 표현한다. 이를 bias correction 편향 보정이라 부른다. (0으로) 편향된 것을 보정한다는 뜻이다.

 

지수가중이동평균을 할 수록 첫 항의 영향력이 감쇠되어 점점 같은 그래프로 그려진다는 것에도 주목하라.

 

파란색 그래프와 초록색 그래프의 차이에 주목하자.

 

 

 

유도는 끝났다. 이제 이를 코드로 표현해 보자. beta1과 beta2 값이 각각 0.9, 0.999인 것은 이 값이 가장 잘 수렴한다고 알려진 값이라고 한다. 얼마든지 바꿀 수는 있다.

beta1 = 0.9
beta2 = 0.999
v = 0
s = 0
for i in range(100):
	i += 1 # 0으로 나누어지는 것을 방지
    dL_dx = loss_gradient(W)
    v = beta1*v + (1-beta1)*dL_dx
    s = beta2*s + (1-beta2)*dL_dx**2
    v_avg = v/(1-np.power(beta1, i)) # 가중평균
    s_avg = s/(1-np.power(beta2, i)) # 가중평균
    W = W - learning_rate*(1/(np.sqrt(s_avg)+1e-10))*v_avg

 

 

 

이전 예제의 오차함수에서 잘 작동할까? 

 

1) 시작위치 -8, learning rate = 1.3 부터 전역 최소점에 수렴함.

 

 

 

2) 시작위치 -8, learning rate = 10.2 부터 전역 최소점에서 한 번 이상 튕긴다.

 

 

 

3) 뭔가 이전보다 성능이 안 좋은 것 같다고 생각이 들 수 있다. 하지만 사실이다. 변수들이 많아질 수록 성능에 영향을 주는 변수들인 hyper parameters 등을 잘 tuning 해야 한다. beta2값을 0.999에서 0.5로 바꾸어 보자. 

같은 위치 -8에서 learning rate = 0.2 부터 전역 최소점에 수렴하고, 3.2부터 전역 최소점에서 1번 이상 튕긴다. 

 

high level 머신러닝 라이브러리인 tensorflow와 pytorch에서 최적화 방법인 optimizer 클래스에는 adam이 있고 이에 대한 parameter는 아래와 같다.

 

torch.optim.Adam(params, lr=0.001, betas=(0.9, 0.999), eps=1e-08, weight_decay=0, amsgrad=False

tf.keras.optimizers.Adam(
    learning_rate=0.001, beta_1=0.9, beta_2=0.999, epsilon=1e-07, amsgrad=False,
    name='Adam', **kwargs

별다른 수정이 없다면 beta1=0.9, beta2=0.999가 default값임을 알 수 있다.

 

 

 

 

아래는 전체 코드이다.

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.animation import FuncAnimation
import matplotlib.animation as animation

fig = plt.figure()
ax = plt.axes(xlim=(-10, 10))
start_x = -8
learning_rate=1.3

def loss_function(x):
    return (1/200)*(x-9)*(x)*(x+1)*(x+6) + 4

def loss_gradient(x):
    return (1/100)*(2*np.power(x, 3)-3*np.power(x, 2)-57*x-27)

xrange = np.arange(-10, 10, 0.1)
loss_array = []

for x in xrange:
    loss = loss_function(x)
    loss_array.append(loss)
    
ax.plot(xrange, loss_array)
ax.set_xlabel('x')
ax.set_ylabel('loss')


train_x = []
beta1 = 0.9
beta2 = 0.999 
v = 0
s = 0
for i in range(100):
    if i == 0:
        pass
    else:
        dL_dx = loss_gradient(start_x)
        v = beta1*v + (1-beta1)*dL_dx
        s = beta2*s + (1-beta2)*dL_dx**2
        v_avg = v/(1-np.power(beta1, i)) 
        s_avg = s/(1-np.power(beta2, i)) 
        start_x = start_x - learning_rate*(1/(np.sqrt(s_avg)+1e-10))*v_avg
    train_x.append(start_x)

redDot, = ax.plot([], [], 'ro')

def animate(frame):
    loss = loss_function(frame)
    redDot.set_data(frame, loss)
    return redDot

ani = FuncAnimation(fig, animate, frames=train_x)
#FFwriter = animation.FFMpegWriter(fps=5) 
#ani.save('adam_sx-8lr102.mp4', writer = FFwriter)
plt.show()

 

그렇다면 adam을 사용하면 무조건 전역 최소점에 수렴할까? 답은 그럴 가능성이 높다는 것이다. 지역 최소점에 수렴하는 원인은 여러가지가 있다. adam을 사용하는 것은 문제 해결 방법 중 한 가지일 뿐이지 전부는 아니다. 

 

나중에 언급하겠지만 될 수 있으면 adam을 쓰자.

(하지만 이번 Numpy 딥러닝 시리즈에서는 자료들이 복잡하지 않으므로 단순 경사하강법을 사용할 것이고 수렴이 잘 안되는 경우에 adam을 사용할 것이다.)