Phase 01 - Lesson 08
Otimização
This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.
Treinar uma rede neural nada mais é do que encontrar o fundo de um vale.
Tipo: Build Linguagem: Python Pré-requisitos: Fase 1, Lições 04-05 (Derivadas, Gradientes) Tempo: ~75 minutos
Objetivos de Aprendizagem
- Implementar o gradiente descendente puro, o SGD com momento e o Adam do zero
- Comparar a convergência dos otimizadores na função de Rosenbrock e explicar por que o Adam adapta as taxas de aprendizado por peso
- Distinguir paisagens de perda convexas de não convexas e explicar o papel dos pontos de sela em altas dimensões
- Configurar agendamentos de taxa de aprendizado (decaimento por degrau, recozimento por cosseno, aquecimento) para a estabilidade do treinamento
O Problema
Você tem uma função de perda. Ela te diz o quão errado seu modelo está. Você tem gradientes. Eles te dizem qual direção torna a perda pior. Agora você precisa de uma estratégia para descer a ladeira.
A abordagem ingênua é simples: mova-se na direção oposta ao gradiente. Escale o passo por um número chamado taxa de aprendizado. Repita. Isto é o gradiente descendente, e ele funciona. Mas "funciona" tem ressalvas. Taxa de aprendizado grande demais e você passa direto pelo vale, quicando entre as paredes. Pequena demais e você se arrasta em direção à resposta ao longo de milhares de passos desnecessários. Atinja um ponto de sela e você para de se mover mesmo sem ter encontrado um mínimo.
Todo otimizador em deep learning é uma resposta para a mesma pergunta: como chegar ao fundo do vale mais rápido e de forma mais confiável?
O Conceito
O que otimização significa
Otimização é encontrar os valores de entrada que minimizam (ou maximizam) uma função. Em machine learning, a função é a perda. As entradas são os pesos do modelo. O treinamento é otimização.
minimizar L(w) onde:
L = funcao de perda
w = pesos do modelo (podem ser milhoes de parametros)
Gradiente descendente (puro)
O otimizador mais simples. Calcule o gradiente da perda em relação a cada peso. Mova cada peso na direção oposta ao seu gradiente. Escale o passo pela taxa de aprendizado.
w = w - lr * gradient
Esse é o algoritmo inteiro. Uma linha.
graph TD
A["* Starting point (high loss)"] --> B["Moving downhill along gradient"]
B --> C["Approaching minimum"]
C --> D["o Minimum (low loss)"]
Taxa de aprendizado: o hiperparâmetro mais importante
A taxa de aprendizado controla o tamanho do passo. Ela determina tudo sobre a convergência.
graph LR
subgraph TooLarge["Too Large (lr = 1.0)"]
A1["Step 1"] -->|overshoot| A2["Step 2"]
A2 -->|overshoot| A3["Step 3"]
A3 -->|diverging| A4["..."]
end
subgraph TooSmall["Too Small (lr = 0.0001)"]
B1["Step 1"] -->|tiny step| B2["Step 2"]
B2 -->|tiny step| B3["Step 3"]
B3 -->|10,000 steps later| B4["Minimum"]
end
subgraph JustRight["Just Right (lr = 0.01)"]
C1["Start"] --> C2["..."] --> C3["Converged in ~100 steps"]
end
Não há fórmula para a taxa de aprendizado certa. Você a encontra por experimentação. Pontos de partida comuns: 0.001 para Adam, 0.01 para SGD com momento.
SGD vs batch vs mini-batch
O gradiente descendente puro calcula o gradiente sobre o conjunto de dados inteiro antes de dar um passo. Isso é chamado de gradiente descendente em batch. É estável mas lento.
O gradiente descendente estocástico (SGD) calcula o gradiente em uma única amostra aleatória e dá o passo imediatamente. É ruidoso mas rápido.
O gradiente descendente em mini-batch fica no meio-termo. Calcule o gradiente sobre um pequeno batch (32, 64, 128, 256 amostras), depois dê o passo. É isso que todo mundo de fato usa.
| Variante | Tamanho do batch | Qualidade do gradiente | Velocidade por passo | Ruído |
|---|---|---|---|---|
| GD em batch | Conjunto de dados inteiro | Exato | Lento | Nenhum |
| SGD | 1 amostra | Muito ruidoso | Rápido | Alto |
| Mini-batch | 32-256 | Boa estimativa | Equilibrado | Moderado |
O ruído no SGD e no mini-batch não é um bug. Ele ajuda a escapar de mínimos locais rasos e de pontos de sela.
Momento: a bola rolando ladeira abaixo
O gradiente descendente puro só olha para o gradiente atual. Se o gradiente faz ziguezague (comum em vales estreitos), o progresso é lento. O momento corrige isso acumulando gradientes passados em um termo de velocidade.
v = beta * v + gradient
w = w - lr * v
A analogia: uma bola rolando ladeira abaixo. Ela não para e reinicia a cada elevação. Ela ganha velocidade em direções consistentes e amortece oscilações.
graph TD
subgraph Without["Without Momentum (zigzag, slow)"]
W1["Start"] -->|left| W2[" "]
W2 -->|right| W3[" "]
W3 -->|left| W4[" "]
W4 -->|right| W5[" "]
W5 -->|left| W6[" "]
W6 --> W7["Minimum"]
end
subgraph With["With Momentum (smooth, fast)"]
M1["Start"] --> M2[" "] --> M3[" "] --> M4["Minimum"]
end
beta (tipicamente 0.9) controla quanto do histórico manter. Beta maior significa mais momento, caminhos mais suaves, mas resposta mais lenta a mudanças de direção.
Adam: taxas de aprendizado adaptativas
Pesos diferentes precisam de taxas de aprendizado diferentes. Um peso que raramente recebe gradientes grandes deve dar passos maiores quando finalmente os recebe. Um peso que recebe gradientes enormes constantemente deve dar passos menores.
O Adam (Adaptive Moment Estimation) rastreia duas coisas por peso:
- Primeiro momento (m): média movel dos gradientes (como o momento)
- Segundo momento (v): média movel dos gradientes ao quadrado (magnitude do gradiente)
m = beta1 * m + (1 - beta1) * gradient
v = beta2 * v + (1 - beta2) * gradient^2
m_hat = m / (1 - beta1^t) bias correction
v_hat = v / (1 - beta2^t) bias correction
w = w - lr * m_hat / (sqrt(v_hat) + epsilon)
A divisão por sqrt(v_hat) é a ideia central. Pesos com gradientes grandes são divididos por um número grande (passo efetivo pequeno). Pesos com gradientes pequenos são divididos por um número pequeno (passo efetivo grande). Cada peso recebe sua própria taxa de aprendizado adaptativa.
Hiperparâmetros padrão: lr=0.001, beta1=0.9, beta2=0.999, epsilon=1e-8. Esses padrões funcionam bem para a maioria dos problemas.
Agendamentos de taxa de aprendizado
Uma taxa de aprendizado fixa é um meio-termo. No início do treinamento, você quer passos grandes para fazer progresso rápido. No fim do treinamento, você quer passos pequenos para o ajuste fino perto do mínimo.
Agendamentos comuns:
| Agendamento | Fórmula | Caso de uso |
|---|---|---|
| Decaimento por degrau | lr = lr * fator a cada N épocas | Simples, controle manual |
| Decaimento exponencial | lr = lr_0 * decay^t | Redução suave |
| Recozimento por cosseno | lr = lr_min + 0.5 * (lr_max - lr_min) * (1 + cos(pi * t / T)) | Transformers, treinamento moderno |
| Aquecimento + decaimento | Rampa linear de subida, depois decaimento | Modelos grandes, evita instabilidade inicial |
Convexo vs não convexo
Uma função convexa tem um mínimo. O gradiente descendente sempre o encontra. Uma quadrática como f(x) = x^2 é convexa.
As funções de perda de redes neurais são não convexas. Elas têm muitos mínimos locais, pontos de sela e regiões planas.
graph LR
subgraph Convex["Convex: One valley, one answer"]
direction TB
CV1["High loss"] --> CV2["Global minimum"]
end
subgraph NonConvex["Non-convex: Multiple valleys, saddle points"]
direction TB
NC1["Start"] --> NC2["Local minimum"]
NC1 --> NC3["Saddle point"]
NC1 --> NC4["Global minimum"]
end
Na prática, mínimos locais em redes neurais de alta dimensão raramente são um problema. A maioria dos mínimos locais tem valores de perda próximos do mínimo global. Os pontos de sela (planos em algumas direções, curvos em outras) são o verdadeiro obstáculo. O momento e o ruído dos mini-batches ajudam a escapar deles.
Visualização da paisagem de perda
A perda é uma função de todos os pesos. Para um modelo com 1 milhão de pesos, a paisagem de perda vive em um espaço de 1.000.001 dimensões. Nós a visualizamos escolhendo duas direções aleatórias no espaço de pesos e plotando a perda ao longo dessas direções, produzindo uma superfície 2D.
graph TD
HL["High loss region"] --> SP["Saddle point"]
HL --> LM["Local minimum"]
SP --> LM
SP --> GM["Global minimum"]
LM -.->|"shallow barrier"| GM
style HL fill:#ff6666,color:#000
style SP fill:#ffcc66,color:#000
style LM fill:#66ccff,color:#000
style GM fill:#66ff66,color:#000
Mínimos agudos generalizam mal. Mínimos planos generalizam bem. Esta é uma das razões pelas quais o SGD com momento frequentemente supera o Adam na acurácia final de teste: seu ruído evita o assentamento em mínimos agudos.
Construa
Passo 1: Definir uma função de teste
A função de Rosenbrock é um benchmark clássico de otimização. Seu mínimo está em (1, 1) dentro de um vale curvo e estreito que é fácil de encontrar mas difícil de seguir.
f(x, y) = (1 - x)^2 + 100 * (y - x^2)^2
def rosenbrock(params):
x, y = params
return (1 - x) ** 2 + 100 * (y - x ** 2) ** 2
def rosenbrock_gradient(params):
x, y = params
df_dx = -2 * (1 - x) + 200 * (y - x ** 2) * (-2 * x)
df_dy = 200 * (y - x ** 2)
return [df_dx, df_dy]
Passo 2: Gradiente descendente puro
class GradientDescent:
def __init__(self, lr=0.001):
self.lr = lr
def step(self, params, grads):
return [p - self.lr * g for p, g in zip(params, grads)]
Passo 3: SGD com momento
class SGDMomentum:
def __init__(self, lr=0.001, momentum=0.9):
self.lr = lr
self.momentum = momentum
self.velocity = None
def step(self, params, grads):
if self.velocity is None:
self.velocity = [0.0] * len(params)
self.velocity = [
self.momentum * v + g
for v, g in zip(self.velocity, grads)
]
return [p - self.lr * v for p, v in zip(params, self.velocity)]
Passo 4: Adam
class Adam:
def __init__(self, lr=0.001, beta1=0.9, beta2=0.999, epsilon=1e-8):
self.lr = lr
self.beta1 = beta1
self.beta2 = beta2
self.epsilon = epsilon
self.m = None
self.v = None
self.t = 0
def step(self, params, grads):
if self.m is None:
self.m = [0.0] * len(params)
self.v = [0.0] * len(params)
self.t += 1
self.m = [
self.beta1 * m + (1 - self.beta1) * g
for m, g in zip(self.m, grads)
]
self.v = [
self.beta2 * v + (1 - self.beta2) * g ** 2
for v, g in zip(self.v, grads)
]
m_hat = [m / (1 - self.beta1 ** self.t) for m in self.m]
v_hat = [v / (1 - self.beta2 ** self.t) for v in self.v]
return [
p - self.lr * mh / (vh ** 0.5 + self.epsilon)
for p, mh, vh in zip(params, m_hat, v_hat)
]
Passo 5: Rodar e comparar
def optimize(optimizer, func, grad_func, start, steps=5000):
params = list(start)
history = [params[:]]
for _ in range(steps):
grads = grad_func(params)
params = optimizer.step(params, grads)
history.append(params[:])
return history
start = [-1.0, 1.0]
gd_history = optimize(GradientDescent(lr=0.0005), rosenbrock, rosenbrock_gradient, start)
sgd_history = optimize(SGDMomentum(lr=0.0001, momentum=0.9), rosenbrock, rosenbrock_gradient, start)
adam_history = optimize(Adam(lr=0.01), rosenbrock, rosenbrock_gradient, start)
for name, history in [("GD", gd_history), ("SGD+M", sgd_history), ("Adam", adam_history)]:
final = history[-1]
loss = rosenbrock(final)
print(f"{name:6s} -> x={final[0]:.6f}, y={final[1]:.6f}, loss={loss:.8f}")
Saída esperada: o Adam converge mais rápido. O SGD com momento segue um caminho mais suave. O GD puro faz progresso lento ao longo do vale estreito.
Use
Na prática, use os otimizadores do PyTorch ou JAX. Eles cuidam de grupos de parâmetros, weight decay, recorte de gradiente e aceleração por GPU.
import torch
model = torch.nn.Linear(784, 10)
sgd = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
adam = torch.optim.Adam(model.parameters(), lr=0.001)
adamw = torch.optim.AdamW(model.parameters(), lr=0.001, weight_decay=0.01)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(adam, T_max=100)
Regras gerais:
- Comece com Adam (lr=0.001). Ele funciona para a maioria dos problemas sem ajuste fino.
- Mude para SGD com momento (lr=0.01, momentum=0.9) quando precisar da melhor acurácia final e puder arcar com mais ajuste.
- Use AdamW (Adam com weight decay desacoplado) para transformers.
- Sempre use um agendamento de taxa de aprendizado para execuções de treinamento mais longas que algumas épocas.
- Se o treinamento estiver instável, reduza a taxa de aprendizado. Se o treinamento estiver lento demais, aumente-a.
Entregue
Esta lição produz um prompt para escolher o otimizador certo. Veja outputs/prompt-optimizer-guide.md.
As classes de otimizador construidas aqui reaparecem na Fase 3 quando treinamos uma rede neural do zero.
Exercícios
Varredura da taxa de aprendizado. Rode o gradiente descendente puro na função de Rosenbrock com taxas de aprendizado [0.0001, 0.0005, 0.001, 0.005, 0.01]. Plote ou imprima a perda final após 5000 passos para cada uma. Encontre a maior taxa de aprendizado que ainda converge.
Comparação de momento. Rode o SGD com valores de momento [0.0, 0.5, 0.9, 0.99] na função de Rosenbrock. Acompanhe a perda em cada passo. Qual valor de momento converge mais rápido? Qual passa direto?
Escape de ponto de sela. Defina a função
f(x, y) = x^2 - y^2(um ponto de sela na origem). Comece em (0.01, 0.01). Compare como o GD puro, o SGD com momento e o Adam se comportam. Qual escapa do ponto de sela?Implemente o decaimento da taxa de aprendizado. Adicione um agendamento de decaimento exponencial a classe GradientDescent:
lr = lr_0 * 0.999^step. Compare a convergência com e sem decaimento na função de Rosenbrock.
Termos-Chave
| Termo | O que as pessoas dizem | O que realmente significa |
|---|---|---|
| Gradiente descendente | "Vá ladeira abaixo" | Atualizar os pesos subtraindo o gradiente escalado pela taxa de aprendizado. O otimizador mais básico. |
| Taxa de aprendizado | "Tamanho do passo" | Um escalar que controla o quão longe cada atualização move os pesos. Grande demais causa divergência. Pequena demais desperdica computação. |
| Momento | "Continue rolando" | Acumular gradientes passados em um vetor de velocidade. Amortece oscilações e acelera o movimento por direções consistentes. |
| SGD | "Amostragem aleatória" | Gradiente descendente estocástico. Calcular o gradiente em um subconjunto aleatório em vez do conjunto de dados completo. Quase sempre significa SGD em mini-batch na prática. |
| Mini-batch | "Um pedaço de dados" | Um pequeno subconjunto dos dados de treinamento (32-256 amostras) usado para estimar o gradiente. Equilibra velocidade e acurácia do gradiente. |
| Adam | "O otimizador padrão" | Adaptive Moment Estimation. Rastreia médias móveis por peso de gradientes e gradientes ao quadrado para dar a cada peso sua própria taxa de aprendizado. |
| Correção de viés | "Conserte a partida a frio" | O primeiro e o segundo momento do Adam são inicializados em zero. A correção de viés divide por (1 - beta^t) para compensar durante os passos iniciais. |
| Agendamento de taxa de aprendizado | "Mudar a lr ao longo do tempo" | Uma função que ajusta a taxa de aprendizado durante o treinamento. Passos grandes no início, passos pequenos no fim. |
| Função convexa | "Um vale" | Uma função onde qualquer mínimo local é o mínimo global. O gradiente descendente sempre o encontra. As perdas de redes neurais não são convexas. |
| Ponto de sela | "Plano mas não é um mínimo" | Um ponto onde o gradiente é zero, mas é um mínimo em algumas direções e um máximo em outras. Comum em altas dimensões. |
| Paisagem de perda | "O terreno" | A função de perda plotada sobre o espaço de pesos. Visualizada fatiando-a ao longo de duas direções aleatórias. |
| Convergência | "Chegar lá" | O otimizador alcançou um ponto onde passos adicionais não reduzem significativamenté a perda. |
Leitura Adicional
- Sebastian Ruder: An overview of gradient descent optimization algorithms - levantamento abrangente de todos os principais otimizadores
- Why Momentum Really Works (Distill) - visualização interativa da dinâmica do momento
- Adam: A Method for Stochastic Optimization (Kingma & Ba, 2014) - o artigo original do Adam, legível e curto
- Visualizing the Loss Landscape of Neural Nets (Li et al., 2018) - o artigo que mostrou mínimos agudos vs planos