Перейти к содержимому
Ступень 11.4 Оптимизация

Momentum, RMSProp, Adam, AdamW

Как из «голого» градиентного спуска шаг за шагом получается AdamW — оптимизатор, которым обучены почти все LLM, — и зачем learning rate сначала разогревают, а потом гасят по косинусу.

УРОКstage-1/1-4-optimization/02-momentum-adam.mdx1 ч 50 минУрок 2 из 2
После урока вы сможете
  • Объяснить, что дают инерция (momentum) и адаптивный шаг (RMSProp), и собрать из них Adam по формулам
  • Объяснить поправку смещения в Adam и почему его первый шаг равен learning rate в каждой координате
  • Отличить AdamW от Adam с L2-регуляризацией и правильно настроить weight decay в PyTorch
  • Построить расписание learning rate с разогревом и косинусным спадом и прочитать рецепт оптимизации из техотчёта DeepSeek-V3

В уроке 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):

m←β m+(1−β) xm \leftarrow \beta\, m + (1 - \beta)\, x

Читаем: «новое среднее — это на β\beta старое среднее плюс на 1−β1 - \beta свежее значение». Старые значения забываются экспоненциально: значение, пришедшее на шаг раньше, весит в β\beta раз меньше, на два шага раньше — в β2\beta^2 раз меньше и так далее. Грубо говоря, EMA помнит последние 1/(1−β)1/(1-\beta) значений: при β=0.9\beta = 0.9 — около 10, при β=0.999\beta = 0.999 — около 1000.

EMA сглаживает шум. Если градиенты мини-батчей скачут вокруг истинного значения (урок 1.4.1), их EMA гораздо ближе к истине, чем любой отдельный градиент. Но у EMA есть изъян, к которому мы вернёмся: она стартует с нуля. Если все x=1x = 1, то при β=0.9\beta = 0.9 получаем m=0.1m = 0.1, потом 0.190.19, потом 0.2710.271… До единицы среднее доползёт только через десятки шагов.

Представьте тяжёлый шар, катящийся по склону. Он не останавливается мгновенно и не разворачивается от каждой кочки: у него есть скорость. В оврагах это ровно то, что нужно. Вдоль дна градиент всё время указывает в одну сторону, и скорость накапливается. Поперёк оврага знак градиента чередуется (зигзаг), и вклады гасят друг друга.

импульсимпульс (momentum)EN momentumШаг делается по накопленной «скорости» v = βv + g, а не по текущему градиенту. Гасит зигзаги и разгоняет движение вдоль оврага. в той форме, как он реализован в torch.optim.SGD(momentum=0.9):

v←β v+g,θ←θ−η vv \leftarrow \beta\, v + g, \qquad \theta \leftarrow \theta - \eta\, v

Здесь gg — градиент на текущем шаге, vv — накопленная «скорость», β\beta обычно 0.9. Сначала обновляем скорость, потом делаем шаг по ней. Если градиент постоянен, например всё время g=1g = 1, скорость растёт так: v=1v = 1, затем 0.9⋅1+1=1.90.9 \cdot 1 + 1 = 1.9, затем 0.9⋅1.9+1=2.710.9 \cdot 1.9 + 1 = 2.71, 3.443.44, … и приближается к g/(1−β)=10g / (1 - \beta) = 10: вдоль дна оврага шаг становится в 10 раз длиннее. Если градиент меняет знак через шаг (+1,−1,+1,…+1, -1, +1, \dots), то v=1v = 1, 0.9−1=−0.10.9 - 1 = -0.1, −0.09+1=0.91-0.09 + 1 = 0.91, … — остаётся порядка gg, и зигзаг не раскачивается. Идею предложил советский математик Борис Поляк в 1964 году — отсюда другое название, «метод тяжёлого шарика» (heavy ball).

Momentum ускоряет, но не решает вторую проблему — один learning rate на всех. Идея RMSPropRMSPropEN RMSProp (root mean square propagation)Делит шаг каждого параметра на корень из EMA квадратов его градиентов — адаптивный learning rate по координатам. Предложен Хинтоном в курсе лекций, без статьи.: поделить шаг каждого параметра на типичный размер его градиента. Типичный размер оценим как корень из EMA квадратов:

