Перейти к содержимому
ПереводыStanford

CS231n: обратное распространение ошибки — интуиция, цепное правило и поток градиентов (перевод конспекта)

Полный перевод конспекта Stanford CS231n «Backpropagation, Intuitions»: интерпретация градиента, цепное правило на вычислительных схемах, локальность backprop, сигмоидный нейрон, поэтапные вычисления, накопление градиентов на развилках, шаблоны add/max/mul, градиенты матричных операций.

Мотивация. В этом разделе мы выработаем интуитивное понимание обратного распространения ошибки (backpropagation) — способа вычислять градиенты выражений рекурсивным применением цепного правила. Понимать этот процесс и его тонкости критически важно, чтобы эффективно разрабатывать, проектировать и отлаживать нейронные сети.

Постановка задачи. Основная задача раздела такова: дана функция f(x)f(x), где xx — вектор входов, и мы хотим вычислить градиент ff в точке xx (то есть ∇f(x)\nabla f(x)).

Мотивация. Напомним, что главная причина интереса к этой задаче в том, что для нейронных сетей ff — это функция потерь (LL), а входы xx — обучающие данные и веса нейронной сети. Например, функцией потерь может быть потеря SVM, а входами — и обучающие данные (xi,yi),i=1…N(x_i, y_i), i = 1 \ldots N, и веса со сдвигами W,bW, b. Заметьте, что (как обычно в машинном обучении) обучающие данные мы считаем заданными и фиксированными, а веса — переменными, которыми управляем. Поэтому, хотя обратным распространением легко вычислить и градиент по входным примерам xix_i, на практике обычно вычисляют только градиент по параметрам (например, W,bW, b), чтобы с его помощью обновить параметры. Однако, как мы увидим позже, градиент по xix_i тоже бывает полезен, например для визуализации и интерпретации того, что делает нейронная сеть.

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

Простые выражения и интерпретация градиента

Заголовок раздела «Простые выражения и интерпретация градиента»

Начнём с простого, чтобы выработать обозначения и соглашения для более сложных выражений. Рассмотрим функцию умножения двух чисел f(x,y)=xyf(x, y) = x y. Частные производные по каждому входу находятся простым дифференцированием:

f(x,y)=xy→∂f∂x=y∂f∂y=xf(x, y) = x y \qquad \rightarrow \qquad \frac{\partial f}{\partial x} = y \qquad \frac{\partial f}{\partial y} = x

Интерпретация. Помните, что сообщают производные: скорость изменения функции по данной переменной в бесконечно малой окрестности конкретной точки:

df(x)dx=lim⁡h→0f(x+h)−f(x)h\frac{df(x)}{dx} = \lim_{h \to 0} \frac{f(x + h) - f(x)}{h}

Техническое замечание: знак дроби в левой части, в отличие от правой, — не деление. Эта запись означает, что оператор ddx\frac{d}{dx} применяется к функции ff и возвращает другую функцию (производную). Выражение выше удобно понимать так: при очень малом hh функция хорошо приближается прямой, а производная — её наклон. Иначе говоря, производная по каждой переменной сообщает чувствительность всего выражения к значению этой переменной. Например, если x=4,y=−3x = 4, y = -3, то f(x,y)=−12f(x, y) = -12, а производная по xx равна ∂f∂x=−3\frac{\partial f}{\partial x} = -3. Это значит: если чуть-чуть увеличить эту переменную, всё выражение уменьшится (из-за знака минус), причём в три раза сильнее. Это видно, если переписать уравнение выше: f(x+h)=f(x)+hdf(x)dxf(x + h) = f(x) + h \frac{df(x)}{dx}. Аналогично, поскольку ∂f∂y=4\frac{\partial f}{\partial y} = 4, увеличение yy на очень малое hh тоже увеличит выход функции (знак плюс), причём на 4h4h.

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

Как уже говорилось, градиент ∇f\nabla f — вектор частных производных, так что ∇f=[∂f∂x,∂f∂y]=[y,x]\nabla f = [\frac{\partial f}{\partial x}, \frac{\partial f}{\partial y}] = [y, x]. Хотя формально градиент — вектор, для простоты мы часто будем говорить «градиент по x» вместо формально правильного «частная производная по x».

Можно вывести и производные для сложения:

