0%

11. 인공신경망 최적화 - Optimizer

해당 시리즈는 프로그래밍 언어 중 하나인 줄리아(Julia)로 딥러닝(Deep learning)을 구현하면서 원리를 설명합니다.


인공신경망 최적화란

무작위값으로 가중치와 편향이 주어진 인공신경망은 바보와 같다. 따라서 신경망은 학습을 통해서 데이터에 적절한 가중치와 편향 값을 찾아야 한다. 인공신경망 최적화란 데이터에 따라 더 적절한 학습 방법을 찾는 과정을 의미한다. 즉, 적절한 매개 변수 값을 찾기 위한 학습을 좀 더 효율적으로 하고 싶은 사람들이 만든 기술이라는 것이다. 이번 글부터는 최적화에 대해서 다룰 것이며, 어떤 부분들을 최적화할 수 있는지 알아보자.

신경망 학습을 최적화하기 위해서는 몇 가지의 설정이 필요하다. 필요한 설정은 다음과 같다.

  • 미분을 어떻게 구할 것인가? (순전파 또는 역전파)
  • 매개 변수 갱신을 어떻게 할 것인가? (optimizers)
  • 가중치의 초기값을 어떻게 설정할 것인가? (std, Xavier, He)
  • 오버피팅을 어떻게 막을 것인가? (가중치 감소 또는 Dropout)
  • 배치 정규화를 사용할 것인가?

위의 질문 중에서 첫 번째는 이전 글들에서 직접 확인하였다. 따라서 첫 번째 질문에 대한 답변은 순전파 알고리즘역전파 알고리즘으로 대신할 것이다.

다음 질문은 매개 변수 갱신 방법에 대한 논의이며, 이번 글에서 우리가 다룰 주제이다.

Optimizers

옵티마이저(Optimizer)는 도출된 미분 값을 어떻게 계산하여 적용하는지를 다루는 기법이다. 종류는 매우 다양하지만 이 글에서는 ‘SGD’, ‘Momentum’, ‘AdaGrad’, ‘RMSProp’, ‘Adam’만 다룰 것이다. 몇몇 옵티마이저는 갱신할 때 이전 값들을 필요로 한다. 따라서 옵티마이저 구조체를 설정하여 값을 보관해야 한다.

1
2
3
4
5
6
7
8
mutable struct optimizers
v
h
m
iter
end

optimizer = optimizers(0,0,0,0)

SGD

SGD는 ‘Stochastic Gradient Descent’의 약자로서 한국어로 ‘확률적 경사 하강법’이라고도 불린다. SGD 수식은 다음과 같다.

위 식을 분석해보면 SGD는 매개 변수의 편미분 값에 학습률 $\eta$를 곱한 뒤, 기존 매개 변수에서 뺀다. 계산된 결과를 다시 매개 변수로 갱신한다.

위의 식을 코드로 구현하면 다음과 같다.

1
2
3
4
5
6
7
function SGD(params,grads)

for key in keys(params)
params[key] -= learning_rate * grads[key]
end
return params
end

SGD()는 기존 매개 변수가 있는 params와 편미분값이 저장된 grads를 인수로 받는다. 이후 위 수식처럼 계산을 하여 다시 params를 갱신한 후 결과로 내보낸다.

Momentum

Momentum(이하 모멘텀)은 운동량을 뜻하는 단어로 물리에서 사용하는 원리가 추가되었다. 보통 물리에서 모멘텀은 어떤 물체가 한 방향으로 지속적으로 변동하려는 경향을 뜻한다. 여기서도 같은 맥락으로 사용되었다. 모멘텀의 수식은 다음과 같다.

위 수식에서 확인할 수 있듯이 모멘텀은 SGD와는 다르게 새로운 변수 $v$가 추가되었다. $v$는 물리에서 속도와 같은 의미이다. 즉, 모멘텀이 한 방향으로 지속하려는 경향을 숫자로 나타낸 것이 $v$인 것이다. 따라서 기울기의 각도가 수직적일수록 속도는 증가하며, 수평적일수록 속도는 감소한다.

모멘텀은 $v$로 인해 로컬 미니멈에 도착하여도 그 공간을 벗어날 수 있다. 이는 로컬 미니멈에 도착하면 기울기가 0이 되어 멈추는 SGD의 단점을 보완한 기법이기도 하다.

모멘텀을 코드로 구현하면 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Momentum(params,grads)

momentum = 0.

if optimizer.v == 0
optimizer.v = Dict()
for key in keys(params)
optimizer.v[key] = zeros(size(params[key]))

end
end

for key in keys(params)
optimizer.v[key] = (optimizer.v[key].* momentum) - (learning_rate * grads[key])
params[key] += optimizer.v[key]
end
return params
end

Momentum()도 SGD와 같이 paramsgrads를 인수로 받은 후 계산하여 params를 갱신한 후 결과로 내보낸다.

AdaGrad

매개 변수를 갱신함에 있어서 가장 중요한 부분 중 하나는 ‘학습률’이다. SGD나 모멘텀에서는 학습률 $\eta$가 상수로 사용되었다. 하지만 AdaGrad에서는 학습률이 변수로서 사용되며, 최솟점에 다다를수록 학습률이 감소한다. AdaGrad를 수식으로 나타내면 다음과 같다.

위 수식에서 $\odot$는 ‘아다마르 곱(Hadamard product)’이다. 아다마르 곱은 동일한 크기의 행렬 두 개를 원소별로 곱한다. 따라서 첫 번째 수식은 해당 매개 변수의 미분값을 원소 별로 곱셈한 후, 기존의 $h$와 더하여 $h$를 갱신한다. 이렇게 계산된 $h$는 매개 변수를 갱신하는 두 번째 수식으로 들어가며, $\frac{1}{\sqrt{h}}$는 $h$가 클수록 작아진다. 따라서 학습이 진행되면서 $h$는 점점 커지고, 학습률은 점점 작아진다.