s←β s+(1−β) g2,θ←θ−η gs+εs \leftarrow \beta\, s + (1 - \beta)\, g^2, \qquad \theta \leftarrow \theta - \eta\, \frac{g}{\sqrt{s} + \varepsilon}

Все операции — покоординатные: у каждого параметра свои ss и свой эффективный шаг. Параметр, у которого градиенты огромные, делит их на большое s\sqrt{s}. Параметр с крошечными градиентами делит на маленькое. В итоге оба шагают примерно на η\eta. Числа: если градиент параметра всё время равен 100, то ss приближается к 1002=10 000100^2 = 10\,000, s=100\sqrt{s} = 100, и шаг η⋅100/100=η\eta \cdot 100 / 100 = \eta. Если градиент всё время 0.01, то s=0.01\sqrt{s} = 0.01, и шаг снова η\eta. В «овраге», вытянутом вдоль осей, это значит: шаги вдоль пологого дна и поперёк крутых стенок становятся одного порядка, и спуск больше не ползёт вдоль дна. Маленькое ε≈10−8\varepsilon \approx 10^{-8} защищает от деления на ноль (урок 1.0.3).

Предшественник RMSProp, AdaGrad (2011), копил сумму всех квадратов градиентов. Сумма только растёт, поэтому шаг со временем падал до нуля, и обучение останавливалось. RMSProp заменил сумму на EMA, которая «забывает» старые градиенты. Любопытно, что RMSProp так и не вышел отдельной статьёй: Джеффри Хинтон предложил его в слайдах своего онлайн-курса, и весь мир ссылается на эти слайды.

AdamAdamEN Adam (adaptive moment estimation)Momentum + RMSProp + поправка смещения. Стандартный оптимизатор нейросетей с 2015 года. (Kingma и Ba, 2014) объединяет обе идеи. Название — от adaptive moment estimation: «моменты» — это E[g]\mathbb{E}[g] и E[g2]\mathbb{E}[g^2] из урока 1.3.1, а оценивают их через EMA. На шаге tt (считая с 1) для каждого параметра:

  1. g=∇θLg = \nabla_\theta \mathcal{L} — градиент на мини-батче;
  2. m←β1m+(1−β1) gm \leftarrow \beta_1 m + (1 - \beta_1)\, g — среднее градиента: направление с инерцией;
  3. v←β2v+(1−β2) g2v \leftarrow \beta_2 v + (1 - \beta_2)\, g^2 — среднее квадрата: масштаб;
  4. m^=m1−β1t\hat m = \dfrac{m}{1 - \beta_1^t}, v^=v1−β2t\hat v = \dfrac{v}{1 - \beta_2^t} — поправка смещенияпоправка смещения (bias correction)EN bias correctionДеление EMA на (1 − βᵗ) в Adam; исправляет занижение средних на первых шагах, когда m и v стартуют с нуля.;
  5. θ←θ−η m^v^+ε\theta \leftarrow \theta - \eta\, \dfrac{\hat m}{\sqrt{\hat v} + \varepsilon}.

Числа: первый шаг (t=1t = 1, m=v=0m = v = 0 на старте, β1=0.9\beta_1 = 0.9, β2=0.999\beta_2 = 0.999, η=0.001\eta = 0.001), градиент g=0.5g = 0.5. Строка 2: m=0.9⋅0+0.1⋅0.5=0.05m = 0.9 \cdot 0 + 0.1 \cdot 0.5 = 0.05. Строка 3: v=0.001⋅0.52=0.00025v = 0.001 \cdot 0.5^2 = 0.00025. Строка 4: m^=0.05/(1−0.9)=0.5\hat m = 0.05 / (1 - 0.9) = 0.5, v^=0.00025/(1−0.999)=0.25\hat v = 0.00025 / (1 - 0.999) = 0.25. Строка 5: 0.25=0.5\sqrt{0.25} = 0.5, и параметр уменьшается на 0.001⋅0.5/0.5=0.0010.001 \cdot 0.5 / 0.5 = 0.001 — ровно на η\eta. Почему так вышло, разберём в разделе «Главное свойство» ниже.

