Перейти к содержимому
ПереводыАндрей Карпатый

Zero to Hero, лекция 5: становимся ниндзя обратного распространения (makemore, часть 4)

Подробный конспект лекции-упражнения Карпатого: обратный проход через MLP с BatchNorm пишется руками на уровне тензоров — от cross-entropy до таблицы эмбеддингов. Короткие формулы для cross-entropy и BatchNorm, правило «сумма ↔ растягивание» и обучение сети без loss.backward().

  • Карпатый берёт двухслойную MLP с BatchNorm из прошлой лекции и убирает строку loss.backward(). Весь обратный проход пишется руками — уже не для отдельных чисел, как в micrograd, а для тензоров: через cross-entropy, второй линейный слой, tanh, BatchNorm, первый слой и таблицу эмбеддингов.
  • Зачем: backpropобратное распространение ошибкиEN backpropagation — «дырявая абстракция». Насыщение, мёртвые нейроны, взрывающиеся градиенты и тонкие баги вроде обрезки loss вместо градиента не видны, пока не понимаешь, что происходит внутри. Лет десять назад обратный проход и так писали руками все.
  • Четыре упражнения. Первое — пройти назад по каждой элементарной операции и сверить с PyTorch. Второе и третье — вывести на бумаге короткие формулы для cross-entropy и BatchNorm целиком. Четвёртое — обучить сеть только на своих градиентах.
  • Два правила, на которых держится всё: где в прямом проходе было растягивание (broadcasting), в обратном — сумма по той же оси, и наоборот. А формулы для матричного умножения не нужно помнить — их однозначно задают формы тензоров.
  • Градиент cross-entropy по логитам — (softmax−onehot)/n(\text{softmax} - \text{onehot})/n. Его Карпатый объясняет как систему сил: неправильные ответы тянутся вниз, правильный вверх, и силы уравновешены.

Это видео не для пассивного просмотра. Карпатый прямо советует открыть ноутбук-упражнение (есть версия в Google Colab), пытаться вывести каждый градиент самому и включать видео, только когда застряли.

В курсе ту же работу, но без BatchNorm, делает раздел «Backprop руками» урока 4.1.4: cross-entropy, линейные слои, tanh и scatter-add для таблицы эмбеддингов. Его стоит пройти до лекции — тогда в видео новыми будут только BatchNorm и техника работы с broadcasting. Формулы градиента линейного слоя выведены в разделе «Градиент матричного умножения» урока 1.2.3, обратный проход «по кирпичикам» — в уроке 3.1.2, правило += — в уроке 3.2.2.

Новичку можно сначала пройти только упражнения 1 и 2, а вывод формулы BatchNorm (упражнение 3) оставить на второй заход. Ноутбук лекции — makemore_part4_backprop.ipynb (MIT); в репозитории он с решениями, в Colab — с пропусками. Китайская учебная версия — 005_makemore_BackpropNinja.

Таймкоды — из глав в описании видео.

ВремяГлава в оригиналеО чём
0:00intro: why you should care & fun historyзачем это нужно и как было раньше
7:26starter codeстартовый код, функция сверки cmp
13:03exercise 1: backproping the atomic compute graphупражнение 1: назад по каждой операции
1:05:20brief digression: bessel’s correction in batchnormотступление: nn или n−1n-1 в дисперсии
1:26:31exercise 2: cross entropy loss backward passупражнение 2: градиент cross-entropy одной формулой
1:36:42exercise 3: batch norm layer backward passупражнение 3: BatchNorm одной формулой
1:50:05exercise 4: putting it all togetherупражнение 4: обучение на своих градиентах
1:54:25outroитоги

У Карпатого есть пост 2016 года об этом — «Yes you should understand backprop». Главная мысль: backprop — дырявая абстракция. Нельзя сложить произвольные дифференцируемые «кубики», скрестить пальцы и ждать, что всё обучится. Примеры знакомы по прошлым лекциям: плоские хвосты нелинейностей, мёртвые нейроны, взрывающиеся и исчезающие градиенты в рекуррентных сетях (о них речь впереди).

Ещё один пример — фрагмент кода, найденный «в дикой природе». Автор хотел ограничить градиенты, а вместо этого обрезал loss сверху. У примеров-выбросов с обрезанным loss градиент становится нулём, и сеть их молча игнорирует. Такой баг не найти, если не понимать обратный проход.

