Adam은 어떻게 동작하는가
— 파라미터마다 따로 재는 보폭
SGD는 모든 파라미터에 같은 학습률 하나를 곱합니다. 기울기가 1000인 파라미터와 0.001인 파라미터가 한 모델 안에 섞여 있어도 말이죠. Adam은 파라미터마다 "최근 기울기가 보통 얼마나 컸는지"를 기억해 두었다가 그걸로 나눕니다. 그래서 학습률 α는 "기울기에 곱하는 수"가 아니라 "한 스텝에 대략 움직이는 거리"가 됩니다. 수식은 다섯 줄이지만 줄마다 이유가 있고, 그 이유를 하나씩 브라우저에서 계산해 봅니다.
§1 다섯 줄 — Adam 전체 알고리즘
논문의 Algorithm 1을 기호만 조금 정리하면 이렇습니다. g는 이번 스텝의 (미니배치) 기울기이고,
모든 연산은 원소별(element-wise)입니다 — 파라미터가 1억 개면 아래 다섯 줄이 1억 번, 서로 독립적으로 계산됩니다.
m ← β₁·m + (1 − β₁)·g ① 기울기의 이동 평균 (1차 모멘트, "방향")
v ← β₂·v + (1 − β₂)·g² ② 기울기 제곱의 이동 평균 (2차 모멘트, "평소 크기²")
m̂ ← m / (1 − β₁ᵗ) ③ 편향 보정
v̂ ← v / (1 − β₂ᵗ) ④ 편향 보정
θ ← θ − α · m̂ / (√v̂ + ε) ⑤ 갱신
기본값: α = 0.001, β₁ = 0.9, β₂ = 0.999, ε = 10⁻⁸, m = v = 0으로 시작, t = 1, 2, 3, …
각 줄이 무엇을 하는지 먼저 한 문장씩 정리해 두고, 다음 절부터 하나씩 실험합니다.
| 줄 | 하는 일 | 빼 버리면 | 절 |
|---|---|---|---|
| ① | 최근 기울기 약 10개의 평균 → 노이즈를 걸러 낸 방향 | 미니배치 노이즈가 그대로 보폭이 된다 (= RMSprop) | §2 |
| ② | 최근 기울기² 약 1000개의 평균 → 이 파라미터의 "평소 크기" | 파라미터마다 보폭을 맞출 수 없다 (= Momentum SGD) | §2, §4 |
| ③④ | 0에서 출발한 평균이 작게 나오는 것을 되돌림 | 초반 스텝이 최대 6.6배 커진다 | §3 |
| ⑤ | 방향 ÷ 평소 크기 → 단위 없는 수 × α | — | §4, §5 |
§2 이동 평균 — β는 "몇 스텝을 기억하는가"
①과 ②는 같은 모양의 식입니다. 이전 값을 β만큼 남기고, 새 값을 (1 − β)만큼 섞습니다.
이것을 지수 이동 평균(EMA)이라고 부르는데, 풀어 쓰면 과거 기울기에 β, β², β³, …으로 줄어드는 가중치를 준 평균입니다.
β가 크면 오래 기억해서 매끄럽지만 느리게 반응하고, β가 작으면 빠르지만 흔들립니다. 아래는 진짜 기울기가 +1이다가 150스텝에서 갑자기 −1로 바뀌는 상황입니다. 회색 점이 매 스텝 관측되는 미니배치 기울기입니다.
β = 0.9에서 흔들림은 σ의 4분의 1 정도로 줄고(이론 23%), 대신 방향이 바뀐 걸 알아채는 데 6스텝쯤 걸립니다 (이론값 ln 2 / ln(1/β) ≈ 6.6). β = 0.99로 올리면 흔들림은 더 줄지만(이론 7%) 지연이 수십 스텝으로 늘어납니다(이론 69스텝). Adam이 ①에 β₁ = 0.9를 쓰는 이유가 여기 있습니다 — 방향은 빨리 따라가야 하니까요. 반대로 ②의 β₂ = 0.999는 약 1000스텝을 기억합니다. ②가 재는 것은 "지금 어느 쪽인가"가 아니라 "이 파라미터의 기울기는 평소에 어느 정도 크기인가"라서, 천천히 변하고 안정적인 값이 필요하기 때문입니다.
torch.optim.SGD(momentum=0.9))는 m ← 0.9·m + g로,
(1 − β)를 곱하지 않습니다. 그래서 정상 상태에서 m이 기울기의 약 10배가 되고, 학습률이 그만큼 작아야 합니다.
Adam의 ①은 진짜 평균이라 크기가 g와 같습니다. 어차피 ⑤에서 √v̂로 나눠 크기를 지워 버리므로 Adam에게는 이 차이가 문제되지 않습니다.
§3 편향 보정 — 0에서 출발한 평균은 처음에 거짓말을 한다
m과 v는 0에서 시작합니다. 첫 스텝의 기울기가 g라면 m₁ = 0.1·g, v₁ = 0.001·g²입니다.
기울기가 매번 똑같이 g로 들어와도 t스텝 뒤에는
실험 1에서 "편향 보정"을 끄고 β를 0.99 이상으로 올려 보세요. 주황색 선이 0 근처에서 한참 기어오르는 것이 보입니다. 그런데 이 편향이 m과 v에 다른 속도로 걸린다는 것이 진짜 문제입니다. m은 10스텝이면 거의 회복되지만 v는 1000스텝이 걸립니다. ⑤는 m을 √v로 나누니까, 보정하지 않으면 초반 스텝의 크기가 (1 − β₁ᵗ) / √(1 − β₂ᵗ)배로 틀어집니다.
기본값(β₁ = 0.9, β₂ = 0.999)에서 보정을 모두 빼면, 첫 스텝이 이미 3.16배이고 12스텝째에 6.57배까지 커졌다가 수천 스텝에 걸쳐 천천히 1로 돌아옵니다. 학습 초반은 가중치가 무작위라 기울기가 가장 크고 불안정한 때인데, 바로 그때 보폭이 몇 배로 튀는 것입니다. m만 보정하고 v를 보정하지 않으면(노란 선) 첫 스텝이 √1000 ≈ 31.6배입니다.
v ← ρ·v + (1 − ρ)·g²만 있고 편향 보정이 없어서,
ρ = 0.99로 시작하면 첫 스텝이 α의 10배(= 1/√0.01)입니다. Adam 논문 6.4절이 편향 보정을 뺀 버전을 실험해,
β₂가 1에 가까울 때 학습이 불안정해진다는 것을 보였습니다.
§4 √v로 나누기 — 학습률이 "거리"가 된다
⑤를 다시 봅시다. 기울기가 꾸준히 g라면 m̂ ≈ g, √v̂ ≈ |g|이므로
이것이 Adam의 핵심입니다. SGD에서 Δθ = −η·g이므로 손실을 10배로 키우면(기울기도 10배) 보폭도 10배가 되고,
같은 η로는 발산합니다. Adam은 기울기를 10배 해도, 1/100로 해도 똑같이 움직입니다.
그래서 층마다 기울기 크기가 천차만별인 깊은 신경망에서 "학습률 하나"가 그럭저럭 통하는 것입니다.
아래는 한쪽 방향으로 κ배 가파른 골짜기입니다. SGD·Momentum은 학습률 η를, RMSprop·Adam은 α를 씁니다. 손실 스케일을 바꿔 보고, 골짜기를 45° 돌려 보세요.
기본 설정에서 몇 가지를 확인할 수 있습니다.
- Adam이 제일 빠르지는 않습니다. 매끄러운 2차 함수에서는 잘 맞춘 Momentum이 더 빨리 도착합니다. Adam의 장점은 "최고 속도"가 아닙니다.
- 손실 스케일을 ×10으로 올리면 SGD와 Momentum은 몇 스텝 만에 발산하고, ×0.01로 내리면 거의 움직이지 않습니다. RMSprop과 Adam의 궤적은 한 픽셀도 달라지지 않습니다(ε 때문에 생기는 10⁻⁸ 수준의 차이뿐). 이것이 Adam의 진짜 장점 — 학습률을 다시 찾지 않아도 되는 것입니다.
- Adam의 경로는 처음에 대각선으로 갑니다. 두 좌표 모두 sign(g) × α만큼 움직이기 때문입니다 — 가파른 y든 완만한 x든 같은 보폭입니다. SGD는 가파른 y 방향으로 크게 튀고 완만한 x 방향으로 기어갑니다.
- 회전을 45°로 바꾸면 RMSprop과 Adam이 눈에 띄게 느려집니다. √v는 좌표축마다 따로 재는 값이라, 가파른 방향이 x나 y 축과 나란할 때만 제대로 보정됩니다. 골짜기가 비스듬하면 두 좌표가 모두 가파른 성분과 완만한 성분을 섞어 받아서 "축마다 다른 보폭"이 아무 도움이 되지 않습니다. Adam은 대각 행렬로만 곡률을 흉내 냅니다.
§5 신호 대 잡음비 — 확신이 없으면 보폭이 줄어든다
§4는 기울기가 "꾸준할 때"의 이야기였습니다. 실제 미니배치 기울기는 평균 μ 주변에서 σ만큼 흔들립니다.
그러면 m̂은 평균 μ를, v̂는 평균 μ² + σ²를 추정하므로
방향이 확실하면(μ가 σ보다 크면) α만큼 성큼 가고, 방향이 불확실하면(σ가 크면) 알아서 조금만 움직입니다. 학습이 최솟값에 가까워지면 진짜 기울기 μ는 작아지는데 미니배치 노이즈 σ는 그대로이므로, Adam은 학습률을 줄이지 않아도 보폭이 저절로 줄어듭니다. 논문이 "자동 담금질(automatic annealing)"이라고 부른 성질입니다.
μ = 1, σ = 1이면 이론값 0.707, 실측 0.710입니다. σ = 3이면 0.32로 줄어듭니다. 기울기 배율을 10⁴까지 올려도 Adam의 스텝은 변하지 않지만, 오른쪽 "SGD라면" 값은 같은 배율로 커집니다. μ = 0으로 두면 — 최솟값 한가운데 — Adam 스텝은 평균 0 근처에서 흔들리지만 그 폭은 0이 아닙니다. 상수 α로는 최솟값 주위를 맴도는 것이 멈추지 않으므로, 실전에서는 결국 학습률 스케줄을 함께 씁니다(§10).
§6 희소한 기울기 — 드물게 나오는 단어도 배운다
단어 임베딩을 생각해 봅시다. "the"의 임베딩은 거의 모든 배치에서 기울기를 받지만, "양자역학"의 임베딩은 1000배치에 한 번쯤 받습니다. 기울기가 0인 스텝에는 SGD가 아무것도 하지 않으므로, 드문 단어는 흔한 단어보다 빈도에 정비례해서 덜 배웁니다.
Adam에서는 다릅니다. 드문 파라미터는 v도 작습니다 — 1000스텝 중 1번만 g²가 들어가니까요. 작은 v로 나누니 기울기가 들어오는 그 순간 보폭이 커집니다. 주기 p마다 한 번 기울기를 받는다면 대략
p = 50이면 SGD는 2%, Adam은 14%로 √p 법칙과 거의 정확히 맞습니다. p가 β₂의 기억 길이(1/(1 − β₂) = 1000)를 넘어서면 법칙이 어긋나기 시작합니다 — 다음 기울기가 오기 전에 v가 잊혀 버리기 때문입니다. 이런 성질은 원래 AdaGrad(Duchi 외, 2011)가 희소한 특성(feature)을 위해 만든 것이고, Adam이 그 장점을 물려받았습니다.
α·(1 − β₁)/√(1 − β₂) — 기본값으로 약 3.16α — 로, 그렇지 않을 때 α로 적고 있고,
"오랫동안 0이다가 갑자기 기울기가 들어오는" 희소한 경우가 바로 앞의 상황이라고 설명합니다.
§7 Adam과 AdamW — weight decay는 어디에 넣어야 하는가
SGD에서 L2 정규화(손실에 ½λ‖θ‖²를 더하기)와 weight decay(매 스텝 θ ← (1 − ηλ)·θ)는 같은 것입니다.
L2의 기울기가 λθ이므로 θ − η(g + λθ) = (1 − ηλ)θ − ηg가 되니까요.
Adam에서는 같지 않습니다. L2 항을 기울기에 더하면 그 λθ도 ⑤에서 √v̂로 나뉩니다. 그러면 기울기가 평소에 큰 파라미터(√v̂가 큰 것)는 정규화가 약하게, 기울기가 작은 파라미터는 강하게 걸립니다. "큰 가중치를 벌준다"는 원래 의도와 무관한 기준으로 벌의 크기가 정해지는 셈입니다. Loshchilov & Hutter (ICLR 2019)는 decay를 기울기에서 떼어 내(decoupled) 따로 적용하자고 제안했고, 이것이 AdamW입니다.
# Adam + L2 (torch.optim.Adam(weight_decay=λ))
g ← g + λ·θ ← decay가 m, v 안으로 들어간다
... ①~④ ...
θ ← θ − α · m̂ / (√v̂ + ε)
# AdamW (torch.optim.AdamW(weight_decay=λ))
θ ← θ − α·λ·θ ← decay는 옆에서 따로
... ①~④ ... (g는 그대로)
θ ← θ − α · m̂ / (√v̂ + ε)
Adam + L2에서는 기울기가 작은 가중치가 수백 스텝 만에 0으로 쓸려 가고(λθ가 기울기를 지배해 sign처럼 α씩 깎임), 기울기가 큰 가중치는 훨씬 천천히 줄어듭니다. AdamW에서는 두 가중치가 정확히 같은 곡선을 그립니다 — decay가 √v̂와 무관하니까요.
torch.optim.Adam(params, weight_decay=0.01)은 AdamW가 아니라 Adam + L2입니다.
decoupled decay를 원하면 torch.optim.AdamW를 써야 합니다. 또 AdamW는 decay에 α를 곱하므로(θ ← θ − α·λ·θ),
학습률 스케줄로 α를 줄이면 decay도 함께 줄어듭니다. 논문의 원래 식은 스케줄 배수 ηₜ만 곱하는 형태라 이 부분이 구현마다 조금씩 다릅니다.
§8 Adam이 틀리는 곳 — 수렴 반례와 워밍업
수렴하지 않는 볼록 문제
Adam 논문은 볼록 문제에서의 수렴 증명을 실었지만, 3년 뒤 Reddi, Kale & Kumar (ICLR 2018 최우수 논문)가 그 증명에 오류가 있음을 보이고 아주 단순한 반례를 제시했습니다. x ∈ [−1, 1]에서 매 스텝 손실이 이렇게 바뀝니다.
크게 틀린 기울기 C는 드물게, 작은 기울기 −1은 자주 옵니다. C가 들어오는 순간 v가 커져서 C 스텝은 크게 눌리고,
그 뒤 v가 빨리 잊히면서(β₂가 작으므로) −1 스텝은 상대적으로 크게 반영됩니다. 결국 "자주 오는 작은 신호"가 "드물게 오는 큰 신호"를 이깁니다.
Reddi 등이 제안한 AMSGrad는 v 대신 지금까지의 최댓값 v̂ = max(v̂, v)로 나누어, 큰 기울기를 한 번 본 파라미터가 그것을 잊지 않게 합니다.
β₂를 0.9 이상으로 올리면 이 예제에서는 Adam도 −1로 갑니다. 반례가 β₂에 맞춰 설계되어 있기 때문입니다.
하지만 논문의 정리 3은 어떤 β₁, β₂(β₁ < √β₂)를 골라도 수렴하지 않는 확률적 볼록 문제가 존재함을 보였습니다.
실전에서는 AMSGrad가 Adam보다 일관되게 낫다는 증거가 약해 널리 쓰이지는 않지만(torch.optim.Adam(amsgrad=True)로 켤 수는 있습니다),
"Adam은 항상 수렴한다"는 믿음을 깬 결과로 의미가 큽니다.
워밍업은 왜 필요한가
편향 보정은 v̂의 기댓값을 맞춰 줄 뿐, 초반에 v̂가 몇 개 안 되는 표본으로 만든 추정값이라는 사실은 바꾸지 못합니다. t = 1이면 v̂ = g₁² 하나, 즉 표본 1개로 잰 분산입니다. 우연히 작은 기울기가 들어오면 그 파라미터의 보폭이 크게 튑니다. Liu 외 (RAdam, ICLR 2020)는 이 분산이 초반에 발산하다시피 크다는 것을 분석하고, 처음 몇 천 스텝 동안 학습률을 0에서 천천히 올리는 워밍업이 바로 이 문제를 덮어 주는 장치라고 설명했습니다. Transformer가 워밍업 없이 잘 학습되지 않던 이유 중 하나가 이것입니다(다른 하나는 Post-LN 구조 — 같은 시리즈의 용어 사전 참고).
§9 대가 — 파라미터마다 숫자 두 개 더
m과 v는 파라미터와 모양이 같은 텐서입니다. 파라미터가 70억 개면 m도 70억 개, v도 70억 개. 그래서 Adam으로 학습할 때 메모리의 대부분은 모델 자체가 아니라 옵티마이저 상태가 차지합니다.
7B 모델을 fp16으로 추론만 하면 가중치 14 GB면 되지만, Adam으로 학습하려면 활성값을 빼고도 112 GB가 필요합니다. 그중 m과 v가 56 GB — 절반입니다. 이 부담 때문에 m, v를 8비트로 양자화하는 8-bit Adam(Dettmers 외, 2022), 옵티마이저 상태를 GPU들에 나눠 담는 ZeRO, 2차 모멘트를 행·열 통계로 압축하는 Adafactor(Shazeer & Stern, 2018) 같은 방법이 나왔습니다.
§10 실전 메모 — 기본값과 이름의 혼선
| 하이퍼파라미터 | 논문 / PyTorch 기본값 | 자주 바꾸는 값 | 메모 |
|---|---|---|---|
| α (lr) | 1e-3 | 1e-4 ~ 3e-4 (대형 모델) | "한 스텝에 움직이는 거리"에 가까우므로 모델 크기가 커지면 줄인다. 거의 항상 워밍업 + 감쇠 스케줄과 함께 |
| β₁ | 0.9 | 0.9 | 거의 안 바꾼다 |
| β₂ | 0.999 | 0.95 (LLM) | GPT-3 등 대형 언어 모델은 0.95 — v가 기울기 변화를 더 빨리 따라가 손실 스파이크에 덜 취약하다 |
| ε | 1e-8 | 1e-6 ~ 1e-8 | 0으로 나누는 것을 막는 값. 기울기가 아주 작은 파라미터에서는 ε가 √v̂를 넘어서 Adam이 SGD처럼 동작한다 |
| weight_decay | Adam: 0 · AdamW: 0.01 | 0.01 ~ 0.1 (AdamW) | §7 — Adam의 weight_decay는 L2다 |
헷갈리기 쉬운 것
- ε의 위치가 구현마다 다릅니다. PyTorch는 논문처럼
√v̂ + ε, TensorFlow/Keras의epsilon은 편향 보정 전의 √v에 더하는 "ε̂"(논문 Algorithm 1 아래의 효율적인 버전)이라, 같은 숫자를 넣어도 효과가 다릅니다. 프레임워크 사이에 하이퍼파라미터를 옮길 때 주의하세요. - "Adam 스텝 ≈ α"는 '대략'입니다. §5처럼 노이즈가 크면 훨씬 작고, §6처럼 희소하면 α보다 클 수 있습니다.
- Adam이 SGD보다 일반화가 나쁘다는 이야기. 이미지 분류(ResNet 등)에서는 잘 튜닝한 Momentum SGD가 테스트 정확도에서 앞선다는 보고가 있습니다(Wilson 외, 2017). 반면 Transformer 계열은 사실상 Adam(W)만 씁니다. 주제마다 답이 다릅니다.
- 옵티마이저 상태도 체크포인트에 저장해야 합니다. 가중치만 저장했다가 이어서 학습하면 m = v = 0에서 다시 시작해 §3·§8의 초반 불안정을 한 번 더 겪습니다.
§11 정리 — 한 문장으로
| SGD | Momentum | RMSprop | Adam | |
|---|---|---|---|---|
| 방향을 평균 내나 (①) | ✗ | ✓ | ✗ | ✓ |
| 크기로 나누나 (②) | ✗ | ✗ | ✓ | ✓ |
| 편향 보정 (③④) | — | ✗ | ✗ | ✓ |
| 손실 스케일 ×10 | η 다시 찾기 | η 다시 찾기 | 그대로 | 그대로 |
| 파라미터당 추가 상태 | 0 | 1 | 1 | 2 |
§12 이해도 퀴즈
부록 · AdaGrad → RMSprop → Adam, 이름에 담긴 계보
AdaGrad (Duchi, Hazan & Singer, 2011) — 파라미터마다 지금까지의 기울기 제곱 합 전체로 나눕니다. 희소한 특성에 강하지만, 합이 계속 커지기만 하므로 학습률이 단조롭게 줄어 결국 멈춰 버립니다.
RMSprop (Hinton, 2012) — 논문이 아니라 Coursera 강의 Neural Networks for Machine Learning 6강 슬라이드에서 처음 공개됐습니다. AdaGrad의 "전체 합"을 "지수 이동 평균"으로 바꿔, 오래된 기울기를 잊게 한 것이 전부입니다. 그래서 아직도 논문을 인용할 때 강의 슬라이드를 인용합니다.
Adam (Kingma & Ba, 2014년 12월 arXiv · ICLR 2015) — RMSprop에 모멘텀(①)과 편향 보정(③④)을 더했습니다. 논문은 같은 글에서 ‖·‖∞로 나누는 변형 AdaMax도 제안했습니다. 이후 Nesterov 모멘텀을 넣은 NAdam(Dozat, 2016), §7의 AdamW, §8의 AMSGrad와 RAdam이 이어졌고, 오늘날 대형 모델 학습의 사실상 기본값은 AdamW입니다.