본문 바로가기

딥러닝

매개변수 갱신 방법(1)

1. 매개변수 갱신

 신경망 학습의 목적은 손실 함수의 값을 최대한 감소시키는 매개변수를 찾는, 즉 매개변수의 최적값을 찾는 문제이다.

 이러한 문제를 푸는 것을 최적화(Optimization)라고 한다.

■ 신경망은 매개변수의 수가 많아질수록 매개변수의 공간이 넓고 복잡해져서 바로 최적의 매개변수를 구해 손실 함수의 최솟값을 찾기 어렵다.

■ 앞서, 최적의 매개변수를 찾기 위해 매개변수의 기울기(미분)을 구해서, 기울어진 방향으로 매개변수 값을 갱신하는 작업을 반복해 조금씩 손실 함수의 최솟값에 다가가는 방법을 보았는데, 이 방법을 경사 하강법이라고 하며,

신경망 학습(2)

 

신경망 학습(2)

1. 학습1.1 신경망의 학습에 대한 지표는 손실 함수■ 손실 함수를 사용하는 이유는, 학습 과정에서 손실 함수의 결과를 바탕으로 손실 함수의 결과값을 최대한 작게 만드는 최적의 매개변수(가

hyeon-jae.tistory.com

미니 배치를 통해 무작위로 선정한 데이터에 경사 하강법을 적용하여 매개변수를 갱신하는 방법을 확률적 경사 하강법(SGD)이라 한다.

■ 이렇게 매개변수와 손실 함수에 대한 기울기 정보만으로 매개변수를 갱신하는 SGD를 'optimizer'라고 한다.

 예를 들어 갖고 있는 데이터가 1,000개라 하면, 100개씩 나누어 순전파와 역전파를 10번 반복한다. 이 한 과정을 'Epoch'라 하며, 여기서 100개의 데이터를 'Mini - Batch'라 하며 100의 크기에 대해서는 'Batch Size'라 한다.

 이렇게 데이터를 나눠 Gradient Descent Method하는 방법을 'Stochastic Gradient Method(SGD)'라 하며, 이렇게 Gradient Descent 해주는 것을 통틀어 'optimizer'라 한다.

 

2. Optimizer

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

 SGD를 수식으로 나타내면 다음과 같다.\[
W \leftarrow W - \eta\dfrac{\partial L}{\partial W} \]

- \( W_{\text{t+1}} \)는 갱신할 가중치 매개변수, \( \eta \)는 학습률, \(
\dfrac{\partial L}{\partial W}
\)는 가중치 매개변수에 대한 손실 함수의 기울기이다.

- 이 수식은 기존의 가중치\( (W) \)에서 학습률(\( \eta \))과 기존의 가중치\( ( W) \)에 대한 \( Loss \)의 미분 값을 곱한 값을 빼서 기울어진 방향으로 일정 거리만큼 학습률로 조정해 이동하는 방법을 의미한다.

params = {
    'W1': np.array([[0.2, -0.5], [1.5, 0.3]]),
    'b1': np.array([0.1, -0.2])
}
grads = {
    'W1': np.array([[0.01, -0.02], [0.03, -0.04]]),
    'b1': np.array([0.005, -0.01])
}
## SGD
class SGD:
    def __init__(self, lr):
        self.lr = lr # 학습률
        
    def update(self, params, grads):
        for key in params.keys(): # params의 key는 가중치 매개변수
            params[key] -= self.lr * grads[key] # ex) params['W1'] = params['W1'] - self.lr * grads['W1']
s = SGD(lr = 0.01)
s.update(params, grads)
print(params)
```#결과#```
{'W1': array([[ 0.1999, -0.4998],
       [ 1.4997,  0.3004]]), 'b1': array([ 0.09995, -0.1999 ])}
````````````

■ time step \( t \)를 반영하면, SGD의 매개변수 업데이트는 \( \theta_{t + 1} = \theta_t - \eta \times g_t \)이다. 최솟값을 찾기 위해 gradient 반대 방향으로 업데이트를 진행하는데 gradient의 방향이 바뀌면 특정 local minimum에 빠질 가능성이 높아진다.

- \( \theta_{t + 1} = \theta_t - \eta \times g_t \)를 \( t = 1 \)부터 전개하면

\[
\begin{align*}
\theta_1 &= \theta_0 - \eta g_0 \\
\theta_2 &= \theta_1 - \eta g_1 = \theta_0 - \eta g_0 - \eta g_1 \\
\theta_3 &= \theta_2 - \eta g_2 = \theta_0 - \eta g_0 - \eta g_1 - \eta g_2 \\
& \vdots \\
\end{align*}
\]

- 이렇게 SGD의 매개변수 갱신은 단순히 \(
\theta_t = \theta_0 - \eta \left( g_0 + g_1 + \ldots + g_{t - 1} \right)
\)으로 \( g_0, g_1, \ldots, g_{t - 1} \)을 모두 더한 값을 \( \eta \)에 곱해 초기 \( \theta_0 \)에서 빼기 때문에 (모든 \( g_t \)에서 학습률 값이 동일하다는 가정)

- gradient의 방향이 바뀌면 이전 단계에서의 gradient와 현재 gradient가 상쇄되거나 합쳐져 업데이트의 방향이 바뀔 수 있다. 또한, 학습률의 크기에 크게 의존된다는 것을 볼 수 있다.

■ 예를 들어 \(
\dfrac{1}{20} \cdot (x^2 + y^2)
\)
은 다음 그림과 같이 2차원에서 등고선으로 보면 \( x \)축 방향으로 늘린 타원처럼 생겼다.

- 이 함수의 기울기를 그려보면 다음 그림처럼 \( y \) 축 방향은 크고 \( x \) 축 방향은 작다.

또한 \(
\dfrac{1}{20} \cdot (x^2 + y^2)
\)
최솟값을 갖는 지점은 \( (x, y) = (0, 0) \)이지만, 기울기 대부분이 (0, 0) 방향을 가리키지 않으며 (0, 0) 부근에서 기울기가 서로 반대 방향임을 볼 수 있다.

■ 이 함수에 SGD를 적용해보면 다음과 같다.

■ 위의 그림과 같이 \(
f(x, y) = \dfrac{1}{20} \cdot (x^2 + y^2)
\)에 SGD를 적용하면 최솟값 (0, 0)까지 지그재그로 비효율적으로 이동하는 것을 볼 수 있다.

■ 이렇게 SGD의 단점은 방향에 따라 기울기가 달라지는, 즉 기울기가 가리키는 지점이 하나가 아닌 여러 지점인 비등방성 함수에서 무작정 기울기가 최소인 방향으로 이동하는 방법을 사용하면 최솟값까지의 탐색 경로가 비효율적이다.

■ 최솟값을 찾는 과정에서 지그재그로 움직이는 이유도 비등방성 함수에서 기울기가 가리키는 방향이 최솟값 하나가 아니라 여러 다른 방향을 가리키기 때문이다.

■ SGD의 이런 단점을 개선해주는 방법으로 모멘텀(Momentum), AdaGrad, Adam이 있다.

 

2.2 모멘텀(Momentum)

■ Momentum은 물리학에서 운동량(물체의 질량과 속도의 곱으로 나타내는 물리량)을 의미한다.

■ 모멘텀 기법은 수식으로 다음과 같이 나타낼 수 있다.

\[ v \leftarrow \alpha v - \eta \dfrac{\partial L}{\partial W} \]

\[ W \leftarrow W + v \]

- \( W \)는 갱신할 가중치 매개변수 \( \dfrac{\partial L}{\partial W} \)는 \( W \)에 대한 손실 함수의 기울기, \( \alpha \)는 모멘텀 계수(momentum coefficient),  \( \eta \)는 학습률, \( v \)는 물리에서 말하는 '물체의 속도(velocity)'이다.

class Momentum:
    def __init__(self, lr, momentum):
        self.lr = lr
        self.momentum = momentum
        self.v = None
        
    def update(self, params, grads):
        # 속도 v 초기화
        if self.v is None:
            self.v = {key: np.zeros_like(value) for key, value in params.items()}
        
        for key in params.keys():
            self.v[key] = self.momentum * self.v[key] - self.lr * grads[key] # v 업데이트
            params[key] += self.v[key] # W <- W + v, 가중치 업데이트
m = Momentum(lr = 0.01, momentum = 0.2)
m.update(params, grads)
print(m.v); print(params)
```#결과#```
{'W1': array([[-0.0001,  0.0002],
       [-0.0003,  0.0004]]), 'b1': array([-5.e-05,  1.e-04])}
{'W1': array([[ 0.19961, -0.49922],
       [ 1.49883,  0.30156]]), 'b1': array([ 0.099805, -0.19961 ])}
````````````

 \( v \leftarrow \alpha v - \eta \dfrac{\partial L}{\partial W} \)은 다음 그림과 같이 기울기 방향으로 힘을 받아 물체가 속도가 붙어 이동하는 것을 의미한다. 즉, \( v \)는 그래디언트가 감소하는 속도를 나타낸 이동 벡터이다.

[출처] https://medium.com/ai%C2%B3-theory-practice-business/hyper-parameter-momentum-dc7a7336166e

■ 여기서 \( \alpha v \)는 물체가 아무런 힘을 받지 않을 때 서서히 하강시키는 관성의 역할을 한다. 즉, 위의 그림처럼 global minimum을 찾기 위해 아래로 향할 때, \( v \)라는 속도 항이 존재하여 아래로 향할 때는 가속이 붙고, 위로 향할 때는 감속을 받는다.

■ time step \( t \)를 반영해서 모멘텀의 매개변수 업데이트 과정과 SGD의 매개변수 업데이트 과정을 비교해 보면 관성, 가속도를 의미하는 momentum의 개념 \( v_{t + 1} \)이 추가되었다.

\[
\theta_{t+1} = \theta_t - \eta \cdot g_t \quad \text{VS.} \quad v_{t+1} = \alpha v_t - \eta \cdot g_t, \quad \theta_{t+1} = \theta_t + v_{t+1}
\]

- gradient가 SGD 때 처럼 변곡점을 지나도 gradient의 방향이 바뀌지 않도록 \( g_t \)를 보완하는 방법은 과거의 기울기 방향에 대한 정보들을 사용하는 것이다. 

- 현재 gradient \( \dfrac{\partial L}{\partial W} = 0 \)일 때(= 안장점이나 local minimum) 또는 gradient의 방향이 바뀌더라도 과거 기울기 정보를 더해준다면, 위의 그림과 같이 \( v \)라는 속도 항이 탐색 경로를 공이 구르듯이 만들어줘 아래로 향할 때는 가속이 붙고 위로 향할 때는 감속을 받아 local mimimum에서 탈출할 수 있는 것이다.

- \( '- \eta \times g_t' \)는 현재 gradient의 정보이며 \( av_t \)는 과거의 gradient의 방향에 대한 정보이다. 

- \( v_{t + 1} \)을 전개해 보면

\[
\begin{align*}
v_1 &= \alpha v_0 - \eta g_1 \\
v_2 &= \alpha v_1 - \eta g_2 = \alpha \left( \alpha v_0 - \eta g_1\right) - \eta g_2 \\
v_3 &= \alpha v_2 - \eta g_3 = \alpha \left( \alpha \left( \alpha v_0 - \eta g_1\right) - \eta g_2 \right) - \eta g_3 \\
& \vdots
\end{align*}
\]

- 각 시점마다의 gradient 정보 \( g_1, g_2, g_3, \ldots \)가 포함된 것을 볼 수 있고 시점이 과거로 갈수록 모멘텀 계수 \( \alpha \)가 곱해져 gradient의 영향력이 작아지는 것을 볼 수 있다. 

- SGD와 momentum을 다음과 같이 벡터로 표현하면, SGD는 단순히 gradient 반대 방향에 학습률 \( \eta \)를 곱한 형태인 반면, momentum은 현재 gradient에 과거의 모멘텀을 더해 다음 이동 방향을 결정한다.

- 예를 들어 \( y = x^2 \)이 global minimum인 0에 수렴하는 과정을 보면 관성에 의해 아래로 향할 때는 가속이 붙고, 위로 향할 때는 감속을 받는 것을 볼 수 있다. 

 여기서 momentum의 문제점을 볼 수 있다. 바로 관성에 의해 global minimum에 바로 수렴하지 못하고 몇 번 지나치는 것이다.

이 문제점을 개선하기 위해서는 새로운 기울기에 대해서는 크게 반영하고 과거의 기울기에 대해서는 약하게 반영해야 한다. 

이는 '가중이동평균' 개념을 이용하면 된다.

 

2.3 Nesterov Accelerated Gradient, NAG

■ 네스테로프 모멘텀은 모멘텀 방향으로 '한 걸음 미리 가본 곳'에서 손실 함수의  gradient vector를 구해서 다음 위치를 결정하는 방법이다.

모멘텀 방향으로 한 걸음 미리 가본 곳을 \( \theta_t + \alpha v_t \)라고 한다면, NAG의 매개변수 업데이트 과정은 다음과 같다.

\[ v_{t + 1} = \alpha v_t - \eta g(\theta_t + \alpha v_t) \]

\[ \theta_{t + 1} = \theta_t + v_{t + 1} \]

■ 즉, NAG는 \( \theta_t \)에서 \( \theta_t + \alpha v_t \)로 이동한 다음, 그곳에서의 gradient를 구해서 다음 이동 방향을 결정한다. 이를 벡터로 표현하면 다음과 같다.

■ 기존의 momentum 방법과 다른 점은 momentum의 \( v_{t + 1} \)은 \( - \eta \times g_t \)라는 현재 gradient 정보를 이용하지만, NAG는 \(
- \eta \times g(\theta_t + \alpha v_t)
\)로 momentum 이후의 위치에서의 gradient 정보를 이용한다는 점이다.

이 차이는 매개변수 업데이트 과정에서 현재 속도 \( v_t \)와 \( v_{t + 1} \)을 이용한 에러 보정 항을 추가하게 되어 기존 momentum 방식이 과하게 이동한다는 단점을 완화시킨다. 이는 위의 NAG 수식을 Bengio Nesterov Momentum 방식으로 식을 변형하면 알 수 있다.

- 1) \(
\tilde{\theta}_t = \theta_t + \alpha v_t
\)

        \(
\bullet \) \( \tilde{\theta}_t
\)는 현재 gradient가 아닌 momentum 방향에서 조금 앞선 곳