Исторический экскурс. Сейчас обратный проход руками никто не пишет, кроме как в учебных целях. Но лет десять назад это было нормой. Карпатый показывает статью Хинтона и Салахутдинова 2006 года в Science (автокодировщики на ограниченных машинах Больцмана) и свой код 2010 года на Matlab. Тогда Python в глубоком обучении почти не использовали, а градиенты оценивали методом contrastive divergence и вписывали в код прямо в строке. Ещё пример — код его статьи 2014 года о «фрагментных эмбеддингах» (сопоставление объектов на картинках и кусочков фраз, что-то вроде CLIP). Там уже NumPy, но обратный проход написан руками и проверен численным сравнением градиентовпроверка градиентаEN gradient checkСравнение аналитического градиента с численным (центральные разности) по относительной ошибке. Главный способ найти баг в backward.. Его вывод: сейчас всё делает автоград, и кое-что мы потеряли.

Данные и сеть те же, что в прошлой лекции: окно из трёх символов, эмбеддинги длины 10, скрытый слой из 64 нейронов, BatchNorm, tanh, выходной слой — 4137 параметров. Три изменения:

  1. Функция сверки cmp(name, dt, t). Она печатает, совпал ли наш градиент dt с t.grad от PyTorch бит в бит (exact), с точностью до округлений (torch.allclose) и какова максимальная разница.
  2. Инициализация маленькими случайными числами вместо нулей, даже у смещений. Нули упрощают выражения и могут скрыть ошибку в формуле градиента. b1 оставлен перед BatchNorm, хотя он бесполезен, — «для веселья»: проверить, что его градиент тоже считается правильно.
  3. Прямой проход разбит на мелкие шаги. Вместо F.cross_entropy softmax и NLL выписаны явно, BatchNorm тоже разобран на части. У каждого промежуточного тензора будет свой градиент с приставкой d: для bnraw — dbnraw, для logprobs — dlogprobs.

Код из ноутбука лекции (MIT), комментарии наши:

emb = C[Xb] # (32, 3, 10): эмбеддинги символов окна
embcat = emb.view(emb.shape[0], -1) # (32, 30): склейка
hprebn = embcat @ W1 + b1 # (32, 64): первый линейный слой
# BatchNorm по шагам
bnmeani = 1/n * hprebn.sum(0, keepdim=True) # (1, 64): среднее по батчу
bndiff = hprebn - bnmeani # (32, 64): отклонения
bndiff2 = bndiff**2
bnvar = 1/(n-1) * bndiff2.sum(0, keepdim=True) # (1, 64): дисперсия с поправкой Бесселя
bnvar_inv = (bnvar + 1e-5)**-0.5 # 1/sqrt(дисперсия + eps)
bnraw = bndiff * bnvar_inv # нормированные значения
hpreact = bngain * bnraw + bnbias # масштаб и сдвиг
h = torch.tanh(hpreact) # (32, 64): нелинейность
logits = h @ W2 + b2 # (32, 27): второй линейный слой
# cross-entropy по шагам (то же, что F.cross_entropy)
logit_maxes = logits.max(1, keepdim=True).values
norm_logits = logits - logit_maxes # вычитаем максимум ради устойчивости
counts = norm_logits.exp()
counts_sum = counts.sum(1, keepdims=True)
counts_sum_inv = counts_sum**-1 # с 1.0/counts_sum не выходит совпадение бит в бит
probs = counts * counts_sum_inv
logprobs = probs.log()
loss = -logprobs[range(n), Yb].mean()

Перед loss.backward() для всех этих тензоров вызывается retain_grad(). Иначе PyTorch не сохранит градиенты промежуточных значений, и сверять будет не с чем.

Идём снизу вверх: от loss к C. Ниже — главные шаги и приёмы, которые Карпатый на них показывает. Общий рецепт он повторяет много раз. Сначала посмотреть на формы тензоров. Потом взять игрушечный пример 2×22 \times 2 или 3×33 \times 3 и выписать, какой элемент от какого зависит. Потом обобщить.

logprobs. Loss — минус среднее 32 выбранных элементов. Если L=−13(a+b+c)L = -\frac{1}{3}(a + b + c), то ∂L/∂a=−1/3\partial L / \partial a = -1/3. Значит, у выбранных элементов градиент −1/n-1/n, а у остальных 27·32 − 32 — ноль: они в loss не участвуют.

