← Redes Neurais Artificiais e Deep Learning · Insper/6. OtimizaçãoMaterial complementar

Material de referência · Laboratório de 2 horas

A mesma rede, a mesma base.
Vinte curvas diferentes.

A arquitetura desta aula é uma só: 784 → 128 → 64 → 10, ReLU, entropia cruzada. Ela não muda em nenhum experimento. O que muda é de quantos em quantos exemplos você atualiza os pesos, qual regra usa para atualizá-los e de que tamanho é o passo. Só isso separa 95,9% de 98,3% de acurácia — e um treino de 2 segundos de um de 80.

784Entradas
109 386Parâmetros
7Otimizadores
120Treinos medidos
7Checkpoints

Abertura · 8 min

O que ainda não foi decidido

Você já sabe o que é um perceptron, como um MLP encadeia camadas e o que cada otimizador faz com o gradiente. Falta a parte que ninguém deriva no quadro: as escolhas de treino.

Nas três aulas anteriores o assunto foi a arquitetura — quantas camadas, quantos neurônios, qual ativação — e as regras de atualização — como momentum acumula, como Adam normaliza. Tudo isso define o modelo e o algoritmo. Nada disso define o treino.

Treinar exige mais três decisões, e elas não estão na arquitetura nem na regra de atualização:

DecisãoO que ela controlaSintoma de escolha ruim
Tamanho do batchQuantos exemplos entram em cada estimativa de gradiente.Ou a rede quase não sai do lugar em 20 épocas, ou a perda vira nan.
OtimizadorComo o gradiente vira um passo no espaço de pesos.Convergência lenta, ou oscilação em torno do mínimo.
Learning rateO tamanho desse passo.É o mais sensível dos três — erra por um fator de 10 e o treino não acontece.
As três decisões interagem: o melhor learning rate depende do batch e do otimizador. É isso que os blocos 2, 3 e 4 vão medir, um de cada vez.
Ao final desta aula o aluno consegue
  • Escrever um loop de treino em PyTorch e trocar batch, otimizador e learning rate sem reescrever nada.
  • Explicar por que o mini-batch venceu, usando ruído do gradiente e tempo de relógio como argumentos — não preferência.
  • Prever o efeito de reduzir o batch pela metade sobre o learning rate que a rede suporta.
  • Escolher o learning rate com uma busca barata em vez de chute, e ler a grade de resultados.
  • Registrar e plotar a curva de perda de forma que ela signifique alguma coisa — e saber quando parar de treinar.
Como esta aula funciona
  • Em duplas, um notebook aberto. Metade dos blocos termina numa etapa para rodar; a outra metade, numa aposta para escrever.
  • Aposte antes de olhar. Cinco vezes ao longo da página aparece uma caixa Aposte antes: escreva o palpite no papel antes de abrir a resposta. Errar a aposta é o momento em que a aula funciona.
  • Dois simuladores — a grade de treinos e o amostrador de gradiente — usam dados reais e existem para você mexer antes de ler a conclusão. Não são ilustração.
  • Sete checkpoints. Os marcados no notebook são para executar; os demais, para responder em voz alta com o colega.

0.1A base e a rede de referência

MNIST: 70 000 imagens de dígitos manuscritos, 28 × 28 pixels em tons de cinza, 60 000 para treino e 10 000 para teste. É a base mais gasta do aprendizado de máquina e é exatamente por isso que serve aqui: qualquer MLP razoável passa de 97% de acurácia, então as diferenças que você vai ver não são da rede — são das escolhas de treino.

4 4 2 9 8 3 2 8 5 2 8 0 4 1 2 1 0 9 5 9 5 1 2 9 0 9 1 5 3 8 4 7 0 2 7 8 5 5 4 6 2 4 3 9 1 3 0 5 0 0 2 1 9 2 2 8 4 9 1 8 8 7 4 6
Quarenta amostras do conjunto de treino, com o rótulo em azul. Cada imagem vira um vetor de 784 números entre 0 e 1; a rede nunca vê a grade 28 × 28, apenas a linha achatada. É a mesma cegueira do bloco de pré-processamento: o modelo não vê o dígito, vê o vetor.
import torch, time
import torch.nn as nn
import matplotlib.pyplot as plt
from torchvision import datasets, transforms

torch.manual_seed(42)

# normalização: média e desvio do próprio conjunto de treino
tf = transforms.Compose([transforms.ToTensor(),
                         transforms.Normalize((0.1307,), (0.3081,))])

treino = datasets.MNIST("./dados", train=True,  download=True, transform=tf)
teste  = datasets.MNIST("./dados", train=False, download=True, transform=tf)

# tudo cabe na memória: viramos tensores de uma vez e evitamos o DataLoader,
# que domina o tempo quando o batch é pequeno
X_tr = treino.data.reshape(-1, 784).float().div(255).sub(0.1307).div(0.3081)
y_tr = treino.targets
X_te = teste.data.reshape(-1, 784).float().div(255).sub(0.1307).div(0.3081)
y_te = teste.targets
X_tr.shape, y_tr.shape          # (60000, 784) (60000,)
A rede não muda nesta aula
def criar_rede(semente=42):
    torch.manual_seed(semente)                # mesma inicialização em todos os testes
    return nn.Sequential(
        nn.Linear(784, 128), nn.ReLU(),
        nn.Linear(128,  64), nn.ReLU(),
        nn.Linear( 64,  10),           # sem softmax: CrossEntropyLoss já aplica
    )

São 109 386 parâmetros: 784·128 + 128 na primeira camada, 128·64 + 64 na segunda e 64·10 + 10 na saída. A semente fixa é o que torna a comparação honesta — todas as curvas desta aula partem exatamente dos mesmos pesos iniciais. Sem isso, parte da diferença entre duas curvas seria sorte de inicialização.

Sem softmax na última camada

nn.CrossEntropyLoss espera logits e aplica log_softmax internamente, de forma numericamente estável. Empilhar um nn.Softmax antes dela aplica a operação duas vezes: o treino ainda anda, mas devagar e com gradientes achatados. É um dos erros mais comuns em código de aluno — e não gera erro nenhum, só uma curva pior.

↑ topo

Bloco 1 · 12 min

O loop, escrito uma vez

Uma única função de treino, com três parâmetros. Todo o resto da aula é trocar o valor desses três parâmetros e olhar o gráfico.

O loop de treino de qualquer rede tem sempre a mesma anatomia. Vale escrevê-lo com atenção uma vez, porque os experimentos dos próximos blocos são literalmente chamadas diferentes desta mesma função.

