Если данных очень много
Пока в обучающей таблице всего восемь заказов, на каждом шаге можно спокойно пройти по всей базе, собрать градиент и только после этого изменить параметры.
Но представим, что заказов не восемь, а восемь миллионов. Тогда ради одного обновления весов пришлось бы обработать все восемь миллионов строк.
Оказывается, это необязательно. Чтобы сделать шаг градиентного спуска, можно оценить направление не по всей базе, а только по её части.
| метод |
сколько объектов используется для одного обновления |
| 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, меняют величину шага по дополнительным правилам. До них мы дойдём позже.
Сейчас важнее понимать основу: батч определяет, по каким объектам мы считаем очередной градиент, а оптимизатор определяет, как по этому градиенту изменить параметры.