dlogprobs = torch.zeros_like(logprobs)
dlogprobs[range(n), Yb] = -1.0 / n

probs. Логарифм поэлементный, (log⁡x)′=1/x(\log x)' = 1/x: dprobs = (1.0 / probs) * dlogprobs. Интуиция Карпатого: если правильному символу дана маленькая вероятность, деление на неё усиливает градиент такого примера.

counts_sum_inv — первый broadcasting. probs = counts * counts_sum_inv, где формы (32,27)(32, 27) и (32,1)(32, 1). Здесь на самом деле две операции: сначала столбец копируется 27 раз по горизонтали, потом поэлементное умножение. Назад через умножение — «умножить на соседа», как в micrograd. Назад через копирование — сложить градиенты всех копий, потому что это одна переменная, использованная много раз:

dcounts_sum_inv = (counts * dprobs).sum(1, keepdim=True) # (32, 1): сумма по растянутой оси
dcounts = counts_sum_inv * dprobs # первая из двух веток для counts

Ветки и +=. counts участвует дважды: прямо в probs и через counts_sum. Поэтому dcounts сначала неполный, а вторая ветка потом прибавляется через +=. Пока ветка не дописана, cmp честно показывает несовпадение — заодно видно, что проверка не всегда отвечает «всё хорошо».

counts_sum — сумма. counts_sum = counts.sum(1): каждый элемент суммы зависит от своей строки с коэффициентом 1. Значит, градиент суммы копируется во все элементы строки: dcounts += torch.ones_like(counts) * dcounts_sum.

logit_maxes — градиент, который должен быть нулём. Вычитание максимума не меняет softmax, а значит, и loss. Поэтому градиент по logit_maxes должен быть нулевым. Так и выходит, с точностью до округлений: числа порядка 10−910^{-9}. Через сам max градиент попадает только в ту позицию, где был максимум. Её удобно отметить через one-hot по logits.max(1).indices.

Линейный слой: формулы по формам. Для logits = h @ W2 + b2 Карпатый выписывает на бумаге пример D=AB+cD = AB + c с матрицами 2×22 \times 2. Каждый элемент DD — сумма произведений, а это micrograd: сложения и умножения. Если собрать результаты в матрицы, получится dA=dD B⊤dA = dD\, B^\top, dB=A⊤dDdB = A^\top dD, dcdc — сумма dDdD по строкам. Дальше его признание: он никогда не помнит эти формулы. Это и не нужно. Форма dh должна совпадать с формой h, (32,64)(32, 64). Получить её из dlogits (32,27)(32, 27) и W2 (64,27)(64, 27) можно единственным способом: dlogits @ W2.T. Так же однозначно находятся dW2 = h.T @ dlogits и db2 = dlogits.sum(0).

tanh, gain, bias. dhpreact = (1.0 - h**2) * dh — производная tanh через его выход, как в micrograd. bngain и bnbias имеют форму (1,64)(1, 64) и растягиваются на 32 строки, значит, их градиенты — суммы по оси 0 с keepdim=True.

Внутри BatchNorm. Тот же набор приёмов: bndiff участвует в двух ветках (bnraw и через дисперсию), степень −0.5-0.5 даёт множитель −0.5 (σ2+ε)−1.5-0.5\,(\sigma^2 + \varepsilon)^{-1.5}. Отсюда Карпатый формулирует двойственность: сумма по оси в прямом проходе становится растягиванием по той же оси в обратном. А растягивание (повторное использование переменной) в прямом становится суммой в обратном.

Последние шаги. Первый линейный слой — те же три формулы. view назад — снова view в исходную форму (32,3,10)(32, 3, 10). Выборка C[Xb] назад: градиент каждой позиции окна нужно положить в «свою» строку таблицы, складывая, если символ встречался несколько раз. Векторного способа Карпатый в видео не нашёл и пишет цикл:

dC = torch.zeros_like(C)
for k in range(Xb.shape[0]): # по примерам батча
for j in range(Xb.shape[1]): # по позициям окна
ix = Xb[k, j] # какой символ здесь стоял
dC[ix] += demb[k, j] # += : одна строка C могла использоваться много раз

Сводка правил, которые понадобились:

Операция в прямом проходеЧто делать в обратном
поэлементная функция y=f(x)y = f(x)dx=f′(x)⋅dydx = f'(x) \cdot dy (для exp, log, tanh, степени)
поэлементное умножение на растянутый тензорумножить на соседа, затем сложить по растянутой оси с keepdim=True
сумма по осирастянуть градиент обратно: torch.ones_like(x) * dy
выбор элементов по индексамположить градиенты на их места, остальное — нули
max по строкеградиент только в позицию максимума
матричное умножение D=AB+cD = AB + cdA=dD B⊤dA = dD\,B^\top, dB=A⊤dDdB = A^\top dD, dcdc — сумма по строкам; проверять формами
viewview обратно в исходную форму
переменная используется в нескольких местах+= от каждой ветки
выборка строк таблицы C[X]scatter-add: сложить градиенты в строки таблицы

В прямом проходе дисперсия делится на n−1n - 1, а не на nn, хотя в статье о BatchNorm стоит nn. Есть две оценки дисперсии. Деление на nn даёт смещённую оценку: на маленьких выборках она почти всегда занижает настоящую дисперсию. Деление на n−1n - 1 даёт несмещённую. Мини-батч из 32 примеров — как раз маленькая выборка из всего обучающего набора.

Путаница в том, что статья при обучении использует смещённую оценку, а для скользящих средних, которые работают на инференсе, — несмещённую. Получается расхождение между обучением и применением (train/test mismatch). nn.BatchNorm1d в PyTorch повторяет статью. По словам Карпатого, документация здесь неточна, на это не раз указывали в обсуждениях на GitHub, а выбрать одну оценку для обоих режимов нельзя. Сам он считает расхождение багом. Поэтому он последовательно использует n−1n - 1 — при больших батчах разница становится малой.

Разбирать cross-entropy на восемь шагов было полезно для тренировки, но так не делают. Если продифференцировать формулу loss целиком, почти всё сокращается. Для одного примера с логитами ll и правильным классом yy:

L=−log⁡ely∑jelj=−ly+log⁡∑jelj,∂L∂li=eli∑jelj−[i=y]=pi−[i=y].L = -\log \frac{e^{l_y}}{\sum_j e^{l_j}} = -l_y + \log \sum_j e^{l_j}, \qquad \frac{\partial L}{\partial l_i} = \frac{e^{l_i}}{\sum_j e^{l_j}} - [i = y] = p_i - [i = y].

Читается так: градиент по каждому логиту — вероятность этого класса, а у правильного класса из неё ещё вычитается единица. Для батча loss усредняется, поэтому всё делится на nn. Вся цепочка из упражнения 1 заменяется тремя строками:

dlogits = F.softmax(logits, 1) # p
dlogits[range(n), Yb] -= 1 # p - 1 на месте правильного символа
dlogits /= n # loss — среднее по батчу

С PyTorch совпадает приблизительно: максимальная разница около 5⋅10−95 \cdot 10^{-9}, это округления float32.

Теперь интуиция, которую Карпатый считает очень красивой. Строка dlogits для одного примера — это вероятности, у которых в позиции правильного ответа вычли единицу. Сумма строки — ноль. Думайте о градиенте как о силе. Неправильные символы тянутся вниз, каждый пропорционально тому, насколько модель в него верила. Правильный тянется вверх ровно с той же суммарной силой. Если модель уже права с вероятностью 1, строка — одни нули, и тянуть нечего. Если модель уверенно ошиблась, ошибочный символ тянется вниз очень сильно. Вся сеть — как система блоков и тросов: на логиты действуют силы, они передаются назад до весов, и с каждым шагом параметры понемногу уступают.

Курс выводит ту же формулу в уроке 1.3.2 (раздел «Почему именно cross-entropy: градиент “p минус ответ”») и использует её в 4.1.4.

То же для BatchNorm: вход xx (hprebn), выход yy (hpreact), нужен dxdx по известному dydy. Градиенты γ\gamma и β\beta простые, их сейчас опускаем. Карпатый выводит формулу на двух листах бумаги. Записываем по формулам статьи (но с n−1n - 1):

μ=1n∑ixi,σ2=1n−1∑i(xi−μ)2,x^i=xi−μσ2+ε,yi=γx^i+β.\mu = \frac{1}{n} \sum_i x_i, \quad \sigma^2 = \frac{1}{n-1} \sum_i (x_i - \mu)^2, \quad \hat x_i = \frac{x_i - \mu}{\sqrt{\sigma^2 + \varepsilon}}, \quad y_i = \gamma \hat x_i + \beta.