- 2) \(
\tilde{\theta}_t = \theta_t + \alpha v_t \rightarrow v_{t+1} = \alpha v_t - \eta g(\tilde{\theta}_t)
\)
\[
\begin{align*}
\tilde{\theta}_{t+1} &= \theta_{t+1} + \alpha v_{t+1} \\
&= \theta_t + v_{t+1} + \alpha v_{t+1} \\
&= \theta_t + (1 + \alpha) v_{t+1}, \quad \theta_t = \tilde{\theta}_t - \alpha v_t \\
&= \tilde{\theta}_t - \alpha v_t + (1 + \alpha) v_{t+1} \\
&= \tilde{\theta}_t + v_{t+1} + \alpha (v_{t+1} - v_t)
\end{align*}
\] \(
\bullet \) \( \tilde{\theta}_{t+1} = \tilde{\theta}_t + v_{t+1} + \alpha (v_{t+1} - v_t)
\)에서 \( \alpha (v_{t+1} - v_t) \) 
항이 \( t + 1 \) 시점과 \( t \) 시점의 velocity 간의 오차를 보정, 속도의 변화량을 조정해서 과하게 이동하는 단점을 완화시킨다.

- 위와 같이 식을 변형하면 Loss와 gradient를 같은 점에서 구할 수 있다.

