.. _sec_multivariable_calculus: Многомерное исчисление ====================== Теперь, когда у нас есть достаточно чёткое понимание производных функции одной переменной, вернёмся к нашему исходному вопросу: мы рассматривали функцию потерь, зависящую от потенциально миллиардов весов. Дифференцирование в многомерном пространстве -------------------------------------------- Из :numref:`sec_single_variable_calculus` следует, что если мы изменим одно из этих миллиардов весов, оставив все остальные неизменными, мы знаем, что произойдёт! Это не что иное, как функция одной переменной, поэтому мы можем записать .. math:: L(w_1+\epsilon_1, w_2, \ldots, w_N) \approx L(w_1, w_2, \ldots, w_N) + \epsilon_1 \frac{d}{dw_1} L(w_1, w_2, \ldots, w_N). :label: eq_part_der Производную по одной переменной при фиксированных остальных мы назовём *частной производной*, а для обозначения производной по :eq:`eq_part_der` будем использовать обозначение :math:`\frac{\partial}{\partial w_1}`. Теперь давайте немного изменим :math:`w_2`, чтобы получить :math:`w_2 + \epsilon_2`: .. math:: \begin{aligned} L(w_1+\epsilon_1, w_2+\epsilon_2, \ldots, w_N) & \approx L(w_1, w_2+\epsilon_2, \ldots, w_N) + \epsilon_1 \frac{\partial}{\partial w_1} L(w_1, w_2+\epsilon_2, \ldots, w_N+\epsilon_N) \\ & \approx L(w_1, w_2, \ldots, w_N) \\ & \quad + \epsilon_2\frac{\partial}{\partial w_2} L(w_1, w_2, \ldots, w_N) \\ & \quad + \epsilon_1 \frac{\partial}{\partial w_1} L(w_1, w_2, \ldots, w_N) \\ & \quad + \epsilon_1\epsilon_2\frac{\partial}{\partial w_2}\frac{\partial}{\partial w_1} L(w_1, w_2, \ldots, w_N) \\ & \approx L(w_1, w_2, \ldots, w_N) \\ & \quad + \epsilon_2\frac{\partial}{\partial w_2} L(w_1, w_2, \ldots, w_N) \\ & \quad + \epsilon_1 \frac{\partial}{\partial w_1} L(w_1, w_2, \ldots, w_N). \end{aligned} Мы вновь воспользовались идеей, что :math:`\epsilon_1\epsilon_2` является членом более высокого порядка, который можно отбросить так же, как мы отбросили :math:`\epsilon^{2}` в предыдущем разделе, вместе с тем, что мы видели в :eq:`eq_part_der`. Продолжая в том же духе, мы можем записать, что .. math:: L(w_1+\epsilon_1, w_2+\epsilon_2, \ldots, w_N+\epsilon_N) \approx L(w_1, w_2, \ldots, w_N) + \sum_i \epsilon_i \frac{\partial}{\partial w_i} L(w_1, w_2, \ldots, w_N). Это может выглядеть как беспорядок, но мы можем сделать это более привычным, заметив, что сумма справа в точности напоминает скалярное произведение, поэтому, если мы обозначим .. math:: \boldsymbol{\epsilon} = [\epsilon_1, \ldots, \epsilon_N]^\top \; \textrm{and} \; \nabla_{\mathbf{x}} L = \left[\frac{\partial L}{\partial x_1}, \ldots, \frac{\partial L}{\partial x_N}\right]^\top, то .. math:: L(\mathbf{w} + \boldsymbol{\epsilon}) \approx L(\mathbf{w}) + \boldsymbol{\epsilon}\cdot \nabla_{\mathbf{w}} L(\mathbf{w}). :label: eq_nabla_use Мы будем называть вектор :math:`\nabla_{\mathbf{w}} L` *градиентом* функции :math:`L`. Уравнение :eq:`eq_nabla_use` стоит обдумать немного. Оно имеет тот же формат, что и в одномерном случае, только все величины преобразованы в векторы, а операции — в скалярные произведения. Оно позволяет приблизительно оценить, как изменится функция :math:`L` при любом возмущении входных данных. Как мы увидим в следующем разделе, это даст нам важный инструмент для геометрического понимания того, как можно обучаться, используя информацию, содержащуюся в градиенте. Но сначала давайте посмотрим, как это приближение работает на примере. Предположим, что мы работаем с функцией .. math:: f(x, y) = \log(e^x + e^y) \textrm{ with gradient } \nabla f (x, y) = \left[\frac{e^x}{e^x+e^y}, \frac{e^y}{e^x+e^y}\right]. Если мы рассмотрим точку, такую как :math:`(0, \log(2))`, то увидим, что .. math:: f(x, y) = \log(3) \textrm{ with gradient } \nabla f (x, y) = \left[\frac{1}{3}, \frac{2}{3}\right]. Таким образом, если мы хотим аппроксимировать :math:`f` в :math:`(\epsilon_1, \log(2) + \epsilon_2)`, мы видим, что нам следует использовать конкретный объект :eq:`eq_nabla_use`: .. math:: f(\epsilon_1, \log(2) + \epsilon_2) \approx \log(3) + \frac{1}{3}\epsilon_1 + \frac{2}{3}\epsilon_2. Мы можем проверить это в коде, чтобы оценить качество аппроксимации. .. raw:: latex \diilbookstyleinputcell .. code:: python %matplotlib inline import numpy as np import torch from IPython import display from mpl_toolkits import mplot3d from d2l import torch as d2l def f(x, y): return torch.log(torch.exp(x) + torch.exp(y)) def grad_f(x, y): return torch.tensor([torch.exp(x) / (torch.exp(x) + torch.exp(y)), torch.exp(y) / (torch.exp(x) + torch.exp(y))]) epsilon = torch.tensor([0.01, -0.03]) grad_approx = f(torch.tensor([0.]), torch.log( torch.tensor([2.]))) + epsilon.dot( grad_f(torch.tensor([0.]), torch.log(torch.tensor(2.)))) true_value = f(torch.tensor([0.]) + epsilon[0], torch.log( torch.tensor([2.])) + epsilon[1]) f'approximation: {grad_approx}, true Value: {true_value}' Геометрия градиентов и градиентный спуск ---------------------------------------- Рассмотрим снова выражение из :eq:`eq_nabla_use`: .. math:: L(\mathbf{w} + \boldsymbol{\epsilon}) \approx L(\mathbf{w}) + \boldsymbol{\epsilon}\cdot \nabla_{\mathbf{w}} L(\mathbf{w}). Предположим, что мы хотим использовать это для минимизации наших потерь :math:`L`. Сначала рассмотрим геометрическую интерпретацию алгоритма градиентного спуска, впервые описанного в :numref:`sec_autograd`. Мы поступим следующим образом: 1. Начните со случайного выбора начальных параметров :math:`\mathbf{w}`. 2. Найдите направление :math:`\mathbf{v}`, в котором :math:`L` убывает наиболее быстро в точке :math:`\mathbf{w}`. 3. Сделаем небольшой шаг в этом направлении: :math:`\mathbf{w} \rightarrow \mathbf{w} + \epsilon\mathbf{v}`. 4. Повторяем. Единственное, что мы пока не умеем точно вычислять, — это вектор :math:`\mathbf{v}` на втором шаге. Мы будем называть такое направление *направлением наискорейшего спуска*. Используя геометрическое понимание скалярного произведения из :numref:`sec_geometry-linear-algebraic-ops`, мы видим, что можем переписать :eq:`eq_nabla_use` в виде .. math:: L(\mathbf{w} + \mathbf{v}) \approx L(\mathbf{w}) + \mathbf{v}\cdot \nabla_{\mathbf{w}} L(\mathbf{w}) = L(\mathbf{w}) + \|\nabla_{\mathbf{w}} L(\mathbf{w})\|\cos(\theta). Обратите внимание, что для удобства мы приняли длину направления равной единице и использовали :math:`\theta` для обозначения угла между :math:`\mathbf{v}` и :math:`\nabla_{\mathbf{w}} L(\mathbf{w})`. Если мы хотим найти направление, в котором :math:`L` уменьшается максимально быстро, нам нужно сделать это выражение как можно более отрицательным. Единственный способ, которым выбранное нами направление влияет на это уравнение, заключается через :math:`\cos(\theta)`, поэтому мы стремимся сделать этот косинус как можно более отрицательным. Вспоминая свойства функции косинуса, мы можем добиться этого, сделав :math:`\cos(\theta) = -1`, или, что эквивалентно, задав угол между градиентом и выбранным направлением равным :math:`\pi` радиан, или, что то же самое, :math:`180` градусам. Единственный способ достичь этого — двигаться в строго противоположном направлении: выбрать :math:`\mathbf{v}` так, чтобы оно указывало в направлении, строго противоположном :math:`\nabla_{\mathbf{w}} L(\mathbf{w})`! Это приводит нас к одному из самых важных математических понятий в машинном обучении: направление наискорейшего спуска указывает направление, противоположное :math:`-\nabla_{\mathbf{w}}L(\mathbf{w})`. Таким образом, наш неформальный алгоритм можно переписать следующим образом. 1. Начните со случайного выбора начальных параметров :math:`\mathbf{w}`. 2. Вычислите :math:`\nabla_{\mathbf{w}} L(\mathbf{w})`. 3. Сделаем небольшой шаг в противоположном направлении: :math:`\mathbf{w} \leftarrow \mathbf{w} - \epsilon\nabla_{\mathbf{w}} L(\mathbf{w})`. 4. Повторяем. Этот базовый алгоритм был модифицирован и адаптирован многими исследователями множеством способов, но основная идея остаётся неизменной во всех них. Используем градиент, чтобы найти направление, в котором функция потерь убывает наиболее быстро, и обновляем параметры, делая шаг в этом направлении. Примечание по математической оптимизации ---------------------------------------- На протяжении всей книги мы сосредотачиваемся исключительно на численных методах оптимизации по практической причине: все функции, с которыми мы сталкиваемся в контексте глубокого обучения, слишком сложны для явной минимизации. Однако полезно рассмотреть, что полученное выше геометрическое понимание говорит о прямой оптимизации функций. Предположим, мы хотим найти значение :math:`\mathbf{x}_0`, которое минимизирует некоторую функцию :math:`L(\mathbf{x})`. Допустим, нам также предоставили значение и утверждают, что это именно то значение, которое минимизирует :math:`L`. Можем ли мы проверить, насколько правдоподобным является их ответ? Снова рассмотрим :eq:`eq_nabla_use`: .. math:: L(\mathbf{x}_0 + \boldsymbol{\epsilon}) \approx L(\mathbf{x}_0) + \boldsymbol{\epsilon}\cdot \nabla_{\mathbf{x}} L(\mathbf{x}_0). Если градиент не равен нулю, мы знаем, что можем сделать шаг в направлении :math:`-\epsilon \nabla_{\mathbf{x}} L(\mathbf{x}_0)`, чтобы найти значение :math:`L`, которое будет меньше. Следовательно, если мы действительно находимся в точке минимума, это не может быть так! Мы можем заключить, что если :math:`\mathbf{x}_0` является минимумом, то :math:`\nabla_{\mathbf{x}} L(\mathbf{x}_0) = 0`. Точки, в которых :math:`\nabla_{\mathbf{x}} L(\mathbf{x}_0) = 0`, мы называем *критическими точками*. Это хорошо, потому что в некоторых редких случаях мы *можем* явно найти все точки, где градиент равен нулю, и выбрать ту, которая имеет наименьшее значение. Для конкретного примера рассмотрим функцию .. math:: f(x) = 3x^4 - 4x^3 -12x^2. У этой функции производная .. math:: \frac{df}{dx} = 12x^3 - 12x^2 -24x = 12x(x-2)(x+1). Единственные возможные точки минимума находятся в :math:`x = -1, 0, 2`, где функция принимает значения :math:`-5,0, -32` соответственно, и поэтому мы можем заключить, что минимизируем нашу функцию, когда :math:`x = 2`. Быстрый график подтверждает это. .. raw:: latex \diilbookstyleinputcell .. code:: python x = torch.arange(-2, 3, 0.01) f = (3 * x**4) - (4 * x**3) - (12 * x**2) d2l.plot(x, f, 'x', 'f(x)') Это подчеркивает важный факт, который необходимо знать как при теоретическом, так и при численном анализе: единственными возможными точками, в которых функция может достигать минимума (или максимума), являются точки, в которых градиент равен нулю. Однако не каждая точка с нулевым градиентом является истинным *глобальным* минимумом (или максимумом). Многомерное цепное правило -------------------------- Предположим, что у нас есть функция от четырёх переменных (:math:`w, x, y` и :math:`z`), которую можно получить путём композиции нескольких слагаемых: .. math:: \begin{aligned}f(u, v) & = (u+v)^{2} \\u(a, b) & = (a+b)^{2}, \qquad v(a, b) = (a-b)^{2}, \\a(w, x, y, z) & = (w+x+y+z)^{2}, \qquad b(w, x, y, z) = (w+x-y-z)^2.\end{aligned} :label: eq_multi_func_def Подобные цепочки уравнений часто встречаются при работе с нейронными сетями, поэтому понимание того, как вычислять градиенты таких функций, является ключевым. Мы можем начать замечать визуальные подсказки об этой связи в :numref:`fig_chain-1`, если посмотрим на то, какие переменные напрямую связаны друг с другом. .. _fig_chain-1: .. figure:: ../img/chain-net1.svg Вышеприведенные функциональные связи, где узлы представляют значения, а ребра показывают функциональную зависимость. Ничто не мешает нам просто составить всё из :eq:`eq_multi_func_def` и записать это .. math:: f(w, x, y, z) = \left(\left((w+x+y+z)^2+(w+x-y-z)^2\right)^2+\left((w+x+y+z)^2-(w+x-y-z)^2\right)^2\right)^2. Затем мы можем вычислить производную, используя только производные от одной переменной, но если мы сделаем это, то быстро обнаружим, что нас заваливает множеством слагаемых, многие из которых повторяются! Действительно, можно заметить, что, например: .. math:: \begin{aligned} \frac{\partial f}{\partial w} & = 2 \left(2 \left(2 (w + x + y + z) - 2 (w + x - y - z)\right) \left((w + x + y + z)^{2}- (w + x - y - z)^{2}\right) + \right.\\ & \left. \quad 2 \left(2 (w + x - y - z) + 2 (w + x + y + z)\right) \left((w + x - y - z)^{2}+ (w + x + y + z)^{2}\right)\right) \times \\ & \quad \left(\left((w + x + y + z)^{2}- (w + x - y - z)^2\right)^{2}+ \left((w + x - y - z)^{2}+ (w + x + y + z)^{2}\right)^{2}\right). \end{aligned} Если бы мы также захотели вычислить :math:`\frac{\partial f}{\partial x}`, мы получили бы аналогичное уравнение с множеством повторяющихся слагаемых, причём многие из этих повторяющихся слагаемых были бы *общими* для обеих производных. Это означало бы колоссальную трату вычислительных ресурсов, и если бы нам приходилось вычислять производные таким образом, вся революция в глубоком обучении так и не началась бы! Давайте разберем задачу. Сначала мы попробуем понять, как :math:`f` изменяется при изменении :math:`a`, по сути предполагая, что :math:`w, x, y` и :math:`z` не существуют. Мы будем рассуждать так же, как и тогда, когда мы впервые работали с градиентом. Возьмем :math:`a` и добавим к нему небольшое значение :math:`\epsilon`. .. math:: \begin{aligned} & f(u(a+\epsilon, b), v(a+\epsilon, b)) \\ \approx & f\left(u(a, b) + \epsilon\frac{\partial u}{\partial a}(a, b), v(a, b) + \epsilon\frac{\partial v}{\partial a}(a, b)\right) \\ \approx & f(u(a, b), v(a, b)) + \epsilon\left[\frac{\partial f}{\partial u}(u(a, b), v(a, b))\frac{\partial u}{\partial a}(a, b) + \frac{\partial f}{\partial v}(u(a, b), v(a, b))\frac{\partial v}{\partial a}(a, b)\right]. \end{aligned} Первая строка следует из определения частной производной, а вторая — из определения градиента. Отслеживать, в какой именно точке вычисляется каждая производная, как в выражении :math:`\frac{\partial f}{\partial u}(u(a, b), v(a, b))`, громоздко, поэтому мы часто используем более краткую и запоминающуюся запись .. math:: \frac{\partial f}{\partial a} = \frac{\partial f}{\partial u}\frac{\partial u}{\partial a}+\frac{\partial f}{\partial v}\frac{\partial v}{\partial a}. Полезно задуматься о смысле этого процесса. Мы хотим понять, как функция вида :math:`f(u(a, b), v(a, b))` изменяет свое значение при изменении :math:`a`. Это может происходить по двум путям: один путь связан с :math:`a \rightarrow u \rightarrow f`, а другой — с :math:`a \rightarrow v \rightarrow f`. Мы можем вычислить оба этих вклада с помощью цепного правила: :math:`\frac{\partial w}{\partial u} \cdot \frac{\partial u}{\partial x}` и :math:`\frac{\partial w}{\partial v} \cdot \frac{\partial v}{\partial x}` соответственно, а затем сложить их. Представим, что у нас есть другая сеть функций, где функции справа зависят от тех, к которым они подключены слева, как показано на :numref:`fig_chain-2`. .. _fig_chain-2: .. figure:: ../img/chain-net2.svg Ещё один более тонкий пример цепного правила. Чтобы вычислить величину :math:`\frac{\partial f}{\partial y}`, необходимо просуммировать по всем (в данном случае :math:`3`) путям от :math:`y` до :math:`f`, что даёт .. math:: \frac{\partial f}{\partial y} = \frac{\partial f}{\partial a} \frac{\partial a}{\partial u} \frac{\partial u}{\partial y} + \frac{\partial f}{\partial u} \frac{\partial u}{\partial y} + \frac{\partial f}{\partial b} \frac{\partial b}{\partial v} \frac{\partial v}{\partial y}. Понимание цепного правила таким образом принесёт огромную пользу при попытке понять, как градиенты распространяются через сети, и почему различные архитектурные решения, такие как в LSTM (:numref:`sec_lstm`) или остаточных слоях (:numref:`sec_resnet`), могут влиять на процесс обучения, контролируя поток градиентов. Алгоритм обратного распространения ошибки ----------------------------------------- Вернёмся к примеру :eq:`eq_multi_func_def` из предыдущего раздела, где .. math:: \begin{aligned} f(u, v) & = (u+v)^{2} \\ u(a, b) & = (a+b)^{2}, \qquad v(a, b) = (a-b)^{2}, \\ a(w, x, y, z) & = (w+x+y+z)^{2}, \qquad b(w, x, y, z) = (w+x-y-z)^2. \end{aligned} Если мы хотим вычислить, скажем, :math:`\frac{\partial f}{\partial w}`, мы можем применить многомерное цепное правило, чтобы получить: .. math:: \begin{aligned} \frac{\partial f}{\partial w} & = \frac{\partial f}{\partial u}\frac{\partial u}{\partial w} + \frac{\partial f}{\partial v}\frac{\partial v}{\partial w}, \\ \frac{\partial u}{\partial w} & = \frac{\partial u}{\partial a}\frac{\partial a}{\partial w}+\frac{\partial u}{\partial b}\frac{\partial b}{\partial w}, \\ \frac{\partial v}{\partial w} & = \frac{\partial v}{\partial a}\frac{\partial a}{\partial w}+\frac{\partial v}{\partial b}\frac{\partial b}{\partial w}. \end{aligned} Давайте попробуем использовать эту декомпозицию для вычисления :math:`\frac{\partial f}{\partial w}`. Обратите внимание, что здесь нам нужны лишь различные одношаговые частные производные: .. math:: \begin{aligned} \frac{\partial f}{\partial u} = 2(u+v), & \quad\frac{\partial f}{\partial v} = 2(u+v), \\ \frac{\partial u}{\partial a} = 2(a+b), & \quad\frac{\partial u}{\partial b} = 2(a+b), \\ \frac{\partial v}{\partial a} = 2(a-b), & \quad\frac{\partial v}{\partial b} = -2(a-b), \\ \frac{\partial a}{\partial w} = 2(w+x+y+z), & \quad\frac{\partial b}{\partial w} = 2(w+x-y-z). \end{aligned} Если записать это в виде кода, получится вполне управляемое выражение. .. raw:: latex \diilbookstyleinputcell .. code:: python # Compute the value of the function from inputs to outputs w, x, y, z = -1, 0, -2, 1 a, b = (w + x + y + z)**2, (w + x - y - z)**2 u, v = (a + b)**2, (a - b)**2 f = (u + v)**2 print(f' f at {w}, {x}, {y}, {z} is {f}') # Compute the single step partials df_du, df_dv = 2*(u + v), 2*(u + v) du_da, du_db, dv_da, dv_db = 2*(a + b), 2*(a + b), 2*(a - b), -2*(a - b) da_dw, db_dw = 2*(w + x + y + z), 2*(w + x - y - z) # Compute the final result from inputs to outputs du_dw, dv_dw = du_da*da_dw + du_db*db_dw, dv_da*da_dw + dv_db*db_dw df_dw = df_du*du_dw + df_dv*dv_dw print(f'df/dw at {w}, {x}, {y}, {z} is {df_dw}') Однако обратите внимание, что это всё ещё не упрощает вычисление чего-то вроде :math:`\frac{\partial f}{\partial x}`. Причина заключается в *способе*, которым мы применили цепное правило. Если посмотреть на то, что мы делали выше, мы всегда оставляли :math:`\partial w` в знаменателе, когда это было возможно. Таким образом, мы выбрали применение цепного правила, рассматривая, как :math:`w` изменяется при изменении каждой другой переменной. Если бы это было нашей целью, это было бы хорошим решением. Однако вспомним нашу мотивацию из глубокого обучения: мы хотим видеть, как каждый *параметр* влияет на *потери*. По сути, мы хотим применять цепное правило, оставляя :math:`\partial f` в числителе, whenever это возможно! Более подробно отметим, что мы можем записать .. math:: \begin{aligned} \frac{\partial f}{\partial w} & = \frac{\partial f}{\partial a}\frac{\partial a}{\partial w} + \frac{\partial f}{\partial b}\frac{\partial b}{\partial w}, \\ \frac{\partial f}{\partial a} & = \frac{\partial f}{\partial u}\frac{\partial u}{\partial a}+\frac{\partial f}{\partial v}\frac{\partial v}{\partial a}, \\ \frac{\partial f}{\partial b} & = \frac{\partial f}{\partial u}\frac{\partial u}{\partial b}+\frac{\partial f}{\partial v}\frac{\partial v}{\partial b}. \end{aligned} Обратите внимание, что применение цепного правила требует от нас явного вычисления :math:`\frac{\partial f}{\partial u}, \frac{\partial f}{\partial v}, \frac{\partial f}{\partial a}, \frac{\partial f}{\partial b}, \; \textrm{and} \; \frac{\partial f}{\partial w}`. Ничто не мешает нам также включить следующие уравнения: .. math:: \begin{aligned} \frac{\partial f}{\partial x} & = \frac{\partial f}{\partial a}\frac{\partial a}{\partial x} + \frac{\partial f}{\partial b}\frac{\partial b}{\partial x}, \\ \frac{\partial f}{\partial y} & = \frac{\partial f}{\partial a}\frac{\partial a}{\partial y}+\frac{\partial f}{\partial b}\frac{\partial b}{\partial y}, \\ \frac{\partial f}{\partial z} & = \frac{\partial f}{\partial a}\frac{\partial a}{\partial z}+\frac{\partial f}{\partial b}\frac{\partial b}{\partial z}. \end{aligned} а затем отслеживать, как :math:`f` изменяется при изменении *любого* узла во всей сети. Давайте реализуем это. .. raw:: latex \diilbookstyleinputcell .. code:: python # Compute the value of the function from inputs to outputs w, x, y, z = -1, 0, -2, 1 a, b = (w + x + y + z)**2, (w + x - y - z)**2 u, v = (a + b)**2, (a - b)**2 f = (u + v)**2 print(f'f at {w}, {x}, {y}, {z} is {f}') # Compute the derivative using the decomposition above # First compute the single step partials df_du, df_dv = 2*(u + v), 2*(u + v) du_da, du_db, dv_da, dv_db = 2*(a + b), 2*(a + b), 2*(a - b), -2*(a - b) da_dw, db_dw = 2*(w + x + y + z), 2*(w + x - y - z) da_dx, db_dx = 2*(w + x + y + z), 2*(w + x - y - z) da_dy, db_dy = 2*(w + x + y + z), -2*(w + x - y - z) da_dz, db_dz = 2*(w + x + y + z), -2*(w + x - y - z) # Now compute how f changes when we change any value from output to input df_da, df_db = df_du*du_da + df_dv*dv_da, df_du*du_db + df_dv*dv_db df_dw, df_dx = df_da*da_dw + df_db*db_dw, df_da*da_dx + df_db*db_dx df_dy, df_dz = df_da*da_dy + df_db*db_dy, df_da*da_dz + df_db*db_dz print(f'df/dw at {w}, {x}, {y}, {z} is {df_dw}') print(f'df/dx at {w}, {x}, {y}, {z} is {df_dx}') print(f'df/dy at {w}, {x}, {y}, {z} is {df_dy}') print(f'df/dz at {w}, {x}, {y}, {z} is {df_dz}') Тот факт, что мы вычисляем производные от :math:`f` в направлении к входам, а не от входов к выходам (как в первом фрагменте кода выше), и даёт этому алгоритму название: *обратное распространение ошибки*. Обратите внимание, что здесь есть два этапа: 1. Вычислите значение функции и частные производные на каждом шаге от входа к выходу. Хотя это не было сделано в приведённом выше примере, это можно объединить в один *прямой проход*. 2. Вычислите градиент :math:`f` от конца к началу. Этот этап мы называем *обратным проходом*. Именно это и реализует каждый алгоритм глубокого обучения, позволяя вычислить градиент функции потерь по всем весам сети за один проход. Поразительно, что существует такое разложение. Чтобы понять, как это инкапсулировать, давайте быстро рассмотрим этот пример. .. raw:: latex \diilbookstyleinputcell .. code:: python # Initialize as ndarrays, then attach gradients w = torch.tensor([-1.], requires_grad=True) x = torch.tensor([0.], requires_grad=True) y = torch.tensor([-2.], requires_grad=True) z = torch.tensor([1.], requires_grad=True) # Do the computation like usual, tracking gradients a, b = (w + x + y + z)**2, (w + x - y - z)**2 u, v = (a + b)**2, (a - b)**2 f = (u + v)**2 # Execute backward pass f.backward() print(f'df/dw at {w.data.item()}, {x.data.item()}, {y.data.item()}, ' f'{z.data.item()} is {w.grad.data.item()}') print(f'df/dx at {w.data.item()}, {x.data.item()}, {y.data.item()}, ' f'{z.data.item()} is {x.grad.data.item()}') print(f'df/dy at {w.data.item()}, {x.data.item()}, {y.data.item()}, ' f'{z.data.item()} is {y.grad.data.item()}') print(f'df/dz at {w.data.item()}, {x.data.item()}, {y.data.item()}, ' f'{z.data.item()} is {z.grad.data.item()}') Всё, что мы сделали выше, можно автоматизировать, вызвав ``f.backwards()``. Гессианы -------- Как и в одномерном анализе, полезно рассмотреть производные более высокого порядка, чтобы лучше понять, как можно получить более точное приближение функции, чем при использовании одного лишь градиента. При работе с производными высшего порядка функций нескольких переменных возникает одна очевидная проблема: их очень много. Если у нас есть функция :math:`f(x_1, \ldots, x_n)` от :math:`n` переменных, то мы можем вычислить :math:`n^{2}` вторых производных, а именно для любых выбора :math:`i` и :math:`j`: .. math:: \frac{d^2f}{dx_idx_j} = \frac{d}{dx_i}\left(\frac{d}{dx_j}f\right). Это традиционно собирают в матрицу, называемую *гессианом*: .. math:: \mathbf{H}_f = \begin{bmatrix} \frac{d^2f}{dx_1dx_1} & \cdots & \frac{d^2f}{dx_1dx_n} \\ \vdots & \ddots & \vdots \\ \frac{d^2f}{dx_ndx_1} & \cdots & \frac{d^2f}{dx_ndx_n} \\ \end{bmatrix}. :label: eq_hess_def Не все элементы этой матрицы независимы. Действительно, можно показать, что если *смешанные частные производные* (частные производные по более чем одной переменной) существуют и непрерывны, то для любых :math:`i` и :math:`j` выполняется следующее: .. math:: \frac{d^2f}{dx_idx_j} = \frac{d^2f}{dx_jdx_i}. Это следует из того, что сначала функция возмущается в направлении :math:`x_i`, а затем в направлении :math:`x_j`, после чего полученный результат сравнивается с тем, что происходит, если сначала возмущать в направлении :math:`x_j`, а затем в направлении :math:`x_i`, при этом известно, что оба этих порядка приводят к одному и тому же конечному изменению выходных данных :math:`f`. Как и в случае с одной переменной, мы можем использовать эти производные, чтобы получить гораздо более точное представление о поведении функции в окрестности точки. В частности, мы можем использовать их для нахождения наилучшей квадратичной аппроксимации вблизи точки :math:`\mathbf{x}_0`, как мы видели для случая одной переменной. Рассмотрим пример. Предположим, что :math:`f(x_1, x_2) = a + b_1x_1 + b_2x_2 + c_{11}x_1^{2} + c_{12}x_1x_2 + c_{22}x_2^{2}`. Это общая форма квадратичной функции от двух переменных. Если мы посмотрим на значение функции, её градиент и гессиан :eq:`eq_hess_def` в точке ноль: .. math:: \begin{aligned} f(0,0) & = a, \\ \nabla f (0,0) & = \begin{bmatrix}b_1 \\ b_2\end{bmatrix}, \\ \mathbf{H} f (0,0) & = \begin{bmatrix}2 c_{11} & c_{12} \\ c_{12} & 2c_{22}\end{bmatrix}, \end{aligned} мы можем восстановить наш исходный многочлен, записав .. math:: f(\mathbf{x}) = f(0) + \nabla f (0) \cdot \mathbf{x} + \frac{1}{2}\mathbf{x}^\top \mathbf{H} f (0) \mathbf{x}. В общем случае, если мы вычислим это разложение в любой точке :math:`\mathbf{x}_0`, то увидим, что .. math:: f(\mathbf{x}) = f(\mathbf{x}_0) + \nabla f (\mathbf{x}_0) \cdot (\mathbf{x}-\mathbf{x}_0) + \frac{1}{2}(\mathbf{x}-\mathbf{x}_0)^\top \mathbf{H} f (\mathbf{x}_0) (\mathbf{x}-\mathbf{x}_0). Это работает для входных данных любой размерности и обеспечивает наилучшее квадратичное приближение любой функции в заданной точке. В качестве примера построим график функции .. math:: f(x, y) = xe^{-x^2-y^2}. Можно вычислить, что градиент и гессиан равны .. math:: \nabla f(x, y) = e^{-x^2-y^2}\begin{pmatrix}1-2x^2 \\ -2xy\end{pmatrix} \; \textrm{and} \; \mathbf{H}f(x, y) = e^{-x^2-y^2}\begin{pmatrix} 4x^3 - 6x & 4x^2y - 2y \\ 4x^2y-2y &4xy^2-2x\end{pmatrix}. Итак, с помощью несложных алгебраических преобразований убеждаемся, что аппроксимирующий квадратичный многочлен в точке :math:`[-1,0]^\top` имеет вид .. math:: f(x, y) \approx e^{-1}\left(-1 - (x+1) +(x+1)^2+y^2\right). .. raw:: latex \diilbookstyleinputcell .. code:: python # Construct grid and compute function x, y = torch.meshgrid(torch.linspace(-2, 2, 101), torch.linspace(-2, 2, 101)) z = x*torch.exp(- x**2 - y**2) # Compute approximating quadratic with gradient and Hessian at (1, 0) w = torch.exp(torch.tensor([-1.]))*(-1 - (x + 1) + 2 * (x + 1)**2 + 2 * y**2) # Plot function ax = d2l.plt.figure().add_subplot(111, projection='3d') ax.plot_wireframe(x.numpy(), y.numpy(), z.numpy(), **{'rstride': 10, 'cstride': 10}) ax.plot_wireframe(x.numpy(), y.numpy(), w.numpy(), **{'rstride': 10, 'cstride': 10}, color='purple') d2l.plt.xlabel('x') d2l.plt.ylabel('y') d2l.set_figsize() ax.set_xlim(-2, 2) ax.set_ylim(-2, 2) ax.set_zlim(-1, 1) ax.dist = 12 Это лежит в основе алгоритма Ньютона, рассмотренного в :numref:`sec_gd`, где мы итеративно выполняем оптимизацию, находя наилучшую аппроксимирующую квадратичную функцию, а затем точно минимизируя её. Немного матричного исчисления ----------------------------- Производные функций, содержащих матрицы, оказываются особенно удобными. Этот раздел может показаться перегруженным обозначениями, поэтому его можно пропустить при первом чтении, однако полезно знать, что производные функций, включающих распространенные матричные операции, часто оказываются гораздо более лаконичными, чем можно было бы предположить изначально, особенно учитывая центральную роль матричных операций в приложениях глубокого обучения. Начнем с примера. Предположим, у нас есть некоторый фиксированный вектор-столбец :math:`\boldsymbol{\beta}`, и мы хотим рассмотреть функцию произведения :math:`f(\mathbf{x}) = \boldsymbol{\beta}^\top\mathbf{x}` и понять, как изменяется скалярное произведение при изменении :math:`\mathbf{x}`. Для работы с матричными производными в машинном обучении полезно использовать нотацию, называемую *матричной производной с компоновкой по знаменателю* (denominator layout). В этом подходе частные производные собираются в форму того вектора, матрицы или тензора, который находится в знаменателе дифференциала. В этом случае мы запишем .. math:: \frac{df}{d\mathbf{x}} = \begin{bmatrix} \frac{df}{dx_1} \\ \vdots \\ \frac{df}{dx_n} \end{bmatrix}, где мы сопоставили форму столбцового вектора :math:`\mathbf{x}`. Если мы раскроем нашу функцию по компонентам, то получим .. math:: f(\mathbf{x}) = \sum_{i = 1}^{n} \beta_ix_i = \beta_1x_1 + \cdots + \beta_nx_n. Если теперь взять частную производную по, скажем, :math:`\beta_1`, заметим, что все слагаемые равны нулю, кроме первого, которое представляет собой произведение :math:`x_1` на :math:`\beta_1`, поэтому получаем, что .. math:: \frac{df}{dx_1} = \beta_1, или, более общо, что .. math:: \frac{df}{dx_i} = \beta_i. Теперь мы можем собрать это в матрицу, чтобы увидеть .. math:: \frac{df}{d\mathbf{x}} = \begin{bmatrix} \frac{df}{dx_1} \\ \vdots \\ \frac{df}{dx_n} \end{bmatrix} = \begin{bmatrix} \beta_1 \\ \vdots \\ \beta_n \end{bmatrix} = \boldsymbol{\beta}. Это иллюстрирует несколько аспектов матричного исчисления, с которыми мы будем часто сталкиваться в этом разделе: - Во-первых, вычисления будут довольно сложными. - Во-вторых, итоговые результаты значительно чище промежуточных вычислений и всегда выглядят аналогично случаю одной переменной. В данном случае отметим, что :math:`\frac{d}{dx}(bx) = b` и :math:`\frac{d}{d\mathbf{x}} (\boldsymbol{\beta}^\top\mathbf{x}) = \boldsymbol{\beta}` также похожи. - В-третьих, транспонирование часто возникает seemingly из ниоткуда. Основная причина этого заключается в соглашении, согласно которому форма результата должна совпадать с формой знаменателя. Поэтому при умножении матриц нам приходится использовать транспонирование, чтобы вернуть форму к виду исходного выражения. Чтобы продолжать развивать интуицию, давайте попробуем вычисление, которое немного сложнее. Предположим, у нас есть вектор :math:`\mathbf{x}` и квадратная матрица :math:`A`, и мы хотим вычислить .. math:: \frac{d}{d\mathbf{x}}(\mathbf{x}^\top A \mathbf{x}). :label: eq_mat_goal_1 Чтобы перейти к более удобной для манипуляций записи, рассмотрим эту задачу с использованием нотации Эйнштейна. В этом случае мы можем записать функцию как .. math:: \mathbf{x}^\top A \mathbf{x} = x_ia_{ij}x_j. Чтобы вычислить нашу производную, нам нужно понять, каково значение для каждого :math:`k` .. math:: \frac{d}{dx_k}(\mathbf{x}^\top A \mathbf{x}) = \frac{d}{dx_k}x_ia_{ij}x_j. Согласно правилу произведения, это .. math:: \frac{d}{dx_k}x_ia_{ij}x_j = \frac{dx_i}{dx_k}a_{ij}x_j + x_ia_{ij}\frac{dx_j}{dx_k}. Для такого члена, как :math:`\frac{dx_i}{dx_k}`, нетрудно видеть, что он равен единице, когда :math:`i=k`, и нулю в остальных случаях. Это означает, что все члены, в которых :math:`i` и :math:`k` различны, исчезают из этой суммы, так что в первой сумме остаются только те члены, в которых :math:`i=k`. Те же рассуждения справедливы и для второго члена, где требуется :math:`j=k`. Это даёт .. math:: \frac{d}{dx_k}x_ia_{ij}x_j = a_{kj}x_j + x_ia_{ik}. Имена индексов в нотации Эйнштейна произвольны — тот факт, что :math:`i` и :math:`j` различны, не имеет значения для вычислений на данном этапе, поэтому мы можем переиндексировать их так, чтобы оба использовали :math:`i`, и увидеть, что .. math:: \frac{d}{dx_k}x_ia_{ij}x_j = a_{ki}x_i + x_ia_{ik} = (a_{ki} + a_{ik})x_i. Теперь нам потребуется некоторая практика, чтобы продвинуться дальше. Попробуем выразить этот результат через матричные операции. :math:`a_{ki} + a_{ik}` — это :math:`k, i`-я компонента :math:`\mathbf{A} + \mathbf{A}^\top`. Это дает .. math:: \frac{d}{dx_k}x_ia_{ij}x_j = [\mathbf{A} + \mathbf{A}^\top]_{ki}x_i. Аналогично, этот член представляет собой произведение матрицы :math:`\mathbf{A} + \mathbf{A}^\top` на вектор :math:`\mathbf{x}`, поэтому мы видим, что .. math:: \left[\frac{d}{d\mathbf{x}}(\mathbf{x}^\top A \mathbf{x})\right]_k = \frac{d}{dx_k}x_ia_{ij}x_j = [(\mathbf{A} + \mathbf{A}^\top)\mathbf{x}]_k. Таким образом, мы видим, что :math:`k`-я компонента искомой производной от :eq:`eq_mat_goal_1` совпадает с :math:`k`-й компонентой вектора справа, и поэтому они равны. Отсюда следует, что .. math:: \frac{d}{d\mathbf{x}}(\mathbf{x}^\top A \mathbf{x}) = (\mathbf{A} + \mathbf{A}^\top)\mathbf{x}. Это потребовало значительно больше усилий, чем предыдущий пример, но итоговый результат оказался компактным. Более того, рассмотрим следующее вычисление для традиционных производных одной переменной: .. math:: \frac{d}{dx}(xax) = \frac{dx}{dx}ax + xa\frac{dx}{dx} = (a+a)x. Эквивалентно :math:`\frac{d}{dx}(ax^2) = 2ax = (a+a)x`. Мы снова получаем результат, который выглядит очень похоже на результат для функции одной переменной, но с транспонированием. На этом этапе закономерность выглядит довольно подозрительно, поэтому давайте разберемся, почему это так. Когда мы вычисляем производные матриц таким образом, сначала предположим, что результатом будет ещё одно матричное выражение: выражение, которое можно записать через произведения и суммы матриц и их транспонированные версии. Если такое выражение существует, оно должно быть верным для всех матриц. В частности, оно должно быть верным для :math:`1 \times 1` матриц, в случае которых матричное произведение сводится к произведению чисел, матричная сумма — к сумме, а транспонирование вообще ничего не меняет! Иными словами, любое полученное нами выражение *обязано* совпадать с выражением для одной переменной. Это означает, что с некоторой практикой можно часто угадывать производные матриц, просто зная, как должно выглядеть соответствующее выражение для одной переменной! Давайте попробуем. Предположим, что :math:`\mathbf{X}` — это матрица размера :math:`n \times m`, :math:`\mathbf{U}` — матрица размера :math:`n \times r`, а :math:`\mathbf{V}` — матрица размера :math:`r \times m`. Попробуем вычислить .. math:: \frac{d}{d\mathbf{V}} \|\mathbf{X} - \mathbf{U}\mathbf{V}\|_2^{2} = \;? :label: eq_mat_goal_2 Это вычисление важно в области, называемой факторизацией матриц. Однако для нас это просто производная, которую нужно вычислить. Попробуем представить, как это будет выглядеть для матриц :math:`1\times1`. В этом случае мы получаем выражение .. math:: \frac{d}{dv} (x-uv)^{2}= -2(x-uv)u, где производная является довольно стандартной. Если мы попробуем преобразовать это обратно в матричное выражение, то получим .. math:: \frac{d}{d\mathbf{V}} \|\mathbf{X} - \mathbf{U}\mathbf{V}\|_2^{2}= -2(\mathbf{X} - \mathbf{U}\mathbf{V})\mathbf{U}. Однако при более внимательном рассмотрении выясняется, что это не совсем так. Напомним, что :math:`\mathbf{X}` равно :math:`n \times m`, как и :math:`\mathbf{U}\mathbf{V}`, поэтому матрица :math:`2(\mathbf{X} - \mathbf{U}\mathbf{V})` равна :math:`n \times m`. С другой стороны, :math:`\mathbf{U}` равно :math:`n \times r`, и мы не можем умножить матрицу :math:`n \times m` на матрицу :math:`n \times r`, поскольку их размеры не совпадают! Нам нужно получить :math:`\frac{d}{d\mathbf{V}}`, который имеет ту же форму, что и :math:`\mathbf{V}`, то есть :math:`r \times m`. Следовательно, нам каким-то образом необходимо перемножить матрицу :math:`n \times m` и матрицу :math:`n \times r` (возможно, с транспонированием), чтобы получить :math:`r \times m`. Это можно сделать, умножив :math:`U^\top` на :math:`(\mathbf{X} - \mathbf{U}\mathbf{V})`. Таким образом, мы можем предположить, что решением уравнения :eq:`eq_mat_goal_2` является .. math:: \frac{d}{d\mathbf{V}} \|\mathbf{X} - \mathbf{U}\mathbf{V}\|_2^{2}= -2\mathbf{U}^\top(\mathbf{X} - \mathbf{U}\mathbf{V}). Чтобы показать, что это работает, было бы упущением не привести подробные вычисления. Если вы уже считаете, что это эмпирическое правило работает, можете смело пропустить данный вывод. Для вычисления .. math:: \frac{d}{d\mathbf{V}} \|\mathbf{X} - \mathbf{U}\mathbf{V}\|_2^2, нам необходимо найти для каждого :math:`a` и :math:`b` .. math:: \frac{d}{dv_{ab}} \|\mathbf{X} - \mathbf{U}\mathbf{V}\|_2^{2}= \frac{d}{dv_{ab}} \sum_{i, j}\left(x_{ij} - \sum_k u_{ik}v_{kj}\right)^2. Поскольку все элементы :math:`\mathbf{X}` и :math:`\mathbf{U}` являются константами с точки зрения :math:`\frac{d}{dv_{ab}}`, мы можем вынести производную за знак суммы и применить цепное правило к квадрату, чтобы получить .. math:: \frac{d}{dv_{ab}} \|\mathbf{X} - \mathbf{U}\mathbf{V}\|_2^{2}= \sum_{i, j}2\left(x_{ij} - \sum_k u_{ik}v_{kj}\right)\left(-\sum_k u_{ik}\frac{dv_{kj}}{dv_{ab}} \right). Как и в предыдущем выводе, отметим, что :math:`\frac{dv_{kj}}{dv_{ab}}` отличен от нуля только при выполнении условий :math:`k=a` и :math:`j=b`. Если хотя бы одно из этих условий не выполняется, слагаемое в сумме равно нулю, и мы можем смело его отбросить. Таким образом, получаем: .. math:: \frac{d}{dv_{ab}} \|\mathbf{X} - \mathbf{U}\mathbf{V}\|_2^{2}= -2\sum_{i}\left(x_{ib} - \sum_k u_{ik}v_{kb}\right)u_{ia}. Важный нюанс заключается в том, что :math:`k=a` не входит во внутреннюю сумму, поскольку :math:`k` — это фиктивная переменная, по которой мы суммируем в пределах внутреннего слагаемого. Для более наглядного примера рассмотрим, почему .. math:: \frac{d}{dx_1} \left(\sum_i x_i \right)^{2}= 2\left(\sum_i x_i \right). С этого момента мы можем приступить к выделению компонентов суммы. Во-первых, .. math:: \sum_k u_{ik}v_{kb} = [\mathbf{U}\mathbf{V}]_{ib}. Таким образом, всё выражение внутри суммы равно .. math:: x_{ib} - \sum_k u_{ik}v_{kb} = [\mathbf{X}-\mathbf{U}\mathbf{V}]_{ib}. Это означает, что теперь мы можем записать производную в виде .. math:: \frac{d}{dv_{ab}} \|\mathbf{X} - \mathbf{U}\mathbf{V}\|_2^{2}= -2\sum_{i}[\mathbf{X}-\mathbf{U}\mathbf{V}]_{ib}u_{ia}. Мы хотим, чтобы это выглядело как элемент :math:`a, b` матрицы, чтобы мы могли использовать метод, аналогичный предыдущему примеру, и получить матричное выражение. Это означает, что нам нужно поменять порядок индексов в :math:`u_{ia}`. Заметив, что :math:`u_{ia} = [\mathbf{U}^\top]_{ai}`, мы можем записать .. math:: \frac{d}{dv_{ab}} \|\mathbf{X} - \mathbf{U}\mathbf{V}\|_2^{2}= -2\sum_{i} [\mathbf{U}^\top]_{ai}[\mathbf{X}-\mathbf{U}\mathbf{V}]_{ib}. Это матричное произведение, следовательно, мы можем заключить, что .. math:: \frac{d}{dv_{ab}} \|\mathbf{X} - \mathbf{U}\mathbf{V}\|_2^{2}= -2[\mathbf{U}^\top(\mathbf{X}-\mathbf{U}\mathbf{V})]_{ab}. и, следовательно, мы можем записать решение для :eq:`eq_mat_goal_2` .. math:: \frac{d}{d\mathbf{V}} \|\mathbf{X} - \mathbf{U}\mathbf{V}\|_2^{2}= -2\mathbf{U}^\top(\mathbf{X} - \mathbf{U}\mathbf{V}). Это совпадает с решением, которое мы предположили выше! Уместно спросить: «Почему бы просто не записать матричные версии всех правил дифференциального исчисления, которые я выучил? Это всё ещё механическая работа. Почему бы просто не сделать это раз и навсегда!» И действительно, такие правила существуют, а :cite:`Petersen.Pedersen.ea.2008` предоставляет их отличный обзор. Однако из-за множества способов комбинирования матричных операций по сравнению с одиночными значениями правил дифференцирования матриц гораздо больше, чем для функций одной переменной. Часто оказывается, что лучше всего работать с индексами или полагаться на автоматическое дифференцирование, когда это уместно. Итоги ----- - В многомерном случае мы можем определить градиенты, которые выполняют ту же роль, что и производные в одномерном случае. Они позволяют нам понять, как изменяется функция многих переменных при произвольном малом изменении входных данных. - Алгоритм обратного распространения ошибки можно рассматривать как метод организации применения цепного правила для функций многих переменных, что позволяет эффективно вычислять множество частных производных. - Матричное исчисление позволяет записывать производные матричных выражений в компактной форме. Упражнения ---------- 1. Для заданного вектора-столбца :math:`\boldsymbol{\beta}` вычислите производные от :math:`f(\mathbf{x}) = \boldsymbol{\beta}^\top\mathbf{x}` и :math:`g(\mathbf{x}) = \mathbf{x}^\top\boldsymbol{\beta}`. Почему вы получаете одинаковый ответ? 2. Пусть :math:`\mathbf{v}` будет вектором размерности :math:`n`. Чему равно :math:`\frac{\partial}{\partial\mathbf{v}}\|\mathbf{v}\|_2`? 3. Пусть :math:`L(x, y) = \log(e^x + e^y)`. Вычислите градиент. Какова сумма компонент градиента? 4. Пусть :math:`f(x, y) = x^2y + xy^2`. Покажите, что единственной критической точкой является :math:`(0,0)`. Проанализировав :math:`f(x, x)`, определите, является ли :math:`(0,0)` максимумом, минимумом или ни тем, ни другим. 5. Предположим, что мы минимизируем функцию :math:`f(\mathbf{x}) = g(\mathbf{x}) + h(\mathbf{x})`. Как можно геометрически интерпретировать условие :math:`\nabla f = 0` с помощью :math:`g` и :math:`h`? `Обсуждения `__