.. _sec_multivariable_calculus:
Cálculo Multivariável
=====================
Agora que temos um entendimento bastante forte das derivadas de uma
função de uma única variável, vamos voltar à nossa questão original,
onde estávamos considerando uma função de perda de potencialmente
bilhões de pesos.
Diferenciação de Dimensões Superiores
-------------------------------------
O que :numref:`sec_single_variable_calculus` nos diz é que se mudarmos
um desses bilhões de pesos deixando todos os outros fixos, sabemos o que
vai acontecer! Isso nada mais é do que uma função de uma única variável,
então podemos escrever
.. 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
Chamaremos a derivada em uma variável enquanto fixamos a outra *derivada
parcial*, e usaremos a notação :math:`\frac{\partial}{\partial w_1}`
para a derivada em :eq:`eq_part_der`.
Agora, vamos pegar isso e mudar $ w_2 $ um pouco para
: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}
Usamos novamente a ideia de que :math:`\epsilon_1\epsilon_2` é um termo
de ordem superior que podemos descartar da mesma forma que descartamos
:math:`\epsilon^{2}` na seção anterior, junto com o que vimos em
:eq:`eq_part_der`. Continuando dessa maneira, podemos escrever que
.. 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).
Isso pode parecer uma bagunça, mas podemos tornar isso mais familiar
observando que a soma à direita parece exatamente com um produto
escalar, então, se deixarmos
.. math::
\boldsymbol{\epsilon} = [\epsilon_1, \ldots, \epsilon_N]^\top \; \text{and} \;
\nabla_{\mathbf{x}} L = \left[\frac{\partial L}{\partial x_1}, \ldots, \frac{\partial L}{\partial x_N}\right]^\top,
então
.. math:: L(\mathbf{w} + \boldsymbol{\epsilon}) \approx L(\mathbf{w}) + \boldsymbol{\epsilon}\cdot \nabla_{\mathbf{w}} L(\mathbf{w}).
:label: eq_nabla_use
Chamaremos o vetor :math:`\nabla_{\mathbf{w}} L` de *gradiente* de
:math:`L`.
Equação :eq:`eq_nabla_use` vale a pena ponderar por um momento. Tem
exatamente o formato que encontramos em uma dimensão, apenas convertemos
tudo para vetores e produtos escalares. Isso nos permite dizer
aproximadamente como a função :math:`L` mudará dada qualquer perturbação
na entrada. Como veremos na próxima seção, isso nos fornecerá uma
ferramenta importante para compreender geometricamente como podemos
aprender usando as informações contidas no gradiente.
Mas, primeiro, vejamos essa aproximação em funcionamento com um exemplo.
Suponha que estejamos trabalhando com a função
.. math::
f(x, y) = \log(e^x + e^y) \text{ with gradient } \nabla f (x, y) = \left[\frac{e^x}{e^x+e^y}, \frac{e^y}{e^x+e^y}\right].
Se olharmos para um ponto como :math:`(0, \log(2))`, vemos que
.. math::
f(x, y) = \log(3) \text{ with gradient } \nabla f (x, y) = \left[\frac{1}{3}, \frac{2}{3}\right].
Assim, se quisermos aproximar :math:`f` em
:math:`(\epsilon_1, \log(2) + \epsilon_2)`, vemos que devemos ter a
instância específica de :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.
Podemos testar isso no código para ver o quão boa é a aproximação.
.. raw:: html
.. raw:: html
.. code:: python
%matplotlib inline
from IPython import display
from mpl_toolkits import mplot3d
from mxnet import autograd, np, npx
from d2l import mxnet as d2l
npx.set_np()
def f(x, y):
return np.log(np.exp(x) + np.exp(y))
def grad_f(x, y):
return np.array([np.exp(x) / (np.exp(x) + np.exp(y)),
np.exp(y) / (np.exp(x) + np.exp(y))])
epsilon = np.array([0.01, -0.03])
grad_approx = f(0, np.log(2)) + epsilon.dot(grad_f(0, np.log(2)))
true_value = f(0 + epsilon[0], np.log(2) + epsilon[1])
f'approximation: {grad_approx}, true Value: {true_value}'
.. parsed-literal::
:class: output
'approximation: 1.0819456577301025, true Value: 1.0821242332458496'
.. raw:: html
.. raw:: html
.. 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}'
.. parsed-literal::
:class: output
'approximation: tensor([1.0819]), true Value: tensor([1.0821])'
.. raw:: html
.. raw:: html
.. code:: python
%matplotlib inline
import numpy as np
import tensorflow as tf
from IPython import display
from mpl_toolkits import mplot3d
from d2l import tensorflow as d2l
def f(x, y):
return tf.math.log(tf.exp(x) + tf.exp(y))
def grad_f(x, y):
return tf.constant([(tf.exp(x) / (tf.exp(x) + tf.exp(y))).numpy(),
(tf.exp(y) / (tf.exp(x) + tf.exp(y))).numpy()])
epsilon = tf.constant([0.01, -0.03])
grad_approx = f(tf.constant([0.]), tf.math.log(
tf.constant([2.]))) + tf.tensordot(
epsilon, grad_f(tf.constant([0.]), tf.math.log(tf.constant(2.))), axes=1)
true_value = f(tf.constant([0.]) + epsilon[0], tf.math.log(
tf.constant([2.])) + epsilon[1])
f'approximation: {grad_approx}, true Value: {true_value}'
.. parsed-literal::
:class: output
'approximation: [1.0819457], true Value: [1.0821242]'
.. raw:: html
.. raw:: html
Geometria de Gradientes e Gradiente Descendente
-----------------------------------------------
Considere novamente :eq:`eq_nabla_use`:
.. math::
L(\mathbf{w} + \boldsymbol{\epsilon}) \approx L(\mathbf{w}) + \boldsymbol{\epsilon}\cdot \nabla_{\mathbf{w}} L(\mathbf{w}).
Suponhamos que eu queira usar isso para ajudar a minimizar nossa perda
:math:`L`. Vamos entender geometricamente o algoritmo de gradiente
descendente descrito pela primeira vez em :numref:`sec_autograd`. O
que faremos é o seguinte:
1. Comece com uma escolha aleatória para os parâmetros iniciais
:math:`\mathbf{w}`.
2. Encontre a direção :math:`\mathbf{v}` que faz :math:`L` diminuir mais
rapidamente em :math:`\mathbf{w}`.
3. Dê um pequeno passo nessa direção:
:math:`\mathbf{w} \rightarrow \mathbf{w} + \epsilon\mathbf{v}`.
4. Repita.
A única coisa que não sabemos exatamente como fazer é calcular o vetor
:math:`\mathbf{v}` no segundo passo. Chamaremos tal direção de *direção
da descida mais íngreme*. Usando o entendimento geométrico de produtos
escalares de :numref:`sec_geometry-linear-algebraic-ops`, vemos que
podemos reescrever :eq:`eq_nabla_use` como
.. 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).
Observe que seguimos nossa orientação para ter comprimento um por
conveniência, e usamos :math:`\theta` para o ângulo entre
:math:`\mathbf{v}` e :math:`\nabla_{\mathbf{w}} L(\mathbf{w})` Se
quisermos encontrar a direção que diminui :math:`L` o mais rápido
possível, queremos tornar essa expressão o mais negativa possível. A
única maneira pela qual a direção que escolhemos entra nesta equação é
através de :math:`\cos(\theta)` e, portanto, desejamos tornar esse
cosseno o mais negativo possível. Agora, relembrando a forma do cosseno,
podemos torná-lo o mais negativo possível, tornando
:math:`\cos(\theta) = -1` ou equivalentemente tornando o ângulo entre o
gradiente e nossa direção escolhida em :math:`\pi` radianos, ou
equivalentemente :math:`180` graus. A única maneira de conseguir isso é
seguir na direção oposta exata: escolha :math:`\mathbf{v}` para apontar
na direção oposta exata para :math:`\nabla_{\mathbf{w}} L(\mathbf{w})`!
Isso nos leva a um dos conceitos matemáticos mais importantes no
aprendizado de máquina: a direção dos pontos decentes mais íngremes na
direção de :math:`-\nabla_{\mathbf{w}}L(\mathbf{w})`. Assim, nosso
algoritmo informal pode ser reescrito da seguinte maneira.
1. Comece com uma escolha aleatória para os parâmetros iniciais
:math:`\mathbf{w}`.
2. Calcule :math:`\nabla_{\mathbf{w}} L(\mathbf{w})`.
3. Dê um pequeno passo na direção oposta:
:math:`\mathbf{w} \rightarrow \mathbf{w} - \epsilon\nabla_{\mathbf{w}} L(\mathbf{w})`.
4. Repita.
Este algoritmo básico foi modificado e adaptado de várias maneiras por
muitos pesquisadores, mas o conceito central permanece o mesmo em todos
eles. Usar o gradiente para encontrar a direção que diminui a perda o
mais rápido possível e atualizar os parâmetros para dar um passo nessa
direção.
Uma Nota Sobre Otimização Matemática
------------------------------------
Ao longo deste livro, enfocamos diretamente as técnicas de otimização
numérica pela razão prática de que todas as funções que encontramos no
ambiente de aprendizado profundo são muito complexas para serem
minimizadas explicitamente.
No entanto, é um exercício útil considerar o que a compreensão
geométrica que obtivemos acima nos diz sobre como otimizar funções
diretamente.
Suponha que desejamos encontrar o valor de :math:`\mathbf{x}_0` que
minimiza alguma função :math:`L(\mathbf{x})`. Suponhamos que, além
disso, alguém nos dê um valor e nos diga que é o valor que minimiza
:math:`L`. Existe algo que possamos verificar para ver se a resposta
deles é plausível?
Considere novamente :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).
Se o gradiente não for zero, sabemos que podemos dar um passo na direção
:math:`-\epsilon \nabla_{\mathbf{x}} L(\mathbf{x}_0)` para encontrar um
valor de :math:`L` que é menor. Portanto, se realmente estamos no
mínimo, não pode ser esse o caso! Podemos concluir que se
:math:`\mathbf{x}_0` é um mínimo, então
:math:`\nabla_{\mathbf{x}} L(\mathbf{x}_0) = 0`. Chamamos pontos com
:math:`\nabla_{\mathbf{x}} L(\mathbf{x}_0) = 0` *pontos críticos*.
Isso é bom, porque em algumas configurações raras, nós *podemos*
encontrar explicitamente todos os pontos onde o gradiente é zero e
encontrar aquele com o menor valor.
Para um exemplo concreto, considere a função
.. math::
f(x) = 3x^4 - 4x^3 -12x^2.
Esta função tem derivada
.. math::
\frac{df}{dx} = 12x^3 - 12x^2 -24x = 12x(x-2)(x+1).
A única localização possível dos mínimos está em :math:`x = -1, 0, 2`,
onde a função assume os valores :math:`-5,0, -32` respectivamente, e
assim podemos concluir que minimizamos nossa função quando
:math:`x = 2`. Um gráfico rápido confirma isso.
.. raw:: html
.. raw:: html
.. code:: python
x = np.arange(-2, 3, 0.01)
f = (3 * x**4) - (4 * x**3) - (12 * x**2)
d2l.plot(x, f, 'x', 'f(x)')
.. figure:: output_multivariable-calculus_bf678a_15_0.svg
.. raw:: html
.. raw:: html
.. 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)')
.. figure:: output_multivariable-calculus_bf678a_18_0.svg
.. raw:: html
.. raw:: html
.. code:: python
x = tf.range(-2, 3, 0.01)
f = (3 * x**4) - (4 * x**3) - (12 * x**2)
d2l.plot(x, f, 'x', 'f(x)')
.. figure:: output_multivariable-calculus_bf678a_21_0.svg
.. raw:: html
.. raw:: html
Isso destaca um fato importante a saber ao trabalhar teoricamente ou
numericamente: os únicos pontos possíveis onde podemos minimizar (ou
maximizar) uma função terão gradiente igual a zero, no entanto, nem todo
ponto com gradiente zero é o verdadeiro *global* mínimo (ou máximo).
Regra da Cadeia Multivariada
----------------------------
Vamos supor que temos uma função de quatro variáveis (:math:`w, x, y`, e
:math:`z`) que podemos fazer compondo muitos termos:
.. 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
Essas cadeias de equações são comuns ao trabalhar com redes neurais,
portanto, tentar entender como calcular gradientes de tais funções é
fundamental. Podemos começar a ver dicas visuais dessa conexão em
:numref:`fig_chain-1` se dermos uma olhada em quais variáveis se
relacionam diretamente entre si.
.. _fig_chain-1:
.. figure:: ../img/chain-net1.svg
As relações de função acima, onde os nós representam valores e as
arestas mostram dependência funcional.
Nada nos impede de apenas compor tudo de :eq:`eq_multi_func_def` e
escrever isso
.. 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.
Podemos então tirar a derivada usando apenas derivadas de variável
única, mas se fizéssemos isso, rapidamente nos veríamos inundados com
termos, muitos dos quais são repetições! Na verdade, pode-se ver que,
por exemplo:
.. 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}
Se também quiséssemos calcular :math:`\frac{\partial f}{\partial x}`,
acabaríamos com uma equação semelhante novamente com muitos termos
repetidos e muitos termos repetidos *compartilhados* entre as duas
derivadas. Isso representa uma enorme quantidade de trabalho
desperdiçado e, se precisássemos calcular as derivadas dessa forma, toda
a revolução do aprendizado profundo teria estagnado antes de começar!
Vamos resolver o problema. Começaremos tentando entender como :math:`f`
muda quando mudamos :math:`a`, essencialmente supondo que
:math:`w, x, y`, e :math:`z` não existem. Vamos raciocinar como fazíamos
quando trabalhamos com gradiente pela primeira vez. Vamos pegar
:math:`a` e adicionar uma pequena quantia :math:`\epsilon` a ele.
.. 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}
A primeira linha segue da definição de derivada parcial e a segunda
segue da definição de gradiente. É notacionalmente pesado rastrear
exatamente onde avaliamos cada derivada, como na expressão
:math:`\frac{\partial f}{\partial u}(u(a, b), v(a, b))`, então
frequentemente abreviamos para muito mais memorável
.. 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}.
É útil pensar sobre o significado do processo. Estamos tentando entender
como uma função da forma :math:`f(u(a, b), v(a, b))` muda seu valor com
uma mudança em :math:`a`. Isso pode ocorrer de duas maneiras: há o
caminho onde :math:`a \rightarrow u \rightarrow f` e onde
:math:`a \rightarrow v \rightarrow f`. Podemos calcular ambas as
contribuições por meio da regra da cadeia:
:math:`\frac{\partial w}{\partial u} \cdot \frac{\partial u}{\partial x}`
e
:math:`\frac{\partial w}{\partial v} \cdot \frac{\partial v}{\partial x}`
respectivamente, e somados.
Imagine que temos uma rede diferente de funções onde as funções à
direita dependem daquelas que estão conectadas à esquerda, como mostrado
em :numref:`fig_chain-2`.
.. _fig_chain-2:
.. figure:: ../img/chain-net2.svg
Outro exemplo mais sutil da regra da cadeia.
Para calcular algo como :math:`\frac{\partial f}{\partial y}`,
precisamos somar todos (neste caso :math:`3`) caminhos de :math:`y` a
:math:`f` dando
.. 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}.
Entender a regra da cadeia desta forma renderá grandes dividendos ao
tentar entender como os gradientes fluem através das redes, e por que
várias escolhas arquitetônicas como aquelas em LSTMs
(:numref:`sec_lstm`) ou camadas residuais (:numref:`sec_resnet`)
podem ajudam a moldar o processo de aprendizagem, controlando o fluxo
gradiente.
O Algoritmo de Retropropagação
------------------------------
Vamos retornar ao exemplo de :eq:`eq_multi_func_def` a seção
anterior onde
.. 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}
Se quisermos calcular, digamos :math:`\frac{\partial f}{\partial w}`,
podemos aplicar a regra da cadeia multivariada para ver:
.. 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}
Vamos tentar usar esta decomposição para calcular
:math:`\frac{\partial f}{\partial w}`. Observe que tudo o que precisamos
aqui são as várias parciais de etapa única:
.. 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}
Se escrevermos isso no código, isso se tornará uma expressão bastante
gerenciável.
.. raw:: html
.. raw:: html
.. 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}')
.. parsed-literal::
:class: output
f at -1, 0, -2, 1 is 1024
df/dw at -1, 0, -2, 1 is -4096
.. raw:: html
.. raw:: html
.. 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}')
.. parsed-literal::
:class: output
f at -1, 0, -2, 1 is 1024
df/dw at -1, 0, -2, 1 is -4096
.. raw:: html
.. raw:: html
.. 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}')
.. parsed-literal::
:class: output
f at -1, 0, -2, 1 is 1024
df/dw at -1, 0, -2, 1 is -4096
.. raw:: html
.. raw:: html
No entanto, observe que isso ainda não facilita o cálculo de algo como
:math:`\frac{\partial f}{\partial x}`. A razão para isso é a *forma* que
escolhemos para aplicar a regra da cadeia. Se observarmos o que fizemos
acima, sempre mantivemos :math:`\partial w` no denominador quando
podíamos. Desta forma, optamos por aplicar a regra da cadeia vendo como
:math:`w` mudou todas as outras variáveis. Se é isso que queríamos,
seria uma boa ideia. No entanto, pense em nossa motivação com o
aprendizado profundo: queremos ver como cada parâmetro altera a *perda*.
Em essência, queremos aplicar a regra da cadeia mantendo
:math:`\partial f` no numerador sempre que pudermos!
Para ser mais explícito, observe que podemos escrever
.. 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}
Observe que esta aplicação da regra da cadeia nos faz computar
explicitamente
:math:`\frac{\partial f}{\partial u}, \frac{\partial f}{\partial v}, \frac{\partial f}{\partial a}, \frac{\partial f}{\partial b}, \; \text{e} \; \frac{\partial f}{\partial w}`.
Nada nos impede de incluir também as equações:
.. 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}
e acompanhar como :math:`f` muda quando mudamos *qualquer* nó em toda a
rede. Vamos implementar.
.. raw:: html
.. raw:: html
.. 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}')
.. parsed-literal::
:class: output
f at -1, 0, -2, 1 is 1024
df/dw at -1, 0, -2, 1 is -4096
df/dx at -1, 0, -2, 1 is -4096
df/dy at -1, 0, -2, 1 is -4096
df/dz at -1, 0, -2, 1 is -4096
.. raw:: html
.. raw:: html
.. 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}')
.. parsed-literal::
:class: output
f at -1, 0, -2, 1 is 1024
df/dw at -1, 0, -2, 1 is -4096
df/dx at -1, 0, -2, 1 is -4096
df/dy at -1, 0, -2, 1 is -4096
df/dz at -1, 0, -2, 1 is -4096
.. raw:: html
.. raw:: html
.. 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}')
.. parsed-literal::
:class: output
f at -1, 0, -2, 1 is 1024
df/dw at -1, 0, -2, 1 is -4096
df/dx at -1, 0, -2, 1 is -4096
df/dy at -1, 0, -2, 1 is -4096
df/dz at -1, 0, -2, 1 is -4096
.. raw:: html
.. raw:: html
O fato de calcularmos derivadas de :math:`f` de volta para as entradas
em vez de das entradas para as saídas (como fizemos no primeiro trecho
de código acima) é o que dá a esse algoritmo seu nome:
*retropropagação*. Observe que existem duas etapas: 1. Calcular o valor
da função e as parciais de etapa única da frente para trás. Embora não
feito acima, isso pode ser combinado em um único *passe para frente*. 2.
Calcule o gradiente de :math:`f` de trás para frente. Chamamos isso de
*passe para trás*.
Isso é precisamente o que todo algoritmo de aprendizado profundo
implementa para permitir o cálculo do gradiente da perda em relação a
cada peso na rede em uma passagem. É um fato surpreendente que tenhamos
tal decomposição.
Para ver como encapsular isso, vamos dar uma olhada rápida neste
exemplo.
.. raw:: html
.. raw:: html
.. code:: python
# Initialize as ndarrays, then attach gradients
w, x, y, z = np.array(-1), np.array(0), np.array(-2), np.array(1)
w.attach_grad()
x.attach_grad()
y.attach_grad()
z.attach_grad()
# Do the computation like usual, tracking gradients
with autograd.record():
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}, {x}, {y}, {z} is {w.grad}')
print(f'df/dx at {w}, {x}, {y}, {z} is {x.grad}')
print(f'df/dy at {w}, {x}, {y}, {z} is {y.grad}')
print(f'df/dz at {w}, {x}, {y}, {z} is {z.grad}')
.. parsed-literal::
:class: output
df/dw at -1.0, 0.0, -2.0, 1.0 is -4096.0
df/dx at -1.0, 0.0, -2.0, 1.0 is -4096.0
df/dy at -1.0, 0.0, -2.0, 1.0 is -4096.0
df/dz at -1.0, 0.0, -2.0, 1.0 is -4096.0
[03:48:47] src/base.cc:49: GPU context requested, but no GPUs found.
.. raw:: html
.. raw:: html
.. 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()}')
.. parsed-literal::
:class: output
df/dw at -1.0, 0.0, -2.0, 1.0 is -4096.0
df/dx at -1.0, 0.0, -2.0, 1.0 is -4096.0
df/dy at -1.0, 0.0, -2.0, 1.0 is -4096.0
df/dz at -1.0, 0.0, -2.0, 1.0 is -4096.0
.. raw:: html
.. raw:: html
.. code:: python
# Initialize as ndarrays, then attach gradients
w = tf.Variable(tf.constant([-1.]))
x = tf.Variable(tf.constant([0.]))
y = tf.Variable(tf.constant([-2.]))
z = tf.Variable(tf.constant([1.]))
# Do the computation like usual, tracking gradients
with tf.GradientTape(persistent=True) as t:
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
w_grad = t.gradient(f, w).numpy()
x_grad = t.gradient(f, x).numpy()
y_grad = t.gradient(f, y).numpy()
z_grad = t.gradient(f, z).numpy()
print(f'df/dw at {w.numpy()}, {x.numpy()}, {y.numpy()}, '
f'{z.numpy()} is {w_grad}')
print(f'df/dx at {w.numpy()}, {x.numpy()}, {y.numpy()}, '
f'{z.numpy()} is {x_grad}')
print(f'df/dy at {w.numpy()}, {x.numpy()}, {y.numpy()}, '
f'{z.numpy()} is {y_grad}')
print(f'df/dz at {w.numpy()}, {x.numpy()}, {y.numpy()}, '
f'{z.numpy()} is {z_grad}')
.. parsed-literal::
:class: output
df/dw at [-1.], [0.], [-2.], [1.] is [-4096.]
df/dx at [-1.], [0.], [-2.], [1.] is [-4096.]
df/dy at [-1.], [0.], [-2.], [1.] is [-4096.]
df/dz at [-1.], [0.], [-2.], [1.] is [-4096.]
.. raw:: html
.. raw:: html
Tudo o que fizemos acima pode ser feito automaticamente chamando
``f.backwards ()``.
Hessians
--------
Como no cálculo de variável única, é útil considerar derivadas de ordem
superior para entender como podemos obter uma melhor aproximação de uma
função do que usar apenas o gradiente.
Há um problema imediato que se encontra ao trabalhar com derivadas de
funções de várias variáveis de ordem superior, que é o grande número
delas. Se temos uma função :math:`f(x_1, \ldots, x_n)` de :math:`n`
variáveis, então podemos tomar :math:`n^{2}` derivadas secundas,
nomeadamente para qualquer escolha de :math:`i` e :math:`j`:
.. math::
\frac{d^2f}{dx_idx_j} = \frac{d}{dx_i}\left(\frac{d}{dx_j}f\right).
Isso é tradicionalmente montado em uma matriz chamada *Hessian*:
.. 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
Nem todas as entradas desta matriz são independentes. Na verdade,
podemos mostrar que, enquanto ambas \* parciais mistas \* (derivadas
parciais em relação a mais de uma variável) existem e são contínuos,
podemos dizer que para qualquer :math:`i` e :math:`j`,
.. math::
\frac{d^2f}{dx_idx_j} = \frac{d^2f}{dx_jdx_i}.
Isto segue considerando primeiro perturbar uma função na direção de
:math:`x_i`, e então perturbá-la em :math:`x_j` e então comparar o
resultado disso com o que acontece se perturbarmos primeiro :math:`x_j`
e então :math:`x_i`, com o conhecimento que ambos os pedidos levam à
mesma mudança final na produção de :math:`f`.
Assim como acontece com variáveis únicas, podemos usar essas derivadas
para ter uma ideia muito melhor de como a função se comporta perto de um
ponto. Em particular, podemos usar isso para encontrar a quadrática de
melhor ajuste próximo a um ponto :math:`\mathbf{x}_0`, como vimos em uma
única variável.
Vejamos um exemplo. Suponha que
: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}`.
Esta é a forma geral de uma quadrática em duas variáveis. Se olharmos
para o valor da função, seu gradiente e seu Hessian
:eq:`eq_hess_def`, tudo no ponto zero:
.. 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}
podemos obter nosso polinômio original de volta, dizendo
.. math::
f(\mathbf{x}) = f(0) + \nabla f (0) \cdot \mathbf{x} + \frac{1}{2}\mathbf{x}^\top \mathbf{H} f (0) \mathbf{x}.
In general, if we computed this expansion any point
:math:`\mathbf{x}_0`, we see that
.. 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).
Isso funciona para entradas de qualquer dimensão e fornece a melhor
aproximação quadrática para qualquer função em um ponto. Para dar um
exemplo, vamos representar graficamente a função
.. math::
f(x, y) = xe^{-x^2-y^2}.
Pode-se calcular que o gradiente e Hessian são
.. math::
\nabla f(x, y) = e^{-x^2-y^2}\begin{pmatrix}1-2x^2 \\ -2xy\end{pmatrix} \; \text{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}.
E assim, com um pouco de álgebra, veja que a aproximação quadrática em
:math:`[-1,0]^\top` é
.. math::
f(x, y) \approx e^{-1}\left(-1 - (x+1) +(x+1)^2+y^2\right).
.. raw:: html
.. raw:: html
.. code:: python
# Construct grid and compute function
x, y = np.meshgrid(np.linspace(-2, 2, 101),
np.linspace(-2, 2, 101), indexing='ij')
z = x*np.exp(- x**2 - y**2)
# Compute approximating quadratic with gradient and Hessian at (1, 0)
w = np.exp(-1)*(-1 - (x + 1) + (x + 1)**2 + y**2)
# Plot function
ax = d2l.plt.figure().add_subplot(111, projection='3d')
ax.plot_wireframe(x, y, z, **{'rstride': 10, 'cstride': 10})
ax.plot_wireframe(x, y, w, **{'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
.. figure:: output_multivariable-calculus_bf678a_63_0.svg
.. raw:: html
.. raw:: html
.. 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
.. figure:: output_multivariable-calculus_bf678a_66_0.svg
.. raw:: html
.. raw:: html
.. code:: python
# Construct grid and compute function
x, y = tf.meshgrid(tf.linspace(-2., 2., 101),
tf.linspace(-2., 2., 101))
z = x*tf.exp(- x**2 - y**2)
# Compute approximating quadratic with gradient and Hessian at (1, 0)
w = tf.exp(tf.constant([-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
.. figure:: output_multivariable-calculus_bf678a_69_0.svg
.. raw:: html
.. raw:: html
Isso forma a base para o Algoritmo de Newton discutido em
:numref:`sec_gd`, onde realizamos a otimização numérica encontrando
iterativamente a quadrática de melhor ajuste e, em seguida, minimizando
exatamente essa quadrática.
Um Pouco de Cálculo Matricial
-----------------------------
Derivadas de funções envolvendo matrizes revelaram-se particularmente
interessantes. Esta seção pode se tornar notacionalmente pesada,
portanto, pode ser ignorada em uma primeira leitura, mas é útil saber
como as derivadas de funções que envolvem operações de matriz comum são
muitas vezes muito mais limpas do que se poderia prever inicialmente,
especialmente considerando como as operações de matriz centrais são para
o aprendizado profundo aplicações.
Vamos começar com um exemplo. Suponha que temos algum vetor de coluna
fixo :math:`\boldsymbol{\beta}`, e queremos obter a função de produto
:math:`f(\mathbf{x}) = \boldsymbol{\beta}^\top\mathbf{x}`, e entender
como o produto escalar muda quando mudamos :math:`\mathbf{x}`.
Um pouco de notação que será útil ao trabalhar com derivadas de matriz
em ML é chamado de *derivada de matriz de layout de denominador*, onde
montamos nossas derivadas parciais na forma de qualquer vetor, matriz ou
tensor que esteja no denominador do diferencial. Neste caso, vamos
escrever
.. math::
\frac{df}{d\mathbf{x}} = \begin{bmatrix}
\frac{df}{dx_1} \\
\vdots \\
\frac{df}{dx_n}
\end{bmatrix},
onde combinamos a forma do vetor coluna :math:`\mathbf{x}`.
Se escrevermos nossa função em componentes, isso é
.. math::
f(\mathbf{x}) = \sum_{i = 1}^{n} \beta_ix_i = \beta_1x_1 + \cdots + \beta_nx_n.
Se agora tomarmos a derivada parcial em relação a :math:`\beta_1`, note
que tudo é zero, exceto o primeiro termo, que é apenas :math:`x_1`
multiplicado por :math:`\beta_1`,, então obtemos isso
.. math::
\frac{df}{dx_1} = \beta_1,
ou mais geralmente isso
.. math::
\frac{df}{dx_i} = \beta_i.
Agora podemos remontar isso em uma matriz para ver
.. 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}.
Isso ilustra alguns fatores sobre o cálculo de matriz que muitas vezes
iremos contrariar ao longo desta seção:
- Primeiro, os cálculos ficarão bastante complicados.
- Em segundo lugar, os resultados finais são muito mais limpos do que o
processo intermediário e sempre serão semelhantes ao caso de uma
única variável. Neste caso, observe que :math:`\frac{d}{dx}(bx) = b`
and
:math:`\frac{d}{d\mathbf{x}} (\boldsymbol{\beta}^\top\mathbf{x}) = \boldsymbol{\beta}`
são ambos semelhantes.
- Terceiro, as transpostas muitas vezes podem aparecer aparentemente do
nada. A principal razão para isso é a convenção de que combinamos a
forma do denominador, portanto, quando multiplicamos as matrizes,
precisaremos fazer transposições para corresponder à forma do termo
original.
Para continuar construindo a intuição, vamos tentar um cálculo um pouco
mais difícil. Suponha que temos um vetor coluna :math:`\mathbf{x}` e uma
matriz quadrada :math:`A` e queremos calcular
.. math:: \frac{d}{d\mathbf{x}}(\mathbf{x}^\top A \mathbf{x}).
:label: eq_mat_goal_1
Para direcionar para uma notação mais fácil de manipular, vamos
considerar este problema usando a notação de Einstein. Neste caso,
podemos escrever a função como
.. math::
\mathbf{x}^\top A \mathbf{x} = x_ia_{ij}x_j.
Para calcular nossa derivada, precisamos entender para cada :math:`k`,
qual é o valor de
.. math::
\frac{d}{dx_k}(\mathbf{x}^\top A \mathbf{x}) = \frac{d}{dx_k}x_ia_{ij}x_j.
Pela regra do produto, isso é
.. 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}.
Para um termo como :math:`\frac{dx_i}{dx_k}`, não é difícil ver que este
é um quando :math:`i=k` e zero caso contrário. Isso significa que todos
os termos em que :math:`i` e :math:`k` são diferentes desaparecem dessa
soma, de modo que os únicos termos que permanecem nessa primeira soma
são aqueles em que :math:`i=k`. O mesmo raciocínio vale para o segundo
termo em que precisamos de :math:`j=k`. Isto dá
.. math::
\frac{d}{dx_k}x_ia_{ij}x_j = a_{kj}x_j + x_ia_{ik}.
Agora, os nomes dos índices na notação de Einstein são arbitrários — o
fato de que :math:`i` e :math:`j` são diferentes é irrelevante para este
cálculo neste ponto, então podemos reindexar para que ambos usem
:math:`i` para ver isso
.. math::
\frac{d}{dx_k}x_ia_{ij}x_j = a_{ki}x_i + x_ia_{ik} = (a_{ki} + a_{ik})x_i.
Agora, é aqui que precisamos de um pouco de prática para ir mais longe.
Vamos tentar identificar esse resultado em termos de operações de
matriz. :math:`a_{ki} + a_{ik}` é o :math:`k, i`-ésimo componente de
:math:`\mathbf{A} + \mathbf{A}^\top`. Isto dá
.. math::
\frac{d}{dx_k}x_ia_{ij}x_j = [\mathbf{A} + \mathbf{A}^\top]_{ki}x_i.
Da mesma forma, este termo é agora o produto da matriz
:math:`\mathbf{A} + \mathbf{A}^\top` pelo vetor :math:`\mathbf{x}`,
então vemos que
.. 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.
Assim, vemos que a :math:`k` -ésima entrada da derivada desejada de
:eq:`eq_mat_goal_1` é apenas a :math:`k`-ésima entrada do vetor à
direita e, portanto, os dois são iguais. Assim
.. math::
\frac{d}{d\mathbf{x}}(\mathbf{x}^\top A \mathbf{x}) = (\mathbf{A} + \mathbf{A}^\top)\mathbf{x}.
Isso exigiu muito mais trabalho do que o anterior, mas o resultado final
é pequeno. Mais do que isso, considere o seguinte cálculo para derivadas
de variável única tradicionais:
.. math::
\frac{d}{dx}(xax) = \frac{dx}{dx}ax + xa\frac{dx}{dx} = (a+a)x.
Equivalentemente :math:`\frac{d}{dx}(ax^2) = 2ax = (a+a)x`. Novamente,
obtemos um resultado que se parece bastante com o resultado de uma única
variável, mas com uma transposição inserida.
Neste ponto, o padrão deve parecer bastante suspeito, então vamos tentar
descobrir o porquê. Quando tomamos derivadas de matriz como esta, vamos
primeiro supor que a expressão que obtemos será outra expressão de
matriz: uma expressão que podemos escrevê-la em termos de produtos e
somas de matrizes e suas transposições. Se tal expressão existir, ela
precisará ser verdadeira para todas as matrizes. Em particular, ele
precisará ser verdadeiro para matrizes :math:`1 \times 1`, caso em que o
produto da matriz é apenas o produto dos números, a soma da matriz é
apenas a soma e a transposta não faz nada! Em outras palavras, qualquer
expressão que obtivermos *deve* corresponder à expressão de variável
única. Isso significa que, com alguma prática, muitas vezes pode-se
adivinhar as derivadas de matriz apenas por saber como a expressão da
única variável associada deve se parecer!
Vamos tentar fazer isso. Suponha que :math:`\mathbf{X}` é uma matriz
:math:`n \times m`, :math:`\mathbf{U}` é uma :math:`n \times r` e
:math:`\mathbf{V}` é uma :math:`r \times m`. Vamos tentar calcular
.. math:: \frac{d}{d\mathbf{V}} \|\mathbf{X} - \mathbf{U}\mathbf{V}\|_2^{2} = \;?
:label: eq_mat_goal_2
Este cálculo é importante em uma área chamada fatoração de matrizes.
Para nós, no entanto, é apenas uma derivada para calcular. Vamos tentar
imaginar o que seria para as matrizes :math:`1\times1`. Nesse caso,
obtemos a expressão
.. math::
\frac{d}{dv} (x-uv)^{2}= -2(x-uv)u,
onde, a derivada é bastante padrão. Se tentarmos converter isso de volta
em uma expressão de matriz, obteremos
.. math::
\frac{d}{d\mathbf{V}} \|\mathbf{X} - \mathbf{U}\mathbf{V}\|_2^{2}= -2(\mathbf{X} - \mathbf{U}\mathbf{V})\mathbf{U}.
No entanto, se olharmos para isso, não funciona bem. Lembre-se de que
:math:`\mathbf{X}` é :math:`n \times m`, assim como
:math:`\mathbf{U}\mathbf{V}`, então a matriz
:math:`2(\mathbf{X} - \mathbf{U}\mathbf{V})` é :math:`n \times m`. Por
outro lado :math:`\mathbf{U}` é :math:`n \times r`, e não podemos
multiplicar uma matriz :math:`n \times m` e uma :math:`n \times r`
porque as dimensões não combinam!
Queremos obter :math:`\frac{d}{d\mathbf{V}}`, que tem a mesma forma de
:math:`\mathbf{V}`, que é :math:`r \times m`. Então, de alguma forma,
precisamos pegar uma matriz :math:`n \times m` e uma matriz
:math:`n \times r`, multiplicá-las juntas (talvez com algumas
transposições) para obter uma :math:`r \times m`. Podemos fazer isso
multiplicando :math:`U^\top` por
:math:`(\mathbf{X} - \mathbf{U}\mathbf{V})`.. Assim, podemos adivinhar a
solução para :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}).
Para mostrar que isso funciona, seríamos negligentes em não fornecer um
cálculo detalhado. Se já acreditamos que essa regra prática funciona,
fique à vontade para pular esta derivação. Para calcular
.. math::
\frac{d}{d\mathbf{V}} \|\mathbf{X} - \mathbf{U}\mathbf{V}\|_2^2,
devemos encontrar para cada :math:`a` e :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.
Lembrando que todas as entradas de :math:`\mathbf{X}` e
:math:`\mathbf{U}` são constantes no que diz respeito a
:math:`\frac{d}{dv_{ab}}`, podemos colocar a derivada dentro da soma, e
aplicar a regra da cadeia ao quadrado para obter
.. 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).
Como na derivação anterior, podemos notar que
:math:`\frac{dv_{kj}}{dv_{ab}}` só é diferente de zero se :math:`k=a`
and :math:`j=b`. Se qualquer uma dessas condições não for válida, o
termo na soma é zero e podemos descartá-lo livremente. Nós vemos que
.. 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}.
Uma sutileza importante aqui é que o requisito de que :math:`k=a` não
ocorre dentro da soma interna, uma vez que :math:`k` é uma variável que
estamos somando dentro do termo interno. Para um exemplo notacionalmente
mais limpo, considere por que
.. math::
\frac{d}{dx_1} \left(\sum_i x_i \right)^{2}= 2\left(\sum_i x_i \right).
A partir deste ponto, podemos começar a identificar os componentes da
soma. Primeiro,
.. math::
\sum_k u_{ik}v_{kb} = [\mathbf{U}\mathbf{V}]_{ib}.
Portanto, toda a expressão no interior da soma é
.. math::
x_{ib} - \sum_k u_{ik}v_{kb} = [\mathbf{X}-\mathbf{U}\mathbf{V}]_{ib}.
Isso significa que agora podemos escrever nossa derivada como
.. 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}.
Queremos que se pareça com o elemento :math:`a, b` de uma matriz, para
que possamos usar a técnica como no exemplo anterior para chegar a uma
expressão de matriz, o que significa que precisamos trocar a ordem dos
índices em :math:`u_{ia}`. Se notarmos que
:math:`u_{ia} = [\mathbf{U}^\top]_{ai}`, podemos escrever
.. 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}.
Este é um produto de matriz e, portanto, podemos concluir que
.. math::
\frac{d}{dv_{ab}} \|\mathbf{X} - \mathbf{U}\mathbf{V}\|_2^{2}= -2[\mathbf{U}^\top(\mathbf{X}-\mathbf{U}\mathbf{V})]_{ab}.
e assim podemos escrever a solução para :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}).
Isso corresponde à solução que adivinhamos acima!
É razoável perguntar neste ponto: “Por que não posso simplesmente
escrever versões de matriz de todas as regras de cálculo que aprendi?
Está claro que isso ainda é mecânico. Por que não simplesmente acabamos
com isso!” E de fato existem tais regras e
:cite:`Petersen.Pedersen.ea.2008` fornece um excelente resumo. No
entanto, devido à infinidade de maneiras pelas quais as operações de
matriz podem ser combinadas em comparação com valores únicos, há muito
mais regras de derivadas de matriz do que regras de variável única.
Geralmente, é melhor trabalhar com os índices ou deixar para a
diferenciação automática, quando apropriado.
Resumo
------
- Em dimensões superiores, podemos definir gradientes que têm o mesmo
propósito que os derivadas em uma dimensão. Isso nos permite ver como
uma função multivariável muda quando fazemos uma pequena mudança
arbitrária nas entradas.
- O algoritmo de retropropagação pode ser visto como um método de
organizar a regra da cadeia multivariável para permitir o cálculo
eficiente de muitas derivadas parciais.
- O cálculo matricial nos permite escrever as derivadas das expressões
matriciais de maneiras concisas.
Exercícios
----------
1. Dado um vetor de coluna :math:`\boldsymbol{\beta}`, calcule as
derivadas de
:math:`f(\mathbf{x}) = \boldsymbol{\beta}^\top\mathbf{x}` e
:math:`g(\mathbf{x}) = \mathbf{x}^\top\boldsymbol{\beta}`. Por que
você obtém a mesma resposta?
2. Seja :math:`\mathbf{v}` um vetor de dimensão :math:`n`. O que é
:math:`\frac{\partial}{\partial\mathbf{v}}\|\mathbf{v}\|_2`?
3. Seja :math:`L(x, y) = \log(e^x + e^y)`. Calcule o gradiente. Qual é a
soma dos componentes do gradiente?
4. Seja :math:`f(x, y) = x^2y + xy^2`. Mostre que o único ponto crítico
é :math:`(0,0)`. Considerando $ f(x, x)$, determine se :math:`(0,0)`
é máximo, mínimo ou nenhum.
5. Suponha que estejamos minimizando uma função
:math:`f(\mathbf{x}) = g(\mathbf{x}) + h(\mathbf{x})`. Como podemos
interpretar geometricamente a condição de :math:`\nabla f = 0` em
termos de :math:`g` e :math:`h`?
.. raw:: html
.. raw:: html
`Discussões `__
.. raw:: html
.. raw:: html
`Discussões `__
.. raw:: html
.. raw:: html
`Discussões `__
.. raw:: html
.. raw:: html
.. raw:: html