Модуль: Градиентный спуск. Основы


Если данных очень много


Если данных очень много

Пока в обучающей таблице всего восемь заказов, на каждом шаге можно спокойно пройти по всей базе, собрать градиент и только после этого изменить параметры.

Но представим, что заказов не восемь, а восемь миллионов. Тогда ради одного обновления весов пришлось бы обработать все восемь миллионов строк.

Оказывается, это необязательно. Чтобы сделать шаг градиентного спуска, можно оценить направление не по всей базе, а только по её части.

метод сколько объектов используется для одного обновления
batch gradient descent вся обучающая выборка
stochastic gradient descent (SGD) один объект
mini-batch gradient descent небольшая группа объектов

Батч — это просто несколько строк обучающей таблицы, по которым мы собираемся сделать одно обновление параметров.

Никакого специального типа данных для батча не требуется. В обычной программе на Python это может быть самый обычный список строк.

Формула градиента не меняется. Меняется только то, по каким объектам мы её усредняем.

Раньше:

градиент =
сумма вкладов всех объектов /  число всех объектов


Теперь:

градиент батча =
сумма вкладов объектов батча /  число объектов в батче

Например, для нашей линейной модели вклад одного заказа в производную по весу остаётся тем же:

2 · промах · признак

Мы просто складываем такие вклады не по всей базе, а только по строкам текущего батча.

Что такое batch size

Batch size — число объектов, по которым считается одно обновление весов.

Вернёмся к нашей базе из восьми заказов и возьмём:

batch_size = 3

Пока даже не будем ничего перемешивать. Разобьём таблицу подряд:

заказы 1, 2, 3
    ↓
посчитали градиент
    ↓
обновили параметры

заказы 4, 5, 6
    ↓
посчитали новый градиент
    ↓
ещё раз обновили параметры

заказы 7, 8
    ↓
посчитали новый градиент
    ↓
ещё раз обновили параметры

Обратите внимание: последний батч оказался меньше трёх. Это нормально. В нём осталось два объекта, поэтому градиент этого батча усредняем именно по двум объектам.

объектов в базе batch size батчи за один проход обновлений весов
8 8 8 1
8 3 3 + 3 + 2 3
8 1 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 8

Что такое эпоха

Эпоха — один полный проход по всей обучающей выборке.

В нашем примере при batch_size = 3 одна эпоха содержит три обновления параметров:

батч 1 → обновление
батч 2 → обновление
батч 3 → обновление
-----------------------
все 8 заказов использованы
-----------------------
эпоха закончилась

Следующая эпоха начинается не с нулевых весов. Она продолжает обучение с тех параметров, которые получились в конце предыдущей эпохи.

Это важное различие:

  • веса модели задаются один раз перед обучением и сохраняются между батчами и эпохами;
  • накопители градиента обнуляются перед каждым новым батчем.

Как получить батчи обычным Python

Сначала посмотрим только на разбиение списка, без машинного обучения.

zakazy = [1, 2, 3, 4, 5, 6, 7, 8]
batch_size = 3

for start in range(0, len(zakazy), batch_size):
    batch = zakazy[start:start + batch_size]
    print(batch)

Получим:

[1, 2, 3]
[4, 5, 6]
[7, 8]

Переменная start принимает значения 0, 3 и 6, поэтому Python берёт срезы:

zakazy[0:3]
zakazy[3:6]
zakazy[6:9]

Последний срез спокойно возвращает только оставшиеся элементы. Поэтому отдельно обрабатывать неполный последний батч не требуется.

Где теперь обновляются параметры

Раньше мы проходили всю базу и только потом меняли веса:

вся база
   ↓
градиент
   ↓
одно обновление

Теперь правило такое:

батч
   ↓
градиент этого батча
   ↓
обновление параметров
   ↓
следующий батч

Но правило, которое мы уже изучили, остаётся в силе: пока считается один градиент, параметры менять нельзя.

