fast.ai · Parte 2 · Unidad 2 de 5
Por qué las redes profundas no explotan (cuando están bien hechas)
Entrenar una red de más de unas pocas capas sin cuidado da NaN o ceros por todos lados. Esta
unidad es la explicación matemática precisa de por qué pasa eso, y las tres técnicas (inicialización,
normalización, skip connections) que lo resuelven.
1. El problema: 50 capas sin cuidado dan NaN o cero
La demostración es brutal y directa:
x = torch.randn(200, 100)
for i in range(50): x = x @ torch.randn(100,100)
x[0:5,0:5] # resultado: NaN en todos lados
Multiplicando 50 veces por pesos aleatorios estándar, los valores explotan a infinito (y de ahí a
NaN). Reduciendo la escala de los pesos (* 0.01) pasa lo opuesto: todo colapsa a
cero. Ninguno de los dos extremos entrena nada — hace falta encontrar la escala exacta que mantenga la
varianza de las activaciones estable capa tras capa.
2. Inicialización: Xavier/Glorot y Kaiming/He
Xavier Glorot y Yoshua Bengio (2010) derivaron la escala exacta: si cada capa tiene
n_in entradas, escalar los pesos por 1/√n_in mantiene la desviación estándar de
las activaciones en 1 a través de capas sucesivas — matemáticamente, porque sumar n productos
de variables independientes de media 0 y varianza 1 da una varianza total de n, y dividir por
√n la normaliza de vuelta a 1.
Pero Xavier asume que no hay no-linealidad entre medio. Con ReLU (que descarta la mitad de los valores,
los negativos), la varianza se reduce a la mitad en cada capa — por eso Kaiming He et al.
(2015, "Delving Deep into Rectifiers") derivaron una escala corregida: √(2/n_in), el factor 2
compensando exactamente lo que ReLU descarta.
# Sin corrección: colapsa a cero tras 50 capas
x = torch.randn(200, 100)
for i in range(50): x = relu(x @ (torch.randn(100,100) * 0.1))
# Con Kaiming/He: estable
for i in range(50): x = relu(x @ (torch.randn(100,100) * sqrt(2/100)))
En PyTorch esto es init.kaiming_normal_(m.weight, a=leaky) aplicado a cada capa
convolucional. El notebook también introduce GeneralRelu: un ReLU con leak (para no matar
completamente los negativos), sub (restar una constante, recentrando la distribución) y un máximo — cada
parámetro corrigiendo una asimetría distinta que el ReLU estándar introduce en la distribución de
activaciones.
3. Normalización: LayerNorm y BatchNorm
Inicializar bien resuelve el problema al principio del entrenamiento — pero a medida que los pesos cambian, la distribución de activaciones de cada capa vuelve a moverse ("internal covariate shift", el término del paper original de Ioffe & Szegedy 2015). La solución es normalizar activamente, no solo una vez al inicio.
LayerNorm normaliza sobre todos los canales/alto/ancho de cada ejemplo individual:
class LayerNorm(nn.Module):
def forward(self, x):
m = x.mean((1,2,3), keepdim=True)
v = x.var ((1,2,3), keepdim=True)
x = (x-m) / ((v+self.eps).sqrt())
return x*self.mult + self.add # parámetros aprendidos, no fijos
BatchNorm normaliza sobre el batch completo (por canal), y mantiene un promedio corrido de media/varianza para usar en inferencia (cuando no hay un batch completo de donde calcular estadísticas):
def update_stats(self, x):
m = x.mean((0,2,3), keepdim=True)
v = x.var ((0,2,3), keepdim=True)
self.means.lerp_(m, self.mom) # promedio móvil exponencial
self.vars.lerp_ (v, self.mom)
return m,v
Con BatchNorm el learning rate estable sube notablemente (de 0.2 a 0.4 en el experimento del notebook, con el resto de hiperparámetros iguales) — normalizar activamente permite pasos de gradiente más agresivos sin que el entrenamiento se desestabilice.
4. LSUV: inicializar mirando las activaciones reales
Layer-wise Sequential Unit-Variance (Mishkin & Matas, "All You Need is a Good Init") es un enfoque distinto: en vez de calcular la escala correcta analíticamente, se ejecuta un forward pass real y se ajusta cada capa hasta que su output tenga media 0 y varianza 1 medida, no derivada por fórmula:
def lsuv_init(model, m, m_in, xb):
h = Hook(m, _lsuv_stats)
with torch.no_grad():
while model(xb) is not None and (abs(h.std-1)>1e-3 or abs(h.mean)>1e-3):
m_in.bias -= h.mean
m_in.weight.data /= h.std
Es un enfoque empírico y directo: medí, ajustá, repetí hasta converger — útil sobre todo cuando la arquitectura es lo suficientemente rara como para que la derivación analítica de Kaiming/He no aplique limpiamente.
5. ResNets: skip connections
Incluso con init y norm perfectos, apilar demasiadas capas puede empeorar el resultado — no por overfitting, sino porque el gradiente tiene que atravesar todas las capas para llegar a las primeras, y se degrada en el camino. La solución de Kaiming He et al. (2015, "Deep Residual Learning"): cada bloque no aprende la transformación completa, aprende solo la diferencia respecto a la identidad, sumada al input original:
class ResBlock(nn.Module):
def __init__(self, ni, nf, stride=1):
self.convs = _conv_block(ni, nf, stride) # dos convoluciones
self.idconv = fc.noop if ni==nf else conv(ni, nf, ks=1, act=None)
self.pool = fc.noop if stride==1 else nn.AvgPool2d(2, ceil_mode=True)
def forward(self, x): return self.act(self.convs(x) + self.idconv(self.pool(x)))
La suma self.convs(x) + self.idconv(self.pool(x)) es el "skip connection": si el bloque
completo no aporta nada útil, puede converger a aproximar la función identidad (salida ≈ entrada) en vez
de tener que "recordar" activamente cómo pasar la información sin distorsión — mucho más fácil de
aprender por gradiente que la identidad exacta desde cero. Con ResBlocks + BatchNorm + OneCycleLR, el
notebook llega a resultados que igualan modelos de timm (la librería de arquitecturas de
visión preentrenadas de referencia) entrenados desde cero.
6. Resumen
- Sin cuidado, apilar capas hace que las activaciones exploten a NaN o colapsen a cero — un problema de escala, no de arquitectura.
- Xavier/Glorot deriva la escala correcta sin no-linealidad; Kaiming/He la corrige para ReLU (factor √2 extra).
- LayerNorm y BatchNorm normalizan activamente durante el entrenamiento, no solo al inicio — permiten learning rates más altos.
- LSUV inicializa empíricamente, midiendo activaciones reales en vez de derivar una fórmula.
- ResNets resuelven la degradación de redes muy profundas haciendo que cada bloque aprenda una diferencia respecto a la identidad, no la transformación completa.