참고) https://tensorflow.blog/2017/03/22/momentum-nesterov-momentum/#4

 

Momentum & Nesterov momentum

경사하강법gradient descent 최적화 알고리즘의 한 종류인 모멘텀momentum과 네스테로프 모멘텀nesterov momentum 방식은 여러 신경망 모델에서 널리 사용되고 있습니다. 비교적 이 두가지 알고리즘은 직관

tensorflow.blog

참고) https://www.youtube.com/watch?v=_JB0AO7QxSA&list=PL3FW7Lu3i5JvHM8ljYj-zLfQRF3EO8sYv&index=7

■ 위의 변형한 수식으로 NAG를 구현하려면 이전 시점을 계산해야 한다.

from copy import deepcopy

class Nesterov:
    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():
            old_v = deepcopy(self.v[key]) # v_t
            self.v[key] *= self.momentum
            self.v[key] -= self.lr * grads[key] # v_{t+1}
            params[key] += -self.momentum * old_v # params = params_t - a*v_t
            params[key] += (1 + self.momentum) * self.v[key] # params = params - a*v_t + (1 + a)v_{t+1}
nag = Nesterov()
nag.update(params, grads)
print(nag.v); print(params)
```#결과#```
{'W1': array([[-0.0001,  0.0002],
       [-0.0003,  0.0004]]), 'b1': array([-5.e-05,  1.e-04])}
{'W1': array([[ 0.19981, -0.49962],
       [ 1.49943,  0.30076]]), 'b1': array([ 0.099905, -0.19981 ])}
````````````

