Производная на компьютере: конечные разности, цепное правило, обратное распространение ошибки

Первая часть определила производную как предел наклона секущей,

f′(x)=lim⁡h→0f(x+h)−f(x)h,f'(x) = \lim_{h \to 0} \frac{f(x+h) - f(x)}{h},

и упёрлась в проблему: по одним значениям функции компьютер получает только оценку. Здесь мы измерим, насколько точной бывает такая оценка, а потом показывает, что вместо неё делают нейросети: один раз дифференцируют формулу по правилам, связывают их цепным правилом и прогоняют результат назад по вычислительному графу. Последний шаг — это обратное распространение ошибки, к которому и ведёт первая лекция курса Андрея Карпаты Neural Networks: Zero to Hero. Все панели ниже живые: поменяйте что-нибудь — и числа пересчитаются, ни одно не вписано вручную.

Кривая та же, что и в первой части:

f(x)=3sin⁡(2.5x)+0.4x2−1.5x.f(x) = 3\sin(2.5x) + 0.4x^2 - 1.5x .

Производная как функция

Раз у каждой точки свой наклон, производная сама является функцией: x↦f′(x)x \mapsto f'(x). Напрямую через предел её вычисляют редко. Достаточно нескольких правил, каждое из которых один раз доказывается из определения:

Вместе они дают tanh⁡x=e2x−1e2x+1\tanh x = \dfrac{e^{2x} - 1}{e^{2x} + 1} — активацию первого нейрона в лекции: по правилам (tanh⁡x)′=1−tanh⁡2x(\tanh x)' = 1 - \tanh^2 x. Этот наклон равен 11 в нуле и затухает к 00 с ростом ∣x∣|x|, поэтому нейрон, загнанный глубоко в насыщение, почти не учится: любой пришедший к нему градиент умножается почти на ноль.

Для нашей кривой цепное правило превращает sin⁡(2.5x)\sin(2.5x) в cos⁡(2.5x)⋅2.5\cos(2.5x) \cdot 2.5, а остальное дифференцируется почленно:

f′(x)=7.5cos⁡(2.5x)+0.8x−1.5.f'(x) = 7.5\cos(2.5x) + 0.8x - 1.5 .

Именно по этой формуле построены пунктирные касательные в обеих частях.

Там, где f′(x)=0f'(x) = 0, касательная горизонтальна. На показанном отрезке это происходит 7 раз; где f′f' меняет знак с минуса на плюс, у кривой локальный минимум — самый глубокий при x≈1.885x \approx 1.885, где f≈−4.406f \approx -4.406, — а где с плюса на минус, локальный максимум.

Не у всякой функции производная есть всюду. У ∣x∣|x| наклон −1-1 слева от 00 и +1+1 справа; у секущей в 00 нет единого предела, поэтому ∣x∣|x| там не дифференцируема. У функции активации ReLU, max⁡(0,x)\max(0, x), такой же излом, и библиотеки для нейросетей просто договариваются использовать там 00.

Приблизьте излом и убедитесь сами. Панель проводит из точки две секущие, влево и вправо, и hh привязано к масштабу. На гладкой кривой приближение распрямляет картинку, и две секущие складываются в одну прямую. В изломе ∣x∣|x| картинка не меняется никогда: при любом увеличении это та же галочка, левая секущая остаётся на −1-1, правая — на +1+1. Единого числа, которым могла бы быть производная, просто нет.

Излом |x| под лупой: левая и правая секущие из x = 0 сохраняют наклоны −1 и +1 при любом увеличении, а на гладкой x² сливаются в одну прямую; для ReLU показан 0, который библиотеки берут в изломе.

Насколько малым брать h на компьютере?

В точной арифметике чем меньше hh, тем лучше. На компьютере — нет.

Наклон секущей из первой части, (f(x+h)−f(x))/h\bigl(f(x+h) - f(x)\bigr)/h, прямая разность, вычитает два почти равных числа. 64-битное число с плавающей точкой хранит около 16 значащих цифр, поэтому разность теряет столько из них, сколько их общих у f(x+h)f(x+h) и f(x)f(x). Оставшаяся ошибка округления затем делится на крошечное hh.

Две ошибки тянут в противоположные стороны:

Их сумма минимальна около h≈ε≈1.5⋅10−8h \approx \sqrt{\varepsilon} \approx 1.5 \cdot 10^{-8}.

Центральная разность (f(x+h)−f(x−h))/2h\bigl(f(x+h) - f(x-h)\bigr)/2h берёт точки с обеих сторон. Слагаемые с f′′f'' сокращаются, её ошибка усечения имеет порядок h2h^2, а наилучшее hh сдвигается вверх, примерно к ε3≈6.1⋅10−6\sqrt[3]{\varepsilon} \approx 6.1 \cdot 10^{-6}.