AdaGrad를 코드로 구현하면 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function AdaGrad(params,grads)

if optimizer.h == 0
optimizer.h = Dict()
for key in keys(params)
optimizer.h[key] = zeros(size(params[key]))
end
end

for key in keys(params)
optimizer.h[key] += grads[key] .* grads[key]
params[key] -= (learning_rate * grads[key]) ./ (optimizer.h[key].^(1/2).+ 1e-7)
end
return params
end

RMSProp

RMSProp는 AdaGrad의 단점을 보완한 기법이다. AdaGrad는 변수 $h$가 지속적으로 증가하면서 이동 속도를 줄이며, 단순한 볼록 형태를 가진 환경에서는 최소점을 잘 찾을 수 있다. 하지만 로컬 미니멈이 존재하는 비볼록 형태의 환경에서는 로컬 미니멈에서 벗어나기 어렵다. 이런 부분을 보완하고자 감소하는 속도를 나타내는 새로운 상수 $\beta$를 대입하여 로컬 미니멈에서 벗어날 수 있도록 수정한 것이 RMSProp 기법이다. 이를 수식으로 보면 다음과 같다.

위 수식에서 확인할 수 있듯이 감소 속도인 $\beta$가 추가된 것 외에는 AdaGrad와 동일하다. 보통 $\beta$는 0.9를 사용한다. 이를 코드로 구현하면 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function RMSProp(params,grads)

beta = 0.9

if optimizer.h == 0
optimizer.h = Dict()
for key in keys(params)
optimizer.h[key] = zeros(size(params[key]))
end
end

for key in keys(params)
optimizer.h[key] = (beta * optimizer.h[key]) + (1.0 - beta)*(grads[key] .* grads[key])
params[key] -= (learning_rate * grads[key]) ./ (optimizer.h[key].^(1/2).+ 1e-7)
end
return params
end

Adam

Adam은 ‘Adaptive moments’의 약자로 모멘텀과 RMSProp를 섞은 형태의 기법이다. Adam에는 총 2개의 변수가 등장하는데, 모멘텀의 속도 원리가 적용된 변수 $m$과 RMSProp에서 감소 속도 원리가 적용된 변수 $v$이다. 또한 RMSProp에서 사용했던 감소 속도 상수인 $\beta$도 사용된다. 보통 $m$에서는 $\beta_1$이 사용되며 $v$에서는 $\beta_2$가 사용된다. 수식은 다음과 같다.

보통 $\beta_1$은 0.9를 사용하고 $\beta_2$는 0.999를 사용한다. Adam은 학습률로 보통 0.01을 쓰는 다른 기법과 다르게 학습률을 0.001을 사용한다. 이를 코드로 구현하면 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Adam(params,grads,learning_rate = 0.001)

beta1 = 0.9
beta2 = 0.999

if optimizer.m == 0
optimizer.m = Dict()
optimizer.v = Dict()
for key in keys(params)
optimizer.m[key] = zeros(size(params[key]))
optimizer.v[key] = zeros(size(params[key]))
end
end

optimizer.iter += 1
lr_t = learning_rate * (1.0 - beta2^(optimizer.iter))^(1/2) / (1.0 - beta1^(optimizer.iter))

for key in keys(params)
optimizer.m[key] += (1 - beta1) * (grads[key] - optimizer.m[key])
optimizer.v[key] += (1 - beta2) * (grads[key].^2 - optimizer.v[key])
params[key] -= (lr_t * optimizer.m[key]) ./ ((optimizer.v[key]).^(1/2) .+ 1e-7)
end
return params
end

옵티마이저 비교

지금까지 총 5가지의 옵티마이저 기법들을 살펴보았다. 이제 MNIST데이터를 사용하여 각 기법들을 비교해보자.

비교하기 위해 사용할 신경망은 2층 구조이며, 활성화 함수로는 ReLU, 미분은 역전파 알고리즘을 사용할 것이다. 각 옵티마이저 별로 3에폭씩 학습하였다. 우리가 비교할 부분은 크게 3가지인 시간, 정확도, 손실 함수이다. 시간은 적게 걸릴 수록, 정확도는 높을 수록, 손실 함수는 낮을 수록 더 좋은 알고리즘이다.

WARNING
이 글에서 진행하는 성능 비교는 2층 신경망이며, MNIST데이터를 기반으로 한 분류 모델이다. 다른 모델에서 성능은 이 글의 결과와 차이가 있을 수 있다.

먼저 5가지의 시간부터 확인해보자.

1
2
3
4
5
SGD: 46.004452 seconds
Momentum: 47.702307 seconds
AdaGrad: 49.991280 seconds
RMSProp: 49.674666 seconds
Adam: 50.698803 seconds

대부분 45초~50초 사이의 결과가 나왔다. 각 기법 별로 살펴보면 SGD가 가장 시간이 적게 걸리고 Adam이 가장 오래걸리는 것을 확인할 수 있다. 수식과 시간을 대조하여 확인해보면 수식이 복잡할수록 시간이 오래 걸리는 것을 확인할 수 있다.

다음으로는 정확도를 나타낸 그래프이다.

옵티마이저 정확도

정확도에서는 SGD만 유일하게 90%를 넘지 못하였다. 또한 RMSProp가 다른 기법보다 더 높은 정확도를 도출하는 것을 확인할 수 있다.

다음으로는 손실 함수를 나타낸 그래프를 살펴보자.

옵티마이저 손실 함수

손실 함수 값 또한 RMSProp가 더 낮은 결과를 도출하는 것을 확인할 수 있다.