Значения по умолчанию: β1=0.9\beta_1 = 0.9, β2=0.999\beta_2 = 0.999, ε=10−8\varepsilon = 10^{-8}. При обучении LLM обычно берут β2=0.95\beta_2 = 0.95: тогда vv помнит около 20 последних шагов вместо 1000 и быстрее реагирует на всплески градиента, что делает большие прогоны устойчивее.

mm и vv стартуют с нуля, и первые их значения сильно занижены. Пусть градиент всё время равен 1. Без поправки шаг был бы m/vm / \sqrt{v} (в единицах η\eta):

Шаг ttmmvvшаг без поправкишаг с поправкой
10.1000.00103.161.00
100.6510.01006.531.00
1001.0000.09523.241.00
10001.0000.63231.261.00

vv с β2=0.999\beta_2 = 0.999 «разгоняется» гораздо медленнее, чем mm. Без поправки первая сотня шагов оказалась бы в 3–6.5 раза длиннее задуманного, следующие несколько сотен — ещё в 1.5–3 раза. Опасно именно тогда, когда веса ещё случайны. Деление на 1−βt1 - \beta^t точно компенсирует старт с нуля: при t=1t = 1 получаем m^=g\hat m = g и v^=g2\hat v = g^2.

Посмотрите на первый шаг: m^/v^=g/∣g∣=±1\hat m / \sqrt{\hat v} = g / \lvert g \rvert = \pm 1. Каким бы ни был градиент — 0.001 или 1000, — каждый параметр сдвинется ровно на η\eta. И дальше шаг Adam остаётся порядка η\eta: это отношение среднего градиента к его типичному размеру. Отсюда два практических следствия:

  • learning rate в Adam — это «на сколько примерно может измениться каждый вес за шаг». Поэтому для LLM он маленький, порядка 10−410^{-4}: веса имеют порядок 10−210^{-2} (инициализация из урока 1.3.1), и менять их на десятки процентов за шаг нельзя;
  • Adam почти не замечает масштаба loss. Умножьте функцию потерь на 1000 — траектория Adam не изменится (если не считать крошечного ε\varepsilon), а SGD с прежним learning rate разлетится.

Сравним три оптимизатора на «овраге» f=0.005x2+5y2f = 0.005x^2 + 5y^2, где отношение кривизн уже 1000. Старт (−9,3.5)(-9, 3.5), 100 шагов, для каждого метода — лучший learning rate из логарифмической сетки:

МетодЛучший η\etaФиниш (x,y)(x, y)ff
SGD0.178(−7.53, 0.00)(-7.53,\ 0.00)0.284
Momentum0.316(−0.04, 0.004)(-0.04,\ 0.004)9⋅10−59 \cdot 10^{-5}
Adam0.316(−0.05, −0.01)(-0.05,\ -0.01)5⋅10−45 \cdot 10^{-4}

SGD за 100 шагов прошёл по дну оврага всего 1.5 единицы из 9: больший шаг запрещён крутыми стенками. Momentum и Adam дошли почти до дна. На идеальной квадратичной чаше хорошо настроенный momentum может даже обогнать Adam. Сила Adam в другом: устойчивость. Один и тот же learning rate годится для слоёв с совершенно разными масштабами градиентов, и подбирать его приходится гораздо реже.

Посмотрите на это вживую. В песочнице «Овраг» 0.1x2+2y20.1x^2 + 2y^2 из прошлого урока. Увеличивайте learning rate: после 0.5 красный SGD разлетается (правило η<2/a\eta < 2/a из урока 1.4.1), синий Momentum держится примерно до 0.95, а жёлтый Adam остаётся устойчивым на всём диапазоне ползунка — его шаг порядка η\eta и не зависит от крутизны стенок. На маленьких learning rate (около 0.05), наоборот, впереди Momentum. Затем переключитесь на функцию Розенброка — изогнутый «банан», классический тест оптимизаторов.

Песочница оптимизаторовинтерактив
Кликните по карте, чтобы выбрать стартовую точку. Звёздочка — минимум функции.