■ 식을 다음과 같이 변형하면 시점 \( t \)만 고려해서 계산할 수 있다.

- ① \( \tilde{\theta}_t = \theta_t + \alpha v_t \Rightarrow v_t = \dfrac{\tilde{\theta}_t - \theta_t}{\alpha}, \quad \tilde{\theta}_{t+1} = \theta_{t+1} + \alpha v_{t+1} \)

- ② \( \tilde{\theta}_t = \theta_t + \alpha v_t \Rightarrow \tilde{\theta}_{t+1} = \theta_{t+1} + \alpha v_{t+1} \Leftrightarrow \theta_{t+1} = \tilde{\theta}_{t+1} - \alpha v_{t+1} \)

- ③ \( v_{t+1} = \alpha v_t - \eta g_{\tilde{\theta}_t} \) \[ \begin{align*} \theta_{t+1} &= \theta_t + v_{t+1} \\ &= \theta_t + \alpha v_t - \eta \cdot g_{\tilde{\theta}_t} \\ &= \theta_t + \tilde{\theta}_t - \theta_t - \eta \cdot g_{\tilde{\theta}_t} \end{align*} \] \[ \begin{align*} \Rightarrow \tilde{\theta}_{t+1} - \alpha v_{t+1} &= \tilde{\theta}_t - \eta \cdot g_{\tilde{\theta}_t} \\ \Rightarrow \tilde{\theta}_{t+1} &= \tilde{\theta}_t + \alpha v_{t+1} - \eta \cdot g_{\tilde{\theta}_t} \\ &= \tilde{\theta}_t + \alpha \left( \alpha v_t - \eta \cdot g_{\tilde{\theta}_t} \right) - \eta \cdot g_{\tilde{\theta}_t} \\ &= \tilde{\theta}_t - (1 + \alpha) \eta \cdot g_{\tilde{\theta}_t} + \alpha^2 v_t \end{align*} \]

class Nesterov2:
    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.momentum * self.momentum * self.v[key]
            params[key] -= (1 + self.momentum) * self.lr * grads[key] # params = params + a^2*v_t - (1 + a)*lr*g(params)
nag2 = Nesterov2()
nag2.update(params, grads)
print(nag2.v); print(params)
```#결과#```
{'W1': array([[-0.0001,  0.0002],
       [-0.0003,  0.0004]]), 'b1': array([-5.e-05,  1.e-04])}
{'W1': array([[ 0.199729, -0.499458],
       [ 1.499187,  0.301084]]), 'b1': array([ 0.0998645, -0.199729 ])}
````````````

■ \(
\dfrac{1}{20} \cdot (x^2 + y^2)
\)
에 대해 momentum 방법과 NAG 방법을 적용하면 다음과 같다.


■ 모멘텀을 적용할 경우 최솟값 탐색 경로가 다음 그림과 같이 SGD에 비해 지그재그로 움직이는 정도가 덜하며, 탐색 경로가 공이 구르듯이 움직이는 것을 볼 수 있다.

■ 이는 \(
f(x, y) = \frac{1}{100} \cdot (x^2 + y^2)
\)의 기울기 그림에서 보았듯이 \( x \) 축의 힘은 아주 작지만 \( y \) 축처럼 방향은 변하지 않으므로 모멘텀의 영향을 받으면, 한 방향으로 일정하게 가속되어 SGD보다 

\( x \)축 방향으로 빠르게 다가가므로 지그재그 움직임이 줄어든다.

■ 이에 반해 \( y \) 축은 힘은 크지만, \( y = 0 \) 근처에서 위아래로 교차하면서 힘이 상쇄된다. 즉, \( y \) 축 방향으로의 힘은 크지만 상충하는 힘 때문에 \( y \) 축 방향의 속도는 일관된 속도를 유지하기 어렵다.

네스테로프 방법은 모멘텀과 달리 속도의 변화량을 조정하여 모멘텀에 비해 더 부드럽게 수렴하는 모습을 볼 수 있다.

 

2.4 AdaGrad

■ 신경망 학습에서 학습률은 사용자가 얼마의 값을 설정하느냐에 따라서 너무 작게 설정하면 학습 시간이 길어지고, 너무 크게 설정하면 학습이 너무 빨리 끝나 학습이 제대로 이뤄지지 않을 수 있다. 따라서 학습률을 조절해가며 학습을 진행해야 한다.

■ 이 학습률을 조절하는 효과적인 기술로 '학습률 감소'가 있다. 학습을 진행하면서 학습률을 점차 줄여가는 방법으로 처음에는 학습률을 크게 설정하여 학습하다가 조금씩 학습률을 낮춰 학습한다. 이 방법에서 더 발전된 방법이 AdaGrad이다.

■ 즉, AdaGrad는 가중치 매개변수를 갱신할 때마다 스탭 사이즈(step size)를 다르게 설정해 주는 방식이다.

■ AdaGrad의 수식은 다음과 같다.\[
h \leftarrow h + \frac{\partial L}{\partial W} \odot \frac{\partial L}{\partial W}
\]

\[ W \leftarrow W - \eta \dfrac{1}{\sqrt{h}} \dfrac{\partial L}{\partial W} \]

- \( W \)는 갱신할 가중치 매개변수, \( \dfrac{\partial L}{\partial w} \)는 \( W \)에 대한 손실 함수의 기울기, \( \eta \)는 학습률이며, \( h \)는 기존 기울기 값을 계속 제곱한 값이다.

class AdaGrad:
    
    def __init__(self, lr = 0.01):
        self.lr = lr
        self.eps = 1e-8
        self.h = None
        
    def update(self, params, grads):
        # h 초기화
        if self.h is None:
            self.h = {key: np.zeros_like(value) for key, value in params.items()}

        for key in params.keys():
            self.h[key] += grads[key] * grads[key] # h 업데이트 
            params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + self.eps) # 가중치 업데이트

