Phase 03 - Lesson 08

Inicialización de Pesos y Estabilidad del Entrenamiento

This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.

Inicializa mal y el entrenamiento nunca arranca. Inicializa bien y 50 capas entrenan de forma tan fluida como 3.

Tipo: Build Lenguajes: Python Requisitos previos: Lección 03.04 (Funciones de Activación), Lección 03.07 (Regularización) Tiempo: ~90 minutos

Objetivos de Aprendizaje

  • Implementar las estrategias de inicialización cero, aleatoria, Xavier/Glorot y Kaiming/He y medir su efecto sobre las magnitudes de activación a lo largo de 50 capas
  • Derivar por qué la inicialización Xavier usa Var(w) = 2/(fan_in + fan_out) y la Kaiming usa Var(w) = 2/fan_in
  • Demostrar el problema de simetría con la inicialización cero y explicar por qué la escala aleatoria por sí sola es insuficiente
  • Asociar la estrategia de inicialización correcta a la función de activación: Xavier para sigmoid/tanh, Kaiming para ReLU/GELU

El Problema

Inicializa todos los pesos en cero. Nada aprende. Cada neurona computa la misma función, recibe el mismo gradiente y se actualiza de forma idéntica. Después de 10.000 épocas, tu capa oculta de 512 neuronas siguen siendo 512 copias de la misma neurona. Pagaste por 512 parámetros y obtuviste 1.

Inicialízalos demasiado grandes. Las activaciones explotan a través de la red. Para la capa 10, los valores llegan a 1e15. Para la capa 20, se desbordan al infinito. Los gradientes siguen la misma trayectoria en sentido inverso.

Inicialízalos aleatoriamente a partir de una distribución normal estándar. Funciona para 3 capas. Con 50 capas, la señal colapsa a cero o detona al infinito según si la escala aleatoria fue ligeramente demasiado pequeña o ligeramente demasiado grande. La frontera entre "funciona" y "roto" es finísima.

La inicialización de pesos es la decisión más subestimada en deep learning. La arquitectura recibe papers. Los optimizadores reciben entradas de blog. La inicialización recibe una nota al pie. Pero si te equivocas en ella, nada más importa -- tu red está muerta antes de que el entrenamiento comience.

El Concepto

El Problema de Simetría

Cada neurona en una capa tiene la misma estructura: multiplicar entradas por pesos, sumar el sesgo, aplicar la activación. Si todos los pesos comienzan con el mismo valor (cero es el caso extremo), cada neurona computa la misma salida. Durante la retropropagación, cada neurona recibe el mismo gradiente. Durante el paso de actualización, cada neurona cambia en la misma cantidad.

Te quedas atascado. La red tiene cientos de parámetros, pero todos se mueven al unísono. Esto se llama simetría, y la inicialización aleatoria es la manera de fuerza bruta de romperla. Cada neurona comienza en un punto diferente del espacio de pesos, así que cada una aprende una característica diferente.

Pero "aleatorio" no es suficiente. La escala de la aleatoriedad determina si la red entrena.

Propagación de Varianza a Través de las Capas

Considera una sola capa con fan_in entradas:

z = w1*x1 + w2*x2 + ... + w_n*x_n

Si cada peso wi se extrae de una distribución con varianza Var(w) y cada entrada xi tiene varianza Var(x), la varianza de la salida es:

Var(z) = fan_in * Var(w) * Var(x)

Si Var(w) = 1 y fan_in = 512, la varianza de la salida es 512x la varianza de la entrada. Después de 10 capas: 512^10 = 1.2e27. Tu señal ha explotado.

Si Var(w) = 0.001, la varianza de la salida se reduce en 0.001 * 512 = 0.512 por capa. Después de 10 capas: 0.512^10 = 0.00013. Tu señal se ha desvanecido.

El objetivo: elegir Var(w) de modo que Var(z) = Var(x). La magnitud de la señal se mantiene constante a través de las capas.

Inicialización Xavier/Glorot

Glorot y Bengio (2010) derivaron la solución para las activaciones sigmoid y tanh. Para mantener la varianza constante tanto en el paso forward como en el backward:

Var(w) = 2 / (fan_in + fan_out)

En la práctica, los pesos se extraen de:

w ~ Uniform(-limit, limit)  where limit = sqrt(6 / (fan_in + fan_out))

o:

w ~ Normal(0, sqrt(2 / (fan_in + fan_out)))

Esto funciona porque sigmoid y tanh son aproximadamente lineales cerca de cero, donde viven las activaciones correctamente inicializadas. La varianza se mantiene estable a través de decenas de capas.