Порядок — как топологическая сортировка в micrograd, только на бумаге: сначала x^\hat x, потом σ2\sigma^2, потом μ\mu, потом xx. Главная ловушка — помнить, что xx, x^\hat x и yy — векторы из nn чисел, а μ\mu и σ2\sigma^2 — по одному числу. Поэтому:

  • σ2\sigma^2 влияет на все x^i\hat x_i, и в её градиенте сумма по ii;
  • в μ\mu сходятся 32 стрелки от x^i\hat x_i и одна от σ2\sigma^2. Но производная σ2\sigma^2 по μ\mu равна −2n−1∑i(xi−μ)-\frac{2}{n-1}\sum_i (x_i - \mu), а когда μ\mu — среднее, эта сумма равна нулю. Второе слагаемое исчезает;
  • из каждого xix_i выходят три стрелки: в μ\mu, в σ2\sigma^2 и в свой x^i\hat x_i. Их вклады складываются. При подстановке нужно переименовать индекс суммирования ii в jj, чтобы не спутать его с «нашим» ii.

После сокращений, если обозначить gi=∂L/∂yig_i = \partial L / \partial y_i:

∂L∂xi=γnσ2+ε(n gi−∑jgj−nn−1 x^i∑jgj x^j).\frac{\partial L}{\partial x_i} = \frac{\gamma}{n \sqrt{\sigma^2 + \varepsilon}} \Big( n\, g_i - \sum_j g_j - \frac{n}{n-1}\, \hat x_i \sum_j g_j\, \hat x_j \Big).

В коде это одна строка. Сложность в том, что формула выведена для одного нейрона, а нейронов 64. Все суммы должны идти по оси батча и правильно растягиваться обратно на (32,64)(32, 64):

dhprebn = bngain * bnvar_inv / n * (n * dhpreact - dhpreact.sum(0) - n / (n - 1) * bnraw * (dhpreact * bnraw).sum(0))

Совпадение с PyTorch — до 10−910^{-9}. Карпатый честно говорит, что перейти от формулы на бумаге к этой строке было нетривиально, и это отдельное упражнение: проверить формы и понять, почему всё растягивается правильно.

Последний шаг — собрать всё вместе. Сеть та же (200 нейронов в скрытом слое), обучение то же (200 тысяч шагов), но градиенты наши. Весь обратный проход — около 20 строк. Ноутбук лекции (MIT), комментарии наши:

with torch.no_grad(): # autograd больше не нужен: граф не строим
for i in range(max_steps):
# ...мини-батч и прямой проход как раньше, loss = F.cross_entropy(logits, Yb)
# cross-entropy
dlogits = F.softmax(logits, 1)
dlogits[range(n), Yb] -= 1
dlogits /= n
# второй линейный слой
dh = dlogits @ W2.T
dW2 = h.T @ dlogits
db2 = dlogits.sum(0)
# tanh
dhpreact = (1.0 - h**2) * dh
# BatchNorm
dbngain = (bnraw * dhpreact).sum(0, keepdim=True)
dbnbias = dhpreact.sum(0, keepdim=True)
dhprebn = bngain * bnvar_inv / n * (n * dhpreact - dhpreact.sum(0)
- n / (n - 1) * bnraw * (dhpreact * bnraw).sum(0))
# первый линейный слой
dembcat = dhprebn @ W1.T
dW1 = embcat.T @ dhprebn
db1 = dhprebn.sum(0)
# склейка и таблица эмбеддингов
demb = dembcat.view(emb.shape)
dC = torch.zeros_like(C)
for k in range(Xb.shape[0]):
for j in range(Xb.shape[1]):
dC[Xb[k, j]] += demb[k, j]
grads = [dC, dW1, db1, dW2, db2, dbngain, dbnbias]
lr = 0.1 if i < 100000 else 0.01
for p, grad in zip(parameters, grads):
p.data += -lr * grad # шаг по нашим градиентам, а не p.grad

