Phase 03 - Lesson 07
Regularização
This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.
Seu modelo atinge 99% nos dados de treino e 60% nos dados de teste. Ele memorizou em vez de aprender. Regularização é o imposto que você cobra sobre a complexidade para forçar a generalização.
Tipo: Build Linguagens: Python Pré-requisitos: Lição 03.06 (Otimizadores) Tempo: ~75 minutos
Objetivos de Aprendizagem
- Implementar dropout com escalonamento invertido, weight decay L2, batch normalization, layer normalization e RMSNorm do zero
- Medir a diferença de acurácia entre treino e teste e diagnosticar overfitting usando experimentos de regularização
- Explicar por que transformers usam LayerNorm em vez de BatchNorm e por que LLMs modernos preferem RMSNorm
- Aplicar a combinação correta de técnicas de regularização com base na severidade do overfitting
O Problema
Uma rede neural com parâmetros suficientes pode memorizar qualquer dataset. Isso não é hipotético -- Zhang et al. (2017) provaram isso treinando redes padrão na ImageNet com rótulos aleatórios. As redes atingiram loss de treino próxima de zero em atribuições de rótulos completamente aleatórias. Elas memorizaram um milhão de pares entrada-saída aleatórios sem nenhum padrão a aprender. A loss de treino era perfeita. A acurácia de teste era zero.
Esse é o problema do overfitting, e ele piora à medida que os modelos crescem. O GPT-3 tem 175 bilhões de parâmetros. O conjunto de treino tem cerca de 500 bilhões de tokens. Com tantos parâmetros, o modelo tem capacidade suficiente para memorizar partes significativas dos dados de treino literalmente. Sem regularização, ele simplesmente regurgitaria exemplos de treino em vez de aprender padrões generalizáveis.
A diferença entre o desempenho de treino e o desempenho de teste é o gap de overfitting. Cada técnica nesta lição ataca esse gap de um ângulo diferente. O dropout força a rede a não depender de nenhum neurônio isolado. O weight decay impede que qualquer peso individual cresça demais. A batch normalization suaviza o panorama da loss para que o otimizador encontre mínimos mais planos e generalizáveis. A layer normalization faz a mesma coisa, mas funciona onde a batch normalization falha (batches pequenos, sequências de comprimento variável). A RMSNorm faz isso 10% mais rápido ao descartar o cálculo da média. Cada técnica é simples. Juntas, elas são a diferença entre um modelo que memoriza e um que generaliza.
O Conceito
O Espectro do Overfitting
Todo modelo fica em algum ponto de um espectro que vai do underfitting (simples demais para capturar o padrão) ao overfitting (tão complexo que captura ruído). O ponto ideal está no meio, e a regularização empurra os modelos em sua direção a partir do lado do overfit.
graph LR
Under["Underfitting<br/>Treino: 60%<br/>Teste: 58%<br/>Modelo simples demais"] --> Good["Bom Ajuste<br/>Treino: 95%<br/>Teste: 92%<br/>Generaliza bem"]
Good --> Over["Overfitting<br/>Treino: 99.9%<br/>Teste: 65%<br/>Memorizou ruído"]
Dropout["Dropout"] -->|"Empurra para a esquerda"| Over
WD["Weight Decay"] -->|"Empurra para a esquerda"| Over
BN["BatchNorm"] -->|"Empurra para a esquerda"| Over
Aug["Data Augmentation"] -->|"Empurra para a esquerda"| Over
Dropout
A técnica de regularização mais simples, com a interpretação mais elegante. Durante o treino, zere aleatoriamente a saída de cada neurônio com probabilidade p.
output = activation(z) * mask where mask[i] ~ Bernoulli(1 - p)
Com p = 0.5, metade dos neurônios é zerada a cada passagem forward. A rede precisa aprender representações redundantes porque não consegue prever quais neurônios estarão disponíveis. Isso evita a co-adaptação -- neurônios aprendendo a depender da presença de outros neurônios específicos.
A interpretação como ensemble: uma rede com N neurônios e dropout cria 2^N possíveis sub-redes (toda combinação de quais neurônios estão ligados ou desligados). Treinar com dropout treina aproximadamente todas as 2^N sub-redes simultaneamente, cada uma em mini-batches diferentes. No momento do teste, você usa todos os neurônios (sem dropout) e escala as saídas por (1 - p) para corresponder ao valor esperado durante o treino. Isso equivale a fazer a média das previsões de 2^N sub-redes -- um ensemble enorme a partir de um único modelo.
Na prática, o escalonamento é aplicado durante o treino em vez do teste (dropout invertido):
During training: output = activation(z) * mask / (1 - p)
During testing: output = activation(z) (no change needed)
Isso é mais limpo porque o código de teste não precisa saber nada sobre dropout.
Taxas padrão: p = 0.1 para transformers, p = 0.5 para MLPs, p = 0.2-0.3 para CNNs. Dropout mais alto = regularização mais forte = mais risco de underfitting.
Weight Decay (Regularização L2)
Adicione a magnitude ao quadrado de todos os pesos à loss:
total_loss = task_loss + (lambda / 2) * sum(w_i^2)
O gradiente do termo de regularização é lambda * w. Isso significa que, a cada passo, cada peso é encolhido em direção a zero por uma fração proporcional à sua magnitude. Pesos grandes são penalizados mais. O modelo é empurrado em direção a soluções onde nenhum peso isolado domina.
Por que isso ajuda na generalização: modelos com overfit tendem a ter pesos grandes que amplificam o ruído nos dados de treino. O weight decay mantém os pesos pequenos, o que limita a capacidade efetiva do modelo e o força a depender de features robustas e generalizáveis em vez de peculiaridades memorizadas.
O hiperparâmetro lambda controla a intensidade. Valores típicos:
- 0.01 para AdamW em transformers
- 1e-4 para SGD em CNNs
- 0.1 para modelos com overfit severo
Como discutido na lição 06: weight decay e regularização L2 são equivalentes no SGD, mas não no Adam. Sempre use o AdamW (weight decay desacoplado) ao treinar com Adam.
Batch Normalization
Normalize a saída de cada camada ao longo do mini-batch antes de passá-la para a próxima camada.
Para um mini-batch de ativações em alguma camada:
mu = (1/B) * sum(x_i) (batch mean)
sigma^2 = (1/B) * sum((x_i - mu)^2) (batch variance)
x_hat = (x_i - mu) / sqrt(sigma^2 + eps) (normalize)
y = gamma * x_hat + beta (scale and shift)
Gamma e beta são parâmetros aprendíveis que permitem à rede desfazer a normalização caso isso seja ótimo. Sem eles, você estaria forçando a saída de cada camada a ter média zero e variância unitária, o que pode não ser o que a rede quer.
Divisão entre treino e inferência: Durante o treino, mu e sigma vêm do mini-batch atual. Durante a inferência, você usa médias móveis acumuladas durante o treino (média móvel exponencial com momentum = 0.1, o que significa 90% do antigo + 10% do novo).
Por que a BatchNorm funciona ainda é debatido. O artigo original afirmava que ela reduz o "internal covariate shift" (a distribuição das entradas das camadas mudando à medida que as camadas anteriores se atualizam). Santurkar et al. (2018) mostraram que essa explicação está errada. O motivo real: a BatchNorm torna o panorama da loss mais suave. Os gradientes são mais preditivos, as constantes de Lipschitz são menores e o otimizador pode dar passos maiores com segurança. É por isso que a BatchNorm permite usar taxas de aprendizado mais altas e convergir mais rápido.
A BatchNorm tem uma limitação fundamental: ela depende das estatísticas do batch. Com batch size 1, a média e a variância não fazem sentido. Com batches pequenos (< 32), as estatísticas são ruidosas e prejudicam o desempenho. Isso importa para tarefas como detecção de objetos (onde os limites de memória restringem o tamanho do batch) e modelagem de linguagem (onde os comprimentos das sequências variam).
Layer Normalization
Normalize ao longo das features em vez de ao longo do batch. Para uma única amostra:
mu = (1/D) * sum(x_j) (feature mean)
sigma^2 = (1/D) * sum((x_j - mu)^2) (feature variance)
x_hat = (x_j - mu) / sqrt(sigma^2 + eps)
y = gamma * x_hat + beta
D é a dimensão das features. Cada amostra é normalizada de forma independente -- sem dependência do tamanho do batch. É por isso que transformers usam LayerNorm em vez de BatchNorm. As sequências têm comprimentos variáveis, os tamanhos de batch costumam ser pequenos (ou 1 durante a geração) e o cálculo é idêntico entre treino e inferência.
A LayerNorm em transformers é aplicada após cada bloco de self-attention e cada bloco feed-forward (Post-LN), ou antes deles (Pre-LN, que é mais estável para o treino).
RMSNorm
LayerNorm sem a subtração da média. Proposta por Zhang & Sennrich (2019).
rms = sqrt((1/D) * sum(x_j^2))
y = gamma * x / rms
É isso. Sem cálculo de média, sem parâmetro beta. A observação: a recentralização (subtração da média) na LayerNorm contribui muito pouco para o desempenho do modelo, mas custa computação. Removê-la dá a mesma acurácia com cerca de 10% menos de overhead.
LLaMA, LLaMA 2, LLaMA 3, Mistral e a maioria dos LLMs modernos usam RMSNorm em vez de LayerNorm. Na escala de bilhões de parâmetros e trilhões de tokens, essa economia de 10% é significativa.
Comparação de Normalizações
graph TD
subgraph "Batch Normalization"
BN_D["Normaliza ao longo do BATCH<br/>para cada feature"]
BN_S["Batch: [x1, x2, x3, x4]<br/>Feature 1: normaliza [x1f1, x2f1, x3f1, x4f1]"]
BN_P["Precisa de batch > 32<br/>Treino diferente de eval<br/>Usada em CNNs"]
end
subgraph "Layer Normalization"
LN_D["Normaliza ao longo das FEATURES<br/>para cada amostra"]
LN_S["Amostra x1: normaliza [f1, f2, f3, f4]"]
LN_P["Independente do batch<br/>Treino igual a eval<br/>Usada em Transformers"]
end
subgraph "RMS Normalization"
RN_D["Como LayerNorm<br/>mas pula a subtração da média"]
RN_S["Apenas divide pela RMS<br/>Sem centralização"]
RN_P["10% mais rápida que LayerNorm<br/>Mesma acurácia<br/>Usada em LLaMA, Mistral"]
end
Data Augmentation como Regularização
Não é uma modificação do modelo, mas uma modificação dos dados. Transforme as entradas de treino preservando os rótulos:
- Imagens: crop aleatório, flip, rotação, color jitter, cutout
- Texto: substituição de sinônimos, back-translation, deleção aleatória
- Áudio: time stretch, pitch shift, adição de ruído
O efeito é idêntico ao da regularização: aumenta o tamanho efetivo do conjunto de treino, tornando mais difícil para o modelo memorizar exemplos específicos. Um modelo que vê cada imagem apenas uma vez em sua forma original consegue memorizá-la. Um modelo que vê 50 versões aumentadas de cada imagem é forçado a aprender a estrutura invariante.
Early Stopping
O regularizador mais simples: pare o treino quando a loss de validação começar a aumentar. O modelo ainda não fez overfit nesse ponto. Na prática, você acompanha a loss de validação a cada época, salva o melhor modelo e continua treinando por uma janela de "paciência" (tipicamente 5-20 épocas). Se a loss de validação não melhorar dentro da janela de paciência, você para e carrega o melhor modelo salvo.
Quando Aplicar o Quê
flowchart TD
Gap{"Gap de acurácia<br/>treino-teste?"} -->|"> 10%"| Heavy["Regularização pesada"]
Gap -->|"5-10%"| Medium["Regularização moderada"]
Gap -->|"< 5%"| Light["Regularização leve"]
Heavy --> D5["Dropout p=0.3-0.5"]
Heavy --> WD2["Weight decay 0.01-0.1"]
Heavy --> Aug["Data augmentation agressiva"]
Heavy --> ES["Early stopping"]
Medium --> D3["Dropout p=0.1-0.2"]
Medium --> WD1["Weight decay 0.001-0.01"]
Medium --> Norm["BatchNorm ou LayerNorm"]
Light --> D1["Dropout p=0.05-0.1"]
Light --> WD0["Weight decay 1e-4"]
Construa
Passo 1: Dropout (Modo Treino e Eval)
import random
import math
class Dropout:
def __init__(self, p=0.5):
self.p = p
self.training = True
self.mask = None
def forward(self, x):
if not self.training:
return list(x)
self.mask = []
output = []
for val in x:
if random.random() < self.p:
self.mask.append(0)
output.append(0.0)
else:
self.mask.append(1)
output.append(val / (1 - self.p))
return output
def backward(self, grad_output):
grads = []
for g, m in zip(grad_output, self.mask):
if m == 0:
grads.append(0.0)
else:
grads.append(g / (1 - self.p))
return grads
Passo 2: Weight Decay L2
def l2_regularization(weights, lambda_reg):
penalty = 0.0
for w in weights:
penalty += w * w
return lambda_reg * 0.5 * penalty
def l2_gradient(weights, lambda_reg):
return [lambda_reg * w for w in weights]
Passo 3: Batch Normalization
class BatchNorm:
def __init__(self, num_features, momentum=0.1, eps=1e-5):
self.gamma = [1.0] * num_features
self.beta = [0.0] * num_features
self.eps = eps
self.momentum = momentum
self.running_mean = [0.0] * num_features
self.running_var = [1.0] * num_features
self.training = True
self.num_features = num_features
def forward(self, batch):
batch_size = len(batch)
if self.training:
mean = [0.0] * self.num_features
for sample in batch:
for j in range(self.num_features):
mean[j] += sample[j]
mean = [m / batch_size for m in mean]
var = [0.0] * self.num_features
for sample in batch:
for j in range(self.num_features):
var[j] += (sample[j] - mean[j]) ** 2
var = [v / batch_size for v in var]
for j in range(self.num_features):
self.running_mean[j] = (1 - self.momentum) * self.running_mean[j] + self.momentum * mean[j]
self.running_var[j] = (1 - self.momentum) * self.running_var[j] + self.momentum * var[j]
else:
mean = list(self.running_mean)
var = list(self.running_var)
self.x_hat = []
output = []
for sample in batch:
normalized = []
out_sample = []
for j in range(self.num_features):
x_h = (sample[j] - mean[j]) / math.sqrt(var[j] + self.eps)
normalized.append(x_h)
out_sample.append(self.gamma[j] * x_h + self.beta[j])
self.x_hat.append(normalized)
output.append(out_sample)
return output
Passo 4: Layer Normalization
class LayerNorm:
def __init__(self, num_features, eps=1e-5):
self.gamma = [1.0] * num_features
self.beta = [0.0] * num_features
self.eps = eps
self.num_features = num_features
def forward(self, x):
mean = sum(x) / len(x)
var = sum((xi - mean) ** 2 for xi in x) / len(x)
self.x_hat = []
output = []
for j in range(self.num_features):
x_h = (x[j] - mean) / math.sqrt(var + self.eps)
self.x_hat.append(x_h)
output.append(self.gamma[j] * x_h + self.beta[j])
return output
Passo 5: RMSNorm
class RMSNorm:
def __init__(self, num_features, eps=1e-6):
self.gamma = [1.0] * num_features
self.eps = eps
self.num_features = num_features
def forward(self, x):
rms = math.sqrt(sum(xi * xi for xi in x) / len(x) + self.eps)
output = []
for j in range(self.num_features):
output.append(self.gamma[j] * x[j] / rms)
return output
Passo 6: Treino Com e Sem Regularização
def sigmoid(x):
x = max(-500, min(500, x))
return 1.0 / (1.0 + math.exp(-x))
def make_circle_data(n=200, seed=42):
random.seed(seed)
data = []
for _ in range(n):
x = random.uniform(-2, 2)
y = random.uniform(-2, 2)
label = 1.0 if x * x + y * y < 1.5 else 0.0
data.append(([x, y], label))
return data
class RegularizedNetwork:
def __init__(self, hidden_size=16, lr=0.05, dropout_p=0.0, weight_decay=0.0):
random.seed(0)
self.hidden_size = hidden_size
self.lr = lr
self.dropout_p = dropout_p
self.weight_decay = weight_decay
self.dropout = Dropout(p=dropout_p) if dropout_p > 0 else None
self.w1 = [[random.gauss(0, 0.5) for _ in range(2)] for _ in range(hidden_size)]
self.b1 = [0.0] * hidden_size
self.w2 = [random.gauss(0, 0.5) for _ in range(hidden_size)]
self.b2 = 0.0
def forward(self, x, training=True):
self.x = x
self.z1 = []
self.h = []
for i in range(self.hidden_size):
z = self.w1[i][0] * x[0] + self.w1[i][1] * x[1] + self.b1[i]
self.z1.append(z)
self.h.append(max(0.0, z))
if self.dropout and training:
self.dropout.training = True
self.h = self.dropout.forward(self.h)
elif self.dropout:
self.dropout.training = False
self.h = self.dropout.forward(self.h)
self.z2 = sum(self.w2[i] * self.h[i] for i in range(self.hidden_size)) + self.b2
self.out = sigmoid(self.z2)
return self.out
def backward(self, target):
eps = 1e-15
p = max(eps, min(1 - eps, self.out))
d_loss = -(target / p) + (1 - target) / (1 - p)
d_sigmoid = self.out * (1 - self.out)
d_out = d_loss * d_sigmoid
for i in range(self.hidden_size):
d_relu = 1.0 if self.z1[i] > 0 else 0.0
d_h = d_out * self.w2[i] * d_relu
self.w2[i] -= self.lr * (d_out * self.h[i] + self.weight_decay * self.w2[i])
for j in range(2):
self.w1[i][j] -= self.lr * (d_h * self.x[j] + self.weight_decay * self.w1[i][j])
self.b1[i] -= self.lr * d_h
self.b2 -= self.lr * d_out
def evaluate(self, data):
correct = 0
total_loss = 0.0
for x, y in data:
pred = self.forward(x, training=False)
eps = 1e-15
p = max(eps, min(1 - eps, pred))
total_loss += -(y * math.log(p) + (1 - y) * math.log(1 - p))
if (pred >= 0.5) == (y >= 0.5):
correct += 1
return total_loss / len(data), correct / len(data) * 100
def train_model(self, train_data, test_data, epochs=300):
history = []
for epoch in range(epochs):
total_loss = 0.0
correct = 0
for x, y in train_data:
pred = self.forward(x, training=True)
self.backward(y)
eps = 1e-15
p = max(eps, min(1 - eps, pred))
total_loss += -(y * math.log(p) + (1 - y) * math.log(1 - p))
if (pred >= 0.5) == (y >= 0.5):
correct += 1
train_loss = total_loss / len(train_data)
train_acc = correct / len(train_data) * 100
test_loss, test_acc = self.evaluate(test_data)
history.append((train_loss, train_acc, test_loss, test_acc))
if epoch % 75 == 0 or epoch == epochs - 1:
gap = train_acc - test_acc
print(f" Epoch {epoch:3d}: train_acc={train_acc:.1f}%, test_acc={test_acc:.1f}%, gap={gap:.1f}%")
return history
Use
O PyTorch fornece toda a normalização e regularização como módulos:
import torch
import torch.nn as nn
model = nn.Sequential(
nn.Linear(784, 256),
nn.BatchNorm1d(256),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(256, 128),
nn.BatchNorm1d(128),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(128, 10),
)
model.train()
out_train = model(torch.randn(32, 784))
model.eval()
out_test = model(torch.randn(1, 784))
O toggle model.train() / model.eval() é crítico. Ele liga/desliga o dropout e informa à BatchNorm para usar estatísticas do batch ou estatísticas móveis. Esquecer o model.eval() antes da inferência é um dos bugs mais comuns em deep learning. Sua acurácia de teste vai flutuar aleatoriamente porque o dropout continua ativo e a BatchNorm está usando estatísticas do mini-batch.
Para transformers, o padrão é diferente:
class TransformerBlock(nn.Module):
def __init__(self, d_model=512, nhead=8, dropout=0.1):
super().__init__()
self.attention = nn.MultiheadAttention(d_model, nhead, dropout=dropout)
self.norm1 = nn.LayerNorm(d_model)
self.ff = nn.Sequential(
nn.Linear(d_model, d_model * 4),
nn.GELU(),
nn.Linear(d_model * 4, d_model),
nn.Dropout(dropout),
)
self.norm2 = nn.LayerNorm(d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
attended, _ = self.attention(x, x, x)
x = self.norm1(x + self.dropout(attended))
x = self.norm2(x + self.ff(x))
return x
LayerNorm, não BatchNorm. Dropout p=0.1, não p=0.5. Esses são os padrões dos transformers.
Entregue
Esta lição produz:
outputs/prompt-regularization-advisor.md-- um prompt que diagnostica overfitting e recomenda a estratégia de regularização correta
Exercícios
Implemente spatial dropout para dados 2D: em vez de descartar neurônios individuais, descarte canais de features inteiros. Simule isso tratando grupos de features consecutivas como canais e descartando grupos inteiros. Compare o gap treino-teste com o dropout padrão no dataset do círculo com hidden_size=32.
Implemente label smoothing da lição 05 combinado com o dropout desta lição. Treine com quatro configurações: nenhum, só dropout, só label smoothing, ambos. Meça o gap final de acurácia treino-teste para cada um. Qual combinação dá o menor gap?
Adicione uma camada de BatchNorm entre a camada oculta e a ativação na sua rede do dataset do círculo. Treine com e sem BatchNorm nas taxas de aprendizado 0.01, 0.05 e 0.1. A BatchNorm deve permitir treino estável em taxas de aprendizado mais altas, onde a rede comum diverge.
Implemente early stopping: acompanhe a loss de teste a cada época, salve os melhores pesos e pare se a loss de teste não melhorar por 20 épocas. Rode a rede regularizada por 1000 épocas. Reporte qual época teve a melhor acurácia de teste e quantas épocas de computação você economizou.
Compare LayerNorm vs RMSNorm em uma rede de 4 camadas (não apenas 2). Inicialize ambas com os mesmos pesos. Treine por 200 épocas e compare a acurácia final, a velocidade de treino (tempo por época) e as magnitudes dos gradientes na primeira camada. Verifique que a RMSNorm é mais rápida com a mesma acurácia.
Termos-Chave
| Termo | O que as pessoas dizem | O que realmente significa |
|---|---|---|
| Overfitting | "O modelo memorizou os dados" | Quando o desempenho de treino de um modelo supera significativamente seu desempenho de teste, indicando que ele aprendeu ruído em vez de sinal |
| Regularização | "Prevenir overfitting" | Qualquer técnica que restringe a complexidade do modelo para melhorar a generalização: dropout, weight decay, normalização, augmentation |
| Dropout | "Deleção aleatória de neurônios" | Zerar neurônios aleatórios durante o treino com probabilidade p, forçando representações redundantes; equivalente a treinar um ensemble |
| Weight decay | "Penalidade L2" | Encolher todos os pesos em direção a zero subtraindo lambda * w a cada passo; penaliza a complexidade pela magnitude dos pesos |
| Batch normalization | "Normalizar por batch" | Normalizar as saídas das camadas ao longo da dimensão do batch usando estatísticas do batch durante o treino e médias móveis durante a inferência |
| Layer normalization | "Normalizar por amostra" | Normalizar ao longo das features dentro de cada amostra; independente do batch, usada em transformers onde o tamanho do batch varia |
| RMSNorm | "LayerNorm sem a média" | Normalização por raiz da média quadrática; descarta a subtração da média da LayerNorm para um speedup de 10% com acurácia igual |
| Early stopping | "Parar antes do overfit" | Interromper o treino quando a loss de validação para de melhorar; o regularizador mais simples, frequentemente usado junto com outros |
| Data augmentation | "Mais dados a partir de menos" | Transformar as entradas de treino (flip, crop, ruído) para aumentar o tamanho efetivo do dataset e forçar o aprendizado de invariância |
| Gap de generalização | "Divisão treino-teste" | A diferença entre o desempenho de treino e o de teste; a regularização busca minimizar esse gap |
Leitura Adicional
- Srivastava et al., "Dropout: A Simple Way to Prevent Neural Networks from Overfitting" (2014) -- o artigo original do dropout com a interpretação como ensemble e experimentos extensos
- Ioffe & Szegedy, "Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift" (2015) -- introduziu a BatchNorm e seu procedimento de treino, um dos artigos de deep learning mais citados
- Zhang & Sennrich, "Root Mean Square Layer Normalization" (2019) -- mostrou que a RMSNorm iguala a acurácia da LayerNorm com computação reduzida; adotada por LLaMA e Mistral
- Zhang et al., "Understanding Deep Learning Requires Rethinking Generalization" (2017) -- o artigo marcante mostrando que redes neurais podem memorizar rótulos aleatórios, desafiando visões tradicionais sobre generalização