Поэтому внутри батча сначала обрабатываются все его строки, накапливаются g1, g2, g3 и gb, и только после этого веса обновляются одновременно.

Почему делим на размер батча

Допустим, в батче три заказа. Тогда производная MSE по одному из весов имеет вид:

вклад заказа 1
+ вклад заказа 2
+ вклад заказа 3
------------------
        3

Если в последнем батче осталось два заказа, делим на 2:

вклад заказа 7 + вклад заказа 8
-------------------------------
               2

Поэтому в программе удобно писать:

m = len(batch)

и использовать именно m, а не размер всей базы и не заранее заданный batch_size.

Зачем перемешивать данные

Если на каждой эпохе всегда брать одни и те же строки в одном порядке, состав батчей никогда не изменится. Поэтому перед новой эпохой обучающую таблицу обычно перемешивают.

Важно перемешивать целые строки, чтобы признаки заказа не оторвались от правильного ответа.

В обычном Python для этого достаточно стандартного модуля random:

import random

random.shuffle(train)

После перемешивания в батчи попадут другие сочетания заказов, а за одну эпоху по-прежнему будут использованы все строки.

При этом градиент одного батча обычно отличается от градиента всей базы. Поэтому траектория обучения становится менее гладкой: разные группы данных немного по-разному подсказывают направление.

Это не ошибка. Mini-batch градиент является приближением полного градиента. Следующий батч немного уточнит направление, затем следующий — ещё раз.

Три цикла

В программе с mini-batch появляются три уровня работы:

for epoch ...                 # повторяем эпохи

    for каждый batch ...      # выбираем очередную группу

        for объект in batch   # считаем градиент этой группы

После внутреннего цикла обновляем параметры. После среднего цикла закончилась эпоха.

Схема целиком:

начальные параметры
        ↓
--------------------------------
эпоха 1
        ↓
перемешали train
        ↓
батч 1 → градиент → обновление
батч 2 → градиент → обновление
батч 3 → градиент → обновление
--------------------------------
        ↓
эпоха 2
        ↓
снова перемешали train
        ↓
батч 1 → градиент → обновление
...

Batch, SGD и mini-batch — один и тот же код

Теперь видно, что это не три совершенно разных алгоритма. Отличается в основном размер группы.

batch_size = len(train)  → batch gradient descent

batch_size = 1           → stochastic gradient descent

1 < batch_size < len(train)
                         → mini-batch gradient descent

Само правило шага остаётся прежним:

w = w − lr · градиент

А что изменится в задаче классификации

Пример ниже оставляет знакомую линейную модель и MSE, чтобы отдельно разобраться с организацией батчей.

В практической задаче модель будет предсказывать не время доставки, а вероятность опоздания. Структура обучения останется той же:

эпоха
  ↓
батч
  ↓
прогнозы
  ↓
градиент батча
  ↓
обновление параметров

Изменится только вклад одного объекта в градиент.

Для логистической регрессии:

z = w₁x₁ + w₂x₂ + ... + b
p = sigmoid(z)

При использовании sigmoid вместе с binary cross-entropy сигнал одного объекта равен:

p − y

Поэтому вклад объекта в производную веса:

(p − y) · признак

а для свободного члена:

p − y

То есть внутри батча вместо:

2 · промах · признак

будет:

(p − y) · признак

А всё остальное — батчи, эпохи, усреднение и обновление параметров — остаётся тем же.

Про оптимизатор

Пока правило обновления параметров мы пишем сами:

w = w − lr · градиент

В библиотеках эту часть обычно поручают оптимизатору. Оптимизатор получает уже вычисленные градиенты и решает, как по ним изменить параметры.

Простейший вариант — SGD: по смыслу это то же обновление, которое мы сейчас пишем вручную. Более сложные оптимизаторы, например Adam, меняют величину шага по дополнительным правилам. До них мы дойдём позже.

Сейчас важнее понимать основу: батч определяет, по каким объектам мы считаем очередной градиент, а оптимизатор определяет, как по этому градиенту изменить параметры.