Машинное обучение

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

От склонов Коши к стохастическому обучению

В 1847 году французский математик Огюстен-Луи Коши описал метод наискорейшего спуска: чтобы найти минимум функции, нужно раз за разом делать шаг в направлении, противоположном градиенту. Он применял его для решения систем уравнений в астрономии - за век с лишним до появления компьютеров. Вторая часть истории случилась в 1951 году, когда Герберт Роббинс и Саттон Монро опубликовали работу о стохастической аппроксимации. Они показали, что к минимуму можно сходиться, используя зашумлённые выборочные оценки градиента вместо точного значения. Именно эта идея лежит в основе стохастического градиентного спуска - метода, которым обучают почти каждую современную нейросеть.

Когда ChatGPT генерирует текст, а Midjourney рисует картины, внутри работают нейросети с миллиардами параметров. Но как компьютер находит правильные значения этих миллиардов чисел? Не перебором - при 175 миллиардах параметров GPT-3 вариантов больше, чем атомов во Вселенной. Ответ: gradient descent - алгоритм, который маленькими шагами спускается к оптимальному решению, обрабатывая данные порциями. И от того, какого размера эти порции и насколько большие шаги, зависит, обучится ли модель за часы или не обучится никогда.

  • **GPT-4** обучался несколько месяцев на тысячах GPU - и всё обучение сводилось к миллиардам шагов mini-batch gradient descent с тщательно подобранным learning rate schedule
  • **Tesla Autopilot** постоянно дообучается на новых данных с камер миллионов автомобилей - SGD позволяет обновлять модель инкрементально, не переобучая с нуля каждый раз
  • **Выбор learning rate** может стоить компании миллионы долларов: слишком большой - обучение расходится и недели GPU-времени потрачены впустую, слишком маленький - модель не успевает сойтись за отведённый бюджет

Предварительные знания

  • Linear Regression

Batch Gradient Descent

В уроке про линейную регрессию мы узнали, что модель ищет параметры (веса), минимизирующие функцию потерь. Но *как именно* искать минимум? Представьте: вы стоите на горном склоне в густом тумане. Вершину не видно, но вы чувствуете наклон под ногами. Стратегия простая - шагайте в сторону наибольшего спуска. Это и есть **gradient descent** (градиентный спуск) - основной алгоритм оптимизации в машинном обучении.

**Градиент** - это вектор частных производных функции потерь по каждому параметру. Он показывает направление наибыстрейшего *роста* функции. Чтобы двигаться к минимуму, мы идём *против* градиента. Формула обновления весов на каждом шаге: **w = w - lr * gradient(J)**, где lr (learning rate) - размер шага, а J - функция потерь.

**Batch Gradient Descent** - это классический вариант: на каждом шаге градиент вычисляется по **всему** датасету. Слово "batch" здесь означает "вся пачка данных целиком". Алгоритм суммирует ошибки на всех примерах, усредняет их, вычисляет направление и делает один шаг. Потом повторяет. Для convex (выпуклых) функций потерь - например, MSE в линейной регрессии - batch GD **гарантированно** сходится к глобальному минимуму.

**Главная проблема batch GD - скорость.** Если в датасете 10 миллионов примеров, каждый шаг требует обработки всех 10 миллионов. Градиент получается точный и стабильный, но один шаг может занимать минуты. А шагов нужны тысячи. Для больших данных (ImageNet - 14 млн изображений, GPT - сотни миллиардов токенов) чистый batch GD неприменим.

**Итерация vs эпоха:** - **Итерация** - один шаг обновления весов - **Эпоха** - один полный проход по всему датасету В batch GD одна итерация = одна эпоха (потому что каждый шаг использует все данные). В stochastic и mini-batch GD одна эпоха содержит много итераций - это ключевое отличие, которое мы разберём дальше.

Почему batch gradient descent плохо подходит для датасета из 50 миллионов примеров?

Stochastic Gradient Descent (SGD)

Что если вместо обработки всех 50 миллионов примеров на каждом шаге мы будем обновлять веса после **каждого отдельного примера**? Это и есть **Stochastic Gradient Descent (SGD)** - вместо точного градиента по всему датасету мы используем приблизительный градиент по одному случайному примеру. Слово "stochastic" означает "случайный" - отсюда и название.