LinhaO que acontece
perm = randperm(n)Embaralha a ordem dos exemplos a cada época. Sem isso o gradiente de cada passo fica correlacionado com o anterior e o treino enviesa.
xb, yb = ...Recorta o mini-batch. É aqui, e só aqui, que o tamanho do batch aparece.
opt.zero_grad()Zera os gradientes acumulados. PyTorch soma gradientes por padrão; esquecer esta linha soma o passo atual a todos os anteriores.
loss.backward()Backpropagation: preenche p.grad de cada parâmetro.
opt.step()Aplica a regra de atualização. É aqui, e só aqui, que o otimizador aparece.
def avaliar(rede, X, y, bs=2000):
    """Perda média e acurácia sobre um conjunto inteiro, sem gradiente."""
    rede.eval()
    perda_total, acertos = 0.0, 0
    lf = nn.CrossEntropyLoss(reduction="sum")
    with torch.no_grad():
        for i in range(0, len(X), bs):
            saida = rede(X[i:i+bs])
            perda_total += lf(saida, y[i:i+bs]).item()
            acertos     += (saida.argmax(1) == y[i:i+bs]).sum().item()
    rede.train()
    return perda_total / len(X), acertos / len(X)


def treinar(X_tr, y_tr, X_te, y_te, *, batch_size, otimizador, lr, epocas, semente=42):
    rede = criar_rede(semente)
    opt  = criar_otimizador(otimizador, rede.parameters(), lr)   # bloco 3
    lf   = nn.CrossEntropyLoss()

    n  = len(X_tr)
    bs = n if batch_size == "full" else int(batch_size)   # batch completo = uma única fatia
    g  = torch.Generator().manual_seed(semente)

    hist = {"epoca": [], "perda_treino": [], "perda_teste": [],
            "acc_teste": [], "tempo": [], "passos": []}
    passos, t0 = 0, time.time()

    for ep in range(1, epocas + 1):
        perm = torch.randperm(n, generator=g)          # embaralha a cada época
        for i in range(0, n, bs):
            idx = perm[i:i+bs]
            opt.zero_grad(set_to_none=True)
            perda = lf(rede(X_tr[idx]), y_tr[idx])     # forward no mini-batch
            perda.backward()                           # gradiente
            opt.step()                                 # atualização
            passos += 1

        p_tr, _      = avaliar(rede, X_tr, y_tr)     # perda no treino INTEIRO
        p_te, ac_te  = avaliar(rede, X_te, y_te)
        for k, v in zip(hist, [ep, p_tr, p_te, ac_te, time.time()-t0, passos]):
            hist[k].append(v)

    return rede, hist
Duas perdas diferentes com o mesmo nome

Dentro do laço, perda é a perda daquele mini-batch com os pesos daquele instante. Fora do laço, p_tr é a perda no conjunto de treino inteiro ao fim da época. As duas são chamadas de "perda de treino" no dia a dia e não são a mesma coisa — a primeira é uma amostra ruidosa da segunda. O bloco 5 é inteiro sobre essa diferença.

Checkpoint 1 · o custo de uma época

C1Com 60 000 exemplos de treino, quantas chamadas a opt.step() acontecem em uma época com batch_size=128? E com batch_size="full"? E com batch_size=1?2 min

resposta

ceil(60000/128) = 469 passos; 1 passo; 60 000 passos. O custo de forward e backward por época é praticamente o mesmo nos três casos — cada exemplo passa pela rede exatamente uma vez. O que muda em três ordens de grandeza é quantas vezes os pesos se movem. Guarde este número: ele é a chave do bloco 2.

↑ topo

Bloco 2 · 30 min

Batch, mini-batch e SGD

Três nomes para a mesma linha de código com três valores diferentes. A diferença entre eles não é de qualidade do gradiente — é de quantos gradientes ruins você consegue dar no tempo de um gradiente bom.

O gradiente que interessa é o da função de custo sobre a base inteira:

Calculá-lo exige uma passada completa pelos N exemplos. As três estratégias são três respostas diferentes para a pergunta "quantos dos N eu realmente preciso ver antes de mexer nos pesos?".

EstratégiaBPassos por épocaO que se ganha e o que se perde
Batch (completo)
full-batch GD
N1Gradiente exato, curva lisa, totalmente determinístico. Um passo por época é pouco demais.
Estocástico
SGD puro
1NMáximo de atualizações. Estimativa péssima do gradiente, e nenhum aproveitamento de paralelismo — cada passo é uma multiplicação de matriz minúscula.
Mini-batch16 – 512N/BEstimativa razoável, muitos passos e uso pleno de BLAS/GPU. É o que todo mundo usa — e a aula vai medir por quê.
Na literatura "SGD" quase sempre significa mini-batch SGD. O termo original, com B = 1, praticamente não é usado na prática desde os anos 2000.

2.1O experimento: mesmo learning rate para todos

Para isolar o efeito do batch, tudo o mais fica fixo: mesma rede, mesma inicialização, otimizador SGD sem momentum, lr = 0,1, 20 épocas, 10 000 exemplos de treino (um subconjunto do MNIST — cabe no tempo da aula e o efeito é o mesmo).

configs = [("full", "batch completo"), (1024, "mini-batch 1024"),
           (128, "mini-batch 128"), (16, "mini-batch 16"), (1, "SGD puro")]

resultados = {}
for bs, nome in configs:
    _, h = treinar(X_tr[:10_000], y_tr[:10_000], X_te, y_te,
                    batch_size=bs, otimizador="sgd", lr=0.1, epocas=20)
    resultados[nome] = h
    print(f"{nome:>16}  perda {h['perda_treino'][-1]:.4f}  "
          f"acc {h['acc_teste'][-1]:.4f}  passos {h['passos'][-1]}")
Aposte antes · 1 min

Antes de rodar: as cinco configurações acima têm o mesmo learning rate e o mesmo número de épocas. Coloque-as em ordem, da menor para a maior perda de treino ao fim das 20 épocas. Escreva a ordem no papel; não vale mudar depois.

0 2,5 5 7,5 10 12,5 15 17,5 20 época (uma passada pelos 10 000 exemplos) 0 0,5 1 1,5 2 2,5 perda de treino Mesmo orçamento de épocas batch completo (10 000) mini-batch 1024 mini-batch 128 mini-batch 16 SGD puro (1 amostra) 1 0 0 1 0 1 1 0 2 1 0 3 1 0 4 1 0 5 número de atualizações de peso (escala log) 0 0,5 1 1,5 2 2,5 SGD puro diverge: lr = 0,1 é grande demais para uma amostra A mesma perda, cobrada em atualizações
Esquerda: com o mesmo orçamento de 20 épocas, o batch completo mal sai de 1,15 de perda enquanto o mini-batch 16 chega a 0,0001. Direita: a mesma coisa em função do número de atualizações de peso — e aí as curvas quase se sobrepõem. Uma época não é uma unidade de progresso; a unidade de progresso é o passo. O SGD puro (tracejado) não aparece: com lr = 0,1 ele diverge nas primeiras épocas e a perda vira nan.
estratégiapassos em 20 épocasperda de treino finalacurácia de teste
batch completo (10 000)201,147178,98%
mini-batch 10242000,251691,84%
mini-batch 1281 5800,013395,90%
mini-batch 1612 5000,000196,89%
SGD puro (1 amostra)50 000divergiu9,80%
Mesmo lr = 0,1, mesma inicialização, mesmas 20 épocas. 9,8% de acurácia é o que se obtém chutando sempre a mesma classe — a rede do SGD puro não aprendeu nada.
"Diminuir o batch acelera o treino" é meia verdade

