Ir para o conteúdo

Overview

Handout prático — Treinando um MLP: batch, otimizador e learning rate

Laboratório de duas horas com MNIST que mantém um MLP fixo — 784 → 128 → 64 → 10 — e varia só as três escolhas de treino: tamanho do batch, otimizador e learning rate. Todos os gráficos vêm de execuções reais, e a página traz dois simuladores e sete checkpoints com solução.

abrir o handout

Treinar uma rede neural é um problema de otimização. Temos uma função de perda \(J(\theta)\) que mede o quanto o modelo erra e procuramos os parâmetros \(\theta^\star\) que a tornam a menor possível:

\[ \theta^\star = \arg\min_{\theta} \; J(\theta) \]

Para regressão linear isso tem solução fechada e vale entender por quê. Com um modelo linear e erro quadrático, \(J\) é uma função quadrática de \(\theta\), logo o gradiente é linear em \(\theta\) — e perguntar onde o gradiente se anula vira apenas um sistema de equações lineares, que sabemos resolver exatamente:

\[ \nabla J(\theta) = 0 \quad\Longleftrightarrow\quad X^{\top}\!X\,\theta = X^{\top}y \quad\Longrightarrow\quad \theta^\star = (X^{\top}\!X)^{-1}X^{\top}y \]

Coloque uma ativação não linear no meio e isso desaba: \(\nabla J = 0\) passa a ser um sistema de equações não lineares, sem fórmula que isole \(\theta\). E, mesmo quando a fórmula existe, ela pede uma inversão de matriz — algo em torno de \(O(n^3)\) operações para \(n\) parâmetros, inviável quando \(n\) está na casa dos milhões. Então fazemos a única coisa que resta: começar em algum lugar e caminhar ladeira abaixo.

A intuição que todo mundo lembra

Você está em uma montanha, à noite, com neblina densa. Quer chegar ao vilarejo no vale, mas só enxerga o chão sob os próprios pés. Então sente a inclinação ao redor, dá um passo na direção de descida mais acentuada e repete1.

Duas decisões definem todo o algoritmo: em qual direção pisar (o gradiente responde isso) e qual o tamanho do passo (essa é a taxa de aprendizado — e é de onde vem quase todo o problema).

O gradiente aponta para cima

Para uma função de várias variáveis, o gradiente é o vetor das derivadas parciais:

\[ \nabla J(\theta) = \left[ \frac{\partial J}{\partial \theta_1}, \frac{\partial J}{\partial \theta_2}, \dots, \frac{\partial J}{\partial \theta_n} \right]^{\top} \]

Cada entrada responde a uma pergunta local: se eu aumentar um pouquinho este parâmetro, quanto a perda muda? Juntas, elas apontam na direção de subida mais acentuada e a magnitude do vetor diz o quão íngreme é a encosta. Como queremos descer, andamos contra ele — daí o sinal negativo na regra de atualização:

Por que para cima?

Comece com um parâmetro só: \(J(\theta) = \theta^2\).

Em \(\theta = 3\) a derivada vale \(+6\). Leia esse número como uma frase: um passinho para a direita aumenta a perda — e a aumenta a uma taxa de 6 por unidade de passo. Derivada positiva quer dizer, então, que a direita é subida.

Em \(\theta = -3\) a derivada vale \(-6\): agora é o sinal negativo que indica o lado, a esquerda, e é para a esquerda que a perda cresce. Repare que nos dois pontos ela aponta para o lado oposto ao do mínimo.

Com vários parâmetros nada muda, exceto que agora há uma derivada por eixo. Suponha

\[ \frac{\partial J}{\partial \theta_1} = +6, \qquad \frac{\partial J}{\partial \theta_2} = -2 \]

Lendo uma de cada vez: aumentar \(\theta_1\) sobe a perda a 6 por unidade; aumentar \(\theta_2\) a desce a 2 por unidade. Para subir o mais rápido possível você faz as duas coisas ao mesmo tempo — aumenta \(\theta_1\) e diminui \(\theta_2\) — e dá um passo três vezes maior em \(\theta_1\) do que em \(\theta_2\), porque ali a perda cresce três vezes mais rápido. Ou seja: você anda no sentido do vetor \((+6, -2)\), que é o próprio \(\nabla J\).

Para descer, faça o oposto: \((-6, +2) = -\nabla J\). É só isso que o sinal de menos da regra de atualização quer dizer2.

\[ \theta_{t+1} = \theta_t - \eta \, \nabla J(\theta_t) \]

Gradiente Descendente Puro (Vanilla)

onde:

  • \(\theta\) são os parâmetros do modelo,
  • \(\eta\) é a taxa de aprendizado, hiperparâmetro que controla o tamanho do passo,
  • \(\nabla J(\theta)\) é o gradiente da perda em relação aos parâmetros.

Um método baseado em gradiente é qualquer algoritmo que encontra mínimos de uma função supondo que o gradiente é barato de calcular. Exige que a função seja contínua e diferenciável quase em todo lugar — a ReLU tem um bico em zero e isso não é problema1. Tudo neste capítulo é uma variação da regra acima: o mesmo esqueleto, com formas mais espertas de escolher a direção e o tamanho do passo.

Trajetória do gradiente descendente

O gradiente descendente descendo uma superfície de perda. Repare que o passo é longo onde a superfície é íngreme e curto perto do fundo — o próprio gradiente encolhe conforme nos aproximamos do mínimo.

A taxa de aprendizado: o hiperparâmetro que você precisa sentir

Considere a perda mais simples possível, \(J(\theta) = \theta^2\), cujo gradiente é \(2\theta\). A atualização vira

