Momentum, RMSProp, Adam, AdamW
Как из «голого» градиентного спуска шаг за шагом получается AdamW — оптимизатор, которым обучены почти все LLM, — и зачем learning rate сначала разогревают, а потом гасят по косинусу.
Зачем это нужно
Заголовок раздела «Зачем это нужно»В уроке 1.4.1 градиентный спуск споткнулся о две проблемы. В «оврагах» он зигзагует: шаг ограничен самым крутым направлением, а вдоль пологого дна движение мучительно медленное. И learning rate один на все параметры, хотя у нейросети они живут в совершенно разных масштабах: градиенты разных слоёв и параметров могут различаться на порядки.
Решение собиралось по кусочкам больше полувека, от 1960-х до 2017 года, и все кусочки сошлись в одном оптимизатореоптимизаторEN optimizer — AdamWAdamWEN AdamW (Adam with decoupled weight decay)Adam, в котором затухание весов применяется отдельно от адаптивного шага. Оптимизатор по умолчанию для обучения LLM.. Им обучены GPT-3, Llama, Qwen, DeepSeek-V3, MiniMind из урока 5.6.1 и, скорее всего, любая модель, которую вы запустите в этом курсе. Цель урока — собрать AdamW своими руками, деталь за деталью, понять каждую из пяти его строк и научиться читать «рецепт оптимизации» в техотчётах.
flowchart LR A["SGD"] -->|"+ инерция"| B["Momentum"] A -->|"+ свой шаг<br/>для каждого параметра"| C["AdaGrad → RMSProp"] B --> D["Adam<br/>(+ поправка смещения)"] C --> D D -->|"+ развязанный<br/>weight decay"| E["AdamW"] E -.->|"матрицы целиком,<br/>ступень 5"| F["Muon<br/>(Джордан; Kimi K2)"]
Кирпичик: экспоненциальное скользящее среднее
Заголовок раздела «Кирпичик: экспоненциальное скользящее среднее»Все оптимизаторы этого урока построены на одной операции — экспоненциальное скользящее среднееэкспоненциальное скользящее среднее (EMA)EN exponential moving average (EMA)m ← βm + (1 − β)x. Помнит примерно последние 1/(1 − β) значений; при β = 0.9 — около 10. (EMA):
Читаем: «новое среднее — это на старое среднее плюс на свежее значение». Старые значения забываются экспоненциально: значение, пришедшее на шаг раньше, весит в раз меньше, на два шага раньше — в раз меньше и так далее. Грубо говоря, EMA помнит последние значений: при — около 10, при — около 1000.
EMA сглаживает шум. Если градиенты мини-батчей скачут вокруг истинного значения (урок 1.4.1), их EMA гораздо ближе к истине, чем любой отдельный градиент. Но у EMA есть изъян, к которому мы вернёмся: она стартует с нуля. Если все , то при получаем , потом , потом … До единицы среднее доползёт только через десятки шагов.
Momentum: инерция
Заголовок раздела «Momentum: инерция»Представьте тяжёлый шар, катящийся по склону. Он не останавливается мгновенно и не разворачивается от каждой кочки: у него есть скорость. В оврагах это ровно то, что нужно. Вдоль дна градиент всё время указывает в одну сторону, и скорость накапливается. Поперёк оврага знак градиента чередуется (зигзаг), и вклады гасят друг друга.
импульсимпульс (momentum)EN momentumШаг делается по накопленной «скорости» v = βv + g, а не по текущему градиенту. Гасит зигзаги и разгоняет движение вдоль оврага. в той форме, как он реализован в torch.optim.SGD(momentum=0.9):
Здесь — градиент на текущем шаге, — накопленная «скорость», обычно 0.9. Сначала обновляем скорость, потом делаем шаг по ней. Если градиент постоянен, например всё время , скорость растёт так: , затем , затем , , … и приближается к : вдоль дна оврага шаг становится в 10 раз длиннее. Если градиент меняет знак через шаг (), то , , , … — остаётся порядка , и зигзаг не раскачивается. Идею предложил советский математик Борис Поляк в 1964 году — отсюда другое название, «метод тяжёлого шарика» (heavy ball).
RMSProp: свой шаг для каждого параметра
Заголовок раздела «RMSProp: свой шаг для каждого параметра»Momentum ускоряет, но не решает вторую проблему — один learning rate на всех. Идея RMSPropRMSPropEN RMSProp (root mean square propagation)Делит шаг каждого параметра на корень из EMA квадратов его градиентов — адаптивный learning rate по координатам. Предложен Хинтоном в курсе лекций, без статьи.: поделить шаг каждого параметра на типичный размер его градиента. Типичный размер оценим как корень из EMA квадратов:
Все операции — покоординатные: у каждого параметра свои и свой эффективный шаг. Параметр, у которого градиенты огромные, делит их на большое . Параметр с крошечными градиентами делит на маленькое. В итоге оба шагают примерно на . Числа: если градиент параметра всё время равен 100, то приближается к , , и шаг . Если градиент всё время 0.01, то , и шаг снова . В «овраге», вытянутом вдоль осей, это значит: шаги вдоль пологого дна и поперёк крутых стенок становятся одного порядка, и спуск больше не ползёт вдоль дна. Маленькое защищает от деления на ноль (урок 1.0.3).
Предшественник RMSProp, AdaGrad (2011), копил сумму всех квадратов градиентов. Сумма только растёт, поэтому шаг со временем падал до нуля, и обучение останавливалось. RMSProp заменил сумму на EMA, которая «забывает» старые градиенты. Любопытно, что RMSProp так и не вышел отдельной статьёй: Джеффри Хинтон предложил его в слайдах своего онлайн-курса, и весь мир ссылается на эти слайды.
Adam: всё вместе
Заголовок раздела «Adam: всё вместе»AdamAdamEN Adam (adaptive moment estimation)Momentum + RMSProp + поправка смещения. Стандартный оптимизатор нейросетей с 2015 года. (Kingma и Ba, 2014) объединяет обе идеи. Название — от adaptive moment estimation: «моменты» — это и из урока 1.3.1, а оценивают их через EMA. На шаге (считая с 1) для каждого параметра:
- — градиент на мини-батче;
- — среднее градиента: направление с инерцией;
- — среднее квадрата: масштаб;
- , — поправка смещенияпоправка смещения (bias correction)EN bias correctionДеление EMA на (1 − βᵗ) в Adam; исправляет занижение средних на первых шагах, когда m и v стартуют с нуля.;
- .
Числа: первый шаг (, на старте, , , ), градиент . Строка 2: . Строка 3: . Строка 4: , . Строка 5: , и параметр уменьшается на — ровно на . Почему так вышло, разберём в разделе «Главное свойство» ниже.
Значения по умолчанию: , , . При обучении LLM обычно берут : тогда помнит около 20 последних шагов вместо 1000 и быстрее реагирует на всплески градиента, что делает большие прогоны устойчивее.
Зачем поправка смещения
Заголовок раздела «Зачем поправка смещения»и стартуют с нуля, и первые их значения сильно занижены. Пусть градиент всё время равен 1. Без поправки шаг был бы (в единицах ):
| Шаг | шаг без поправки | шаг с поправкой | ||
|---|---|---|---|---|
| 1 | 0.100 | 0.0010 | 3.16 | 1.00 |
| 10 | 0.651 | 0.0100 | 6.53 | 1.00 |
| 100 | 1.000 | 0.0952 | 3.24 | 1.00 |
| 1000 | 1.000 | 0.6323 | 1.26 | 1.00 |
с «разгоняется» гораздо медленнее, чем . Без поправки первая сотня шагов оказалась бы в 3–6.5 раза длиннее задуманного, следующие несколько сотен — ещё в 1.5–3 раза. Опасно именно тогда, когда веса ещё случайны. Деление на точно компенсирует старт с нуля: при получаем и .
Главное свойство: шаг порядка η
Заголовок раздела «Главное свойство: шаг порядка η»Посмотрите на первый шаг: . Каким бы ни был градиент — 0.001 или 1000, — каждый параметр сдвинется ровно на . И дальше шаг Adam остаётся порядка : это отношение среднего градиента к его типичному размеру. Отсюда два практических следствия:
- learning rate в Adam — это «на сколько примерно может измениться каждый вес за шаг». Поэтому для LLM он маленький, порядка : веса имеют порядок (инициализация из урока 1.3.1), и менять их на десятки процентов за шаг нельзя;
- Adam почти не замечает масштаба loss. Умножьте функцию потерь на 1000 — траектория Adam не изменится (если не считать крошечного ), а SGD с прежним learning rate разлетится.
Сравним три оптимизатора на «овраге» , где отношение кривизн уже 1000. Старт , 100 шагов, для каждого метода — лучший learning rate из логарифмической сетки:
| Метод | Лучший | Финиш | |
|---|---|---|---|
| SGD | 0.178 | 0.284 | |
| Momentum | 0.316 | ||
| Adam | 0.316 |
SGD за 100 шагов прошёл по дну оврага всего 1.5 единицы из 9: больший шаг запрещён крутыми стенками. Momentum и Adam дошли почти до дна. На идеальной квадратичной чаше хорошо настроенный momentum может даже обогнать Adam. Сила Adam в другом: устойчивость. Один и тот же learning rate годится для слоёв с совершенно разными масштабами градиентов, и подбирать его приходится гораздо реже.
Посмотрите на это вживую. В песочнице «Овраг» из прошлого урока. Увеличивайте learning rate: после 0.5 красный SGD разлетается (правило из урока 1.4.1), синий Momentum держится примерно до 0.95, а жёлтый Adam остаётся устойчивым на всём диапазоне ползунка — его шаг порядка и не зависит от крутизны стенок. На маленьких learning rate (около 0.05), наоборот, впереди Momentum. Затем переключитесь на функцию Розенброка — изогнутый «банан», классический тест оптимизаторов.
Weight decay и AdamW
Заголовок раздела «Weight decay и AdamW»затухание весовзатухание весов (weight decay)EN weight decayНа каждом шаге веса умножаются на (1 − ηλ) — «притягиваются» к нулю. Для обычного SGD равносильно L2-регуляризации, для Adam — нет. на каждом шаге слегка уменьшает все веса: , то есть умножает их на . Например, при и каждый шаг умножает веса на . Веса «притягиваются» к нулю, и модели труднее подогнаться под шум в данных — «зазубрить» обучающие примеры вместо закономерностей. Это называют переобучениемпереобучениеEN overfitting, а борьбу с ним — регуляризацией (подробно — в уроке 2.1.2). Для обычного SGD это то же самое, что добавить к loss L2-штраф : его градиент как раз .
В Adam это уже не одно и то же. Если добавить L2-штраф в loss, то слагаемое попадёт в градиент , а затем поделится на . Пусть два веса равны 1.0, (штраф ), но типичный градиент первого — 10, а второго — 0.01.
- У первого , и штраф даёт в шаге всего (в единицах ).
- У второго штраф в 10 раз больше его собственного градиента, поэтому , и штраф даёт — почти весь шаг уходит на сжатие веса к нулю.
Разница в сто раз. Веса, которые активно учатся, почти не регуляризуются, а слабо обновляемые сжимаются изо всех сил. Задумывалось совсем не это: в AdamW оба веса получили бы одинаковое затухание .
AdamW (Лощилов и Хуттер, 2017) развязывает затухание и адаптивный шаг:
Затухание одинаково для всех весов и не делится на . В PyTorch это два разных класса:
torch.optim.Adam(weight_decay=…) — это Adam с L2, а torch.optim.AdamW(weight_decay=…) — правильный развязанный
вариант. Для LLM почти всегда берут AdamW с . Затухание обычно не применяют к смещениям (bias)
и коэффициентам нормализаций (обучаемым множителям в слоях вроде RMSNorm, урок 1.1.1): это не «знания» модели, а сдвиги
и масштабы, и тянуть их к нулю вредно.
Пример, где величина weight decay решает исход обучения, — гроккинггроккингEN grokkingЗапоздалое обобщение: модель давно выучила train (точность ≈ 100%), на валидации долго держится на уровне угадывания, а потом точность на валидации резко растёт почти до 100%. Описан в статье Power et al. (2022) на модульной арифметике; weight decay сильно ускоряет переход. В китайских текстах часто пишут просто Grokking. из урока 11.4.1: маленькая сеть сначала зазубривает примеры, а обобщать начинает много позже, и с это происходит в 3–4 раза раньше, чем с .
Расписание learning rate: разогрев и косинус
Заголовок раздела «Расписание learning rate: разогрев и косинус»Последняя деталь рецепта — learning rate меняют по ходу обучения по заданному расписаниюрасписание learning rateEN learning rate scheduleПравило, по которому learning rate меняется по ходу обучения — разогрев, плато, спад..
Разогрев (разогрев learning rateразогрев learning rate (warmup)EN learning rate warmupПервые сотни–тысячи шагов learning rate линейно растёт от нуля до пика, пока статистики Adam не накопились.). В начале веса случайны, градиенты велики и хаотичны, а оценки в Adam
построены по считанным шагам. Полный learning rate в этот момент может «взорвать» обучение: loss подскакивает,
появляются nan. Поэтому первые сотни или тысячи шагов learning rate линейно растёт от почти нуля до пика.
У DeepSeek-V3 разогрев длился 2000 шагов.
Спад (косинусное затухание learning rateкосинусное затухание learning rateEN cosine decay (cosine annealing)После разогрева learning rate плавно спадает от пика до минимума по половине косинусоиды.). В конце обучения мешает шум мини-батчей (урок 1.4.1): с большим шагом параметры «дрожат» вокруг минимума и не могут на него осесть. Уменьшение шага гасит это дрожание, и loss в фазе спада заметно падает. Самый популярный вариант — половина косинусоиды от пика до минимума (часто 10% пика):
Читаем: — номер шага, — всего шагов, — длина разогрева. Дробь под косинусом показывает, какая доля спада пройдена, и превращает её в угол от 0 до (в радианах — это 180°, см. напоминание о косинусе в уроке 1.1.1). При угол 0, косинус равен 1, и ; при угол (180°), косинус равен , и . Посередине угол (90°), косинус 0 — ровно полусумма. Числа: при и на середине спада .
Вот как это собрать в PyTorch, заодно отключив weight decay для смещений:
import mathimport torch
model = torch.nn.Linear(16, 4) # игрушечная «модель» из урока 1.1.2# named_parameters() перебирает пары (имя, массив параметров); p.dim() — число осей массива:# у матрицы весов 2 оси, у вектора смещений — 1decay = [p for n, p in model.named_parameters() if p.dim() >= 2] # матрицы весовno_decay = [p for n, p in model.named_parameters() if p.dim() < 2] # смещения (bias)opt = torch.optim.AdamW( [{"params": decay, "weight_decay": 0.1}, # weight decay только для матриц {"params": no_decay, "weight_decay": 0.0}], lr=3e-4, betas=(0.9, 0.95), eps=1e-8, # типичные настройки для LLM)
total, warmup, min_ratio = 1000, 100, 0.1def lr_lambda(step): # множитель к lr на шаге step if step < warmup: return (step + 1) / warmup # линейный разогрев progress = (step - warmup) / (total - warmup) # от 0 до 1 return min_ratio + (1 - min_ratio) * 0.5 * (1 + math.cos(math.pi * progress))
sched = torch.optim.lr_scheduler.LambdaLR(opt, lr_lambda) # на каждом шаге lr = 3e-4 · lr_lambda(step)for step in range(total): # ... здесь были бы loss.backward() и opt.step() ... if step in (0, 49, 99, 100, 550, 999): print(step, f"{opt.param_groups[0]['lr']:.2e}") opt.step() sched.step()0 3.00e-0649 1.50e-0499 3.00e-04100 3.00e-04550 1.65e-04999 3.00e-05Разогрев за 100 шагов поднял learning rate от до пика . К середине спада он упал
почти вдвое — до полусуммы пика и минимума, 55% пика, — а к концу до 10% пика. Порядок вызовов важен: сначала
opt.step(), потом sched.step(). Этот же каркас вы используете в цикле обучения GPT (урок 4.6.3).
xychart-beta title "Learning rate: разогрев 100 шагов, затем косинус до 10% пика" x-axis "шаг" [0, 50, 100, 150, 200, 250, 300, 350, 400, 450, 500, 550, 600, 650, 700, 750, 800, 850, 900, 950, 1000] y-axis "lr, ×10⁻⁴" 0 --> 3.2 line [0.03, 1.53, 3.0, 2.98, 2.92, 2.82, 2.68, 2.52, 2.33, 2.11, 1.88, 1.65, 1.42, 1.19, 0.98, 0.78, 0.62, 0.48, 0.38, 0.32, 0.3]
Анонс: Muon
Заголовок раздела «Анонс: Muon»Adam обращается с каждым числом по отдельности. Но большинство параметров LLM — матрицы, и у матрицы есть структура, которую покоординатный подход не видит. Оптимизатор Muon (Келлер Джордан, 2024) берёт накопленный momentum для матрицы весов и «ортогонализует» его: делает все сингулярные числа (урок 1.1.3) примерно равными единице, чтобы обновление меняло матрицу равномерно по всем направлениям. Считают это не через дорогой SVD, а несколькими итерациями Ньютона–Шульца — это просто матричные умножения. Хранит Muon только momentum, то есть вдвое меньше состояния, чем Adam. Эмбеддинги, выходной слой и параметры нормализаций при этом обычно по-прежнему учат AdamW.
Moonshot AI (Kimi) в работе Moonlight показала, что Muon работает и на больших LLM, а в Kimi K2 его доработка MuonClip позволила пройти весь претрейн без всплесков loss. Подробно — в уроке 5.4.4.
Практика
Заголовок раздела «Практика»Реализуйте шаги momentum и Adam/AdamW и косинусное расписание с разогревом. Готовая функция run сравнит SGD,
Momentum и Adam на овраге с отношением кривизн 1000. Тесты сверят ваши числа с torch.optim.Adam и torch.optim.AdamW.
import math
def momentum_step(x, g, v, lr, beta=0.9):
"""SGD с инерцией (как torch.optim.SGD(momentum=beta)).
v_new = beta·v + g, x_new = x − lr·v_new (покоординатно).
Верните пару НОВЫХ списков (x_new, v_new).
"""
# TODO: ваш код
...
def adam_step(x, g, m, v, t, lr, beta1=0.9, beta2=0.999, eps=1e-8, weight_decay=0.0):
"""Шаг Adam; при weight_decay > 0 — AdamW (затухание весов отдельно от адаптивного шага).
t — номер шага, начиная с 1. Для каждой координаты:
m = beta1·m + (1 − beta1)·g
v = beta2·v + (1 − beta2)·g²
m̂ = m / (1 − beta1^t), v̂ = v / (1 − beta2^t)
x = x − lr·(m̂ / (√v̂ + eps) + weight_decay·x)
Верните тройку НОВЫХ списков (x_new, m_new, v_new).
"""
# TODO: ваш код
...
def cosine_lr(step, total_steps, peak_lr, warmup_steps, min_lr=0.0):
"""Learning rate на шаге step (шаги считаются с 0): разогрев + косинус.
step < warmup_steps: peak_lr · (step + 1) / warmup_steps — линейный рост
дальше: progress = (step − warmup_steps) / (total_steps − warmup_steps), не больше 1;
min_lr + 0.5·(peak_lr − min_lr)·(1 + cos(π·progress))
"""
# TODO: ваш код
...
# ── Готовый код: сравнение оптимизаторов (менять не нужно) ──────────────────
def run(method, grad, x0, lr, steps):
"""Запускает 'sgd', 'momentum' или 'adam' и возвращает последнюю точку."""
x = list(x0)
s1 = [0.0] * len(x) # v для momentum или m для Adam
s2 = [0.0] * len(x) # v для Adam
for t in range(1, steps + 1):
g = grad(x)
if method == "sgd":
x = [xi - lr * gi for xi, gi in zip(x, g)]
elif method == "momentum":
x, s1 = momentum_step(x, g, s1, lr)
else:
x, s1, s2 = adam_step(x, g, s1, s2, t, lr)
return x
# «Овраг» с числом обусловленности 1000: 0.005x² + 5y²
f = lambda p: 0.005 * p[0] * p[0] + 5 * p[1] * p[1]
grad = lambda p: [0.01 * p[0], 10 * p[1]]
# Проверьте себя кнопкой «Запустить»:
print(momentum_step([1.0], [1.0], [0.0], 0.1)) # ([0.9], [1.0])
print(adam_step([0.0, 0.0], [0.001, -1000.0], [0.0, 0.0], [0.0, 0.0], 1, 0.1)) # x ≈ [-0.1, 0.1]
print([cosine_lr(s, 1000, 3e-4, 100, 3e-5) for s in (0, 99, 550, 1000)]) # 3e-6, 3e-4, 1.65e-4, 3e-5
# Когда реализуете функции, раскомментируйте сравнение на «овраге»:
# for method, lr in [("sgd", 0.19), ("momentum", 0.3), ("adam", 0.3)]:
# x = run(method, grad, [-9.0, 3.5], lr, 100)
# print(method, [round(c, 3) for c in x], f"f = {f(x):.4f}")
Проверь себя
Заголовок раздела «Проверь себя»- Momentum накапливает скорость: разгоняет движение вдоль оврага и гасит зигзаги поперёк.
- RMSProp делит шаг каждого параметра на корень из EMA квадратов его градиентов — у каждого веса свой шаг.
- Adam = momentum + RMSProp + поправка смещения. Его шаг порядка в каждой координате и не зависит от масштаба loss. Цена — два дополнительных числа на параметр.
- AdamW вычитает weight decay отдельно от адаптивного шага; для LLM типично , .
- Learning rate разогревают в начале и гасят в конце: косинус, ступени или WSD, как у DeepSeek и MiniCPM.
Дальше: ступень 2 — что значит «модель учится»: задача, данные, функция потерь и первые модели, которые вы обучите градиентным спуском.