Acelera por época, porque dá mais passos. Mas cada passo usa uma estimativa pior do gradiente, então o learning rate que a rede suporta cai junto — e abaixo de um certo tamanho o treino simplesmente explode, como aconteceu com B = 1 acima. As duas metades dessa frase são o assunto das próximas duas seções.

2.2Por que o batch pequeno exige passo pequeno

O gradiente de um mini-batch é uma média amostral do gradiente verdadeiro. Se os gradientes individuais têm variância σ², a média de B deles tem variância σ²/B — e o desvio padrão, que é o que se sente na prática, cai com 1/√B:

Isso é teoria de amostragem, não de redes neurais. Dá para medir diretamente: congela-se a rede num ponto qualquer do treino, calcula-se o gradiente exato g sobre os 10 000 exemplos e compara-se com estimativas de vários tamanhos de batch.

def gradiente(rede, lf, X, y, idx):
    rede.zero_grad(set_to_none=True)
    lf(rede(X[idx]), y[idx]).backward()
    return torch.cat([p.grad.reshape(-1) for p in rede.parameters()]).clone()

g = gradiente(rede, lf, X, y, torch.arange(len(X)))     # gradiente exato

for B in [1, 4, 16, 64, 256, 1024, 4096]:
    erros = []
    for _ in range(150):                                  # 150 sorteios por tamanho
        idx = torch.randint(0, len(X), (B,))
        gB  = gradiente(rede, lf, X, y, idx)
        erros.append(((gB - g).norm() / g.norm()).item())
    print(B, sum(erros)/len(erros))

O simulador abaixo mostra o resultado dessa medição de outro jeito: cada seta clara é um mini-batch sorteado de verdade, projetado no plano que contém o gradiente exato. Arraste o controle e veja o leque fechar.

Tamanho do mini-batch

B = 64 exemplos

erro relativo médio
cosseno com o exato

Gradientes reais, medidos na rede depois de uma época de mini-batch 128 e projetados em duas dimensões: o eixo horizontal é a direção do gradiente exato, o vertical é a direção em que as estimativas mais se espalham. A escala é ‖g‖ = 1, e o cone claro marca o desvio angular médio.

Repetindo a medição para vários tamanhos e resumindo em duas curvas:

2 8 32 128 512 2048 t a m a n h o   d o   b a t c h   B 1 0 1 1 0 0 ̂ / g g g B Quadruplicar o batch corta o ruído pela metade r e f e r ê n c i a     1 / B erro medido 2 8 32 128 512 2048 t a m a n h o   d o   b a t c h   B 0 0,2 0,4 0,6 0,8 1 c o s s e n o   e n t r e   ̂   e   g g B 0,9 A direção só fica confiável a partir de ~128
Esquerda: o erro relativo medido acompanha a reta de inclinação −1/2 prevista pela teoria — em escala log-log, quadruplicar B desce a curva pela metade. Direita: o cosseno entre a estimativa e o gradiente exato. Com B = 1 o cosseno médio é 0,16: a direção do passo está a quase 81° da direção correta. Só a partir de B ≈ 128 o cosseno passa de 0,9 — a partir daí o mini-batch aponta essencialmente para onde o gradiente exato aponta, e o resto é ruído de amplitude.
B1161285124096
erro relativo ‖ĝBg‖ / ‖g3,671,220,460,220,08
cosseno com g0,160,630,920,981,00
Medido com a rede após uma época de mini-batch 128 sobre 10 000 exemplos, média de 150 sorteios por tamanho. ‖g‖ = 3,04.

Com o erro relativo em 3,7, o passo do SGD puro é dominado pelo ruído: a componente aleatória é quase quatro vezes maior que o sinal. Multiplicar esse vetor por lr = 0,1 e somá-lo aos pesos, 10 000 vezes por época, é um passeio aleatório com passo grande — e é exatamente isso que a curva tracejada da figura anterior mostra. A correção é reduzir o learning rate na mesma proporção em que se reduz o batch.

2.3O mapa completo: 36 treinos, 6 batches × 6 learning rates

A seção anterior explicou por que o batch e o learning rate estão amarrados. Esta mede o quanto. São 36 treinos de 20 épocas sobre os mesmos 10 000 exemplos — tudo idêntico, menos essas duas escolhas. Explore antes de ler a conclusão: escolha um tamanho de batch e passeie pelos learning rates; depois fixe uma curva e mude o batch.

Tamanho do batch
Learning rate
passos por época
passos no total
tempo de treino
perda final
acurácia de teste

36 treinos reais de 20 épocas sobre os mesmos 10 000 exemplos, mesma inicialização, otimizador SGD sem momentum. As curvas apagadas são os outros learning rates do mesmo tamanho de batch.

Aposte antes · 2 min

Com o simulador aberto, responda sem olhar o mapa da próxima figura: (a) qual é o maior learning rate que o batch 128 aguenta sem divergir? (b) e o batch 1? (c) a razão entre os dois se parece mais com 3, com 30 ou com 300?

0,003 0,01 0,03 0,1 0,3 1 learning rate batch completo 1024 128 16 4 1 (SGD puro) tamanho do batch 2,303 2,256 2,101 1,147 1,050 1,716 2,096 1,102 0,418 0,252 0,112 1,402 0,482 0,267 0,131 0,013 0,0020 2,304 0,161 0,025 0,0016 0,0001 diverge 2,304 0,015 0,0009 0,0061 2,303 2,256 diverge 0,0006 0,0001 1,809 diverge diverge diverge Perda de treino após 20 épocas · SGD · 10 000 exemplos
Os mesmos 36 treinos, de uma vez. Quanto mais escura a célula, menor a perda ao fim de 20 épocas; a moldura marca o melhor learning rate de cada linha e "diverge" marca os treinos que produziram nan. A região boa é uma faixa diagonal: cada vez que o batch cai por um fator de 4, o learning rate que ainda funciona cai junto. À direita da faixa o treino explode; à esquerda ele mal sai do lugar. Repare que a diagonal é a mesma inclinação da figura anterior — é o ruído de 1/√B, visto pelo outro lado.
estratégiamelhor lrperda finalacuráciapassostempo
batch completo0,31,049566,99%201,1 s
mini-batch 10240,30,112394,30%2001,4 s
mini-batch 1280,30,002096,00%1 5802,2 s
mini-batch 160,10,000196,89%12 5008,6 s
mini-batch 40,010,000996,30%50 00031,8 s
SGD puro0,010,000196,57%200 00079,4 s
O melhor lr de cada linha do mapa, com o que ele entrega em 20 épocas. De cima para baixo o learning rate ótimo cai de 0,3 para 0,01 — um fator de 30 — enquanto o tempo de treino sobe de 1,1 s para 79,4 s, um fator de quase 75. Medido em CPU de 2 núcleos; os tempos absolutos dependem da máquina, a razão entre eles não.
0 2,5 5 7,5 10 12,5 15 17,5 20 época 1 0 4 1 0 3 1 0 2 1 0 1 1 0 0 perda de treino (escala log) Cada tamanho com o seu melhor learning rate batch completo · lr* = 0,3 1024 · lr* = 0,3 128 · lr* = 0,3 16 · lr* = 0,1 1 amostra · lr* = 0,01 0,1 1 10 100 tempo de treino acumulado, em segundos (escala log) 1 0 4 1 0 3 1 0 2 1 0 1 1 0 0 O mesmo resultado, cobrado em relógio
As mesmas linhas do mapa, agora como curvas. Esquerda: com o learning rate apropriado todos convergem, inclusive o SGD puro, que chega ao menor valor de todos. O batch completo continua para trás porque 20 passos são 20 passos. Direita: a mesma corrida medida em segundos, e a ordem inverte. Em escala logarítmica: o mini-batch 128 chega a 2 × 10⁻³ de perda em 2,2 s; o SGD puro precisa de 79 s para um resultado equivalente. Ordenar por época favorece o batch pequeno; ordenar por relógio favorece o mini-batch.
A linha do batch completo tem uma armadilha

