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.
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:
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:
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:
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
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.
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.
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
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
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:
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.
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.
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.
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}\).
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\) é 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
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
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
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:
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 lá. 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:
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.
onde:
- \(V_t\) é a média móvel dos gradientes ao quadrado (tipicamente \(\beta = 0{,}9\); no PyTorch ele se chama
alphae 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).
-
Calcular o gradiente:
\[g_t = \nabla J(\theta_t)\] -
Primeiro momento (direção, com inércia):
\[m_t = \beta_1 m_{t-1} + (1 - \beta_1) g_t\] -
Segundo momento (escala):
\[v_t = \beta_2 v_{t-1} + (1 - \beta_2) g_t^2\] -
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}\] -
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:
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.
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:
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.
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
- O gradiente descendente é uma linha; todo o resto é sobre escolher bem o tamanho do passo.
- Uma quadrática só converge quando \(\eta < 2/L\). Divergir não é azar, é aritmética.
- 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.
- Os obstáculos reais são o mau condicionamento e os pontos de sela, não mínimos locais ruins.
- 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.
- 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.
- 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:
-
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:
-
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.
-
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:
-
Introduction to Gradient Descent and Backpropagation Algorithm — LeCun, Y., notas do curso de Deep Learning da NYU. ↩↩↩
-
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. ↩
-
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. ↩
-
Robbins, H., & Monro, S. (1951). A Stochastic Approximation Method — Annals 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. ↩
-
Stochastic and Mini-batch Gradient Descent — Watt, J., Borhani, R., & Katsaggelos, A., Machine Learning Refined. ↩
-
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. ↩
-
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. ↩
-
Polyak, B. T. (1964). Some methods of speeding up the convergence of iteration methods — USSR Computational Mathematics and Mathematical Physics 4(5), 1–17. A bola pesada, 22 anos antes da retropropagação. ↩
-
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. ↩
-
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. ↩
-
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. ↩
-
Duchi, J., Hazan, E., & Singer, Y. (2011). Adaptive Subgradient Methods for Online Learning and Stochastic Optimization — JMLR 12, 2121–2159. O AdaGrad e a taxa de aprendizado por parâmetro que o resto da família herdou. ↩
-
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. ↩
-
Kingma, D. P., & Ba, J. (2015). Adam: A Method for Stochastic Optimization — ICLR. ↩
-
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. ↩
-
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. ↩
-
Loshchilov, I., & Hutter, F. (2019). Decoupled Weight Decay Regularization — ICLR. O AdamW. ↩
-
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". ↩
-
Loshchilov, I., & Hutter, F. (2017). SGDR: Stochastic Gradient Descent with Warm Restarts — ICLR. De onde vem o decaimento por cosseno. ↩
-
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. ↩

