💡 'Deep Learning from Scratch'와 'CS231N'을 참고하여 작성
신경망(neural network)의 학습 목적은 손실 함수(loss function)의 값을 최대한 낮추는 매개변수(parameter)를 찾는 것이었습니다. 이는 곧 매개변수의 최적값을 찾는 문제이며, 이를 최적화 문제(optimization)라 합니다.
최적의 매개변수를 찾기 위해서는 학습 데이터들을 이용해 기울기(gradient)의 값을 구하고, 그 값을 기준으로 나아갈 방향을 결정해야 합니다. 이번 게시물에서는 최적의 매개변수를 찾는 방법인 옵티마이저에 대해 알아보겠습니다.
1. 기울기
학습 데이터를 이용하여 기울기를 구하기 위해서는 각 변수들에 대한 편미분을 동시에 계산해야 합니다.
$$ f(x_0, x_1) = x_0^2 + x_1^2 $$
위의 공식에서 $ x_0 = 3, x_1 = 4 $일 때, ($ x_0, x_1 $)의 양쪽의 편미분을 묶어서 ($ \frac{\partial{f}}{\partial{x_0}}, \frac{\partial{f}}{\partial{x_1}} $)로 계산합니다. ($ \frac{\partial{f}}{\partial{x_0}}, \frac{\partial{f}}{\partial{x_1}} $)처럼 모든 변수의 편미분을 벡터로 정리한 것을 기울기라고 합니다. 기울기를 구하는 함수는 아래와 같이 구현할 수 있습니다.
def numerical_gradient_no_batch(f: Callable, x: np.array) -> np.array:
h = 1e-4
# x와 같은 형상의 0으로 구성된 배열 생성
grad = np.zeros_like(x)
for idx in range(x.size):
tmp_val = x[idx]
# f(x+h) 계산
x[idx] = float(tmp_val) + h
fxh1 = f(x)
# f(x-h) 계산
x[idx] = tmp_val - h
fxh2 = f(x)
grad[idx] = (fxh1 - fxh2) / (2 * h)
x[idx] = tmp_val # 값 복원
return grad
기울기의 결과에 마이너스를 붙인 벡터를 그려보면, 기울기가 의미하는 것을 더 잘 이해할 수 있습니다. 아래의 그림 1은 방향을 가진 벡터(화살표)로 그려졌습니다. 이는 마치 함수의 '가장 낮은 장소(최솟값)'를 가리키는 것 같습니다. 또한, '가장 낮은 곳'에서 멀어질수록 화살표의 크기가 커진다는 것 역시 확인할 수 있습니다.
기울기는 각 지점에서 낮아지는 방향을 가리킵니다. 정확히 표현하면, 기울기가 가리키는 쪽은 각 장소에서 함수의 출력 값을 가장 크게 줄여주는 방향이라고 할 수 있습니다.
2. 옵티마이저
2.1 옵티마이저란?
옵티마이저는 '최적을 행하는 자'라는 뜻의 단어입니다. 기계학습(machine learning)의 학습 단계에서 최적의 매개변수를 찾아내는 것이 옵티마이저의 역할입니다. 여기서 최적이란, 손실 함수가 최솟값이 될 때의 매개변수 값을 의미합니다. 그러나 일반적인 문제의 손실 함수는 매우 복잡합니다. 이런 상황에서 기울기를 잘 이용하여 함수의 최솟값을 찾는 것이 바로 옵티마이저입니다.
여기서 주의할 점은 각 지점에서 함수의 값을 낮추는 방안을 제시하는 지표가 기울기라는 것입니다. 복잡한 함수에서 기울기가 가리키는 곳이 정말로 나아가야 할 방향인지를 보장할 수 없습니다. 실제로 함수가 복잡할수록 기울기가 가리키는 방향에 최솟값이 없는 경우가 대부분입니다. 안장점(saddle point)과 지역 최적해(local minima)가 그 대표적인 사례입니다.
안장점
어느 방향에서 보면 극대값이고 다른 방향에서 보면 극솟값이 되는 점.
지역 최적해
주위의 모든 점의 함수값보다는 이하의 값을 갖는 점이지만, 모든 실현 가능한 함숫값보다는 더 클 수 있는 점.
이처럼 기울어진 방향이 꼭 최솟값을 가리키는 것은 아니지만, 그 방향으로 가야 함수의 값을 줄일 수 있습니다. 그래서 전역 최솟값(global minimum)이 되는 장소를 찾는 문제에서는 기울기의 정보를 바탕으로 나아가야할 방향을 정해야 합니다. 여기서 등장하는 방법이 옵티마이저의 시초인 경사하강법(gradient descent)입니다.
2.2 Stochastic Gradient Descent (SGD)
경사하강법은 현 위치에서 최솟값을 향해 일정 거리만큼 이동합니다. 이동한 곳에서 다시 기울기를 구하고 또 최솟값을 향해 나아가기를 반복합니다. 최솟값 도달이라는 목표를 달성하기 위해 경사하강법은 계속해서 매개변수를 갱신합니다. 이때, 데이터를 미니배치(mini-batch)로 랜덤하게 선정하는 경사하강법을 SGD라고 부릅니다. 이는 '확률적(stochastic)으로 무작위하게 골라낸 데이터'에 대해 수행하는 경사하강법이라는 의미를 가지고 있습니다. SGD의 수식은 아래와 같이 작성할 수 있습니다.
$$ \theta_{t} = \theta_{t-1} - \eta \triangledown J(\theta_{t-1}) $$
- $ \theta $ : 매개변수
- $ \eta $ : 학습률(learning rate)
- $ \triangledown J(\theta) $ : 손실 함수의 기울기
학습률
갱신하는 양. 한 번의 학습으로 얼마만큼 학습해야 할지, 즉 매개변수 값을 얼마나 갱신하는 지를 정함
이를 구현하면 아래와 같습니다.
class SGD:
def __init__(self, lr: float=0.01) -> None:
self.lr = lr
def update(self, params: dict, grads: dict) -> None:
for key in params.keys():
params[key] -= self.lr * grads[key]
SGD는 단순하고 구현하기 쉽지만, 문제에 따라서 비효율적인 경우가 있습니다. 아래의 수식에 SGD를 적용하여 최솟값을 구하는 문제를 생각해 보겠습니다.
$$ f(x,y)=\frac{1}{20}x^2+y^2 $$
탐색을 시작하는 초기값을 $ (x, y) = (-8.0, 2.0) $으로 하겠습니다. 이를 시각화하면 아래와 같습니다.
SGD는 그림 3과 같이 심하게 굽이져 비효율적인 움직임을 보여줍니다. SGD의 단점은 비등방성(anisotropy) 함수에서는 탐색 경로가 비효율적이라는 것입니다. SGD 같이 무작정 기울어진 방향으로 진행하는 단순한 방식을 사용하기보다 이를 개선할 방법이 필요했습니다.
비등방성 함수
방향에 따라 성질(여기서는 기울기)이 달라지는 함수
이외에도 SGD는 지역 최적해와 안장점에서 벗어나지 못하는 문제를 갖고 있습니다. 또한, 모든 파라미터에서 동일한 학습 보폭(step size)이 적용되기 때문에 파라미터가 변한 결과를 학습에 반영하지 못한다는 문제점이 있습니다. 이러한 문제를 개선하기 위해 새로운 옵티마이저들이 등장했습니다.
2.3 SGD + Momentum
모멘텀(Momentum)은 '운동량'을 뜻하는 단어로 물리와 관계가 있습니다. SGD가 지역 최적해와 안장점에서 벗어나지 못하던 문제를 개선하기 위해 SGD에 모멘텀의 개념을 추가하였습니다. 모멘텀은 다음과 같이 수식을 작성할 수 있습니다.
$$ v_t = \gamma v_{t-1}-\eta\triangledown J(\theta_{t-1}) $$
$$ \theta_{t} = \theta_{t-1} + v_t $$
- $ v_t $ : 속도(velocity)
위 공식에서 $ \gamma v_t $는 물체가 아무런 힘을 받지 않을 때 서서히 하강시키는 역할을 합니다($ \gamma $는 0.9 등의 값으로 설정). $ \gamma $는 물리에서 지면 마찰이나 공기 저항에 해당합니다. 모멘텀의 속도 개념을 통해 최솟값에서 바로 멈추지 않고 '오버슈팅(Overshooting)'하여 지역 최적해와 안장점을 통과할 수 있었습니다. 모멘텀을 구현하면 아래와 같습니다.
class momentum:
def __init__(self, lr: float=0.01, momentum: float=0.9) -> None:
self.lr = lr
self.m = momentum
self.v = None
def update(self, params: dict, grads: dict) -> None:
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.m * self.v[key] - self.lr * grads[key]
params[key] += self.v[key]
코드에서 변수 v는 물체의 속도를 뜻합니다. 이를 시각화하면 아래 그림 5와 같습니다.
그림 5에서 갱신 경로가 SGD에 비하여 부드러워진 것을 확인할 수 있습니다. 이는 $ x $축의 힘은 아주 작지만 방향은 변하지 않아서 한 방향으로 일정하게 가속하기 때문입니다. 반대로 $ y $축의 힘은 크지만 위아래로 번갈아 받아서 상충하여 $ y $축 방향의 속도는 안정적이지 않습니다.
또한, 그림 5에서 전역 최솟값에서도 바로 멈추거나 늦춰지지 않고 지나쳐가는 것을 확인할 수 있습니다. 이는 지역 최적해나 안장점을 통과하는데는 유효하였지만, 전역 최솟값에서 빠르게 수렴하지 않아 학습 시간이 늘어난다는 문제로 연결됩니다.
2.4 Nesterov Accelerated Gradient (NAG)
모멘텀은 갱신 과정에서 현재의 기울기 값을 기반으로 다음 단계의 값을 도출하였습니다. 이는 관성에 의해 최적의 파라미터를 지나칠 수 있다는 문제로 이어집니다. NAG에서는 모멘텀으로 이동한 지점에서의 기울기를 활용하여 갱신하기 때문에 이러한 문제를 해결할 수 있었습니다. 이를 수식으로 표현하면 아래와 같습니다.
$$ v_t = \gamma v_{t-1}-\eta\triangledown J(\theta_{t-1}-\gamma v_{t-1}) $$
$$ \theta_{t} = \theta_{t-1} + v_t $$
모멘텀과 달리 $ v_t $를 구할 때, $ \triangledown J(\theta-\gamma v_{t-1}) $과 같이 관성에 의하여 이동한 곳의 기울기를 적용하는 것을 확인할 수 있습니다. 이는 관성에 의하여 빠르게 이동하는 이점을 적용하면서도 수렴해야 하는 곳에서 효과적으로 멈추는 결과를 만들었습니다. NAG와 모멘텀의 움직임을 비교하면 아래의 그림 6과 같습니다.
NAG는 아래와 같이 구현할 수 있습니다.
class momentum:
def __init__(self, lr: float=0.01, momentum: float=0.9,
nesterov : bool=False) -> None:
self.lr = lr
self.m = momentum
self.v = None
self.nesterov = nesterov
def update(self, params: dict, grads: dict) -> None:
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():
if self.nesterov: # NAG
self.v[key] = self.m * self.v[key] - self.lr * grads[key]
params[key] += (self.m * self.v[key] - self.lr * grads[key])
else: # vanila momentum
self.v[key] = self.m * self.v[key] - self.lr * grads[key]
params[key] += self.v[key]
기존의 모멘텀 클래스에 nesterov 여부를 추가하여 구현하였습니다. 이를 시각화한 결과는 아래와 같습니다.
모멘텀과 동일하게 오버슈팅을 통해 전역 최솟값을 지나치지만, 모멘텀에 비해 더 빠르게 수렴하는 것을 확인할 수 있습니다.
(스텝 사이즈를 개선한 옵티마이저는 다음 글에서 이어집니다)
참고자료 출처
- 하용호, "자습해도 모르겠던 딥러닝, 머리속에 인스톨 시켜드립니다.", https://www.slideshare.net/yongho/ss-79607172, 2017
- CS213N, "Convolutional Neural Networks for Visual Recognition", https://cs231n.github.io/neural-networks-3/, 2021
'기초 > 인공지능' 카테고리의 다른 글
학습과 관련된 기술들 (0) | 2021.09.10 |
---|---|
옵티마이저(Optimizer) (2/2) (0) | 2021.09.02 |
오차역전파(Back-Propagation) (0) | 2021.08.29 |
손실 함수(Loss function) (0) | 2021.08.28 |
활성화 함수(Activation function) (0) | 2021.08.26 |