\[ \theta_{t+1} = \theta_t - \eta \cdot 2\theta_t = (1 - 2\eta)\,\theta_t \]

Todo o comportamento do gradiente descendente está dentro desse fator \((1 - 2\eta)\). Brinque com a taxa de aprendizado abaixo e veja quatro mundos qualitativamente diferentes aparecerem.

As transições nesse simulador não são arbitrárias. Para uma quadrática cuja curvatura (segunda derivada) é \(L\) — a letra vem de Lipschitz3 — o gradiente descendente converge somente se

\[ \eta < \frac{2}{L} \]

Aqui \(L = 2\), então a fronteira fica exatamente em \(\eta = 1\) — teste. Abaixo de \(1/L = 0{,}5\) a aproximação é monótona; entre \(1/L\) e \(2/L\) ela ziguezagueia sobre o mínimo; acima de \(2/L\) explode. Superfícies de perda reais não são quadráticas, mas perto de um mínimo elas se parecem com uma — e esse limite é a razão pela qual uma taxa de aprendizado que funcionava ontem faz o modelo divergir hoje depois que você mudou a arquitetura: você mudou o \(L\).

Como ler um log de treino

  • Perda caindo devagar e suavemente → η pequeno demais, ou você está em um platô.
  • Perda caindo mas tremendo muito → η um pouco grande demais (ou batch pequeno demais).
  • Perda subindo e descendo entre épocas → η na região de oscilação.
  • Perda virou NaN → η passou do limite de estabilidade, ou gradientes explodindo. Divida η por 10 primeiro.

Quantos dados por passo? Batch, Estocástico e Mini-Batch

A perda sobre um conjunto de \(N\) amostras é a média das perdas individuais:

\[ J(\theta) = \frac{1}{N}\sum_{i=1}^{N} J\big(\theta; x^{(i)}, y^{(i)}\big) \]

Calcular \(\nabla J\) exatamente significa uma passada completa pelos dados para uma única atualização. As três variantes clássicas diferem apenas em quantas amostras são olhadas antes de dar o passo.

Usa o conjunto inteiro a cada atualização. O gradiente é exato, a trajetória é suave — e, na ImageNet, você teria uma atualização por passada em 1,2 milhão de imagens.

\[ \theta_{t+1} = \theta_t - \eta \, \frac{1}{N} \sum_{i=1}^{N} \nabla J(\theta_t; x^{(i)}, y^{(i)}) \]
for epoch in range(num_epochs):
    gradients = compute_gradients(X, y, model)      # uma passada por tudo
    model.parameters -= learning_rate * gradients   # uma única atualização

Uma amostra por atualização. Cada gradiente é uma estimativa péssima do gradiente verdadeiro, mas é não enviesada4 — na média aponta para o lado certo — e você ganha \(N\) atualizações por época.

\[ \theta_{t+1} = \theta_t - \eta \, \nabla J(\theta_t; x^{(i)}, y^{(i)}) \]
for epoch in range(num_epochs):
    for i in random_permutation(len(X)):            # embaralhe a cada época
        gradients = compute_gradients(X[i], y[i], model)
        model.parameters -= learning_rate * gradients

Um pequeno subconjunto aleatório de \(B\) amostras — tipicamente de 32 a 512. Amostras suficientes para a estimativa ser utilizável, poucas o bastante para o passo ser barato e em um formato que a GPU consegue paralelizar5.

\[ \theta_{t+1} = \theta_t - \eta \, \frac{1}{B} \sum_{i=1}^{B} \nabla J(\theta_t; x^{(i)}, y^{(i)}) \]
for epoch in range(num_epochs):
    for batch in get_mini_batches(X, y, batch_size):
        gradients = compute_gradients(batch.X, batch.y, model)
        model.parameters -= learning_rate * gradients

O fato quantitativo essencial: fazer a média de \(B\) amostras independentes divide o desvio-padrão (dp) da estimativa do gradiente por \(\sqrt{B}\).

\[ \operatorname{dp}\big[\hat{g}_B\big] \;=\; \frac{\sigma}{\sqrt{B}} \]

Essa raiz quadrada é o motivo de o tamanho do batch ter retorno decrescente: passar de 32 para 128 custa 4× mais computação por passo e reduz o ruído apenas pela metade. E o ruído nunca desaparece de vez — com taxa de aprendizado constante, o SGD não converge para o mínimo, ele converge para uma nuvem em torno do mínimo, cujo raio cresce com \(\eta\) e encolhe com \(\sqrt{B}\). Veja acontecer:

O que o círculo tracejado significa

É o raio teórico da nuvem estacionária, \(r \approx \dfrac{\eta\,\sigma}{\sqrt{B}\sqrt{2\mu}}\) para uma quadrática de curvatura \(\mu\). Há duas formas de encolhê-lo: mais dados por passo (\(B\) maior, mas só na proporção de \(\sqrt{B}\)) ou passos menores (\(\eta\) menor, linearmente). A segunda é de graça — e é exatamente por isso que todo treino sério decai a taxa de aprendizado no fim. Você explora com passos grandes e ruidosos no começo e se acomoda com passos pequenos e quietos no final.

Batch e taxa de aprendizado andam juntos

Se você multiplica o tamanho do batch por \(k\), o ruído do gradiente cai e normalmente dá para dar passos maiores. As heurísticas comuns são a regra de escala linear (\(\eta \to k\eta\), usada para treinar a ResNet-50 em uma hora com batch 8192)6 e a regra da raiz quadrada (\(\eta \to \sqrt{k}\eta\), que casa com o argumento de ruído acima e costuma funcionar melhor com o Adam). Nunca mude o tamanho do batch esperando que a taxa antiga continue ótima.

