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

Градиентный спуск

Как найти минимум функции, шагая против градиента, — learning rate и его границы, «овраги», выпуклые и невыпуклые функции, стохастический и мини-батч спуск. Этим алгоритмом обучают каждую нейросеть.

УРОКstage-1/1-4-optimization/01-gradient-descent.mdx1 ч 40 минУрок 1 из 2
После урока вы сможете
  • Объяснить идею градиентного спуска и сделать несколько шагов вручную и в коде
  • Предсказать по кривизне функции, при каком learning rate спуск сходится, зигзагует или расходится (правило η<2/a\eta < 2/a)
  • Отличить выпуклые задачи от невыпуклых и объяснить, почему в нейросетях опаснее седловые точки и овраги, чем локальные минимумы
  • Реализовать мини-батч SGD и объяснить, как размер батча влияет на шум градиента и число шагов

Обучить модель — значит найти такие значения её параметров, при которых функция потерьфункция потерь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. В уроке три главных вопроса: какой длины делать шаг, как форма функции мешает спуску и что делать, когда данных так много, что даже один точный градиент посчитать невозможно.

Представьте, что вы стоите на склоне горы в густом тумане и хотите спуститься в долину. Видно только землю под ногами. Разумная стратегия — нащупать, куда склон идёт вниз круче всего, и сделать шаг туда. Потом повторить.

Градиент ∇f\nabla f показывает направление наискорейшего роста, поэтому шаг делаем в обратную сторону:

θt+1=θt−η ∇f(θt)\theta_{t+1} = \theta_t - \eta\, \nabla f(\theta_t)

Эту формулу вы читали в уроке 1.0.3: θ\theta — параметры, η\eta («эта») — скорость обученияскорость обученияEN learning rate, то есть длина шага в единицах градиента, tt — номер шага. В одномерном случае градиент — это просто производная: xt+1=xt−ηf′(xt)x_{t+1} = x_t - \eta f'(x_t).

Пример вручную. f(x)=(x−3)2f(x) = (x - 3)^2 — парабола с дном в x=3x = 3 (урок 1.0.1), производная f′(x)=2(x−3)f'(x) = 2(x - 3) (цепное правило, урок 1.2.2). Старт x0=0x_0 = 0, η=0.1\eta = 0.1. Первый шаг: f′(0)=2⋅(0−3)=−6f'(0) = 2 \cdot (0 - 3) = -6, производная отрицательна — правее функция ниже, значит, идём вправо: x1=0−0.1⋅(−6)=0.6x_1 = 0 - 0.1 \cdot (-6) = 0.6. Дальше так же:

Шагxxf′(x)f'(x) до шагаf(x)f(x) после шага
10−0.1⋅(−6)=0.60 - 0.1 \cdot (-6) = 0.6−6-65.76
20.6−0.1⋅(−4.8)=1.080.6 - 0.1 \cdot (-4.8) = 1.08−4.8-4.83.69
31.08−0.1⋅(−3.84)=1.4641.08 - 0.1 \cdot (-3.84) = 1.464−3.84-3.842.36
62.214—0.62

Заметьте: шаги становятся короче сами собой. Ближе ко дну склон положе, градиент меньше, и при том же η\eta шаг уменьшается. Поэтому градиентный спуск естественно «притормаживает» у минимума.

flowchart LR
A["Параметры θ"] --> B["Прямой проход:<br/>loss на батче"]
B --> C["Обратный проход:<br/>градиент ∇L"]
C --> D["Шаг: θ ← θ − η·∇L"]
D --> A
Цикл обучения любой нейросети — это градиентный спуск

Learning rate — главный гиперпараметргиперпараметрEN hyperparameterНастройка, которую не учат градиентом, а выбирают по валидации — степень полинома, λ, learning rate, число слоёв. обучения. Посмотрим, почему, на самой простой функции f(x)=x2f(x) = x^2 с производной 2x2x. Шаг спуска: xt+1=xt−η⋅2xt=(1−2η) xtx_{t+1} = x_t - \eta \cdot 2x_t = (1 - 2\eta)\, x_t. Каждый шаг просто умножает xx на число q=1−2ηq = 1 - 2\eta. Старт x0=1x_0 = 1:

η\etaq=1−2ηq = 1 - 2\etaПервые шагиЧто происходит
0.010.981, 0.98, 0.96, 0.94ползёт: через 100 шагов x≈0.13x \approx 0.13
0.10.81, 0.8, 0.64, 0.51уверенно сходится
0.501, 0, 0, 0попадает в минимум за один шаг
0.9−0.81, −0.8, 0.64, −0.51перепрыгивает минимум, но сходится зигзагом
1.0−11, −1, 1, −1вечные качели
1.1−1.21, −1.2, 1.44, −1.73расходится: через 100 шагов x≈8⋅107x \approx 8 \cdot 10^{7}