- 분모에 엡실론 값을 더하는 이유는 \( h \)가 gradient의 제곱이므로 \( h \) 값이 누적되어 0에 수렴할 경우 발산하므로, 이를 방지하기 위해서이다.

ada = AdaGrad()
ada.update(params, grads)
print(params)
```#결과#```
{'W1': array([[ 0.19000001, -0.49      ],
       [ 1.49      ,  0.31      ]]), 'b1': array([ 0.09000002, -0.19000001])}
````````````

 \( h \)는 기존 기울기 값을 계속 제곱해 더한 값이기 때문에 \( h \)를 계산할 때,  크게 갱신된 과거 매개변수 \( W_{t - 1} \)가 있다면 현재 \( h_t \) 값도 커지므로 현재 \( \dfrac{\eta}{\sqrt{h_t}} \)의 값은 작아지게 된다. 즉, step size(학습률)를 낮추게 만든다. 반대로 \( W_{t - 1} \)가 작은 값이라면,  \( h_t \) 값도 작아지므로 \( \dfrac{\eta}{\sqrt{h_t}} \)의 값은 커지게 되어 step size가 커지게 된다.

■ 즉, step size는 매개변수 원소마다 다르게 작용된다.

- 가중치 업데이트 변화량이 크다면 최적화가 많이 진행됐을 확률이 높기 때문에 step size를 작게 만들어 작은 크기로 이동하면서 세밀하게 조정하고,

- 가중치 업데이트 변화량이 작다면 최적화하기 위해 많이 이동해야 할 확률이 높기 때문에 step size를 크게 만들어 빠르게 이동한다.

■ AdaGrad는 이렇게 학습이 진행될수록 \( h \)의 값은 커지고, 학습률은 점점 더 감소한다. 따라서 \( h \) 값이 누적될수록 감소된 학습률로 인해 \( W \)의 업데이트 크기, 즉 갱신 강도가 약해지며 어느 순간 갱신량이 0이 되어 갱신이 멈추게 된다. 

■ 이 문제를 개선한 기법으로서 RMSProp 이라는 방법이 있다.

■ \(
\dfrac{1}{20} \cdot (x^2 + y^2)
\)
 AdaGrad 방법을 적용할 경우

\(
\dfrac{1}{20} \cdot (x^2 + y^2)
\) \( y \)축 방향은 기울기가 크니까 처음에는 갱신 정도가 크지만, \( h \)로 인해 앞전의 업데이트 크기에 비례하여 갱신 정도가 큰 폭으로 작아지는 것을 볼 수 있다. 그리고 반복될수록 \( h \) 값이 커짐에 따라 \( y \)축 방향으로의 갱신 강도가 빠르게 약해져서 SGD, Momentum에 비해 지그재그로 움직이는 것이 크게 줄어든 것을 볼 수 있다.

2.5 RMSProp(Root Mean Square Propagation)

■ RMSProp은 AdaGrad가 \(
\left( \frac{\partial L}{\partial W} \right)^2
\)을 계속 더해서 학습을 진행해 갱신량이 0이 되어가는 문제를 개선하고자 gradient의 크기에 지수이동평균을 사용해 gradient의 크기에 따라 각각의 파라미터를 갱신하는 방법이다.

\[
h \leftarrow \rho h + (1 - \rho) \frac{\partial L}{\partial W} \odot \frac{\partial L}{\partial W}
\] 
\[ W \leftarrow W - \eta \dfrac{1}{\sqrt{h}} \dfrac{\partial L}{\partial W} \]

- \( \rho \) 로우는 지수 이동 평균의 업데이트 계수이다.

- 지수이동평균(Exponential Moving Average)은 과거 모든 기간을 계산 대상으로 하며 최근의 데이터에 더 높은 가중치를 두는 일종의 가중이동평균법이다.

- 즉, \( \rho \)는 과거의 값에 대한 영향력의 정도를 나타낸다.

class RMSprop:
    def __init__(self, lr = 0.01, decay_rate = 0.99):
        self.lr = lr
        self.eps = 1e-8
        self.decay_rate = decay_rate
        self.h = None
        
    def update(self, params, grads):
        if self.h is None:
            self.h = {key: np.zeros_like(value) for key, value in params.items()}
            
        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]) + self.eps)
rms = RMSprop()
rms.update(params, grads)
print(params)
```#결과#```
{'W1': array([[ 0.100001  , -0.4000005 ],
       [ 1.40000033,  0.39999975]]), 'b1': array([ 1.99996e-06, -1.00001e-01])}
````````````

■ RMSProp은 지수이동평균을 이용하므로 최근 변화(새로운 기울기 값)는 크게 반영하고 과거의 기울기는 작게 반영하는데 이는 \(
h_t = \rho h_{t - 1} + (1 - \rho) \nabla f(x_{t - 1})^2
\)의 식을 \( h_{t + 1} = \rho h_t + (1 - \rho) \nabla f(x_t)^2 \)으로 바꿔보면 ( \( t + 1 \) 대입)

\[
\begin{align*}
h_{t + 1} &= \rho h_t + (1 - \rho) \nabla f(x_t)^2 \\
&= \rho \left( \rho h_{t - 1} + \left( 1 - \rho \right) \nabla f(x_{t - 1})^2 \right) + (1 - \rho) \nabla f(x_t)^2 \\
&= \rho^2 h_{t - 1} + \rho (1 - \rho) \nabla f(x_{t - 1})^2 + (1 - \rho) \nabla f(x_t)^2 \\
&= \rho^2 h_{t - 1} + (1 - \rho) \left( \nabla f(x_t)^2 + \rho \nabla f(x_{t - 1})^2 \right)
\end{align*}
\]

- 이때, \( h_{t - 1} = \rho h_{t - 2} + (1 - \rho) \nabla f(x_{t - 2})^2 \)이므로 