O melhor learning rate da primeira linha do mapa é 0,3, pela perda de treino. Mas a acurácia de teste dessa célula é 66,99%, enquanto a de lr = 0,1, com perda pior, é 78,98%. Com apenas 20 atualizações, a rede ainda está tão longe de qualquer mínimo que a perda de treino simplesmente não é um bom indicador de nada. É um aviso barato para uma lição cara: o critério de seleção precisa ser o que você de fato quer otimizar — assunto que volta no bloco 4.

A regra prática que sai daqui
  • Comece em 32, 64 ou 128. É a faixa onde o cosseno com o gradiente exato já passa de 0,9 e a matriz do batch ainda é grande o bastante para a BLAS trabalhar.
  • Dobrou o batch, pode dobrar o learning rate — a regra de escalonamento linear. É a diagonal do mapa. Vale bem no miolo e quebra nos extremos: com batch muito grande, o lr necessário passa do que a curvatura da função de custo suporta, e a diagonal achata (compare as linhas 1024 e batch completo).
  • Batch grande não é "mais preciso", é mais caro por passo. Só compensa se você tem paralelismo ocioso para preenchê-lo.
  • Se o treino explodiu, a primeira coisa a mexer é o learning rate, não o otimizador nem a arquitetura. O mapa mostra que a fronteira entre "converge" e nan é de um único degrau de fator 3.

Checkpoints 2 e 3

C2Um colega treina com B = 256 e lr = 0,2, e a curva está boa. Ele decide passar para B = 64 para "dar mais passos por época". Que valor de lr você sugere para ele começar, e por quê?3 min

resposta

Cerca de 0,05. O batch caiu por um fator de 4, então o desvio padrão da estimativa do gradiente dobrou (√4 = 2); pela regra de escalonamento linear, divide-se o learning rate pelo mesmo fator 4 com que se dividiu o batch. Manter lr = 0,2 é o cenário do gráfico 2.1: mais passos, cada um mais ruidoso e do mesmo tamanho — a receita da divergência. Se 0,05 se mostrar lento demais, subir para 0,1 é seguro; o ponto é começar mais baixo, não adivinhar o valor exato.

C3Reproduza duas linhas do mapa da figura 2.3 no notebook: B = 64 e B = 512, os seis learning rates, 20 épocas. Os lr* que você encontra caem entre os vizinhos do mapa (128 e 1024, e 128 e 16)?7 min · no notebook

o que esperar

Os dois vão dar lr* = 0,3, igual ao 128. Isso é parte da resposta: a regra de escalonamento é uma orientação de ordem de grandeza, e uma grade de fator ~3 não resolve diferenças de fator 2 — no miolo do mapa (16 a 1024) o ótimo se move devagar. O escalonamento só fica visível quando o batch muda por um fator grande, como de 128 para 1. Se quiser separar os vizinhos, refine a grade em torno do vencedor ([0.1, 0.15, 0.2, 0.3, 0.45]) — e repare em quanto tempo a mais isso custa para um ganho que provavelmente não aparece na acurácia.

↑ topo

Bloco 3 · 20 min

Trocando o otimizador

Sete regras de atualização, uma linha de código de diferença entre elas. A pergunta desta seção não é "qual é o melhor", e sim o que exatamente cada um resolve — e o que isso custa em ajuste.

Todo otimizador desta aula recebe o mesmo ĝB e devolve um novo vetor de pesos. O SGD puro faz o mínimo:

Os demais acrescentam memória. Momentum guarda a média dos gradientes recentes; AdaGrad, RMSProp e Adam guardam também a magnitude típica de cada coordenada, para dar passos maiores nas direções que se movem pouco. A tabela abaixo é o resumo operacional — a derivação de cada um foi a aula de Otimização.

OtimizadorO que acrescentalr típicoQuando importa
SGDNada. Passo proporcional ao gradiente.0,01 – 0,5Base de comparação. Com o lr certo, é competitivo em quase tudo.
MomentumMédia móvel exponencial do gradiente (β = 0,9).0,003 – 0,1Vales estreitos e alongados: cancela a oscilação transversal e acumula na direção do vale.
NesterovMomentum avaliado no ponto projetado.0,003 – 0,1Correção teoricamente melhor perto do mínimo. Na prática, quase indistinguível de momentum.
AdaGradDivide o passo pela raiz da soma dos gradientes ao quadrado, por coordenada.0,01 – 0,1Features esparsas. A soma só cresce, então o passo efetivo tende a zero — problema em treinos longos.
RMSPropIgual ao AdaGrad, mas com média móvel em vez de soma.0,0003 – 0,003Resolve o passo que morre do AdaGrad. Sem momentum, oscila mais perto do mínimo.
AdamRMSProp + momentum, com correção de viés nos primeiros passos.0,0003 – 0,003O padrão de fato. Funciona razoavelmente sem ajuste nenhum — que é a sua principal virtude.
AdamWAdam com weight decay desacoplado da adaptação.0,0003 – 0,003Quando há regularização L2. Em Adam, o decay é distorcido pelo denominador adaptativo; AdamW corrige isso.
As faixas de lr típicas não são folclore — são o que a grade da seção 4 mede nesta base. Repare no salto de duas ordens de grandeza entre a família SGD e a família adaptativa: trocar de otimizador sem trocar o lr é trocar duas coisas ao mesmo tempo.
def criar_otimizador(nome, params, lr):
    if nome == "sgd":      return torch.optim.SGD(params, lr=lr)
    if nome == "momentum": return torch.optim.SGD(params, lr=lr, momentum=0.9)
    if nome == "nesterov": return torch.optim.SGD(params, lr=lr, momentum=0.9, nesterov=True)
    if nome == "adagrad":  return torch.optim.Adagrad(params, lr=lr)
    if nome == "rmsprop":  return torch.optim.RMSprop(params, lr=lr, alpha=0.99)
    if nome == "adam":     return torch.optim.Adam(params, lr=lr)
    if nome == "adamw":    return torch.optim.AdamW(params, lr=lr, weight_decay=0.01)
    raise ValueError(nome)