Обобщим на «чашу» f(x)=a2x2f(x) = \frac{a}{2} x^2, где aa — кривизна: насколько круто стенки загибаются вверх. Формально кривизна — это вторая производная, то есть производная от производной (урок 1.2.1). У нашей чаши первая производная f′(x)=axf'(x) = ax — наклон, а вторая f′′(x)=af''(x) = a — скорость, с которой меняется сам наклон. Например, у f(x)=x2f(x) = x^2 (это a=2a = 2) наклон 2x2x растёт на 2 с каждой единицей xx. Чем больше aa, тем уже и круче чаша. Шаг спуска x−η⋅axx - \eta \cdot ax умножает xx на 1−ηa1 - \eta a, и:

  • спуск сходится, если 0<η<2a0 < \eta < \dfrac{2}{a};
  • идеальный шаг η=1a\eta = \dfrac{1}{a} — минимум за один шаг;
  • при η>2a\eta > \dfrac{2}{a} каждый шаг перелетает дно всё дальше, и значения улетают в бесконечность.

Вывод: чем круче функция, тем меньше допустимый шаг. Настоящая функция потерь — не идеальная чаша, но рядом с минимумом она на неё похожа, поэтому правило работает и для нейросетей.

Попробуйте сами в песочнице: функция «Чаша» x2+y2x^2 + y^2, смотрите на красную траекторию SGD. Здесь кривизна a=2a = 2, поэтому порог 2/a=12/a = 1 приходится ровно на правый край ползунка. Сдвигайте ползунок скорости обучения вправо: до 0.5 путь идёт прямо к минимуму (при 0.5 — за один шаг), дальше начинает перелетать его зигзагом, а у правого края застревает в «вечных качелях» — как в таблице выше. Расходимость увидите на функции «Овраг» из следующего раздела: поднимите там learning rate выше 0.5. Синюю и жёлтую линии (Momentum и Adam) разберём в следующем уроке. Кликом по карте можно выбрать другую стартовую точку.

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

В нейросети параметров миллионы, и градиент — вектор из миллионов производных. Формула шага та же, она применяется к каждой координате. Новая проблема появляется уже в двух измерениях. Возьмём «овраг»:

f(x,y)=0.1 x2+2 y2,∇f=(0.2 x,  4 y)f(x, y) = 0.1\,x^2 + 2\,y^2, \qquad \nabla f = (0.2\,x,\; 4\,y)

По xx кривизна 0.2 (пологое дно), по yy — 4 (крутые стенки). Learning rate один на обе координаты, и правило устойчивости η<2/a\eta < 2/a должно выполняться для самой крутой из них: η<2/4=0.5\eta < 2/4 = 0.5. Но при таком шаге вдоль пологого дна xx умножается на 1−0.2η≥0.91 - 0.2\eta \ge 0.9 — движение очень медленное.

η\etaff через 10 шаговчерез 50через 100
0.15.411.070.142
0.41.530.00194.6⋅10−74.6 \cdot 10^{-7}
0.4911.90.410.0070
0.5154.61 24062 500

Старт (−9,3.5)(-9, 3.5), f=32.6f = 32.6. При η=0.4\eta = 0.4 координата yy на каждом шаге умножается на 1−1.6=−0.61 - 1.6 = -0.6 и скачет со стороны на сторону, но быстро гаснет. При η=0.49\eta = 0.49 множитель −0.96-0.96: yy почти не затухает, и спуск весь путь зигзагует между стенками. Достаточно чуть перейти порог (η=0.51\eta = 0.51) — и всё разлетается.

Отношение самой большой кривизны к самой маленькой называют число обусловленностичисло обусловленностиEN condition numberОтношение самой большой кривизны к самой маленькой. Большое число обусловленности — вытянутый «овраг», где градиентный спуск зигзагует.. Здесь оно равно 4/0.2=204 / 0.2 = 20. Число шагов, которое нужно спуску, растёт примерно пропорционально этому отношению: вдвое более вытянутый овраг — примерно вдвое больше шагов. У нейросетей это отношение огромно — отсюда медленное обучение «голым» градиентным спуском. Лечение — momentum и адаптивные шаги Adam (урок 1.4.2).

Выпуклая функциявыпуклая функцияEN convex function«Чаша» без ложных ям — любой локальный минимум глобальный. Отрезок между двумя точками графика лежит не ниже графика. — это «чаша» без ложных ям: отрезок между любыми двумя точками графика лежит не ниже графика. Главное свойство: у выпуклой функции любой локальный минимум — глобальный. Градиентный спуск с разумным η\eta найдёт его откуда угодно. Выпуклы, например, MSE линейной регрессии и loss логистической регрессии (урок 2.2.2) как функции параметров.