f(x,y)=x+y→∂f∂x=1∂f∂y=1f(x, y) = x + y \qquad \rightarrow \qquad \frac{\partial f}{\partial x} = 1 \qquad \frac{\partial f}{\partial y} = 1

то есть производная и по xx, и по yy равна единице независимо от их значений. Это логично: увеличение любого из x,yx, y увеличит выход ff, и скорость роста не зависит от самих значений x,yx, y (в отличие от умножения). Последняя функция, которой мы будем много пользоваться, — операция max:

f(x,y)=max⁡(x,y)→∂f∂x=1(x≥y)∂f∂y=1(y≥x)f(x, y) = \max(x, y) \qquad \rightarrow \qquad \frac{\partial f}{\partial x} = \mathbb{1}(x \geq y) \qquad \frac{\partial f}{\partial y} = \mathbb{1}(y \geq x)

То есть (суб)градиент равен 1 для большего входа и 0 для другого. Интуитивно: если входы x=4,y=2x = 4, y = 2, то максимум равен 4, и функция нечувствительна к yy. Если увеличить yy на крошечное hh, функция по-прежнему выдаст 4, поэтому градиент нулевой: эффекта нет. Конечно, если изменить yy сильно (например, больше чем на 2), значение ff изменится, но производные ничего не говорят о влиянии таких больших изменений входов: они информативны только для крошечных, бесконечно малых изменений, на что указывает lim⁡h→0\lim_{h \rightarrow 0} в определении.

Перейдём к более сложным выражениям из нескольких вложенных функций, например f(x,y,z)=(x+y)zf(x, y, z) = (x + y) z. Это выражение ещё достаточно простое, чтобы продифференцировать его напрямую, но мы подойдём к нему особым образом, который поможет понять интуицию обратного распространения. Заметим, что выражение раскладывается на два: q=x+yq = x + y и f=qzf = q z. Производные каждого мы уже умеем считать, как в предыдущем разделе. ff — просто произведение qq и zz, так что ∂f∂q=z,∂f∂z=q\frac{\partial f}{\partial q} = z, \frac{\partial f}{\partial z} = q, а qq — сумма xx и yy, так что ∂q∂x=1,∂q∂y=1\frac{\partial q}{\partial x} = 1, \frac{\partial q}{\partial y} = 1. Однако градиент по промежуточному значению qq нам сам по себе не нужен: значение ∂f∂q\frac{\partial f}{\partial q} бесполезно. В конечном счёте нас интересует градиент ff по её входам x,y,zx, y, z. Цепное правило говорит, что правильный способ «сцепить» эти выражения для градиентов — умножение. Например, ∂f∂x=∂f∂q∂q∂x\frac{\partial f}{\partial x} = \frac{\partial f}{\partial q} \frac{\partial q}{\partial x}. На практике это просто произведение двух чисел, хранящих два градиента. Посмотрим на примере:

# задаём входы
x = -2; y = 5; z = -4
# прямой проход
q = x + y # q становится 3
f = q * z # f становится -12
# обратный проход (обратное распространение) в обратном порядке:
# сначала через f = q * z
dfdz = q # df/dz = q, поэтому градиент по z равен 3
dfdq = z # df/dq = z, поэтому градиент по q равен -4
dqdx = 1.0
dqdy = 1.0
# теперь через q = x + y
dfdx = dfdq * dqdx # Это умножение и есть цепное правило!
dfdy = dfdq * dqdy

Остаются градиенты по переменным [dfdx, dfdy, dfdz], которые сообщают чувствительность f к переменным x, y, z! Это простейший пример обратного распространения. Дальше мы будем пользоваться более краткой записью без префикса df. Например, будем писать просто dq вместо dfdq и всегда подразумевать, что градиент берётся от итогового выхода.

Это вычисление удобно изобразить в виде схемы:

flowchart LR
X["x<br/>значение −2<br/>градиент −4"] --> ADD(("+"))
Y["y<br/>значение 5<br/>градиент −4"] --> ADD
ADD -->|"q: значение 3, градиент −4"| MUL(("×"))
Z["z<br/>значение −4<br/>градиент 3"] --> MUL
MUL --> F["f<br/>значение −12<br/>градиент 1"]
Схема с вещественными значениями — наглядное представление вычисления. Прямой проход вычисляет значения от входов к выходу. Обратный проход выполняет обратное распространение: начинает с конца и рекурсивно применяет цепное правило, вычисляя градиенты вплоть до входов схемы. Градиенты можно представлять как поток, текущий по схеме назад. (В оригинале значения зелёные, градиенты красные.)

