.. _sec_momentum:
Momentum
========
Em :numref:`sec_sgd`, revisamos o que acontece ao realizar a descida
do gradiente estocástico, ou seja, ao realizar a otimização onde apenas
uma variante barulhenta do gradiente está disponível. Em particular,
notamos que, para gradientes ruidosos, precisamos ser extremamente
cautelosos ao escolher a taxa de aprendizado em face do ruído. Se
diminuirmos muito rapidamente, a convergência para. Se formos tolerantes
demais, não conseguiremos convergir para uma solução boa o suficiente,
pois o ruído continua nos afastando da otimização.
Fundamentos
-----------
Nesta seção, exploraremos algoritmos de otimização mais eficazes,
especialmente para certos tipos de problemas de otimização que são
comuns na prática.
Médias com vazamento
~~~~~~~~~~~~~~~~~~~~
A seção anterior nos viu discutindo o minibatch SGD como um meio de
acelerar a computação. Também teve o bom efeito colateral de que a média
dos gradientes reduziu a quantidade de variância. O minibatch SGD pode
ser calculado por:
.. math::
\ mathbf {g} _ {t, t-1} = \ partial _ {\ mathbf {w}} \frac{1}{|\mathcal{B}_t|} \sum_{i \in \mathcal{B}_t} f(\ mathbf {x}_{ i}, \ mathbf {w }_{t-1}) = \frac{1}{|\mathcal{B}_t|} \sum_{i \in \mathcal{B}_t} \mathbf{h}_{i, t-1}.
Para manter a notação simples, aqui usamos
:math:`\mathbf{h}_{i, t-1} = \partial_{\mathbf{w}} f(\mathbf{x}_i, \mathbf{w}_{t-1})`
como o SGD para a amostra :math:`i` usando os pesos atualizados no tempo
$ t-1 $. Seria bom se pudéssemos nos beneficiar do efeito da redução da
variância, mesmo além da média dos gradientes em um minibatch. Uma opção
para realizar esta tarefa é substituir o cálculo do gradiente por uma
“média com vazamento”:
.. math:: \mathbf{v}_t = \beta \mathbf{v}_{t-1} + \mathbf{g}_{t, t-1}
por algum :math:`\beta \in (0, 1)`. Isso substitui efetivamente o
gradiente instantâneo por um que foi calculado em vários gradientes
*anteriores*. :math:`\mathbf{v}` é chamado *momentum*. Ele acumula
gradientes anteriores semelhantes a como uma bola pesada rolando pela
paisagem da função objetivo se integra às forças passadas. Para ver o
que está acontecendo com mais detalhes, vamos expandir
:math:`\mathbf{v}_t` recursivamente em
.. math::
\begin{aligned}
\mathbf{v}_t = \beta^2 \mathbf{v}_{t-2} + \beta \mathbf{g}_{t-1, t-2} + \mathbf{g}_{t, t-1}
= \ldots, = \sum_{\tau = 0}^{t-1} \beta^{\tau} \mathbf{g}_{t-\tau, t-\tau-1}.
\end{aligned}
:math:`\beta` grande equivale a uma média de longo alcance, enquanto
:math:`\beta` pequeno equivale a apenas uma ligeira correção em relação
a um método de gradiente. A nova substituição de gradiente não aponta
mais para a direção da descida mais íngreme em uma instância particular,
mas sim na direção de uma média ponderada de gradientes anteriores. Isso
nos permite obter a maioria dos benefícios da média de um lote sem o
custo de realmente calcular os gradientes nele. Iremos revisitar este
procedimento de média com mais detalhes posteriormente.
O raciocínio acima formou a base para o que agora é conhecido como
métodos de gradiente *acelerado*, como gradientes com momentum. Eles têm
o benefício adicional de serem muito mais eficazes nos casos em que o
problema de otimização é mal condicionado (ou seja, onde há algumas
direções onde o progresso é muito mais lento do que em outras, parecendo
um desfiladeiro estreito). Além disso, eles nos permitem calcular a
média dos gradientes subsequentes para obter direções de descida mais
estáveis. Na verdade, o aspecto da aceleração, mesmo para problemas
convexos sem ruído, é uma das principais razões pelas quais o momentum
funciona e por que funciona tão bem.
Como seria de esperar, devido ao seu momentum de eficácia, é um assunto
bem estudado em otimização para aprendizado profundo e além. Veja, por
exemplo, o belo `artigo
expositivo `__ por
:cite:`Goh.2017` para uma análise aprofundada e animação interativa.
Foi proposto por :cite:`Polyak.1964`. :cite:`Nesterov.2018` tem uma
discussão teórica detalhada no contexto da otimização convexa. O
momentum no aprendizado profundo é conhecido por ser benéfico há muito
tempo. Veja, por exemplo, a discussão de
:cite:`Sutskever.Martens.Dahl.ea.2013` para obter detalhes.
Um problema mal condicionado
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Para obter uma melhor compreensão das propriedades geométricas do método
do momento, revisitamos a descida do gradiente, embora com uma função
objetivo significativamente menos agradável. Lembre-se de que em
:numref:`sec_gd` usamos :math:`f(\mathbf{x}) = x_1^2 + 2 x_2^2`, ou
seja, um objetivo elipsóide moderadamente distorcido. Distorcemos esta
função ainda mais estendendo-a na direção :math:`x_1` por meio de
.. math:: f(\mathbf{x}) = 0.1 x_1^2 + 2 x_2^2.
Como antes, :math:`f` tem seu mínimo em :math:`(0, 0)`. Esta função é
*muito* plana na direção de :math:`x_1`. Vamos ver o que acontece quando
executamos a descida gradiente como antes nesta nova função. Escolhemos
uma taxa de aprendizagem de :math:`0,4`.
.. raw:: html
.. raw:: html
.. code:: python
%matplotlib inline
from mxnet import np, npx
from d2l import mxnet as d2l
npx.set_np()
eta = 0.4
def f_2d(x1, x2):
return 0.1 * x1 ** 2 + 2 * x2 ** 2
def gd_2d(x1, x2, s1, s2):
return (x1 - eta * 0.2 * x1, x2 - eta * 4 * x2, 0, 0)
d2l.show_trace_2d(f_2d, d2l.train_2d(gd_2d))
.. figure:: output_momentum_e3683f_3_0.svg
.. raw:: html
.. raw:: html
.. code:: python
%matplotlib inline
import torch
from d2l import torch as d2l
eta = 0.4
def f_2d(x1, x2):
return 0.1 * x1 ** 2 + 2 * x2 ** 2
def gd_2d(x1, x2, s1, s2):
return (x1 - eta * 0.2 * x1, x2 - eta * 4 * x2, 0, 0)
d2l.show_trace_2d(f_2d, d2l.train_2d(gd_2d))
.. figure:: output_momentum_e3683f_6_0.svg
.. raw:: html
.. raw:: html
.. code:: python
%matplotlib inline
import tensorflow as tf
from d2l import tensorflow as d2l
eta = 0.4
def f_2d(x1, x2):
return 0.1 * x1 ** 2 + 2 * x2 ** 2
def gd_2d(x1, x2, s1, s2):
return (x1 - eta * 0.2 * x1, x2 - eta * 4 * x2, 0, 0)
d2l.show_trace_2d(f_2d, d2l.train_2d(gd_2d))
.. figure:: output_momentum_e3683f_9_0.svg
.. raw:: html
.. raw:: html
Por construção, o gradiente na direção :math:`x_2` é *muito* maior e
muda muito mais rapidamente do que na direção :math:`x_1` horizontal.
Portanto, estamos presos entre duas escolhas indesejáveis: se
escolhermos uma pequena taxa de aprendizado, garantimos que a solução
não diverge na direção :math:`x_2`, mas estamos sobrecarregados com uma
convergência lenta na direção :math:`x_1`. Por outro lado, com uma
grande taxa de aprendizado, progredimos rapidamente na direção
:math:`x_1`, mas divergimos em :math:`x_2`. O exemplo abaixo ilustra o
que acontece mesmo após um ligeiro aumento na taxa de aprendizagem de
:math:`0,4` para :math:`0,6`. A convergência na direção :math:`x_1`
melhora, mas a qualidade geral da solução é muito pior.
.. raw:: html
.. raw:: html
.. code:: python
eta = 0.6
d2l.show_trace_2d(f_2d, d2l.train_2d(gd_2d))
.. figure:: output_momentum_e3683f_15_0.svg
.. raw:: html
.. raw:: html
.. code:: python
eta = 0.6
d2l.show_trace_2d(f_2d, d2l.train_2d(gd_2d))
.. figure:: output_momentum_e3683f_18_0.svg
.. raw:: html
.. raw:: html
.. code:: python
eta = 0.6
d2l.show_trace_2d(f_2d, d2l.train_2d(gd_2d))
.. figure:: output_momentum_e3683f_21_0.svg
.. raw:: html
.. raw:: html
Método Momentum
~~~~~~~~~~~~~~~
O método do momento nos permite resolver o problema de descida gradiente
descrito acima de. Olhando para o traço de otimização acima, podemos
intuir que calcular a média de gradientes em relação ao passado
funcionaria bem. Afinal, na direção :math:`x_1`, isso agregará
gradientes bem alinhados, aumentando assim a distância que percorremos a
cada passo. Por outro lado, na direção :math:`x_2` onde os gradientes
oscilam, um gradiente agregado reduzirá o tamanho do passo devido às
oscilações que se cancelam. Usar :math:`\mathbf{v}_t` em vez do
gradiente :math:`\mathbf{g}_t` produz as seguintes equações de
atualização:
.. math::
\begin{aligned}
\mathbf{v}_t &\leftarrow \beta \mathbf{v}_{t-1} + \mathbf{g}_{t, t-1}, \\
\mathbf{x}_t &\leftarrow \mathbf{x}_{t-1} - \eta_t \mathbf{v}_t.
\end{aligned}
Observe que para :math:`\beta = 0` recuperamos a descida gradiente
regular. Antes de nos aprofundarmos nas propriedades matemáticas, vamos
dar uma olhada rápida em como o algoritmo se comporta na prática.
.. raw:: html
.. raw:: html
.. code:: python
def momentum_2d(x1, x2, v1, v2):
v1 = beta * v1 + 0.2 * x1
v2 = beta * v2 + 4 * x2
return x1 - eta * v1, x2 - eta * v2, v1, v2
eta, beta = 0.6, 0.5
d2l.show_trace_2d(f_2d, d2l.train_2d(momentum_2d))
.. figure:: output_momentum_e3683f_27_0.svg
.. raw:: html
.. raw:: html
.. code:: python
def momentum_2d(x1, x2, v1, v2):
v1 = beta * v1 + 0.2 * x1
v2 = beta * v2 + 4 * x2
return x1 - eta * v1, x2 - eta * v2, v1, v2
eta, beta = 0.6, 0.5
d2l.show_trace_2d(f_2d, d2l.train_2d(momentum_2d))
.. figure:: output_momentum_e3683f_30_0.svg
.. raw:: html
.. raw:: html
.. code:: python
def momentum_2d(x1, x2, v1, v2):
v1 = beta * v1 + 0.2 * x1
v2 = beta * v2 + 4 * x2
return x1 - eta * v1, x2 - eta * v2, v1, v2
eta, beta = 0.6, 0.5
d2l.show_trace_2d(f_2d, d2l.train_2d(momentum_2d))
.. figure:: output_momentum_e3683f_33_0.svg
.. raw:: html
.. raw:: html
Como podemos ver, mesmo com a mesma taxa de aprendizado que usamos
antes, o momentum ainda converge bem. Vamos ver o que acontece quando
diminuímos o parâmetro momentum. Reduzi-lo para :math:`\beta = 0,25`
leva a uma trajetória que quase não converge. No entanto, é muito melhor
do que sem momentum (quando a solução diverge).
.. raw:: html
.. raw:: html
.. code:: python
eta, beta = 0.6, 0.25
d2l.show_trace_2d(f_2d, d2l.train_2d(momentum_2d))
.. figure:: output_momentum_e3683f_39_0.svg
.. raw:: html
.. raw:: html
.. code:: python
eta, beta = 0.6, 0.25
d2l.show_trace_2d(f_2d, d2l.train_2d(momentum_2d))
.. figure:: output_momentum_e3683f_42_0.svg
.. raw:: html
.. raw:: html
.. code:: python
eta, beta = 0.6, 0.25
d2l.show_trace_2d(f_2d, d2l.train_2d(momentum_2d))
.. figure:: output_momentum_e3683f_45_0.svg
.. raw:: html
.. raw:: html
Observe que podemos combinar momentum com SGD e, em particular,
minibatch-SGD. A única mudança é que, nesse caso, substituímos os
gradientes :math:`\mathbf{g}_{t, t-1}` por :math:`\mathbf{g}_t`. Por
último, por conveniência, inicializamos :math:`\mathbf{v}_0 = 0` no
momento :math:`t=0`. Vejamos o que a média de vazamento realmente faz
com as atualizações.
Peso Efetivo da Amostra
~~~~~~~~~~~~~~~~~~~~~~~
Lembre-se de que
:math:`\mathbf{v}_t = \sum_{\tau = 0}^{t-1} \beta^{\tau} \mathbf{g}_{t-\tau, t-\tau-1}`.
No limite, os termos somam
:math:`\sum_{\tau=0}^\infty \beta^\tau = \frac{1}{1-\beta}`. Em outras
palavras, em vez de dar um passo de tamanho :math:`\eta` em GD ou SGD,
damos um passo de tamanho :math:`\frac{\eta}{1-\beta}` enquanto, ao
mesmo tempo, lidamos com um potencial muito direção de descida melhor
comportada. Esses são dois benefícios em um. Para ilustrar como a
ponderação se comporta para diferentes escolhas de :math:`\beta`,
considere o diagrama abaixo.
.. raw:: html
.. raw:: html
.. code:: python
d2l.set_figsize()
betas = [0.95, 0.9, 0.6, 0]
for beta in betas:
x = np.arange(40).asnumpy()
d2l.plt.plot(x, beta ** x, label=f'beta = {beta:.2f}')
d2l.plt.xlabel('time')
d2l.plt.legend();
.. figure:: output_momentum_e3683f_51_0.svg
.. raw:: html
.. raw:: html
.. code:: python
d2l.set_figsize()
betas = [0.95, 0.9, 0.6, 0]
for beta in betas:
x = torch.arange(40).detach().numpy()
d2l.plt.plot(x, beta ** x, label=f'beta = {beta:.2f}')
d2l.plt.xlabel('time')
d2l.plt.legend();
.. figure:: output_momentum_e3683f_54_0.svg
.. raw:: html
.. raw:: html
.. code:: python
d2l.set_figsize()
betas = [0.95, 0.9, 0.6, 0]
for beta in betas:
x = tf.range(40).numpy()
d2l.plt.plot(x, beta ** x, label=f'beta = {beta:.2f}')
d2l.plt.xlabel('time')
d2l.plt.legend();
.. figure:: output_momentum_e3683f_57_0.svg
.. raw:: html
.. raw:: html
Experimentos Práticos
---------------------
Vamos ver como o momentum funciona na prática, ou seja, quando usado no
contexto de um otimizador adequado. Para isso, precisamos de uma
implementação um pouco mais escalonável.
Implementação do zero
~~~~~~~~~~~~~~~~~~~~~
Em comparação com (minibatch) SGD, o método de momentum precisa manter
um conjunto de variáveis auxiliares, ou seja, a velocidade. Tem a mesma
forma dos gradientes (e variáveis do problema de otimização). Na
implementação abaixo, chamamos essas variáveis de ``estados``.
.. raw:: html
.. raw:: html
.. code:: python
def init_momentum_states(feature_dim):
v_w = np.zeros((feature_dim, 1))
v_b = np.zeros(1)
return (v_w, v_b)
def sgd_momentum(params, states, hyperparams):
for p, v in zip(params, states):
v[:] = hyperparams['momentum'] * v + p.grad
p[:] -= hyperparams['lr'] * v
.. raw:: html
.. raw:: html
.. code:: python
def init_momentum_states(feature_dim):
v_w = torch.zeros((feature_dim, 1))
v_b = torch.zeros(1)
return (v_w, v_b)
def sgd_momentum(params, states, hyperparams):
for p, v in zip(params, states):
with torch.no_grad():
v[:] = hyperparams['momentum'] * v + p.grad
p[:] -= hyperparams['lr'] * v
p.grad.data.zero_()
.. raw:: html
.. raw:: html
.. code:: python
def init_momentum_states(features_dim):
v_w = tf.Variable(tf.zeros((features_dim, 1)))
v_b = tf.Variable(tf.zeros(1))
return (v_w, v_b)
def sgd_momentum(params, grads, states, hyperparams):
for p, v, g in zip(params, states, grads):
v[:].assign(hyperparams['momentum'] * v + g)
p[:].assign(p - hyperparams['lr'] * v)
.. raw:: html
.. raw:: html
Vamos ver como isso funciona na prática.
.. raw:: html
.. raw:: html
.. code:: python
def train_momentum(lr, momentum, num_epochs=2):
d2l.train_ch11(sgd_momentum, init_momentum_states(feature_dim),
{'lr': lr, 'momentum': momentum}, data_iter,
feature_dim, num_epochs)
data_iter, feature_dim = d2l.get_data_ch11(batch_size=10)
train_momentum(0.02, 0.5)
.. parsed-literal::
:class: output
loss: 0.248, 0.363 sec/epoch
.. figure:: output_momentum_e3683f_75_1.svg
.. raw:: html
.. raw:: html
.. code:: python
def train_momentum(lr, momentum, num_epochs=2):
d2l.train_ch11(sgd_momentum, init_momentum_states(feature_dim),
{'lr': lr, 'momentum': momentum}, data_iter,
feature_dim, num_epochs)
data_iter, feature_dim = d2l.get_data_ch11(batch_size=10)
train_momentum(0.02, 0.5)
.. parsed-literal::
:class: output
loss: 0.244, 0.011 sec/epoch
.. figure:: output_momentum_e3683f_78_1.svg
.. raw:: html
.. raw:: html
.. code:: python
def train_momentum(lr, momentum, num_epochs=2):
d2l.train_ch11(sgd_momentum, init_momentum_states(feature_dim),
{'lr': lr, 'momentum': momentum}, data_iter,
feature_dim, num_epochs)
data_iter, feature_dim = d2l.get_data_ch11(batch_size=10)
train_momentum(0.02, 0.5)
.. parsed-literal::
:class: output
loss: 0.244, 0.079 sec/epoch
.. figure:: output_momentum_e3683f_81_1.svg
.. raw:: html
.. raw:: html
Quando aumentamos o hiperparâmetro de momento ``momentum`` para 0,9,
resulta em um tamanho de amostra efetivo significativamente maior de
:math:`\frac{1}{1 - 0.9} = 10`. Reduzimos ligeiramente a taxa de
aprendizagem para :math:`0,01` para manter os assuntos sob controle.
.. raw:: html
.. raw:: html
.. code:: python
train_momentum(0.01, 0.9)
.. parsed-literal::
:class: output
loss: 0.249, 0.407 sec/epoch
.. figure:: output_momentum_e3683f_87_1.svg
.. raw:: html
.. raw:: html
.. code:: python
train_momentum(0.01, 0.9)
.. parsed-literal::
:class: output
loss: 0.253, 0.011 sec/epoch
.. figure:: output_momentum_e3683f_90_1.svg
.. raw:: html
.. raw:: html
.. code:: python
train_momentum(0.01, 0.9)
.. parsed-literal::
:class: output
loss: 0.250, 0.075 sec/epoch
.. figure:: output_momentum_e3683f_93_1.svg
.. raw:: html
.. raw:: html
A redução da taxa de aprendizagem resolve ainda mais qualquer questão de
problemas de otimização não suave. Configurá-lo como :math:`0,005`
produz boas propriedades de convergência.
.. raw:: html
.. raw:: html
.. code:: python
train_momentum(0.005, 0.9)
.. parsed-literal::
:class: output
loss: 0.242, 0.369 sec/epoch
.. figure:: output_momentum_e3683f_99_1.svg
.. raw:: html
.. raw:: html
.. code:: python
train_momentum(0.005, 0.9)
.. parsed-literal::
:class: output
loss: 0.242, 0.012 sec/epoch
.. figure:: output_momentum_e3683f_102_1.svg
.. raw:: html
.. raw:: html
.. code:: python
train_momentum(0.005, 0.9)
.. parsed-literal::
:class: output
loss: 0.248, 0.072 sec/epoch
.. figure:: output_momentum_e3683f_105_1.svg
.. raw:: html
.. raw:: html
Implementação concisa
~~~~~~~~~~~~~~~~~~~~~
Há muito pouco a fazer no Gluon, uma vez que o solucionador ``sgd``
padrão já tem o momentum embutido. A configuração dos parâmetros
correspondentes produz uma trajetória muito semelhante.
.. raw:: html
.. raw:: html
.. code:: python
d2l.train_concise_ch11('sgd', {'learning_rate': 0.005, 'momentum': 0.9},
data_iter)
.. parsed-literal::
:class: output
loss: 0.245, 0.346 sec/epoch
.. figure:: output_momentum_e3683f_111_1.svg
.. raw:: html
.. raw:: html
.. code:: python
trainer = torch.optim.SGD
d2l.train_concise_ch11(trainer, {'lr': 0.005, 'momentum': 0.9}, data_iter)
.. parsed-literal::
:class: output
loss: 0.245, 0.011 sec/epoch
.. figure:: output_momentum_e3683f_114_1.svg
.. raw:: html
.. raw:: html
.. code:: python
trainer = tf.keras.optimizers.SGD
d2l.train_concise_ch11(trainer, {'learning_rate': 0.005, 'momentum': 0.9},
data_iter)
.. parsed-literal::
:class: output
loss: 0.247, 0.079 sec/epoch
.. figure:: output_momentum_e3683f_117_1.svg
.. raw:: html
.. raw:: html
Análise teórica
---------------
Até agora, o exemplo 2D de :math:`f(x) = 0.1 x_1^2 + 2 x_2^2` parecia
bastante artificial. Veremos agora que isso é na verdade bastante
representativo dos tipos de problemas que podemos encontrar, pelo menos
no caso de minimizar funções objetivas quadráticas convexas.
Funções quadráticas convexas
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Considere a função
.. math:: h(\mathbf{x}) = \frac{1}{2} \mathbf{x}^\top \mathbf{Q} \mathbf{x} + \mathbf{x}^\top \mathbf{c} + b.
Esta é uma função quadrática geral. Para matrizes definidas positivas
:math:`\mathbf{Q} \succ 0`, ou seja, para matrizes com autovalores
positivos, tem um minimizador em
:math:`\mathbf{x}^* = -\mathbf{Q}^{-1} \mathbf{c}` com valor mínimo
:math:`b - \frac{1}{2} \mathbf{c}^\top \mathbf{Q}^{-1} \mathbf{c}`.
Portanto, podemos reescrever :math:`h` como
.. math:: h(\mathbf{x}) = \frac{1}{2} (\mathbf{x} - \mathbf{Q}^{-1} \mathbf{c})^\top \mathbf{Q} (\mathbf{x} - \mathbf{Q}^{-1} \mathbf{c}) + b - \frac{1}{2} \mathbf{c}^\top \mathbf{Q}^{-1} \mathbf{c}.
O gradiente é dado por
:math:`\partial_{\mathbf{x}} f(\mathbf{x}) = \mathbf{Q} (\mathbf{x} - \mathbf{Q}^{-1} \mathbf{c})`.
Ou seja, é dada pela distância entre :math:`\mathbf{x}` e o minimizador,
multiplicada por :math:`\mathbf{Q}`. Consequentemente, também o momento
é uma combinação linear de termos
:math:`\mathbf{Q} (\mathbf{x}_t - \mathbf{Q}^{-1} \mathbf{c})`.
Uma vez que :math:`\mathbf{Q}` é definido positivo, pode ser decomposto
em seu auto-sistema via
:math:`\mathbf{Q} = \mathbf{O}^\top \boldsymbol{\Lambda} \mathbf{O}`
para um ortogonal ( rotação) matriz :math:`\mathbf{O}` e uma matriz
diagonal :math:`\boldsymbol{\Lambda}` de autovalores positivos. Isso nos
permite realizar uma mudança de variáveis de :math:`\mathbf{x}` para
:math:`\mathbf{z} := \mathbf{O} (\mathbf{x} - \mathbf{Q}^{-1} \mathbf{c})`
para obter uma expressão muito simplificada:
.. math:: h(\mathbf{z}) = \frac{1}{2} \mathbf{z}^\top \boldsymbol{\Lambda} \mathbf{z} + b'.
Aqui
:math:`c' = b - \frac{1}{2} \mathbf{c}^\top \mathbf{Q}^{-1} \mathbf{c}`.
Uma vez que :math:`\mathbf{O}` é apenas uma matriz ortogonal, isso não
perturba os gradientes de uma forma significativa. Expresso em termos de
:math:`\mathbf{z}` gradiente, a descida torna-se
.. math:: \mathbf{z}_t = \mathbf{z}_{t-1} - \boldsymbol{\Lambda} \mathbf{z}_{t-1} = (\mathbf{I} - \boldsymbol{\Lambda}) \mathbf{z}_{t-1}.
O fato importante nesta expressão é que a descida gradiente *não se
mistura* entre diferentes espaços auto. Ou seja, quando expresso em
termos do autossistema de :math:`\mathbf{Q}`, o problema de otimização
ocorre de maneira coordenada. Isso também vale para o momento.
.. math::
\begin{aligned}
\mathbf{v}_t & = \beta \mathbf{v}_{t-1} + \boldsymbol{\Lambda} \mathbf{z}_{t-1} \\
\mathbf{z}_t & = \mathbf{z}_{t-1} - \eta \left(\beta \mathbf{v}_{t-1} + \boldsymbol{\Lambda} \mathbf{z}_{t-1}\right) \\
& = (\mathbf{I} - \eta \boldsymbol{\Lambda}) \mathbf{z}_{t-1} - \eta \beta \mathbf{v}_{t-1}.
\end{aligned}
Ao fazer isso, acabamos de provar o seguinte teorema: Gradiente
descendente com e sem momento para uma função quadrática convexa se
decompõe em otimização coordenada na direção dos vetores próprios da
matriz quadrática.
Funções Escalares
~~~~~~~~~~~~~~~~~
Dado o resultado acima, vamos ver o que acontece quando minimizamos a
função :math:`f(x) = \frac{\lambda}{2} x^2`. Para descida gradiente,
temos
.. math:: x_{t+1} = x_t - \eta \lambda x_t = (1 - \eta \lambda) x_t.
Sempre que :math:`|1 - \eta \lambda| < 1` esta otimização converge a uma
taxa exponencial, pois após :math:`t` passos temos
:math:`x_t = (1 - \eta \lambda)^t x_0`. Isso mostra como a taxa de
convergência melhora inicialmente à medida que aumentamos a taxa de
aprendizado :math:`\eta` até :math:`\eta \lambda = 1`. Além disso, as
coisas divergem e para :math:`\eta \lambda > 2` o problema de otimização
diverge.
.. raw:: html
.. raw:: html
.. code:: python
lambdas = [0.1, 1, 10, 19]
eta = 0.1
d2l.set_figsize((6, 4))
for lam in lambdas:
t = np.arange(20).asnumpy()
d2l.plt.plot(t, (1 - eta * lam) ** t, label=f'lambda = {lam:.2f}')
d2l.plt.xlabel('time')
d2l.plt.legend();
.. figure:: output_momentum_e3683f_123_0.svg
.. raw:: html
.. raw:: html
.. code:: python
lambdas = [0.1, 1, 10, 19]
eta = 0.1
d2l.set_figsize((6, 4))
for lam in lambdas:
t = torch.arange(20).detach().numpy()
d2l.plt.plot(t, (1 - eta * lam) ** t, label=f'lambda = {lam:.2f}')
d2l.plt.xlabel('time')
d2l.plt.legend();
.. figure:: output_momentum_e3683f_126_0.svg
.. raw:: html
.. raw:: html
.. code:: python
lambdas = [0.1, 1, 10, 19]
eta = 0.1
d2l.set_figsize((6, 4))
for lam in lambdas:
t = tf.range(20).numpy()
d2l.plt.plot(t, (1 - eta * lam) ** t, label=f'lambda = {lam:.2f}')
d2l.plt.xlabel('time')
d2l.plt.legend();
.. figure:: output_momentum_e3683f_129_0.svg
.. raw:: html
.. raw:: html
Para analisar a convergência no caso de momentum, começamos reescrevendo
as equações de atualização em termos de dois escalares: um para
:math:`x` e outro para o momentum :math:`v`. Isso produz:
.. math::
\begin{bmatrix} v_{t+1} \\ x_{t+1} \end{bmatrix} =
\begin{bmatrix} \beta & \lambda \\ -\eta \beta & (1 - \eta \lambda) \end{bmatrix}
\begin{bmatrix} v_{t} \\ x_{t} \end{bmatrix} = \mathbf{R}(\beta, \eta, \lambda) \begin{bmatrix} v_{t} \\ x_{t} \end{bmatrix}.
Usamos :math:`\mathbf{R}` para denotar o comportamento de convergência
que rege :math:`2 \times 2`. Após :math:`t` passos, a escolha inicial
:math:`[v_0, x_0]` torna-se
:math:`\mathbf{R}(\beta, \eta, \lambda)^t [v_0, x_0]`. Consequentemente,
cabe aos autovalores de :math:`\mathbf{R}` determinar a velocidade de
convergência. Veja o `Post do
Destill `__ de :cite:`Goh.2017`
para uma ótima animação e :cite:`Flammarion.Bach.2015` para uma
análise detalhada. Pode-se mostrar que
:math:`0 < \eta \lambda < 2 + 2 \beta` momentum converge. Este é um
intervalo maior de parâmetros viáveis quando comparado a
:math:`0 < \eta \lambda < 2` para descida de gradiente. Também sugere
que, em geral, grandes valores de :math:`\beta` são desejáveis. Mais
detalhes requerem uma boa quantidade de detalhes técnicos e sugerimos
que o leitor interessado consulte as publicações originais.
Sumário
-------
- Momentum substitui gradientes por uma média com vazamento em relação
aos gradientes anteriores. Isso acelera a convergência
significativamente.
- É desejável tanto para descida gradiente sem ruído quanto para
descida gradiente estocástica (ruidosa).
- O momentum evita a paralisação do processo de otimização, que é muito
mais provável de ocorrer na descida do gradiente estocástico.
- O número efetivo de gradientes é dado por :math:`\frac{1}{1-\beta}`
devido à redução exponenciada de dados anteriores.
- No caso de problemas quadráticos convexos, isso pode ser analisado
explicitamente em detalhes.
- A implementação é bastante direta, mas exige que armazenemos um vetor
de estado adicional (momentum :math:`\mathbf{v}`).
Exercícios
----------
1. Use outras combinações de hiperparâmetros de momentum e taxas de
aprendizagem e observe e analise os diferentes resultados
experimentais.
2. Experimente GD e momentum para um problema quadrático onde você tem
vários autovalores, ou seja,
:math:`f(x) = \frac{1}{2} \sum_i \lambda_i x_i^2` ou seja
:math:`\lambda_i = 2^{-i}`. Trace como os valores de :math:`x`
diminuem para a inicialização :math:`x_i = 1`.
3. Derive o valor mínimo e minimizador para
:math:`h(\mathbf{x}) = \frac{1}{2} \mathbf{x}^\top \mathbf{Q} \mathbf{x} + \mathbf{x}^\top \mathbf{c} + b`.
4. O que muda quando executamos SGD com momentum? O que acontece quando
usamos minibatch SGD com momentum? Experimentar com os parâmetros?
.. raw:: html
.. raw:: html
`Discussão `__
.. raw:: html
.. raw:: html
`Discussão `__
.. raw:: html
.. raw:: html
`Discussão `__
.. raw:: html
.. raw:: html
.. raw:: html