“딥러닝 알아듣기” 시리즈는 딥러닝의 기초 지식을 저만의 방식으로 쉽게 풀어내는 시리즈입니다. 이번 챕터에서는 손실 함수를 최적화하는 다양한 기법에 대해서 알아봅니다.
앞선 챕터들에서 여러 문제들에 대한 딥 러닝 모델을 학습시키면서 많은 모델을 최적화했다. 또한 최대한 많은 데이터에 대해 모델의 성능을 높이기 위한 다양한 아이디어들에 대해서도 알아보았다. 여러 가지 기술들이 모델의 일반화를 돕고 성능을 향상시켜주는 것은 맞지만, 근본적으로 모델의 최적화 방식 이 변화하지 않으면 성능 향상에도 한계가 있을 수밖에 없다.
3.7.1. 지역 최적점과 전역 최적점
다시 한번 모델의 최적화란 무엇인지 상기해보자. 딥 러닝 모델의 최적화는 입력되는 데이터들에 따른 모델의 손실을 최소화하는 방향으로 모델 파라미터의 최적값을 찾는 과정이다. 다른 어떤 파라미터들보다 손실이 가장 낮아지는 지점의 파라미터 조합을 모델의 전역 최적점(Global minima) 이라고 한다.
전역 최적점을 찾아가는 과정에 경사 하강법을 이용한다. 실제로 모델을 최적화할 때는 파라미터에 대한 모든 손실 값의 분포를 한번에 알 수가 없으며, 어느 방향으로 모델의 파라미터를 갱신해야 할지 전혀 모르는 상태의 연속이다. 마치 한치 앞도 보이지 않는 안개 속을 걸어나가는 느낌이다. 그래서 현재 손실 값으로 각 파라미터들의 그래디언트를 구해, 손실 값이 낮아질 가능성이 큰 쪽으로 파라미터를 갱신하는 과정을 반복하는 것이 최선의 방법이다.
그런데 경사 하강법에는 피할 수 없는 문제점이 하나 존재한다. 바로 지역 최적점(Local minima) 에 빠질 가능성이 크다는 것이다. 손실 함수의 그래프가 항상 앞의 그래프처럼 내려가기 쉬우면 참 좋을 것이다. 그러나 모델과 손실 함수가 복잡해질수록 손실의 그래프도 복잡해짐은 어쩔 수 없다. 경사 하강법 알고리즘이 최적값이라고 착각할만 한 수많은 지역 최적점들이 생기는 것이다.
학습률이 너무 낮은 상태로 최적화를 반복하다 지역 최적점에 도달하면, 파라미터를 조금씩 변경해봐도 더 이상 손실이 낮아지는 점을 찾을 수 없어 그 점에서 최적화가 멈춰버린다. 반대로 학습률이 너무 높은 상태로 최적화를 반복하면 작은 그래디언트에도 파라미터가 큰 폭으로 바뀌어 미세하게 경사를 내려가지 못하고 최적점으로 수렴에 실패할 것이다. 정말 이상적으로는 전역 최적점을 찾기 가장 좋은 학습률을 설정하면 되지만, 학습률은 온전히 개발자의 경험에 의존하는 하이퍼 파라미터이기 때문에 현실적으로 불가능한 일이다. 확실한 딜레마가 생기는 셈이므로, 단순한 확률적 경사 하강법으로는 복잡한 딥 러닝 모델의 최적화가 쉽지 않을 것임을 쉽게 예측할 수 있다.
이 문제를 해결하려면 결국 최적화 기법 자체를 손봐야 한다는 결론에 도달한다. 학습률의 선택만으로는 전역 최적점을 찾는 데에 현실적인 한계가 있기 때문이다. 이번 챕터에서는 경사 하강법을 발전시켜, 지역 최적점으로의 파라미터 수렴을 피하고 전역 최저점을 더욱 적극적으로 탐색할 수 있도록 하는 다양한 시도들을 알아볼 것이다. 이 방법들은 단순 경사 하강법에 여러 변수를 추가로 넣기도 하고, 기존의 학습 경향을 기억하기도 하면서 그래디언트의 크기를 조절한다. 많은 학자들이 같은 반복 횟수 안에서 더 효율적으로 전역 최적점을 탐색하는 방법을 고민한 흔적을 순서대로 따라가보자.
이번 챕터의 내용은 현업에서 딥러닝을 바로 활용할 것이 아니라면 굳이 깊게 이해하려고 하지 않아도 된다. 내용의 설명을 위해 필연적으로 어느 정도의 수식이 사용되어야 함도 있고, 대부분의 경우 최적화 기법은 모델에 의존하지 않기 때문이기도 하다. 각 방법들이 이전 방법의 어떤 문제점을 발견해서 어떤 방법으로 개선했는지, 그 흐름을 이해하는 것을 목표로 하자.
3.7.2. SGD
먼저 가장 기본이 되는 확률적 경사 하강법부터 복습하고 시작하자.
딥 러닝 모델의 학습은 모델의 출력에 따른 손실 함수의 값이 작아지도록 모델의 파라미터들을 최적화하는 과정이다. 이 과정에서 경사 하강법 을 이용한다는 것은 앞선 챕터들에서 이해한 바 있다. 궁극적으로는 학습 데이터셋 전체에 대한 모델의 손실을 한 번에 구해서 모델의 파라미터들을 갱신하는 것이 최고다. 그러나 한 번의 최적화 반복마다 학습 데이터셋 전체에 대해 모델의 출력을 구하고 손실을 계산해야 하므로 계산량이 매우 많아져 문제가 된다.
그래서 우리는 전체 학습 데이터셋을 대표할 수 있는 데이터셋의 일부인 미니 배치 를 사용해 경사 하강법을 실행하며 이를 확률적 경사 하강법(SGD) 라고 한다. 미니 배치는 학습 데이터셋의 일부를 무작위로 추출한 묶음이며, 이 자체로 학습 데이터셋을 대표한다. 작은 미니 배치에 대해서만 그래디언트를 계산하니 계산량이 크게 줄어, 거대한 학습 데이터셋을 사용하더라도 경사 하강법을 적용할 수 있는 길이 열렸다. 앞으로 이야기할 최적화 기법은 모두 SGD의 미니 배치 최적화를 기반으로 한다.
SGD는 어떤 추가적인 계산도, 파라미터도 없는 순수한 경사 하강법 알고리즘이다. 먼저 손실 함수를 미분하여 모델의 각 파라미터들에 대한 그래디언트를 구한다. 그 후, 그래디언트의 반대 방향으로 파라미터를 갱신해서 같은 입력 데이터에 대해 손실 함수가 더욱 작아지도록 만든다. 학습률로 갱신의 폭을 조절하는 것 이외에 추가적인 하이퍼 파라미터나 연산은 없다.
특정 파라미터 위치에서 계산한 그래디언트가 동일해도, 학습률이 커지면 파라미터 갱신의 폭이 훨씬 커진다. 단순히 파라미터에서 학습률과 그래디언트를 곱한 값을 뺴는 방식으로 갱신하기 때문이다.
그림만 봐도 학습률 설정에 따라 모델의 최적화 양상이 완전히 달라질 것임을 예측할 수 있다. 그래서 학습률은 모델 학습에 있어 가장 중요한 하이퍼 파라미터이다. 또한 여기서 예상되는 문제점을 쉽게 찾아낼 수 있다. 학습률이 너무 작은 경우 파라미터가 지역 최저점에 수렴해버릴 가능성이 크다. 반대로 학습률이 너무 큰 경우 최저점을 잘 찾지 못하거나 아예 파라미터가 발산해버릴 수 있다.
그림을 보면, 학습률이 너무 작은 경우에는 손실 값이 하락하다가 다시 잠깐 상승하는 지점을 탈출하지 못한다. 손실이 하강하는 방향으로의 그래디언트가 너무 작아 지역 최적점을 탈출할 힘이 없기 때문이다. 학습률이 너무 큰 경우에는 같은 그래디언트라도 모델의 갱신 폭이 너무 커서, 어디 하나 최적점을 찾지 못하고 왔다갔다 하는 모습을 보인다. 그림에서는 상대적으로 약하게 표현해놓았지만, 실제로 모델을 학습하다보면 어느 순간 파라미터가 무한대로 발산하는 현상도 자주 일어난다. 많은 경우 학습률이 너무 커서 발생하는 현상이다.
이처럼 단순 SGD는 많은 문제점을 안고 있는 알고리즘이다. 이후에 나올 최적화 기법들은 이러한 SGD의 문제점을 해결하기 위해 고민하면서 발전하였다.
PyTorch에서 SGD 최적화 알고리즘은 다음과 같이 사용할 수 있다. 설정해야 하는 하이퍼 파라미터는 학습률 lr
이다.
optimizer = torch.optim.SGD(model.parameters(), lr=0.001)
3.7.3. Momentum
Momentum은 SGD의 문제점을 해결하기 위해 가장 먼저 제안된 알고리즘이다. Momentum의 사전적 뜻은 ‘가속력’, ‘추진력’ 등인데, 사전적인 의미 그대로 SGD에 의한 파라미터 갱신에 추진력을 더 주는 알고리즘이다. 마치 자동차가 엑셀러레이터를 밟으면 밟을수록 속도가 올라가는것처럼, Momentum은 특정 방향으로 파라미터가 반복해 갱신될수록 갱신의 폭을 늘려준다.
Momentum은 파라미터 갱신의 현재 속도 를 정의한다. 특정 파라미터 위치에서 속도 \(v\)는 다음과 같이 정의된다.
뒷 항은 학습률과 그래디언트를 곱한 파라미터의 갱신량으로, SGD에서의 그것과 동일하다. 기존의 속도 \(v\)에 이번에 계산된 그래디언트에 의한 갱신량을 더하면, 이번 그래디언트가 반영된 새로운 현재 속도가 계산된다. 기존 속도와 이번 그래디언트 중 어느 쪽을 더 크게 반영할지는 하이퍼 파라미터 \(\alpha\)가 결정한다. \(\alpha\)는 보통 0.9와 같은 값을 사용한다. 이렇게 계산된 속도를 이용해 모든 파라미터를 갱신한다.
이렇게 가중치를 업데이트하면, 가중치는 그래디언트의 방향으로 더욱 가속되어 업데이트된다. \(\alpha v\)항이 기존의 속도를 계속 기억하고 있기 때문이다. 다음의 사진을 보자.
사진에서 SGD와 Momentum은 같은 위치의 파라미터를 최적하고 있다. 둘 다 파라미터들의 그래디언트는 비슷하게 계산될 것이다. SGD는 정직하게 그래디언트에 비례해서만 경사를 내려가기 때문에, 다시 손실이 오르막을 보이는 구간을 만나는 순간 파라미터 갱신의 방향이 반대로 바뀐다. 지역 최적점에서는 그래디언트를 아무리 계산해도 0에 수렴하므로 지역 최적점을 탈출할 수 없게 된다. 그러나 Momentum은 다른 양상을 보인다. 그래디언트가 계속 같은 방향으로 나오면서 속도가 점점 증가하는 모습을 볼 수 있다. 파라미터가 지역 최적점에 도달했을 때도, 그래디언트는 0에 가깝지만 경사를 내려오던 속도는 아직 그대로인 상태다. 속도의 힘만으로 파라미터를 갱신해도 마지막 갱신량과 비슷하게 파라미터를 움직일 수 있으므로, 내려오던 방향으로 한번 더 업데이트되어 지역 최저점을 탈출하는 것이다.
PyTorch에서 Momentum 최적화 알고리즘은 다음과 같이 사용할 수 있다. SGD를 그대로 사용하되 momentum
매개변수로 Momentum의 속도 반영 비율 파라미터인 \(\alpha\)를 설정할 수 있다.
optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
3.7.4. NAG (Nesterov Accelerated Gradient)
Momentum은 정말 괜찮은 아이디어지만, 다른 안전장치 없이 증가하는 속도를 그대로 파라미터 갱신에 이용하기 때문에 최적화가 불안정하다는 단점이 있다. 단순히 높은 언덕에서 공을 굴리는 것과 같다. 빠른 속도를 유지하면서 파라미터를 갱신하다 보면, 기존의 속도 때문에 전역 최저점을 한참 지나칠 가능성도 있다. 속도를 이용해 지역 최적화를 피하고 학습을 가속하고자 했던 방법이 오히려 최적점을 찾지 못하게 만들 수도 있다는 이야기이다. Momentum은 현재 속도가 다음번의 그래디언트를 어떻게 바꿀 것인지 생각하지 않기 때문이다.
그래서 NAG(Nesterov Accelerated Gradient) 알고리즘이 제안되었다. Nesterov Momentum 이라고도 부른다. 기존의 Momentum 알고리즘과 크게 다르지 않으면서, 속도를 계산하는 부분만을 약간 변경한 알고리즘이다. Momentum이 앞으로 손실 값이 어떻게 변화할지 모르는 상태로 단순히 가속도를 붙여 내려가기만 한다면, NAG 알고리즘은 현재 속도를 유지한 상태로 최적화를 한번 더 진행하면 파라미터가 어떻게 변할지 예측해본 후 그에 맞춰 적당히 이동한다는 것이 차이점이다.
방법은 단순하다. 현재 속도로 인해 갱신된 파라미터를 미리 계산한 후 그것에 대한 그래디언트를 구해 사용한다. 수식으로 보면 다음과 같다.
\(\widehat{W}\)는 현재 파라미터를 현재 속도만큼 갱신했을 때 변화한 파라미터다. 당연히 아직 갱신되지 않은 상태이고, 현재 속도의 영향을 미리 계산해보는 것이다. 이번 그래디언트를 속도를 미리 더해본 파라미터 \(\widehat{W}\)에 대해서 계산한다. 현재 속도만큼 파라미터를 갱신한다면, 갱신 후의 파라미터가 어떤 그래디언트를 가질 것인지 미리 예상해보는 것이다. 그 위치의 그래디언트만큼 속도를 더해서 현재 파라미터를 갱신한다. 이렇게 하면, 현재 속도를 그대로 유지해서 갱신했을 때 최적점을 뛰어넘어 손실이 다시 커지는 현상을 예방할 수 있다. 말로 풀어놓아 살짝 어려운 감이 있으니, 그림으로 다시 살펴보자.
기존의 Momentum 알고리즘이었다면, 초록 점의 현재 위치에서 그래디언트가 0에 가깝더라도 속도의 영향으로 파라미터가 한번 더 크게 갱신되었을 것이다. 그러나 NAG는 속도를 한번 더 더한 파라미터가 어떤 그래디언트를 가지는지 미리 계산해보고, 그에 맞춰서 속도를 변화시킨다. 현재의 큰 속도를 더했을 때 예상되는 파라미터의 그래디언트가 완전히 반대 방향을 향하므로, 현재 속도를 유지하면 손실이 다시 커질 가능성이 있다고 판단하는 것이다. NAG는 이 반대 방향의 그래디언트를 속도에 더해 속도를 크게 줄이고, 최적점에 수렴할 수 있는 가능성을 높인다.
결론적으로 NAG는 속도를 이용한 빠른 경사 하강이라는 이점을 살리면서도, 더욱 안정적으로 최적점에 수렴할 수 있도록 알고리즘을 발전시킨 것으로 볼 수 있다. 실제로도 일반 Momentum보다 평균적으로 더 좋은 최적화 성능을 보인다.
PyTorch에서 NAG 최적화 알고리즘은 다음과 같이 사용할 수 있다. Momentum이 적용된 SGD를 그대로 사용하되 nesterov
매개변수를 True로 주면 속도의 계산이 NAG의 방식으로 변경된다.
optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.9, nesterov=True)
3.7.5. AdaGrad
앞서 살펴보았던 Momentum 방식의 경사 하강법 알고리즘은 파라미터의 갱신 정도를 직접 조정하는 것에 초점을 맞춘 방법들이었다. 그와 다르게 지금부터 살펴볼 알고리즘들은, 최적화 상황에 따라 학습률 자체를 어떻게 조정할 것인지 고민하는 방법들이다. 경사 하강법 알고리즘에 가장 큰 영향을 미치는 하이퍼 파라미터가 학습률이기 때문에, 학습률을 건드리지 않고서는 안정적인 경사 하강법 알고리즘을 만들어낼 수 없다.
일반적인 경우, 최적화가 진행될수록 학습률은 지속적으로 낮아져야 한다. 최적점에 가까워질수록 그래디언트가 점점 작아질 것이기 때문이다. 그래디언트가 클 때와 작을 때 동일한 학습률으로 파라미터를 갱신한다면, 그래디언트가 작은 최적점 근처에서 파라미터가 어느 한 점으로 수렴하지 못하고 계속 진동할 것이다. 그래서 도입한 개념이 학습률 감소 이다. 학습이 반복될수록 학습률을 낮추는 방법을 말한다. 많은 딥 러닝 모델 최적화에 있어 빼놓을 수 없는 중요한 기법이다. 지금 살펴볼 AdaGrad 알고리즘이 학습률 감소를 도입하기 시작한 알고리즘이다.
가장 단순하게 학습률 감소를 구현한다면, 단순히 반복이 진행된 횟수에 비례해 학습률을 낮춰가면 될 듯 하다. 그러나 문제를 이렇게 쉽게 해결할 수 있었으면 이렇게 많은 알고리즘이 개발되지 않았을 것이다. AdaGrad는 한 발짝 더 나아가서, 파라미터의 원소별로 최적의 학습률을 제공 하고자 한다. 최적화 과정에서 각각의 원소들을 효율적이면서 안정적으로 갱신할 수 있도록, 모든 원소의 그래디언트 반영 비율을 따로 계산한다.
같은 학습률이어도 그래디언트가 클수록 갱신 폭이 크다. 반대로 생각해보면, 파라미터의 원소들 중 그래디언트가 작은 녀석들과 큰 녀석들이 모두 동일한 학습률을 적용받아 갱신되면 문제가 생길 수 있다. 모든 원소가 동시에 최적점을 찾아가야 하는데도 불구하고 그래디언트가 큰 원소들만 상대적으로 빠르게 최적화될 것이기 때문이다. 이렇게 파라미터 갱신 시에 원소별로 밸런스가 맞지 않는 것은 최적화가 느려지는 원인이 된다.
그래서 AdaGrad는 그래디언트를 바탕으로 각 원소의 학습률을 모두 따로 설정하도록 한다. 수식을 통해 AdaGrad가 학습률을 조정하는 방식을 이해할 수 있다. 먼저 AdaGrad가 만들어내는 학습률 감소 파라미터 \(h\)를 정의하자.
현재 파라미터에 대해 계산된 그래디언트를 원소별로 제곱해 \(h\)에 더한다. \(h\)는 매 반복마다 갱신되므로, 기울기 값의 변화를 계속 기억하고 있는 일종의 메모리 변수라고 볼 수 있다. \(h\)가 음수가 되는 일을 방지하도록 제곱해서 더한다. 여기서 꼭 상기하고 넘어가야 할 점은, 그래디언트 \(\frac{\partial L}{\partial W}\)은 파라미터 행렬과 모양이 같은 행렬이라는 점이다. 파라미터의 각각 원소들에 대한 모든 그래디언트가 행렬 형태로 모여있다. 따라서 \(h\)또한 파라미터와 모양이 같은 행렬이다.
이제 \(h\)를 이용해 학습률을 조정한 후 파라미터를 갱신한다.
현재 파라미터의 그래디언트에 적용되는 학습률은 \(\eta \frac{1}{\sqrt{h}}\)가 된다. \(\eta\)는 학습률 하이퍼 파라미터다. \(h\)의 값이 커질수록 학습률은 작아지게 된다. \(h\)에는 이전에 나타난 그래디언트의 제곱이 누적되어 있으므로, 계속해서 그래디언트가 큰 상태였다면 \(h\)는 작아질 것이고, 그래디언트가 작아졌다면 \(h\)의 값은 커질 것이다. \(h\)가 파라미터와 모양이 같은 행렬이므로, 파라미터의 각 원소별로 최적의 학습률을 제공할 수 있게 된다.
파라미터별로 최적의 학습률을 제공해줄 수 있게 되어 빠르고 안정적인 최적화에 한 발짝 더 가까워졌다. 그러나 AdaGrad 알고리즘에도 아직 문제는 남아 있다. 앞의 수식을 보면 알 수 있듯이 \(h\)에는 반복마다 끝없이 그래디언트를 더하기만 하므로, 언젠가는 모든 파라미터의 학습률이 0으로 수렴할 것이다. AdaGrad 알고리즘 때문에 어느 순간 학습이 정지할 수 있다는 것이다.
PyTorch에서 AdaGrad 알고리즘은 다음과 같이 사용할 수 있다.
optimizer = torch.optim.AdaGrad(model.parameters(), lr=0.001)
3.7.6. RMSProp
AdaGrad의 학습률이 사라지는 문제를 해결하기 위해 제안된 방법이 RMSProp 알고리즘이다. AdaGrad 알고리즘은 학습률 감소를 위해서 먼 과거의 그래디언트까지 모두 기억하고 있었다. 그래서 결국 학습률이 0에 수렴하는 문제도 발생했다. RMSProp은 이 문제를 개선해서, 먼 과거의 그래디언트 정보는 잊어버리면서 최근의 그래디언트 정보를 더 중요하게 반영하는 학습률 감소 방법을 제안한다.
RMSProp 알고리즘의 핵심은 과거의 그래디언트 정보일수록 현재 학습률에 미치는 영향이 줄어들도록 만들어야 한다는 것이다. 이를 위해 지수이동평균(EMA) 개념을 도입하여 학습률을 조정한다. 지수이동평균은 원래 경제학 분야에서 다뤄지는 개념으로, 어느 시점까지 정보들의 평균을 구한다. 평균을 구하되 시간의 개념을 도입하여 과거 정보의 영향력은 낮추고 최근 정보의 영향력은 높인 평균을 계산한다.
간단한 수식으로 지수이동평균을 나타내보자. 특정 시점 \(n\)에서 가장 최신의 정보를 \(x_n\), 과거 정보들의 누적 총합을 \(s_n\), 0과 1 사이의 임의의 계수를 \(\alpha\)로 정의할 때, 다음 시점에 갱신되는 평균 정보는 다음과 같이 계산된다.
갱신된 평균을 구할 때 가장 최신의 정보인 \(x_n\)이 반영되는 비율이 \(\alpha\)이고, 기존의 누적 평균인 \(s_n\)이 반영되는 비율은 \((1 - \alpha)\)이다. 즉, \(\alpha\)의 값이 클수록 평균 계산 시 최신의 정보에 더 큰 비중을 두는 것이고, 반대로 \(\alpha\)가 작을수록 과거의 정보에 큰 비중을 두는 것이다. 중요한 사실은 \(\alpha = 1\)이 아닌 이상 과거의 정보가 사라질 일은 없다. \(s_n\)이 갱신될수록 먼 과거에 나타난 정보들의 영향은 점점 줄어들지만 사라지지는 않는다.
이제 지수이동평균에서 ‘정보’를 ‘그래디언트’로 바꾸면 RMSProp 알고리즘의 핵심이 완성된다. AdaGrad처럼 학습률 감소를 위해 그래디언트를 계속 누적하는 방식은 동일하다. 그러나 누적하는 방식에 지수이동평균을 접목해서, 과거의 그래디언트들은 현재의 새로운 그래디언트들에 비해 평균에 적은 영향력을 미치도록 만든다. 다음의 식이 RMSProp이 학습률 감소에 사용하는 파라미터 계산 방법이다.
지수이동평균 식에서 단순히 정보를 그래디언트의 제곱으로 대체한 식이다. 최적화 반복마다 나타나는 그래디언트들을 제곱해서 갱신된 그래디언트의 지수이동평균을 구하고 그것을 학습률 감소에 사용한다. \(\gamma\)는 하이퍼 파라미터이며 보통 0.9나 0.99 등의 값을 많이 사용한다. 기존의 누적 평균 항과 현재 그래디언트 항의 위치가 바뀌었음에 주의해야 한다. \(\gamma\)의 값이 너무 작으면 그래디언트를 누적하는 의미가 사라지기 때문에 0.9정도의 적당히 큰 값을 사용한다.
파라미터를 갱신하는 수식은 AdaGrad와 동일하다. 누적된 그래디언트의 평균이 클수록 학습률이 작아진다. 이 때 그래디언트의 평균 \(h\)는 AdaGrad의 파라미터 \(h\)에 비해 현재 그래디언트의 반영 비율이 훨씬 크다.
AdaGrad는 영향력의 차이 없이 먼 과거의 그래디언트들부터 모두 정직하게 더하므로 학습률이 눈에 띄게 0으로 수렴한다. 반면에 RMSProp은 과거 그래디언트들의 반영 비율이 낮으므로 학습률이 빠르게 낮아지는 현상을 막으면서, 현재 그래디언트의 반영 비율을 상대적으로 키워 더욱 효과적으로 지역 최적점을 탈출할 수 있게 만든다. 현재 그래디언트가 낮으면 학습률을 키워 최적점 탈출의 가능성을 높일 수 있는 것이다.
그러나 RMSProp도 아직 해결하지 못한 문제가 있다. 그래디언트의 평균 \(h\)는 처음에 0으로 초기화되는데, \(\gamma\)가 1에 가까운 값을 가지고 있으므로 최적화 초반에 \(h\)의 값이 0에 가깝도록 편향되어있다는 점이다. 과거의 기울기 정보가 0에 크게 편향된 상태에서 갱신을 시작하기 때문에, 최적화 초반에는 그래디언트의 평균보다 현재의 그래디언트가 훨씬 큰 영향을 미칠 수밖에 없다. 그래서 최적화 초반에 경사를 잘 내려가지 못하고, 심지어 파라미터가 발산하는 현상까지 발생하기도 한다.
PyTorch에서 RMSProp 알고리즘은 다음과 같이 사용할 수 있다. alpha
매개변수로 지수이동평균 식에서 과거 누적 평균의 반영 비율을 결정하는 \(\gamma\)를 설정한다.
optimizer = torch.optim.RMSProp(model.parameters(), lr=0.001, alpha=0.99)
3.7.7. Adam
RMSProp의 단점을 보완하기 위해 최적화 기법이 더 깊게 연구되던 중, 앞서 살펴보았던 모든 기법의 장점을 모으고 단점을 최대한 해결하려고 시도한 Adam 알고리즘이 등장했다. Momentum 알고리즘의 아이디어와 RMSProp 알고리즘의 아이디어를 섞어서 최대한 안정적인 경사 하강법 알고리즘을 만들고자 했다.
지금까지 살펴본 경사 하강법 알고리즘들의 아이디어를 종합해보았을 때, 최적의 알고리즘은 다음의 내용을 모두 포함하고 있어야 한다.
- Momentum 개념을 차용하여 최적화 속도를 가속하고, 지역 최적점을 탈출할 가능성을 높인다.
- RMSProp 알고리즘의 아이디어를 차용하여 그래디언트의 변화에 따라 학습률을 조정한다.
- 최적화 초반에 학습률 감소 파라미터가 0에 가깝게 편향되지 않도록 만들어야 한다.
Adam 알고리즘이 각각의 아이디어를 어떻게 합쳤는지 순서대로 알아보도록 하자.
먼저 RMSProp 알고리즘의 아이디어를 차용해, 그래디언트의 변화에 따라 학습률 감소를 구현하는 2차 모멘트 를 계산한다. 2차 모멘트는 RMSProp 알고리즘이 계산했던 그래디언트의 지수이동평균과 동일하다. 먼저 수식을 간단히 하기 위해 시점 \(t\)에서의 그래디언트 \(g_t\)와 그래디언트의 제곱 \(g_t^2\)를 다음과 같이 정의하자.
2차 모멘트 \(v_t\)는 다음과 같이 정의된다.
2차 모멘트는 이름만 바뀌었을 뿐이지 RMSProp의 \(h\)파라미터와 동일하다.
다음으로 Momentum의 아이디어를 차용해 그래디언트의 변화에 따른 파라미터 갱신 폭의 증가를 구현하는 1차 모멘트 를 계산한다. 1차 모멘트는 기존 Momentum이 그래디언트의 변화를 누적하여 속도를 갱신하는 부분에 지수이동평균을 적용한 것이다. 수식으로 보면 아래와 같다.
기존 Momentum 알고리즘의 속도 수식이 단순히 그래디언트를 계속 누적하여 더했음을 생각해보면, Adam 알고리즘의 1차 모먼트는 지수이동평균을 이용하여 Momentum의 속도를 구하도록 의도된 것임을 알 수 있다.
1차 모먼트와 2차 모먼트의 과거 그래디언트 반영 비율인 \(\beta_1, \beta_2\)는 각각 0.9, 0.999를 사용하는 것이 가장 좋다고 알려져 있다.
Momentum과 RMSProp 알고리즘의 핵심 아이디어를 차용하여 그래디언트의 누적 변화를 살펴보는 두 개의 모먼트 행렬을 구했다. 그런데 앞서 이야기했던 RMSProp 알고리즘의 문제점이 아직 해결되지 않았다. 1차 모멘트와 2차 모멘트 변수 둘 다 지수이동평균을 이용하는 동시에 최적화 시작 시 0으로 초기화되므로, 최적화 초반에 값이 0에 가까이 편향되는 문제가 사라지지 않았다.
Adam 알고리즘은 이 편향을 없애기 위해 두 하이퍼 파라미터 \(\beta_1, \beta_2\)를 이용해서, 학습 초기에 두 개의 모멘트 변수가 너무 작아지지 않도록 만든다. 시점 \(t\)에서 각각 아래와 같이 편향이 보정된 모멘트 변수들을 구할 수 있다.
학습 초반에는 \(\beta_1^t, \beta_2^t\)의 값이 큰 상태이므로 각각의 분모가 작아 \(\widehat{m}^t\)와 \(\widehat{v}^t\)의 값이 상대적으로 커진다. 학습이 진행될수록 분모가 작아지므로 \(\widehat{m}^t\)와 \(\widehat{v}^t\)도 같이 작아지게 된다. 학습 초반에만 두 모멘트가 0에 가까이 편향되는 문제를 해결한 것이다.
알고리즘의 핵심인 두 개의 모멘트를 구했으니, 그것들을 이용해서 파라미터를 업데이트하는 일만 남았다. 갱신 방법은 간단하다. Momentum을 적용하는 1차 모멘트의 크기를, 학습률 감소를 적용하는 2차 모멘트의 크기에 비례해 줄인 후 파라미터에 더해주면 된다.
2차 모멘트 \(\widehat{v}_t\)의 값이 커질수록 1차 모멘트 \(\widehat{m}_t\)로 인한 파라미터의 갱신 폭이 작아진다. 1차 모멘트가 클수록 학습률을 낮추는 학습률 감소 방법을 Adam 알고리즘만의 방법으로 구현한 것이다. 하이퍼 파라미터 \(\alpha\)는 학습률이고, \(\epsilon\)은 분모가 0이 되는 것을 방지하기 위한 값으로, 보통 \(10^{-8}\) 등의 매우 작은 값을 사용한다.
정리하자면, Adam 알고리즘은 파라미터의 최적화를 빠르게 만들어주는 1차 모멘트를 구하고, 1차 모멘트로 인한 파라미터의 갱신 폭을 조정하기 위한 2차 모멘트로 학습률 감소를 구현한다. 또한 두 모멘트 변수의 계산에는 지수이동평균을 사용해서, 그래디언트의 변화를 더욱 효과적으로 누적해 반영할 수 있도록 만든다. 이제 왜 Momentum과 RMSProp의 특징을 합친 알고리즘이라고 했는지 이해할 수 있다. Adam 알고리즘은 Momentum의 파라미터 갱신 폭 조정과 RMSProp의 효과적인 학습률 감소, 지수이동평균을 활용해 적절한 과거 그래디언트 반영까지 세 박자를 모두 잡은 알고리즘이다.
앞서 제시되었던 모든 경사 하강 알고리즘의 장점을 모두 수용하고 단점을 최대한 해결한 알고리즘으로 볼 수 있다. 때문에 Adam은 오랜 기간동안 대체하기 힘든 최고의 최적화 기법이었다. 실제로 “최적화 기법은 무조건 Adam을 써라!” 라는 말이 유행했을 정도로 Adam은 대부분의 상황에서 강력한 성능을 보여준다.
PyTorch에서 Adam 알고리즘은 다음과 같이 사용할 수 있다. betas
매개변수는 값이 두 개인 튜플이며 각각 \(\beta_1, \beta_2\)의 값을 설정한다. eps
매개변수는 \(\epsilon\)에 대응되는 아주 작은 값이다.
optimizer = torch.optim.Adam(model.parameters(), lr=0.001, betas(0.9, 0.999), eps=1e-8)
한동안 Adam이 모든 상황에 적용할 수 있는 만능 알고리즘처럼 여겨졌지만, 최근 들어 Adam 또한 항상 좋은 성능을 내지는 못한다는 사실이 여러 연구자들에 의해 밝혀졌다. 만능 알고리즘을 찾는 것은 당연히 불가능한 일일 것이다. 대신 Adam의 단점을 극복하고 더욱 안정적으로 학습시키거나 일반화 성능을 높이는 RAdam, AdamW 등의 새로운 알고리즘들이 등장했다. 이번 챕터의 알고리즘들을 대강 이해했다면 최근에 개발된 새로운 알고리즘들을 이해하기도 어렵지 않으므로, 이후 챕터를 지나가면서 필요한 때에 간단히 이야기하도록 하겠다. 다만, 아직도 많은 상황에서 Adam은 가장 강력한 경사 하강법 알고리즘이다.