1. Optimizer
Neural Network 에서는 특정 기능을 수행하기 위해 Loss 값을 최소화하는 방향으로 학습한다. 이때 우리는 Loss를 낮추기 위해 각 Weight의 Gradient를 구했었다. 구한 Gradient를 바탕으로 Weight를 조정하는 과정을 Opimization 과정이라한다.
각 Optimizer의 방법은 극단적이진 않지만, 학습 효율이나, 정확성에 대해 유의미한 차이를 드러낸다.
모든 모델에 학습이 진행되기에 Optimizer을 재대로 이해하는 것은 매우 중요하다.
2. Stochastic Gradient Descent(SGD)
Gradient Descent는 다변수 미분 방정식 이론에 근거하여 Neural Network의 Weight를 조정하는 방법이다. Gradient Descent에서는 Loss Function 을 최소화하는 기울기를 찾아 Weight값을 갱신한다.
그렇다면 왜 앞에 Stochastic(확률론적) 이라는 말이 추가되었을까?
Loss Function을 계산할때 Train Dataset 전체를 사용하여 계산할 경우 전체 데이터에 대해 계산을 진행해야 하므로, 너무 많은 계산양을 필요로 한다. 이러한 문제점을 해결하기 위해 일부 데이터만을 이용하여 학습을 진행하는 mini-batch 학습방법을 진행한다. batch(전체 데이터)에서 mini-batch를 추출하는 과정에서 확률적으로 추출하기에 우리는 Stochastic Gradient Descent라 한다.
그림을 수식으로 보면 다음과 같다.
$X_{n+1} = X_n - \eta \nabla f(X_n)$
그렇다면 Gradient를 왜 빼는 것일까?
우리는 Loss 값을 최소화 하는 것이 목적이다. 따라서 Gradient를 빼주어야 optimal minimum값을 향해 weight값이 update된다.
앞에 붙은 $\eta$는 무엇인가?
learning rate는 $\alpha , \eta$등 다양한 매개변수로 표현되며, Weight를 얼마나 update할지 결정해주는 hyper-parameter이다.
아주 간단한 예제를 통해 깊이 이해해보자. $f(x,y) = x^2+y^2+xy-4x-8y\ ,\ \eta=0.5$ (0,0)에서 학습이 두번 이루어진 결과는 다음과 같다.
$\nabla f(x,y) = (2x+y-4,2y+x-8)$
$\nabla f(0,0) = (-4,-8), \nabla f(2,4) = (4,2)$
$x_1 = (0,0) - 0.5(-4, -8) = (2,4)\ , \ x_2 = (2,4) - 0.5(4,2) = (0,3)$
(0,0)에서 출발하여 학습이 한번 이루어졌을 때 (2,4)가 되고, 한번 더 학습하면 (0,3)이 된다.
이를 코드로 보면 다음과 같다.
class SGD:
def __init__(self, lr = 0.01):
self.lr = lr
def update(self, params, grads):
for key in params.keys():
param[key] -= self.lr * grads[key]
SGD의 문제점은 무엇일까?
첫번째 문제점은 local minimum이 되거나, saddle point 가 되면, Zero Gradient가 되어 더이상 학습이 이루어지지 않는다. 즉 local optima에 안착한다는 단점이 존재한다. 이렇게 되면 초기값에 의해 최종 weight가 결정된다는 단점을 초래한다.
$f(x) = x(x-1)(x-2)(x-4)$을 0, 1, 2, 4로 초기화 했을때의 경우를 각각 살펴보면 다음과 같다. 즉 초기 값에 따라 optimal minimum에 못하기도 한다.
두번째 문제점은 SGD 는 학습이 이루어질 때 gradient의 반대 방향으로 학습이 이루어지기에, 등위선의 수직 방향으로 학습이 이루어져 zigzag 형태로 비효율적인 학습이 이루어진다.
위의 그림을 보면 x평면과 y평면의 가중치가 다를때, 등위선의 수직 방향으로 학습이 이루어지기에, zigzag가 발생하여 효율적인 학습이 안이루어 지는 것을 볼 수 있다.
3. Momentum
Momentum은 기존 Gradient값에 현재의 관성을 추가한 아이디어이다. 쉽게 설명하면, 물리계에서 공이 굴러가는 방향은 중력뿐만 아닌, 기존의 관성에도 영향을 받는다.
Momentum을 이용하면 위에서 언급한 SGD의 문제 2가지를 해결할 수 있다.
종종 극소점에 빠졌다가 관성의 힘으로 빠져나올 수 있다.
관성에 의해 효율적으로 학습된다.
수식을 통해 자세히 알아보자. $v_n$은 속도, $v_{n_1}$은 관성, $\alpha$는 관성계수이다.
$v_n = \alpha v_{n-1} - \eta \nabla f(x_n) \ , v_{-1}=0$
$x_{n+1} = x_n + v_n$
기존의 SGD와 다른점을 살펴보면, $\eta \nabla f(x_n)$만 빼는 것이 아닌, $\alpha$를 더한 값을 뺀다. 이로써 훨씬 빠르게 학습될 수 있다.
예시를 통해 자세히 이해해보자. $f(x,y) = x^2 + xy$, $x_0 = (1,1)$, $\eta = 1$, $\alpha=1$
$\nabla f(x,y) = (2x+y,x)$
$v_0 = -\eta \nabla(x_0) = (-3, -1),\ \ x_1 = x_0 + v_0 = (1,1) + (-3,-1) = (-2,0) $
$v_1 = \alpha x_0 - \eta \nabla f(x_1) = (-3,-1) - (-4, -2) = (1,1),\ \ x_2 = x_1 + v_1 = (-2,0) + (1,1)$
다음과 같이 다음 weight값을 계산하는데 velocity값이 반영되는 것을 확인할 수 있다.
이를 코드로 보면 다음과 같다.
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]
4. Nesterov Accelated Gradient(NAG)
NAG는 momentum방식과 유사하다. 하지만 momentum을 더욱 공격적인 방식으로 변형한다. 이전 관성에 현재 Gradient값의 반대 방향이 아닌, 현재 위치에서 관성 방향으로 움직인 Gradient의 반대방향을 합친다.
Momentum update의 경우 현재 위치에서 Gradient의 반대 방향으로 이동을 하는데 반해, Nesterov Momentum의 update경우 Velocity 만큼 이동한 위치에서 Gradient를 구하여 다음 step을 찾는다. 이러한 방법 때문에, 더욱 공격적이라고 얘기한다.
수식은 다음과 같다.
$v_n = \alpha v_{n-1} - \eta \nabla f(x_n+\alpha v_{n-1}), \ \ v_{-1}=0$
$x_{n+1} = x_n + v_n$
그런데 수식을 보면 이동한 위치의 gradient를 구한다. 우리는 더욱 효율적인 gradient를 구하는 방식을 찾고 싶은데, 오히려 미분 방정식의 내부가 더욱 복잡해진 것이다. 즉 $x_n$이 아닌 다른점에서 gradient를 구하기 때문에 구현에 적합하지 않다.
이러한 문제를 해결하기 위해 우리는 Bengio 근사 방식을 사용한다.
$x_n$에서 momentum step후의 위치를 $x'_n$이라 한다면 수식은 다음과 같다.
$x'_{n+1} = x'_n - \eta \nabla f(x'_n) + \alpha v_n$
$\ \ \ \ \ \ \ \ = x'_n - \eta \nabla f(x'_n) + \alpha (\alpha v_{n-1} - \eta \nabla f(x'_n))$
$\ \ \ \ \ \ \ \ = x'_n - (1+\alpha ) \eta \nabla f (x'_n) + \alpha ^2 v_{n-1}$
$ v_n = \alpha v_{n-1} - \eta \nabla f(x'_n),\ \ \ x'_0 = x_0$
이를 의미적으로 이해하면 다음과 같다.
원래 기존에는 파란점을 기준으로 $x'_n$에서 gradient값을 구해 다음 파란점으로 넘어갔는데, 이제는 빨간점을 기준으로 gradient를 구해 다음 빨간 빨간점으로 넘어간다.
5. AdaGrad
위에서 보았던 SGD, Momentum, NAG의 경우 learning rate가 고정되어 있었다. 실제로 인간이 학습할때 보면, 처음에 배우는 양이 가장 많고, 이후 시간이 지날수록 새로 습득하는 정보량은 적어진다. 이러한 아이디어를 optimizer에 적용한 optimizer가 Adaptive Gradient이다. AdaGrad는 다음 두가지를 기준으로 learning rate $\eta$를 결정한다.
1. 시간이 지날수록 learning rate의 값은 작아진다.
2. 편미분 값이 큰 변수의 learning rate는 크게 작아지고, 편미분 값이 작은 변수의 learning rate는 소폭 감소한다.
두번째 조건의 경우 이해하기가 힘들 수 있다. 예시를 보고 이해해보자.
현재 위치가 $(2,2)$이고 학습률이 $1$일때 $\nabla f (2,2) = (1,0.1)$이라고 가정하자.
경사하강법을 적용하면 $(2,2)-(1,0.1) = (1,1.9)$가 된다. 이를 통해 우리는 손실함수의 값이 작아지길 기대한다. 이걸 우리는 인공 신경망의 성능이 좋아진다고 표현하고, 의인화해서 학습된다고 표현하는 것이다.
그런데 $x$의 변화량은 큰데, $y$의 변화량은 작다. 따라서 $x$가 더 많이 학습되었다고 표현한다.
점화식을 보면 다음과 같다.
$h_n = h_{n-1} + \nabla f(x_n) \odot \nabla f(x_n)$
$x_{n+1} = x_n - \eta \frac{1}{\sqrt{h_n}} \odot \nabla f(x_n)$
전체적인 수식은 SGD와 동일한데, learning rate가 $\eta$와 $h_n$을 이용하여 계산된다.
즉 $h_n$의 값을 계속 커지며, 나중에는 learning rate가 매우 낮아져 학습이 진행되지 않는다.
$\odot$는 $hadamard\ product$ 라는 연산자인데, element wise하게 곱해주는 역할을 수행한다.
이때 $h_n$는 이렇게도 표현이 가능하다.
$h_n = \sum _{k=0} ^n \nabla f(x_k) \odot \nabla f(x_k)$
learning rate를 능동적으로 변환한다는 측면에서는 효과적인 접근 방식이지만, step이 많이 진행되면 누적 $h_n$이 커져 학습률이 작아져 학습이 거의 되지 않는다.
예시를 통해 더욱 자세히 이해해보자.
$f(x,y) = x^2+xy,\ \ x_0=(1,1), \ \ \eta =\frac{1}{2}$
$\nabla f(x,y) = (2x+y, x)$
$\nabla f(1,1) = (3,1),\ \ h_0=\nabla f(x_0) \odot \nabla f(x_0) = (3,1)\odot (3,1) = (9,1)$
$x_1 = x_0 - \eta \frac{1}{\sqrt{h_0}} \odot \nabla f(x_0) = (1,1) - \frac{1}{2} (\frac{1}{3} , 1) \odot (3,1) = (\frac{1}{2} , \frac{1}{2} ) $
$x_2 = x_1 - \eta \frac{1}{h_1} \odot \nabla f (x_1) = (\frac{1}{2} , \frac{1}{2}) - \frac{1}{2} (\frac{2}{3\sqrt{5}}, \frac{2}{\sqrt{5}}) \odot (\frac{3}{2}, \frac{1}{2})=(\frac{1}{2} - \frac{1}{2\sqrt{5}} , \frac{1}{2} - \frac{1}{2\sqrt{5}})$
즉 학습이 진행되면서 learning rate가 계속해서 update되는 것을 확인할 수 있다.
코드를 보면 다음과 같다.
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)
여기서 $1e-7$을 마지막에 더해주는 이유는 분모값이 0이 되어 에러를 방지하기 위함이다.
6. RMSProp
RMSProp은 기존 AdaGrad의 이후 학습이 잘 되지 않는다는 단점을 해결하기 위해 나온 optimizer이다.
이를 보완하기 위해 RMSProp는 이전의 누적치와 함께 현재 gradinet에 제곱별 가중치의 평균을 반영한다.
점화식을 보면 다음과 같다.
$h_n = \gamma h_{n-1} +(1- \gamma ) \nabla f(x_n) \odot \nabla f(x_n), \ \ \ h_{-1}=0$
$x_{n+1} = x_n - \eta \frac{1}{\sqrt{h_n}} \odot \nabla f(x_n)$
즉 전체적인 형태는 AdaGrad와 유사하지만, 이전 가중치 값에 $\gamma$를 사용하여 반영해주는 것을 확인할 수 있다.
$forgetting\ factor\ :\ \gamma$가 클수록 과거가 중요하다.
7. Adam
Adam은 여태까지 고안된 Optimizer을 활용하여 제시한 최적의 Optimizer이다. Momentum과, RMSProp 두가지를 섞어 만든 알고리즘으로, 진행하던 속도에 관성을 주고, 최근 경로의 곡면의 변화량에 따른 Adaptive Learning rate를 가지는 알고리즘이다. 두가지 방식이 합쳐지기에 복잡하다. 하지만 Adam은 넓은 범위의 아키텍쳐를 가진 신경망에서 잘 작동하여, 가장 많은 범위에서 사용되고 있다.
먼저 Momentum을 가중치 $\beta _1$으로 변형하여 점화식을 다음과 같이 세울 수 있다.
$m_n = \beta _1 m_{n-1} + (1- \beta _1) \nabla f(x_n)$
이후 AdaGrad를 변형하여 가중치 $\beta _2$로 다음과 같이 점화식을 세울 수 있다.
$v_n = \beta _2 v_{n-1} + (1- \beta _2 ) \nabla f (x_n) \odot \nabla f (x_n)$
그렇다면 이를 바로 적용하면 될까?
보통 $\beta$를 거의 1에 가까운 값을 사용하기 때문에, 초기 값인 0에서 크게 벗어나지 못한다. 따라서 이를 보정한 값을 사용한다.
보정한 값은 다음과 같다.
$\hat m_n = \frac{m_n}{1-\beta _1 ^{n+1}},\ \ \ \hat v_n = \frac{v_n}{1-\beta _2 ^{n+1}}$
따라서 이를 바탕으로 최종 Adam의 식은 다음과 같다.
$x_{n+1} = x_n - \eta \frac{1}{\sqrt{\hat v_n}} \odot \hat m_n$
'공부 > 머신러닝' 카테고리의 다른 글
RNN : Recurrent Neural Networks (0) | 2024.04.29 |
---|---|
Weight Initialization (0) | 2024.03.25 |
Convolution Neural Network (0) | 2024.03.19 |
Activation Function (1) | 2024.03.17 |
Backpropagation (0) | 2024.03.17 |