3.1Primeiro erro: comparar todos no mesmo learning rate

É o teste que quase todo mundo faz primeiro — fixa lr = 0,01, roda os sete, e conclui alguma coisa. Vale fazer, desde que se saiba o que está sendo medido. MNIST completo, 60 000 exemplos, mini-batch 128, 15 épocas:

0 2 4 6 8 10 12 14 época 1 0 2 1 0 1 1 0 0 perda de treino (escala log) Família SGD SGD Momentum Nesterov 0 2 4 6 8 10 12 14 época Adaptativos AdaGrad RMSProp Adam AdamW MNIST completo · 60 000 exemplos · mini-batch 128 · todos com lr = 0,01
Em lr = 0,01, o SGD puro fica claramente para trás e os adaptativos parecem superiores. Não é uma comparação de otimizadores — é uma comparação de otimizadores naquele learning rate, que por acaso é bom para momentum e ruim para SGD. Note também que Momentum e Nesterov são praticamente a mesma curva, e que RMSProp começa a subir depois da época 12: sem momentum, ele oscila quando a perda já está baixa.

3.2Agora com o melhor learning rate de cada um

Refazendo com o lr vencedor da grade da seção 4 para cada otimizador — 0,3 para SGD, 0,03 para momentum, Nesterov e AdaGrad, 0,003 para os três adaptativos restantes:

Aposte antes · 1 min

No gráfico anterior o SGD puro terminou com uma perda cerca de 20× maior que a do momentum. Agora cada um vai rodar no seu melhor learning rate. Aposte na diferença entre o primeiro e o último colocado depois do ajuste: continua em 20×? Cai para 2×? Some?

0 2 4 6 8 10 12 14 época 1 0 3 1 0 2 1 0 1 1 0 0 perda de treino (escala log) Família SGD SGD (lr 0,3) Momentum (lr 0,03) Nesterov (lr 0,03) 0 2 4 6 8 10 12 14 época Adaptativos AdaGrad (lr 0,03) RMSProp (lr 0,003) Adam (lr 0,003) AdamW (lr 0,003) MNIST completo · 60 000 exemplos · mini-batch 128 · cada um no seu melhor lr
Com cada um no seu ponto, as sete curvas praticamente se sobrepõem — e o SGD puro, que era o pior no gráfico anterior, agora é o que desce mais fundo. AdamW (tracejado) acompanha Adam de perto, como esperado: em 15 épocas o weight decay de 0,01 mal teve tempo de agir.
92 93 94 95 96 97 98 99 melhor acurácia de teste em 15 épocas (%) SGD Momentum Nesterov AdaGrad RMSProp Adam AdamW 95,89 97,99 97,94 97,71 96,11 97,17 96,81 98,31 98,14 98,05 97,82 97,78 97,70 97,76 Ajustar o learning rate move mais do que trocar de otimizador lr = 0,01 para todos melhor lr de cada um
A mesma história em acurácia de teste. A barra clara é o teste ingênuo (lr = 0,01 para todos); a escura, cada um no seu melhor lr. Trocar de otimizador mexeu 0,6 ponto percentual; ajustar o learning rate do SGD mexeu 2,4. A largura da barra clara é o tamanho do erro que se comete ao comparar otimizadores sem ajustar o passo de cada um.
O que a comparação honesta mostra
  • Nenhum otimizador é magicamente melhor aqui. Numa rede pequena e uma base bem comportada, todos chegam ao mesmo lugar quando ajustados.
  • A vantagem real do Adam é outra: a faixa de lr em que ele funciona bem é mais larga e mais previsível entre problemas. Ele economiza busca, não perda.
  • Momentum é o melhor custo-benefício da família SGD: uma linha a mais, e o lr ótimo cai de 0,3 para 0,03 com resultado equivalente e menos sensibilidade.
  • Nesterov ≈ Momentum nesta escala. A diferença teórica existe; nesta base ela não aparece.

Checkpoint 4

C4Alguém publica um gráfico mostrando que "o otimizador X é 3% melhor que Adam", com todos rodando em lr = 0,001. Qual é a primeira pergunta que você faz? E qual experimento adicional torna a comparação defensável?3 min

resposta

Pergunta: esse lr foi ajustado para cada otimizador ou escolhido uma vez só? 0,001 é o padrão do Adam — usá-lo para todos coloca o Adam no seu ponto e todos os outros fora do deles, o que é exatamente o viés inverso do gráfico 3.1.

Experimento: uma grade de lr por otimizador (como a da seção 4), reportando cada um no seu melhor valor, com a mesma inicialização, o mesmo batch e o mesmo orçamento de épocas — e de preferência 3 sementes por célula, com a variação entre sementes no gráfico. Se a diferença de 3% for menor que a variação entre sementes, ela não existe.

↑ topo

Bloco 4 · 15 min

A grade otimizador × learning rate

Sete otimizadores, nove learning rates, 63 treinos de 6 épocas cada. Menos de dois minutos de máquina — e é a figura mais útil da aula.

O padrão de busca mais barato que existe é uma grade logarítmica: potências de dez e os pontos intermediários em 3 — 1e-4, 3e-4, 1e-3, 3e-3, 1e-2, 3e-2, 1e-1, 3e-1, 1. A razão de 3,16 entre vizinhos cobre quatro ordens de grandeza em nove pontos, e é fina o bastante para achar a região certa. Refinar dentro dela é uma segunda passada, opcional.

OPTS = ["sgd", "momentum", "nesterov", "adagrad", "rmsprop", "adam", "adamw"]
LRS  = [1e-4, 3e-4, 1e-3, 3e-3, 1e-2, 3e-2, 1e-1, 3e-1, 1.0]

grade = {}
for o in OPTS:
    for lr in LRS:
        _, h = treinar(X_tr[:10_000], y_tr[:10_000], X_te, y_te,
                        batch_size=128, otimizador=o, lr=lr, epocas=6)
        v = h["perda_treino"][-1]
        grade[(o, lr)] = v if math.isfinite(v) and v < 10 else None   # None = divergiu
Aposte antes · 1 min

Você já viu, no bloco 2, que o SGD sobrevive a lr = 0,3 com batch 128. Qual otimizador vai divergir primeiro conforme o learning rate sobe — o SGD puro, o momentum ou o Adam? E qual vai ser o único que ainda aprende alguma coisa em lr = 0,0001?

