Стохастический спуск, мини-батчи и локальные минимумы
На больших данных считать градиент по всей выборке дорого — спасают стохастический и мини-батч спуск, а их шум ещё и помогает не застрять.
Стохастический градиентный спуск (SGD) обновляет параметры по одному случайному объекту за раз, а мини-батч — по небольшой случайной порции, вместо всей выборки сразу.
Проблема полного (batch) спуска
В уроках выше мы на каждом шаге считали градиент по всем данным. Это называется полный (batch) градиентный спуск. Он точен, но если объектов миллионы, один шаг требует пройти весь датасет — обучение ползёт. Идея SGD: зачем смотреть на все данные, чтобы понять направление вниз? Достаточно оценки градиента по части данных — она шумная, но в среднем верная, зато шагов в секунду в сотни раз больше.
SGD: один объект за шаг
Стохастический спуск берёт случайный объект, считает градиент только по нему и сразу делает шаг. Каждый отдельный шаг «дёрганый», но направление в среднем правильное, и за то же время мы делаем гораздо больше обновлений. Реализуем SGD для той же линейной регрессии.
import random
random.seed(0)
xs = [1, 2, 3, 4, 5]
ys = [3.1, 4.9, 7.2, 8.8, 11.1] # около y = 2x + 1
w, b = 0.0, 0.0
lr = 0.01
for epoch in range(300):
# перемешиваем и идём по одному объекту
order = list(range(len(xs)))
random.shuffle(order)
for i in order:
x, y = xs[i], ys[i]
err = w * x + b - y # ошибка на ОДНОМ объекте
w -= lr * 2 * err * x # градиент по одной точке
b -= lr * 2 * err
print("SGD нашёл: w =", round(w, 3), ", b =", round(b, 3))
Вывод:
SGD нашёл: w = 1.995 , b = 1.04
SGD пришёл примерно туда же (w ≈ 2, b ≈ 1), что и полный спуск, но обновлялся по одному объекту за раз — на огромных данных это решающее преимущество.
Мини-батч — золотая середина
На практике берут мини-батч: не один объект и не всю выборку, а порцию из 16–256 объектов. Это компромисс — меньше шума, чем у чистого SGD, но всё ещё быстрые шаги и эффективная работа на видеокартах. Почти всё современное глубокое обучение — это мини-батч спуск.
import random
random.seed(1)
xs = list(range(1, 21))
ys = [3 * x + 2 for x in xs] # точная зависимость y = 3x + 2
def batches(indices, size):
for start in range(0, len(indices), size):
yield indices[start:start + size]
w, b = 0.0, 0.0
lr = 0.001
n = len(xs)
for epoch in range(2000):
order = list(range(n))
random.shuffle(order)
for batch in batches(order, 4): # мини-батч из 4 объектов
dw = sum(2 * (w * xs[i] + b - ys[i]) * xs[i] for i in batch) / len(batch)
db = sum(2 * (w * xs[i] + b - ys[i]) for i in batch) / len(batch)
w -= lr * dw
b -= lr * db
print("Мини-батч спуск: w =", round(w, 3), ", b =", round(b, 3), " (цель 3 и 2)")
Вывод:
Мини-батч спуск: w = 3.001 , b = 1.983 (цель 3 и 2)
Шум помогает выбираться из ловушек
У стохастичности есть бонус. Полный спуск, скатившись в локальный минимум невыпуклой функции, застревает там навсегда: градиент равен нулю, шагать некуда. А шумные шаги SGD иногда «выталкивают» точку из мелкой ямки, давая шанс найти яму поглубже. Поэтому случайность — не только про скорость, но и про качество решения на сложных, невыпуклых ландшафтах нейросетей.
| Метод | Данных на шаг | Когда применять |
| Полный (batch) | вся выборка | маленькие данные, нужна точность |
| SGD | один объект | очень большие данные, онлайн-обучение |
| Мини-батч | 16–256 объектов | стандарт глубокого обучения |
Итог
- Полный спуск точен, но считает градиент по всем данным — медленно на больших выборках.
- SGD обновляет параметры по одному объекту: шумно, зато много шагов.
- Мини-батч (16–256 объектов) — компромисс и стандарт глубокого обучения.
- Шум стохастичности помогает выбираться из локальных минимумов невыпуклых функций.