Стохастический градиентный спуск

Стохастический спуск считает градиент по случайной горстке данных вместо всех — это делает обучение огромных моделей возможным.

SGD (стохастический градиентный спуск) — вариант спуска, оценивающий градиент по одному примеру или мини-батчу данных, а не по всему датасету.

Проблема полного градиента

В машинном обучении цель — средняя ошибка по всем данным: $f(\theta)=\frac1N\sum_{i=1}^N \ell_i(\theta)$. Честный (batch) градиент требует пройти все $N$ примеров на каждом шаге. При $N=10^7$ один шаг — это десять миллионов вычислений, а шагов нужны тысячи. Непозволительно дорого.

Идея SGD

А что если оценивать градиент по случайному подмножеству данных? Градиент по одному примеру $\nabla\ell_i(\theta)$ — это шумная, но несмещённая оценка полного: в среднем он указывает в ту же сторону. Делая много дешёвых шумных шагов вместо немногих дорогих точных, мы продвигаемся к минимуму куда быстрее по «настенным часам». Компромисс — мини-батч: градиент по $32$–$256$ примерам, баланс между шумом и стоимостью.

Шум как друг

Стохастический шум не только удешевляет, но и помогает: случайные толчки помогают выскакивать из неглубоких локальных минимумов и сёдел невыпуклого ландшафта нейросети. Поэтому SGD часто находит решения, обобщающиеся лучше, чем гладкий полный градиент. Минус — шум мешает точно осесть на дне, поэтому шаг $\alpha$ со временем уменьшают (расписание).

Реализация: линейная регрессия через SGD

Подгоним прямую $y=wx+b$ к данным, сгенерированным по $y=2x+1$, обновляя веса по одному примеру за раз (порядок перемешиваем каждую эпоху).

import random

random.seed(1)
data = [(x, 2 * x + 1) for x in range(10)]
w, b = 0.0, 0.0
alpha = 0.01
for epoch in range(1, 51):
    random.shuffle(data)
    for x, y in data:               # один пример за шаг = чистый SGD
        pred = w * x + b
        err = pred - y              # производная MSE по pred
        w -= alpha * err * x        # dL/dw = err * x
        b -= alpha * err            # dL/db = err
    if epoch in (1, 10, 25, 50):
        mse = sum((w * x + b - y) ** 2 for x, y in data) / len(data)
        print(f"эпоха {epoch:2d}: w={w:.4f} b={b:.4f} MSE={mse:.5f}")
print("Итог: w=%.4f b=%.4f (истина w=2 b=1)" % (w, b))

Вывод:

эпоха  1: w=2.1172 b=0.3153 MSE=0.13806
эпоха 10: w=2.0794 b=0.4710 MSE=0.08150
эпоха 25: w=2.0576 b=0.6599 MSE=0.03391
эпоха 50: w=2.0288 b=0.8363 MSE=0.00801
Итог: w=2.0288 b=0.8363 (истина w=2 b=1)

Веса уверенно идут к истинным $w=2,\ b=1$, MSE падает к нулю. Обратите внимание: наклон $w$ нашёлся почти сразу, а сдвиг $b$ тянется медленнее — это эффект разной чувствительности параметров, лечится нормировкой или Adam.

SGD против batch

СвойствоBatch GDSGD / мини-батч
Градиент поВсем $N$ примерам1 или $b$ примерам
Стоимость шага$O(N)$$O(b)$, дёшево
ТраекторияГладкаяШумная (зигзаг)
Локальные минимумыЗастревает легчеШум помогает сбежать

Как работает под капотом

SGD — фундамент всего глубокого обучения; Adam (из раздела про ускорения) — это SGD с адаптивным шагом и моментом. Теория сходимости SGD требует убывающего шага: классическое условие Роббинса–Монро $\sum_k\alpha_k=\infty,\ \sum_k\alpha_k^2<\infty$ (например $\alpha_k\sim1/k$) гарантирует сходимость к минимуму выпуклой функции несмотря на шум. На практике используют расписания (cosine decay, warmup) и батчи, подобранные под память GPU. Мини-батч ещё и эффективно параллелится на видеокартах — вторая причина его доминирования.

Частые ошибки

  • Постоянный большой шаг. Из-за шума SGD не осядет на дне — шаг нужно уменьшать со временем.
  • Не перемешивать данные. Без shuffle порядок примеров вносит систематическое смещение в траекторию.
  • Слишком большой батч ради «точности». Теряется полезный шум и преимущество по скорости; иногда страдает обобщение.

Итоги

  • SGD оценивает градиент по 1 примеру или мини-батчу — дёшево и масштабируемо.
  • Оценка несмещённая, но шумная; шум помогает выбираться из локальных минимумов.
  • Нужен убывающий шаг (Роббинс–Монро) и перемешивание; основа всего глубокого обучения.
Проверьте себя
1. По чему SGD вычисляет градиент?
AПо всем данным сразу
BПо одному примеру или мини-батчу (случайному подмножеству)
CПо гессиану
DПо прошлым шагам
2. Чем полезен шум в SGD?
AНичем, это вред
BПомогает выскакивать из неглубоких локальных минимумов и сёдел
CУскоряет каждый шаг точно
DГарантирует глобальный оптимум
3. Почему в SGD шаг $\alpha$ обычно уменьшают со временем?
AДля экономии памяти
BИз-за шума постоянный шаг не даёт точно осесть на дне
CЧтобы увеличить батч
DЭто не нужно