← Redes Neurais Artificiais e Deep Learning · Insper/7. RegularizaçãoMaterial complementar

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

A mesma rede, os mesmos 300 exemplos.
Quase oito pontos de acurácia.

A arquitetura desta aula é uma só: 64 → 128 → 128 → 10, ReLU, entropia cruzada, 200 épocas. Ela não muda em nenhum experimento — e nem o otimizador, nem o learning rate, nem os dados. O que muda é o que impedimos a rede de fazer. Sem nada, ela acerta 100% do treino e 88,8% do teste. Com a mesma arquitetura e os mesmos 300 exemplos, ela chega a 96,4%.

300Exemplos de treino
30Com rótulo errado
109 kParâmetros
17Treinos medidos
8Checkpoints

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 aprenderComo isso aparece no treinoComo 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.
Regularização é toda técnica que torna a segunda linha cara o bastante para não valer a pena.
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.

image/svg+xml Matplotlib v3.10.9, https://matplotlib.org/
Vinte dos 300 exemplos de treino. Em 8 × 8 pixels, um 3 e um 8 diferem por meia dúzia de valores — e é por isso que decorar é tão mais fácil do que generalizar.
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:

image/svg+xml Matplotlib v3.10.9, https://matplotlib.org/
À esquerda, as perdas; à direita, as acurácias. A perda de treino chega a 2 × 10-4 — a rede acerta os 300 exemplos, inclusive os 30 com rótulo errado. A perda de validação atinge o mínimo na época 13 e depois sobe pelas 187 épocas restantes. A linha tracejada marca esse mínimo.
Na épocaPerda de treinoPerda de validaçãoAcurácia de treinoAcurácia de validação
13 (melhor validação)0,100,32197,7%89,5%
200 (fim do treino)0,00020,750100,0%86,0%
187 épocas a mais de computação, uma perda de treino 500 vezes menor — e um modelo pior. Esse é o quadro inteiro da aula em duas linhas.
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:

image/svg+xml Matplotlib v3.10.9, https://matplotlib.org/
A norma dos pesos (roxo, eixo à esquerda) sai de 529 e chega a 1 067 — dobra. A lacuna de acurácia entre treino e validação (laranja, eixo à direita) sai de zero e estaciona em 14 pontos. As duas curvas sobem juntas e no mesmo ritmo, e depois da época ~60 as duas param juntas.

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ÉpocasAcurácia no teste
Pesos do fim do treino20088,8%
Checkpoint da melhor perda de validação1390,0%
1,2 ponto de teste e 94% do tempo de treino, de graça. É a melhor relação custo-benefício da aula — e a única técnica que não tem hiperparâmetro para errar, além da paciência.
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:

image/svg+xml Matplotlib v3.10.9, https://matplotlib.org/
À esquerda, as duas acurácias ao fim das 200 épocas. A de treino (azul) cai de 100% a 81,7%; a de validação (vermelho) sobe até λ = 0,01 e depois desce — o U invertido previsto no checkpoint 3. À direita, a perda de validação ao longo do treino para três valores: sem penalidade ela sobe, com λ = 0,01 ela desce e fica.
λAcurácia de treinoAcurácia de validação‖W‖² finalTeste (última época)
0100,0%86,0%1 06788,8%
0,0001100,0%87,3%69989,2%
0,001100,0%86,3%32788,8%
0,00399,0%88,8%19788,6%
0,0190,7%89,2%9991,4%
0,0381,7%86,0%4585,2%
Em λ = 0,01 a rede deixa de acertar 100% do treino — e é justamente aí que ela fica boa. A norma caiu de 1 067 para 99: dez vezes menor, e a lacuna entre treino e validação praticamente sumiu (90,7% contra 89,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.

image/svg+xml Matplotlib v3.10.9, https://matplotlib.org/
A lacuna entre treino e validação fecha de 14 pontos (p = 0) para 6,6 pontos (p = 0,3). A partir de p = 0,4 as duas curvas despencam: em p = 0,5 a rede acerta 72,7% do próprio treino. Não é mais regularização, é falta de rede.
pTreinoValidaçãoLacunaTeste (última época)
0100,0%86,0%14,088,8%
0,297,3%87,8%9,589,2%
0,395,3%88,8%6,689,2%
0,487,3%78,2%9,176,4%
0,572,7%64,5%8,265,0%
O p = 0,5 do artigo original vale para as camadas densas de 4 096 unidades da AlexNet. Aqui as camadas têm 128, e treinam com 300 exemplos.

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)
image/svg+xml Matplotlib v3.10.9, https://matplotlib.org/
Um exemplo e quinze versões dele. Em 8 × 8, uma rotação de 12° e um deslocamento de 1 pixel já mudam boa parte dos 64 valores — é bastante variação para uma imagem tão pequena.

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 é:

image/svg+xml Matplotlib v3.10.9, https://matplotlib.org/
À esquerda, a probabilidade que cada modelo atribui à classe que escolheu, nos 500 exemplos de teste. Sem smoothing, 414 dos 500 recebem confiança acima de 0,96 — inclusive boa parte dos que estão errados. Com ε = 0,1 a distribuição se espalha. À direita, a curva de confiabilidade: acurácia observada contra confiança declarada, comparadas à diagonal da calibração perfeita.
ModeloAcurácia de testeConfiança médiaECE (erro de calibração)
Sem smoothing88,8%0,9580,070
ε = 0,189,6%0,7270,169
O modelo sem smoothing declara 95,8% de confiança e acerta 88,8%: excesso de confiança. O com smoothing declara 72,7% e acerta 89,6%: agora ele é pouco confiante — e o ECE, que mede a distância entre as duas colunas, piorou.
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:

image/svg+xml Matplotlib v3.10.9, https://matplotlib.org/
Em azul, os pesos da última época; em verde, o checkpoint de melhor validação daquele mesmo treino. Repare que a barra verde é maior em todas as linhas — e que a distância entre as duas é a medida de quanto cada configuração ainda estava decorando ao final.
ConfiguraçãoTreinoValidaçãoTeste (fim)Teste (melhor checkpoint)
Sem nada100,0%86,0%88,8%90,0%
L2, λ = 0,0190,7%89,2%91,4%94,2%
Dropout, p = 0,395,3%88,8%89,2%91,8%
Aumento de dados ×5100,0%88,5%89,2%89,6%
Label smoothing, ε = 0,1100,0%88,7%89,6%93,6%
Tudo junto91,0%90,5%91,8%96,4%
De 88,8% para 96,4%: o erro cai de 11,2% para 3,6%, ou seja, dois terços dos erros desapareceram — com a mesma arquitetura, os mesmos 300 exemplos e o mesmo orçamento de 200 épocas.
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:

  1. 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.
  2. 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.
  3. 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ê.
  4. 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.
  5. 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écnicaValor inicialLigue quandoFoi longe demais quando
Early stoppingpaciência ≈ 5–10% das épocasSempre. Não tem contraindicação.Nunca prejudica — mas para cedo demais se a paciência for pequena e a curva, ruidosa.
L2 / weight decay1e-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 dadosas invariâncias que você consegue nomearO 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.
Dropoutp = 0,1–0,3 em camadas densasHá 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,1Classificaçã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.
A ordem das linhas é a ordem de tentar. E antes de todas elas: consiga mais dados, se houver como.
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.

#PerguntaResposta em uma linha
1Por 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.
2O 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.
3Efeito de λ nas duas acurácias.Treino cai monotonicamente; validação faz um U invertido, com pico em λ = 0,01.
4Para 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.
5Por 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.
6Por 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.
7Quais 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.
8Quando 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.