Градиентный спуск
Как найти минимум функции, шагая против градиента, — learning rate и его границы, «овраги», выпуклые и невыпуклые функции, стохастический и мини-батч спуск. Этим алгоритмом обучают каждую нейросеть.
Зачем это нужно
Заголовок раздела «Зачем это нужно»Обучить модель — значит найти такие значения её параметров, при которых функция потерьфункция потерьEN loss function минимален. У DeepSeek-V3 671 миллиард параметров, у маленькой учебной модели MiniMind (её вы обучите сами на ступени 5) — десятки миллионов. Формулы, которая сразу выдаёт лучшие значения, нет (кроме простейших моделей вроде линейной регрессии, урок 2.2.1). Есть только местная информация: значение loss в текущей точке и направление, в котором он растёт быстрее всего, — градиентградиентEN gradientВектор всех частных производных ∇f. Указывает направление наискорейшего роста функции; его длина — крутизна склона. (урок 1.2.3).
градиентный спускградиентный спускEN gradient descent превращает эту местную информацию в алгоритм: шаг против градиента, пересчитать, снова шаг. Этим алгоритмом и его потомками (Adam, AdamW, Muon) обучены все нейросети, от распознавателя цифр до DeepSeek-R1. В уроке три главных вопроса: какой длины делать шаг, как форма функции мешает спуску и что делать, когда данных так много, что даже один точный градиент посчитать невозможно.
Идея: спуск в тумане
Заголовок раздела «Идея: спуск в тумане»Представьте, что вы стоите на склоне горы в густом тумане и хотите спуститься в долину. Видно только землю под ногами. Разумная стратегия — нащупать, куда склон идёт вниз круче всего, и сделать шаг туда. Потом повторить.
Градиент показывает направление наискорейшего роста, поэтому шаг делаем в обратную сторону:
Эту формулу вы читали в уроке 1.0.3: — параметры, («эта») — скорость обученияскорость обученияEN learning rate, то есть длина шага в единицах градиента, — номер шага. В одномерном случае градиент — это просто производная: .
Пример вручную. — парабола с дном в (урок 1.0.1), производная (цепное правило, урок 1.2.2). Старт , . Первый шаг: , производная отрицательна — правее функция ниже, значит, идём вправо: . Дальше так же:
| Шаг | до шага | после шага | |
|---|---|---|---|
| 1 | 5.76 | ||
| 2 | 3.69 | ||
| 3 | 2.36 | ||
| 6 | 2.214 | — | 0.62 |
Заметьте: шаги становятся короче сами собой. Ближе ко дну склон положе, градиент меньше, и при том же шаг уменьшается. Поэтому градиентный спуск естественно «притормаживает» у минимума.
flowchart LR A["Параметры θ"] --> B["Прямой проход:<br/>loss на батче"] B --> C["Обратный проход:<br/>градиент ∇L"] C --> D["Шаг: θ ← θ − η·∇L"] D --> A
Learning rate: слишком мало, слишком много
Заголовок раздела «Learning rate: слишком мало, слишком много»Learning rate — главный гиперпараметргиперпараметрEN hyperparameterНастройка, которую не учат градиентом, а выбирают по валидации — степень полинома, λ, learning rate, число слоёв. обучения. Посмотрим, почему, на самой простой функции с производной . Шаг спуска: . Каждый шаг просто умножает на число . Старт :
| Первые шаги | Что происходит | ||
|---|---|---|---|
| 0.01 | 0.98 | 1, 0.98, 0.96, 0.94 | ползёт: через 100 шагов |
| 0.1 | 0.8 | 1, 0.8, 0.64, 0.51 | уверенно сходится |
| 0.5 | 0 | 1, 0, 0, 0 | попадает в минимум за один шаг |
| 0.9 | −0.8 | 1, −0.8, 0.64, −0.51 | перепрыгивает минимум, но сходится зигзагом |
| 1.0 | −1 | 1, −1, 1, −1 | вечные качели |
| 1.1 | −1.2 | 1, −1.2, 1.44, −1.73 | расходится: через 100 шагов |
Обобщим на «чашу» , где — кривизна: насколько круто стенки загибаются вверх. Формально кривизна — это вторая производная, то есть производная от производной (урок 1.2.1). У нашей чаши первая производная — наклон, а вторая — скорость, с которой меняется сам наклон. Например, у (это ) наклон растёт на 2 с каждой единицей . Чем больше , тем уже и круче чаша. Шаг спуска умножает на , и:
- спуск сходится, если ;
- идеальный шаг — минимум за один шаг;
- при каждый шаг перелетает дно всё дальше, и значения улетают в бесконечность.
Вывод: чем круче функция, тем меньше допустимый шаг. Настоящая функция потерь — не идеальная чаша, но рядом с минимумом она на неё похожа, поэтому правило работает и для нейросетей.
Попробуйте сами в песочнице: функция «Чаша» , смотрите на красную траекторию SGD. Здесь кривизна , поэтому порог приходится ровно на правый край ползунка. Сдвигайте ползунок скорости обучения вправо: до 0.5 путь идёт прямо к минимуму (при 0.5 — за один шаг), дальше начинает перелетать его зигзагом, а у правого края застревает в «вечных качелях» — как в таблице выше. Расходимость увидите на функции «Овраг» из следующего раздела: поднимите там learning rate выше 0.5. Синюю и жёлтую линии (Momentum и Adam) разберём в следующем уроке. Кликом по карте можно выбрать другую стартовую точку.
Многомерный случай и «овраги»
Заголовок раздела «Многомерный случай и «овраги»»В нейросети параметров миллионы, и градиент — вектор из миллионов производных. Формула шага та же, она применяется к каждой координате. Новая проблема появляется уже в двух измерениях. Возьмём «овраг»:
По кривизна 0.2 (пологое дно), по — 4 (крутые стенки). Learning rate один на обе координаты, и правило устойчивости должно выполняться для самой крутой из них: . Но при таком шаге вдоль пологого дна умножается на — движение очень медленное.
| через 10 шагов | через 50 | через 100 | |
|---|---|---|---|
| 0.1 | 5.41 | 1.07 | 0.142 |
| 0.4 | 1.53 | 0.0019 | |
| 0.49 | 11.9 | 0.41 | 0.0070 |
| 0.51 | 54.6 | 1 240 | 62 500 |
Старт , . При координата на каждом шаге умножается на и скачет со стороны на сторону, но быстро гаснет. При множитель : почти не затухает, и спуск весь путь зигзагует между стенками. Достаточно чуть перейти порог () — и всё разлетается.
Отношение самой большой кривизны к самой маленькой называют число обусловленностичисло обусловленностиEN condition numberОтношение самой большой кривизны к самой маленькой. Большое число обусловленности — вытянутый «овраг», где градиентный спуск зигзагует.. Здесь оно равно . Число шагов, которое нужно спуску, растёт примерно пропорционально этому отношению: вдвое более вытянутый овраг — примерно вдвое больше шагов. У нейросетей это отношение огромно — отсюда медленное обучение «голым» градиентным спуском. Лечение — momentum и адаптивные шаги Adam (урок 1.4.2).
Выпуклые и невыпуклые функции
Заголовок раздела «Выпуклые и невыпуклые функции»Выпуклая функциявыпуклая функцияEN convex function«Чаша» без ложных ям — любой локальный минимум глобальный. Отрезок между двумя точками графика лежит не ниже графика. — это «чаша» без ложных ям: отрезок между любыми двумя точками графика лежит не ниже графика. Главное свойство: у выпуклой функции любой локальный минимум — глобальный. Градиентный спуск с разумным найдёт его откуда угодно. Выпуклы, например, MSE линейной регрессии и loss логистической регрессии (урок 2.2.2) как функции параметров.
Функция потерь нейросети не выпукла. На ней бывают:
- локальные минимумылокальный минимумEN local minimumТочка, рядом с которой нет точек ниже, — дно «ямы». Глобальный минимум (global minimum) — самая низкая точка вообще. — ямы, из которых градиентный спуск не выберется;
- плато — почти горизонтальные области, где градиент ничтожен;
- седловые точкиседловая точкаEN saddle pointГрадиент равен нулю, но по одним направлениям функция растёт, по другим — убывает. В многомерных нейросетях встречается гораздо чаще локальных минимумов. — градиент равен нулю, но это не минимум.
Пример с двумя ямами. . Запустим спуск с на 200 шагов из разных точек:
| Старт | 2.0 | 0.5 | 0.0 | −0.5 | −2.0 |
|---|---|---|---|---|---|
| Финиш | 1.131 | 1.131 | −1.301 | −1.301 | −1.301 |
| в финише | −1.07 | −1.07 | −3.51 | −3.51 | −3.51 |
xychart-beta title "f(x) = x⁴ − 3x² + x: две ямы и горб между ними" x-axis "x" -2 --> 2 y-axis "f(x)" -4 --> 6 line [2, 0.3, -1.02, -2.02, -2.73, -3.19, -3.44, -3.51, -3.45, -3.27, -3, -2.67, -2.31, -1.93, -1.55, -1.19, -0.85, -0.56, -0.32, -0.13, 0, 0.07, 0.08, 0.04, -0.05, -0.19, -0.35, -0.53, -0.71, -0.87, -1, -1.07, -1.05, -0.91, -0.64, -0.19, 0.47, 1.38, 2.58, 4.1, 6]
Из точек правее горба (он около ) спуск скатывается в мелкую правую яму и остаётся там навсегда. Результат зависит от старта — поэтому так важны инициализация весов (урок 3.4.1) и сид (урок 0.3.3).
Седловые точки важнее локальных минимумов
Заголовок раздела «Седловые точки важнее локальных минимумов»Седло — как у функции в точке : вдоль это минимум, вдоль — максимум, а градиент равен нулю. Чтобы точка с нулевым градиентом была настоящим минимумом, функция должна загибаться вверх по всем направлениям сразу. Когда направлений миллионы, это редкость: почти всегда найдётся хотя бы одно, по которому можно спуститься ниже. Поэтому в больших сетях обучение чаще «вязнет» у седловых точек и на плато, где градиент мал, а не попадает в плохие локальные минимумы. Эмпирически у больших сетей найденные минимумы дают близкий друг к другу loss.
Как выбираться из сёдел? Помогают шум стохастического спуска (следующий раздел) и инерция momentum (урок 1.4.2).
Стохастический и мини-батч спуск
Заголовок раздела «Стохастический и мини-батч спуск»До сих пор мы считали градиент точно. Но loss модели — это среднее по всем обучающим примерам, то есть матожидание из урока 1.3.1:
Читаем: (рукописная «эль») — loss на одном примере при параметрах ; точка с запятой отделяет данные от параметров. Общий loss — среднее по всем примерам, а его градиент — среднее градиентов отдельных примеров (производная суммы — сумма производных, урок 1.2.1).
Чтобы сделать один шаг полного спуска, надо пройти по всем данным. DeepSeek-V3 обучалась на 14.8 триллиона токенов: один точный градиент — это месяцы работы кластера. Так учить нельзя.
Выход — оценивать градиент по случайной части данных:
- стохастический градиентный спускстохастический градиентный спуск (SGD)EN stochastic gradient descent (SGD)Градиент считают не по всем данным, а по случайному примеру или мини-батчу. В PyTorch torch.optim.SGD — это просто «шаг против градиента», какой бы батч вы ни подали. в исходном смысле — градиент по одному случайному примеру;
- мини-батч SGD — средний градиент по мини-батчмини-батчEN mini-batchНебольшая случайная порция примеров, по которой считается одна оценка градиента и делается один шаг. из случайных примеров.
Такая оценка несмещённая: её матожидание равно точному градиенту. Но она шумит, и шум измеряется формулой из урока 1.3.1: стандартное отклонение падает как (это та же стандартная ошибка среднего, что в уроке 1.3.4). Проверим на линейной регрессии :
import numpy as np
rng = np.random.default_rng(0)N = 10_000x = rng.normal(size=N)y = 2 * x + 1 + rng.normal(scale=0.5, size=N) # «истина»: w = 2, b = 1, плюс шум
def grad(w, b, idx): """Градиент MSE по (w, b) на примерах с номерами idx.""" # loss одного примера: (w·x + b − y)². По цепному правилу (урок 1.2.2): # производная по w равна 2·(w·x + b − y)·x, по b — 2·(w·x + b − y)·1. Усредняем по примерам. err = w * x[idx] + b - y[idx] # ошибки предсказаний на выбранных примерах return np.array([2 * np.mean(err * x[idx]), 2 * np.mean(err)])
full = grad(0.0, 0.0, np.arange(N)) # точный градиент по всем даннымprint("полный градиент:", full.round(3))for B in [1, 4, 16, 64, 256]: # 2000 раз берём B случайных номеров примеров и считаем по ним градиент est = np.array([grad(0.0, 0.0, rng.integers(0, N, size=B)) for _ in range(2000)]) print(f"B={B:3d}: среднее {est.mean(axis=0).round(2)}, std {est.std(axis=0).round(3)}")полный градиент: [-3.983 -2.028]B= 1: среднее [-3.78 -1.92], std [5.666 4.012]B= 4: среднее [-4.13 -2.07], std [3.069 2.064]B= 16: среднее [-4.03 -2.05], std [1.551 1.042]B= 64: среднее [-3.98 -2.03], std [0.741 0.521]B=256: среднее [-3.99 -2.03], std [0.371 0.265]В среднем оценки попадают в точный градиент при любом , а разброс уменьшается вдвое при каждом учетверении батча. При шум больше самого градиента — отдельный шаг может пойти даже не туда. Но в среднем шаги ведут вниз.
Что выгоднее при одинаковом бюджете
Заголовок раздела «Что выгоднее при одинаковом бюджете»Словарь: шаг (step, iteration) — одно обновление параметров; размер батчаразмер батчаEN batch sizeЧисло примеров (у LLM — часто число токенов) на один шаг оптимизатора. Больше батч — меньше шум градиента, но дороже шаг. — сколько примеров идёт на шаг; эпохаэпохаEN epochОдин полный проход по обучающим данным. LLM на претрейне часто не делают и одной эпохи. — полный проход по данным, это шагов (скобки значат «округлить вверх»: при и выходит 4 шага, последний батч из одного примера). Сравним три варианта, каждый ровно за одну эпоху (одинаковый объём вычислений):
| Вариант | Батч | Шагов за эпоху | loss | |||
|---|---|---|---|---|---|---|
| SGD | 1 | 0.01 | 10 000 | 2.070 | 1.010 | 0.2530 |
| мини-батч | 32 | 0.1 | 313 | 2.044 | 1.029 | 0.2503 |
| полный спуск | 10 000 | 0.1 | 1 | 0.398 | 0.203 | 3.4337 |
Минимально возможный loss здесь около 0.25 — это дисперсия шума в данных (). Полный спуск за эпоху сделал один шаг и почти не сдвинулся. SGD сделал 10 000 шагов и дошёл до цели, но каждый шаг обрабатывает один пример — видеокарта простаивает. Learning rate у SGD пришлось взять в 10 раз меньше: с шаги по одному примеру слишком шумные, и loss после эпохи хуже — 0.31. Мини-батч — компромисс: шагов достаточно, шум умеренный, а 32 примера считаются параллельно почти за то же время, что один.
| Полный спуск | SGD () | Мини-батч | |
|---|---|---|---|
| Цена шага | все примеров | 1 пример | примеров, параллельно на GPU |
| Шум градиента | нет | очень большой | |
| Шагов за эпоху | 1 | ||
| Где используют | маленькие задачи | почти нигде | всегда |
У шума есть и плюс: он «встряхивает» параметры и помогает уходить с седловых точек и плато. В LLM батч меряют в токенах, и он огромен — миллионы токенов на шаг. У DeepSeek-V3 батч в начале обучения ещё и растёт (урок 1.4.2). Претрейн LLM часто не проходит даже одной эпохи: данных больше, чем успевают прочитать.
То же самое в PyTorch
Заголовок раздела «То же самое в PyTorch»В PyTorch градиенты считает autograd (вы видели его в уроках 1.2.1–1.2.2, подробно — урок 3.3.1), а шаг делает
оптимизатор torch.optim.SGD.
Несмотря на название, он просто делает шаг по тому градиенту, который вы
посчитали. «Стохастическим» спуск делает случайный мини-батч:
import torch
torch.manual_seed(0)x = torch.randn(10_000)y = 2 * x + 1 + 0.5 * torch.randn(10_000) # те же данные: w = 2, b = 1 плюс шум
w = torch.zeros(1, requires_grad=True) # параметры, по которым нужен градиентb = torch.zeros(1, requires_grad=True)opt = torch.optim.SGD([w, b], lr=0.1) # «голый» градиентный спуск
for step in range(301): idx = torch.randint(0, 10_000, (32,)) # случайный мини-батч из 32 примеров loss = ((w * x[idx] + b - y[idx]) ** 2).mean() # MSE на батче opt.zero_grad() # обнулить старые градиенты loss.backward() # посчитать ∂loss/∂w и ∂loss/∂b opt.step() # w ← w − lr·∂loss/∂w (и для b) if step % 100 == 0: print(step, round(loss.item(), 3), round(w.item(), 3), round(b.item(), 3))0 5.242 0.437 0.162100 0.316 2.017 0.99200 0.324 2.057 0.988300 0.268 2.001 1.013Loss на батче шумит (0.316, 0.324, 0.268) — это нормально, каждый батч новый. Параметры при этом уверенно пришли
к , . Четыре строки внутри цикла — loss, zero_grad, backward, step — вы будете
писать в каждой программе обучения до конца курса.
Практика
Заголовок раздела «Практика»Соберите градиентный спуск на чистом Python: шаг, траекторию, автоматический подбор learning rate с защитой от расходимости и мини-батч SGD. Тесты проверят числа из таблиц урока, «качели» при , расходимость в овраге и воспроизводимость по сиду.
import math
import random
def gd_step(x, g, lr):
"""Один шаг градиентного спуска: новая точка x − lr·g (покоординатно).
x и g — списки чисел одинаковой длины. Верните НОВЫЙ список, входной не меняйте.
"""
# TODO: ваш код
...
def gradient_descent(grad, x0, lr, steps):
"""Траектория спуска: [x0, x1, ..., x_steps] — список из steps + 1 точек.
grad(x) возвращает градиент (список) в точке x.
"""
# TODO: ваш код
...
def best_lr(f, grad, x0, lrs, steps):
"""Из списка lrs выберите learning rate, после которого f(последняя точка) минимальна.
Расходящиеся прогоны (f = inf или nan) пропускайте — проверяйте math.isfinite.
"""
# TODO: ваш код
...
def minibatch_sgd_mean(data, lr, batch_size, epochs, seed=0):
"""Мини-батч SGD для L(w) = среднее (w − x)² по данным. Минимум L — среднее данных.
Старт w = 0. Каждую эпоху перемешайте номера примеров через random.Random(seed)
(один генератор на всё обучение) и идите по ним кусками по batch_size
(последний кусок может быть короче). На батче градиент равен 2·(w − среднее батча).
Верните историю: список значений w после КАЖДОГО шага.
"""
# TODO: ваш код
...
# Проверьте себя кнопкой «Запустить»:
print(gd_step([1.0, 2.0], [0.5, -1.0], 0.1)) # [0.95, 2.1]
path = gradient_descent(lambda x: [2 * (x[0] - 3)], [0.0], 0.1, 3)
print(path) # [[0.0], [0.6], [1.08], [1.464]]
ravine = lambda p: 0.1 * p[0] * p[0] + 2 * p[1] * p[1] # «овраг» из урока
ravine_grad = lambda p: [0.2 * p[0], 4 * p[1]]
print(best_lr(ravine, ravine_grad, [-9.0, 3.5], [0.01, 0.1, 0.4, 0.6], 100)) # 0.4
hist = minibatch_sgd_mean([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], lr=0.05, batch_size=2, epochs=20)
if hist: # покажется, когда функция заработает
print(len(hist), round(hist[-1], 2)) # 100 шагов, w ≈ 5.5
Проверь себя
Заголовок раздела «Проверь себя»- Градиентный спуск: . Шаги сами укорачиваются у минимума.
- Для чаши с кривизной спуск сходится при . Шаг ограничен самым крутым направлением, поэтому в «оврагах» с большим числом обусловленности спуск зигзагует и медленно идёт вдоль дна.
- Выпуклые функции просты: любой минимум глобальный. Loss нейросетей невыпуклый, и главная беда в нём — седла и плато.
- Мини-батч SGD оценивает градиент по случайным примерам: оценка несмещённая, шум . Это компромисс между ценой шага и числом шагов.
Дальше: momentum, RMSProp, Adam и AdamW — как разогнать спуск в оврагах и подобрать шаг для каждого параметра отдельно.