0,0001 0,0003 0,001 0,003 0,01 0,03 0,1 0,3 1 learning rate SGD Momentum 0,9 Nesterov AdaGrad RMSProp Adam AdamW 2,31 2,28 2,15 1,54 0,50 0,32 0,21 0,14 2,30 2,16 1,59 0,50 0,30 0,16 0,05 0,07 2,31 2,33 2,16 1,58 0,50 0,29 0,16 0,05 0,13 2,30 diverge 1,86 0,94 0,39 0,25 0,12 0,06 0,18 0,70 1,90 0,34 0,23 0,09 0,06 0,13 2,29 1,91 diverge diverge 0,36 0,22 0,10 0,06 0,09 0,28 1,63 2,33 2,33 0,36 0,22 0,10 0,06 0,13 0,19 1,48 2,33 2,10 moldura = melhor lr da linha Perda de treino após 6 épocas · mini-batch 128 · 10 000 exemplos
Perda de treino após 6 épocas. Quanto mais escura a célula, melhor. A moldura marca o melhor lr de cada linha; as células hachuradas são treinos que produziram nan. A leitura é diagonal: a região boa desce para a esquerda conforme se acrescenta memória ao otimizador — SGD em 0,3, momentum e AdaGrad em 0,03, os adaptativos em 0,003. São duas ordens de grandeza entre o topo e a base da tabela.
Quatro coisas que só a grade mostra
  • A janela do SGD é estreita e alta. Entre 0,003 e 0,3 a perda cai de 1,54 para 0,14; em 1,0 ele para de aprender de novo (2,30). Nenhum valor "seguro" abaixo de 0,01.
  • Momentum desloca a janela do SGD em cerca de 10× — o que faz sentido: a média móvel com β = 0,9 acumula um passo efetivo aproximadamente 1/(1−β) = 10 vezes maior.
  • Os adaptativos toleram valores muito baixos — em 1e-4 Adam já chega a 0,36 enquanto o SGD está em 2,31, ou seja, parado. Por isso Adam "funciona sem ajuste".
  • E quebram mais cedo: RMSProp diverge em 0,3, enquanto o SGD ainda melhora. Tolerância embaixo, fragilidade em cima.
Perda de treino não é o critério final

A grade acima seleciona pela perda de treino após 6 épocas, que é o que se quer para responder "este otimizador está andando?". Para escolher a configuração que vai para produção, o critério é a perda de validação — e as duas nem sempre apontam para o mesmo lugar, como o bloco 5 vai mostrar. Um lr um pouco menor que o ótimo de treino costuma generalizar melhor.

Checkpoint 5

C5Você tem orçamento para 12 treinos de 5 épocas e precisa escolher otimizador e learning rate para um problema novo, parecido com este. Como você gasta os 12? Escreva a lista de configurações antes de olhar a resposta.4 min

uma resposta defensável

Passo 1 (8 treinos): fixe um otimizador conservador — Adam — e varra o lr inteiro: 1e-4 a 3e-1. Isso localiza a ordem de grandeza da região boa e, de quebra, dá uma linha de base decente.

Passo 2 (4 treinos): pegue o melhor lr do passo 1 e teste momentum em quatro valores em torno de 10× esse lr. Se momentum empatar ou ganhar, você tem um treino mais barato por passo e menos estado a carregar.

O que não fazer com 12 treinos: uma grade 3 × 4 de otimizador × lr. Ela é fina demais no eixo que importa (o lr) e larga demais no eixo que quase não importa (o otimizador) — como as figuras 3.2 e 4 mostraram.

↑ topo

Bloco 5 · 20 min

A curva de aprendizado

Acompanhar o decaimento do erro parece a parte trivial da aula. É onde mora o maior número de conclusões erradas: três curvas diferentes recebem o mesmo nome, e só uma delas responde à pergunta que você está fazendo.

5.1Três curvas do mesmo treino

Durante um treino existem pelo menos três séries que podem ser chamadas de "perda de treino":

SérieCusto de registrarO que ela responde
Perda do mini-batch
um ponto por passo
Zero — já foi calculada."O treino travou ou virou nan?" Serve para diagnóstico imediato, não para comparar configurações.
Média móvel
janela de 20–100 passos
Zero."A tendência está descendo?" É a curva que se olha enquanto o treino roda.
Perda no conjunto inteiro
um ponto por época
Uma passada extra, sem gradiente."Qual configuração é melhor?" É a única comparável entre treinos, porque é medida nos mesmos dados e com os mesmos pesos.
Aposte antes · 1 min

O painel da direita treina a rede com Adam por 40 épocas sobre 10 000 exemplos, e mostra treino e teste juntos. Em que época a perda de teste para de melhorar? Escreva um número. E a acurácia de teste no fim vai ser melhor ou pior do que nessa época?

0 200 400 600 800 atualizações de peso 0 0,2 0,4 0,6 0,8 1 1,2 1,4 1,6 perda Três curvas do mesmo treino perda do mini-batch, a cada passo média móvel de 25 passos perda no treino inteiro, ao fim de cada época 0 5 10 15 20 25 30 35 40 época 0 0,05 0,1 0,15 0,2 0,25 0,3 0,35 0,4 perda mínimo da perda de teste época 13 Adam, 40 épocas, 10 000 exemplos treino teste (10 000 dígitos não vistos)
Esquerda: as três séries do mesmo treino (SGD, mini-batch 128, lr = 0,05, 12 épocas). A série por passo, em azul claro, oscila num intervalo de largura comparável ao progresso de várias épocas — comparar dois treinos por ela é ler ruído. A média móvel e a perda no conjunto inteiro contam a mesma história, e essa história é a que interessa. Direita: por que a perda de treino sozinha não basta.
O gráfico que engana: perda por passo entre duas configurações

Plotar a série azul-clara de duas configurações no mesmo eixo é o erro mais comum de relatório de aluno. Como a amplitude do ruído depende do tamanho do batch, a configuração com batch menor parece pior mesmo quando é melhor — o que você está comparando é a variância da estimativa, não a qualidade do modelo. Para comparar, use sempre a perda no conjunto inteiro, ao fim de cada época.

5.2Treino e validação na mesma figura

O painel da direita da figura acima é Adam com lr = 0,001 sobre 10 000 exemplos, 40 épocas. A perda de treino cai até 0,0004 — a rede memorizou o conjunto. A perda de teste atinge o mínimo de 0,1558 na época 13 e depois sobe, terminando em 0,2020. Tudo o que foi treinado depois da época 13 piorou o modelo em dados novos.

Uma sutileza que vale a discussão

A acurácia de teste no mínimo da perda (época 13) é 95,75%; na época 40, com a perda bem pior, é 96,05%. Perda e acurácia não atingem o ótimo na mesma época. A perda pune a confiança errada — a rede continua acertando o mesmo número de dígitos, mas passa a errar com mais convicção nos que erra. Qual das duas usar para parar depende do que o modelo vai fazer: se a saída alimenta uma decisão de custo assimétrico, ou se você precisa das probabilidades calibradas, a perda manda; se só o rótulo final importa, a acurácia.

5.3O código do gráfico

Nada de sofisticado — mas com quatro decisões que fazem diferença: escala logarítmica no eixo da perda (senão as últimas épocas viram uma linha reta colada no zero), as duas séries na mesma figura, marcador no mínimo de validação e rótulo direto em vez de legenda quando são poucas curvas.

