Curso / fast.ai / Lección 8
● piloto de formato

fast.ai · Practical Deep Learning for Coders · Lección 8 (última de la Parte 1)

Convoluciones: qué hay realmente dentro de una CNN

Usaste vision_learner con ResNet desde la Lección 1 sin mirar adentro. Esta lección cierra ese círculo: qué es literalmente una convolución, con las manos, sin ninguna abstracción de por medio.

1. Una convolución, con las manos

Una convolución detecta features visuales usando solo multiplicación y suma. Se desliza un kernel chico (típicamente 3×3) sobre cada bloque de la imagen: se multiplica elemento a elemento y se suma el resultado. El kernel de detección de bordes horizontales del libro:

top_edge = tensor([[-1,-1,-1],
                   [ 0, 0, 0],
                   [ 1, 1, 1]]).float()

def apply_kernel(row, col, kernel):
    return (im3_t[row-1:row+2,col-1:col+2] * kernel).sum()

Si la ventana 3×3 se numera fila por fila como a1..a9, este kernel calcula -a1-a2-a3+a7+a8+a9: si la fila de arriba y la de abajo son igual de oscuras, los términos se cancelan (≈0); si abajo es más brillante que arriba, da un número alto. Así el filtro detecta, literalmente, bordes horizontales de oscuro a claro — invirtiendo filas se detecta lo opuesto, y usando columnas en vez de filas se detectan bordes verticales. Repitiendo esto sobre toda la grilla de coordenadas de una imagen se obtiene el mapa de activación completo — sin ningún parámetro aprendido todavía, es puramente el kernel fijo aplicado a mano.

2. Padding y stride

Con una imagen h×w y un kernel ks×ks, solo caben (h-2)×(w-2) ventanas para un kernel 3×3 — la salida se encoge en cada capa y las esquinas nunca se tocan. Padding agrega píxeles extra (normalmente ceros) alrededor de la imagen para preservar el tamaño; con kernel impar, el padding necesario por lado es ks//2. Stride mueve el kernel más de un píxel por paso — stride-2 reduce la resolución espacial a la mitad, una alternativa a usar pooling. El tamaño de salida por dimensión sigue esta fórmula:

(n + 2*pad - ks)//stride + 1

Regla práctica del libro: kernel 3×3 con padding 1 es lo más común; stride-1 para agregar profundidad sin cambiar tamaño, stride-2 para reducir el mapa de activación paso a paso (14×14 → 7×7 → 4×4 → 2×2 → 1×1).

3. Conv2d en PyTorch y receptive fields

def conv(ni, nf, ks=3, act=True):
    res = nn.Conv2d(ni, nf, stride=2, kernel_size=ks, padding=ks//2)
    if act: res = nn.Sequential(res, nn.ReLU())
    return res

El número de canales de entrada y salida define cuántos kernels distintos aprende cada capa: cada canal de salida es un kernel completo de forma in_channels × ks × ks, más un bias propio. Regla práctica: al usar stride-2 (reducir la grilla en un factor de 4), conviene duplicar el número de filtros para no perder capacidad de cómputo neta entre capas.

El receptive field de una activación es el área de la imagen original que influye en su cálculo. Con dos capas stride-2 consecutivas, una sola celda de la segunda capa ya depende de un área 7×7 de la imagen de entrada — más grande que el kernel 3×3 de cada capa individual, porque cada celda de la primera capa ya "resume" su propia ventana. Cuantas más capas stride-2 haya antes de una activación, mayor su receptive field — por eso las capas más profundas necesitan más canales: cubren áreas cada vez más grandes y deben representar conceptos semánticamente más ricos (ojos, pelaje, ruedas), no solo bordes.

4. Imágenes a color y batch normalization

Una imagen color es un tensor rank-3 [canales, filas, columnas]. El kernel deja de ser ks×ks y pasa a tener profundidad ch_in×ks×ks: en cada ventana se multiplica cada canal (R, G, B) por su porción correspondiente del kernel, y los tres resultados se suman en un solo número por ubicación y por filtro de salida. No hace falta nada especial para entrenar con color más allá de que la primera capa tenga 3 canales de entrada.

Entrenar con learning rates altos sin normalizar produce redes inestables — activaciones cerca de cero, ciclos de crecimiento exponencial seguidos de colapso. Batch normalization (Ioffe & Szegedy, 2015) normaliza las activaciones de una capa usando la media y desviación estándar del mini-batch, y agrega dos parámetros entrenables (gamma, beta) para que la red pueda reescalar/desplazar esa salida normalizada según le convenga. En entrenamiento usa estadísticas del batch actual; en validación, un promedio acumulado. Permite usar learning rates mucho más altos y generaliza mejor, gracias a la aleatoriedad extra que introduce entre mini-batches distintos.

5. Una CNN completa desde cero vs. transfer learning

simple_cnn = sequential(
    conv(1 ,4),             #14x14
    conv(4 ,8),             #7x7
    conv(8 ,16),            #4x4
    conv(16,32),            #2x2
    conv(32,2, act=False),  #1x1
    Flatten(),
)
learn = Learner(dls, simple_cnn, loss_func=F.cross_entropy, metrics=accuracy)
learn.fit_one_cycle(2, 0.01)

Entrenando MNIST completo (los 10 dígitos, no solo 3 vs. 7) desde cero con esta CNN chica más batchnorm y fit_one_cycle, el resultado se acerca al de un ResNet18 preentrenado — pero explícitamente no lo iguala. La lección implícita de todo el capítulo: transfer learning con una arquitectura probada (ResNet) sigue siendo superior a una CNN pequeña entrenada desde cero, incluso aplicando todos los trucos de estabilidad vistos acá (batch size, 1cycle, batchnorm). Entender la convolución por dentro no es un ejercicio académico separado de la práctica — es lo que te permite saber por qué vision_learner(dls, resnet34) de la Lección 1 funciona tan bien de entrada.

6. Resumen — cierre de la Parte 1

  1. Una convolución es multiplicación elemento a elemento + suma de un kernel chico deslizado sobre la imagen — nada más misterioso que eso.
  2. Padding preserva el tamaño espacial entre capas; stride lo reduce deliberadamente, como alternativa a pooling.
  3. El receptive field crece con la profundidad — cada capa stride-2 adicional hace que una activación "vea" un área más grande de la imagen original.
  4. Batch normalization estabiliza el entrenamiento con learning rates altos, normalizando activaciones por mini-batch.
  5. Una CNN desde cero con todos los trucos de estabilidad se acerca a un ResNet preentrenado, pero no lo alcanza — transfer learning sigue ganando.

Con esto se cierra la Parte 1 de fast.ai: ocho lecciones que fueron, en espejo, del extremo opuesto al recorrido de HF — acá empezaste con un modelo funcionando y terminaste mirando adentro del mecanismo; en HF empezaste por la mecánica (atención, tokenización) y terminaste con modelos de razonamiento entrenados con RL. Antes de la Parte 2 (fundamentos matemáticos avanzados y difusión, desde matmul hasta Stable Diffusion), queda un bonus pendiente: la lección de ética de datos.