Backpropagation 🔄
Если убрать из современного глубокого обучения буквально всё, кроме одного алгоритма, и оставить только его — нейросети всё равно продолжили бы обучаться. Убери любой другой компонент: конкретную архитектуру, конкретную функцию активации, конкретный оптимизатор — и на его место найдётся замена. Но убери backpropagation (обратное распространение ошибки) — и не останется способа обучить сеть с более чем одним слоем весов ни для чего сложнее игрушечной задачи. Это не преувеличение ради красивого вступления: буквально каждая современная нейросеть — от небольшого MLP, который ты собирал в уроках 327–329, до языковых моделей с сотнями миллиардов параметров — обучается одним и тем же алгоритмом, который сегодня ты разберёшь до последней цифры.
В уроке 328 ты научился считать forward pass — прямой проход, вычисление предсказания сети слой за слоем при уже заданных весах. Но сеть со случайными весами предсказывает случайный мусор. Чтобы она хоть чему-то научилась, нужно знать, в какую сторону и насколько сдвинуть каждый вес, чтобы уменьшить ошибку. Формально это означает: нужно вычислить частную производную функции потерь по каждому весу сети — $\partial L/\partial w$ для каждого $w$. Проблема в том, что современная сеть может содержать миллионы, а то и миллиарды весов, и каждый из них влияет на итоговую ошибку не напрямую, а через длинную цепочку промежуточных вычислений — слой за слоем, нейрон за нейроном. Backpropagation — это алгоритм, который вычисляет все эти градиенты сразу, эффективно и точно, опираясь на единственный, давно и хорошо известный инструмент математического анализа — цепное правило дифференцирования (chain rule).
Сегодняшний урок — центральный в этом блоке курса, и не только по расположению, но и по содержанию: здесь сходятся сразу несколько линий, которые ты уже прошёл. Цепное правило дифференцирования из курса математического анализа даёт саму математику алгоритма. Вычислительный граф и топологическая сортировка из урока 262 дают структуру, по которой backpropagation движется, — ты увидишь, что backward pass буквально совпадает с топологической сортировкой графа вычислений с обращёнными рёбрами, о которой шла речь ещё тогда. Градиентный спуск (урок 285) и его усовершенствованная версия Adam (урок 293) — это то, что делает с градиентами после того, как backpropagation их вычислил: они используют эти значения, чтобы обновить веса. Сам backpropagation не решает, куда двигать веса и с каким шагом, — он лишь отвечает на вопрос «насколько сильно и в какую сторону каждый конкретный вес виноват в ошибке», а дальше эту информацию использует оптимизатор.
План урока такой: сначала — цепное правило как фундамент, без которого невозможен ни один шаг алгоритма. Затем — напоминание про forward pass с явным акцентом на то, что нужно сохранять на этом шаге, чтобы backward pass вообще было возможно выполнить. Потом — сердце урока: полный, пошаговый разбор backward pass на конкретной сети $2 \to 2 \to 1$ с настоящими числами, от начала до самого последнего обновления веса. И в конце — разговор об эффективности: почему backpropagation вычисляет градиенты по всем весам сети за время, сравнимое с одним-единственным forward pass, и что случилось бы, если бы вместо него применяли наивный метод конечных разностей.
Современные фреймворки — PyTorch, TensorFlow, JAX — скрывают весь этот механизм за одним вызовом loss.backward(). Возникает соблазн считать, что раз фреймворк всё делает сам, разбираться в деталях не нужно. Это ошибка, причём практическая, а не только академическая: когда обучение модели «зависает» и градиенты становятся близки к нулю (затухающий градиент, vanishing gradient) или, наоборот, взрываются до NaN (взрывающийся градиент, exploding gradient), диагностировать проблему и понять, почему автоматическое дифференцирование выдало именно такие числа, можно только имея в голове точную модель того, что происходит внутри backward(). Этот урок и даёт тебе такую модель — с числами, которые ты сможешь пересчитать вручную и сверить.
История
Идея, лежащая в основе backpropagation — эффективное вычисление производных сложной функции через последовательное применение цепного правила по графу вычислений, — была известна задолго до того, как её начали применять к нейросетям. Математик из теории оптимального управления Генри Кельдыш и, чуть позже и куда более заметно, финский студент Сеппо Линнайнмаа в своей магистерской диссертации 1970 года описали технику, которую сегодня называют обратным накоплением (reverse-mode automatic differentiation) — по сути, математическое ядро backpropagation, представленное как общий численный метод, без всякой привязки к нейросетям. Независимо близкую идею для управления динамическими системами развивал Артур Брайсон в 1960-х. То есть математика была готова примерно на пятнадцать лет раньше своего самого известного применения — история, которая повторяется в науке регулярно: инструмент существует, но ещё не встретил задачу, ради которой он окажется незаменимым.
Применительно к нейросетям идею впервые описал Пол Вербос в своей диссертации 1974 года, а затем к близким результатам независимо пришли Дэвид Паркер и Ян Лекун в 1985 году. Но алгоритм по-настоящему вошёл в общее пользование и получил своё нынешнее имя после статьи 1986 года «Learning representations by back-propagating errors» («Обучение представлений с помощью обратного распространения ошибки»), опубликованной в Nature Дэвидом Румельхартом, Джеффри Хинтоном и Рональдом Уильямсом. Важно понимать точно, в чём была заслуга этой статьи: не в изобретении математики с нуля (она уже существовала), а в том, что авторы ясно, доходчиво и на конкретных примерах показали: этот метод решает ту самую проблему присвоения кредита (credit assignment problem), которая пятнадцать лет держала многослойные сети в тени после книги Минского и Пейперта «Перцептроны» (см. урок 328) — как понять, какой вклад в ошибку сети внёс вес глубоко скрытого слоя, если видно только итоговую ошибку на выходе.
Дальнейшая судьба алгоритма — это, по сути, судьба всего глубокого обучения. В 1989 году Ян Лекун применил backpropagation для обучения свёрточной сети распознаванию рукописных цифр — один из первых практических успехов метода на реальной задаче. Следующие два десятилетия backpropagation оставался стандартным способом обучения нейросетей, но по-настоящему раскрылся только с приходом больших размеченных датасетов и GPU-вычислений в 2010-х: без backpropagation невозможно было бы обучить ни AlexNet в 2012 году, ни трансформеры, лежащие в основе современных языковых моделей. Сегодня backpropagation реализован под капотом как автоматическое дифференцирование (autograd) в PyTorch, tf.GradientTape в TensorFlow и аналогичных механизмах в других фреймворках — один и тот же математический принцип, сформулированный полвека назад, обучает буквально все нейросети, которые ты встречаешь каждый день.
Цепное правило как математическая основа алгоритма
Интуиция
Представь, что итоговая ошибка сети — это результат длинной цепочки вычислений: вход проходит через первый слой, результат — через второй, потом через функцию активации, потом ещё через слой, и в самом конце сравнивается с истинным ответом, давая число-ошибку. Чтобы понять, как изменение одного конкретного веса где-то в середине этой цепочки повлияет на итоговую ошибку, нужно проследить весь путь влияния — вес влияет на промежуточный результат, тот влияет на следующий промежуточный результат, и так далее, вплоть до самой ошибки. Цепное правило дифференцирования — это точный математический рецепт, как перемножить производные на каждом шаге этого пути и получить итоговое влияние.
Ключевая мысль, ради которой стоит удерживать в голове именно эту интуицию: производная сложной функции — это не что-то, что вычисляется заново с нуля для каждого веса. Она собирается из уже посчитанных, более простых кусочков — производных отдельных операций, — перемноженных друг с другом вдоль пути от веса до выхода. Backpropagation — это систематический, организованный способ применить именно эту идею к каждому весу сети сразу, не считая одно и то же дважды.
Формула
Цепное правило (chain rule). Если $y = f(u)$ и $u = g(x)$, то
$$\frac{dy}{dx} = \frac{dy}{du}\cdot\frac{du}{dx}$$Для функции, зависящей от переменной через несколько промежуточных звеньев $x \to u_1 \to u_2 \to \cdots \to u_k \to y$, производная — это произведение производных на каждом звене цепочки:
$$\frac{dy}{dx} = \frac{dy}{du_k}\cdot\frac{du_k}{du_{k-1}}\cdots\frac{du_2}{du_1}\cdot\frac{du_1}{dx}$$Если промежуточная переменная влияет на $y$ несколькими путями одновременно (граф вычислений ветвится), вклады по каждому пути складываются:
$$\frac{\partial y}{\partial x} = \sum_{\text{пути } p} \left(\prod_{\text{звенья на пути }p} \frac{\partial(\cdot)}{\partial(\cdot)}\right)$$
Разбор примеров
Пример 1 (простая цепочка из трёх звеньев). Пусть $u = 2x+1$, $v = u^2$, $y = 3v$. Хочешь найти $\dfrac{dy}{dx}$ при $x=2$. Прямая подстановка: $u=2\cdot2+1=5$, $v=5^2=25$, $y=3\cdot25=75$ — но это значение функции, не производная. По цепному правилу: $\dfrac{dy}{dv}=3$, $\dfrac{dv}{du}=2u=2\cdot5=10$, $\dfrac{du}{dx}=2$. Перемножаем: $\dfrac{dy}{dx}=3\cdot10\cdot2=60$. Проверка прямым способом: подставив $u=2x+1$ и $v=(2x+1)^2$, получаем $y=3(2x+1)^2$, а её производная $y'=3\cdot2(2x+1)\cdot2=12(2x+1)$; при $x=2$: $12\cdot5=60$ — числа совпали, цепное правило дало тот же результат без раскрытия всей формулы целиком.
Пример 2 (ветвящийся граф — переменная влияет на результат двумя путями). Пусть $x$ входит в выражение $y = x^2 + 3x$ дважды: как $u=x^2$ и как $v=3x$, а $y=u+v$. Наивно кажется, что нужно выбрать «один путь», но правильный подход — просуммировать вклад по обоим путям: $\dfrac{\partial y}{\partial u}=1$, $\dfrac{\partial u}{\partial x}=2x$, вклад первого пути $=2x$; $\dfrac{\partial y}{\partial v}=1$, $\dfrac{\partial v}{\partial x}=3$, вклад второго пути $=3$. Итог: $\dfrac{dy}{dx}=2x+3$. При $x=4$: $2\cdot4+3=11$. Проверка прямым дифференцированием $y=x^2+3x$: $y'=2x+3$, при $x=4$ тоже $11$. Этот пример — не абстрактная придирка: в нейросети практически любой вес входит в вычисление итоговой ошибки через несколько путей одновременно (через разные нейроны следующего слоя, которые он «питает»), и правило суммирования вкладов по путям — это ровно то, что заставляет backpropagation в сетях с ветвлением работать корректно.
Пример 3 (цепочка из пяти звеньев — предвестник настоящей сети). Пусть $z_1=w\cdot x$ (вес умножает вход), $z_2=z_1+b$ (прибавляется смещение), $a=\sigma(z_2)$ (сигмоида), $L=(a-y_{\text{true}})^2$ (квадратичная ошибка). Даны числа: $w=0{,}5$, $x=2$, $b=0{,}1$, $y_{\text{true}}=1$. Считаем вперёд: $z_1=0{,}5\cdot2=1{,}0$; $z_2=1{,}0+0{,}1=1{,}1$; $a=\sigma(1{,}1)=\dfrac{1}{1+e^{-1{,}1}}\approx0{,}7503$; $L=(0{,}7503-1)^2\approx0{,}0624$. Теперь производная $\dfrac{\partial L}{\partial w}$ по цепному правилу: $\dfrac{\partial L}{\partial a}=2(a-y_{\text{true}})=2(0{,}7503-1)\approx-0{,}4994$; $\dfrac{\partial a}{\partial z_2}=\sigma(z_2)(1-\sigma(z_2))=0{,}7503\cdot0{,}2497\approx0{,}1873$; $\dfrac{\partial z_2}{\partial z_1}=1$ (сложение с константой); $\dfrac{\partial z_1}{\partial w}=x=2$. Перемножаем всю цепочку: $\dfrac{\partial L}{\partial w}=(-0{,}4994)\cdot0{,}1873\cdot1\cdot2\approx-0{,}1870$. Это ровно тот же самый расчёт, который в следующем разделе будет выполняться для целой сети, только здесь — один вес и одна прямая цепочка без ветвлений.
Почему это важно
Каждое из трёх звеньев цепного правила, разобранных выше, — умножение производных по цепочке, суммирование вкладов по нескольким путям и применение этого правила к конкретным числовым значениям, полученным на forward pass, — это, буквально без единого дополнительного допущения, весь математический аппарат, который потребуется для backward pass ниже. Backpropagation не изобретает новую математику специально для нейросетей — он берёт цепное правило, известное из первого курса математического анализа, и применяет его организованно, слой за слоем, ко всем весам сети сразу, используя граф вычислений как карту, по которой видно, какие производные перемножать и какие — складывать.
Forward pass: что вычислить и что обязательно сохранить
Интуиция
В уроке 328 ты уже считал forward pass — последовательное вычисление $z^{(l)}=W^{(l)}a^{(l-1)}+b^{(l)}$, $a^{(l)}=f(z^{(l)})$ слой за слоем, вплоть до предсказания $\hat y = a^{(L)}$. Тогда единственной целью было получить итоговое число — предсказание. Сейчас цель шире: тот же самый forward pass нужно выполнить ещё раз, но с одним принципиальным добавлением — каждое промежуточное значение $z^{(l)}$ и $a^{(l)}$ на каждом слое нужно сохранить в памяти, а не выбросить сразу после использования. Причина станет ясна в следующем разделе: чтобы вычислить производную по весу конкретного слоя, цепному правилу нужны конкретные числовые значения активаций и предактиваций именно этого и соседних слоёв — без них перемножить цепочку производных попросту не из чего.
Формула
Forward pass с сохранением промежуточных значений. Для сети из $L$ слоёв, вход $a^{(0)}=x$. Для каждого слоя $l=1,\dots,L$:
$$z^{(l)} = W^{(l)}a^{(l-1)} + b^{(l)}, \qquad a^{(l)} = f^{(l)}\bigl(z^{(l)}\bigr)$$Все значения $z^{(l)}$ и $a^{(l)}$ для $l=0,\dots,L$ сохраняются (кэшируются). После вычисления предсказания $\hat y = a^{(L)}$ и известного истинного ответа $y$ считается функция потерь $L = \mathcal{L}(\hat y, y)$.
Разбор примеров
Пример 1 (какие именно значения понадобятся backward pass — предметно, по формуле производной). Возьмём один слой $z=Wa_{\text{prev}}+b$, $a=f(z)$. Производная функции потерь по весу этого слоя раскладывается по цепному правилу как $\dfrac{\partial L}{\partial W}=\dfrac{\partial L}{\partial a}\cdot f'(z)\cdot a_{\text{prev}}^\top$. В этом выражении фигурируют сразу два сохранённых на forward pass значения: $z$ (нужен, чтобы вычислить $f'(z)$ — например, для ReLU нужно знать, был ли вход положительным) и $a_{\text{prev}}$ (нужен как множитель напрямую). Ни то, ни другое нельзя восстановить постфактум, если значения не были сохранены на forward pass, — их пришлось бы вычислять заново, что означало бы повторный (и от того более медленный) проход по всей сети.
Пример 2 (что случится, если не сохранить активации — на конкретных числах). Пусть на forward pass слой вычислил $z=-0{,}5$ и после ReLU получил $a=0$. Если при backward pass сохранено только итоговое значение $a=0$, а значение $z=-0{,}5$ выброшено, узнать производную ReLU в этой точке становится невозможно напрямую: смотря лишь на $a=0$, нельзя отличить случай «$z$ было отрицательным» (производная $0$) от гипотетического случая обнуления по другой причине. Именно поэтому фреймворки автоматического дифференцирования сохраняют не только выход операции, но и её вход (или производную, вычисленную сразу на месте) — в PyTorch это происходит автоматически при построении графа вычислений с requires_grad=True, и именно из-за этого требования по памяти обучение сети всегда расходует существенно больше памяти, чем один только инференс (чистый forward pass без сохранения графа).
Пример 3 (числовой forward pass целой мини-сети, который будет использован дальше). Ниже — конкретная сеть $2\to2\to1$, полный forward pass которой станет отправной точкой для главного разбора этого урока (backward pass в следующем разделе). Веса: скрытый слой $W^{(1)}=\begin{pmatrix}0{,}15&0{,}20\\0{,}25&0{,}30\end{pmatrix}$, $b^{(1)}=(0{,}35;\ 0{,}35)$, активация — сигмоида; выходной слой $W^{(2)}=(0{,}40,\ 0{,}45)$, $b^{(2)}=0{,}60$, активация — сигмоида. Вход $x=(0{,}05;\ 0{,}10)$, истинный ответ $y_{\text{true}}=0{,}01$.
Скрытый слой: $z^{(1)}_1=0{,}15\cdot0{,}05+0{,}20\cdot0{,}10+0{,}35=0{,}0075+0{,}02+0{,}35=0{,}3775$; $z^{(1)}_2=0{,}25\cdot0{,}05+0{,}30\cdot0{,}10+0{,}35=0{,}0125+0{,}03+0{,}35=0{,}3925$. После сигмоиды: $a^{(1)}_1=\sigma(0{,}3775)\approx0{,}5933$; $a^{(1)}_2=\sigma(0{,}3925)\approx0{,}5969$.
Выходной слой: $z^{(2)}=0{,}40\cdot0{,}5933+0{,}45\cdot0{,}5969+0{,}60=0{,}2373+0{,}2686+0{,}60=1{,}1059$. После сигмоиды: $\hat y=\sigma(1{,}1059)\approx0{,}7513$.
Все пять сохранённых значений — $z^{(1)}_1, z^{(1)}_2, a^{(1)}_1, a^{(1)}_2, z^{(2)}$ — понадобятся уже в следующем разделе для точного, пошагового backward pass по этой же самой сети.
Почему это важно
Forward pass с сохранением значений — не дополнительная опция, а строго обязательный первый шаг перед backward pass: без него цепному правилу физически не из чего собирать произведение производных. Это же объясняет практический факт, с которым сталкивается почти каждый, кто обучает большие модели: потребление памяти при обучении значительно превышает потребление памяти при одном только инференсе — потому что при обучении фреймворк обязан держать в памяти весь граф вычислений с промежуточными активациями каждого слоя, вплоть до момента, когда backward pass через них пройдёт и они больше не понадобятся. Именно поэтому техника gradient checkpointing (сохранение не всех, а лишь части активаций, с пересчётом недостающих заново во время backward pass) стала стандартным приёмом для обучения по-настоящему больших сетей — прямой компромисс между памятью и вычислениями, выросший ровно из требования, разобранного в этом разделе.
Backward pass: вычисление градиента по каждому весу
Интуиция
Вот главная идея всего алгоритма. Backward pass начинается там, где forward pass закончился, — с функции потерь $L$ — и двигается в обратном направлении, слой за слоем, к самому началу сети. На каждом шаге он вычисляет один и тот же тип величины: насколько изменение значения в данном узле графа вычислений (некоторого $z$ или $a$) повлияет на итоговую ошибку $L$. Эта величина называется градиентом (или «сигналом ошибки») этого узла. Как только градиент узла известен, вычислить градиент по весам, которые в этот узел входят, — уже совсем короткий, последний шаг по цепному правилу.
Ключевое наблюдение, которое делает алгоритм эффективным: градиент выходного слоя вычисляется первым и используется как готовый строительный блок для вычисления градиента предыдущего слоя — ничего не пересчитывается с нуля. Информация буквально «течёт» назад по тем же связям сети, по которым на forward pass она текла вперёд, только в обратную сторону и в виде не значений активаций, а значений градиентов.
Формула
Backward pass. Обозначим $\delta^{(l)} = \dfrac{\partial L}{\partial z^{(l)}}$ — градиент функции потерь по предактивации слоя $l$ («сигнал ошибки» слоя $l$). Для выходного слоя $L$:
$$\delta^{(L)} = \frac{\partial \mathcal{L}}{\partial a^{(L)}} \odot f'^{(L)}\bigl(z^{(L)}\bigr)$$Для каждого предыдущего слоя $l = L-1, \dots, 1$, двигаясь строго в обратном порядке:
$$\delta^{(l)} = \Bigl(\bigl(W^{(l+1)}\bigr)^\top \delta^{(l+1)}\Bigr) \odot f'^{(l)}\bigl(z^{(l)}\bigr)$$И, наконец, градиенты по весам и смещениям каждого слоя:
$$\frac{\partial L}{\partial W^{(l)}} = \delta^{(l)} \bigl(a^{(l-1)}\bigr)^\top, \qquad \frac{\partial L}{\partial b^{(l)}} = \delta^{(l)}$$где $\odot$ — поэлементное умножение (произведение Адамара).
Разбор примеров
Пример 1 (полная пошаговая трассировка backward pass на сети $2\to2\to1$ из предыдущего раздела). Продолжаем ровно ту сеть и те сохранённые значения из примера 3 предыдущего раздела: $z^{(1)}_1=0{,}3775$, $z^{(1)}_2=0{,}3925$, $a^{(1)}_1\approx0{,}5933$, $a^{(1)}_2\approx0{,}5969$, $z^{(2)}=1{,}1059$, $\hat y\approx0{,}7513$, $y_{\text{true}}=0{,}01$. Функция потерь — квадратичная ошибка $L=\dfrac{1}{2}(\hat y-y_{\text{true}})^2$ (множитель $\tfrac12$ берётся для удобства — он ровно сокращает двойку, возникающую при дифференцировании квадрата).
Шаг 1. Градиент на выходе. $\dfrac{\partial L}{\partial \hat y}=\hat y-y_{\text{true}}=0{,}7513-0{,}01=0{,}7413$.
Шаг 2. Сигнал ошибки выходного слоя $\delta^{(2)}$. Производная сигмоиды: $\sigma'(z^{(2)})=\hat y(1-\hat y)=0{,}7513\cdot(1-0{,}7513)=0{,}7513\cdot0{,}2487\approx0{,}1868$. Значит $\delta^{(2)}=0{,}7413\cdot0{,}1868\approx0{,}1385$.
Шаг 3. Градиенты по весам выходного слоя. $\dfrac{\partial L}{\partial W^{(2)}_1}=\delta^{(2)}\cdot a^{(1)}_1=0{,}1385\cdot0{,}5933\approx0{,}0822$; $\dfrac{\partial L}{\partial W^{(2)}_2}=\delta^{(2)}\cdot a^{(1)}_2=0{,}1385\cdot0{,}5969\approx0{,}0827$; $\dfrac{\partial L}{\partial b^{(2)}}=\delta^{(2)}\approx0{,}1385$.
Шаг 4. Пробрасываем ошибку назад в скрытый слой. Каждый скрытый нейрон получает свою долю сигнала ошибки, пропорционально весу, которым он соединён с выходом: вклад в нейрон 1 — $\delta^{(2)}\cdot W^{(2)}_1=0{,}1385\cdot0{,}40\approx0{,}0554$; вклад в нейрон 2 — $\delta^{(2)}\cdot W^{(2)}_2=0{,}1385\cdot0{,}45\approx0{,}0623$. Поскольку выходной слой всего один нейрон, суммировать по нескольким путям здесь не нужно (это понадобится в сетях с более широким следующим слоем) — но сама операция «умножить на транспонированную матрицу весов следующего слоя» это суммирование делает автоматически в общем случае.
Шаг 5. Сигналы ошибки скрытого слоя $\delta^{(1)}$. Производные сигмоиды скрытого слоя: $\sigma'(z^{(1)}_1)=a^{(1)}_1(1-a^{(1)}_1)=0{,}5933\cdot0{,}4067\approx0{,}2413$; $\sigma'(z^{(1)}_2)=a^{(1)}_2(1-a^{(1)}_2)=0{,}5969\cdot0{,}4031\approx0{,}2406$. Значит $\delta^{(1)}_1=0{,}0554\cdot0{,}2413\approx0{,}01337$; $\delta^{(1)}_2=0{,}0623\cdot0{,}2406\approx0{,}01499$.
Шаг 6. Градиенты по весам скрытого слоя. Вход сети $x=(0{,}05;\ 0{,}10)$ играет роль $a^{(0)}$. $\dfrac{\partial L}{\partial W^{(1)}_{11}}=\delta^{(1)}_1\cdot x_1=0{,}01337\cdot0{,}05\approx0{,}000669$; $\dfrac{\partial L}{\partial W^{(1)}_{12}}=\delta^{(1)}_1\cdot x_2=0{,}01337\cdot0{,}10\approx0{,}001337$; $\dfrac{\partial L}{\partial W^{(1)}_{21}}=\delta^{(1)}_2\cdot x_1=0{,}01499\cdot0{,}05\approx0{,}000750$; $\dfrac{\partial L}{\partial W^{(1)}_{22}}=\delta^{(1)}_2\cdot x_2=0{,}01499\cdot0{,}10\approx0{,}001499$; $\dfrac{\partial L}{\partial b^{(1)}_1}=\delta^{(1)}_1\approx0{,}01337$; $\dfrac{\partial L}{\partial b^{(1)}_2}=\delta^{(1)}_2\approx0{,}01499$.
Обрати внимание: чтобы получить эти шесть чисел для скрытого слоя, не пришлось заново вычислять ни одной производной, которая уже была посчитана на шагах 1–4, — весь backward pass строится строго на переиспользовании уже готовых промежуточных градиентов $\delta^{(2)}$, а затем $\delta^{(1)}$, именно в этом и состоит эффективность алгоритма, к которой мы вернёмся в следующем разделе.
Пример 2 (обновление весов одним шагом градиентного спуска). Используя градиенты, посчитанные в примере 1, применим одно обновление с шагом обучения (learning rate) $\eta=0{,}5$ по правилу $w \leftarrow w - \eta\cdot\dfrac{\partial L}{\partial w}$ из урока 285. Для веса $W^{(2)}_1$: $0{,}40-0{,}5\cdot0{,}0822=0{,}40-0{,}0411=0{,}3589$. Для веса $W^{(2)}_2$: $0{,}45-0{,}5\cdot0{,}0827=0{,}45-0{,}0414=0{,}4087$. Для веса $W^{(1)}_{11}$: $0{,}15-0{,}5\cdot0{,}000669\approx0{,}1497$. Обрати внимание на масштаб изменения: вес выходного слоя, стоящий ближе к ошибке, сдвинулся заметно (на сотые доли), а вес скрытого слоя, стоящий на два звена дальше от ошибки, — совсем незначительно (на тысячные доли), поскольку его градиент оказался в цепочке умножений на несколько чисел меньше единицы (производные сигмоид). Это конкретное, посчитанное руками проявление той самой проблемы затухающего градиента, о которой пойдёт речь в разделе про частые ошибки: чем дальше слой от выхода, тем меньше типичный масштаб его градиента при сигмоидных активациях.
Пример 3 (проверка через численный градиент — конечные разности). Убедимся, что аналитический градиент из примера 1 верен, сравнив его с приближённым, посчитанным напрямую через определение производной: $\dfrac{\partial L}{\partial w}\approx\dfrac{L(w+\varepsilon)-L(w-\varepsilon)}{2\varepsilon}$. Возьмём вес $W^{(2)}_1=0{,}40$ и малое $\varepsilon=0{,}0001$. При $W^{(2)}_1=0{,}4001$: пересчитывая только выходной слой (скрытые активации не меняются), $z^{(2)}_+=0{,}4001\cdot0{,}5933+0{,}45\cdot0{,}5969+0{,}60\approx1{,}10596$, $\hat y_+=\sigma(1{,}10596)\approx0{,}75131$, $L_+=\tfrac12(0{,}75131-0{,}01)^2\approx0{,}27470$. При $W^{(2)}_1=0{,}3999$: аналогично $z^{(2)}_-\approx1{,}10584$, $\hat y_-\approx0{,}75128$, $L_-\approx0{,}27466$. Численная оценка: $\dfrac{0{,}27470-0{,}27466}{2\cdot0{,}0001}=\dfrac{0{,}00004}{0{,}0002}\approx0{,}0822$ — с точностью до округления это ровно значение $\dfrac{\partial L}{\partial W^{(2)}_1}\approx0{,}0822$, полученное аналитически через backpropagation в примере 1. Совпадение не случайное — это стандартный способ проверки правильности реализации backpropagation, называемый gradient checking, и почти каждая учебная реализация автоматического дифференцирования тестируется именно так.
Почему это важно
Пример 1 — это не абстрактная демонстрация формул, а полный, воспроизводимый рецепт: имея любую сеть, набор весов, forward pass и функцию потерь, ты теперь можешь вручную получить точный градиент по каждому весу, слой за слоем, начиная с выхода. Это ровно то, что происходит внутри loss.backward() в PyTorch — с той разницей, что фреймворк делает это автоматически, для произвольного графа вычислений, а не только для последовательности полносвязных слоёв. Пример 3, показавший совпадение аналитического и численного градиента, — это не просто красивая проверка: она демонстрирует, что backpropagation вычисляет точное значение производной (в пределах точности арифметики с плавающей точкой), а не приближение, — что принципиально отличает его от метода конечных разностей, разбираемого в следующем разделе.
Вычислительный граф и связь с топологической сортировкой
Интуиция
Сеть $2\to2\to1$ из предыдущего раздела удобно представить в виде вычислительного графа — точно того же понятия, что было введено в уроке 262: узлы — это операции (умножение на вес, суммирование, применение сигмоиды), рёбра — потоки данных между ними. Урок 262 уже показал главный факт, который сегодня получает конкретное числовое подтверждение: backward pass — это буквально топологическая сортировка того же графа, но с обращёнными рёбрами, начиная с узла-выхода (где входящая степень в обращённом графе равна нулю — градиент по себе самому равен единице) и двигаясь туда, где входящая степень обнуляется по мере того, как приходят градиенты от всех «потребителей» узла.
Формула
Backward pass как топологическая сортировка обращённого графа (урок 262). Пусть вычислительный граф сети — DAG с узлами-операциями. Обращаем каждое ребро $u\to v$ в $v\to u$. Алгоритм Кана на обращённом графе: узел-выход $L$ имеет входящую степень $0$ и обрабатывается первым ($\partial L/\partial L=1$); каждый следующий узел $u$ обрабатывается только тогда, когда получены и просуммированы градиенты от всех узлов $v$, которые использовали $u$ как вход (то есть от всех рёбер $u\to v$ исходного графа) — это условие математически и есть правило суммирования вкладов по путям из раздела про цепное правило.
Разбор примеров
Пример 1 (граф сети $2\to2\to1$ и обращение его рёбер). Узлы графа для нашей сети: входы $x_1, x_2$; предактивации $z^{(1)}_1, z^{(1)}_2$; активации $a^{(1)}_1, a^{(1)}_2$; предактивация выхода $z^{(2)}$; выход $\hat y$; и, наконец, $L$. Исходные рёбра (forward): $x_1,x_2 \to z^{(1)}_1$ и $x_1,x_2 \to z^{(1)}_2$ (через веса $W^{(1)}$), $z^{(1)}_1\to a^{(1)}_1$, $z^{(1)}_2\to a^{(1)}_2$ (через сигмоиду), $a^{(1)}_1, a^{(1)}_2 \to z^{(2)}$ (через веса $W^{(2)}$), $z^{(2)}\to\hat y$ (через сигмоиду), $\hat y\to L$. Обращаем всё: $L\to\hat y\to z^{(2)}\to \{a^{(1)}_1, a^{(1)}_2\} \to \{z^{(1)}_1, z^{(1)}_2\} \to \{x_1,x_2\}$. Входящие степени в обращённом графе: $L$ — $0$ (обрабатывается первым, шаг 1 из предыдущего раздела); $\hat y$ — $1$; $z^{(2)}$ — $1$; $a^{(1)}_1$ и $a^{(1)}_2$ — по $1$ каждый (оба получают градиент только от $z^{(2)}$, поскольку в этой сети выход всего один); $z^{(1)}_1$ и $z^{(1)}_2$ — по $1$; $x_1$ и $x_2$ — не нужны для backward pass по весам (входы не обучаются), но формально получили бы степень $2$ каждый, поскольку оба используются и в $z^{(1)}_1$, и в $z^{(1)}_2$.
Пример 2 (где в этой конкретной сети возникло бы суммирование вкладов). Если бы скрытых нейронов было больше одного, а выход зависел от обоих через отдельные веса, — именно так и есть в нашей сети! — узел $a^{(1)}_1$ является входом только для $z^{(2)}$ (единственный узел, куда он ведёт в этом примере), поэтому суммирования по нескольким путям на этом конкретном шаге не требуется. Но если бы сеть содержала второй выходной нейрон, использующий тот же $a^{(1)}_1$, входящая степень узла $a^{(1)}_1$ в обращённом графе стала бы $2$, и алгоритм Кана потребовал бы дождаться градиентов от обоих выходных узлов, прежде чем обрабатывать $a^{(1)}_1$, — складывая их, в точности как того требует правило суммирования вкладов по путям из раздела про цепное правило. Именно такая ситуация — один скрытый нейрон, влияющий на несколько узлов следующего слоя, — типична для реальных сетей и объясняет, почему выражение $\bigl(W^{(l+1)}\bigr)^\top \delta^{(l+1)}$ в формуле backward pass — это в точности матричная запись суммирования вкладов от всех потребителей узла одновременно.
Пример 3 (почему обход «в правильном порядке» вообще необходим, а не просто удобен). Представь, что backward pass попытался бы вычислить градиент по $W^{(1)}$ (скрытый слой) раньше, чем полностью вычислен $\delta^{(2)}$ (сигнал ошибки выходного слоя). Формула шага 5 предыдущего раздела прямо использует $\delta^{(2)}$ как множитель — без него значение $\delta^{(1)}$ вычислить попросту не из чего, потому что оно определено именно как что-то, что «приходит» от следующего слоя. Это ровно то же ограничение, что в уроке 262 не позволяло вычислить узел mul раньше узлов add и c при forward pass, — только здесь, в обращённом графе, то же самое ограничение звучит как «нельзя вычислить градиент узла, пока не пришли градиенты от всех узлов, которые его использовали». Backward pass, нарушивший этот порядок, вычислил бы неверные, преждевременные числа — точно так же, как топологическая сортировка, нарушившая зависимости графа, выдала бы бессмысленный порядок выполнения.
Почему это важно
Связь с уроком 262 — не красивая аналогия, а буквальное техническое описание того, как работает autograd в PyTorch и аналогичные механизмы в других фреймворках: при построении графа вычислений (динамически, операция за операцией, по мере выполнения кода в PyTorch, или статически, до запуска, в старом TensorFlow 1.x) фреймворк отслеживает зависимости между узлами, а при вызове .backward() обходит этот граф в порядке, гарантирующем, что градиент каждого узла считается только после того, как готовы градиенты всех узлов, которые его использовали, — это и есть топологическая сортировка обращённого графа, выполняемая автоматически и для сетей произвольной, сколь угодно ветвящейся архитектуры, а не только для аккуратной последовательности полносвязных слоёв, как в нашем ручном примере.
Эффективность backpropagation: почему это не наивное вычисление градиента
Интуиция
Есть куда более прямолинейный, «наивный» способ оценить, как функция потерь зависит от конкретного веса, — метод конечных разностей, использованный выше исключительно для проверки: слегка изменить один вес, пересчитать весь forward pass заново, посмотреть, насколько изменилась ошибка, и поделить изменение ошибки на изменение веса. Этот способ работает и даже даёт правильный (приближённый) ответ, как показал пример 3 предыдущего раздела. Проблема в другом: чтобы получить градиент по всем весам сети таким способом, нужно повторить полный forward pass отдельно для каждого веса — а в сети с миллионом весов это означает миллион полных прогонов сети только для того, чтобы сделать один-единственный шаг обучения. Backpropagation решает ровно ту же задачу за один forward pass и один backward pass, вычисляя градиенты по всем весам одновременно, — именно эта разница в эффективности делает возможным обучение сетей с миллиардами параметров в принципе.
Формула
Сравнение вычислительной стоимости. Пусть сеть содержит $P$ обучаемых весов, а один forward pass стоит $O(C)$ операций. Метод конечных разностей требует отдельного forward pass на каждый вес: полная стоимость вычисления градиента по всем весам — $O(P \cdot C)$. Backpropagation требует один forward pass и один backward pass, стоимость которого того же порядка, что и forward pass: полная стоимость — $O(C)$, то есть не зависит от числа весов $P$ явно (оно уже учтено внутри $C$ как объём вычислений одного прохода).
Разбор примеров
Пример 1 (счёт на сети $2\to2\to1$ — разница в разы уже на игрушечном примере). В сети из основного примера этого урока — $6$ весов и $3$ смещения в скрытом слое ($2\times2$ весов плюс $2$ смещения) и $2$ веса и $1$ смещение в выходном — итого $9$ обучаемых параметров. Один forward pass для этой сети требует порядка $4$ умножений с накоплением (MAC) на скрытый слой плюс $2$ MAC на выходной, то есть около $6$ операций MAC плюс несколько вычислений сигмоиды. Backpropagation вычисляет градиенты по всем $9$ параметрам за один backward pass — по стоимости сравнимый с этими же $6$ операциями (плюс производные сигмоид, уже вычисленные на forward pass). Конечные разности потребовали бы отдельного forward pass на каждый из $9$ параметров — в девять раз больше вычислений уже на этой крошечной сети, при том что каждое такое вычисление ещё и приближённое (зависит от выбора $\varepsilon$), а не точное, как у backpropagation.
Пример 2 (масштабирование на реалистичную сеть). Возьмём сеть для MNIST архитектуры $784\to256\to10$ (пример из урока 328) с $203\,530$ параметрами. Один forward pass стоит порядка $203\,530$ операций MAC (число операций умножения-накопления в полносвязном слое примерно равно числу весов). Backpropagation вычисляет градиенты по всем $203\,530$ параметрам за backward pass, по стоимости сравнимый с этим же одним forward pass, — то есть весь шаг обучения (forward плюс backward) стоит порядка $400\,000$–$600\,000$ операций. Конечными разностями пришлось бы выполнить $203\,530$ отдельных forward pass — то есть порядка $203\,530 \times 203\,530 \approx 4{,}1\times10^{10}$ операций, что примерно в $100\,000$ раз дороже одного шага backpropagation. Для сети таких скромных размеров это уже разница между шагом обучения, занимающим доли секунды, и шагом, который занимал бы часы.
Пример 3 (почему это критично для современных больших моделей). Языковая модель с $70$ миллиардами параметров (типичный порядок величины для современных больших языковых моделей) выполняет один forward pass за время, пропорциональное числу параметров, — это уже само по себе дорого, но выполнимо на современных GPU-кластерах за разумное время. Backpropagation вычисляет градиенты по всем $70$ миллиардам весов за время того же порядка. Метод конечных разностей потребовал бы $70$ миллиардов отдельных forward pass только для одного шага обучения — при том, что реальное обучение таких моделей включает сотни тысяч и миллионы таких шагов. Оценка по порядку величины: даже если один forward pass занимает одну секунду на всём кластере, конечные разности растянули бы один-единственный шаг обучения на тысячи лет. Backpropagation — не просто более удобный способ обучения таких моделей, а единственный практически осуществимый способ в принципе; без алгоритма, вычисляющего все градиенты за время одного прохода, современные большие модели не существовали бы физически, независимо от объёма доступных вычислительных мощностей.
Почему это важно
Разница между $O(C)$ и $O(P \cdot C)$ — это не количественная деталь, а качественный порог возможности: при миллионах и миллиардах параметров конечные разности переходят из категории «медленно» в категорию «физически неосуществимо в разумное время ни на каком существующем оборудовании». Это объясняет, почему backpropagation — не одна из многих технических опций, а единственный алгоритм, лежащий в основе всего современного глубокого обучения: любая альтернатива, требующая отдельного прохода по сети на каждый параметр, немедленно упирается в эту экспоненциальную по практическому эффекту разницу в стоимости, как только модель выходит за пределы игрушечного размера.
Практика: 30 заданий
Базовые задания (1–10)
Задание 1: По цепному правилу найди $\dfrac{dy}{dx}$, если $u=3x$, $y=u^2$, при $x=2$.
Задание 2: Дано $z=w\cdot x+b$, $w=2$, $x=3$, $b=1$. Найди $\dfrac{\partial z}{\partial w}$ и $\dfrac{\partial z}{\partial b}$.
Задание 3: Для сигмоиды $\sigma(z)=\dfrac{1}{1+e^{-z}}$ найди $\sigma'(0)$, используя формулу $\sigma'(z)=\sigma(z)(1-\sigma(z))$.
Задание 4 (машинное обучение): Зачем при forward pass нужно сохранять промежуточные значения $z^{(l)}$ и $a^{(l)}$, а не только итоговое предсказание $\hat y$?
Задание 5: Дано $L=(a-y)^2$, $a=0{,}8$, $y=0{,}3$. Найди $\dfrac{\partial L}{\partial a}$.
Задание 6: Нейрон: $z=w\cdot x+b$, $a=\sigma(z)$, $w=1$, $x=0{,}5$, $b=0$. Найди $z$, $a$ и $\sigma'(z)$.
Задание 7 (машинное обучение): Что вычисляет $\delta^{(l)}$ в формулах backward pass и почему он называется «сигналом ошибки»?
Задание 8: Используя цепное правило, найди $\dfrac{dy}{dx}$ для $y=\sigma(2x+1)$ при $x=0$ (используй $\sigma'(z)=\sigma(z)(1-\sigma(z))$).
Задание 9: Дано $\delta^{(2)}=0{,}2$, $a^{(1)}=0{,}6$ (выход предыдущего слоя). Найди $\dfrac{\partial L}{\partial W^{(2)}}$ по формуле $\dfrac{\partial L}{\partial W^{(l)}}=\delta^{(l)}\cdot a^{(l-1)}$.
Задание 10 (машинное обучение): Почему backward pass обязан двигаться от выходного слоя к входному, а не наоборот?
Средние задания (11–20)
Задание 11: Сеть: $z_1=w_1\cdot x$, $a_1=\mathrm{ReLU}(z_1)$, $z_2=w_2\cdot a_1$, $L=(z_2-y)^2$. Даны $w_1=2$, $x=1{,}5$, $w_2=0{,}5$, $y=2$. Выполни forward pass: найди $z_1$, $a_1$, $z_2$, $L$.
Задание 12: Для сети из задания 11 выполни backward pass: найди $\dfrac{\partial L}{\partial z_2}$, $\dfrac{\partial L}{\partial w_2}$.
Задание 13: Продолжи backward pass для сети из заданий 11–12: найди $\dfrac{\partial L}{\partial a_1}$, $\dfrac{\partial L}{\partial z_1}$ (учти, что $\mathrm{ReLU}'(z_1)=1$, поскольку $z_1=3>0$), и $\dfrac{\partial L}{\partial w_1}$.
Задание 14: Используя градиенты из заданий 12–13, обнови $w_1$ и $w_2$ одним шагом градиентного спуска с $\eta=0{,}1$.
Задание 15 (машинное обучение): В сети из заданий 11–13 представь, что $z_1$ на forward pass оказался равен $-1$ (отрицательным) вместо $3$. Как это изменит $\dfrac{\partial L}{\partial w_1}$, не пересчитывая всё заново?
Задание 16: Дан узел вычислительного графа, в который входят два ребра от узлов $p$ и $q$ с градиентами $\partial L/\partial p=0{,}3$ и $\partial L/\partial q=0{,}5$, оба полученных через общий узел-родитель $r$ с весами связи $1$ и $1$ соответственно. Как правильно посчитать $\partial L/\partial r$?
Задание 17: Для сети $2\to2\to1$ из основного примера урока (веса $W^{(1)}=\begin{pmatrix}0{,}15&0{,}20\\0{,}25&0{,}30\end{pmatrix}$, $b^{(1)}=(0{,}35;0{,}35)$, $W^{(2)}=(0{,}40,0{,}45)$, $b^{(2)}=0{,}60$) при новом входе $x=(1{,}0;\ 0{,}5)$ выполни только forward pass: найди $z^{(1)}_1$, $z^{(1)}_2$.
Задание 18: Продолжи задание 17: найди $a^{(1)}_1$, $a^{(1)}_2$ (сигмоида) и $z^{(2)}$.
Задание 19: Продолжи задания 17–18: найди $\hat y=\sigma(z^{(2)})$ и, если $y_{\text{true}}=1$, значение $L=\tfrac12(\hat y-y_{\text{true}})^2$.
Задание 20 (машинное обучение): Продолжи задания 17–19: найди $\delta^{(2)}$ и градиент $\dfrac{\partial L}{\partial W^{(2)}_1}$.
Продвинутые задания (21–30)
Задание 21: Для сети из заданий 17–20 найди $\dfrac{\partial L}{\partial W^{(2)}_2}$ и $\dfrac{\partial L}{\partial b^{(2)}}$.
Задание 22: Продолжи расчёт для сети из заданий 17–21: найди сигналы ошибки скрытого слоя $\delta^{(1)}_1$ и $\delta^{(1)}_2$ (используй $\sigma'(z^{(1)}_1)=a^{(1)}_1(1-a^{(1)}_1)$, $\sigma'(z^{(1)}_2)=a^{(1)}_2(1-a^{(1)}_2)$).
Задание 23: Используя $\delta^{(1)}_1$ и $\delta^{(1)}_2$ из задания 22 и вход $x=(1{,}0;\ 0{,}5)$, найди $\dfrac{\partial L}{\partial W^{(1)}_{11}}$ и $\dfrac{\partial L}{\partial W^{(1)}_{22}}$.
Задание 24 (машинное обучение): Сравни порядок величины градиентов выходного слоя ($\approx0{,}03$–$0{,}04$ из заданий 20–21) и скрытого слоя ($\approx0{,}002$–$0{,}004$ из заданий 22–23) для сети из заданий 17–23. Объясни разницу.
Задание 25: Дана сеть с ReLU в скрытом слое вместо сигмоиды, все остальные условия задания 17 сохраняются, $z^{(1)}_1=0{,}60$, $z^{(1)}_2=0{,}75$ (оба положительны). Как изменится значение $\sigma'(z^{(1)})$ на $\mathrm{ReLU}'(z^{(1)})$ в формуле $\delta^{(1)}$, и как это повлияет на численное значение градиента по сравнению с заданием 22?
Задание 26 (машинное обучение): Сеть из $50$ слоёв использует сигмоиду в каждом скрытом слое. Оцени, во сколько раз ослабнет градиент, дошедший до первого слоя, по сравнению с градиентом на выходе, если производная сигмоиды на каждом слое в среднем равна $0{,}2$ (используй только порядок величины).
Задание 27: Сравни стоимость вычисления градиентов по всем весам сети с $10^7$ параметрами методом конечных разностей и методом backpropagation, если один forward pass стоит $C$ операций. Дай ответ в виде отношения стоимостей.
Задание 28 (машинное обучение): Объясни своими словами, почему выражение $(W^{(l+1)})^\top\delta^{(l+1)}$ в формуле backward pass — это именно то, что нужно для правила суммирования вкладов по путям из раздела про цепное правило.
Задание 29: В сети $2\to2\to1$ из основного примера урока (первый расчёт, вход $x=(0{,}05;0{,}10)$) после одного шага градиентного спуска с $\eta=0{,}5$ вес $W^{(1)}_{11}$ изменился с $0{,}15$ на $\approx0{,}1497$ (пример 2 раздела backward pass). Оцени, сколько таких шагов потребовалось бы, чтобы $W^{(1)}_{11}$ сдвинулся на $0{,}01$, если градиент оставался бы примерно тем же ($\approx0{,}000669$).
Задание 30 (машинное обучение): Объясни, почему проверку через конечные разности (gradient checking) используют только для отладки реализации backpropagation, а не как основной способ обучения сети.
Частые ошибки
-
Путают forward pass и backward pass или считают, что backward pass можно выполнить без сохранённых значений forward pass. Как показано в разделе про forward pass, формулы backward pass напрямую используют значения $z^{(l)}$ и $a^{(l)}$, вычисленные и сохранённые на прямом проходе, — без них ни один градиент вычислить нельзя (задание 4).
-
Забывают суммировать градиент по нескольким путям, если узел графа используется больше одного раза. Пример 2 раздела про цепное правило и задание 16 прямо показывают: если переменная влияет на итог несколькими путями, вклады по каждому пути складываются, а не выбирается «главный» путь.
-
Считают затухающий градиент (vanishing gradient) редкой аномалией, а не системным следствием сигмоидных активаций в глубоких сетях. Задание 26 численно показывает: при $50$ слоях с сигмоидой и типичной производной $\approx0{,}2$ градиент, доходящий до первого слоя, ослабляется примерно в $10^{34}$ раз — это не исключение, а закономерный результат перемножения многих множителей меньше единицы.
-
Путают взрывающийся градиент (exploding gradient) с затухающим и применяют не то решение. Взрывающийся градиент возникает, когда множители в цепочке производных (обычно — веса, а не производные активации) в среднем больше единицы, и градиент растёт экспоненциально с глубиной вместо затухания; стандартное решение — обрезка градиента (gradient clipping), тогда как для затухающего градиента помогают ReLU-активации, нормализация (урок 332) и более аккуратная инициализация весов (урок 331), а не обрезка.
-
Реализуют backpropagation вручную и не проверяют результат через gradient checking. Пример 3 раздела про backward pass показывает: сравнение аналитического градиента с приближённым, посчитанным через конечные разности, — стандартный и надёжный способ найти ошибку в реализации до того, как она незаметно испортит всё обучение (задание 30).
-
Думают, что backpropagation сам решает, куда и насколько сдвинуть веса. Backpropagation вычисляет только градиент $\partial L/\partial w$ — направление наискорейшего роста ошибки. Что делать с этим градиентом дальше — с каким шагом и по какому правилу обновлять веса — решает отдельный алгоритм, оптимизатор (градиентный спуск, урок 285, или Adam, урок 293); сам backpropagation про шаг обучения (learning rate) ничего не знает.
Главное запомнить
-
Backpropagation — алгоритм, который вычисляет градиент функции потерь по каждому весу сети, опираясь на цепное правило дифференцирования; это единственный практически применимый способ обучать сети с более чем одним слоем весов.
-
Цепное правило для цепочки $x\to u_1\to\cdots\to y$ — это произведение производных на каждом звене; если переменная влияет на результат несколькими путями, вклады по путям складываются, а не выбираются.
-
Forward pass должен сохранять все промежуточные значения $z^{(l)}$ и $a^{(l)}$ — без них backward pass не сможет вычислить ни одной производной.
-
Backward pass движется от выходного слоя к входному: сначала вычисляется $\delta^{(2)}=\partial L/\partial z^{(2)}$ на выходе, затем оно используется, чтобы вычислить $\delta^{(1)}$ предыдущего слоя, и так далее — ничего не пересчитывается заново.
-
На полном численном примере сети $2\to2\to1$ пошагово вычислены все градиенты $\partial L/\partial w$ от выходного слоя до входного и выполнено одно обновление весов градиентным спуском.
-
Backward pass — это буквально топологическая сортировка вычислительного графа сети с обращёнными рёбрами (урок 262): градиент узла вычисляется только тогда, когда получены и просуммированы градиенты от всех узлов, которые его использовали.
-
Backpropagation вычисляет градиенты по всем весам сети за время, сравнимое с одним forward pass ($O(C)$), тогда как наивный метод конечных разностей требует отдельного forward pass на каждый вес ($O(P\cdot C)$) — при миллионах параметров разница составляет миллионы раз.
-
Метод конечных разностей даёт лишь приближённое значение производной и используется только для проверки правильности реализации backpropagation (gradient checking), а не как основной способ обучения.
-
Затухающий градиент (vanishing gradient) — системное следствие перемножения многих производных сигмоиды (каждая меньше $0{,}25$) в глубокой сети; взрывающийся градиент (exploding gradient) — противоположная проблема, когда множители в среднем больше единицы.
-
Backpropagation вычисляет только градиент — направление и относительную величину изменения ошибки по каждому весу; фактическое обновление весов выполняет отдельный алгоритм — оптимизатор (градиентный спуск или Adam).
Связь с темами курса
Этот урок — прямое продолжение урока 328, где был разобран forward pass как вычисление предсказания сети при фиксированных весах: сегодня та же самая сеть научилась находить веса самостоятельно, используя ту же самую последовательность слоёв, но пройденную в обратном направлении. Урок 329 (функции активации) напрямую определяет конкретные множители $f'(z)$ в формулах $\delta^{(l)}$ — выбор между сигмоидой, ReLU и другими активациями буквально задаёт, насколько сильно градиент ослабляется или сохраняется на каждом слое, что и было явно посчитано в заданиях 24–26.
Связь с уроком 262 (топологическая сортировка) — не иллюстративная параллель, а точное техническое соответствие: раздел «Вычислительный граф и связь с топологической сортировкой» этого урока прямо показывает, что backward pass — это алгоритм Кана, применённый к вычислительному графу сети с обращёнными рёбрами, начиная с узла-выхода (входящая степень $0$ в обращённом графе) и обрабатывая каждый следующий узел только тогда, когда получены градиенты от всех его «потребителей» — то же самое условие, что в уроке 262 гарантировало корректность порядка выполнения задач. Именно на этом принципе построен autograd в PyTorch и аналогичные механизмы в других фреймворках.
Урок 285 (градиентный спуск) и урок 293 (Adam) описывают, что делать с градиентами, которые backpropagation вычисляет: градиентный спуск использует их напрямую в правиле $w\leftarrow w-\eta\cdot\partial L/\partial w$ (применённом в заданиях 14 и 29 этого урока), а Adam добавляет к этому же градиенту адаптивные моменты, чтобы ускорить и стабилизировать обучение. Backpropagation отвечает только на вопрос «какой градиент», оптимизаторы — на вопрос «что с ним делать». Урок 331 (инициализация весов) и урок 332 (batch normalization) — прямое продолжение проблемы затухающего и взрывающегося градиента, впервые количественно продемонстрированной в этом уроке (задания 25–26): обе техники существуют именно для того, чтобы держать масштаб градиентов, вычисляемых backpropagation, в разумных пределах на всех слоях глубокой сети.
Интересные факты
-
Математическое ядро backpropagation — обратное накопление производных (reverse-mode automatic differentiation) — было описано Сеппо Линнайнмаа в его магистерской диссертации 1970 года как общий численный метод, безо всякой связи с нейросетями; идея «дождалась» своего самого известного применения почти двадцать лет.
-
Статья Румельхарта, Хинтона и Уильямс 1986 года не была первой публикацией об этом алгоритме применительно к нейросетям — тот же метод независимо описывали Пол Вербос в 1974 году и Дэвид Паркер с Яном Лекуном в 1985 году; заслугой авторов статьи 1986 года считается не первенство, а ясность и убедительность изложения, которые действительно вернули многослойные сети в центр внимания исследователей.
-
Название техники gradient checkpointing (частичное сохранение активаций с пересчётом недостающих во время backward pass) отражает прямой компромисс между памятью и вычислениями, который следует непосредственно из требования сохранять промежуточные значения forward pass, разобранного в этом уроке, — без этой техники обучение по-настоящему больших современных моделей на доступной видеопамяти было бы физически невозможно.
-
Один шаг обучения языковой модели с десятками миллиардов параметров с помощью backpropagation вычисляет градиенты по всем этим параметрам за время того же порядка, что и один forward pass; методом конечных разностей та же задача, по грубой оценке из задания урока, растянулась бы на тысячи лет на том же оборудовании — вся современная эпоха больших языковых моделей опирается именно на эту разницу в эффективности.
Лайфхаки
-
Прежде чем доверять реализации нейросети «на глаз», выполни gradient checking на маленьком примере — сравни аналитический градиент, посчитанный backpropagation, с приближённым, полученным через конечные разности (как в примере 3 раздела про backward pass), на пары порядков величины расхождение сразу укажет на ошибку в формулах.
-
Если обучение застряло, а loss (функция потерь) почти не меняется, проверь масштаб градиентов по слоям (можно вывести норму градиента для каждого слоя отдельно) — резко убывающие к первым слоям значения указывают на затухающий градиент, резко возрастающие — на взрывающийся; в первом случае помогают ReLU-подобные активации и нормализация, во втором — обрезка градиента (gradient clipping).
-
Прогоняя backward pass впервые вручную на бумаге (как в примерах этого урока), всегда фиксируй все сохранённые значения forward pass отдельным списком перед тем, как приступать к вычислению градиентов, — так легче не потерять множитель и не перепутать, какое значение к какому слою относится.
-
Помни направление, в котором течёт информация: активации ($a^{(l)}$) текут вперёд по сети при forward pass, а градиенты ($\delta^{(l)}$) текут назад при backward pass — спутать эти два потока при отладке кода на низком уровне (без автоматического дифференцирования) одна из самых частых причин тихих, незаметных ошибок.
-
При работе с PyTorch/TensorFlow не пытайся вручную реализовать backward pass для стандартных слоёв — доверяй autograd, но обязательно проверяй
.gradотдельных параметров послеbackward()на маленьком синтетическом примере при отладке новой, нестандартной архитектуры, где легко допустить ошибку в forward-коде, которая тихо сломает автоматически вычисляемый градиент.
Сегодня ты прошёл путь от абстрактного цепного правила из курса математического анализа до полной, воспроизводимой на бумаге трассировки того, как нейросеть узнаёт, что каждый её вес должен измениться, и на сколько именно. Это не один инструмент среди многих в арсенале глубокого обучения — это единственный механизм, которым обучаются буквально все нейросети мира, от простейшего MLP из предыдущих уроков до языковых моделей с сотнями миллиардов параметров. Всё, что будет дальше в курсе, — инициализация весов, нормализация, новые архитектуры, — так или иначе существует ради одной цели: чтобы градиенты, которые вычисляет backpropagation, оставались осмысленными и полезными на любой глубине сети. Понимание этого механизма на уровне конкретных чисел — фундамент, без которого весь дальнейший курс глубокого обучения (deep learning) остался бы набором рецептов без объяснения, почему они вообще работают.
Понял тему? Закрепи в боте! 🚀
Попрактикуйся на задачах и получи персональные рекомендации от AI
💪 Начать тренировку