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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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
0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).