Функция потерь нейросети не выпукла. На ней бывают:

  • локальные минимумылокальный минимумEN local minimumТочка, рядом с которой нет точек ниже, — дно «ямы». Глобальный минимум (global minimum) — самая низкая точка вообще. — ямы, из которых градиентный спуск не выберется;
  • плато — почти горизонтальные области, где градиент ничтожен;
  • седловые точкиседловая точкаEN saddle pointГрадиент равен нулю, но по одним направлениям функция растёт, по другим — убывает. В многомерных нейросетях встречается гораздо чаще локальных минимумов. — градиент равен нулю, но это не минимум.

Пример с двумя ямами. f(x)=x4−3x2+xf(x) = x^4 - 3x^2 + x. Запустим спуск с η=0.01\eta = 0.01 на 200 шагов из разных точек:

Старт x0x_02.00.50.0−0.5−2.0
Финиш1.1311.131−1.301−1.301−1.301
ff в финише−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]
Глубокая яма около x = −1.30 (f ≈ −3.51), мелкая — около x = 1.13 (f ≈ −1.07), горб — около x = 0.17. Куда скатится спуск, решает старт

Из точек правее горба (он около x≈0.17x \approx 0.17) спуск скатывается в мелкую правую яму и остаётся там навсегда. Результат зависит от старта — поэтому так важны инициализация весов (урок 3.4.1) и сид (урок 0.3.3).

Седловые точки важнее локальных минимумов

Заголовок раздела «Седловые точки важнее локальных минимумов»

Седло — как у функции f(x,y)=x2−y2f(x, y) = x^2 - y^2 в точке (0,0)(0, 0): вдоль xx это минимум, вдоль yy — максимум, а градиент равен нулю. Чтобы точка с нулевым градиентом была настоящим минимумом, функция должна загибаться вверх по всем направлениям сразу. Когда направлений миллионы, это редкость: почти всегда найдётся хотя бы одно, по которому можно спуститься ниже. Поэтому в больших сетях обучение чаще «вязнет» у седловых точек и на плато, где градиент мал, а не попадает в плохие локальные минимумы. Эмпирически у больших сетей найденные минимумы дают близкий друг к другу loss.

Как выбираться из сёдел? Помогают шум стохастического спуска (следующий раздел) и инерция momentum (урок 1.4.2).

До сих пор мы считали градиент точно. Но loss модели — это среднее по всем NN обучающим примерам, то есть матожидание из урока 1.3.1:

L(θ)=1N∑i=1Nℓ(xi;θ),∇L=1N∑i=1N∇ℓ(xi;θ)\mathcal{L}(\theta) = \frac{1}{N} \sum_{i=1}^{N} \ell(x_i; \theta), \qquad \nabla \mathcal{L} = \frac{1}{N} \sum_{i=1}^{N} \nabla \ell(x_i; \theta)

Читаем: ℓ(xi;θ)\ell(x_i; \theta) (рукописная «эль») — loss на одном примере xix_i при параметрах θ\theta; точка с запятой отделяет данные от параметров. Общий loss — среднее по всем NN примерам, а его градиент — среднее градиентов отдельных примеров (производная суммы — сумма производных, урок 1.2.1).

Чтобы сделать один шаг полного спуска, надо пройти по всем данным. DeepSeek-V3 обучалась на 14.8 триллиона токенов: один точный градиент — это месяцы работы кластера. Так учить нельзя.

Выход — оценивать градиент по случайной части данных:

  • стохастический градиентный спускстохастический градиентный спуск (SGD)EN stochastic gradient descent (SGD)Градиент считают не по всем данным, а по случайному примеру или мини-батчу. В PyTorch torch.optim.SGD — это просто «шаг против градиента», какой бы батч вы ни подали. в исходном смысле — градиент по одному случайному примеру;
  • мини-батч SGD — средний градиент по мини-батчмини-батчEN mini-batchНебольшая случайная порция примеров, по которой считается одна оценка градиента и делается один шаг. из BB случайных примеров.

Такая оценка несмещённая: её матожидание равно точному градиенту. Но она шумит, и шум измеряется формулой из урока 1.3.1: стандартное отклонение падает как 1/B1 / \sqrt{B} (это та же стандартная ошибка среднего, что в уроке 1.3.4). Проверим на линейной регрессии y=2x+1+шумy = 2x + 1 + \text{шум}:

import numpy as np
rng = np.random.default_rng(0)
N = 10_000
x = 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]

В среднем оценки попадают в точный градиент при любом BB, а разброс уменьшается вдвое при каждом учетверении батча. При B=1B = 1 шум больше самого градиента — отдельный шаг может пойти даже не туда. Но в среднем шаги ведут вниз.