Панель вычисляет обе формулы в обычной 64-битной арифметике и строит график их ошибки в зависимости от hh; ведите по графику, чтобы менять hh.

График в логарифмическом масштабе по обеим осям: ошибка прямой и центральной разностей для f при x = 3 в зависимости от h; обе ошибки падают с уменьшением h, пока не начинает преобладать округление и они снова не растут; минимум прямой разности — около h = 1e-8, центральной — около 1e-5.

Читайте график справа налево — так, как уменьшается hh. Сначала обе линии падают с наклонами 1 и 2: каждое уменьшение hh в десять раз делит ошибку прямой разности на 10, а центральной — на 100. Затем верх берёт округление, и обе линии поворачивают вверх, в шум.

При x=3x = 3 прямая разность точнее всего при h≈4⋅10−9h \approx 4 \cdot 10^{-9}, с ошибкой 1.3⋅10−81.3 \cdot 10^{-8}. Центральная разность достигает 3⋅10−113 \cdot 10^{-11} при h≈4⋅10−6h \approx 4 \cdot 10^{-6} — примерно в 450 раз точнее, при hh в 1000 раз больше.

При h=10−16h = 10^{-16} обе оценки равны ровно 00: ближайшее к 3+10−163 + 10^{-16} число с плавающей точкой — само 33, поэтому f(x+h)−f(x)=0f(x+h) - f(x) = 0.

Поэтому численные производные используют, чтобы проверять аналитические, — это «проверка градиента» центральной разностью с hh около 10−510^{-5}, — а не чтобы обучать сети: каждая оценка стоит дополнительных вычислений функции на каждый вход, а ответ верен лишь примерно до десяти знаков.

Цепное правило на вычислительном графе

Настоящие формулы — это композиции простых операций. Разобьём

L=(a⋅b+c)⋅sL = (a \cdot b + c) \cdot s

на шаги: e=a⋅be = a \cdot b, d=e+cd = e + c, L=d⋅sL = d \cdot s. Это вычислительный граф.

Прямой проход вычисляет значения слева направо. При a=2, b=−3, c=10, s=−2a = 2,\ b = -3,\ c = 10,\ s = -2: e=−6, d=4, L=−8e = -6,\ d = 4,\ L = -8.

Обратный проход отвечает на другой вопрос: насколько изменится LL, если чуть сдвинуть данный узел? Это производная ∂L/∂node\partial L / \partial \text{node}, которую называют градиентом узла. Значение узла и его градиент — разные числа.

Каждой операции достаточно знать собственную локальную производную:

Скорости вдоль цепочки перемножаются. Лекция берёт пример Джорджа Симмонса: если машина едет вдвое быстрее велосипеда, а велосипед вчетверо быстрее пешехода, то машина быстрее пешехода в 2⋅4=82 \cdot 4 = 8 раз. Цепное правило точно так же перемножает локальные производные вдоль пути:

∂L∂a=∂L∂d⋅∂d∂e⋅∂e∂a=s⋅1⋅b=(−2)⋅1⋅(−3)=6.\frac{\partial L}{\partial a} = \frac{\partial L}{\partial d} \cdot \frac{\partial d}{\partial e} \cdot \frac{\partial e}{\partial a} = s \cdot 1 \cdot b = (-2)\cdot 1 \cdot(-3) = 6 .

Двигайте ползунок обратного прохода: каждое деление обрабатывает одну операцию, справа налево, и подписывает каждое пройденное ребро его локальной производной. Затем потащите любой вход вбок на графе или сдвиньте его ползунок.

Вычислительный граф L = (a·b + c)·s со значением и градиентом каждого узла; ползунок обратного прохода обрабатывает по одной операции, двигаясь от выхода к входам, и подписывает рёбра локальными производными, а перетаскивание входа сравнивает изменение L с предсказанием градиента.

Всё начинается с ∂L/∂L=1\partial L / \partial L = 1: сдвиг выхода на ε\varepsilon меняет его на ε\varepsilon. В конце ∂L/∂a=6, ∂L/∂b=−4, ∂L/∂c=−2, ∂L/∂s=4\partial L/\partial a = 6,\ \partial L/\partial b = -4,\ \partial L/\partial c = -2,\ \partial L/\partial s = 4, а перетаскивание проверяет их по старинке. Сдвиньте aa с 22 до 2.52.5 — и LL изменится на 33, ровно на ∂L/∂a⋅Δa=6⋅0.5\partial L/\partial a \cdot \Delta a = 6 \cdot 0.5. Градиент и есть это предсказание: насколько сдвигается выход на единицу сдвига входа. Здесь оно точное, потому что LL линейна по каждому входу в отдельности; для кривой функции оно верно только для малых сдвигов — в этом и состоит всё содержание формулы f(x+h)≈f(x)+f′(x) hf(x+h) \approx f(x) + f'(x)\,h.

