3단계 일곱 번째 논문. 이 글은 Ilya Loshchilov, Frank Hutter의 Decoupled Weight Decay Regularization 원문 PDF를 직접 읽고 썼다. arXiv 최초 제출은 2017-11-14이며, 이 글이 확인한 PDF는 ICLR 2019 게재 표기가 있는 판본이다.arXiv 서지 기록 AdamW(Adam with decoupled Weight decay)는 loss의 gradient에 (L2) 항을 섞어 Adam의 적응형 분모로 처리하지 않고, parameter(파라미터) 자체를 별도 비율로 줄이는 Adam 변형이다.
이 글에서 얻을 답과 범위
weight_decay=0.01이라는 설정은 너무 흔해서, “작은 weight를 선호하게 만드는 regularization(정규화)” 정도로만 외우기 쉽다. 하지만 같은 숫자를 SGD(Stochastic Gradient Descent, 확률적 경사 하강법), Adam, AdamW에 넣어도 update(갱신) 식은 같지 않다. 특히 Adam의 일반적인 L2 regularization(엘투 정규화)은 loss gradient와 (\lambda\theta)를 합친 다음, Adam의 first moment(1차 모멘트)와 second moment(2차 모멘트)에 넣는다. 이때 “weight를 줄이는 힘”까지 parameter별 adaptive scaling(적응형 크기 조절)을 받는다.
이 글을 끝까지 읽으면 다음을 답할 수 있어야 한다.
- L2 penalty와 weight decay는 정확히 무엇이며, SGD에서는 어떤 coefficient mapping(계수 대응) 아래 같은 update가 되는가.
- Adam의 (m_t), (v_t), bias correction(편향 보정), (\epsilon)이 무엇이며, L2 gradient가 왜 여기 섞이는가.
- Adam with L2와 AdamW가 같은 initial parameter와 data gradient에서 다른 parameter trajectory(파라미터 경로)를 만드는 이유는 무엇인가.
- “decoupled”는 learning rate와 완전히 무관하다는 뜻이 아닌데, 무엇과 분리된다는 뜻인가.
- 원 논문의 (\lambda)와 현재 PyTorch
AdamW(weight_decay=...)의 숫자를 왜 그대로 복사하면 안 되는가. - 저자 공식 Torch code는 L2와 AdamW를 어느 줄에서 갈라 놓고, learning-rate multiplier(학습률 배수)를 weight decay에 어떻게 적용하는가.
- LLM fine-tuning(미세 조정)에서 parameter group(파라미터 그룹), scheduler(스케줄러), checkpoint를 볼 때 무엇을 확인해야 하는가.
범위는 Loshchilov·Hutter의 AdamW·SGDW(SGD with decoupled weight decay) 논문, 저자 공식 Torch experiment code, 현재 PyTorch AdamW API다. 이 글은 Adam의 convergence theorem(수렴 정리), AMSGrad(Adam의 변형), weight norm control(가중치 노름 제어), 모든 optimizer benchmark를 대신 설명하지 않는다. 아래 Python 예제는 CIFAR-10·ImageNet32×32 논문 실험을 재현하지 않고, L2 derivative(미분), SGD의 등가성, Adam의 불등가성, moment buffer(모멘트 버퍼)를 작은 scalar(스칼라) 숫자로 확인한다.
먼저 잡을 한 문장
SGD에서는 L2 gradient를 더하는 일과 parameter를 일정 비율로 줄이는 일이 계수만 맞추면 같지만, Adam에서는 L2 gradient도 Adam의 역사 기반 분모를 통과한다. AdamW는 data gradient만 Adam에 맡기고 parameter shrinkage는 따로 적용한다.
왜 필요한가 — 같은 weight decay라는 이름이 다른 update를 가린다
학습은 loss를 낮추는 쪽으로 parameter를 조금 움직이는 일이다
model(모델)의 parameter vector(파라미터 벡터)를 (\theta)라고 하자. training batch(학습 배치) 하나에서 계산한 loss를 (f_t(\theta))라고 적는다. gradient(기울기) (\nabla f_t(\theta))는 각 parameter를 아주 조금 올렸을 때 loss가 얼마나 변할지를 담은 vector다.
가장 단순한 SGD update는 다음이다.
[ \theta_{t+1}=\theta_t-\alpha\nabla f_t(\theta_t) \tag{1} ]
| 기호 | 뜻 | 쉬운 설명 |
|---|---|---|
| (t) | update step 번호 | 몇 번째 mini-batch까지 처리했는지 |
| (\theta_t) | t번째 update 전 parameter | model의 현재 다이얼 묶음 |
| (f_t) | t번째 batch loss | 이 batch에서 틀린 정도 |
| (\nabla f_t) | loss gradient | 어느 다이얼을 어느 방향으로 움직일지 |
| (\alpha) | learning rate(학습률) | 한 번에 움직일 보폭 |
weight가 무한히 커지는 것, training data의 우연한 noise(잡음)를 과하게 외우는 것, 너무 복잡한 함수를 만드는 것을 줄이기 위해 penalty를 더할 수 있다. 가장 흔한 L2 penalty는 다음이다.
[ f_t^{\mathrm{reg}}(\theta) =f_t(\theta)+\frac{\lambda_{L2}}{2}\lVert\theta\rVert_2^2 \tag{2} ]
여기서 (\lVert\theta\rVert_2^2=\sum_i\theta_i^2)다. (\lambda_{L2})는 L2 penalty coefficient(엘투 패널티 계수)다. 미분하면
[ \nabla f_t^{\mathrm{reg}}(\theta) =\nabla f_t(\theta)+\lambda_{L2}\theta \tag{3} ]
가 된다. (\lambda_{L2}\theta_i)는 parameter가 0에서 멀수록 0 쪽으로 더 세게 당기는 gradient다.
weight decay는 loss에 penalty를 쓰지 않고 parameter를 직접 줄인다
Hanson·Pratt 식을 따라 원 논문은 weight decay를 다음처럼 쓴다.
[ \theta_{t+1}=(1-\lambda_{WD})\theta_t -\alpha\nabla f_t(\theta_t) \tag{4} ]
(\lambda_{WD})는 한 step에서 parameter에 곱할 shrinkage rate(축소율) 이다. data gradient가 0이면 (\theta)만 매 step (1-\lambda_{WD})배가 된다. 따라서 여러 step 뒤에는 대략 exponential decay(지수 감쇠) 모양이 된다.
L2 regularization
loss에 0.5 * λ_L2 * ||θ||²를 더한다
→ gradient에 λ_L2 * θ가 추가된다
weight decay
parameter 자체에 (1 - λ_WD)를 곱한다
→ data-loss gradient와 별도 shrinkage step이다
둘은 같은 목적을 향하지만, 계산 경로가 다르다. 왜 이름이 섞였는지는 다음 절의 SGD 등가성 때문이다.
무엇인가 — SGD에서는 같고 Adam에서는 갈라지는 regularization
SGD에서만 성립하는 정확한 등가성
SGD가 식 (2)의 regularized loss를 최적화하면 식 (3)을 넣어
[ \begin{aligned} \theta_{t+1} &=\theta_t-\alpha\left(\nabla f_t(\theta_t)+\lambda_{L2}\theta_t\right)\ &=(1-\alpha\lambda_{L2})\theta_t-\alpha\nabla f_t(\theta_t) \end{aligned} \tag{5} ]
를 얻는다. 식 (4)와 비교하면
[ \lambda_{WD}=\alpha\lambda_{L2} \tag{6} ]
일 때 정확히 같다. 원 논문 Proposition 1도 standard SGD에서 이 coefficient reparameterization(계수 재매개변수화) 아래 L2와 weight decay가 동치라고 쓴다.원 논문 2절·Appendix A Proposition 1
이 등가성에는 조건이 있다.
- standard SGD update여야 한다.
- L2 계수와 per-step decay 계수를 식 (6)처럼 맞춰야 한다.
- learning rate가 바뀌면 같은 L2 계수에 대응하는 per-step decay도 바뀐다.
그래서 L2 regularization = weight decay는 항상 참인 정의가 아니라, 특정 SGD update와 특정 계수 대응에서 성립하는 계산 결과다.
Adam은 gradient마다 서로 다른 분모를 쓴다
Adam(Adaptive Moment Estimation, 적응형 모멘트 추정)은 gradient의 moving average(이동 평균)와 squared gradient의 moving average를 보관한다. data loss gradient를 (g_t=\nabla f_t(\theta_{t-1}))라고 하면 일반적인 Adam은 다음 흐름이다.
[ m_t=\beta_1m_{t-1}+(1-\beta_1)g_t \tag{7} ]
[ v_t=\beta_2v_{t-1}+(1-\beta_2)g_t^2 \tag{8}
]
[ \hat m_t=\frac{m_t}{1-\beta_1^t}, \qquad \hat v_t=\frac{v_t}{1-\beta_2^t} \tag{9}
]
[ \theta_t=\theta_{t-1} -\alpha\frac{\hat m_t}{\sqrt{\hat v_t}+\epsilon} \tag{10}
]
| 값 | 뜻 | 왜 필요한가 |
|---|---|---|
| (m_t) | 1차 모멘트 | 최근 gradient 방향의 평균을 부드럽게 본다 |
| (v_t) | 2차 모멘트 | 최근 gradient 크기의 제곱 평균을 본다 |
| (\beta_1), (\beta_2) | decay coefficient(감쇠 계수) | 얼마나 과거를 기억할지 정한다 |
| (\hat m_t), (\hat v_t) | bias-corrected moment(편향 보정 모멘트) | 0에서 시작한 buffer의 초반 치우침을 보정한다 |
| (\epsilon) | numerical stability term(수치 안정 항) | 분모가 0 또는 너무 작아지는 것을 막는다 |
모든 원소 연산은 element-wise(원소별)다. 어떤 parameter coordinate(좌표)가 역사적으로 큰 gradient를 자주 받았다면 (\sqrt{\hat v_t})가 커져 그 좌표의 update 보폭은 작아질 수 있다. 바로 이 adaptive denominator(적응형 분모)가 L2와 weight decay를 갈라 놓는다.
Adam에 L2를 넣으면 regularizer도 m과 v에 들어간다
Adam에 L2 loss를 더하는 흔한 구현은 Adam에 넣을 gradient를
[ g_t^{\mathrm{L2}} =\nabla f_t(\theta_{t-1})+\lambda_{L2}\theta_{t-1} \tag{11}
로 만든다. 이 합친 gradient가 식 (7)과 (8)의 (m_t), (v_t)에 들어간다.
Adam with L2
data gradient + λ_L2 * θ
↓
m_t, v_t
↓
adaptive division by sqrt(v_hat) + eps
↓
parameter update
data gradient와 L2 gradient는 같은 buffer와 같은 adaptive scaling을 공유한다. 원 논문은 history상 큰 data gradient를 가진 weight는 L2 regularization gradient도 더 작은 상대량으로 처리될 수 있다고 설명한다.원 논문 2절·Proposition 2
AdamW는 shrinkage를 adaptive path 밖으로 꺼낸다
원 논문의 Algorithm 2는 schedule multiplier(스케줄 배수) (\eta_t)를 포함해 AdamW를 다음처럼 적는다.
[ \theta_t =\theta_{t-1} -\eta_t\left( \alpha\frac{\hat m_t}{\sqrt{\hat v_t}+\epsilon} +\lambda_{WD}\theta_{t-1} \right) \tag{12} ]
식 (12)에서 (m_t), (v_t)는 data loss gradient만으로 만든다. weight decay term은 분모 (\sqrt{\hat v_t}+\epsilon)를 통과하지 않는다. 같은 식을 두 단계로 쓰면 더 읽기 쉽다.
[ \theta_t'=(1-\eta_t\lambda_{WD})\theta_{t-1} \tag{13} ]
[ \theta_t=\theta_t' -\eta_t\alpha\frac{\hat m_t}{\sqrt{\hat v_t}+\epsilon} \tag{14} ]
이것이 decoupled다. regularizer gradient를 Adam의 moment·variance estimator와 preconditioner(사전조건자)에 섞지 않는다는 뜻이다.
선행 개념 — norm, momentum, parameter별 보폭
L2 norm은 parameter 크기를 재는 방법이다
parameter가 (\theta=[\theta_1,\theta_2,\ldots,\theta_d])일 때
[ \lVert\theta\rVert_2 =\sqrt{\theta_1^2+\theta_2^2+\cdots+\theta_d^2} ]
이다. L2 penalty (\frac{\lambda}{2}\lVert\theta\rVert_2^2)는 큰 parameter에 더 큰 0 방향 gradient를 준다. 여기서 “작은 weight가 항상 좋은 model”이라는 뜻은 아니다. data fit(데이터 적합), architecture, normalization, learning rate schedule과 함께 generalization(일반화)에 영향을 주는 한 제어 장치다.
momentum buffer는 과거 gradient를 기억한다
Adam의 (m_t)는 velocity(속도) 같은 비유로 설명할 수 있다. 같은 방향 gradient가 여러 step 이어지면 그 방향 기억이 쌓인다. (v_t)는 gradient의 크기 제곱을 기억해, 자주 요동치거나 큰 gradient가 난 coordinate의 보폭을 줄이는 역할을 한다.
중요한 사실은 buffer가 무엇을 입력으로 받았는가다.
| optimizer 형태 | (m), (v)에 들어가는 값 |
|---|---|
| Adam with L2 | (\nabla f+\lambda_{L2}\theta) |
| AdamW | (\nabla f) |
따라서 AdamW의 weight decay does not accumulate in the momentum nor variance라는 PyTorch 공식 설명은 단순한 홍보 문구가 아니라 buffer 입력의 차이다.PyTorch AdamW 공식 문서
decoupled는 learning rate가 필요 없다는 말이 아니다
원 논문 식 (12)에는 decay에도 (\eta_t)가 곱해진다. 현재 PyTorch AdamW documentation은 한 step을 다음 순서로 적는다.
[ \theta\leftarrow\theta-\gamma\lambda\theta ]
그 뒤 data gradient로 (m), (v)를 갱신하고 Adam update를 적용한다. 즉 PyTorch에서 per-step shrinkage factor는
[ 1-\gamma\cdot\texttt{weight_decay} \tag{15}
learning-rate scheduler가 (\gamma)를 작게 만들면, 그 step의 direct decay도 작아진다. AdamW가 decouple한 것은 learning rate schedule 전체와 완전히 분리가 아니라, weight decay가 adaptive gradient normalization과 moment state에서 분리된 것이다.
밑바닥 원리 — 작은 숫자로 SGD의 등가성과 Adam의 불등가성 보기
SGD에서는 계수를 맞추면 같은 2.99가 나온다
다음 값을 사용하자.
[ \theta=3, \quad \nabla f=0.7, \quad \alpha=0.01, \quad \lambda_{L2}=0.1 ]
식 (6)에 따라 (\lambda_{WD}=0.01\times0.1=0.001)이다.
L2 SGD는
[ 3-0.01(0.7+0.1\times3)=2.99 ]
weight decay SGD는
[ (1-0.001)3-0.01\times0.7=2.99 ]
이다. 이것이 사람들이 두 용어를 바꿔 부르게 된 실제 이유다.
Adam에서는 1과 100이 같은 비율로 줄지 않는다
data gradient가 0인 첫 Adam step만 보자. (\alpha=0.01), (\lambda_{L2}=0.1), (\epsilon=10^{-8})이고 initial parameter 두 개가 [1, 100]이라고 하자.
Adam with L2는 각 coordinate에 [0.1, 10]을 gradient로 넣는다. 첫 step의 bias-corrected Adam ratio는 대략 gradient의 sign(부호)만 남는다.
Adam with L2
1 → 0.990000001 상대 비율 약 0.990000001
100 → 99.990000000 상대 비율 약 0.999900000
작은 1은 약 1% 줄고, 큰 100은 약 0.01% 줄었다. “큰 weight일수록 더 많이 0으로 당긴다”는 L2의 직관이 Adam adaptive denominator를 통과하며 이 예에서는 상대 shrinkage로 유지되지 않는다.
동일한 data gradient 0에서 PyTorch-style AdamW를 쓰면 direct decay가 먼저 적용된다.
AdamW
1 → 0.999000000 상대 비율 0.999
100 → 99.900000000 상대 비율 0.999
두 coordinate 모두 같은 (1-0.01\times0.1)=0.999를 곱했다. 이것은 AdamW가 parameter별 data update 크기를 같게 만든다는 뜻이 아니다. data gradient update는 여전히 Adam의 adaptive rule을 따른다. 직접 weight shrinkage만 같은 비율이다.
L2 gradient가 moment history를 바꾸는 것도 차이다
(\theta=10), data gradient (0.2), L2 계수 (0.1)인 첫 step을 보자.
[ g^{\mathrm{L2}}=0.2+0.1\times10=1.2 ]
Adam with L2의 first moment는 (\beta_1=0.9)일 때 (m_1=0.1\times1.2=0.12)이고, second moment는 (v_1=0.001\times1.2^2=0.00144)다. AdamW는 10을 9.99로 먼저 줄이되, moment에는 data gradient 0.2만 넣는다. 그래서 (m_1=0.02), (v_1=0.00004)다.
Adam with L2
moment input = 1.2
m = 0.12, v = 0.00144
AdamW
direct decay 10 → 9.99
moment input = 0.2
m = 0.02, v = 0.00004
이 buffer 차이는 다음 step에도 남는다. 그래서 AdamW를 “Adam step 뒤에 L2 한 번 더”라고 느슨하게 구현하면 update order, state, scheduler 적용이 reference implementation과 달라질 수 있다.
내부 구조와 실제 실행 흐름 — 논문 code와 현재 PyTorch를 대조하기
저자 공식 Torch code는 L2와 AdamW를 분기한다
저자 공식 AdamW-and-SGDW 저장소는 Torch Lua implementation을 공개했다. 이 글이 확인한 commit은 0ae5185b3c655e45cc249adaca4457cd881874cc이다.
UPDATETORCHFILES/adam.lua 39–44행은 adam_type == 0일 때만 dfdx:add(wd, x)로 gradient에 L2 term을 더한다. 뒤의 60–71행은 dfdx로 moment와 denominator를 만들고 parameter를 Adam update한다. 반대로 72–75행은 adam_type > 0일 때 Adam update 뒤 x:add(-wdcur, xold)로 갱신 전 parameter 복사본을 직접 줄인다.
-- L2 path
if (config.adam_type == 0) then
dfdx:add(wd, x)
end
-- Adam moment and adaptive update use dfdx
state.m:mul(beta1):add(1-beta1, dfdx)
state.v:mul(beta2):addcmul(1-beta2, dfdx, dfdx)
x:addcdiv(-stepSize, state.m, state.denom)
-- decoupled path
if (config.adam_type > 0) then
x:add(-wdcur, xold)
end
이것은 최신 PyTorch의 내부 순서와 완전히 같은 code byte(바이트)는 아니다. 현재 PyTorch AdamW는 direct decay를 Adam moment update 전에 적용한다. 그러나 두 구현 모두 핵심은 같다. decay term이 data gradient로 (m), (v)에 들어가지 않는다. source revision과 framework를 바꾸면 update order까지 확인해야 한다.
현재 PyTorch AdamW의 호출 경로
이 글이 확인한 PyTorch main commit은 526b9cee8c9ad8da312bf33ea2c3f308cbfe8876이다. torch/optim/adamw.py는 AdamW가 공통 Adam functional path를 decoupled_weight_decay=True로 호출함을 보여 준다. current documentation은 다음을 명시한다.
AdamW의 weight decay는 momentum과 variance에 accumulate되지 않는다.- default
lr은1e-3,betas는(0.9, 0.999),eps는1e-8,weight_decay는1e-2다. foreachimplementation은 CUDA에서 보통 빠를 수 있지만 tensorlist intermediate 때문에 peak memory를 더 쓸 수 있다.
PyTorch AdamW 공식 문서는 현재 API의 근거다. 논문의 2019 Torch experiment hyperparameter와 현재 PyTorch default를 같다고 가정하면 안 된다.
원 논문의 normalized weight decay는 별도 제안이다
논문 Appendix B.1은 training budget(학습 예산)에 따라 좋은 decay 값이 달라지는 관찰을 줄이기 위해 normalized weight decay를 제안한다.
[ \lambda_{WD} =\lambda_{\mathrm{norm}}\sqrt{\frac{b}{B\cdot T}} \tag{16}
]
| 기호 | 뜻 |
|---|---|
| (\lambda_{\mathrm{norm}}) | 논문이 제안한 normalized decay hyperparameter |
| (b) | batch size |
| (B) | training data point 총수 |
| (T) | total training epochs |
저자 code도 cosine schedule에서 batch 수와 restart period를 이용해 normalized decay를 계산한다.train.lua 64–99행 하지만 논문은 이것이 “few experiments로부터 정보 얻은 한 가지 normalization”이라고 명시한다.원 논문 Appendix B.1 따라서 논문의 weightDecay=0.025 example을 현재 PyTorch weight_decay=0.025에 그대로 복사하면 parameterization·batch budget·scheduler가 달라 같은 실험이 아니다.
직접 검증과 재현 — 작은 scalar로 AdamW를 실행하기
실행 환경과 검증 범위
아래 코드는 macOS의 Python 3.9.6, 표준 라이브러리만 사용해 직접 실행했다. GPU, PyTorch, CIFAR-10을 쓰지 않았다. 검증 대상은 다음이다.
- (\frac{\lambda}{2}\theta^2)의 analytic derivative(해석적 미분)가 finite difference(유한 차분)와 일치하는가.
- SGD에서 (\lambda_{WD}=\alpha\lambda_{L2})일 때 L2·weight decay update가 같은가.
- data gradient가 0일 때 Adam with L2와 AdamW의 좌표별 relative shrinkage가 다른가.
- L2 regularizer가 Adam moment에 들어가고 AdamW에서는 data gradient만 들어가는가.
- data gradient가 0인 AdamW가 반복해서 (1-\mathrm{lr}\cdot\mathrm{wd})를 곱하는가.
#!/usr/bin/env python3
"""Small, dependency-free checks for Loshchilov & Hutter AdamW."""
from __future__ import annotations
import math
def l2_penalty(theta, coefficient):
return 0.5 * coefficient * theta * theta
def finite_difference_l2_gradient(theta, coefficient, delta=1e-6):
return (l2_penalty(theta + delta, coefficient) - l2_penalty(theta - delta, coefficient)) / (2 * delta)
def sgd_l2_step(theta, data_gradient, learning_rate, l2_coefficient):
return theta - learning_rate * (data_gradient + l2_coefficient * theta)
def sgd_weight_decay_step(theta, data_gradient, learning_rate, decay_rate):
return (1.0 - decay_rate) * theta - learning_rate * data_gradient
def adam_l2_step(theta, data_gradient, state, learning_rate, l2_coefficient,
beta1=0.9, beta2=0.999, epsilon=1e-8):
gradient = data_gradient + l2_coefficient * theta
step = state['step'] + 1
m = beta1 * state['m'] + (1.0 - beta1) * gradient
v = beta2 * state['v'] + (1.0 - beta2) * gradient * gradient
m_hat = m / (1.0 - beta1 ** step)
v_hat = v / (1.0 - beta2 ** step)
next_theta = theta - learning_rate * m_hat / (math.sqrt(v_hat) + epsilon)
return next_theta, {'step': step, 'm': m, 'v': v}, gradient
def adamw_step(theta, data_gradient, state, learning_rate, weight_decay,
beta1=0.9, beta2=0.999, epsilon=1e-8):
decayed_theta = theta * (1.0 - learning_rate * weight_decay)
step = state['step'] + 1
m = beta1 * state['m'] + (1.0 - beta1) * data_gradient
v = beta2 * state['v'] + (1.0 - beta2) * data_gradient * data_gradient
m_hat = m / (1.0 - beta1 ** step)
v_hat = v / (1.0 - beta2 ** step)
next_theta = decayed_theta - learning_rate * m_hat / (math.sqrt(v_hat) + epsilon)
return next_theta, {'step': step, 'm': m, 'v': v}, decayed_theta
def fresh_state():
return {'step': 0, 'm': 0.0, 'v': 0.0}
def show(label, values):
if isinstance(values, list):
print(label + ': [' + ', '.join(f'{value:.9f}' for value in values) + ']')
else:
print(f'{label}: {values:.9f}')
def main():
learning_rate = 0.01
l2_coefficient = 0.1
print('== L2 gradient ==')
theta = 3.0
analytical = l2_coefficient * theta
numerical = finite_difference_l2_gradient(theta, l2_coefficient)
show('analytical d(0.5*lambda*theta^2)/dtheta', analytical)
show('finite-difference gradient', numerical)
assert math.isclose(analytical, numerical, rel_tol=1e-8, abs_tol=1e-8)
print('\n== SGD: L2 and decoupled weight decay are equal after coefficient mapping ==')
data_gradient = 0.7
decay_rate = learning_rate * l2_coefficient
sgd_l2 = sgd_l2_step(theta, data_gradient, learning_rate, l2_coefficient)
sgd_wd = sgd_weight_decay_step(theta, data_gradient, learning_rate, decay_rate)
show('SGD with L2 penalty', sgd_l2)
show('SGD with decoupled decay', sgd_wd)
assert math.isclose(sgd_l2, sgd_wd, rel_tol=1e-12, abs_tol=1e-12)
print('\n== first Adam step with zero data gradient ==')
coordinates = [1.0, 100.0]
l2_results, adamw_results = [], []
for value in coordinates:
l2_next, _, _ = adam_l2_step(value, 0.0, fresh_state(), learning_rate, l2_coefficient)
adamw_next, _, _ = adamw_step(value, 0.0, fresh_state(), learning_rate, l2_coefficient)
l2_results.append(l2_next)
adamw_results.append(adamw_next)
show('initial coordinates', coordinates)
show('Adam with L2 penalty', l2_results)
show('AdamW', adamw_results)
show('Adam L2 relative factors', [new / old for old, new in zip(coordinates, l2_results)])
show('AdamW relative factors', [new / old for old, new in zip(coordinates, adamw_results)])
assert not math.isclose(l2_results[0] / coordinates[0], l2_results[1] / coordinates[1])
assert math.isclose(adamw_results[0] / coordinates[0], adamw_results[1] / coordinates[1])
print('\n== L2 regularizer enters Adam moments, AdamW keeps them data-only ==')
theta = 10.0
data_gradient = 0.2
_, l2_state, l2_gradient = adam_l2_step(theta, data_gradient, fresh_state(), learning_rate, l2_coefficient)
_, adamw_state, decayed_theta = adamw_step(theta, data_gradient, fresh_state(), learning_rate, l2_coefficient)
show('data gradient', data_gradient)
show('Adam L2 gradient stored in moments', l2_gradient)
show('AdamW theta after direct decay', decayed_theta)
show('Adam L2 first moment m', l2_state['m'])
show('AdamW first moment m', adamw_state['m'])
show('Adam L2 second moment v', l2_state['v'])
show('AdamW second moment v', adamw_state['v'])
assert math.isclose(l2_gradient, 1.2)
assert math.isclose(l2_state['m'], 0.12)
assert math.isclose(adamw_state['m'], 0.02)
print('\n== repeated AdamW decay with no data gradient ==')
theta = 1.0
state = fresh_state()
for _ in range(5):
theta, state, _ = adamw_step(theta, 0.0, state, learning_rate, l2_coefficient)
expected = (1.0 - learning_rate * l2_coefficient) ** 5
show('five-step AdamW parameter', theta)
show('closed-form (1 - lr*wd)^5', expected)
assert math.isclose(theta, expected, rel_tol=1e-12, abs_tol=1e-12)
print('checks passed')
if __name__ == '__main__':
main()
실제 출력은 다음이다.
== L2 gradient ==
analytical d(0.5*lambda*theta^2)/dtheta: 0.300000000
finite-difference gradient: 0.300000000
== SGD: L2 and decoupled weight decay are equal after coefficient mapping ==
SGD with L2 penalty: 2.990000000
SGD with decoupled decay: 2.990000000
== first Adam step with zero data gradient ==
initial coordinates: [1.000000000, 100.000000000]
Adam with L2 penalty: [0.990000001, 99.990000000]
AdamW: [0.999000000, 99.900000000]
Adam L2 relative factors: [0.990000001, 0.999900000]
AdamW relative factors: [0.999000000, 0.999000000]
== L2 regularizer enters Adam moments, AdamW keeps them data-only ==
data gradient: 0.200000000
Adam L2 gradient stored in moments: 1.200000000
AdamW theta after direct decay: 9.990000000
Adam L2 first moment m: 0.120000000
AdamW first moment m: 0.020000000
Adam L2 second moment v: 0.001440000
AdamW second moment v: 0.000040000
== repeated AdamW decay with no data gradient ==
five-step AdamW parameter: 0.995009990
closed-form (1 - lr*wd)^5: 0.995009990
checks passed
이 출력은 “AdamW가 언제나 더 좋은 validation score를 낸다”는 증명이 아니다. 수치상 확인한 사실은 더 좁다.
- SGD에서는 계수를 맞추면 두 식이 같은 parameter를 낸다.
- 첫 Adam step에서 L2 penalty는 coordinate마다 다른 상대 shrinkage를 냈다.
- AdamW direct decay는 두 coordinate에 같은 비율을 곱했다.
- L2 term이 Adam moment buffer를 바꾸지만 AdamW buffer는 data gradient만 받았다.
성능과 트레이드오프 — AdamW가 없애지 않는 문제
원 논문의 실험 조건과 결과 범위
원 논문은 CIFAR-10·CIFAR-100과 downsampled ImageNet32×32에서 Shake-Shake 기반 26-layer ResNet을 사용했다. batch size는 128, regular image augmentation을 썼고, fixed·step-drop·cosine annealing learning-rate schedule을 비교했다. Figure 1·2는 AdamW가 Adam with L2보다 더 분리된 learning rate·weight decay search space를 보였고 cosine schedule이 더 좋은 결과를 냈다고 보고한다.원 논문 4.1절·그림 1·2
논문은 default base learning rate 0.001 조건에서 AdamW가 CIFAR-10과 ImageNet32×32에서 Adam보다 test error 15% relative improvement를 얻었다고 적었다. 이 수치는 2019 논문의 architecture·training budget·augmentation·normalized decay·schedule 조건의 결과다.원 논문 4.2절·그림 4 오늘날의 LLM, 다른 dataset, 다른 parameter group에서 같은 개선을 보장하지 않는다.
| 주장 | 원문이 뒷받침하는 범위 | 일반화하면 안 되는 말 |
|---|---|---|
| AdamW는 Adam L2와 다른 update다 | algorithm·Proposition 2 | 모든 optimizer의 L2와 decay가 다르다 |
| AdamW search space가 더 separable했다 | 해당 CIFAR·ImageNet32×32 experiments | tuning이 필요 없어졌다 |
| cosine schedule이 해당 실험에서 좋았다 | 논문의 model·budget | 모든 task의 최적 scheduler다 |
| AdamW가 generalization을 개선했다 | 보고된 benchmark 설정 | 모든 model에서 AdamW가 SGD보다 낫다 |
weight decay는 parameter group에 걸린다
optimizer는 params로 전달받은 tensor group에만 update를 적용한다. PyTorch AdamW의 weight_decay도 group option이다. 그래서 fine-tuning에서 “weight decay를 썼다”는 말만으로는 부족하다.
# 정책 예시다. 어떤 tensor를 제외할지는 model·실험 근거가 필요하다.
optimizer = torch.optim.AdamW(
[
{"params": decay_parameters, "weight_decay": 0.01},
{"params": no_decay_parameters, "weight_decay": 0.0},
],
lr=3e-4,
)
이 예는 bias나 normalization parameter를 무조건 제외하라는 원 논문 규칙이 아니다. 저자 논문은 전체 parameter group policy를 LLM fine-tuning 관점에서 규정하지 않았다. production config에는 어떤 parameter names·shapes가 어느 group에 들어갔는지, group별 lr·weight decay가 무엇인지 남겨야 한다.
scheduler와 total update 수를 같이 기록하자
식 (15)처럼 PyTorch AdamW의 shrinkage는 lr에 곱해진다. 같은 weight_decay=0.01도 다음에 따라 누적 효과가 다르다.
- batch size와 dataset 크기가 만들어 내는 steps per epoch
- epoch 수와 gradient accumulation(기울기 누적)
- warmup(워밍업)과 cosine·linear scheduler
- resume(재개) 시 optimizer step count와 scheduler state
- parameter group별 learning rate
paper Appendix B.1의 normalized decay는 이 의존성을 다루려 한 한 제안이다. 현재 framework의 weight_decay 하나에 자동으로 그 정규화가 들어 있다고 가정하지 않는다.
메모리·처리량 비용
AdamW는 parameter마다 보통 first moment와 second moment buffer를 둔다. mixed precision training에서는 master weight, gradient, optimizer state dtype까지 더해 memory가 parameter size보다 몇 배 커질 수 있다. foreach·fused는 throughput을 높일 수 있지만 PyTorch 문서는 foreach가 tensorlist intermediate로 peak memory를 더 사용할 수 있다고 경고한다.PyTorch AdamW 공식 문서
따라서 optimizer 선택은 validation metric만이 아니라 VRAM peak, batch size, checkpoint size, resume time, distributed communication까지 함께 본다. AdamW의 decoupled decay가 optimizer state memory를 없애지는 않는다.
실패와 운영 기준 — config 숫자 하나만 옮기면 생기는 문제
논문 lambda와 framework weight_decay를 혼동하는 실패
| 이름 | 식에서의 의미 | 그대로 복사하면 위험한 이유 |
|---|---|---|
| 논문 식 (4)의 (\lambda_{WD}) | step마다 직접 곱하는 rate | learning rate와 별도 단위의 per-step shrinkage |
| 논문 Algorithm 2의 (\lambda_{WD}) | (\eta_t\lambda\theta)에 들어가는 decay | schedule multiplier와 normalized decay 문맥이 있다 |
PyTorch weight_decay | (\gamma\cdot\texttt{weight_decay})에 곱해지는 coefficient | per-step rate가 lr에 따라 달라진다 |
| L2 coefficient (\lambda_{L2}) | loss의 (\frac{\lambda}{2}\lVert\theta\rVert^2) 계수 | Adam에서는 direct decay와 동치가 아니다 |
논문 README의 -weightDecay 0.025는 Torch code의 normalized schedule과 연결된 example이다. PyTorch experiment에서 같은 0.025를 복사하려면 먼저 update 식·scheduler·step budget이 같은지 확인해야 한다.
checkpoint resume에서 optimizer state는 model weight와 별개다
model state dict(모델 상태 사전)만 저장하고 AdamW state를 잃으면 (m), (v), step count, scheduler state가 초기화될 수 있다. 그 다음 update는 같은 (\theta)에서 시작해도 이전 training trajectory와 다르다. 재현 로그에는 최소한 다음을 남긴다.
- framework와 optimizer implementation version
- AdamW인지 Adam with L2인지, optimizer parameter group 전체
- lr·betas·eps·weight_decay·amsgrad·fused·foreach 설정
- scheduler 종류, current step, warmup·total steps
- gradient clipping, loss scaling, accumulation, batch size
- model·optimizer·scheduler checkpoint를 저장·복원한 순서
PyTorch documentation은 LRScheduler를 초기화한 뒤 optimizer load_state_dict를 호출하지 않으면 loaded learning rate가 덮어써질 수 있다고 경고한다.PyTorch AdamW API 경고 이 문제는 AdamW 논문 알고리즘이 아니라 현재 framework checkpoint contract다.
direct weight decay는 data quality 문제를 고치지 않는다
AdamW가 해결하지 않는 것들도 분명하다.
| 문제 | AdamW만으로 해결되지 않는 이유 |
|---|---|
| label 오류·data leakage | optimizer가 학습 data의 잘못된 정답을 판별하지 못한다 |
| hallucination(환각) | LLM generation·retrieval·alignment 문제다 |
| exploding loss | learning rate, initialization, numerical overflow, data 문제가 함께 원인일 수 있다 |
| validation regression | model capacity·data split·schedule·parameter groups를 측정해야 한다 |
| GPU OOM(Out Of Memory, 메모리 부족) | AdamW state가 오히려 optimizer memory를 요구한다 |
대안과 선택 기준 — Adam, AdamW, SGD를 이름으로 고르지 말자
| 선택지 | regularization 경로 | 장점 | 주의점 | | --- | --- | --- | | SGD + L2 | gradient에 (\lambda\theta)를 더함 | 계수 대응 시 weight decay와 같은 update | lr가 바뀌면 mapping도 바뀐다 | | SGDW | parameter를 직접 shrink | decay와 momentum gradient를 분리 | scheduler·step budget은 여전히 영향 | | Adam + L2 | data·L2 gradient를 m·v에 함께 저장 | 일부 기존 code와 checkpoint의 의미를 유지 | adaptive preconditioner가 regularizer도 변형 | | AdamW | data gradient만 m·v에 저장, direct shrink | Adam의 adaptive path와 decay를 분리 | lr·group·scheduler tuning은 필요 | | RMSprop·AdaGrad 계열 | optimizer마다 adaptive rule이 다름 | task별 장점 | L2와 direct decay 동치 여부를 식으로 확인해야 함 |
원 논문 Proposition 2는 general adaptive optimizer가 (M_t\ne kI)라는 non-scalar preconditioner를 쓰면 L2와 direct decay가 일반적으로 동치가 아니라고 설명한다. 따라서 optimizer 이름만 Adam에서 다른 적응형 optimizer로 바꿔도 자동으로 동일하다고 가정하지 않는다.원 논문 Appendix A Proposition 2
흔한 오해와 최초 질문에 대한 답
AdamW는 Adam보다 weight를 더 많이 줄이나
항상 그렇지 않다. data gradient, parameter 값, m·v history, lr schedule, group 설정에 따라 달라진다. AdamW의 정의상 차이는 “얼마나 많이”가 아니라 direct shrinkage가 m·v adaptive path를 통과하지 않는다는 것이다.
AdamW의 weight decay는 learning rate와 독립인가
adaptive normalization과는 분리됐지만 per-step shrinkage는 PyTorch에서 (\mathrm{lr}\times\mathrm{weight_decay})다. learning rate schedule이 바뀌면 decay trajectory도 바뀐다.
L2 penalty를 loss에 더하면 AdamW와 같은가
아니다. Adam에서는 L2 gradient가 moment와 variance에 들어간다. 위 직접 실행처럼 parameter 값이 다른 coordinate가 같은 상대 비율로 줄지 않을 수 있다.
논문의 0.025를 내 LLM PyTorch config에 넣으면 되나
아니다. 논문의 normalized decay, batch size, total epochs, schedule, Torch code와 PyTorch AdamW의 parameterization이 다를 수 있다. 일단 update 식과 total steps를 확인하고 validation·weight norm·loss·throughput을 측정하며 tune해야 한다.
AdamW를 쓰면 generalization이 보장되나
아니다. 논문은 특정 image classification experiments에서 개선을 보였다. optimizer는 data, architecture, schedule, regularization, evaluation design의 일부다.
한 문장 답
AdamW는 L2 penalty를 Adam gradient에 섞는 대신, parameter를 별도 비율로 직접 줄여 weight decay가 Adam의 moment·variance history와 adaptive denominator에 왜곡되지 않게 만든 optimizer다. 하지만 decay의 실제 누적량은 lr·scheduler·step 수·parameter group에 여전히 의존한다.
출처 및 검증 경로
1차 자료
- Ilya Loshchilov, Frank Hutter. Decoupled Weight Decay Regularization, ICLR 2019. 이 글은 본문 1절부터 5절, Algorithm 1·2, Proposition 1·2·3, Figure 1부터 4, Appendix A·B.1을 직접 확인했다.
- loshchil. AdamW-and-SGDW 공식 저장소, commit
0ae5185b3c655e45cc249adaca4457cd881874cc.UPDATETORCHFILES/adam.lua와train.lua의 L2·direct decay 분기, normalized schedule 처리를 직접 확인했다. - PyTorch. AdamW source, commit
526b9cee8c9ad8da312bf33ea2c3f308cbfe8876.decoupled_weight_decay=True전달 경로를 확인했다. - PyTorch. torch.optim.AdamW current API. 현재 default·algorithm·fused/foreach·scheduler load 경계의 공식 문서다. 이 API는 2019 논문의 Torch implementation과 version·parameterization이 다를 수 있다.
직접 실행
- Python
3.9.6, 표준 라이브러리만 사용했다. L2 derivative의 finite difference, SGD L2와 direct decay의 계수 대응, zero data gradient에서 Adam L2·AdamW 상대 shrinkage, moment buffer 차이, AdamW 5-step closed form을 위 코드로 실행했고 본문 출력은 실제 결과다. - 이 검증은 PyTorch kernel, CUDA fused optimizer, mixed precision, distributed optimizer, CIFAR-10·ImageNet32×32 training, 논문 Figure의 test error 재현이 아니다. 그런 성능 수치는 원 논문의 명시된 experiment 조건에서 나온 외부 결과로 인용했다.
이 글의 해석
- “moment 오염”은 L2 regularizer gradient가 data gradient와 합쳐져 m·v state에 저장된다는 계산적 의미로 썼다. data 자체가 오염됐다는 뜻이 아니다.
bias·normalization parameter의 no-decay group 예시는 production policy가 명시적으로 필요함을 보이는 예시다. 원 논문이 모든 LLM에 적용할 고정 exclusion rule을 제시한 것은 아니다.
댓글