.. _sec_autograd:
Diferenciação automática
========================
Como já explicado em :numref:`sec_calculus`, a diferenciação é uma
etapa crucial em quase todos os algoritmos de otimização de *Deep
Learning*. Embora os cálculos para obter esses derivados sejam diretos,
exigindo apenas alguns cálculos básicos, para modelos complexos,
trabalhando as atualizações manualmente pode ser uma tarefa difícil (e
muitas vezes sujeita a erros). *Frameworks* de *Deep learning* aceleram
este trabalho calculando automaticamente as derivadas, ou seja,
*diferenciação automática*. Na prática, com base em nosso modelo
projetado o sistema constrói um *grafo computacional*, rastreando quais
dados combinados por meio de quais operações produzem a saída. A
diferenciação automática permite que o sistema propague gradientes
posteriormente. Aqui, propagar(do Inglês *backpropagate*) significa
simplesmente traçar o gráfico computacional, preencher as derivadas
parciais em relação a cada parâmetro.
Um exemplo simples
------------------
Como exemplo, digamos que estamos interessados em derivar a função
:math:`y = 2\mathbf{x}^{\top}\mathbf{x}` com respeito ao vetor coluna
:math:`\mathbf{x}`. Inicialmente criamos a variável ``x`` e atribuimos a
ela um valor inicial.
.. raw:: html
.. raw:: html
.. code:: python
from mxnet import autograd, np, npx
npx.set_np()
x = np.arange(4.0)
x
.. parsed-literal::
:class: output
array([0., 1., 2., 3.])
.. raw:: html
.. raw:: html
.. code:: python
import torch
x = torch.arange(4.0)
x
.. parsed-literal::
:class: output
tensor([0., 1., 2., 3.])
.. raw:: html
.. raw:: html
.. code:: python
import tensorflow as tf
x = tf.range(4, dtype=tf.float32)
x
.. parsed-literal::
:class: output
.. raw:: html
.. raw:: html
Antes de calcularmos o gradiente de\ :math:`y` em relação a
:math:`\mathbf{x}`, precisamos armazena-lo. É importante que não
aloquemos nova memória cada vez que tomamos uma derivada em relação a um
parâmetro porque costumamos atualizar os mesmos parâmetros milhares ou
milhões de vezes e podemos rapidamente ficar sem memória. Observe que um
gradiente de uma função com valor escalar com respeito a um vetor
:math:`\mathbf{x}` tem valor vetorial e tem a mesma forma de
:math:`\mathbf{x}`.
.. raw:: html
.. raw:: html
.. code:: python
# Alocamos memória do gradiente do vetor invocando `attach_grad`
x.attach_grad()
# Após calcularmos o gradiente como respeito a `x`, será possível
# acessa-lo vai atributo `grad`, cujo valor inicializará com 0s
x.grad
.. parsed-literal::
:class: output
array([0., 0., 0., 0.])
.. raw:: html
.. raw:: html
.. code:: python
x.requires_grad_(True) # Same as `x = torch.arange(4.0, requires_grad=True)`
x.grad # O valor padrão é None
.. raw:: html
.. raw:: html
.. code:: python
x = tf.Variable(x)
.. raw:: html
.. raw:: html
Então calcularemos :math:`y`.
.. raw:: html
.. raw:: html
.. code:: python
# Colocaremos o código dentro do escopo `autograd.record` para contruir
# o grafo computacional
with autograd.record():
y = 2 * np.dot(x, x)
y
.. parsed-literal::
:class: output
array(28.)
.. raw:: html
.. raw:: html
.. code:: python
y = 2 * torch.dot(x, x)
y
.. parsed-literal::
:class: output
tensor(28., grad_fn=)
.. raw:: html
.. raw:: html
.. code:: python
# Gravando todos os calculos em um *tape*
with tf.GradientTape() as t:
y = 2 * tf.tensordot(x, x, axes=1)
y
.. parsed-literal::
:class: output
.. raw:: html
.. raw:: html
Uma vez que ``x`` é um vetor de comprimento 4, um produto interno de
``x`` e\ ``x`` é realizado, produzindo a saída escalar que atribuímos a
``y``. Em seguida, podemos calcular automaticamente o gradiente de ``y``
com relação a cada componente de ``x`` chamando a função de
retropropagação e imprimindo o gradiente.
.. raw:: html
.. raw:: html
.. code:: python
y.backward()
x.grad
.. parsed-literal::
:class: output
[03:58:07] src/base.cc:49: GPU context requested, but no GPUs found.
.. parsed-literal::
:class: output
array([ 0., 4., 8., 12.])
.. raw:: html
.. raw:: html
.. code:: python
y.backward()
x.grad
.. parsed-literal::
:class: output
tensor([ 0., 4., 8., 12.])
.. raw:: html
.. raw:: html
.. code:: python
x_grad = t.gradient(y, x)
x_grad
.. parsed-literal::
:class: output
.. raw:: html
.. raw:: html
O gradiente da função :math:`y = 2\mathbf{x}^{\top}\mathbf{x}` em
relação a :math:`\mathbf{x}` should be :math:`4\mathbf{x}`. Vamos
verificar rapidamente se nosso gradiente desejado foi calculado
corretamente.
.. raw:: html
.. raw:: html
.. code:: python
x.grad == 4 * x
.. parsed-literal::
:class: output
array([ True, True, True, True])
.. raw:: html
.. raw:: html
.. code:: python
x.grad == 4 * x
.. parsed-literal::
:class: output
tensor([True, True, True, True])
.. raw:: html
.. raw:: html
.. code:: python
x_grad == 4 * x
.. parsed-literal::
:class: output
.. raw:: html
.. raw:: html
Agora calculamos outra função de ``x``.
.. raw:: html
.. raw:: html
.. code:: python
with autograd.record():
y = x.sum()
y.backward()
x.grad # Sobrescrito pelo novo gradiente calculado
.. parsed-literal::
:class: output
array([1., 1., 1., 1.])
.. raw:: html
.. raw:: html
.. code:: python
# O PyTorch acumula os gradientes por padrão, precisamos
# apagar os valores anteriores
x.grad.zero_()
y = x.sum()
y.backward()
x.grad
.. parsed-literal::
:class: output
tensor([1., 1., 1., 1.])
.. raw:: html
.. raw:: html
.. code:: python
with tf.GradientTape() as t:
y = tf.reduce_sum(x)
t.gradient(y, x) # Overwritten by the newly calculated gradient
.. parsed-literal::
:class: output
.. raw:: html
.. raw:: html
Retroceder para variáveis não escalares
---------------------------------------
Tecnicamente, quando ``y`` não é um escalar, a interpretação mais
natural da diferenciação de um vetor ``y`` em relação a um vetor, ``x``
é uma matriz. Para ``y`` e\ ``x`` de ordem superior e dimensão superior,
o resultado da diferenciação pode ser um tensor de ordem alta.
No entanto, embora esses objetos mais exóticos apareçam em aprendizado
de máquina avançado (incluindo em *Deep Learning*), com mais frequência
quando estamos retrocedendo um vetor, estamos tentando calcular as
derivadas das funções de perda para cada constituinte de um *lote* de
exemplos de treinamento. Aqui, nossa intenção é não calcular a matriz de
diferenciação mas sim a soma das derivadas parciais calculado
individualmente para cada exemplo no lote.
.. raw:: html
.. raw:: html
.. code:: python
# Quando invocamos `backward` em uma variável de vetor valorado `y` (em função de `x`),
# uma nova variável escalar é criada somando os elementos em `y`. Então o
# gradiente daquela variável escalar em respeito a `x` é computada
with autograd.record():
y = x * x # `y` is a vector
y.backward()
x.grad # Igual a y = sum(x * x)
.. parsed-literal::
:class: output
array([0., 2., 4., 6.])
.. raw:: html
.. raw:: html
.. code:: python
# Invocar `backward` em um não escalar requer passar um argumento `gradient`
# que especifica o gradiente da função diferenciada w.r.t `self`.
# Em nosso caso, simplesmente queremos somar as derivadas parciais, assim passando
# em um gradiente de uns é apropriado
x.grad.zero_()
y = x * x
# y.backward(torch.ones(len(x))) equivalente a:
y.sum().backward()
x.grad
.. parsed-literal::
:class: output
tensor([0., 2., 4., 6.])
.. raw:: html
.. raw:: html
.. code:: python
with tf.GradientTape() as t:
y = x * x
t.gradient(y, x) # Same as `y = tf.reduce_sum(x * x)`
.. parsed-literal::
:class: output
.. raw:: html
.. raw:: html
Computação *Detaching*
----------------------
Às vezes, desejamos mover alguns cálculos fora do gráfico computacional
registrado. Por exemplo, digamos que ``y`` foi calculado como uma função
de\ ``x``, e que subsequentemente ``z`` foi calculado como uma função
de\ ``y`` e ``x``. Agora, imagine que quiséssemos calcular o gradiente
de ``z`` em relação a\ ``x``, mas queria, por algum motivo, tratar ``y``
como uma constante, e apenas leve em consideração a função que ``x``
jogou após\ ``y`` foi calculado. Aqui, podemos desanexar ``y`` para
retornar uma nova variável ``u`` que tem o mesmo valor que ``y``, mas
descarta qualquer informação sobre como ``y`` foi calculado no grafo
computacional. Em outras palavras, o gradiente não fluirá de volta de
``u`` para ``x``. Assim, a seguinte função de retropropagação calcula a
derivada parcial de ``z = u * x`` com respeito a\ ``x`` enquanto trata
``u`` como uma constante, em vez da derivada parcial de
``z = x * x * x`` em relação a\ ``x``.
.. raw:: html
.. raw:: html
.. code:: python
with autograd.record():
y = x * x
u = y.detach()
z = u * x
z.backward()
x.grad == u
.. parsed-literal::
:class: output
array([ True, True, True, True])
.. raw:: html
.. raw:: html
.. code:: python
x.grad.zero_()
y = x * x
u = y.detach()
z = u * x
z.sum().backward()
x.grad == u
.. parsed-literal::
:class: output
tensor([True, True, True, True])
.. raw:: html
.. raw:: html
.. code:: python
# Defina `persistent=True` para executar `t.gradient` mais de uma vez
with tf.GradientTape(persistent=True) as t:
y = x * x
u = tf.stop_gradient(y)
z = u * x
x_grad = t.gradient(z, x)
x_grad == u
.. parsed-literal::
:class: output
.. raw:: html
.. raw:: html
Uma vez que o cálculo de ``y`` foi registrado, podemos subsequentemente
invocar a retropropagação em ``y`` para obter a derivada
de\ ``y = x * x`` com respeito a ``x``, que é\ ``2 * x``.
.. raw:: html
.. raw:: html
.. code:: python
y.backward()
x.grad == 2 * x
.. parsed-literal::
:class: output
array([ True, True, True, True])
.. raw:: html
.. raw:: html
.. code:: python
x.grad.zero_()
y.sum().backward()
x.grad == 2 * x
.. parsed-literal::
:class: output
tensor([True, True, True, True])
.. raw:: html
.. raw:: html
.. code:: python
t.gradient(y, x) == 2 * x
.. parsed-literal::
:class: output
.. raw:: html
.. raw:: html
Computando o Gradiente do *Python Control Flow*
-----------------------------------------------
Uma vantagem de usar a diferenciação automática é que mesmo se construir
o gráfo computacional de uma função requer muito trabalho com o uso do
*Python Control Flow* (por exemplo, condicionais, loops e chamadas de
função arbitrárias), ainda podemos calcular o gradiente da variável
resultante. No trecho a seguir, observe que o número de iterações do
loop ``while`` e a avaliação da instrução ``if`` ambos dependem do valor
da entrada ``a``.
.. raw:: html
.. raw:: html
.. code:: python
def f(a):
b = a * 2
while np.linalg.norm(b) < 1000:
b = b * 2
if b.sum() > 0:
c = b
else:
c = 100 * b
return c
.. raw:: html
.. raw:: html
.. code:: python
def f(a):
b = a * 2
while b.norm() < 1000:
b = b * 2
if b.sum() > 0:
c = b
else:
c = 100 * b
return c
.. raw:: html
.. raw:: html
.. code:: python
def f(a):
b = a * 2
while tf.norm(b) < 1000:
b = b * 2
if tf.reduce_sum(b) > 0:
c = b
else:
c = 100 * b
return c
.. raw:: html
.. raw:: html
Vamos computar o gradiente:
.. raw:: html
.. raw:: html
.. code:: python
a = np.random.normal()
a.attach_grad()
with autograd.record():
d = f(a)
d.backward()
.. raw:: html
.. raw:: html
.. code:: python
a = torch.randn(size=(), requires_grad=True)
d = f(a)
d.backward()
.. raw:: html
.. raw:: html
.. code:: python
a = tf.Variable(tf.random.normal(shape=()))
with tf.GradientTape() as t:
d = f(a)
d_grad = t.gradient(d, a)
d_grad
.. parsed-literal::
:class: output
.. raw:: html
.. raw:: html
Agora podemos analisar a função ``f`` definida acima. Observe que é
linear por partes em sua entrada ``a``. Em outras palavras, para
qualquer ``a`` existe algum escalar constante\ ``k`` tal que
``f (a) = k * a``, onde o valor de\ ``k`` depende da entrada ``a``.
Consequentemente, ``d / a`` nos permite verificar se o gradiente está
correto.
.. raw:: html
.. raw:: html
.. code:: python
a.grad == d / a
.. parsed-literal::
:class: output
array(True)
.. raw:: html
.. raw:: html
.. code:: python
a.grad == d / a
.. parsed-literal::
:class: output
tensor(True)
.. raw:: html
.. raw:: html
.. code:: python
d_grad == d / a
.. parsed-literal::
:class: output
.. raw:: html
.. raw:: html
Sumário
-------
- *Frameworks* de *Deep learning* podem automatizar o cálculo de
derivadas. Para usá-lo, primeiro anexamos gradientes às variáveis em
relação às quais desejamos as derivadas parciais. Em seguida,
registramos o cálculo de nosso valor alvo, executamos sua função para
retropropagação e acessamos o gradiente resultante.
Exercícios
----------
1. Por que a segunda derivada é muito mais computacionalmente cara de se
calcular do que a primeira derivada?
2. Depois de executar a função de retropropagação, execute-a
imediatamente novamente e veja o que acontece.
3. No exemplo de fluxo de controle onde calculamos a derivada de ``d``
com respeito a ``a``, o que aconteceria se mudássemos a variável
``a`` para um vetor ou matriz aleatória. Neste ponto, o resultado do
cálculo ``f (a)`` não é mais um escalar. O que acontece com o
resultado? Como analisamos isso?
4. Redesenhe um exemplo para encontrar o gradiente do *Control Flow*.
Execute e analise o resultado.
5. Seja :math:`f (x) = \ sin (x)`. Plote :math:`f (x)` e
:math:`\ frac {df (x)} {dx}`, onde o último é calculado sem explorar
que :math:`f '(x) = \ cos (x)`.
.. raw:: html
.. raw:: html
`Discussions `__
.. raw:: html
.. raw:: html
`Discussions `__
.. raw:: html
.. raw:: html
`Discussions `__
.. raw:: html
.. raw:: html
.. raw:: html