Por que o gradiente descendente puro sofre

Se a superfície de perda fosse uma tigela redonda e bem-comportada, uma única taxa de aprendizado bastaria e este capítulo acabaria aqui. Superfícies reais não são redondas e três patologias específicas explicam todos os otimizadores que vêm a seguir.

  • Vales estreitos (mau condicionamento)


    A superfície é muito mais íngreme em uma direção do que na outra. O gradiente aponta principalmente para a parede do vale e não ao longo dele, então os iterados ziguezagueiam de um lado para o outro enquanto mal avançam rumo ao mínimo.

    O limite de estabilidade \(\eta < 2/L\) é ditado pela direção mais íngreme, enquanto o progresso na direção mais plana anda a \(\eta\mu\). A razão \(\kappa = L/\mu\) — o número de condição — é a dificuldade real: a convergência leva \(O(\kappa \log 1/\varepsilon)\) passos. Redes profundas têm rotineiramente \(\kappa\) na casa dos milhares.

  • Pontos de sela e platôs


    Um ponto onde o gradiente se anula, mas que é mínimo em algumas direções e máximo em outras. O gradiente descendente quase para ali, porque aquilo que o move — o gradiente — é praticamente zero.

    Em alta dimensão, selas são muito mais comuns que mínimos locais: para um ponto crítico ser um mínimo local, todas as \(n\) direções de curvatura precisam ser positivas e isso fica exponencialmente improvável conforme \(n\) cresce. Essa é a visão moderna: o inimigo é a planura, não os mínimos ruins.7

  • Ruído e não estacionariedade


    Cada mini-batch fornece um gradiente ligeiramente diferente e as camadas mudam umas sob as outras durante o treino — então a superfície que uma camada enxerga nem sequer é fixa.

    Um bom otimizador precisa, portanto, fazer média ao longo do tempo (isso é o momentum) e ser robusto a gradientes cuja escala varia em ordens de grandeza entre camadas (isso é a adaptatividade).

Um número para guardar: o número de condição

Para \(J(x,y) = \tfrac{1}{2}(\mu x^2 + L y^2)\) com \(L \gg \mu\), a melhor taxa de aprendizado possível é \(\eta = 2/(L+\mu)\) e mesmo assim o erro só decai por um fator de \(\frac{\kappa-1}{\kappa+1}\) por passo. Com \(\kappa = 100\) isso dá \(0{,}98\) — cerca de 115 passos para ganhar uma única ordem de grandeza e com \(\kappa = 10\,000\) são 11 500. Tudo o que vem abaixo é uma tentativa de escapar desse \(\kappa\).

Momentum: dê massa à bolinha

Em vez de andar na direção do gradiente atual, acumule uma média móvel exponencial dos gradientes passados e ande nessa direção. Agora são duas variáveis de estado, \(V\) e \(\theta\):

\[ V_{t+1} = \beta V_t + (1 - \beta) \nabla J(\theta_t) \]
\[ \theta_{t+1} = \theta_t - \eta V_{t+1} \]

\(V\) é uma média corrente dos gradientes: a cada passo amortecemos o valor antigo por \(\beta\) (entre 0 e 1) e misturamos o gradiente novo. Depois movemos \(\theta\) na direção do novo momento \(V\)1.

Por que isso resolve os vales estreitos. Na travessia do vale o gradiente troca de sinal a cada passo, então termos consecutivos se cancelam na média e a oscilação é amortecida. Ao longo do vale o sinal nunca muda, então nada se cancela e a média se acomoda no próprio gradiente. Uma única regra, dois efeitos opostos nas duas direções — o truque é esse.

Quanto de histórico entra nessa média é dado por

\[ \frac{1}{1-\beta} \]

o número efetivo de gradientes passados na média móvel: cerca de 10 passos com \(\beta = 0{,}9\) e cerca de 100 com \(\beta = 0{,}99\). Leia isso como comprimento de memória, não como aceleração — na forma escrita acima, quando \(V\) se acomoda sobre um gradiente constante o passo vale \(\eta V = \eta g\), exatamente o passo do SGD. (Na verdade ele chega atrasado: ao longo de uma rodada longa o momentum fica para trás do SGD puro em \(\beta/(1-\beta)\) passos de progresso, 9 deles com \(\beta = 0{,}9\).)

O que o momentum compra, portanto, não é um passo maior e sim uma direção melhor: o que alterna se cancela, o que persiste sobrevive. Ele também continua andando com gradientes velhos — por isso atravessa pequenas irregularidades e por isso pode passar direto por um mínimo em vez de frear nele.

Forma equivalente: a bola pesada

\[ \theta_{t+1} = \theta_t - \eta \nabla J(\theta_t) + \beta(\theta_t - \theta_{t-1}), \qquad 0 \le \beta < 1 \]

O próximo passo é uma combinação da direção do passo anterior com o novo gradiente negativo. É o método heavy ball de Polyak8 — literalmente a física de uma bola com massa rolando ladeira abaixo, onde \(\beta\) faz o papel de \(1-\text{atrito}\).

Expandindo a recursão, isso vira \(v_{t+1} = \beta v_t + g_t\), \(\theta_{t+1} = \theta_t - \eta v_{t+1}\) — o mesmo método de antes, mas sem o \((1-\beta)\), de modo que o passo é \(1/(1-\beta)\) vezes maior para o mesmo \(\eta\). As duas formas descrevem trajetórias idênticas depois de reescalar a taxa de aprendizado; qual delas você tem em mãos, porém, faz muita diferença na prática — como mostra a caixa seguinte.