Интуитивное понимание обратного распространения

Заголовок раздела «Интуитивное понимание обратного распространения»

Заметьте, что обратное распространение — красиво локальный процесс. Каждый вентиль (gate) схемы получает какие-то входы и сразу может вычислить две вещи: 1) своё выходное значение и 2) локальный градиент своего выхода по своим входам. Вентили делают это совершенно независимо, ничего не зная о деталях всей схемы, в которую встроены. Однако после прямого прохода, во время обратного распространения, вентиль в итоге узнаёт градиент итогового выхода всей схемы по своему выходному значению. Цепное правило говорит, что вентиль должен взять этот градиент и умножить на него каждый градиент, который он обычно вычисляет для своих входов.

Это дополнительное умножение (для каждого входа) по цепному правилу превращает одиночный и сравнительно бесполезный вентиль в шестерёнку сложной схемы — например, целой нейронной сети.

Разберём, как это работает, снова на примере. Вентиль сложения получил входы [−2,5][-2, 5] и вычислил выход 3. Поскольку он вычисляет сложение, его локальный градиент по обоим входам равен +1+1. Остальная схема вычислила итоговое значение −12-12. При обратном проходе, когда цепное правило рекурсивно применяется по схеме назад, вентиль сложения (вход вентиля умножения) узнаёт, что градиент по его выходу равен −4-4. Если очеловечить схему и считать, что она хочет выдать значение побольше (это помогает интуиции), то можно сказать, что схема «хочет», чтобы выход вентиля сложения был меньше (из-за знака минус), причём с силой 4. Чтобы продолжить рекурсию и сцепить градиент, вентиль сложения берёт этот градиент и умножает на него все локальные градиенты по своим входам (градиенты по x и y становятся 1⋅(−4)=−41 \cdot (-4) = -4). Заметьте, что это даёт нужный эффект: если x, y уменьшатся (в ответ на свой отрицательный градиент), выход вентиля сложения уменьшится, а это, в свою очередь, увеличит выход вентиля умножения.

Итак, обратное распространение можно понимать как общение вентилей друг с другом (через сигнал градиента): хотят ли они, чтобы их выходы увеличились или уменьшились (и насколько сильно), чтобы итоговый выход стал больше.

Введённые выше вентили довольно произвольны. Вентилем может служить любая дифференцируемая функция; можно объединять несколько вентилей в один или раскладывать функцию на несколько вентилей — как удобнее. Рассмотрим ещё одно выражение, иллюстрирующее это:

f(w,x)=11+e−(w0x0+w1x1+w2)f(w, x) = \frac{1}{1 + e^{-(w_0 x_0 + w_1 x_1 + w_2)}}

Как мы увидим позже, это выражение описывает двумерный нейрон (с входами x и весами w) с сигмоидной функцией активации. Но пока будем считать его просто функцией от входов w, x к одному числу. Функция состоит из нескольких вентилей. Кроме уже описанных (сложение, умножение, max), есть ещё четыре:

f(x)=1x→dfdx=−1/x2fc(x)=c+x→dfdx=1f(x)=ex→dfdx=exfa(x)=ax→dfdx=a\begin{aligned} f(x) &= \frac{1}{x} & &\rightarrow & \frac{df}{dx} &= -1/x^2 \\ f_c(x) &= c + x & &\rightarrow & \frac{df}{dx} &= 1 \\ f(x) &= e^x & &\rightarrow & \frac{df}{dx} &= e^x \\ f_a(x) &= a x & &\rightarrow & \frac{df}{dx} &= a \end{aligned}

Функции fc,faf_c, f_a сдвигают вход на константу cc и масштабируют его на константу aa соответственно. Формально это частные случаи сложения и умножения, но мы вводим их как (новые) унарные вентили, поскольку градиенты по константам c,ac, a нам не нужны. Полная схема выглядит так:

flowchart LR
W0["w0: 2.00 / −0.20"] --> M1(("×"))
X0["x0: −1.00 / 0.39"] --> M1
W1["w1: −3.00 / −0.39"] --> M2(("×"))
X1["x1: −2.00 / −0.59"] --> M2
M1 -->|"−2.00 / 0.20"| A1(("+"))
M2 -->|"6.00 / 0.20"| A1
A1 -->|"4.00 / 0.20"| A2(("+"))
W2["w2: −3.00 / 0.20"] --> A2
A2 -->|"1.00 / 0.20"| N(("×−1"))
N -->|"−1.00 / −0.20"| E(("exp"))
E -->|"0.37 / −0.53"| P(("+1"))
P -->|"1.37 / −0.53"| R(("1/x"))
R --> OUT["выход: 0.73 / 1.00"]
Пример схемы двумерного нейрона с сигмоидной функцией активации. Входы — [x0, x1], (обучаемые) веса нейрона — [w0, w1, w2]. На каждом проводе: значение при прямом проходе / градиент при обратном. Как мы увидим позже, нейрон вычисляет скалярное произведение с входом, а затем его активация мягко сжимается сигмоидой в диапазон от 0 до 1.

В этом примере видна длинная цепочка применений функций к результату скалярного произведения w и x. Функция, которую реализуют эти операции, называется сигмоидой σ(x)\sigma(x). Оказывается, производная сигмоиды по входу упрощается, если её вывести (после забавного трюка — прибавить и вычесть 1 в числителе):

σ(x)=11+e−x→dσ(x)dx=e−x(1+e−x)2=(1+e−x−11+e−x)(11+e−x)=(1−σ(x))σ(x)\sigma(x) = \frac{1}{1 + e^{-x}} \qquad \rightarrow \qquad \frac{d\sigma(x)}{dx} = \frac{e^{-x}}{(1 + e^{-x})^2} = \left( \frac{1 + e^{-x} - 1}{1 + e^{-x}} \right) \left( \frac{1}{1 + e^{-x}} \right) = \left( 1 - \sigma(x) \right) \sigma(x)

Градиент удивительно упрощается. Например, при прямом проходе выражение с сигмоидой получает вход 1,0 и вычисляет выход 0,73. Вывод выше показывает, что локальный градиент равен просто (1−0,73)⋅0,73≈0,2(1 - 0{,}73) \cdot 0{,}73 \approx 0{,}2 — как и вычислила схема (см. рисунок выше), только теперь одним простым и эффективным выражением (и с меньшими численными проблемами). Поэтому в любом реальном приложении очень полезно объединять эти операции в один вентиль. Посмотрим на backprop для этого нейрона в коде:

w = [2,-3,-3] # пусть веса и данные случайные
x = [-1, -2]
# прямой проход
dot = w[0]*x[0] + w[1]*x[1] + w[2]
f = 1.0 / (1 + math.exp(-dot)) # сигмоида
# обратный проход через нейрон (обратное распространение)
ddot = (1 - f) * f # градиент по переменной dot — по выведенной формуле градиента сигмоиды
dx = [w[0] * ddot, w[1] * ddot] # распространяем в x
dw = [x[0] * ddot, x[1] * ddot, 1.0 * ddot] # распространяем в w
# готово! у нас есть градиенты по входам схемы

Совет по реализации: поэтапное обратное распространение. Как показано в коде выше, на практике всегда полезно разбивать прямой проход на этапы, через которые легко провести обратное распространение. Например, здесь мы завели промежуточную переменную dot, хранящую результат скалярного произведения w и x. При обратном проходе мы последовательно (в обратном порядке) вычисляем соответствующие переменные (например, ddot, а в итоге dw, dx), хранящие градиенты по этим переменным.

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

Посмотрим на ещё одном примере. Пусть есть функция вида:

f(x,y)=x+σ(y)σ(x)+(x+y)2f(x, y) = \frac{x + \sigma(y)}{\sigma(x) + (x + y)^2}

Сразу оговоримся: функция совершенно бесполезна, и непонятно, зачем вообще вычислять её градиент, — разве что она хороший пример обратного распространения на практике. Очень важно подчеркнуть: если взяться дифференцировать её по xx или yy напрямую, получатся очень большие и сложные выражения. Но делать это совершенно не нужно: нам не требуется явная формула для градиента — достаточно знать, как его вычислить. Вот как можно организовать прямой проход для такого выражения:

x = 3 # пример значений
y = -4
# прямой проход
sigy = 1.0 / (1 + math.exp(-y)) # сигмоида в числителе #(1)
num = x + sigy # числитель #(2)
sigx = 1.0 / (1 + math.exp(-x)) # сигмоида в знаменателе #(3)
xpy = x + y #(4)
xpysqr = xpy**2 #(5)
den = sigx + xpysqr # знаменатель #(6)
invden = 1.0 / den #(7)
f = num * invden # готово! #(8)

Уф, к концу выражения прямой проход вычислен. Заметьте: мы организовали код так, что в нём много промежуточных переменных, и каждая — простое выражение, локальные градиенты которого мы уже знаем. Поэтому вычислить обратный проход легко: идём назад, и для каждой переменной прямого прохода (sigy, num, sigx, xpy, xpysqr, den, invden) заводим такую же переменную, но с префиксом d, — она будет хранить градиент выхода схемы по этой переменной. Кроме того, каждый кусок нашего backprop вычисляет локальный градиент выражения и сцепляет его умножением с градиентом по этому выражению. Для каждой строки отмечено, к какой части прямого прохода она относится:

# backprop f = num * invden
dnum = invden # градиент по числителю #(8)
dinvden = num #(8)
# backprop invden = 1.0 / den
dden = (-1.0 / (den**2)) * dinvden #(7)
# backprop den = sigx + xpysqr
dsigx = (1) * dden #(6)
dxpysqr = (1) * dden #(6)
# backprop xpysqr = xpy**2
dxpy = (2 * xpy) * dxpysqr #(5)
# backprop xpy = x + y
dx = (1) * dxpy #(4)
dy = (1) * dxpy #(4)
# backprop sigx = 1.0 / (1 + math.exp(-x))
dx += ((1 - sigx) * sigx) * dsigx # Обратите внимание на += ! См. ниже #(3)
# backprop num = x + sigy
dx += (1) * dnum #(2)
dsigy = (1) * dnum #(2)
# backprop sigy = 1.0 / (1 + math.exp(-y))
dy += ((1 - sigy) * sigy) * dsigy #(1)
# готово! уф

Обратите внимание на несколько вещей.

Кэшируйте переменные прямого прохода. Для обратного прохода очень полезно иметь некоторые переменные, использованные при прямом. На практике код стоит организовать так, чтобы эти переменные кэшировались и были доступны при обратном распространении. Если это слишком сложно, можно (но расточительно) вычислить их заново.

Градиенты складываются на развилках. Прямое выражение использует переменные x, y несколько раз, поэтому при обратном распространении нужно аккуратно использовать += вместо =, чтобы накапливать градиент по этим переменным (иначе мы бы его перезаписали). Это следует из цепного правила для функций многих переменных: если переменная ветвится в разные части схемы, градиенты, текущие к ней обратно, складываются.

Интересно, что во многих случаях обратный поток градиента можно интерпретировать интуитивно. Например, три самых распространённых вентиля нейронных сетей (сложение, умножение, max) имеют очень простые интерпретации того, как они ведут себя при обратном распространении. Рассмотрим пример схемы:

flowchart LR
X["x: 3.00 / −8.00"] --> MUL(("×"))
Y["y: −4.00 / 6.00"] --> MUL
Z["z: 2.00 / 2.00"] --> MAX(("max"))
W["w: −1.00 / 0.00"] --> MAX
MUL -->|"−12.00 / 2.00"| ADD(("+"))
MAX -->|"2.00 / 2.00"| ADD
ADD -->|"−10.00 / 2.00"| TWO(("×2"))
TWO --> OUT["выход: −20.00 / 1.00"]
Пример схемы, показывающий интуицию операций, которые обратное распространение выполняет при обратном проходе, чтобы вычислить градиенты по входам (значение / градиент). Сложение распределяет градиент поровну на все входы. Max направляет градиент к большему входу. Умножение берёт входные значения, меняет их местами и умножает на свой градиент.

Глядя на схему, видим:

Вентиль сложения всегда берёт градиент по своему выходу и раздаёт его поровну всем входам, независимо от их значений при прямом проходе. Это следует из того, что локальный градиент сложения — просто +1,0+1{,}0, так что градиенты по всем входам в точности равны градиенту по выходу: он умножается на 1,0 и не меняется. В примере вентиль + передал градиент 2,00 обоим входам — поровну и без изменений.