Сначала цикл запускается на 100 шагов вместе с loss.backward(), и градиенты сверяются: они совпадают приблизительно, расхождения около 10−910^{-9}. Откуда именно они берутся, Карпатый не знает. Потом loss.backward() удаляется, и весь блок оборачивается в torch.no_grad(). Скользящие средние BatchNorm в этом цикле не копятся, поэтому после обучения статистика считается отдельно, калибровкой по всему train. Итог в ноутбуке — train 2.07, val 2.11: столько же, сколько с автоградом. Иначе и быть не могло — градиенты те же, только теперь видно всё, что происходит внутри.

Каждый слой — несколько строк, и только BatchNorm заметно сложнее. Карпатый надеется, что после лекции градиенты перестанут быть чёрным ящиком: понятно, как они текут от loss через все промежуточные значения. На практике обратный проход руками писать не нужно, но кто прошёл упражнения, может, по его шутке, считать себя «накачанным псом» из мема (swole doge). В конце он анонсирует рекуррентные сети. На деле следующей вышла лекция о WaveNet, а потом сразу GPT.

flowchart RL
L["loss"] --> Z["dlogits = (p − onehot)/n"]
Z --> H["dh = dlogits·W2ᵀ<br/>dW2, db2"]
H --> T["dhpreact = (1 − h²)·dh"]
T --> B["dhprebn — формула BatchNorm<br/>dγ, dβ"]
B --> E["dembcat = dhprebn·W1ᵀ<br/>dW1, db1"]
E --> C["dC — scatter-add<br/>по строкам таблицы"]
Обратный проход из упражнения 4: от loss к таблице эмбеддингов, по одному-трём строкам кода на слой.

Где курс объясняет то же самое и что есть сверх курса

Заголовок раздела «Где курс объясняет то же самое и что есть сверх курса»
Тема лекцииГде в курсеЧто в лекции сверх курса
Ручной backprop через MLP-модель имён4.1.4, раздел «Backprop руками»разбор по каждой атомарной операции и сверка с PyTorch через retain_grad
Градиент линейного слоя1.2.3вывод на примере 2×22 \times 2 и приём «формулы по формам»
Обратный проход по кирпичикам, ветки и +=3.1.2, 3.2.2двойственность «сумма ↔ растягивание» для тензоров
Градиент cross-entropy p−yp - y1.3.2, 4.1.4вывод через атомарный граф; нулевой градиент logit_maxes; образ «блоков и тросов»
Scatter-add для таблицы эмбеддингов4.1.4 (ловушка dC[X] += dE в NumPy)—
BatchNorm3.4.1вывод обратного прохода BatchNorm на бумаге
Несмещённая дисперсия—поправка Бесселя и расхождение train/test в BatchNorm
Проверка градиентов1.2.3функция cmp: точное и приближённое совпадение
  1. Откройте ноутбук-упражнение в Colab (ссылка выше) и пройдите упражнения 1–4 сами, подглядывая в видео только при затруднениях. Карпатый настаивает именно на таком порядке.
  2. В упражнении 1 посмотрите на dlogit_maxes. Почему он почти нулевой и почему не ровно ноль?
  3. Замените цикл для dC векторной операцией: torch.zeros_like(C).index_add_(0, Xb.view(-1), demb.view(-1, n_embd)). У нас результат совпал с циклом точно (разница 0.0). А обычное dC[Xb] += demb даст ошибку при повторах. Объясните, почему (подсказка — в уроке 4.1.4).
  4. Выведите формулу обратного прохода BatchNorm с делением на nn вместо n−1n - 1. Что изменится в строке кода?
  5. Замените tanh на ReLU и перепишите одну строку обратного прохода. Сверьте с PyTorch.
  6. Инициализируйте смещения нулями и найдите формулу градиента, которая «случайно» проходит проверку только при нулях. Так вы увидите, зачем Карпатый инициализирует всё маленькими случайными числами.
ENRU
backprop ninja«ниндзя» обратного распространения
leaky abstractionдырявая абстракция
manual backward passобратный проход руками
atomic compute graphграф из элементарных операций
retain_gradсохранить градиент промежуточного тензора
exact / approximate matchточное / приближённое совпадение
broadcasting ↔ sum (duality)растягивание ↔ сумма (двойственность)
replication, variable reuseкопирование, повторное использование переменной
scatter-addscatter-add: сложение градиентов по индексам
Bessel’s correctionпоправка Бесселя
biased / unbiased variance estimateсмещённая / несмещённая оценка дисперсии
train/test mismatchрасхождение обучения и применения
contrastive divergenceконтрастивная дивергенция
gradient checkerпроверка градиентов