Stochastic Gradient Descent (SGD)
SGD обучает модель с помощью множества недорогих обновлений весов, вычисляемых на небольших случайных пакетах данных. Рассматриваются правило обновления, моментум, размер пакета и код.
Стохастический градиентный спуск (SGD) — это алгоритм оптимизации, который обучает модель, многократно обновляя её веса по градиенту функции потерь, вычисленному на небольшой случайной выборке из обучающих данных, а не на всём наборе данных. Градиент для каждой выборки — это зашумлённая, но несмещённая оценка градиента по всему набору данных, поэтому за время, за которое полно-пакетный градиентный спуск делает один затратный шаг, SGD выполняет множество дешёвых обновлений. Метод берёт начало от метода стохастической аппроксимации, предложенного Роббинсом и Монро в 1951 году; с импульсом он остаётся стандартным оптимизатором для свёрточных нейронных сетей (CNN).
Как работает SGD#
На каждом шаге SGD берёт мини-пакет B, вычисляет градиент функции потерь на нём с помощью обратного распространения ошибки и немного сдвигает каждый вес в направлении, противоположном градиенту: w ← w − η·∇L_B(w), где η — скорость обучения. Строго говоря, SGD использует по одному примеру на шаг, но фреймворки глубокого обучения применяют его к мини-пакетам от нескольких десятков до нескольких тысяч примеров, поэтому на практике термины «SGD» и «градиентный спуск по мини-пакетам» являются синонимами. Больший размер пакета даёт менее зашумлённый градиент и более эффективно использует оборудование, но сокращает число обновлений на эпоху.
В начале каждой эпохи данные перемешиваются, затем модель проходит по ним мини-пакетами и делает по одному шагу на пакет. Весь процесс показывает подгонка прямой к 1 000 зашумлённых точек:
import torch
torch.manual_seed(0)
# 1,000 noisy samples of y = 3x + 2
x = torch.randn(1000, 1)
y = 3 * x + 2 + 0.1 * torch.randn(1000, 1)
model = torch.nn.Linear(1, 1)
optimizer = torch.optim.SGD(model.parameters(), lr=0.05, momentum=0.9)
for epoch in range(5):
# Reshuffle every epoch, then update the weights once per 32-sample mini-batch
for idx in torch.randperm(1000).split(32):
loss = torch.nn.functional.mse_loss(model(x[idx]), y[idx])
optimizer.zero_grad()
loss.backward()
optimizer.step()
print(f"epoch {epoch}: w = {model.weight.item():.3f}, b = {model.bias.item():.3f}, loss = {loss.item():.4f}")После двух эпох, то есть 64 обновлений в общей сложности, вес достигает 3.081, а смещение — 2.012. В течение следующих трёх эпох вес продолжает колебаться между 2.967 и 3.003 вместо того, чтобы стабилизироваться, поскольку градиент каждого мини-пакета содержит шум выборки; значение функции потерь для последнего пакета остаётся около 0.01 — дисперсии шума, добавленного к y.
Скорость обучения, шум и размер батча#
При постоянной скорости обучения SGD колеблется в области вокруг минимума, размер которой растёт вместе со скоростью обучения и градиентным шумом. Теория стохастических приближений гарантирует сходимость только при снижении скорости обучения до нуля, поэтому на практике во время обучения её уменьшают по ступенчатому или косинусному расписанию, обычно после короткого разогрева. Градиентный шум небольших батчей также направляет SGD к плоским минимумам, которые обеспечивают лучшую обобщающую способность, чем острые минимумы, к которым обычно приводит обучение на больших батчах, — это неявная форма регуляризации.
Размер батча и скорость обучения взаимодействуют друг с другом. Если увеличить скорость обучения пропорционально размеру батча и использовать разогрев в течение пяти эпох, ResNet-50 удаётся обучить на ImageNet за один час с мини-батчами по 8 192 изображения без потери точности.
Импульс и импульс Нестерова#
В длинной узкой долине поверхности функции потерь градиент направлен преимущественно поперёк долины, поэтому обычный SGD зигзагами движется между её стенками, медленно продвигаясь вдоль дна. Импульс накапливает скорость по формуле v ← μ·v + g и делает шаг вдоль неё: w ← w − η·v. Поэтому согласованные направления градиента ускоряют движение, а колеблющиеся — взаимно компенсируются. Обычно по умолчанию используют коэффициент μ = 0,9. Импульс Нестерова вычисляет градиент после упреждающего шага вдоль скорости, что ещё сильнее снижает перелёт. SGD в PyTorch реализует оба варианта и применяет weight_decay, добавляя λ·w к градиенту; для SGD это эквивалентно L2-штрафу для весов.
SGD и другие оптимизаторы#
- Пакетный градиентный спуск: вычисляет точный градиент по всему набору данных при каждом обновлении, поэтому за эпоху выполняется всего один шаг, а потребность в памяти растёт вместе с размером набора данных.
- Adam: задаёт каждому параметру собственный размер шага на основе скользящих средних градиентов и их квадратов, сохраняя по два значения состояния для каждого параметра — против одного у SGD с импульсом. Adam быстрее продвигается на начальных этапах и менее чувствителен к скорости обучения, тогда как хорошо настроенный SGD с импульсом часто не уступает ему или превосходит его по итоговой точности на CNN. AdamW, вариант Adam с разделённым затуханием весов, — стандартный оптимизатор для трансформеров.
- Muon: ортогонализует обновление импульса всей матрицы весов и был масштабирован для обучения больших языковых моделей.
При обучении Ultralytics YOLO создаёт SGD в PyTorch с импульсом Нестерова, если задан параметр optimizer="SGD"; по умолчанию используются lr0=0.01 и momentum=0.937. Настройка optimizer="auto" по умолчанию игнорирует lr0 и выбирает MuSGD — гибрид, сочетающий обновления SGD и Muon, — для запусков продолжительностью более 10 000 итераций, а для более коротких запусков — AdamW.
Где используют SGD#
На SGD с импульсом обучили большинство знаковых CNN для ImageNet. В оригинальных моделях ResNet использовали импульс 0,9, затухание весов 0,0001 и мини-батчи по 256 изображений; скорость обучения начиналась с 0,1 и уменьшалась в 10 раз при каждом плато ошибки. За пределами глубокого обучения SGDClassifier и SGDRegressor из scikit-learn обучают линейные модели, например логистическую регрессию и линейные машины опорных векторов, с помощью SGD; их метод partial_fit обучается на слишком больших, чтобы поместиться в памяти, наборах данных, обрабатывая их по одному фрагменту за раз.