Inicialización Kaiming/He

ReLU mata la mitad de las salidas (todo lo negativo se convierte en cero). El fan_in efectivo se reduce a la mitad porque, en promedio, la mitad de las entradas se anula. La inicialización Xavier no tiene esto en cuenta -- subestima la varianza necesaria.

He et al. (2015) ajustaron la fórmula:

Var(w) = 2 / fan_in

Los pesos se extraen de:

w ~ Normal(0, sqrt(2 / fan_in))

El factor de 2 compensa que ReLU anule la mitad de las activaciones. Sin él, la señal se reduce en ~0.5x por capa. Con 50 capas: 0.5^50 = 8.8e-16. La inicialización Kaiming evita esto.

Inicialización de Transformers

GPT-2 introdujo un patrón diferente. Las conexiones residuales suman la salida de cada subcapa a su entrada:

x = x + sublayer(x)

Cada suma aumenta la varianza. Con N capas residuales, la varianza crece proporcionalmente a N. GPT-2 escala los pesos de las capas residuales por 1/sqrt(2N), donde N es el número de capas. Esto mantiene estable la magnitud de la señal acumulada.

Llama 3 (405B de parámetros, 126 capas) usa un esquema similar. Sin esta escala, el flujo residual crecería sin límite a través de las 126 capas de bloques de atención y feedforward.

flowchart TD
    subgraph "Inicialización Cero"
        Z1["Capa 1<br/>Todos los pesos = 0"] --> Z2["Capa 2<br/>Todas las neuronas idénticas"]
        Z2 --> Z3["Capa 3<br/>Aún idénticas"]
        Z3 --> ZR["Resultado: 1 neurona efectiva<br/>sin importar el ancho"]
    end

    subgraph "Inicialización Xavier"
        X1["Capa 1<br/>Var = 2/(fan_in+fan_out)"] --> X2["Capa 2<br/>Señal estable"]
        X2 --> X3["Capa 50<br/>Señal estable"]
        X3 --> XR["Resultado: Entrena con<br/>sigmoid/tanh"]
    end

    subgraph "Inicialización Kaiming"
        K1["Capa 1<br/>Var = 2/fan_in"] --> K2["Capa 2<br/>Señal estable"]
        K2 --> K3["Capa 50<br/>Señal estable"]
        K3 --> KR["Resultado: Entrena con<br/>ReLU/GELU"]
    end

Magnitud de Activación a Través de 50 Capas

graph LR
    subgraph "Magnitud Media de Activación"
        direction LR
        L1["Capa 1"] --> L10["Capa 10"] --> L25["Capa 25"] --> L50["Capa 50"]
    end

    subgraph "Resultados"
        R1["Aleatoria N(0,1): EXPLOTA en la capa 5"]
        R2["Aleatoria N(0,0.01): Se desvanece en la capa 10"]
        R3["Xavier + Sigmoid: ~1.0 en la capa 50"]
        R4["Kaiming + ReLU: ~1.0 en la capa 50"]
    end

Eligiendo la Inicialización Correcta

flowchart TD
    Start["¿Qué activación?"] --> Act{"¿Tipo de activación?"}

    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 que ReLU)"]
    Act -->|"Residual de Transformer"| GPT["Escalar por 1/sqrt(2N)<br/>N = num capas"]

    Xavier --> Check["Verificar: las magnitudes de activación<br/>se mantienen entre 0.5 y 2.0<br/>en todas las capas"]
    Kaiming --> Check
    Kaiming2 --> Check
    GPT --> Check

Constrúyelo

Paso 1: Estrategias de Inicialización

Cuatro formas de inicializar una matriz de pesos. Cada una retorna una lista de listas (una matriz 2D) con fan_in columnas y fan_out filas.

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)]

Paso 2: Funciones de Activación

Necesitamos sigmoid, tanh y ReLU para probar cada estrategia de inicialización con su activación prevista.

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)

Paso 3: Forward Pass a Través de 50 Capas

Pasa datos aleatorios por una red profunda y mide la magnitud media de activación en cada capa.

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

Paso 4: El Experimento

Ejecuta todas las combinaciones: inicialización cero, aleatoria N(0,1), aleatoria N(0,0.01), Xavier con sigmoid, Xavier con tanh, Kaiming con ReLU. Imprime la magnitud en capas clave.

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)

Paso 5: Demostración de Simetría

Muestra que la inicialización cero produce neuronas idénticas.

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])})")

