20 / 27 · Нейронные сети
Обратное распространение ошибки
Как градиент функции потерь по каждому весу сети вычисляется за один обратный проход: вычислительный граф, цепное правило в матричной форме с размерностями, полный прямой и обратный проход двухслойной сети с кросс-энтропией, почему обратный режим стоит примерно столько же, сколько прямой проход, что вычисляет autograd в PyTorch и как проверять градиенты конечными разностями.
От линейных моделей к нейросетям · Градиентный спуск · Математический анализ ↗ · Линейная алгебра ↗
На этой странице 13
В предыдущей заметке построены сети с тысячами параметров; у маленького классификатора цифр оттуда их уже 17 226. Градиентному спуску на каждом шаге нужен градиент функции потерь по всем ним. Для логистической регрессии градиент имел замкнутый вид. Для сети функция потерь — композиция многих функций, и выписывать каждую частную производную вручную безнадёжно. Обратное распространение ошибки (backpropagation) вычисляет их все сразу, применяя цепное правило от функции потерь назад и переиспользуя каждый промежуточный результат. Его стоимость — небольшая константа, умноженная на стоимость вычисления функции потерь, при любом числе параметров. Для нейросетей его популяризировали Rumelhart, Hinton and Williams (1986), и это частный случай автоматического дифференцирования в обратном режиме (reverse-mode automatic differentiation), восходящего к Linnainmaa (1976) (Baydin et al., 2018, раздел 3.3).
Обозначения. Как и в предыдущей заметке, и , а функция потерь — скаляр. Для любой величины (скаляра, вектора или матрицы) — градиент функции потерь по ней, называемый её сопряжённой величиной (adjoint); он имеет ту же форму, что и . Для отображения из в якобиан (Jacobian) — это матрица с элементами . — поэлементное произведение, — one-hot-вектор класса .
Почему не дифференцировать численно?#
Отправная точка — определение производной. Сдвинем один параметр, заново прогоним сеть и поделим:
Это стоит двух прямых проходов на параметр: 34 452 прохода на шаг для классификатора цифр и невообразимо много для сети с миллионами весов. К тому же результат лишь приближённый, как показывает последний раздел. Символьное дифференцирование, при котором каждая выписывается формулой, избавляет от приближения, но повторяет одни и те же подвыражения в каждой формуле.
Вычислительный граф#
Любое вычисление — последовательность элементарных операций. Если нарисовать её как ориентированный ациклический граф, вершинами будут промежуточные значения, а ребро из в означает, что вычисляется из . Прямой проход вычисляет вершины по порядку. Обратный проход обходит их в обратном порядке и применяет цепное правило: если используется вершинами , его влияние на проходит через все них, и многомерное цепное правило складывает вклады:
Разобранный пример: один нейрон. Сигмоидный нейрон с квадратичной ошибкой, , , , в точке , , , :
| прямой проход | обратный проход | ||
|---|---|---|---|
| , | , |
Обратный столбец начинается с и в каждой вершине умножается на одну локальную производную. Локальная производная использует значение , сохранённое при прямом проходе, — поэтому значения прямого прохода и хранят.
Цепное правило в матричной форме#
Для векторов то же правило выглядит так: если с якобианом , то
Обратный проход умножает на транспонированные якобианы, и ему никогда не нужно строить их как матрицы. Три операции покрывают MLP.
Линейный слой , где . Поскольку , производная по одному весу собирает только тот выход, в который этот вес идёт: . В матричной форме
Градиент по весам — внешнее произведение сопряжённой величины, пришедшей сверху, и входа, сохранённого при прямом проходе.
Поэлементная активация . Её якобиан диагональный, , поэтому
Для ReLU — это маска : градиент проходит через активные нейроны без изменений и блокируется на неактивных.
Мини-батч. Если объекты записаны строками , то , и при функции потерь, усреднённой по батчу,
суммирует внешние произведения по объектам — так складываются градиенты объектов батча. Полезная проверка: сопряжённая величина имеет форму исходной, и для каждой формулы есть лишь один способ перемножить множители так, чтобы размерности совпали.
Двухслойная сеть: прямой и обратный проход#
Возьмём , скрытый слой из ReLU-нейронов и классов:
Через softmax и кросс-энтропию. Запишем . При дифференцировании по первое слагаемое даёт , а второе — . Следовательно,
предсказанные вероятности минус one-hot-вектор цели — ровно градиент логистической регрессии по её логитам. Дальше — три правила выше в порядке, обратном прямому проходу:
| величина | форма | сопряжённая величина | форма произведения |
|---|---|---|---|
Для батча из объектов с усреднённой функцией потерь , где строки — one-hot-векторы целей, и дальше работают формулы для батча из предыдущего раздела.
Разобранный пример. Возьмём , , , нулевые смещения, истинный класс (первый) и
Прямой проход: , поэтому ; , и . Обратный проход:
- ;
- имеет строки и ;
- ;
- ;
- имеет строки , и .
Третий скрытый нейрон получает наибольшую по модулю сопряжённую величину, , но он неактивен, поэтому маска её останавливает, и его входные веса получают от этого примера нулевой градиент. Нейрон, неактивный на всех объектах, никогда не обучается — это мёртвый ReLU-нейрон из предыдущей заметки.
Обратное распространение в сети с двумя слоями
- Первый слой вычисляет .
- ReLU оставляет положительные компоненты: , а прямой проход сохраняет маску .
- Второй слой даёт логиты , softmax — вероятности , а функция потерь равна .
- Обратный проход начинается с ; через softmax и кросс-энтропию .
- Второй слой даёт свой градиент по весам и передаёт вниз .
- Маска ReLU, сохранённая на шаге 2, обнуляет сопряжённую величину неактивного нейрона: .
- Первый слой даёт и .
Почему обратный режим стоит примерно одного прямого прохода#
Посчитаем работу для одного линейного слоя с батчем из объектов. Прямое произведение требует умножений со сложением. Обратный проход вычисляет два произведения того же размера, и , поэтому стоит вдвое больше прямого; поэлементные шаги пренебрежимо малы по сравнению с матричными произведениями. Весь градиент, таким образом, стоит около трёх прямых проходов, и это отношение не зависит от числа параметров. В общем случае для обратный режим вычисляет полный якобиан за обратных проходов со стоимостью , где гарантировано , а обычно лежит между 2 и 3 (Baydin et al., 2018, раздел 3.2; Griewank and Walther, 2008). Функция потерь — одно число, , так что один проход даёт весь градиент.
Прямой режим (forward mode) применяет цепное правило в другом направлении. Вместе с каждым промежуточным значением он несёт его производную по одному входному направлению и за один проход вычисляет произведение якобиана на вектор . Тогда градиенту по параметрам нужно проходов со стоимостью . Прямой режим выигрывает, когда входов мало, а выходов много; при обучении входов миллионы (параметры), а выход один (функция потерь), поэтому все библиотеки глубинного обучения построены на обратном режиме. В разделе «Код» 17 частных производных разобранного примера вычислены обоими способами: 17 проходов прямого режима против одного обратного прохода, с одинаковыми результатами.
Цена обратного режима — память. Обратному проходу нужны значения прямого прохода, например для и маски ReLU, поэтому все активации всех слоёв приходится хранить, пока до них не дойдёт обратный проход. У больших сетей именно это, а не параметры, заполняет память ускорителя. Контрольные точки градиента (gradient checkpointing) сохраняют лишь некоторые слои и пересчитывают остальные во время обратного прохода; для сети из слоёв это требует памяти ценой одного дополнительного прямого прохода (Chen et al., 2016).
Что вычисляет autograd#
Библиотеки вроде PyTorch реализуют обратный режим автоматически. Пока идёт прямой проход, каждая операция над тензором, которому нужны градиенты, записывает вершину вычислительного графа вместе с функцией, переводящей сопряжённую величину её выхода в сопряжённые величины её входов. PyTorch строит этот граф заново при каждом прямом проходе — это подход к автоматическому дифференцированию в обратном режиме через перегрузку операторов (operator overloading; Paszke et al., 2019, раздел 4.3), — поэтому обычные управляющие конструкции Python дифференцируемы.
| математика | PyTorch |
|---|---|
| параметры | тензоры с requires_grad=True (любой nn.Parameter) |
| вычислительный граф прямого прохода | цепочка вершин grad_fn, прикреплённых к каждому результату |
| начать обратный проход с | loss.backward() |
| сопряжённая величина | param.grad |
| сумма по потребителям вершины | градиенты прибавляются к .grad, поэтому перед каждым шагом вызывают optimizer.zero_grad() |
| один шаг градиентного спуска | optimizer.step() |
| произведение вектора на якобиан для нескалярного выхода | out.backward(v) или torch.autograd.grad(out, inputs, grad_outputs=v) |
| прямой проход, который ничего не записывает | with torch.no_grad(): |
Полезно знать три следствия:
- Градиенты накапливаются. Поскольку к
.gradприбавляют, два вызоваbackward()без обнуления удваивают градиент. Это сумма из многомерного цепного правила, и она позволяет намеренно накапливать градиент по нескольким маленьким батчам. - Для изломов есть соглашение. У ReLU нет производной в 0; PyTorch использует там 0. Любое значение из — допустимый субградиент, а точные нули на практике редки.
- Общие веса складываются. Вес, используемый в нескольких местах, например одна и та же рекуррентная матрица на каждом временном шаге, получает сумму сопряжённых величин от всех использований. Развёрнутая во времени рекуррентная сеть — это просто вычислительный граф с общими весами, и обратное распространение ошибки во времени (backpropagation through time) — это содержание этой заметки, применённое к нему; курс NLP показывает, как произведения якобианов вдоль оси времени заставляют эти градиенты затухать или взрываться.
Autograd вычисляет точные производные той программы, которая была выполнена, с точностью до арифметики с плавающей точкой. Он не решает, полезны ли эти производные: градиент, который равен нулю или почти нулю (мёртвый ReLU, насыщенная сигмоида) или астрономически велик, вычисляется столь же добросовестно. Дискретное решение — другой случай: у него вообще нет пути для градиента. torch.argmax возвращает целочисленный индекс, не связанный с графом (requires_grad равно False), поэтому функцию потерь, вычисленную только по индексу, нельзя продифференцировать обратным проходом, а функция потерь, которая использует индекс для выбора, не передаёт градиента оценкам, по которым этот выбор сделан. Когда сам выбор нужно обучать, его заменяют дифференцируемым приближением, например softmax с температурой; раздел «Код» показывает и то, и другое.
Проверка градиентов конечными разностями#
Написанный вручную обратный проход или собственная функция autograd могут содержать ошибки, при которых функция потерь всё равно убывает. Стандартный тест сравнивает их с центральной разностью из первого раздела, по одной координате за раз, через относительную ошибку
с небольшим нижним порогом, например , чтобы два нулевых градиента считались совпадением, а не давали .
Центральная разность лучше односторонней . По формуле Тейлора , поэтому у одностороннего отношения ошибка , а в центральном чётные члены сокращаются и ошибка равна . Меньшее не всегда лучше: каждое значение функции потерь несёт ошибку округления порядка относительно в двойной точности, а деление на её усиливает.
Ошибка конечных разностей в зависимости от шага
На практике:
- считать в двойной точности (
torch.float64); в одинарной точности ошибка округления порядка , и проверка становится ненадёжной; - брать порядка – с центральными разностями; относительная ошибка около или ниже означает совпадение, около или выше — ошибку в коде;
- избегать точек рядом с изломами: если вход ReLU ближе чем на к нулю, две стороны видят разные наклоны;
- в большой сети проверять несколько случайных координат каждого тензора параметров;
torch.autograd.gradcheckделает то же для собственной функции.
В разделе «Код» написанный вручную обратный проход разобранного примера совпадает с центральными разностями по всем 17 параметрам с относительной ошибкой . Если убрать маску ReLU из обратного прохода — типичная ошибка, — она вырастает до : веса третьего нейрона получают ненулевой градиент там, где истинный равен нулю.
Что изменилось#
| Численное дифференцирование | Обратное распространение ошибки | |
|---|---|---|
| Стоимость полного градиента | два прямых прохода на параметр | около трёх прямых проходов в сумме |
| Точность | ошибки усечения и округления | точно с точностью до плавающей точки |
| Память | один прямой проход | все сохранённые активации (или контрольные точки) |
| Что нужно написать | только прямой проход | только прямой проход, если есть autograd |
| Роль сегодня | проверка обратного прохода | так обучается любая сеть |
Ограничения#
- Память растёт с глубиной и размером батча, потому что активации прямого прохода хранятся для обратного.
- Нужны дифференцируемые операции. Дискретные выборы, сэмплирование и
argmaxне дают полезного градиента:argmaxвозвращает индекс вне графа, а у ступенчатой функции производная почти всюду равна нулю. Дифференцируемые приближения вроде softmax с температурой и приёмы вроде трюка репараметризации (reparameterization trick) из курса о генеративных моделях делают некоторые из них дифференцируемыми. - Градиент локален. Обратное распространение точно вычисляет производную в текущих весах; оно не говорит, придёт ли градиентный спуск к хорошему решению, а произведения многих якобианов могут заставить градиент затухать или взрываться в глубоких или рекуррентных сетях.
- Последовательность по глубине. Обратный проход слоя ждёт слоя над ним, и это ограничивает, насколько слои глубокой сети можно вычислять параллельно.
Что нужно запомнить#
- Сопряжённая величина имеет форму ; вершина, используемая несколько раз, суммирует вклады своих потребителей.
- Линейный слой: , , ; активация: ; softmax с кросс-энтропией: .
- Обратный режим стоит небольшую константу, умноженную на один прямой проход (около 3 для MLP), независимо от числа параметров; прямому режиму нужен один проход на параметр.
- Цена — память: все активации хранятся, пока их не использует обратный проход.
- PyTorch записывает граф во время прямого прохода;
loss.backward()заполняет.gradприбавлением, отсюдаzero_grad(). - Написанный вручную градиент проверяют центральными разностями в двойной точности, , по относительной ошибке.
Код#
Разобранный пример вручную и через autograd, проверка градиента, которая ловит подложенную ошибку, шаг конечных разностей, прямой режим против обратного, два соглашения autograd и argmax без градиента:
import numpy as np, torch
import torch.autograd.forward_ad as fwAD
# разобранный пример: x = (1, 2), истинный класс 0 (первый), нулевые смещения
x = np.array([1.0, 2.0]); y = 0
W1 = np.array([[1.0, 0.0], [-1.0, 1.0], [0.5, -1.0]]); b1 = np.zeros(3)
W2 = np.array([[1.0, -1.0, 2.0], [0.5, 1.0, -1.0]]); b2 = np.zeros(2)
def forward(W1, b1, W2, b2):
z1 = W1 @ x + b1; h = np.maximum(z1, 0); z2 = W2 @ h + b2
m = z2.max(); logZ = m + np.log(np.exp(z2 - m).sum())
return z1, h, z2, np.exp(z2 - logZ), logZ - z2[y]
def backward(W1, b1, W2, b2, mask=True):
z1, h, z2, p, L = forward(W1, b1, W2, b2)
dz2 = p - np.eye(2)[y] # softmax + кросс-энтропия
dW2, db2, dh = np.outer(dz2, h), dz2, W2.T @ dz2
dz1 = dh * (z1 > 0) if mask else dh # mask=False подкладывает ошибку: ReLU пропущена
return np.outer(dz1, x), dz1, dW2, db2
z1, h, z2, p, L = forward(W1, b1, W2, b2)
dW1, db1, dW2, db2 = backward(W1, b1, W2, b2)
print(p.round(3), round(L, 3)) # [0.182 0.818] 1.701
print(dW2.round(3).tolist()) # [[-0.818, -0.818, -0.0], [0.818, 0.818, 0.0]]
print((W2.T @ (p - np.eye(2)[y])).round(3), dW1.round(3).tolist())
# [-0.409 1.635 -2.453] [[-0.409, -0.818], [1.635, 3.27], [-0.0, -0.0]]
# те же градиенты от autograd
T = lambda a: torch.tensor(a, requires_grad=True)
tW1, tb1, tW2, tb2 = T(W1), T(b1), T(W2), T(b2)
loss = torch.nn.functional.cross_entropy((tW2 @ torch.relu(tW1 @ torch.tensor(x) + tb1) + tb2)[None], torch.tensor([y]))
loss.backward()
print(np.allclose(tW1.grad.numpy(), dW1), np.allclose(tW2.grad.numpy(), dW2)) # True True
# проверка градиента: центральные разности по каждому параметру, относительная ошибка против backprop
def check(mask):
params = [W1.copy(), b1.copy(), W2.copy(), b2.copy()]
grads = backward(*params, mask=mask)
worst, eps = 0.0, 1e-6
for P, G in zip(params, grads):
for i in np.ndindex(P.shape):
old = P[i]
P[i] = old + eps; Lp = forward(*params)[-1]
P[i] = old - eps; Lm = forward(*params)[-1]
P[i] = old
num = (Lp - Lm) / (2 * eps)
worst = max(worst, abs(num - G[i]) / max(abs(num), abs(G[i]), 1e-8))
return worst
print(f'{check(True):.1e} {check(False):.1e}') # 3.6e-10 1.0e+00
# шаг конечной разности: ошибка усечения при больших шагах, ошибка округления при крошечных
f = lambda a: forward(np.array([[a, 0.0], [-1.0, 1.0], [0.5, -1.0]]), b1, W2, b2)[-1]
for eps in (1e-1, 1e-5, 1e-12):
print(eps, f'{abs((f(1 + eps) - f(1 - eps)) / (2 * eps) - dW1[0, 0]):.1e}')
# 0.1 2.0e-05 | 1e-05 7.4e-12 | 1e-12 1.1e-04
# прямому режиму нужен один проход на параметр, обратному — один проход всего
flat = torch.cat([t.detach().flatten() for t in (tW1, tb1, tW2, tb2)])
def L_of(theta):
A, a, B, b = theta[:6].view(3, 2), theta[6:9], theta[9:15].view(2, 3), theta[15:]
return torch.nn.functional.cross_entropy((B @ torch.relu(A @ torch.tensor(x) + a) + b)[None], torch.tensor([y]))
def directional(v): # один проход прямого режима: производная вдоль v
with fwAD.dual_level():
return fwAD.unpack_dual(L_of(fwAD.make_dual(flat, v))).tangent
fwd = torch.stack([directional(torch.eye(17, dtype=flat.dtype)[i]) for i in range(17)])
print(len(fwd), torch.allclose(fwd, torch.cat([t.grad.flatten() for t in (tW1, tb1, tW2, tb2)]))) # 17 True
# детали autograd: у ReLU производная 0 в нуле, а .grad накапливается, пока его не обнулят
u = torch.tensor(0.0, requires_grad=True); torch.relu(u).backward(); print(u.grad.item()) # 0.0
w = torch.tensor(3.0, requires_grad=True)
for _ in range(2):
(w ** 2).backward()
print(w.grad.item()) # 12.0: два вызова backward складывают 6 + 6
# argmax возвращает целочисленный индекс без пути для autograd; softmax с температурой 0.5 — дифференцируемое приближение
s = torch.tensor([1.0, 3.0, 2.0], requires_grad=True)
k = s.argmax(); print(k.item(), k.dtype, k.requires_grad) # 1 torch.int64 False
(torch.softmax(s / 0.5, 0) @ torch.arange(3.0)).backward() # ожидаемый индекс под softmax, 1.101
print([round(g, 3) for g in s.grad.tolist()]) # [-0.035, -0.176, 0.211]
Дальше#
Обратное распространение ошибки даёт точные градиенты для сети любой глубины. Точный не значит полезный: в глубокой стопке градиент — произведение одного якобиана на слой и может геометрически уменьшаться или расти, а сеть, достаточно гибкая, чтобы подогнаться под что угодно, подгоняется и под шум обучающей выборки. Следующая заметка разбирает, что делает глубокие сети обучаемыми и удерживает их от переобучения: инициализацию, сохраняющую масштаб сигналов, дропаут, затухание весов, расписания шага обучения, аугментацию данных и остаточные связи.
Литература#
- D. E. Rumelhart, G. E. Hinton, R. J. Williams. Learning representations by back-propagating errors. Nature, 323(6088):533–536, 1986.
- S. Linnainmaa. Taylor expansion of the accumulated rounding error. BIT Numerical Mathematics, 16(2):146–160, 1976.
- A. G. Baydin, B. A. Pearlmutter, A. A. Radul, J. M. Siskind. Automatic differentiation in machine learning: a survey. Journal of Machine Learning Research, 18(153):1–43, 2018. Разделы 3.1–3.2 (прямой и обратный режимы, их стоимость), раздел 3.3 (история).
- A. Griewank, A. Walther. Evaluating Derivatives: Principles and Techniques of Algorithmic Differentiation, 2nd ed. SIAM, 2008.
- T. Chen, B. Xu, C. Zhang, C. Guestrin. Training deep nets with sublinear memory cost. arXiv:1604.06174, 2016.
- A. Paszke et al. PyTorch: an imperative style, high-performance deep learning library. NeurIPS, 2019. Раздел 4.3 (автоматическое дифференцирование).
- I. Goodfellow, Y. Bengio, A. Courville. Deep Learning. MIT Press, 2016. Раздел 6.5 (обратное распространение и другие алгоритмы дифференцирования).
- C. M. Bishop. Pattern Recognition and Machine Learning. Springer, 2006. Раздел 5.3 (обратное распространение ошибки; 5.3.3 — о его эффективности и численных проверках).
- Документация PyTorch: Autograd mechanics,
torch.autograd.gradcheck, forward-mode AD. - E. Sokolov et al. Машинное обучение (HSE), конспекты лекций, лекция 7: введение в глубинное обучение, и материалы семинара о тензорах PyTorch, autograd и MLP.