\(
\begin{align*}
\rho^2 h_{t - 1} + (1 - \rho) \left( \nabla f(x_t)^2 + \rho \nabla f(x_{t - 1})^2 \right) &= \rho^2(\rho h_{t - 2} + (1 - \rho) \nabla f(x_{t - 2})^2) + (1 - \rho)(\nabla f(x_t)^2 + \rho \nabla f(x_{t - 1})^2) \\
&= \rho^3 h_{t - 2} + \rho^2 (1 - \rho) \nabla f(x_{t - 2})^2 + (1 - \rho)(\nabla f(x_t)^2 + \rho \nabla f(x_{t - 1})^2)
\end{align*}
\)이 된다.

■ 이런 식으로 식을 계속 확장할 경우 \( \nabla f(x_t)^2 \)의 계수는 \( 1 - \rho \)지만, \( t = t_1, t_2, t_3, \ldots \) 과거로 가면 갈수록 \( \rho \)의 승이 더 많이 곱해진다. 즉, 최근 변화 \( \nabla f(x_t) \)는 크게 반영하고 과거 기울기 변화는 작게 반영되는 것이다.

- 예를 들어, 만약 \( \rho = 0.9 \)일 경우 \( \nabla f(x_t)^2 \)에는 0.1, 그 뒤의 과거 항들은 \(
\rho^2, \rho^3, \rho^4, \ldots
\)의 영향을 받게 되므로 새로운 기울기 \( \nabla f(x_t) \)에 비해 점점 더 작은 값이 곱해져 작게 반영된다.

■ 이렇게 RMSProp는 AdaGrad와는 달리 상황에 맞춰 step size를 조절해 갱신을 진행한다. 미분 값이 큰 곳에서는 기존 학습률보다 더 작은 학습률로 조정하며, 미분 값이 작은 곳에서는 기존 학습률보다 더 큰 학습률로 조정하여 파라미터를 갱신해서 안정적인 학습을 진행한다.

 \(
f(x, y) = \frac{1}{100} \cdot (x^2 + y^2)
\) 
 RMSProp 방법을 적용할 경우

AdaGrad에 비해 수렴하기까지 일정된 속도가 유지되는 것을 볼 수 있다.

 

2.6 Adam

■ Adam은 Momentum과 RMSProp의 아이디어를 결합한 최적화 기법이다. 

■ Adam optimizer의 pseudo 코드를 보면

[출처] ADAM: A METHOD FOR STOCHASTIC OPTIMIZATION (2015)

- 첫 번째 모멘트와 두 번째 모멘트 그리고 time step \( t \)를 0으로 초기화한다.

- 그리고 \( \theta_t \)가 더 이상 수렴하지 않을 때까지 아래의 항목들을 while 문으로 반복한다.

- (1) \( t \leftarrow t + 1 \)

        \( \bullet \) time step 증가.

- (2) Gradient \( g_t \leftarrow \nabla_\theta f_t(\theta_{t - 1}) \)

        \( \bullet \) 이전 time step \( t - 1 \)의 gradient를 목적 함수 \( f(\theta) \)로 계산.

        \( \bullet \) \( f(\theta) \) 값의 최소화가 Adam의 목표.

- (3) First & Second momentum \( m_t \leftarrow \beta_1 \cdot m_{t-1} + (1 - \beta_1) \cdot g_t \), \( v_t \leftarrow \beta_2 \cdot v_{t-1} + (1 - \beta_2) \cdot g_t^2 \)
        \( \bullet \) 편향된 첫 번째, 두 번째 모멘텀 추정값을 계산한다.

        \( \bullet \) 여기서 첫 번째 모멘텀 \( m_t \)는 모멘텀 알고리즘 기울기의 지수 평균을, 

        \( \bullet \) 두 번째 모멘텀 \( v_t \)는 RMSProp 알고리즘과 유사하게 기울기의 제곱값의 지수이동평균을 계산하여 각 파라미터별 학습 속도를 조절한다.

        \( \bullet \) 이렇게 해서 '경사 방향'은 모멘텀 방법처럼 부드럽게 공기 그릇의 곡면(기울기)을 따라 구르듯이 조정되고, '학습 속도'는 RMSProp 처럼 각 파라미터에 맞게 적응적으로 조정된다.

- (4) Bias correction \( \hat{m}_t \leftarrow m_t / (1 - \beta_1^t) \), \( \hat{v}_t \leftarrow v_t / (1 - \beta_2^t) \)
        \( \bullet \) \( m_t \)와 \( v_t \)는 초깃값이 0이기 때문에

        \( \bullet \) \( (1 - \beta_1^t) \)와 \( (1 - \beta_2^t) \)로 나눠서 편향을 보정한다.

- (5) \(
\theta_t \leftarrow \theta_{t-1} - \alpha \, (\text{learning rate}) \cdot \hat{m}_t / (\sqrt{\hat{v}_t} + \epsilon)
\)
        \( \bullet \) 파라미터 업데이트 과정이다.

        \( \bullet \) 논문에 따르면 이 알고리즘의 (4), (5) 단계를 다음과 같이 변경하면 갱신 과정의 계산 효율성이 향상될 수 있다.

        \( \bullet \) \(
\theta_t \leftarrow \theta_{t-1} - \alpha \cdot \hat{m}_t / (\sqrt{\hat{v}_t} + \epsilon)
\), \(
\hat{m}_t \leftarrow \dfrac{m_t}{1 - \beta_1^t}, \quad \hat{v}_t \leftarrow \dfrac{v_t}{1 - \beta_2^t}
\)
        \( \bullet \) \(
\dfrac{\hat{m}_t}{\sqrt{\hat{v}_t}} = \dfrac{\dfrac{m_t}{1 - \beta_1^t}}{\sqrt{\dfrac{v_t}{1 - \beta_2^t}}} = \dfrac{\sqrt{1 - \beta_2^t} \cdot m_t}{(1 - \beta_1^t) \cdot \sqrt{v_t}} \Rightarrow \theta_t \leftarrow \theta_{t-1} -  \alpha_t \cdot \dfrac{m_t}{\left( \sqrt{v_t} + \hat{\epsilon} \right)}
\), \(
a_t = \alpha \cdot \dfrac{\sqrt{1 - \beta_2^t}}{1 - \beta_1^t} \)

        \( \bullet \) \(
\theta_t \leftarrow \theta_{t-1} - \alpha \cdot \dfrac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon} \Leftrightarrow \theta_t \leftarrow \theta_{t-1} - \alpha_t \cdot \dfrac{m_t}{\sqrt{v_t} + \hat{\epsilon}}, \quad a_t = \alpha \cdot \dfrac{\sqrt{1 - \beta_2^t}}{1 - \beta_1^t}
\)