def plotar_curva(hist, titulo=""):
    fig, ax = plt.subplots(figsize=(7, 4))
    ax.plot(hist["epoca"], hist["perda_treino"], label="treino")
    ax.plot(hist["epoca"], hist["perda_teste"],  label="validação")

    i = min(range(len(hist["perda_teste"])), key=lambda k: hist["perda_teste"][k])
    ax.plot(hist["epoca"][i], hist["perda_teste"][i], "o", ms=9, mfc="none")
    ax.axvline(hist["epoca"][i], ls=":", lw=1)
    ax.annotate(f"mínimo: época {hist['epoca'][i]}",
                (hist["epoca"][i], hist["perda_teste"][i]),
                xytext=(12, 18), textcoords="offset points")

    ax.set_yscale("log")                     # sem isso o fim da curva some
    ax.set_xlabel("época"); ax.set_ylabel("perda")
    ax.set_title(titulo); ax.legend(); ax.grid(alpha=.3)
    return fig


def comparar(historicos, chave="perda_treino"):
    """historicos: dict nome -> hist. Uma curva por configuração."""
    fig, ax = plt.subplots(figsize=(7, 4))
    for nome, h in historicos.items():
        ax.plot(h["epoca"], h[chave], label=nome)
        ax.annotate(nome, (h["epoca"][-1], h[chave][-1]),      # rótulo direto
                    xytext=(6, 0), textcoords="offset points", fontsize=9)
    ax.set_yscale("log"); ax.set_xlabel("época"); ax.set_ylabel(chave)
    ax.grid(alpha=.3)
    return fig
Parada antecipada, em três linhas

Se a intenção é parar no melhor ponto em vez de descobri-lo depois, guarde os pesos do mínimo enquanto treina — o custo é uma cópia do state_dict:

melhor, melhores_pesos, paciencia = float("inf"), None, 0
# ... dentro do laço de épocas, depois de avaliar:
if p_te < melhor:
    melhor, melhores_pesos, paciencia = p_te, copy.deepcopy(rede.state_dict()), 0
else:
    paciencia += 1
    if paciencia >= 5: break          # 5 épocas sem melhorar
rede.load_state_dict(melhores_pesos)

E o lembrete de sempre: em um projeto de verdade essa curva é de validação, não de teste. O conjunto de teste é medido uma vez, no fim. Usar o teste para escolher a época de parada é vazamento — o número que você reporta deixa de ser uma estimativa honesta.

Checkpoint 6

C6Treine a rede com adam, lr = 0,001, mini-batch 128, sobre os 10 000 exemplos, por 40 épocas, e plote treino e validação juntos. Depois repita com os 60 000. Em qual dos dois a perda de validação volta a subir, e por quê?8 min · no notebook

resposta

Com 10 000 exemplos a validação atinge o mínimo por volta da época 13 e sobe claramente depois. Com 60 000 o mínimo aparece bem mais tarde e a subida é quase imperceptível no mesmo número de épocas.

Por quê: uma rede de 109 386 parâmetros tem capacidade de sobra para decorar 10 000 exemplos; com 60 000, a mesma capacidade está bem mais ocupada em aprender o que é geral. Sobreajuste não é propriedade do modelo sozinho — é a razão entre capacidade e quantidade de dados. As três saídas para o mesmo problema: mais dados, menos capacidade, ou regularização (o assunto da próxima aula).

↑ topo

Bloco 6 · 15 min

Laboratório: a sua grade

Agora sem roteiro. Você escolhe um otimizador, roda quatro tentativas com ele e lê o que a sua própria tabela mostra.

A pergunta: qual configuração chega a 97% de acurácia de teste no menor tempo de relógio? Base: os 60 000 exemplos de treino, no máximo 10 épocas por tentativa.

6.1A sua tabela

#hipótese que você está testandobatchlrépoca até 97%tempo (s)acc final
1ponto de partida do bloco 4, batch pequeno64    
2 64    
3o mesmo lr da linha 1, com batch 8× maior512    
4 512    
Cada linha é uma hipótese, não um chute. Se você não consegue escrever a coluna 2 antes de rodar, ainda não é um experimento. As linhas 3 e 4 existem para você testar a regra de escalonamento do bloco 2 no seu otimizador.
def epocas_ate(hist, alvo=0.97):
    """Primeira época em que a acurácia de teste atinge o alvo, ou None."""
    for k, ac in enumerate(hist["acc_teste"]):
        if ac >= alvo:
            return hist["epoca"][k], hist["tempo"][k]
    return None, None

MEU_OTIMIZADOR = "momentum"            # o que você vai testar

tentativas = {}
for bs in [64, 512]:
    for lr in [0.03, 0.1]:            # os dois que você escolheu
        nome = f"{MEU_OTIMIZADOR} · b={bs} · lr={lr}"
        _, h = treinar(X_tr, y_tr, X_te, y_te, batch_size=bs,
                        otimizador=MEU_OTIMIZADOR, lr=lr, epocas=10)
        tentativas[nome] = h
        ep, t = epocas_ate(h)
        print(f"{nome:>28}  " + (f"97% na época {ep} ({t:.1f}s)" if ep
              else f"não chegou — acc final {h['acc_teste'][-1]:.4f}"))

comparar(tentativas, chave="perda_teste")

Checkpoint 7 · a leitura da tabela

C7Com a sua tabela preenchida, e a grade do bloco 4 ao lado dela, responda: (a) qual das três decisões — batch, otimizador ou learning rate — explicou a maior parte da variação nos tempos; (b) a configuração vencedora venceu por dar passos melhores ou por dar mais passos no mesmo tempo; (c) ao passar de batch 64 para 512 mantendo o lr, o que aconteceu — e o que a regra de escalonamento previa?5 min

o que uma boa resposta contém

Números, não adjetivos. "Adam foi melhor" não é resposta; "trocar o lr do SGD de 0,01 para 0,3 tirou 4 épocas do tempo até 97%, enquanto trocar SGD por Adam no mesmo lr tirou 1" é.

Em (b), olhe a coluna de épocas ao lado da de segundos: menos épocas significa passos melhores; mesmo número de épocas em menos segundos significa eficiência por passo — o batch maior aproveitando melhor a máquina.

Em (c), o esperado é que o batch 512 com o mesmo lr fique mais lento por época em progresso (menos passos) e que o efeito seja compensado dobrando ou quadruplicando o lr. Se na sua tabela isso não aconteceu, olhe onde aquele lr cai no mapa da figura 2.3 — provavelmente já estava na parte plana da faixa.

↑ topo

Referência A

Tabela de bolso