O PyTorch não usa a fórmula acima

torch.optim.SGD(momentum=0.9) implementa

\[v_{t+1} = \beta v_t + g_t, \qquad \theta_{t+1} = \theta_t - \eta v_{t+1}\]

sem o fator \((1-\beta)\). A direção é idêntica, mas o passo efetivo é \(1/(1-\beta)\) vezes maior — ou seja, 10× maior com \(\beta = 0{,}9\). Essa é a causa mais comum de uma taxa de aprendizado copiada de um artigo explodir no framework de outra pessoa.

Nesterov: olhe antes de pular

O Nesterov Accelerated Gradient910 avalia o gradiente depois do passo de momentum, não antes:

\[ V_{t+1} = \beta V_t + \nabla J(\theta_t - \eta\beta V_t), \qquad \theta_{t+1} = \theta_t - \eta V_{t+1} \]

A ideia é um termo de correção: já que sabemos que a inércia vai nos levar até \(\theta_t - \eta\beta V_t\), podemos muito bem medir a inclinação . Se a superfície já voltou a subir, o gradiente no ponto de espiada freia antes do que freraria de outro modo. Na prática o ganho é pequeno mas consistente11 e está a uma flag de distância: torch.optim.SGD(..., momentum=0.9, nesterov=True).

Métodos adaptativos: uma taxa de aprendizado por parâmetro

O momentum conserta a direção. Ele não conserta o fato de que um único \(\eta\) tem que servir a todos os parâmetros da rede — o embedding de uma palavra rara, cujo gradiente é quase sempre zero, e um viés da primeira camada, cujo gradiente é grande a cada passo.

A ideia comum a todo método adaptativo: manter, por parâmetro, uma estimativa da magnitude típica do gradiente e dividir por ela. Parâmetros com gradientes consistentemente grandes dão passos menores; parâmetros com gradientes minúsculos dão passos maiores. A atualização fica praticamente livre de escala.

AdaGrad — a primeira ideia e seu defeito

Acumule a soma de todos os gradientes ao quadrado vistos até agora e divida pela sua raiz12:

\[ G_t = G_{t-1} + g_t^2, \qquad \theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{G_t} + \epsilon}\, g_t \]

Isso funciona lindamente para atributos esparsos e é onde toda a família começa. Mas \(G_t\) é uma soma que nunca diminui, então a taxa efetiva \(\eta/\sqrt{G_t}\) decai monotonicamente rumo a zero. Em um treino longo de deep learning, o AdaGrad para de aprender muito antes de ter convergido.

RMSProp — troque a soma por uma média móvel

A correção13 tem o tamanho de um caractere: transforme a soma em uma média móvel exponencial, de modo que gradientes antigos sejam esquecidos.

\[ V_{t+1} = \beta V_t + (1 - \beta) \big(\nabla J(\theta_t)\big)^2 \]
\[ \theta_{t+1} = \theta_t - \eta\,\frac{\nabla J(\theta_t)}{\sqrt{V_{t+1}} + \epsilon} \]

onde:

  • \(V_t\) é a média móvel dos gradientes ao quadrado (tipicamente \(\beta = 0{,}9\); no PyTorch ele se chama alpha e o padrão é 0.99),
  • \(\epsilon \approx 10^{-8}\) é uma constante pequena para estabilidade numérica, evitando divisão por zero.

Agora \(\sqrt{V_t}\) é a raiz do valor quadrático médio dos gradientes recentes — daí o nome — e acompanha o terreno atual em vez de todo o histórico. Note que \(g/\sqrt{\overline{g^2}}\) é adimensional e vale aproximadamente \(\pm 1\): o RMSProp é próximo de "dar um passo de tamanho \(\eta\) no sinal do gradiente", o que é justamente o que o torna imune a uma perda mal escalada.

Adam — momentum e RMSProp em um só

Adaptive Moment Estimation mantém as duas médias móveis: o primeiro momento (a média dos gradientes — momentum) e o segundo momento (a variância não centrada — RMSProp).

  1. Calcular o gradiente:

    \[g_t = \nabla J(\theta_t)\]
  2. Primeiro momento (direção, com inércia):

    \[m_t = \beta_1 m_{t-1} + (1 - \beta_1) g_t\]
  3. Segundo momento (escala):

    \[v_t = \beta_2 v_{t-1} + (1 - \beta_2) g_t^2\]
  4. Correção de viés:

    \[\hat{m}_t = \frac{m_t}{1 - \beta_1^t}, \qquad \hat{v}_t = \frac{v_t}{1 - \beta_2^t}\]
  5. Atualização:

    \[\theta_{t+1} = \theta_t - \eta \, \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon}\]

onde \(\beta_1, \beta_2\) controlam o decaimento das duas médias e \(\epsilon\) protege a divisão. Os valores padrão são \(\beta_1 = 0{,}9\), \(\beta_2 = 0{,}999\) e \(\epsilon = 10^{-8}\)1415 e são notavelmente robustos — o Adam é a coisa mais próxima que temos de um otimizador que funciona já de saída.

Para que serve a correção de viés? Um exemplo numérico

As duas médias começam em zero, então no início elas estão enviesadas rumo a zero — e, o que é crucial, em quantidades diferentes, porque \(\beta_1 \ne \beta_2\).

Suponha um gradiente constante \(g = 1\). No primeiro passo:

valor em \(t=1\) corrigido
\(m_1 = (1-\beta_1)g\) \(0{,}1\) \(\hat{m}_1 = 0{,}1/(1-0{,}9) = 1{,}0\)
\(v_1 = (1-\beta_2)g^2\) \(0{,}001\) \(\hat{v}_1 = 0{,}001/(1-0{,}999) = 1{,}0\)

Sem a correção, o passo seria \(\eta \cdot \dfrac{0{,}1}{\sqrt{0{,}001}} = 3{,}16\,\eta\) — mais de três vezes maior do que deveria, logo no começo do treino, quando o modelo está mais frágil. Com a correção, é exatamente \(\eta \cdot \dfrac{1{,}0}{\sqrt{1{,}0}} = \eta\), o passo que você pediu.

Os dois fatores tendem a 1 conforme \(t\) cresce, mas em ritmos bem diferentes: \(1-\beta_1^t\) chega a 1% de 1 já no passo 44, enquanto \(1-\beta_2^t\) precisa de cerca de 4 600 passos (\(0{,}999^{1000} \approx 0{,}37\), ou seja, a correção do segundo momento ainda faz trabalho real bem depois do início). A correção importa sobretudo no começo — por isso ela e o warmup (próxima seção) atacam problemas que se sobrepõem.

O Adam é o padrão para treinar redes profundas: combina a suavização do momentum com adaptatividade por parâmetro e exige bem menos ajuste da taxa de aprendizado que o SGD. O preço é memória — dois tensores extras do tamanho do modelo — e uma tendência bem documentada a generalizar um pouco pior que um SGD+momentum bem ajustado em tarefas convolucionais de visão.16

AdamW — weight decay desacoplado

O AdamW17 separa o weight decay da atualização adaptativa, o que é matematicamente a coisa certa a fazer:

\[ \theta_{t+1} = \theta_t - \eta \left( \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon} + \lambda \theta_t \right) \]

onde \(\lambda\) é o coeficiente de weight decay aplicado diretamente nos pesos e não no gradiente.

No Adam original, a regularização L2 é implementada somando \(\lambda\theta\) ao gradiente antes do reescalonamento adaptativo — então ela também acaba dividida por \(\sqrt{\hat{v}_t}\). A consequência é perversa: parâmetros com gradientes grandes acabam menos regularizados, o oposto do que se quer. O AdamW aplica o decaimento fora do termo adaptativo, restaurando o comportamento pretendido. É o padrão para treinar Transformers e LLMs (BERT, GPT, LLaMA), normalmente com \(\lambda \approx 0{,}01\)\(0{,}1\).

Não aplique decay em todo parâmetro

O weight decay existe para matrizes de pesos. Vieses e os ganhos/deslocamentos de LayerNorm e BatchNorm devem ficar de fora — aplicar decay neles atrapalha sem trazer benefício de regularização. Todo script de treino sério separa os parâmetros em dois grupos exatamente por isso.

Simulador: a corrida dos otimizadores

Tudo o que veio acima, lado a lado. Escolha um relevo, ajuste a taxa de aprendizado e veja cinco otimizadores partirem do mesmo ponto. O painel de baixo mostra a perda em escala logarítmica — o formato dessas curvas é o que você realmente vê em um log de treino.

O que observar

Alguns experimentos que valem a pena:

  • Vale estreito, η um pouco acima do padrão. O SGD é o primeiro a explodir: seu limite de estabilidade é \(2/L = 0{,}4\), ditado apenas pela direção íngreme. Os métodos adaptativos continuam bem além disso, porque dividir por \(\sqrt{v_t}\) reescala essa direção para baixo. Repare também que o RMSProp nunca assenta de vez — sem momentum, o passo dele permanece da ordem de \(\eta\) e ele fica orbitando o mínimo em vez de pousar nele.
  • Sela, η padrão. SGD e Momentum continuam parados na crista depois de 300 passos — ali o gradiente de fuga vale ~0,004 e um passo proporcional ao gradiente é praticamente passo nenhum. RMSProp e Adam dividem pela própria magnitude do gradiente, então o passo deles fica perto de \(\eta\) e chegam ao mínimo em algumas dezenas de iterações. É esta a razão prática pela qual os métodos adaptativos lidam tão bem com platôs.
  • Não convexo, η em 1×, depois 2× e 4×. Com o padrão, todos ficam presos na depressão mais próxima — a bacia foi decidida nos primeiros passos e é por isso que a inicialização importa. Suba o η e os métodos normalizados (Adam primeiro, depois RMSProp) passam a dar passos largos o bastante para vencer uma crista, enquanto o SGD fica exatamente onde caiu. Repare no que isso significa: quem escapa de uma região ruim é um passo grande o suficiente, não inteligência — e, em treinos reais, esse papel é do ruído dos mini-batches do simulador anterior.
  • AdaGrad em qualquer superfície, rodada longa. Veja-o desacelerar e parar — a curva roxa achata enquanto o RMSProp continua. É o \(G_t\) acumulado estrangulando o tamanho do passo.
  • Rosenbrock com η em 4×. O SGD diverge enquanto Momentum e Adam acompanham o vale curvo até \((1,1)\). Com o η padrão ninguém chega em 300 passos e o AdaGrad não chega nunca — o retrato honesto de um problema mal condicionado e a razão pela qual métodos de segunda ordem ainda existem.

Uma ressalva sobre corridas de otimizadores

Estes são problemas determinísticos de 2 parâmetros. Redes reais têm milhões de parâmetros, gradientes ruidosos e uma superfície que muda conforme as camadas se coadaptam. Use estas imagens para ter intuição sobre os mecanismos — oscilação, inércia, reescalonamento — e não como prova de que um otimizador vence outro. Em tarefas reais, um SGD+momentum bem ajustado ainda ganha benchmarks que o Adam perde.