Вентиль max направляет градиент. В отличие от сложения, раздающего градиент без изменений всем входам, max передаёт градиент (без изменений) ровно одному входу — тому, у которого при прямом проходе было наибольшее значение. Это потому, что локальный градиент max равен 1,0 для наибольшего значения и 0,0 для всех остальных. В примере max направил градиент 2,00 переменной z, значение которой больше, чем у w, а градиент по w остался нулевым.

Вентиль умножения интерпретировать чуть сложнее. Его локальные градиенты — входные значения (но переставленные местами), и по цепному правилу они умножаются на градиент по выходу. В примере градиент по x равен −8,00-8{,}00, то есть −4,00×2,00-4{,}00 \times 2{,}00.

Неинтуитивные эффекты и их последствия. Если один вход вентиля умножения очень мал, а другой очень велик, вентиль поведёт себя немного неинтуитивно: малому входу он назначит относительно огромный градиент, а большому — крошечный. В линейных классификаторах веса скалярно умножаются (wTxiw^T x_i) на входы, а значит, масштаб данных влияет на величину градиента по весам. Например, если при предобработке умножить все входные примеры xix_i на 1000, градиент по весам станет в 1000 раз больше, и, чтобы это компенсировать, придётся во столько же раз уменьшить скорость обучения. Вот почему предобработка так важна, иногда в неочевидных отношениях! Интуитивное понимание того, как текут градиенты, помогает отлаживать такие случаи.

Разделы выше касались отдельных переменных, но все идеи напрямую переносятся на операции с матрицами и векторами. Нужно лишь внимательнее следить за размерностями и транспонированием.

Градиент произведения матриц. Пожалуй, самая хитрая операция — произведение матрицы на матрицу (обобщающее все произведения матрицы на вектор и вектора на вектор):

# прямой проход
W = np.random.randn(5, 10)
X = np.random.randn(10, 3)
D = W.dot(X)
# пусть сверху по схеме пришёл градиент по D
dD = np.random.randn(*D.shape) # той же формы, что и D
dW = dD.dot(X.T) #.T даёт транспонированную матрицу
dX = W.T.dot(dD)

Совет: пользуйтесь анализом размерностей! Выражения для dW и dX не нужно запоминать: их легко вывести заново по размерностям. Например, мы знаем, что градиент по весам dW должен иметь тот же размер, что и W, и должен зависеть от произведения X и dD (как и в случае, когда X, W — числа, а не матрицы). Всегда есть ровно один способ получить это так, чтобы размерности сошлись. Например, X имеет размер [10 × 3], dD — [5 × 3]; если нужен dW, а W имеет форму [5 × 10], то единственный способ — dD.dot(X.T), как показано выше.

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

Эрик Лёрнд-Миллер (Erik Learned-Miller) написал более подробный документ о взятии производных по матрицам и векторам, который может вам пригодиться: он здесь. Ещё подробнее — в нашем переводе конспекта MIT «Матричное исчисление».

  • Мы выработали интуицию о том, что означают градиенты, как они текут по схеме назад и как сообщают, какая часть схемы должна увеличиться или уменьшиться и с какой силой, чтобы итоговый выход стал больше.
  • Мы обсудили важность поэтапных вычислений для практической реализации обратного распространения. Функцию всегда стоит разбивать на модули, для которых легко вывести локальные градиенты, а затем сцеплять их цепным правилом. Важно, что эти выражения почти никогда не нужно выписывать на бумаге и дифференцировать символьно целиком: явная математическая формула для градиента по входным переменным вам не нужна. Поэтому разложите выражения на этапы так, чтобы каждый этап можно было продифференцировать независимо (этапами будут умножения матрицы на вектор, операции max, суммы и т. д.), а затем проводите обратное распространение через переменные шаг за шагом.

В следующем разделе мы начнём определять нейронные сети, и обратное распространение позволит эффективно вычислять градиент функции потерь по их параметрам. Иначе говоря, теперь мы готовы обучать нейронные сети, и концептуально самая трудная часть курса позади! До свёрточных сетей (перевод конспекта) останется лишь небольшой шаг.

ENRU
backpropagationобратное распространение ошибки
chain rule, multivariable chain ruleцепное правило, цепное правило для функций многих переменных
circuit, gateсхема, вентиль
forward pass, backward passпрямой проход, обратный проход
local gradientлокальный градиент
staged computationпоэтапные вычисления
gradients add up at forksградиенты складываются на развилках
dimension analysisанализ размерностей