.. _sec_sgd:
Gradiente Descendente Estocástico
=================================
Nesta seção, apresentaremos os princípios básicos da gradiente
descendente estocástico.
.. raw:: html
.. raw:: html
.. code:: python
f = lambda x1, x2: x1 ** 2 + 2 * x2 ** 2 # Objective
gradf = lambda x1, x2: (2 * x1, 4 * x2) # Gradient
def sgd(x1, x2, s1, s2):
global lr # Learning rate scheduler
(g1, g2) = gradf(x1, x2)
# Simulate noisy gradient
g1 += np.random.normal(0.0, 1, (1,))
g2 += np.random.normal(0.0, 1, (1,))
eta_t = eta * lr() # Learning rate at time t
return (x1 - eta_t * g1, x2 - eta_t * g2, 0, 0) # Update variables
eta = 0.1
lr = (lambda: 1) # Constant learning rate
d2l.show_trace_2d(f, d2l.train_2d(sgd, steps=50))
.. figure:: output_sgd_baca77_15_0.svg
.. raw:: html
.. raw:: html
.. code:: python
f = lambda x1, x2: x1 ** 2 + 2 * x2 ** 2 # Objective
gradf = lambda x1, x2: (2 * x1, 4 * x2) # Gradient
def sgd(x1, x2, s1, s2):
global lr # Learning rate scheduler
(g1, g2) = gradf(x1, x2)
# Simulate noisy gradient
g1 += torch.normal(0.0, 1, (1,))
g2 += torch.normal(0.0, 1, (1,))
eta_t = eta * lr() # Learning rate at time t
return (x1 - eta_t * g1, x2 - eta_t * g2, 0, 0) # Update variables
eta = 0.1
lr = (lambda: 1) # Constant learning rate
d2l.show_trace_2d(f, d2l.train_2d(sgd, steps=50))
.. figure:: output_sgd_baca77_18_0.svg
.. raw:: html
.. raw:: html
.. code:: python
f = lambda x1, x2: x1 ** 2 + 2 * x2 ** 2 # Objective
gradf = lambda x1, x2: (2 * x1, 4 * x2) # Gradient
def sgd(x1, x2, s1, s2):
global lr # Learning rate scheduler
(g1, g2) = gradf(x1, x2)
# Simulate noisy gradient
g1 += tf.random.normal([1], 0.0, 1)
g2 += tf.random.normal([1], 0.0, 1)
eta_t = eta * lr() # Learning rate at time t
return (x1 - eta_t * g1, x2 - eta_t * g2, 0, 0) # Update variables
eta = 0.1
lr = (lambda: 1) # Constant learning rate
d2l.show_trace_2d(f, d2l.train_2d(sgd, steps=50))
.. figure:: output_sgd_baca77_21_0.svg
.. raw:: html
.. raw:: html
Como podemos ver, a trajetória das variáveis no SGD é muito mais
ruidosa do que a que observamos na descida do gradiente na seção
anterior. Isso se deve à natureza estocástica do gradiente. Ou seja,
mesmo quando chegamos perto do mínimo, ainda estamos sujeitos à
incerteza injetada pelo gradiente instantâneo via
:math:`\eta \nabla f_i(\mathbf{x})`. Mesmo após 50 passos, a qualidade
ainda não é tão boa. Pior ainda, não melhorará após etapas adicionais
(encorajamos o leitor a experimentar um número maior de etapas para
confirmar isso por conta própria). Isso nos deixa com a única
alternativa — alterar a taxa de aprendizagem :math:`\eta`. No entanto,
se escolhermos isso muito pequeno, não faremos nenhum progresso
significativo inicialmente. Por outro lado, se o escolhermos muito
grande, não obteremos uma boa solução, como visto acima. A única maneira
de resolver esses objetivos conflitantes é reduzir a taxa de aprendizado
*dinamicamente* à medida que a otimização avança.
Esta também é a razão para adicionar uma função de taxa de aprendizagem
``lr`` na função de passo\ ``sgd``. No exemplo acima, qualquer
funcionalidade para o agendamento da taxa de aprendizagem permanece
latente, pois definimos a função ``lr`` associada como constante, ou
seja,\ ``lr = (lambda: 1)``.
Taxa de aprendizagem dinâmica
-----------------------------
Substituir :math:`\eta` por uma taxa de aprendizado dependente do tempo
:math:`\eta(t)` aumenta a complexidade de controlar a convergência de um
algoritmo de otimização. Em particular, é preciso descobrir com que
rapidez :math:`\eta` deve decair. Se for muito rápido, pararemos de
otimizar prematuramente. Se diminuirmos muito lentamente, perderemos
muito tempo com a otimização. Existem algumas estratégias básicas que
são usadas no ajuste de :math:`\eta` ao longo do tempo (discutiremos
estratégias mais avançadas em um capítulo posterior):
.. math::
\begin{aligned}
\eta(t) & = \eta_i \text{ if } t_i \leq t \leq t_{i+1} && \mathrm{piecewise~constant} \\
\eta(t) & = \eta_0 \cdot e^{-\lambda t} && \mathrm{exponential} \\
\eta(t) & = \eta_0 \cdot (\beta t + 1)^{-\alpha} && \mathrm{polynomial}
\end{aligned}
No primeiro cenário, diminuímos a taxa de aprendizado, por exemplo,
sempre que o progresso na otimização para. Esta é uma estratégia comum
para treinar redes profundas. Alternativamente, poderíamos diminuí-lo de
forma muito mais agressiva por uma redução exponencial. Infelizmente,
isso leva a uma parada prematura antes que o algoritmo tenha convergido.
Uma escolha popular é o decaimento polinomial com :math:`\alpha = 0.5`.
No caso da otimização convexa, há uma série de provas que mostram que
essa taxa é bem comportada. Vamos ver como isso se parece na prática.
.. raw:: html
.. raw:: html
.. code:: python
def exponential():
global ctr
ctr += 1
return math.exp(-0.1 * ctr)
ctr = 1
lr = exponential # Set up learning rate
d2l.show_trace_2d(f, d2l.train_2d(sgd, steps=1000))
.. figure:: output_sgd_baca77_27_0.svg
.. raw:: html
.. raw:: html
.. code:: python
def exponential():
global ctr
ctr += 1
return math.exp(-0.1 * ctr)
ctr = 1
lr = exponential # Set up learning rate
d2l.show_trace_2d(f, d2l.train_2d(sgd, steps=1000))
.. figure:: output_sgd_baca77_30_0.svg
.. raw:: html
.. raw:: html
.. code:: python
def exponential():
global ctr
ctr += 1
return math.exp(-0.1 * ctr)
ctr = 1
lr = exponential # Set up learning rate
d2l.show_trace_2d(f, d2l.train_2d(sgd, steps=1000))
.. figure:: output_sgd_baca77_33_0.svg
.. raw:: html
.. raw:: html
Como esperado, a variância nos parâmetros é reduzida significativamente.
No entanto, isso ocorre às custas de não convergir para a solução ótima
:math:`\mathbf{x} = (0, 0)`. Mesmo depois de 1000 passos, ainda estamos
muito longe da solução ideal. Na verdade, o algoritmo não consegue
convergir. Por outro lado, se usarmos um decaimento polinomial onde a
taxa de aprendizagem decai com a raiz quadrada inversa do número de
passos, a convergência é boa.
.. raw:: html
.. raw:: html
.. code:: python
def polynomial():
global ctr
ctr += 1
return (1 + 0.1 * ctr)**(-0.5)
ctr = 1
lr = polynomial # Set up learning rate
d2l.show_trace_2d(f, d2l.train_2d(sgd, steps=50))
.. figure:: output_sgd_baca77_39_0.svg
.. raw:: html
.. raw:: html
.. code:: python
def polynomial():
global ctr
ctr += 1
return (1 + 0.1 * ctr)**(-0.5)
ctr = 1
lr = polynomial # Set up learning rate
d2l.show_trace_2d(f, d2l.train_2d(sgd, steps=50))
.. figure:: output_sgd_baca77_42_0.svg
.. raw:: html
.. raw:: html
.. code:: python
def polynomial():
global ctr
ctr += 1
return (1 + 0.1 * ctr)**(-0.5)
ctr = 1
lr = polynomial # Set up learning rate
d2l.show_trace_2d(f, d2l.train_2d(sgd, steps=50))
.. figure:: output_sgd_baca77_45_0.svg
.. raw:: html
.. raw:: html
Existem muitas outras opções de como definir a taxa de aprendizagem. Por
exemplo, poderíamos começar com uma taxa pequena, aumentar rapidamente e
diminuí-la novamente, embora mais lentamente. Poderíamos até alternar
entre taxas de aprendizagem menores e maiores. Existe uma grande
variedade de tais horários. Por enquanto, vamos nos concentrar em
tabelas de taxas de aprendizagem para as quais uma análise teórica
abrangente é possível, ou seja, em taxas de aprendizagem em um cenário
convexo. Para problemas não-convexos gerais é muito difícil obter
garantias de convergência significativas, uma vez que, em geral,
minimizar problemas não-convexos não lineares é NP difícil. Para uma
pesquisa, consulte, por exemplo, as excelentes `notas de
aula