Sintoma na curvaCausa mais provávelO que tentar primeiro
Perda vira nan ou explodeLearning rate grande demais para o batch usado.Dividir o lr por 10. Se persistir, aumentar o batch ou verificar a normalização da entrada.
Perda quase não desce em 10 épocasLearning rate pequeno demais, ou batch grande demais (poucos passos).Multiplicar o lr por 3 até algo mudar. Se nada muda em quatro ordens de grandeza, o problema não é o lr.
Desce e depois sobe, no treinoPasso grande demais perto do mínimo.Reduzir o lr, ou agendar decaimento (StepLR, CosineAnnealingLR).
Desce no treino, sobe na validaçãoSobreajuste.Parada antecipada; depois, mais dados, menos capacidade ou regularização.
Curva serrilhada, sem tendência claraVocê está plotando a perda por passo.Trocar pela perda no conjunto inteiro por época, ou por média móvel.
Duas configurações empatamProvavelmente empatam mesmo.Repetir com 3 sementes. Se a diferença cabe dentro da variação entre sementes, escolha a mais barata.
ponto de partidavalorquando mudar
batch_size128Menor se a base é pequena; maior se a GPU está ociosa.
otimizadoradamTrocar por momentum quando houver tempo para ajustar o lr.
lr (adam)1e-3Varrer 1e-4 a 1e-2 antes de mexer em qualquer outra coisa.
lr (sgd / momentum)0,3 / 0,03Reduzir na mesma proporção em que reduzir o batch.
épocasaté a validação parar de melhorarNunca é um número fixo escolhido de antemão.
↑ topo

Referência B

Código completo

O arquivo mínimo que reproduz todos os experimentos da aula. É o mesmo conteúdo do notebook, sem as células de texto.

import copy, math, time
import torch, torch.nn as nn
import matplotlib.pyplot as plt
from torchvision import datasets, transforms

torch.set_num_threads(2)

# ---------- dados ----------
treino = datasets.MNIST("./dados", train=True,  download=True)
teste  = datasets.MNIST("./dados", train=False, download=True)
def prep(d):
    X = d.data.reshape(-1, 784).float().div(255)
    return X.sub(0.1307).div(0.3081), d.targets
X_tr, y_tr = prep(treino)
X_te, y_te = prep(teste)

# ---------- modelo e otimizador ----------
def criar_rede(semente=42):
    torch.manual_seed(semente)
    return nn.Sequential(nn.Linear(784, 128), nn.ReLU(),
                         nn.Linear(128, 64),  nn.ReLU(),
                         nn.Linear(64, 10))

OTIM = {
    "sgd":      lambda p, lr: torch.optim.SGD(p, lr=lr),
    "momentum": lambda p, lr: torch.optim.SGD(p, lr=lr, momentum=0.9),
    "nesterov": lambda p, lr: torch.optim.SGD(p, lr=lr, momentum=0.9, nesterov=True),
    "adagrad":  lambda p, lr: torch.optim.Adagrad(p, lr=lr),
    "rmsprop":  lambda p, lr: torch.optim.RMSprop(p, lr=lr),
    "adam":     lambda p, lr: torch.optim.Adam(p, lr=lr),
    "adamw":    lambda p, lr: torch.optim.AdamW(p, lr=lr, weight_decay=0.01),
}

# ---------- avaliação e treino ----------
def avaliar(rede, X, y, bs=2000):
    rede.eval(); lf = nn.CrossEntropyLoss(reduction="sum")
    total, acertos = 0.0, 0
    with torch.no_grad():
        for i in range(0, len(X), bs):
            o = rede(X[i:i+bs])
            total   += lf(o, y[i:i+bs]).item()
            acertos += (o.argmax(1) == y[i:i+bs]).sum().item()
    rede.train()
    return total/len(X), acertos/len(X)

def treinar(X_tr, y_tr, X_te, y_te, *, batch_size, otimizador, lr, epocas,
            semente=42, registrar_passos=False):
    rede = criar_rede(semente)
    opt  = OTIM[otimizador](rede.parameters(), lr)
    lf   = nn.CrossEntropyLoss()
    n    = len(X_tr)
    bs   = n if batch_size == "full" else int(batch_size)
    g    = torch.Generator().manual_seed(semente)

    hist = {k: [] for k in ("epoca", "perda_treino", "perda_teste",
                            "acc_teste", "tempo", "passos")}
    passos_perda, passos, t0 = [], 0, time.time()

    for ep in range(1, epocas + 1):
        perm = torch.randperm(n, generator=g)
        for i in range(0, n, bs):
            idx = perm[i:i+bs]
            opt.zero_grad(set_to_none=True)
            perda = lf(rede(X_tr[idx]), y_tr[idx])
            perda.backward()
            opt.step()
            passos += 1
            if registrar_passos:
                passos_perda.append(perda.item())
        p_tr, _     = avaliar(rede, X_tr, y_tr)
        p_te, ac_te = avaliar(rede, X_te, y_te)
        for k, v in zip(hist, [ep, p_tr, p_te, ac_te, time.time()-t0, passos]):
            hist[k].append(v)
        if not math.isfinite(p_tr):
            print(f"  divergiu na época {ep}"); break

    if registrar_passos:
        hist["perda_passo"] = passos_perda
    return rede, hist
Reprodutibilidade

Todos os números desta aula saíram deste código, em CPU de 2 núcleos, PyTorch 2.x, semente 42. As perdas e acurácias são reprodutíveis exatamente; os tempos obviamente dependem da máquina — o que se transporta de uma máquina para outra é a razão entre eles, não os valores absolutos.

↑ topo

Referência C

As sete ideias, em uma linha cada

# 
1A unidade de progresso do treino é o passo, não a época. Batch completo dá um passo por época, e isso é pouco.
2O ruído da estimativa do gradiente cai com 1/√B. É medível, e explica por que batch pequeno exige passo pequeno.
3Mini-batch entre 32 e 512 é o ponto onde a direção já é confiável e a máquina ainda trabalha cheia.
4Comparar otimizadores no mesmo learning rate compara duas coisas ao mesmo tempo — e a conclusão sai errada.
5Com o lr ajustado, os sete otimizadores empatam nesta base. A vantagem do Adam é a largura da janela, não o fundo do poço.
6Uma grade logarítmica de nove pontos custa menos de dois minutos e vale mais que qualquer intuição sobre o lr.
7Para comparar treinos, use a perda no conjunto inteiro por época — e sempre com a curva de validação ao lado.
Leituras

Goodfellow, Bengio & Courville, Deep Learning, cap. 8 (Optimization for Training Deep Models) — a referência canônica para tudo nesta aula · Bottou, Curtis & Nocedal, "Optimization Methods for Large-Scale Machine Learning" (SIAM Review, 2018), a análise formal do compromisso entre ruído e número de passos · Goyal et al., "Accurate, Large Minibatch SGD" (2017), a origem prática da regra de escalonamento linear · Smith et al., "Don't Decay the Learning Rate, Increase the Batch Size" (ICLR 2018) · Kingma & Ba, "Adam" (ICLR 2015) e Loshchilov & Hutter, "Decoupled Weight Decay Regularization" (ICLR 2019), o artigo do AdamW · Wilson et al., "The Marginal Value of Adaptive Gradient Methods in Machine Learning" (NeurIPS 2017), que faz em escala o argumento da seção 3.2 · documentação do PyTorch, torch.optim.

↑ topo