Стохастический градиентный спуск
Стохастический спуск считает градиент по случайной горстке данных вместо всех — это делает обучение огромных моделей возможным.
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 GD | SGD / мини-батч |
| Градиент по | Всем $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 примеру или мини-батчу — дёшево и масштабируемо.
- Оценка несмещённая, но шумная; шум помогает выбираться из локальных минимумов.
- Нужен убывающий шаг (Роббинс–Монро) и перемешивание; основа всего глубокого обучения.