1. 매개변수 갱신
■ 신경망 학습의 목적은 손실 함수의 값을 최대한 감소시키는 매개변수를 찾는, 즉 매개변수의 최적값을 찾는 문제이다.
■ 이러한 문제를 푸는 것을 최적화(Optimization)라고 한다.
■ 신경망은 매개변수의 수가 많아질수록 매개변수의 공간이 넓고 복잡해져서 바로 최적의 매개변수를 구해 손실 함수의 최솟값을 찾기 어렵다.
■ 앞서, 최적의 매개변수를 찾기 위해 매개변수의 기울기(미분)을 구해서, 기울어진 방향으로 매개변수 값을 갱신하는 작업을 반복해 조금씩 손실 함수의 최솟값에 다가가는 방법을 보았는데, 이 방법을 경사 하강법이라고 하며,
신경망 학습(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 \)는 그래디언트가 감소하는 속도를 나타낸 이동 벡터이다.
■ 여기서 \( \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 코드를 보면
- 첫 번째 모멘트와 두 번째 모멘트 그리고 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 |