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
- Una convolución es multiplicación elemento a elemento + suma de un kernel chico deslizado sobre la imagen — nada más misterioso que eso.
- Padding preserva el tamaño espacial entre capas; stride lo reduce deliberadamente, como alternativa a pooling.
- 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.
- Batch normalization estabiliza el entrenamiento con learning rates altos, normalizando activaciones por mini-batch.
- 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.