Abertura · 8 min
O problema não é ajustar. É parar de ajustar.
Nas aulas anteriores o objetivo era sempre fazer a perda cair. Aqui ela vai cair até zero — e é exatamente aí que o modelo fica pior.
Uma rede com 109 mil parâmetros e 300 exemplos de treino tem capacidade de sobra para guardar a tabela: para cada imagem, o rótulo que veio junto. Isso zera a perda de treino sem aprender nada sobre dígitos. Para que a diferença fique impossível de ignorar, trocamos de propósito o rótulo de 30 dos 300 exemplos — 10% de ruído, uma taxa perfeitamente realista em dados anotados na pressa.
Agora existem duas coisas que a rede pode aprender, e elas competem pela mesma capacidade:
| O que a rede pode aprender | Como isso aparece no treino | Como isso aparece na validação |
|---|---|---|
| A forma dos dígitos — o que os 300 exemplos têm em comum com todos os outros dígitos do mundo. | A perda cai rápido nas primeiras épocas. | Cai junto. As duas curvas andam coladas. |
| Os 300 exemplos — inclusive os 30 rótulos errados, que só podem ser acertados na marra. | A perda continua caindo, até zero. | Para de cair e começa a subir. É a hora em que a rede passou a decorar. |
Ao final desta aula o aluno consegue
- Diagnosticar underfitting e overfitting olhando duas curvas, e dizer qual das duas linhas da tabela acima está acontecendo.
- Implementar L2, dropout invertido, early stopping com restore, aumento de dados e label smoothing — os cinco em cerca de 20 linhas de NumPy, sem framework.
- Escolher o valor de cada hiperparâmetro por busca na validação, e explicar por que o teste não participa dessa escolha.
- Reconhecer o ponto em que cada técnica passa de regularização a sabotagem — e cada uma tem esse ponto.
- Ler a diferença entre perda de validação e acurácia de validação quando as duas discordam. Nesta aula elas discordam.
Como esta aula funciona
- Em duplas, um notebook aberto. Todo bloco termina numa etapa para rodar ou num checkpoint para responder.
- Aposte antes de olhar. Oito vezes ao longo da página aparece uma caixa de checkpoint. Escreva a resposta antes de abrir a solução — errar com a resposta escrita ensina; concordar depois do fato, não.
- Todos os números desta página vêm de execuções reais, com semente fixa, no código que está no apêndice B. Rodar de novo dá exatamente os mesmos valores.
- Sem PyTorch. Tudo é NumPy: 40 linhas para a rede, uma linha para cada técnica. O objetivo é que nenhuma delas seja uma caixa-preta chamada por nome.
Bloco 1 · 12 min
Os dados, e a partição que decide tudo
Antes de qualquer técnica: três conjuntos, com três funções diferentes. Confundi-los invalida todo o resto da aula.
O digits do scikit-learn são 1 797 imagens de 8 × 8 pixels em tons de cinza, dez classes. É o MNIST reduzido ao osso — pequeno o bastante para 17 treinos completos caberem numa aula, grande o bastante para que overfitting seja um problema de verdade.
import numpy as np
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
digits = load_digits()
X, y = digits.data / 16.0, digits.target # pixels 0..16 -> 0..1
# teste primeiro, e ele sai da nossa frente até o fim da aula
X_pool, X_test, y_pool, y_test = train_test_split(
X, y, test_size=500, random_state=0, stratify=y)
# de propósito pequeno: 300 exemplos para 109 mil parâmetros
X_tr, X_rest, y_tr, y_rest = train_test_split(
X_pool, y_pool, train_size=300, random_state=0, stratify=y_pool)
X_val, y_val = X_rest[:400], y_rest[:400]
# 10% de ruído de rótulo: o que a rede vai decorar primeiro
rng = np.random.default_rng(7)
y_tr = y_tr.copy()
idx = rng.choice(len(y_tr), size=30, replace=False)
y_tr[idx] = (y_tr[idx] + rng.integers(1, 10, size=30)) % 10
print(X_tr.shape, X_val.shape, X_test.shape)
# (300, 64) (400, 64) (500, 64)
Três conjuntos, três funções
- Treino (300) — os pesos são ajustados aqui. Só aqui.
- Validação (400) — todas as escolhas são feitas aqui: λ, p, quando parar, qual técnica usar. Os pesos nunca veem esses dados, mas os hiperparâmetros sim — por isso a validação também acaba um pouco otimista.
- Teste (500) — a estimativa honesta, olhada uma vez, no fim. Se você escolher qualquer coisa olhando o teste, ele deixa de ser teste e vira uma segunda validação.
Checkpoint 1 · por que não bastam dois conjuntos?
Você vai comparar sete valores de λ. Suponha que existam só treino e teste, e que você escolha o λ com maior acurácia de teste. Qual é exatamente o problema — e por que ele piora quanto mais valores você testa?
resposta
Escolher olhando o teste é ajustar um hiperparâmetro nele. O melhor de sete valores não é o melhor em geral: é o que teve mais sorte com aquelas 500 imagens específicas. O número que você reporta deixa de ser uma estimativa do desempenho futuro e vira o máximo de sete estimativas ruidosas — enviesado para cima por construção. Quanto mais configurações você compara, maior o viés: com 7 valores ele é pequeno; com uma busca de 200 combinações, ele é grande o bastante para inverter a ordem entre dois modelos. Daí a validação existir: ela absorve esse viés, e o teste continua limpo.
Bloco 2 · 18 min
Decorando de propósito
Primeiro treino, sem nenhuma técnica. O objetivo aqui não é acertar: é produzir o problema, com números, para ter o que consertar depois.
A rede é escrita à mão, em NumPy, porque cada técnica desta aula é uma linha dentro dela — e é mais fácil confiar no que se viu do que no nome de um argumento. O código completo está no apêndice B; por ora, o essencial:
class MLP:
# 64 -> 128 -> 128 -> 10, ReLU, softmax
def forward(self, X, p_drop=0.0, rng=None):
a = X
for i, (W, b) in enumerate(zip(self.W, self.b)):
z = a @ W + b
if i < len(self.W) - 1:
a = np.maximum(z, 0) # ReLU
if p_drop > 0 and rng is not None: # só no treino
m = (rng.random(a.shape) >= p_drop) / (1 - p_drop)
a = a * m # dropout invertido
else:
a = softmax(z)
return a
def step(self, Xb, yb, lr, lam, p_drop, eps, rng, mom=0.9):
...
gW = cache[i].T @ d + lam * self.W[i] # <- a penalidade L2 mora aqui,
gb = d.sum(0) # e nunca toca o viés
Repare que toda a regularização desta aula cabe em quatro argumentos: lam (L2), p_drop (dropout), eps (label smoothing) e a decisão externa de quando parar. Nenhum deles muda a arquitetura.
Treinando com os quatro desligados, 200 épocas, mini-batches de 32, SGD com momentum 0,9 e learning rate 0,1:
| Na época | Perda de treino | Perda de validação | Acurácia de treino | Acurácia de validação |
|---|---|---|---|---|
| 13 (melhor validação) | 0,10 | 0,321 | 97,7% | 89,5% |
| 200 (fim do treino) | 0,0002 | 0,750 | 100,0% | 86,0% |
Cuidado: a perda de treino é a única curva que sempre melhora
Ela cai por construção — é ela que o otimizador minimiza. Uma perda de treino baixa não é evidência de nada, e uma perda de treino zero é evidência de que a rede tinha capacidade sobrando. Todo diagnóstico desta aula usa duas curvas; nenhum usa uma só.
Checkpoint 2 · o que a rede aprendeu entre a época 13 e a 200?
A acurácia de treino foi de 97,7% para 100,0%: sete exemplos a mais. Quais são, provavelmente, esses sete — e por que acertá-los piorou a validação em 3,5 pontos?
resposta
São os difíceis, e entre eles os 30 de rótulo trocado — os únicos que não podem ser acertados por nenhuma regra geral sobre dígitos, só decorados um a um. Para acertar um 7 rotulado como 2, a rede precisa criar uma exceção estreita em torno daquele ponto: um pedaço de fronteira que serve para uma imagem e atrapalha todas as parecidas. Cada exceção dessas custa um pouco de acurácia em todos os 7 reais da validação. Foi essa a troca feita nas 187 épocas finais: 2,3 pontos de treino comprados a 3,5 pontos de validação.
Bloco 3 · 10 min
A assinatura nos pesos
Decorar não é de graça: exige pesos grandes. E isso é mensurável enquanto o treino acontece.
Aquelas exceções estreitas em torno de pontos isolados só existem se a rede puder mudar de opinião muito rápido dentro de um espaço muito pequeno — e a inclinação de uma camada linear é o tamanho dos seus pesos. Medindo ‖W‖² época a época, ao lado da lacuna entre treino e validação:
Essa correlação é o motivo de existir a família inteira de técnicas do próximo bloco. Se decorar exige norma grande, cobrar pela norma encarece decorar — sem proibir nada, sem tirar um parâmetro sequer da rede.
A correlação não é uma lei
Redes profundas generalizam bem com normas enormes o tempo todo; medir ‖W‖² não diz se um modelo é bom. O que vale aqui é a versão condicional: para esta arquitetura, nestes dados, ao longo deste treino, o crescimento da norma acompanha o crescimento da lacuna. É um sintoma útil, não um diagnóstico.
Checkpoint 3 · uma previsão, antes do próximo bloco
Vamos adicionar λ‖W‖² à perda e varrer λ de 0 a 0,03. Escreva agora, antes de ver o gráfico: o que acontece com a acurácia de treino conforme λ cresce? E com a de validação?
resposta
A de treino cai monotonicamente: a penalidade só atrapalha quem quer decorar, e a capacidade efetiva encolhe conforme λ cresce (100% → 90,7% em λ = 0,01 → 81,7% em λ = 0,03). A de validação faz uma curva em U invertido: sobe enquanto a penalidade está cortando exceções (86,0% → 89,2%) e desce quando ela começa a cortar também a forma dos dígitos (86,0% em λ = 0,03). O ponto alto dessa curva é o λ que você quer, e ele não tem como ser calculado — só medido.
Bloco 4 · 12 min
Early stopping: a técnica que já estava paga
Comecemos pela mais barata. Ela não muda a perda, não muda a rede e não custa uma linha de treino a mais — só a disciplina de guardar uma cópia.
O gráfico do bloco 2 já contém o resultado: o melhor modelo daquele treino aconteceu na época 13 e foi jogado fora. Guardá-lo é isto:
melhor = (np.inf, 0, None)
for e in range(epochs):
treina_uma_epoca(...)
vll, vla = net.loss_acc(X_val, y_val)
if vll < melhor[0]:
melhor = (vll, e, copia_dos_pesos(net)) # <- a parte que todo mundo esquece
if e - melhor[1] > paciencia: # nada melhorou há `paciencia` épocas
break
net.W, net.b = melhor[2] # restaura. sem isto, parar não adianta
| Modelo | Épocas | Acurácia no teste |
|---|---|---|
| Pesos do fim do treino | 200 | 88,8% |
| Checkpoint da melhor perda de validação | 13 | 90,0% |
A perda de validação e a acurácia de validação discordam — e isso é normal
Nesta execução, a perda de validação tem mínimo na época 13, mas a acurácia de validação continua flutuando em torno de 86–89% até a época 50. Não é contradição: a entropia cruzada pune confiança errada, e a acurácia não. Uma rede que erra os mesmos exemplos, porém com muito mais convicção, piora a perda sem mudar a acurácia. Escolha o critério pelo que você vai fazer com o modelo: probabilidade calibrada → pare pela perda; decisão dura → pare pela acurácia (ou pela métrica do problema: F1, AUC, recall).
Checkpoint 4 · escolhendo a paciência
Com paciência 5, este treino teria parado por volta da época 18. Com paciência 100, por volta da 113. Nos dois casos o modelo restaurado é o mesmo — o da época 13. Então para que serve escolher bem a paciência?
resposta
Para duas coisas, e nenhuma delas é a qualidade deste modelo. Custo: paciência 100 gasta 100 épocas para descobrir o que a paciência 5 descobre em 5. Risco de parar cedo demais: a curva de validação é ruidosa, e uma paciência pequena confunde uma subida de três épocas com o fim do treino — em curvas com platôs (comuns com learning rate decrescente) isso custa acurácia de verdade. A regra prática: paciência da ordem de 5–10% do total de épocas, e sempre com restore, porque é o restore que protege você do erro nos dois sentidos.
Bloco 5 · 20 min
L2: cobrando pela norma
A técnica do bloco 3, agora medida. Uma linha no gradiente, sete treinos, uma curva em U invertido.
A perda passa a ser \( J(W) + \tfrac{\lambda}{2}\Vert W\Vert^2 \), o que no gradiente é exatamente o + lam * self.W[i] que você já viu. Sete valores de λ, tudo o mais idêntico:
| λ | Acurácia de treino | Acurácia de validação | ‖W‖² final | Teste (última época) |
|---|---|---|---|---|
| 0 | 100,0% | 86,0% | 1 067 | 88,8% |
| 0,0001 | 100,0% | 87,3% | 699 | 89,2% |
| 0,001 | 100,0% | 86,3% | 327 | 88,8% |
| 0,003 | 99,0% | 88,8% | 197 | 88,6% |
| 0,01 | 90,7% | 89,2% | 99 | 91,4% |
| 0,03 | 81,7% | 86,0% | 45 | 85,2% |
O que penalizar, e o que deixar em paz
No código, a penalidade entra em gW e não em gb: vieses não são penalizados. Eles deslocam, não escalam — encolher um viés não simplifica a função, só desloca a saída para longe de onde ela deveria estar. A mesma regra vale para os parâmetros γ e β das camadas de normalização. Em PyTorch isso não é automático: weight_decay passado ao otimizador atinge todos os parâmetros, e separar exige montar dois grupos de parâmetros na mão.
Checkpoint 5 · a linha de λ = 0,001
Com λ = 0,001, a norma final caiu para menos de um terço (1 067 → 327) e a acurácia de validação praticamente não mudou (86,0% → 86,3%). Se a norma grande é a assinatura de decorar, por que cortá-la em dois terços não resolveu nada?
resposta
Porque a acurácia de treino continuou em 100%: mesmo com pesos três vezes menores, a rede ainda conseguiu pagar as 30 exceções. A norma é sintoma, não mecanismo — o que decide é se a rede ainda consegue decorar sob aquela pressão. Repare que a virada na tabela acontece exatamente na linha em que a acurácia de treino sai de 100% (λ = 0,003, 99,0%) e se completa em λ = 0,01 (90,7%). O critério prático que sobra disto: aumente λ até a acurácia de treino começar a ceder, e escolha na validação entre os poucos valores em torno desse ponto.
Bloco 6 · 18 min
Dropout: apagando metade da rede a cada passo
Outro mecanismo, outro efeito colateral. E, nesta rede pequena, um limite que aparece muito antes do esperado.
A implementação inteira é a linha que você já viu no bloco 2:
m = (rng.random(a.shape) >= p_drop) / (1 - p_drop) a = a * m
Duas coisas acontecem aí. A máscara m zera cada ativação com probabilidade p_drop; a divisão por 1 - p_drop devolve a média ao valor que ela tinha — é o dropout invertido, e é por isso que na inferência não se faz nada: basta não chamar a linha. Se você esquecer a divisão, a rede treina com ativações menores do que as que verá na inferência, e a inferência sai errada sem nenhum erro aparecer.
| p | Treino | Validação | Lacuna | Teste (última época) |
|---|---|---|---|---|
| 0 | 100,0% | 86,0% | 14,0 | 88,8% |
| 0,2 | 97,3% | 87,8% | 9,5 | 89,2% |
| 0,3 | 95,3% | 88,8% | 6,6 | 89,2% |
| 0,4 | 87,3% | 78,2% | 9,1 | 76,4% |
| 0,5 | 72,7% | 64,5% | 8,2 | 65,0% |
Checkpoint 6 · por que p = 0,5 destrói esta rede?
Em p = 0,5 até a acurácia de treino desaba para 72,7% — a rede não consegue nem decorar. O que exatamente falta aqui que não faltava na AlexNet?
resposta
Redundância. Com 128 unidades e p = 0,5, cada passo treina uma sub-rede com ~64 unidades por camada, e cada uma delas precisa dar conta do problema quase sozinha — sobre 300 exemplos vistos em versões diferentes a cada passo, o sinal que chega a cada unidade fica ruidoso demais para convergir em 200 épocas. Com 4 096 unidades, metade ainda é uma rede grande, e cada unidade participa de milhares de sub-redes ao longo do treino. A regra prática: p alto exige largura alta; em camadas estreitas, p entre 0,1 e 0,3, ou nenhum dropout. E, se você observar a acurácia de treino caindo junto, a técnica virou limitação de capacidade, não regularização.
Bloco 7 · 15 min
Aumento de dados: fabricando exemplos
As técnicas anteriores restringem o modelo. Esta ataca a causa — 300 exemplos — e a única coisa que ela precisa de você é conhecimento sobre o problema.
Um 3 girado 10° continua sendo um 3, e um 3 deslocado um pixel para a direita também. Cada afirmação dessas é um exemplo novo de graça:
from scipy.ndimage import rotate, shift
def augment(X, y, rng, factor=4):
imgs, Xs, ys = X.reshape(-1, 8, 8), [X], [y]
for _ in range(factor):
out = np.empty_like(imgs)
for i, im in enumerate(imgs):
r = rng.uniform(-12, 12) # graus
dx, dy = rng.uniform(-1, 1, 2) # pixels
out[i] = shift(rotate(im, r, reshape=False, order=1, mode="constant"),
(dy, dx), order=1, mode="constant")
Xs.append(np.clip(out.reshape(len(X), -1), 0, 1)); ys.append(y)
return np.concatenate(Xs), np.concatenate(ys)
De 300 exemplos passamos a 1 500. O resultado: acurácia de validação de 86,0% para 88,5%, teste de 88,8% para 89,2% — e a rede ainda chega a 100% de acurácia de treino, porque decorar 1 500 imagens continua ao alcance de 109 mil parâmetros.
Por que o ganho foi menor do que se esperava?
Porque as versões novas não são exemplos independentes: são funções dos mesmos 300. Elas ensinam a invariância (giro, translação) e nada mais — e, crucialmente, os 30 rótulos errados foram replicados cinco vezes cada. Aumento de dados multiplica o que você tem, inclusive os erros. Contra ruído de rótulo, ele ajuda pouco; contra falta de variedade, ele é a técnica mais forte da aula.
Checkpoint 7 · quais transformações são válidas aqui?
Marque as que você usaria para dígitos e justifique as recusas: (a) espelhamento horizontal, (b) rotação de 90°, (c) rotação de ±15°, (d) recorte aleatório de 6 × 6, (e) ruído gaussiano leve, (f) inversão de cor (fundo preto ↔ branco).
resposta
Válidas: (c) rotações pequenas — a mão de cada pessoa inclina de um jeito; (e) ruído leve — simula variação de digitalização; (d) recorte, com cuidado: em 8 × 8 tirar 2 pixels pode decapitar o dígito, então só com preenchimento.
Recusadas: (a) espelhar transforma 2 em algo que não é 2 e aproxima 2 de 5 — ensina uma invariância falsa; (b) 90° troca 6 por 9 e 2 por um símbolo inexistente; (f) inverter cor só valeria se o conjunto de teste também tivesse fundo invertido — do contrário você treina para um domínio que não vai encontrar.
A regra geral: uma transformação é válida quando ela preserva o rótulo em todas as classes. Basta uma classe em que ela mente para a técnica virar ruído de rótulo disfarçado — e você já viu, nos blocos 2 e 3, o preço disso.
Bloco 8 · 15 min
Label smoothing, e a confiança que ninguém mede
A última técnica não mexe no modelo nem nos dados: mexe no alvo. E ela expõe uma métrica que a acurácia esconde.
Com alvo one-hot, a entropia cruzada só é minimizada quando o logit correto vai a infinito: a perda nunca fica satisfeita. Com ε = 0,1 e dez classes, o alvo passa a valer 0,91 na classe certa e 0,01 nas outras nove — e o ótimo passa a ser uma diferença de logits finita, que a rede consegue alcançar e parar. São duas linhas:
T = np.full((n, 10), eps / 10) T[np.arange(n), yb] += 1 - eps # em vez do one-hot puro
O efeito na acurácia é modesto: 88,8% → 89,6% no teste. O efeito na confiança não é:
| Modelo | Acurácia de teste | Confiança média | ECE (erro de calibração) |
|---|---|---|---|
| Sem smoothing | 88,8% | 0,958 | 0,070 |
| ε = 0,1 | 89,6% | 0,727 | 0,169 |
Um resultado honesto que contraria o slogan
Diz-se que label smoothing melhora a calibração, e em geral melhora. Aqui piorou o ECE, e o motivo está na própria definição: com ε = 0,1 e 10 classes, a probabilidade máxima que o alvo pede é 0,91 — o modelo é treinado para nunca passar de perto disso, e sobre 300 exemplos ele fica bem abaixo. Trocar excesso de confiança por falta de confiança é uma troca, não uma correção. O que fazer: escolher ε na validação como qualquer hiperparâmetro (0,05 costuma bastar com poucas classes) e, quando a probabilidade calibrada importa de verdade, ajustar uma temperatura na validação depois do treino — uma escalar, um minuto de ajuste, e é a técnica que realmente conserta calibração.
Checkpoint 8 · quando 0,96 de confiança é pior do que estar errado
Os dois modelos erram cerca de 55 dos 500 exemplos de teste. Descreva uma situação de uso em que o modelo sem smoothing é claramente o pior dos dois, apesar dos 0,8 ponto de diferença na acurácia.
resposta
Qualquer uma em que a saída não seja a decisão final, e sim uma entrada para outra coisa. Três exemplos concretos: (1) triagem com revisão humana — você quer mandar para uma pessoa os casos de baixa confiança, e um modelo que declara 0,96 em quase tudo não separa nada, então ou tudo vai para revisão ou nada vai; (2) composição com outros sinais — se a probabilidade entra numa regra de negócio ou num ensemble, um número inflado desequilibra a combinação; (3) detecção de dados fora da distribuição — um modelo que responde com 0,96 a uma imagem que não é dígito nenhum não dá como ser filtrado por limiar. Nos três casos o que importa não é o argmax, é a probabilidade — e ela está errada nos dois modelos, em direções opostas.
Bloco 9 · 12 min
Tudo junto
Cinco técnicas, cada uma valendo entre 0,4 e 2,6 pontos sozinha. Empilhadas, elas não somam — fazem melhor do que somar.
A configuração final usa λ = 0,001, p = 0,2, aumento de dados ×5, ε = 0,1 e early stopping com restore — cada valor escolhido na validação, nenhum no teste. O teste entra agora, uma vez:
| Configuração | Treino | Validação | Teste (fim) | Teste (melhor checkpoint) |
|---|---|---|---|---|
| Sem nada | 100,0% | 86,0% | 88,8% | 90,0% |
| L2, λ = 0,01 | 90,7% | 89,2% | 91,4% | 94,2% |
| Dropout, p = 0,3 | 95,3% | 88,8% | 89,2% | 91,8% |
| Aumento de dados ×5 | 100,0% | 88,5% | 89,2% | 89,6% |
| Label smoothing, ε = 0,1 | 100,0% | 88,7% | 89,6% | 93,6% |
| Tudo junto | 91,0% | 90,5% | 91,8% | 96,4% |
Sobre olhar o teste seis vezes nesta tabela
Fizemos exatamente o que o bloco 1 proíbe — e de propósito, porque o objetivo aqui é comparar técnicas para aprender, não escolher um modelo. Num projeto de verdade a tabela teria uma coluna a menos: você escolhe pela validação, e o teste aparece uma vez, só na linha escolhida. Repare, aliás, que a escolha pela validação (tudo junto, 90,5%) coincide com a melhor linha de teste — o que é um bom sinal, e não uma garantia.
Bloco 10 · 20 min · em duplas
Sua própria busca
A parte da aula em que não há resposta impressa.
Com o código do apêndice B rodando, o trabalho é este:
- Reproduza a linha de base. Rode a configuração sem nada e confirme os números do bloco 2 (100,0% / 86,0%, melhor época 13). Se não bater, a semente ou a partição estão diferentes — resolva isso antes de continuar.
- Encontre o seu λ. Varra λ ∈ {0,003, 0,006, 0,01, 0,02} com early stopping ligado, e não sem, como fizemos no bloco 5. A escolha muda? Anote qual λ vence e por quanto.
- Combine dois. Escolha L2 + dropout e faça uma grade 3 × 3 com os valores em torno dos vencedores individuais. Você vai encontrar o que a literatura chama de interação: o melhor λ na presença de dropout não é o melhor λ sozinho. Explique, em duas linhas, por quê.
- Quebre alguma coisa. Escolha uma das técnicas e ache o valor a partir do qual ela passa a prejudicar. Registre o número. Toda técnica desta aula tem esse valor, e saber onde ele fica vale mais do que decorar o valor recomendado.
- Uma única medida no teste. Escolha uma configuração final pela validação, meça no teste, e escreva a frase que você colocaria num relatório — com o número, o tamanho do conjunto de teste e o que você fez para que ele seja honesto.
Para entregar
Uma página: a grade que você mediu (tabela ou heatmap), a configuração escolhida com a justificativa pela validação, o número final de teste e — a parte que mais conta — o valor em que sua técnica escolhida começou a prejudicar, com o gráfico que mostra isso.
Apêndice A
Tabela de decisão
O que ligar, quando, e o que observar para saber se foi longe demais.
| Técnica | Valor inicial | Ligue quando | Foi longe demais quando |
|---|---|---|---|
| Early stopping | paciência ≈ 5–10% das épocas | Sempre. Não tem contraindicação. | Nunca prejudica — mas para cedo demais se a paciência for pequena e a curva, ruidosa. |
| L2 / weight decay | 1e-4 (SGD) 0,01–0,1 (AdamW) | Quase sempre. É a primeira a ligar depois do early stopping. | A acurácia de treino cai bem abaixo do que a tarefa permite. Suba λ até o treino começar a ceder, e escolha na validação em torno desse ponto. |
| Aumento de dados | as invariâncias que você consegue nomear | O domínio tem invariâncias reais (imagem, áudio). É a técnica mais forte depois de conseguir dados de verdade. | Uma transformação mente para alguma classe. O sintoma é acurácia de treino caindo sem a validação subir. |
| Dropout | p = 0,1–0,3 em camadas densas | Há camadas densas largas e capacidade de sobra. | A acurácia de treino desaba junto com a de validação (aqui, p ≥ 0,4). Camada estreita não sustenta p alto. |
| Label smoothing | ε = 0,05–0,1 | Classificação com muitas classes, ou rótulos com ruído. | O modelo fica sistematicamente pouco confiante (confiança média bem abaixo da acurácia). |
| Menos capacidade | — | Por último, e raramente. | Sempre que você a usou antes de tentar as cinco de cima. |
Antes de qualquer uma delas: faça a rede decorar
Se o seu modelo não consegue chegar perto de 100% num subconjunto pequeno do treino, você não tem um problema de regularização — tem um bug. Rótulos desalinhados, normalização faltando, learning rate errado, um sinal trocado na perda. Nenhuma técnica desta página conserta isso, e todas escondem o sintoma.
Apêndice B
Código completo
A rede, as cinco técnicas e o loop de treino. NumPy e scikit-learn, nada mais.
Este é o arquivo que gerou todos os números e todas as figuras desta página. No repositório do curso ele está em scripts/handouts/regularizacao_digits.py; abaixo, a parte que interessa para a aula.
class MLP:
"""64 -> 128 -> 128 -> 10, ReLU, softmax. Tudo explícito, para poder ver cada peça."""
def __init__(self, sizes=(64, 128, 128, 10), seed=0):
rng = np.random.default_rng(seed)
self.W = [rng.normal(0, np.sqrt(2 / a), (a, b)) for a, b in zip(sizes, sizes[1:])]
self.b = [np.zeros(b) for b in sizes[1:]]
self.vW = [np.zeros_like(w) for w in self.W]
self.vb = [np.zeros_like(b) for b in self.b]
def forward(self, X, p_drop=0.0, rng=None):
a, cache = X, [X]
masks = []
for i, (W, b) in enumerate(zip(self.W, self.b)):
z = a @ W + b
if i < len(self.W) - 1:
a = np.maximum(z, 0)
if p_drop > 0 and rng is not None:
m = (rng.random(a.shape) >= p_drop) / (1 - p_drop) # dropout invertido
a = a * m
masks.append(m)
else:
masks.append(None)
else:
z = z - z.max(axis=1, keepdims=True)
a = np.exp(z); a /= a.sum(axis=1, keepdims=True)
cache.append(a)
return cache, masks
def loss_acc(self, X, y, eps=0.0):
cache, _ = self.forward(X)
P = cache[-1]
T = np.full((len(y), 10), eps / 10)
T[np.arange(len(y)), y] += 1 - eps
loss = -(T * np.log(P + 1e-12)).sum(axis=1).mean()
return loss, float((P.argmax(1) == y).mean())
def step(self, Xb, yb, lr, lam, p_drop, eps, rng, mom=0.9):
cache, masks = self.forward(Xb, p_drop, rng)
P, n = cache[-1], len(yb)
T = np.full((n, 10), eps / 10)
T[np.arange(n), yb] += 1 - eps
d = (P - T) / n
for i in reversed(range(len(self.W))):
gW = cache[i].T @ d + lam * self.W[i] # a penalidade nunca toca os vieses
gb = d.sum(0)
if i > 0:
d = d @ self.W[i].T
if masks[i - 1] is not None:
d *= masks[i - 1]
d = d * (cache[i] > 0)
self.vW[i] = mom * self.vW[i] - lr * gW
self.vb[i] = mom * self.vb[i] - lr * gb
self.W[i] += self.vW[i]
self.b[i] += self.vb[i]
def wnorm(self):
return float(sum((W ** 2).sum() for W in self.W))
def augment(X, y, rng, factor=4):
"""Deslocamentos de ate 1px e rotacoes de ate 12 graus — invariancias reais de digitos."""
imgs = X.reshape(-1, 8, 8)
Xs, ys = [X], [y]
for _ in range(factor):
out = np.empty_like(imgs)
for i, im in enumerate(imgs):
r = rng.uniform(-12, 12)
dx, dy = rng.uniform(-1, 1, 2)
out[i] = shift(rotate(im, r, reshape=False, order=1, mode="constant"), (dy, dx),
order=1, mode="constant")
Xs.append(np.clip(out.reshape(len(X), -1), 0, 1)); ys.append(y)
return np.concatenate(Xs), np.concatenate(ys)
def train(lam=0.0, p_drop=0.0, eps=0.0, aug=False, epochs=200, lr=0.1, batch=32, seed=0):
rng = np.random.default_rng(seed)
Xt, yt = (augment(X_tr, y_tr, rng) if aug else (X_tr, y_tr))
net = MLP(seed=seed)
hist = []
best = (1e9, 0, None)
for e in range(epochs + 1):
if e:
idx = rng.permutation(len(Xt))
for k in range(0, len(Xt), batch):
j = idx[k:k + batch]
net.step(Xt[j], yt[j], lr, lam, p_drop, eps, rng)
trl, tra = net.loss_acc(X_tr, y_tr)
vll, vla = net.loss_acc(X_val, y_val)
hist.append((e, trl, tra, vll, vla, net.wnorm()))
if vll < best[0]:
best = (vll, e, ([W.copy() for W in net.W], [b.copy() for b in net.b]))
tel, tea = net.loss_acc(X_test, y_test)
net_best = MLP(seed=seed); net_best.W, net_best.b = best[2]
bl, ba = net_best.loss_acc(X_test, y_test)
return dict(hist=np.array(hist), final_test=tea, best_epoch=best[1], best_test=ba,
net=net, net_best=net_best)
O mesmo, em PyTorch
Cada peça acima tem um equivalente de uma linha:
net = nn.Sequential(nn.Linear(64,128), nn.ReLU(), nn.Dropout(0.2),
nn.Linear(128,128), nn.ReLU(), nn.Dropout(0.2),
nn.Linear(128,10))
otim = torch.optim.SGD(net.parameters(), lr=0.1, momentum=0.9,
weight_decay=1e-2) # L2 (em Adam, use AdamW)
criterio = nn.CrossEntropyLoss(label_smoothing=0.1) # label smoothing
net.train() / net.eval() # liga e desliga o dropout
Duas diferenças importantes: weight_decay do PyTorch penaliza todos os parâmetros, vieses inclusive (separar exige dois grupos de parâmetros), e net.eval() é obrigatório antes de qualquer avaliação — esquecer isso é o bug mais comum, e ele não levanta erro nenhum.
Apêndice C
Respostas dos checkpoints
As oito, reunidas — para conferir depois da aula, não durante.
| # | Pergunta | Resposta em uma linha |
|---|---|---|
| 1 | Por que não bastam dois conjuntos? | Escolher pelo teste transforma o teste no máximo de várias estimativas ruidosas; o viés cresce com o número de configurações comparadas. |
| 2 | O que a rede aprendeu entre as épocas 13 e 200? | Os 30 rótulos trocados, um a um, com exceções estreitas que custam acurácia em todos os exemplos parecidos. |
| 3 | Efeito de λ nas duas acurácias. | Treino cai monotonicamente; validação faz um U invertido, com pico em λ = 0,01. |
| 4 | Para que serve escolher bem a paciência? | Para custo e para não parar em cima de ruído — o modelo restaurado é o mesmo, mas o preço e o risco não. |
| 5 | Por que λ = 0,001 não resolveu? | Porque a acurácia de treino continuou em 100%: a rede ainda conseguia pagar pelas exceções. O critério é o treino ceder, não a norma cair. |
| 6 | Por que p = 0,5 destrói esta rede? | Falta redundância: 128 unidades pela metade não sustentam o problema. p alto exige camada larga. |
| 7 | Quais transformações valem para dígitos? | Rotação pequena, ruído leve, recorte com cuidado. Espelhar, girar 90° e inverter cor mentem para alguma classe. |
| 8 | Quando 0,96 de confiança é pior? | Sempre que a probabilidade for usada: triagem por limiar, combinação com outros sinais, detecção de fora-da-distribuição. |
Este handout acompanha a página 7. Regularização, que traz os nove simuladores interativos — inclusive um em que você empilha estas mesmas técnicas e vê a fronteira de decisão mudar em tempo real.