затухание весовзатухание весов (weight decay)EN weight decayНа каждом шаге веса умножаются на (1 − ηλ) — «притягиваются» к нулю. Для обычного SGD равносильно L2-регуляризации, для Adam — нет. на каждом шаге слегка уменьшает все веса: θ←θ−ηλθ\theta \leftarrow \theta - \eta \lambda \theta, то есть умножает их на 1−ηλ1 - \eta\lambda. Например, при η=10−3\eta = 10^{-3} и λ=0.1\lambda = 0.1 каждый шаг умножает веса на 0.99990.9999. Веса «притягиваются» к нулю, и модели труднее подогнаться под шум в данных — «зазубрить» обучающие примеры вместо закономерностей. Это называют переобучениемпереобучениеEN overfitting, а борьбу с ним — регуляризацией (подробно — в уроке 2.1.2). Для обычного SGD это то же самое, что добавить к loss L2-штраф λ2∥θ∥2\frac{\lambda}{2} \lVert \theta \rVert^2: его градиент как раз λθ\lambda \theta.

В Adam это уже не одно и то же. Если добавить L2-штраф в loss, то слагаемое λθ\lambda\theta попадёт в градиент gg, а затем поделится на v^\sqrt{\hat v}. Пусть два веса равны 1.0, λ=0.1\lambda = 0.1 (штраф λθ=0.1\lambda\theta = 0.1), но типичный градиент первого — 10, а второго — 0.01.

  • У первого v^≈10\sqrt{\hat v} \approx 10, и штраф даёт в шаге всего 0.1/10=0.010.1 / 10 = 0.01 (в единицах η\eta).
  • У второго штраф в 10 раз больше его собственного градиента, поэтому v^≈0.1\sqrt{\hat v} \approx 0.1, и штраф даёт 0.1/0.1≈10.1 / 0.1 \approx 1 — почти весь шаг уходит на сжатие веса к нулю.

Разница в сто раз. Веса, которые активно учатся, почти не регуляризуются, а слабо обновляемые сжимаются изо всех сил. Задумывалось совсем не это: в AdamW оба веса получили бы одинаковое затухание ηλθ=0.1η\eta\lambda\theta = 0.1\eta.

AdamW (Лощилов и Хуттер, 2017) развязывает затухание и адаптивный шаг:

θ←θ−η(m^v^+ε+λ θ)\theta \leftarrow \theta - \eta \left( \frac{\hat m}{\sqrt{\hat v} + \varepsilon} + \lambda\, \theta \right)

Затухание ηλθ\eta\lambda\theta одинаково для всех весов и не делится на v^\sqrt{\hat v}. В PyTorch это два разных класса: torch.optim.Adam(weight_decay=…) — это Adam с L2, а torch.optim.AdamW(weight_decay=…) — правильный развязанный вариант. Для LLM почти всегда берут AdamW с λ=0.1\lambda = 0.1. Затухание обычно не применяют к смещениям (bias) и коэффициентам нормализаций (обучаемым множителям в слоях вроде RMSNorm, урок 1.1.1): это не «знания» модели, а сдвиги и масштабы, и тянуть их к нулю вредно.

Пример, где величина weight decay решает исход обучения, — гроккинггроккингEN grokkingЗапоздалое обобщение: модель давно выучила train (точность ≈ 100%), на валидации долго держится на уровне угадывания, а потом точность на валидации резко растёт почти до 100%. Описан в статье Power et al. (2022) на модульной арифметике; weight decay сильно ускоряет переход. В китайских текстах часто пишут просто Grokking. из урока 11.4.1: маленькая сеть сначала зазубривает примеры, а обобщать начинает много позже, и с λ=2\lambda = 2 это происходит в 3–4 раза раньше, чем с λ=1\lambda = 1.

Последняя деталь рецепта — learning rate меняют по ходу обучения по заданному расписаниюрасписание learning rateEN learning rate scheduleПравило, по которому learning rate меняется по ходу обучения — разогрев, плато, спад..

Разогрев (разогрев learning rateразогрев learning rate (warmup)EN learning rate warmupПервые сотни–тысячи шагов learning rate линейно растёт от нуля до пика, пока статистики Adam не накопились.). В начале веса случайны, градиенты велики и хаотичны, а оценки vv в 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% пика):