Словарь: шаг (step, iteration) — одно обновление параметров; размер батчаразмер батчаEN batch sizeЧисло примеров (у LLM — часто число токенов) на один шаг оптимизатора. Больше батч — меньше шум градиента, но дороже шаг. — сколько примеров идёт на шаг; эпохаэпохаEN epochОдин полный проход по обучающим данным. LLM на претрейне часто не делают и одной эпохи. — полный проход по данным, это ⌈N/B⌉\lceil N / B \rceil шагов (скобки ⌈  ⌉\lceil \; \rceil значат «округлить вверх»: при N=10N = 10 и B=3B = 3 выходит 4 шага, последний батч из одного примера). Сравним три варианта, каждый ровно за одну эпоху (одинаковый объём вычислений):

ВариантБатчη\etaШагов за эпохуwwbbloss
SGD10.0110 0002.0701.0100.2530
мини-батч320.13132.0441.0290.2503
полный спуск10 0000.110.3980.2033.4337

Минимально возможный loss здесь около 0.25 — это дисперсия шума в данных (0.520.5^2). Полный спуск за эпоху сделал один шаг и почти не сдвинулся. SGD сделал 10 000 шагов и дошёл до цели, но каждый шаг обрабатывает один пример — видеокарта простаивает. Learning rate у SGD пришлось взять в 10 раз меньше: с η=0.1\eta = 0.1 шаги по одному примеру слишком шумные, и loss после эпохи хуже — 0.31. Мини-батч — компромисс: шагов достаточно, шум умеренный, а 32 примера считаются параллельно почти за то же время, что один.

Полный спускSGD (B=1B = 1)Мини-батч
Цена шагавсе NN примеров1 примерBB примеров, параллельно на GPU
Шум градиентанеточень большой∝1/B\propto 1/\sqrt{B}
Шагов за эпоху1NNN/BN / B
Где используютмаленькие задачипочти нигдевсегда

У шума есть и плюс: он «встряхивает» параметры и помогает уходить с седловых точек и плато. В LLM батч меряют в токенах, и он огромен — миллионы токенов на шаг. У DeepSeek-V3 батч в начале обучения ещё и растёт (урок 1.4.2). Претрейн LLM часто не проходит даже одной эпохи: данных больше, чем успевают прочитать.

В PyTorch градиенты считает autograd (вы видели его в уроках 1.2.1–1.2.2, подробно — урок 3.3.1), а шаг делает оптимизатор torch.optim.SGD. Несмотря на название, он просто делает шаг θ←θ−η∇\theta \leftarrow \theta - \eta \nabla по тому градиенту, который вы посчитали. «Стохастическим» спуск делает случайный мини-батч:

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.162
100 0.316 2.017 0.99
200 0.324 2.057 0.988
300 0.268 2.001 1.013

Loss на батче шумит (0.316, 0.324, 0.268) — это нормально, каждый батч новый. Параметры при этом уверенно пришли к w≈2w \approx 2, b≈1b \approx 1. Четыре строки внутри цикла — loss, zero_grad, backward, step — вы будете писать в каждой программе обучения до конца курса.

Соберите градиентный спуск на чистом Python: шаг, траекторию, автоматический подбор learning rate с защитой от расходимости и мини-батч SGD. Тесты проверят числа из таблиц урока, «качели» при η=1\eta = 1, расходимость в овраге и воспроизводимость по сиду.

Градиентный спуск и подбор learning rate7 тестов
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
Проверь себя6 вопросов
1 Функция f(x)=(x−3)2f(x) = (x - 3)^2, старт x0=0x_0 = 0, η=0.25\eta = 0.25. Чему равен x1x_1 после одного шага?
2 Спуск по f(x)=5x2f(x) = 5x^2. При каком learning rate он расходится?
3 Какие утверждения верны?Можно выбрать несколько ответов
4 В датасете 1 000 000 примеров, размер батча 500. Сколько шагов оптимизатора в одной эпохе?
5 Вы увеличили батч в 4 раза. Что произошло со стандартным отклонением оценки градиента?
6 Через 50 шагов обучения loss стал nan. Что проверить в первую очередь?
  • Градиентный спуск: θ←θ−η∇L\theta \leftarrow \theta - \eta \nabla \mathcal{L}. Шаги сами укорачиваются у минимума.
  • Для чаши с кривизной aa спуск сходится при η<2/a\eta < 2/a. Шаг ограничен самым крутым направлением, поэтому в «оврагах» с большим числом обусловленности спуск зигзагует и медленно идёт вдоль дна.
  • Выпуклые функции просты: любой минимум глобальный. Loss нейросетей невыпуклый, и главная беда в нём — седла и плато.
  • Мини-батч SGD оценивает градиент по BB случайным примерам: оценка несмещённая, шум ∝1/B\propto 1/\sqrt{B}. Это компромисс между ценой шага и числом шагов.

Дальше: momentum, RMSProp, Adam и AdamW — как разогнать спуск в оврагах и подобрать шаг для каждого параметра отдельно.