Phase 03 - Lesson 08
Inicialização de Pesos e Estabilidade do Treinamento
This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.
Inicialize errado e o treinamento nunca começa. Inicialize certo e 50 camadas treinam de forma tão suave quanto 3.
Tipo: Build Linguagens: Python Pré-requisitos: Lição 03.04 (Funções de Ativação), Lição 03.07 (Regularização) Tempo: ~90 minutos
Objetivos de Aprendizagem
- Implementar as estratégias de inicialização zero, aleatória, Xavier/Glorot e Kaiming/He e medir seu efeito sobre as magnitudes de ativação ao longo de 50 camadas
- Derivar por que a inicialização Xavier usa Var(w) = 2/(fan_in + fan_out) e a Kaiming usa Var(w) = 2/fan_in
- Demonstrar o problema de simetria com a inicialização zero e explicar por que a escala aleatória sozinha é insuficiente
- Associar a estratégia de inicialização correta à função de ativação: Xavier para sigmoid/tanh, Kaiming para ReLU/GELU
O Problema
Inicialize todos os pesos com zero. Nada aprende. Cada neurônio computa a mesma função, recebe o mesmo gradiente e atualiza de forma idêntica. Após 10.000 épocas, sua camada oculta de 512 neurônios ainda são 512 cópias do mesmo neurônio. Você pagou por 512 parâmetros e obteve 1.
Inicialize-os grandes demais. As ativações explodem ao longo da rede. Na camada 10, os valores chegam a 1e15. Na camada 20, transbordam para o infinito. Os gradientes seguem a mesma trajetória em sentido inverso.
Inicialize-os aleatoriamente a partir de uma distribuição normal padrão. Funciona para 3 camadas. Com 50 camadas, o sinal colapsa para zero ou detona para o infinito, dependendo de a escala aleatória ter sido ligeiramente pequena demais ou ligeiramente grande demais. A fronteira entre "funciona" e "quebrado" é finíssima.
A inicialização de pesos é a decisão mais subestimada em deep learning. A arquitetura ganha papers. Os otimizadores ganham posts de blog. A inicialização ganha uma nota de rodapé. Mas erre nisso e nada mais importa -- sua rede está morta antes de o treinamento começar.
O Conceito
O Problema de Simetria
Cada neurônio em uma camada tem a mesma estrutura: multiplicar entradas por pesos, somar o viés, aplicar a ativação. Se todos os pesos começam com o mesmo valor (zero é o caso extremo), cada neurônio computa a mesma saída. Durante a retropropagação, cada neurônio recebe o mesmo gradiente. Durante o passo de atualização, cada neurônio muda na mesma quantidade.
Você fica travado. A rede tem centenas de parâmetros, mas todos se movem em sincronia perfeita. Isso é chamado de simetria, e a inicialização aleatória é a maneira de força bruta de quebrá-la. Cada neurônio começa em um ponto diferente do espaço de pesos, então cada um aprende uma característica diferente.
Mas "aleatório" não é suficiente. A escala da aleatoriedade determina se a rede treina.
Propagação de Variância Através das Camadas
Considere uma única camada com fan_in entradas:
z = w1*x1 + w2*x2 + ... + w_n*x_n
Se cada peso wi é extraído de uma distribuição com variância Var(w) e cada entrada xi tem variância Var(x), a variância da saída é:
Var(z) = fan_in * Var(w) * Var(x)
Se Var(w) = 1 e fan_in = 512, a variância da saída é 512x a variância da entrada. Após 10 camadas: 512^10 = 1.2e27. Seu sinal explodiu.
Se Var(w) = 0.001, a variância da saída encolhe em 0.001 * 512 = 0.512 por camada. Após 10 camadas: 0.512^10 = 0.00013. Seu sinal desapareceu.
O objetivo: escolher Var(w) de modo que Var(z) = Var(x). A magnitude do sinal permanece constante entre as camadas.
Inicialização Xavier/Glorot
Glorot e Bengio (2010) derivaram a solução para as ativações sigmoid e tanh. Para manter a variância constante tanto no passo forward quanto no backward:
Var(w) = 2 / (fan_in + fan_out)
Na prática, os pesos são extraídos de:
w ~ Uniform(-limit, limit) where limit = sqrt(6 / (fan_in + fan_out))
ou:
w ~ Normal(0, sqrt(2 / (fan_in + fan_out)))
Isso funciona porque sigmoid e tanh são aproximadamente lineares perto de zero, onde vivem as ativações corretamente inicializadas. A variância permanece estável ao longo de dezenas de camadas.
Inicialização Kaiming/He
A ReLU mata metade das saídas (tudo que é negativo vira zero). O fan_in efetivo é reduzido pela metade porque, em média, metade das entradas é zerada. A inicialização Xavier não leva isso em conta -- ela subestima a variância necessária.
He et al. (2015) ajustaram a fórmula:
Var(w) = 2 / fan_in
Os pesos são extraídos de:
w ~ Normal(0, sqrt(2 / fan_in))
O fator de 2 compensa a ReLU zerando metade das ativações. Sem ele, o sinal encolhe em ~0.5x por camada. Com 50 camadas: 0.5^50 = 8.8e-16. A inicialização Kaiming evita isso.
Inicialização de Transformers
O GPT-2 introduziu um padrão diferente. As conexões residuais somam a saída de cada subcamada à sua entrada:
x = x + sublayer(x)
Cada soma aumenta a variância. Com N camadas residuais, a variância cresce proporcionalmente a N. O GPT-2 escala os pesos das camadas residuais por 1/sqrt(2N), onde N é o número de camadas. Isso mantém estável a magnitude do sinal acumulado.
O Llama 3 (405B de parâmetros, 126 camadas) usa um esquema semelhante. Sem essa escala, o fluxo residual cresceria de forma ilimitada ao longo das 126 camadas de blocos de atenção e feedforward.
flowchart TD
subgraph "Inicialização Zero"
Z1["Camada 1<br/>Todos os pesos = 0"] --> Z2["Camada 2<br/>Todos os neurônios idênticos"]
Z2 --> Z3["Camada 3<br/>Ainda idênticos"]
Z3 --> ZR["Resultado: 1 neurônio efetivo<br/>independente da largura"]
end
subgraph "Inicialização Xavier"
X1["Camada 1<br/>Var = 2/(fan_in+fan_out)"] --> X2["Camada 2<br/>Sinal estável"]
X2 --> X3["Camada 50<br/>Sinal estável"]
X3 --> XR["Resultado: Treina com<br/>sigmoid/tanh"]
end
subgraph "Inicialização Kaiming"
K1["Camada 1<br/>Var = 2/fan_in"] --> K2["Camada 2<br/>Sinal estável"]
K2 --> K3["Camada 50<br/>Sinal estável"]
K3 --> KR["Resultado: Treina com<br/>ReLU/GELU"]
end
Magnitude de Ativação Através de 50 Camadas
graph LR
subgraph "Magnitude Média de Ativação"
direction LR
L1["Camada 1"] --> L10["Camada 10"] --> L25["Camada 25"] --> L50["Camada 50"]
end
subgraph "Resultados"
R1["Aleatória N(0,1): EXPLODE na camada 5"]
R2["Aleatória N(0,0.01): Desaparece na camada 10"]
R3["Xavier + Sigmoid: ~1.0 na camada 50"]
R4["Kaiming + ReLU: ~1.0 na camada 50"]
end
Escolhendo a Inicialização Certa
flowchart TD
Start["Qual ativação?"] --> Act{"Tipo de ativação?"}
Act -->|"Sigmoid / Tanh"| Xavier["Xavier/Glorot<br/>Var = 2/(fan_in + fan_out)"]
Act -->|"ReLU / Leaky ReLU"| Kaiming["Kaiming/He<br/>Var = 2/fan_in"]
Act -->|"GELU / Swish"| Kaiming2["Kaiming/He<br/>(igual à ReLU)"]
Act -->|"Residual de Transformer"| GPT["Escalar por 1/sqrt(2N)<br/>N = num camadas"]
Xavier --> Check["Verificar: as magnitudes de ativação<br/>permanecem entre 0.5 e 2.0<br/>em todas as camadas"]
Kaiming --> Check
Kaiming2 --> Check
GPT --> Check
Construa
Passo 1: Estratégias de Inicialização
Quatro formas de inicializar uma matriz de pesos. Cada uma retorna uma lista de listas (uma matriz 2D) com fan_in colunas e fan_out linhas.
import math
import random
def zero_init(fan_in, fan_out):
return [[0.0 for _ in range(fan_in)] for _ in range(fan_out)]
def random_init(fan_in, fan_out, scale=1.0):
return [[random.gauss(0, scale) for _ in range(fan_in)] for _ in range(fan_out)]
def xavier_init(fan_in, fan_out):
std = math.sqrt(2.0 / (fan_in + fan_out))
return [[random.gauss(0, std) for _ in range(fan_in)] for _ in range(fan_out)]
def kaiming_init(fan_in, fan_out):
std = math.sqrt(2.0 / fan_in)
return [[random.gauss(0, std) for _ in range(fan_in)] for _ in range(fan_out)]
Passo 2: Funções de Ativação
Precisamos de sigmoid, tanh e ReLU para testar cada estratégia de inicialização com sua ativação pretendida.
def sigmoid(x):
x = max(-500, min(500, x))
return 1.0 / (1.0 + math.exp(-x))
def tanh_act(x):
return math.tanh(x)
def relu(x):
return max(0.0, x)
Passo 3: Forward Pass Através de 50 Camadas
Passe dados aleatórios por uma rede profunda e meça a magnitude média de ativação em cada camada.
def forward_deep(init_fn, activation_fn, n_layers=50, width=64, n_samples=100):
random.seed(42)
layer_magnitudes = []
inputs = [[random.gauss(0, 1) for _ in range(width)] for _ in range(n_samples)]
for layer_idx in range(n_layers):
weights = init_fn(width, width)
biases = [0.0] * width
new_inputs = []
for sample in inputs:
output = []
for neuron_idx in range(width):
z = sum(weights[neuron_idx][j] * sample[j] for j in range(width)) + biases[neuron_idx]
output.append(activation_fn(z))
new_inputs.append(output)
inputs = new_inputs
magnitudes = []
for sample in inputs:
magnitudes.append(sum(abs(v) for v in sample) / width)
mean_mag = sum(magnitudes) / len(magnitudes)
layer_magnitudes.append(mean_mag)
return layer_magnitudes
Passo 4: O Experimento
Execute todas as combinações: inicialização zero, aleatória N(0,1), aleatória N(0,0.01), Xavier com sigmoid, Xavier com tanh, Kaiming com ReLU. Imprima a magnitude em camadas-chave.
def run_experiment():
configs = [
("Zero init + Sigmoid", lambda fi, fo: zero_init(fi, fo), sigmoid),
("Random N(0,1) + ReLU", lambda fi, fo: random_init(fi, fo, 1.0), relu),
("Random N(0,0.01) + ReLU", lambda fi, fo: random_init(fi, fo, 0.01), relu),
("Xavier + Sigmoid", xavier_init, sigmoid),
("Xavier + Tanh", xavier_init, tanh_act),
("Kaiming + ReLU", kaiming_init, relu),
]
print(f"{'Strategy':<30} {'L1':>10} {'L5':>10} {'L10':>10} {'L25':>10} {'L50':>10}")
print("-" * 80)
for name, init_fn, act_fn in configs:
mags = forward_deep(init_fn, act_fn)
row = f"{name:<30}"
for idx in [0, 4, 9, 24, 49]:
val = mags[idx]
if val > 1e6:
row += f" {'EXPLODED':>10}"
elif val < 1e-6:
row += f" {'VANISHED':>10}"
else:
row += f" {val:>10.4f}"
print(row)
Passo 5: Demonstração de Simetria
Mostre que a inicialização zero produz neurônios idênticos.
def symmetry_demo():
random.seed(42)
weights = zero_init(2, 4)
biases = [0.0] * 4
inputs = [0.5, -0.3]
outputs = []
for neuron_idx in range(4):
z = sum(weights[neuron_idx][j] * inputs[j] for j in range(2)) + biases[neuron_idx]
outputs.append(sigmoid(z))
print("\nSymmetry Demo (4 neurons, zero init):")
for i, out in enumerate(outputs):
print(f" Neuron {i}: output = {out:.6f}")
all_same = all(abs(outputs[i] - outputs[0]) < 1e-10 for i in range(len(outputs)))
print(f" All identical: {all_same}")
print(f" Effective parameters: 1 (not {len(weights) * len(weights[0])})")
Passo 6: Relatório de Magnitude Camada por Camada
Imprima um gráfico de barras visual das magnitudes de ativação ao longo de 50 camadas.
def magnitude_report(name, magnitudes):
print(f"\n{name}:")
for i, mag in enumerate(magnitudes):
if i % 5 == 0 or i == len(magnitudes) - 1:
if mag > 1e6:
bar = "X" * 50 + " EXPLODED"
elif mag < 1e-6:
bar = "." + " VANISHED"
else:
bar_len = min(50, max(1, int(mag * 10)))
bar = "#" * bar_len
print(f" Layer {i+1:3d}: {bar} ({mag:.6f})")
Use
O PyTorch fornece estas como funções integradas:
import torch
import torch.nn as nn
layer = nn.Linear(512, 256)
nn.init.xavier_uniform_(layer.weight)
nn.init.xavier_normal_(layer.weight)
nn.init.kaiming_uniform_(layer.weight, nonlinearity='relu')
nn.init.kaiming_normal_(layer.weight, nonlinearity='relu')
nn.init.zeros_(layer.bias)
Quando você chama nn.Linear(512, 256), o PyTorch usa por padrão a inicialização Kaiming uniforme. É por isso que a maioria das redes simples "simplesmente funciona" -- o PyTorch já fez a escolha certa. Mas quando você constrói arquiteturas personalizadas ou vai além de 20 camadas, você precisa entender o que está acontecendo e potencialmente sobrescrever o padrão.
Para transformers, os modelos da HuggingFace normalmente lidam com a inicialização em seu método _init_weights. A implementação do GPT-2 escala as projeções residuais por 1/sqrt(N). Se você está construindo um transformer do zero, precisa adicionar isso você mesmo.
Entregue
Esta lição produz:
outputs/prompt-init-strategy.md-- um prompt que diagnostica problemas de inicialização de pesos e recomenda a estratégia certa
Exercícios
Adicione a inicialização LeCun (Var = 1/fan_in, projetada para a ativação SELU). Execute o experimento de 50 camadas com a inicialização LeCun + tanh e compare com Xavier + tanh.
Implemente a escala residual do GPT-2: multiplique a saída de cada camada por 1/sqrt(2*N) antes de somar ao fluxo residual. Execute 50 camadas com e sem a escala, meça a velocidade com que a magnitude residual cresce.
Crie uma função de "verificação de saúde da inicialização" que recebe as dimensões das camadas de uma rede e o tipo de ativação, e então recomenda a inicialização correta e alerta se a inicialização atual causará problemas.
Execute o experimento com fan_in = 16 vs fan_in = 1024. Xavier e Kaiming se adaptam ao fan_in, mas a inicialização aleatória não. Mostre como a distância entre "funciona" e "quebra" aumenta com camadas maiores.
Implemente a inicialização ortogonal (gere uma matriz aleatória, compute sua SVD, use a matriz ortogonal U). Compare com Kaiming para redes ReLU com 50 camadas.
Termos-Chave
| Termo | O que as pessoas dizem | O que realmente significa |
|---|---|---|
| Inicialização de pesos | "Definir os pesos iniciais aleatoriamente" | A estratégia para escolher os valores iniciais de peso que determina se uma rede consegue ou não treinar |
| Quebra de simetria | "Tornar os neurônios diferentes" | Usar inicialização aleatória para garantir que os neurônios aprendam características distintas em vez de computar funções idênticas |
| Fan-in | "Número de entradas de um neurônio" | O número de conexões de entrada, que determina como a variância da entrada se acumula na soma ponderada |
| Fan-out | "Número de saídas de um neurônio" | O número de conexões de saída, relevante para manter a variância do gradiente durante a retropropagação |
| Inicialização Xavier/Glorot | "A inicialização do sigmoid" | Var(w) = 2/(fan_in + fan_out), projetada para preservar a variância através das ativações sigmoid e tanh |
| Inicialização Kaiming/He | "A inicialização da ReLU" | Var(w) = 2/fan_in, leva em conta a ReLU zerando metade das ativações |
| Propagação de variância | "Como os sinais crescem ou encolhem através das camadas" | A análise matemática de como a variância de ativação muda camada a camada com base na escala dos pesos |
| Escala residual | "O truque de inicialização do GPT-2" | Escalar os pesos das conexões residuais por 1/sqrt(2N) para evitar o crescimento da variância através de N camadas de transformer |
| Rede morta | "Nada treina" | Uma rede em que a inicialização ruim faz com que todos os gradientes sejam zero ou todas as ativações saturem |
| Ativações explosivas | "Os valores vão para o infinito" | Quando a variância dos pesos é alta demais, fazendo com que as magnitudes de ativação cresçam exponencialmente através das camadas |
Leitura Adicional
- Glorot & Bengio, "Understanding the difficulty of training deep feedforward neural networks" (2010) -- o paper original da inicialização Xavier com análise de variância
- He et al., "Delving Deep into Rectifiers" (2015) -- introduziu a inicialização Kaiming para redes ReLU
- Radford et al., "Language Models are Unsupervised Multitask Learners" (2019) -- paper do GPT-2 com a inicialização de escala residual
- Mishkin & Matas, "All You Need is a Good Init" (2016) -- inicialização layer-sequential unit-variance, uma alternativa empírica às fórmulas analíticas