class Adam:
    def __init__(self, lr=0.001, beta1=0.9, beta2=0.999):
        self.lr = lr
        self.beta1 = beta1
        self.beta2 = beta2
        self.t = 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.t += 1
        lr_t  = self.lr * np.sqrt(1.0 - self.beta2**self.t) / (1.0 - self.beta1**self.t) 

        for key in params.keys():
            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)
adam = Adam()
adam.update(params, grads)
print(params)
```#결과#```
{'W1': array([[ 0.19900032, -0.49900016],
       [ 1.49900011,  0.30099992]]), 'b1': array([ 0.09900063, -0.19900032])}
````````````

■ Adam에서 편향 보정을 하는 이유는 \( m_t, v_t \)가 0으로 초기화되기 때문에 학습 초반에 \( m_t, v_t \) 값이 0에 가깝게 편향된다.

- 편향 보정을 하지 않고 하이퍼파라미터 \( \beta_1 = 0.9, \beta_2 = 0.99 \)를 사용한다면,

\[
\begin{align*}
\beta_1 &= 0.9, \quad \beta_2 = 0.99, \quad \epsilon = 10^{-8}, \quad m_0 = 0, \quad v_0 = 0 \\
m_t &= \beta_1 m_{t-1} + (1 - \beta_1) g_t \Rightarrow m_1 = 0 + 0.1 \cdot g_1 \\
v_t &= \beta_2 v_{t-1} + (1 - \beta_2) g_t^2 \Rightarrow v_1 = 0 + 0.01 \cdot g_1^2 \\
\theta_t &= \theta_{t-1} - \frac{\alpha m_t}{\sqrt{v_t} + \epsilon} \Rightarrow \theta_1 = \theta_0 - \frac{\alpha \cdot 0.1 \cdot g_1}{\sqrt{0.01 \cdot g_1^2} + \epsilon}
\end{align*}
\]

이렇게 \( v_1 \)이 여전히 0에 가까운 값이 되기 때문에 초기에 step size가 커져 최적화 경로에서 벗어나 최적값에 수렴하지 못할 수 있다.

■ 이는 Adam이 \( \beta \) < 1 값을 이용해 과거의 값들을 서서히 잊어가는 지수가중평균의 한 종류이기 때문이다.

- 데이터 포인트를 \( x_t \), \( v_0 = 0 \)이라 했을 때, 지수이동평균의 식은 \( v_t = \beta v_{t - 1} + (1 - \beta) x_t \)이며 \( \beta \)값이 1에 가까울수록 새로운 데이터가 추가되더라도 noise가 최소화되어 평균 값이 천천히 변해 큰 변동이 발생하지 않는다. 

- 이를 smoothing이라 하며, smoothing이 많이 필요한 경우 \( \beta \) 값이 커질수록 smoothing 결과가 원래의 데이터 포인트들에 비해 낮게 위치하게 된다. 이를 보정하고자 \( v_t \)에 \( 1 - \beta^t \)를 나눈다.