Agendamento da taxa de aprendizado

O simulador de ruído mostrou por que uma taxa de aprendizado constante não pode ser ótima: passos grandes exploram rápido, mas deixam você orbitando o mínimo; passos pequenos assentam com precisão, mas demoram uma eternidade para chegar lá. Então faça as duas coisas — comece grande, termine pequeno.

\[ \eta_t = \eta_{\max} \cdot s(t) \]

Dois ingredientes extras importam na prática. O warmup faz \(\eta\) subir a partir de ~0 ao longo das primeiras centenas ou milhares de iterações: na inicialização os gradientes são grandes e a estimativa do segundo momento do Adam ainda é lixo, então passos de tamanho normal logo no começo podem destruir o modelo — isso é essencial em Transformers18. E o decaimento por cosseno leva \(\eta\) suavemente a zero até o fim do treino, o que empiricamente supera o decaimento em degraus em quase tudo.19

import torch, math

opt = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=0.01)

warmup, total = 500, 10_000
def factor(step):                                   # devolve um multiplicador sobre o lr
    if step < warmup:
        return (step + 1) / warmup                  # rampa linear (nunca exatamente 0)
    p = (step - warmup) / (total - warmup)          # progresso em [0, 1]
    return 0.5 * (1 + math.cos(math.pi * p))        # cosseno até zero

sched = torch.optim.lr_scheduler.LambdaLR(opt, factor)

for x, y in loader:
    loss = criterion(model(x), y)
    opt.zero_grad(set_to_none=True)
    loss.backward()
    torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)   # veja abaixo
    opt.step()
    sched.step()                                    # por passo, não por época

Recorte de gradiente (gradient clipping)

Um único mini-batch ruim pode produzir um gradiente centenas de vezes maior que o usual — e um passo desses desfaz uma hora de treino. O recorte reescala o gradiente inteiro sempre que sua norma passa de um limite:

\[ g \leftarrow g \cdot \min\left(1, \frac{c}{\lVert g \rVert}\right) \]

A direção é preservada, só o comprimento é limitado. Com \(c = 1{,}0\) não custa nada e é prática padrão em RNNs, Transformers e qualquer modelo treinado em precisão mista.20

Escolhendo um otimizador na prática

Lado a lado

Batch GD SGD / Mini-batch Momentum RMSProp Adam / AdamW
Direção gradiente exato gradiente ruidoso média móvel dos gradientes gradiente reescalado gradiente médio e reescalado
Tamanho do passo η fixo η fixo η fixo (×\(\frac{1}{1-\beta}\) na convenção do PyTorch) por parâmetro por parâmetro
Custo por atualização alto (base inteira) baixo (um mini-batch) baixo (igual ao SGD, + 1 buffer) baixo (+ 1 buffer) médio (+ 2 buffers)
Memória extra 1× o modelo 1× o modelo 2× o modelo
Lida com vales estreitos mal mal bem bem muito bem
Escapa de selas não pelo ruído pela inércia devagar bem
Sensibilidade a η alta alta alta média baixa
Uso típico problemas convexos pequenos visão, com bom agendamento CNNs, RL RNNs, perdas não estacionárias Transformers, LLMs, padrão
Hiperparâmetros η η, tamanho do batch η, β ≈ 0,9 η ≈ 1e-3, β ≈ 0,9 η ≈ 1e-3, β₁ = 0,9, β₂ = 0,999, λ
Fraqueza principal uma atualização por época oscila, exige ajuste pode passar do ponto sem momento memória; às vezes generaliza pior que SGD

Resumindo: Batch GD é exato, mas impraticável; SGD compra velocidade pagando com ruído; Momentum suaviza esse ruído e acelera nas direções consistentes; RMSProp acerta a escala de cada parâmetro; e Adam/AdamW junta os dois últimos — por isso é o padrão, embora um SGD+momentum bem ajustado ainda generalize melhor em alguns benchmarks de visão.

Comparação de gradientes

A mesma descida com otimizadores diferentes: os adaptativos viram cedo e descem pelo vale, enquanto o SGD puro fica quicando entre as paredes.

A escolha certa depende do tamanho do conjunto de dados, do poder computacional disponível e de quanto ajuste fino você pode pagar — valide em dados separados em vez de confiar em uma tabela.

Padrões sensatos

Situação Otimizador Ponto de partida
Transformer / LLM, do zero AdamW lr=3e-4, betas=(0.9, 0.95), wd=0.1, warmup + cosseno
Ajuste fino de modelo pré-treinado AdamW lr=2e-5 a 5e-5, wd=0.01, warmup curto
CNN em imagens, treino longo SGD + Nesterov lr=0.1 (batch 256), momentum=0.9, wd=5e-4, cosseno
MLP em dados tabulares Adam lr=1e-3, o resto no padrão
Qualquer coisa, primeira tentativa Adam lr=1e-3 — e ajuste a taxa de aprendizado antes de qualquer outra coisa

A única busca de hiperparâmetro que sempre compensa

Varra a taxa de aprendizado em potências de dez (1e-1, 1e-2, 1e-3, 1e-4), treine algumas centenas de passos com cada uma e fique com a maior que não explode — depois divida por dois. Ajustar \(\beta_1\), \(\beta_2\) ou \(\epsilon\) antes disso é esforço desperdiçado: os padrões são bons e o \(\eta\) domina tudo.

Diagnóstico de um treino