Paso 6: Reporte de Magnitud Capa por Capa

Imprime un gráfico de barras visual de las magnitudes de activación a lo largo de 50 capas.

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})")

Úsalo

PyTorch las proporciona como funciones 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)

Cuando llamas a nn.Linear(512, 256), PyTorch usa por defecto la inicialización Kaiming uniforme. Por eso la mayoría de las redes simples "simplemente funcionan" -- PyTorch ya tomó la decisión correcta. Pero cuando construyes arquitecturas personalizadas o vas más allá de 20 capas, necesitas entender qué está pasando y potencialmente sobrescribir el valor por defecto.

Para transformers, los modelos de HuggingFace normalmente manejan la inicialización en su método _init_weights. La implementación de GPT-2 escala las proyecciones residuales por 1/sqrt(N). Si estás construyendo un transformer desde cero, necesitas agregar esto tú mismo.

Entrégalo

Esta lección produce:

  • outputs/prompt-init-strategy.md -- un prompt que diagnostica problemas de inicialización de pesos y recomienda la estrategia correcta

Ejercicios

  1. Agrega la inicialización LeCun (Var = 1/fan_in, diseñada para la activación SELU). Ejecuta el experimento de 50 capas con la inicialización LeCun + tanh y compara con Xavier + tanh.

  2. Implementa la escala residual de GPT-2: multiplica la salida de cada capa por 1/sqrt(2*N) antes de sumar al flujo residual. Ejecuta 50 capas con y sin la escala, mide qué tan rápido crece la magnitud residual.

  3. Crea una función de "chequeo de salud de la inicialización" que reciba las dimensiones de las capas de una red y el tipo de activación, y luego recomiende la inicialización correcta y advierta si la inicialización actual causará problemas.

  4. Ejecuta el experimento con fan_in = 16 vs fan_in = 1024. Xavier y Kaiming se adaptan al fan_in, pero la inicialización aleatoria no. Muestra cómo la brecha entre "funciona" y "se rompe" se amplía con capas más grandes.

  5. Implementa la inicialización ortogonal (genera una matriz aleatoria, computa su SVD, usa la matriz ortogonal U). Compara con Kaiming para redes ReLU con 50 capas.

Términos Clave

Término Lo que la gente dice Lo que realmente significa
Inicialización de pesos "Establecer los pesos iniciales aleatoriamente" La estrategia para elegir los valores iniciales de peso que determina si una red puede entrenar siquiera
Ruptura de simetría "Hacer que las neuronas sean diferentes" Usar inicialización aleatoria para asegurar que las neuronas aprendan características distintas en lugar de computar funciones idénticas
Fan-in "Número de entradas de una neurona" El número de conexiones entrantes, que determina cómo se acumula la varianza de entrada en la suma ponderada
Fan-out "Número de salidas de una neurona" El número de conexiones salientes, relevante para mantener la varianza del gradiente durante la retropropagación
Inicialización Xavier/Glorot "La inicialización del sigmoid" Var(w) = 2/(fan_in + fan_out), diseñada para preservar la varianza a través de las activaciones sigmoid y tanh
Inicialización Kaiming/He "La inicialización de ReLU" Var(w) = 2/fan_in, tiene en cuenta que ReLU anula la mitad de las activaciones
Propagación de varianza "Cómo las señales crecen o se reducen a través de las capas" El análisis matemático de cómo la varianza de activación cambia capa por capa según la escala de los pesos
Escala residual "El truco de inicialización de GPT-2" Escalar los pesos de las conexiones residuales por 1/sqrt(2N) para evitar el crecimiento de la varianza a través de N capas de transformer
Red muerta "Nada entrena" Una red donde la mala inicialización causa que todos los gradientes sean cero o todas las activaciones se saturen
Activaciones explosivas "Los valores van al infinito" Cuando la varianza de los pesos es demasiado alta, causando que las magnitudes de activación crezcan exponencialmente a través de las capas

Lectura Adicional

  • Glorot & Bengio, "Understanding the difficulty of training deep feedforward neural networks" (2010) -- el paper original de la inicialización Xavier con análisis de varianza
  • He et al., "Delving Deep into Rectifiers" (2015) -- introdujo la inicialización Kaiming para redes ReLU
  • Radford et al., "Language Models are Unsupervised Multitask Learners" (2019) -- paper de GPT-2 con la inicialización de escala residual
  • Mishkin & Matas, "All You Need is a Good Init" (2016) -- inicialización layer-sequential unit-variance, una alternativa empírica a las fórmulas analíticas
0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).