참고) Momentum을 이용한 최적화기법 - ADAM - 공돌이의 수학정리노트 (Angelo's Math Notes)

 

Momentum을 이용한 최적화기법 - ADAM - 공돌이의 수학정리노트 (Angelo's Math Notes)

 

angeloyeo.github.io

- 논문에서는 \(
v_t = (1 - \beta_2) \sum_{i=1}^{t} \beta_2^{t-i} \cdot g_i^2
\)의 기댓값을 계산했을 때, \(
\mathbb{E}[v_t] = \mathbb{E}[g_t^2] \cdot (1 - \beta_2^t) + \zeta
\)가 되어 \(
(1 - \beta_2^t)
\)로 \( v_t \)를 나눈 편향 보정된 \( \hat{v}_t \leftarrow v_t / (1 - \beta_2^t) \)를 제안했다. \( m_t \)의 경우도 이와 완전히 유사하므로 \( \hat{m}_t \leftarrow m_t / (1 - \beta_1^t) \)

- 편향 보정된 \(
\hat{m}_t, \quad \hat{v}_t
\)를 이용해 다시 \( \theta_1 \)을 계산하면 \[
\begin{align*}
\beta_1 &= 0.9, \quad \beta_2 = 0.99, \quad \epsilon = 10^{-8}, \quad m_0 = 0, \quad v_0 = 0 \\
m_t &= \beta_1 m_{t-1} + (1 - \beta_1) g_t \Rightarrow m_1 = 0 + 0.1 \cdot g_1 \\
v_t &= \beta_2 v_{t-1} + (1 - \beta_2) g_t^2 \Rightarrow v_1 = 0 + 0.01 \cdot g_1^2 \\
\hat{m}_t &= m_t / (1 - \beta_1^t) \Rightarrow m_1 / (1 - \beta_1) = \frac{0.1 \cdot g_1}{0.1} = g_1 \\
\hat{v}_t &= v_t / (1 - \beta_2^t) \Rightarrow v_1 / (1 - \beta_2) = \frac{0.01 \cdot g_1^2}{0.01} = g_1^2 \\
\theta_t &= \theta_{t-1} - \frac{\alpha \hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon} \Rightarrow \theta_1 = \theta_0 - \frac{\alpha \cdot g_1}{\sqrt{g_1^2} + \epsilon}
\end{align*}
\] 이렇게 편향 보정된 \(
\hat{m}_t, \quad \hat{v}_t
\)를 사용할 경우, 하지 않았을 때보다 step size가 커지지 않는 것을 볼 수 있다.

■ \( m_t \)가 momentum의 지수이동평균인 이유는 모멘텀의 속도 변수처럼 \( t = 1, 2, 3, \ldots \)에 따라 전개시키면 gradient를 누적하기 때문이다.

- 예를 들어, \( \beta_1 = \dfrac{1}{2} \)라고 하자. \( m_0 = 0, t = 0 \)에서 논문의 pseud code를 수행해 보면

\[
\begin{align*}
m_1 &= \dfrac{1}{2} m_0 + \dfrac{1}{2} g_1 = \dfrac{1}{2} g_1 \\
m_2 &= \dfrac{1}{2} m_1 + \dfrac{1}{2} g_2 = \dfrac{1}{2} \left(\dfrac{1}{2} g_1\right) + \dfrac{1}{2} g_2 = \dfrac{1}{4} g_1 + \dfrac{1}{2} g_2 \\
m_3 &= \dfrac{1}{2} m_2 + \dfrac{1}{2} g_3 = \dfrac{1}{2} \left(\dfrac{1}{4} g_1 + \dfrac{1}{2} g_2\right) + \dfrac{1}{2} g_3 = \dfrac{1}{8} g_1 + \dfrac{1}{4} g_2 + \dfrac{1}{2} g_3 \\
m_4 &= \dfrac{1}{2} m_3 + \dfrac{1}{2} g_4 = \dfrac{1}{2} \left(\dfrac{1}{8} g_1 + \dfrac{1}{4} g_2 + \dfrac{1}{2} g_3\right) + \dfrac{1}{2} g_4 = \dfrac{1}{16} g_1 + \dfrac{1}{8} g_2 + \dfrac{1}{4} g_3 + \dfrac{1}{2} g_4 \\
& \vdots
\end{align*}
\]

이렇게 \( m_t \)는 momentum과 같이 gradient를 누적하는데 지수이동평균으로 기울기 반영 규모를 과거 기울기는 기하급수적으로 감소시켜 반영하고 새로운 기울기는 크게 반영해서 누적한다.

■ \( v_t \)가 RMSProp의 지수이동평균인 이유도 마찬가지이다. \( v_t \)는 다음과 같이 graident의 제곱을 누적해서 더허가 때문에

- 예를 들어, \( \beta_2 = \dfrac{1}{2} \)라고 하자. \( v_0 = 0, t = 0 \)에서 논문의 pseud code를 수행해 보면\[
\begin{align*}
v_1 &= \frac{1}{2} g_1^2 \\
v_2 &= \frac{1}{2} v_1 + \frac{1}{2} g_2^2 = \frac{1}{2} \left( \frac{1}{2} g_1^2 \right) + \frac{1}{2} g_2^2 = \frac{1}{4} g_1^2 + \frac{1}{2} g_2^2 \\
v_3 &= \frac{1}{2} v_2 + \frac{1}{2} g_3^2 = \frac{1}{2} \left( \frac{1}{4} g_1^2 + \frac{1}{2} g_2^2 \right) + \frac{1}{2} g_3^2 = \frac{1}{8} g_1^2 + \frac{1}{4} g_2^2 + \frac{1}{2} g_3^2 \\
v_4 &= \frac{1}{2} v_3 + \frac{1}{2} g_4^2 = \frac{1}{2} \left( \frac{1}{8} g_1^2 + \frac{1}{4} g_2^2 + \frac{1}{2} g_3^2 \right) + \frac{1}{2} g_4^2 = \frac{1}{16} g_1^2 + \frac{1}{8} g_2^2 + \frac{1}{4} g_3^2 + \frac{1}{2} g_4^2 \\
&\vdots
\end{align*}
\]
이렇게 \( v_t \)는 RMSProp의 \( h \)와 같이 gradient \( \cdot \) gradient 값을 누적하며 지수이동평균으로 \( m_t \)처럼 과거의 값들을 서서히 잊어가고 새로운 값은 크게 반영해서 누적한다. 

■ \(
\dfrac{1}{20} \cdot (x^2 + y^2)
\)에 Adam을 적용할 경우

갱신 과정이 momentum처럼 공이 그릇 바닥을 구르듯 움직이지만 RMSProp처럼 step size를 조정하기 때문에 momentum에 비해 흔들림의 정도가 작은 것을 볼 수 있다.

■ Adam은 이렇게 momentum과 RMSProp이 결합되었기 때문에, 문제의 차원을 고려해 RMSProp처럼 step을 조정하고 momentum 처럼 올라가는 방향이면 감속, 내려가는 방향이면 가속을 붙여 최적값에 수렴하게 된다. 

 

참고) Optimizers: The Secret Sauce of Deep Learning | by Neha Purohit | 𝐀𝐈 𝐦𝐨𝐧𝐤𝐬.𝐢𝐨 | Medium

 

Optimizers: The Secret Sauce of Deep Learning

Contents:

medium.com

 

'딥러닝' 카테고리의 다른 글

단어의 의미를 파악하는 방법 (1)  (0) 2024.11.20
매개변수 갱신 방법(2)  (0) 2024.11.03
합성곱 신경망(CNN) (1)  (0) 2024.11.01
오차역전파 (3)  (0) 2024.09.24
신경망 학습(2)  (1) 2024.09.13