Sintoma Causa provável O que tentar
Perda vira NaN / inf η acima do limite de estabilidade, gradientes explodindo, log(0) na perda divida η por 10, ative gradient clipping, revise a perda numericamente
Perda plana desde o passo 0 η pequeno demais, ReLUs mortas, pipeline de dados quebrado multiplique η por 10; verifique se o modelo consegue decorar 10 amostras
Perda cai e estaciona alto preso em platô/sela, ou η agora grande demais para esta fase adicione momentum, decaia η, revise a inicialização
Perda de treino boa, validação subindo overfitting — não é problema do otimizador weight decay, dropout, aumento de dados, early stopping
Perda tremendo violentamente batch pequeno demais ou η grande demais aumente o batch, decaia η, adicione momentum
Vai bem e diverge na época k taxa alta demais para a região mais afiada alcançada depois agende a taxa de aprendizado; adicione warmup

Os otimizadores, do zero

Todos eles cabem em dez linhas. Escritos lado a lado, o parentesco fica evidente — cada um é θ -= η · (direção) / (escala).

import numpy as np

class SGD:
    def __init__(self, lr=0.01, beta=0.0):
        self.lr, self.beta, self.v = lr, beta, None

    def step(self, theta, grad):
        if self.v is None:
            self.v = np.zeros_like(theta)
        self.v = self.beta * self.v + (1 - self.beta) * grad     # momentum (beta=0 → SGD puro)
        return theta - self.lr * self.v


class RMSProp:
    def __init__(self, lr=0.01, beta=0.9, eps=1e-8):
        self.lr, self.beta, self.eps, self.v = lr, beta, eps, None

    def step(self, theta, grad):
        if self.v is None:
            self.v = np.zeros_like(theta)
        self.v = self.beta * self.v + (1 - self.beta) * grad ** 2   # magnitude quadrática média
        return theta - self.lr * grad / (np.sqrt(self.v) + self.eps)


class Adam:
    def __init__(self, lr=1e-3, b1=0.9, b2=0.999, eps=1e-8, weight_decay=0.0):
        self.lr, self.b1, self.b2, self.eps, self.wd = lr, b1, b2, eps, weight_decay
        self.m = self.v = None
        self.t = 0

    def step(self, theta, grad):
        if self.m is None:
            self.m = np.zeros_like(theta)
            self.v = np.zeros_like(theta)
        self.t += 1
        self.m = self.b1 * self.m + (1 - self.b1) * grad            # 1o momento: direção
        self.v = self.b2 * self.v + (1 - self.b2) * grad ** 2       # 2o momento: escala
        m_hat = self.m / (1 - self.b1 ** self.t)                    # correção de viés
        v_hat = self.v / (1 - self.b2 ** self.t)
        update = m_hat / (np.sqrt(v_hat) + self.eps)
        return theta - self.lr * (update + self.wd * theta)         # AdamW: decay fora do reescalonamento
# A mesma coisa, em PyTorch
torch.optim.SGD(p, lr=0.1, momentum=0.9, nesterov=True, weight_decay=5e-4)
torch.optim.RMSprop(p, lr=1e-3, alpha=0.9)
torch.optim.Adam(p, lr=1e-3, betas=(0.9, 0.999), eps=1e-8)
torch.optim.AdamW(p, lr=3e-4, betas=(0.9, 0.95), weight_decay=0.1)

Pontos-chave

  1. O gradiente descendente é uma linha; todo o resto é sobre escolher bem o tamanho do passo.
  2. Uma quadrática só converge quando \(\eta < 2/L\). Divergir não é azar, é aritmética.
  3. Mini-batches trocam ruído por velocidade e o ruído cai apenas com \(1/\sqrt{B}\). Uma taxa constante deixa você orbitando o mínimo — daí os agendamentos.
  4. Os obstáculos reais são o mau condicionamento e os pontos de sela, não mínimos locais ruins.
  5. O momentum faz média dos gradientes no tempo, então o que alterna se cancela e o que persiste sobrevive. \(1/(1-\beta)\) é o comprimento dessa memória — e, na convenção do PyTorch, também o fator pelo qual o passo cresce.
  6. Os métodos adaptativos (AdaGrad → RMSProp → Adam) dão a cada parâmetro seu próprio passo, dividindo pela raiz quadrática média dos gradientes recentes. O Adam acrescenta momentum e correção de viés por cima.
  7. AdamW + warmup + cosseno é o padrão moderno. Ajuste a taxa de aprendizado primeiro e quase nada mais.

Recursos Adicionais

Três materiais que continuam de onde esta página para, na ordem em que fazem sentido:

  1. Who's Adam and What's He Optimizing? — Kundu, S. Refaz em animação o mesmo caminho deste capítulo — SGD, momentum, RMSProp, Adam. É a forma mais rápida de revisar a página inteira:

  2. An overview of gradient descent optimization algorithms — Ruder, S. Reúne todos os métodos daqui em uma notação só, mais os que ficaram de fora (Adadelta, Nadam, AMSGrad). Leia depois de fechar a aula, para ver o mapa completo.

  3. Why Momentum Really Works — Goh, G., Distill (2017). Um artigo inteiramente interativo sobre o que o número de condição faz com a convergência. É o aprofundamento natural da seção de momentum: prova com sliders o que aqui ficou na intuição.

Referências bibliográficas