η(t)=ηmin⁡+12(ηmax⁡−ηmin⁡)(1+cos⁡π (t−twarmup)T−twarmup)\eta(t) = \eta_{\min} + \tfrac{1}{2}(\eta_{\max} - \eta_{\min}) \left(1 + \cos \frac{\pi\, (t - t_{\text{warmup}})}{T - t_{\text{warmup}}}\right)

Читаем: tt — номер шага, TT — всего шагов, twarmupt_{\text{warmup}} — длина разогрева. Дробь под косинусом показывает, какая доля спада пройдена, и превращает её в угол от 0 до π\pi (в радианах π\pi — это 180°, см. напоминание о косинусе в уроке 1.1.1). При t=twarmupt = t_{\text{warmup}} угол 0, косинус равен 1, и η=ηmax⁡\eta = \eta_{\max}; при t=Tt = T угол π\pi (180°), косинус равен −1-1, и η=ηmin⁡\eta = \eta_{\min}. Посередине угол π/2\pi/2 (90°), косинус 0 — ровно полусумма. Числа: при ηmax⁡=3⋅10−4\eta_{\max} = 3 \cdot 10^{-4} и ηmin⁡=3⋅10−5\eta_{\min} = 3 \cdot 10^{-5} на середине спада η=3⋅10−5+12(3⋅10−4−3⋅10−5)=1.65⋅10−4\eta = 3 \cdot 10^{-5} + \frac{1}{2}(3 \cdot 10^{-4} - 3 \cdot 10^{-5}) = 1.65 \cdot 10^{-4}.

Вот как это собрать в PyTorch, заодно отключив weight decay для смещений:

import math
import torch
model = torch.nn.Linear(16, 4) # игрушечная «модель» из урока 1.1.2
# named_parameters() перебирает пары (имя, массив параметров); p.dim() — число осей массива:
# у матрицы весов 2 оси, у вектора смещений — 1
decay = [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.1
def 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-06
49 1.50e-04
99 3.00e-04
100 3.00e-04
550 1.65e-04
999 3.00e-05

Разогрев за 100 шагов поднял learning rate от 3⋅10−63 \cdot 10^{-6} до пика 3⋅10−43 \cdot 10^{-4}. К середине спада он упал почти вдвое — до полусуммы пика и минимума, 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]
Расписание из кода выше: быстрый линейный подъём, долгий плавный спуск по косинусу

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.

Momentum, Adam, AdamW и расписание learning rate7 тестов
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}")
Проверь себя6 вопросов
1 EMA с β=0.9\beta = 0.9 стартует с m=0m = 0, на вход дважды подаётся 1. Чему равно mm после второго шага (без поправки смещения)?
2 Adam, первый шаг, η=10−3\eta = 10^{-3}. Градиент параметра равен 0.0030.003. На сколько примерно сдвинется параметр?
3 Какие утверждения про AdamW верны?Можно выбрать несколько ответов
4 Сколько гигабайт занимают состояния mm и vv оптимизатора Adam для модели на 7 млрд параметров в FP32 (1 ГБ = 10910^9 байт)?
5 Зачем нужен разогрев learning rate?
6 Косинусное расписание: пик 10−310^{-3}, минимум 10−410^{-4}. Чему равен learning rate ровно на середине спада?
  • Momentum накапливает скорость: разгоняет движение вдоль оврага и гасит зигзаги поперёк.
  • RMSProp делит шаг каждого параметра на корень из EMA квадратов его градиентов — у каждого веса свой шаг.
  • Adam = momentum + RMSProp + поправка смещения. Его шаг порядка η\eta в каждой координате и не зависит от масштаба loss. Цена — два дополнительных числа на параметр.
  • AdamW вычитает weight decay отдельно от адаптивного шага; для LLM типично β2=0.95\beta_2 = 0.95, λ=0.1\lambda = 0.1.
  • Learning rate разогревают в начале и гасят в конце: косинус, ступени или WSD, как у DeepSeek и MiniCPM.

Дальше: ступень 2 — что значит «модель учится»: задача, данные, функция потерь и первые модели, которые вы обучите градиентным спуском.