Представьте разницу так: batch GD - это опрос всех жителей города перед каждым решением. SGD - это опрос одного случайного прохожего. Ответ одного человека неточен (шумный), но вы получаете его мгновенно. За время одного точного опроса (batch) вы уже успеете опросить тысячи прохожих (SGD) и сделать тысячи шагов. В итоге SGD часто приходит к хорошему решению **быстрее**, несмотря на шум.

**Почему перемешивание данных критически важно?** Если данные упорядочены (например, сначала все положительные примеры, потом отрицательные), SGD будет сначала "учить" одну часть, потом резко переключится на другую. Модель будет метаться, а не сходиться. Перемешивание (shuffle) перед каждой эпохой гарантирует, что каждый шаг получает случайный, репрезентативный пример.

У шума SGD есть неожиданный **плюс**: он помогает выскочить из **локальных минимумов**. Batch GD аккуратно скатывается в ближайшую "яму" и застревает там. SGD из-за шума может перепрыгнуть через невысокие барьеры и найти лучший минимум. Для сложных нейросетей, у которых функция потерь имеет миллионы локальных минимумов, эта способность крайне ценна.

Какое преимущество даёт шумность SGD по сравнению с batch GD?

Mini-batch Gradient Descent

Batch GD: точный, но медленный. SGD: быстрый, но шумный. Есть ли золотая середина? **Mini-batch Gradient Descent** - вместо всего датасета или одного примера берём **небольшую группу** (mini-batch) из 32, 64, 128 или 256 примеров. Это **стандартный метод** обучения в PyTorch, TensorFlow и всех современных фреймворках.

Почему именно степени двойки (32, 64, 128, 256)? Это связано с архитектурой **GPU**. Видеокарты обрабатывают данные параллельно блоками, и их память организована по степеням двойки. Batch size = 64 использует аппаратные ресурсы GPU значительно эффективнее, чем, скажем, batch size = 60. Типичный выбор: **32** для ограниченной GPU-памяти, **256** если памяти достаточно.

**Эпоха в mini-batch GD:** Одна эпоха = один полный проход по датасету. Если в датасете 10 000 примеров и batch_size = 128, то в одной эпохе будет ceil(10000 / 128) = 79 итераций (mini-batches). За 100 эпох модель сделает 7 900 обновлений весов. Для сравнения batch GD за 100 эпох сделает ровно 100 обновлений - при том, что объём обработанных данных одинаков.

**Batch size влияет на обобщение!** Исследования показывают, что меньшие batch sizes (32-128) часто приводят к лучшей обобщающей способности модели, чем большие (1024+). Маленькие батчи создают больше шума в градиенте, что действует как неявная регуляризация - помогает модели не переобучиться. Поэтому увеличение batch size ради скорости не всегда хорошая идея.

Датасет содержит 64 000 примеров, batch_size = 256. Сколько обновлений весов произойдёт за одну эпоху?

Learning Rate: ключевой гиперпараметр

Формула **w = w - lr * gradient** содержит одно число, от которого зависит всё: **learning rate (lr)** - скорость обучения. Это, пожалуй, **самый важный гиперпараметр** в машинном обучении. Слишком большой lr - модель прыгает через минимум и расходится. Слишком маленький - модель едва ползёт и застревает. Именно неправильный lr чаще всего виноват, когда "модель не учится".

На практике learning rate не остаётся постоянным на протяжении обучения. Используют **learning rate schedules** - стратегии изменения lr по мере обучения. Логика простая: в начале делаем большие шаги, чтобы быстро приблизиться к минимуму, потом уменьшаем шаг для точной "настройки".

**Warmup: разогрев перед обучением** Современные модели (BERT, GPT, ResNet) часто используют warmup: первые несколько эпох learning rate линейно *растёт* от нуля до целевого значения, а потом начинает снижаться. Зачем? В начале обучения веса случайные, градиенты огромные и нестабильные. Большой lr на этом этапе "взорвёт" обучение. Warmup даёт модели время стабилизироваться. Типичная схема: 5-10% от общего числа шагов на warmup, затем cosine decay.