As obras citadas ao longo do texto, na ordem em que aparecem:


  1. Introduction to Gradient Descent and Backpropagation Algorithm — LeCun, Y., notas do curso de Deep Learning da NYU. 

  2. Formalmente, a variação de \(J\) ao caminhar numa direção unitária \(u\) é a derivada direcional \(D_u J = \nabla J \cdot u = \lVert \nabla J \rVert \cos\alpha\), em que \(\alpha\) é o ângulo entre \(u\) e \(\nabla J\). Como só o cosseno depende de \(u\), o valor é máximo em \(\alpha = 0\) (com \(u\) alinhado a \(\nabla J\)) e mínimo em \(\alpha = 180°\) (com \(u = -\nabla J / \lVert \nabla J \rVert\)). É esse o argumento que garante que o gradiente é a direção de subida mais íngreme e não apenas uma direção de subida. 

  3. Lipschitz é Rudolf Lipschitz (1832–1903), matemático alemão. Uma função é Lipschitz contínua com constante \(L\) quando nunca varia mais rápido que \(L\): tome dois pontos quaisquer \(x\) e \(y\) e vale \(\lvert f(x) - f(y) \rvert \le L \lvert x - y \rvert\). Em palavras, a reta que liga dois pontos quaisquer do gráfico tem inclinação de no máximo \(L\) em módulo — a função tem um limite de velocidade e \(L\) é esse limite. Aqui quem é Lipschitz não é a perda e sim o gradiente dela: \(\lVert \nabla J(x) - \nabla J(y) \rVert \le L \lVert x - y \rVert\), ou seja, a própria inclinação não pode mudar mais rápido que \(L\) — e limitar a velocidade com que a inclinação muda é exatamente limitar a curvatura (\(L\) é o maior autovalor da Hessiana em módulo, quando \(J\) é duas vezes diferenciável). Daí a intuição por trás de \(\eta < 2/L\): o gradiente medido em \(\theta_t\) só descreve bem a superfície por um trecho e, quanto mais rápido ele puder mudar, menor o passo que você pode dar. 

  4. Robbins, H., & Monro, S. (1951). A Stochastic Approximation MethodAnnals of Mathematical Statistics 22(3), 400–407. A origem da aproximação estocástica: a prova de que estimativas ruidosas do gradiente bastam, desde que os passos encolham na taxa certa. 

  5. Stochastic and Mini-batch Gradient Descent — Watt, J., Borhani, R., & Katsaggelos, A., Machine Learning Refined

  6. Goyal, P., Dollár, P., Girshick, R., et al. (2017). Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour — onde a regra de escala linear e o warmup gradual foram estabelecidos, com batch 8192. 

  7. Dauphin, Y., Pascanu, R., Gulcehre, C., et al. (2014). Identifying and Attacking the Saddle Point Problem in High-Dimensional Non-Convex Optimization — NeurIPS. O artigo por trás da afirmação de que selas — e não mínimos locais — dominam em alta dimensão. 

  8. Polyak, B. T. (1964). Some methods of speeding up the convergence of iteration methodsUSSR Computational Mathematics and Mathematical Physics 4(5), 1–17. A bola pesada, 22 anos antes da retropropagação. 

  9. Nesterov, Y. (1983). A method for solving the convex programming problem with convergence rate \(O(1/k^2)\)Doklady Akademii Nauk SSSR 269, 543–547. O resultado original de aceleração; antecede em três décadas o seu uso em redes neurais. 

  10. Optimization, the Philosophical Background of Artificial Intelligence — palestra de Nesterov, Y. no ICBS 2024. Não é sobre o método em si: é ele falando do lugar da otimização dentro da IA. 

  11. Sutskever, I., Martens, J., Dahl, G., & Hinton, G. (2013). On the importance of initialization and momentum in deep learning — ICML. O artigo que trouxe a aceleração de Nesterov para a prática do deep learning e mediu o quanto ela e a inicialização importam. 

  12. Duchi, J., Hazan, E., & Singer, Y. (2011). Adaptive Subgradient Methods for Online Learning and Stochastic OptimizationJMLR 12, 2121–2159. O AdaGrad e a taxa de aprendizado por parâmetro que o resto da família herdou. 

  13. Tieleman, T., & Hinton, G. (2012). Lecture 6.5 — RMSProp: divide the gradient by a running average of its recent magnitude — Coursera, Neural Networks for Machine Learning. O RMSProp não tem artigo: esses slides são o que todo mundo cita. 

  14. Kingma, D. P., & Ba, J. (2015). Adam: A Method for Stochastic Optimization — ICLR. 

  15. Dive into Deep Learning — Zhang, A., Lipton, Z. C., Li, M., & Smola, A. J. O capítulo 12 deriva todos os otimizadores desta página, com código. 

  16. Wilson, A. C., Roelofs, R., Stern, M., Srebro, N., & Recht, B. (2017). The Marginal Value of Adaptive Gradient Methods in Machine Learning — NeurIPS. A evidência sistemática da diferença de generalização entre Adam e um SGD bem ajustado. 

  17. Loshchilov, I., & Hutter, F. (2019). Decoupled Weight Decay Regularization — ICLR. O AdamW. 

  18. Vaswani, A., Shazeer, N., Parmar, N., et al. (2017). Attention Is All You Need — NeurIPS. A seção 5.3 define o agendamento warmup-e-\(1/\sqrt{t}\) que o simulador desenha como "Noam". 

  19. Loshchilov, I., & Hutter, F. (2017). SGDR: Stochastic Gradient Descent with Warm Restarts — ICLR. De onde vem o decaimento por cosseno. 

  20. Pascanu, R., Mikolov, T., & Bengio, Y. (2013). On the difficulty of training recurrent neural networks — ICML. De onde vem o recorte por norma e por que gradientes explodem.