Теперь сдвиньте все четыре входа сразу. «Шаг по градиенту» прибавляет к каждому входу 0.010.01 его собственного градиента, и LL растёт с −8.00-8.00 до −7.29-7.29. Градиент предсказывал рост на 0.01⋅(62+42+22+42)=0.720.01 \cdot (6^2 + 4^2 + 2^2 + 4^2) = 0.72; настоящий — 0.710.71. Разница — это произведения входов, a⋅ba \cdot b и e⋅se \cdot s, которые меняются одновременно и которых предсказание первого порядка не видит: LL линейна по каждому входу в отдельности, но не по всем сразу. Шаг против градиента, наоборот, уменьшил бы LL — и в этом всё обучение, как покажет последний раздел.

«—», пока проход не дошёл до узла, означает «ещё не вычислено», а это не то же самое, что ноль. Сдвиньте ss до 00 — и все градиенты левее станут нулевыми, хотя значение ни одного из этих узлов не равно нулю: когда последний множитель равен нулю, ничто выше по графу не может сдвинуть LL.

Это и есть обратное распространение ошибки (backpropagation). Один обратный проход даёт градиент единственного выхода по каждому узлу ценой небольшой константы, умноженной на стоимость прямого прохода. У сети одна функция потерь и миллионы параметров, и именно из-за этого соотношения её обучают назад, а не вперёд.

Одна переменная, несколько путей: градиенты складываются

А если значение используется больше одного раза? Возьмём

L=x⋅x+k⋅x.L = x \cdot x + k \cdot x .

Переменная xx входит в граф трижды: дважды как вход x⋅xx \cdot x и один раз в k⋅xk \cdot x. Каждое использование — отдельный путь к LL, и каждый путь возвращает свой вклад: xx с каждой стороны произведения и kk от второго слагаемого. Цепное правило для функций многих переменных говорит, что производная равна их сумме:

∂L∂x=x+x+k=2x+k.\frac{\partial L}{\partial x} = x + x + k = 2x + k .

Поэтому движок обратного распространения ошибки пишет grad += ..., а не grad = .... Вторая кнопка воспроизводит ошибку: каждый вклад затирает предыдущий, выживает только последний, и касательная, построенная по такому градиенту, проходит мимо кривой.

Граф L = x·x + k·x, где x доходит до L по трём путям и возвращает три вклада; рядом — кривая L(x) с касательной, построенной по накопленному градиенту; касательная проходит мимо кривой, если вклады перезаписываются, а не складываются.

Перетащите точку в x=−1.5x = -1.5 при k=3k = 3. Сумма равна нулю, и касательная горизонтальна, но ни один путь не исчез: −1.5−1.5+3=0-1.5 - 1.5 + 3 = 0. Вклады могут взаимно уничтожаться, и нулевой градиент не значит, что от xx ничего не зависит.

«Накоплением» градиентов называют две разные вещи, и их полезно не путать. Внутри одного обратного прохода вклады обязаны складываться. Между шагами обучения сохранённые градиенты нужно сначала обнулить — иначе градиент предыдущего шага прибавится к новому. Первое — это цепное правило; забыть второе — ошибка.

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

Назад к спуску

Первая часть началась с градиентного спуска, x←x−η f′(x)x \leftarrow x - \eta\, f'(x), на кривой одной переменной. Обучение нейросети — тот же цикл: xx превращается в миллионы весов, ff — в функцию потерь на обучающих данных, f′(x)f'(x) — в вектор градиента, а обратное распространение ошибки вычисляет его за один проход. «Шаг по градиенту» на графе выше — один шаг этого цикла, только вверх; шаг против градиента ведёт вниз. Дальше лекция строит именно это — на таком же крошечном движке, как в этих панелях.

Короткие ответы

У любой ли функции есть производная?

Нет. У |x| в нуле излом: слева наклон −1, справа +1, поэтому у секущей нет единого предела. У ReLU, max(0, x), такой же излом; библиотеки для нейросетей просто выбирают там какое-то значение, обычно 0.

Чем производная отличается от градиента?

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

Численное, символьное или автоматическое дифференцирование — что есть что?

Численное дифференцирование оценивает наклон по значениям функции в близких точках и обменивает ошибку усечения на ошибку округления. Символьное дифференцирование преобразует формулы. Автоматическое дифференцирование, частным случаем которого является обратное распространение ошибки, применяет цепное правило к фактической последовательности операций, выполненных программой, и даёт точные производные с погрешностью только на округление в арифметике с плавающей точкой.

Почему обратное распространение ошибки идёт в обратную сторону?

У сети одна функция потерь и миллионы параметров. Если идти назад от функции потерь, один проход по графу даёт производную этого единственного выхода по каждому входу — ценой небольшой константы, умноженной на стоимость прямого прохода. Движение вперёд потребовало бы по одному проходу на каждый параметр.

Почему при обратном распространении ошибки градиенты накапливаются через +=?

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

Литература