**Momentum** - ещё один приём, радикально ускоряющий сходимость. Идея: представьте мяч, катящийся с горы. Он набирает инерцию: если несколько шагов подряд градиент указывает в одну сторону, мяч разгоняется. Если направление меняется - инерция сглаживает колебания. Математически: **v = beta * v + gradient; w = w - lr * v**, где beta (обычно 0.9) - коэффициент инерции, а v - "скорость".

**Седловые точки - реальная проблема в нейросетях.** В функции с тысячами параметров локальные минимумы - не главная угроза. Гораздо чаще встречаются **седловые точки** (saddle points): градиент равен нулю, но это не минимум - в одних направлениях функция идёт вниз, в других вверх (как седло лошади). SGD с momentum и Adam справляются с ними лучше, чем чистый gradient descent, благодаря накопленной инерции.

Нужно найти один идеальный learning rate и использовать его всё обучение

В современном ML learning rate почти всегда меняется по schedule: warmup, decay, cosine annealing. Один фиксированный lr - редкость

В начале обучения нужны большие шаги для быстрого продвижения, а ближе к минимуму - маленькие для точной настройки. Ландшафт функции потерь тоже неоднороден: в разных областях оптимальный размер шага различается. Adam решает это автоматически, подбирая lr для каждого параметра отдельно.

Learning rate = 0.5, модель не сходится: loss скачет вверх-вниз, не уменьшаясь. Какое действие скорее всего решит проблему?

Ключевые идеи

  • **Gradient descent** - итеративный алгоритм оптимизации: вычисляем градиент функции потерь и делаем шаг *против* него (w = w - lr * gradient)
  • **Три варианта:** batch GD (весь датасет, точный, медленный), SGD (1 пример, быстрый, шумный), mini-batch (32-256 примеров, оптимальный компромисс)
  • **Mini-batch GD** - стандарт индустрии: использует GPU-параллелизм, достаточно стабилен и быстр, именно его реализуют PyTorch и TensorFlow
  • **Learning rate** - важнейший гиперпараметр: слишком большой = расходимость, слишком маленький = застревание. Решения: lr schedule (warmup + decay), momentum и Adam
  • Вернёмся к вопросу из начала: миллиарды параметров GPT находятся именно так - миллиардами маленьких шагов mini-batch gradient descent, каждый из которых чуть-чуть уменьшает ошибку

Связанные темы

Gradient descent - фундамент оптимизации в ML, который пронизывает все последующие темы. Вот как он связан с пройденным ранее материалом и с темами, которые идут дальше:

  • Линейная регрессия — В линейной регрессии gradient descent минимизирует MSE - простейший пример применения, который мы использовали для демонстрации всех трёх вариантов
  • Регуляризация — L1/L2 регуляризация добавляет штрафные члены к функции потерь, меняя ландшафт, по которому спускается gradient descent
  • Оптимизаторы: Adam, RMSProp, AdaGrad — Продвинутые оптимизаторы - эволюция SGD с momentum: адаптивные learning rates, gradient clipping и другие улучшения
  • Backpropagation — Backpropagation вычисляет градиенты в нейросетях - без него gradient descent не знал бы, куда шагать в многослойных сетях

Вопросы для размышления

  • Если SGD с его шумом помогает выбраться из локальных минимумов, почему бы не добавить ещё больше шума намеренно? Есть ли предел полезности шума в оптимизации?
  • Почему для computer vision (ResNet) обычно используют SGD + momentum, а для NLP (Transformers) - Adam? Что в структуре этих задач требует разных подходов к оптимизации?
  • Если learning rate schedule заранее определяет, как lr будет меняться, не теряем ли мы адаптивность? Что если оптимальная точка смены lr зависит от конкретного датасета?

Связанные уроки

  • ml-06-linear-regression — SGD - альтернатива нормальному уравнению для больших матриц
  • ml-28-optimizers — Adam, Adagrad, RMSProp - варианты GD с adaptive learning rate
  • ml-08-regularization — Градиент регуляризированной функции потерь = gradient + penalty term
  • calc-19-gradient
  • la-06-gauss
Градиентный спуск

0

1

Войти