Generación de imágenes — Módulo 2: la convolución — filtros que detectan patrones
Segundo módulo del curso de generación de imágenes: el corazón de la CNN. Qué es un filtro o kernel, cómo se desliza sobre la imagen para producir un feature map, por qué compartir pesos da pocos parámetros e invariancia a traslación, y qué hacen el stride y el padding.
☰ Índice del contenido
En el Módulo 1 quedó planteado el problema: una red totalmente conectada tiene demasiados parámetros para una imagen y, peor, ignora que los píxeles vecinos forman patrones. Cerramos con una intuición: hay que mirar la imagen por parches locales y reutilizar el mismo detector en toda ella. La operación que hace exactamente eso tiene nombre propio y es el corazón de toda la visión moderna: la convolución.
La tesis del módulo
La convolución es un detector de patrones que recorre la imagen. Un filtro pequeño —una matriz de pocos números— se desliza por toda la foto buscando un patrón (un borde, una textura) y anota dónde lo encontró. Con eso resuelve de un golpe los dos problemas de la red densa: usa poquísimos parámetros y respeta la estructura espacial de la imagen.
Qué es un filtro (kernel)
Un filtro o kernel (los dos nombres son lo mismo: una matriz chica de pesos, típicamente 3×3 o 5×5) es un detector de un patrón concreto. Lo apoyas sobre un parche de la imagen del mismo tamaño y calculas el producto punto: multiplicas cada número del filtro por el píxel que tiene debajo y sumas todo en un solo número.
Ese número dice cuánto se parece el parche al patrón que el filtro busca. Alto = “aquí está el patrón”; cerca de cero = “aquí no hay nada de eso”.
Un ejemplo clásico: este kernel detecta bordes verticales (dónde el brillo cambia bruscamente de izquierda a derecha).
kernel "borde vertical" (3x3)
-1 0 +1
-1 0 +1
-1 0 +1
Donde a la izquierda hay oscuro y a la derecha claro, la suma da un número grande. Donde todo es parejo, los -1 y +1 se cancelan y da ~0. Así de simple: un puñado de números que resalta un tipo de contorno.
El deslizamiento: de la imagen al feature map
Un filtro solo mira un parche. La gracia es que lo deslizamos por toda la imagen: lo apoyamos arriba a la izquierda, calculamos el producto punto, lo corremos un píxel a la derecha, repetimos… hasta cubrir todo. Cada resultado se guarda en una nueva grilla.
Esa grilla de salida se llama feature map (mapa de activación: una imagen donde cada punto dice cuánto se activó el filtro en esa posición). Un mapa de bordes verticales se ve como la foto original con los contornos verticales iluminados y el resto en negro.
imagen 5x5 filtro 3x3 feature map 3x3
(dónde se activó)
┌─┬─┬─┬─┬─┐
│ │ │ │ │ │
├─┼─┼─┼─┼─┤ ┌─┬─┬─┐ ┌─┬─┬─┐
│ │▓│▓│▓│ │ │ │ │ │ │ │ │ │
├─┼─┼─┼─┼─┤ ├─┼─┼─┤ ├─┼─┼─┤
│ │▓│▓│▓│ │ ─────► │ │ │ │ ─────► │ │ │ │
├─┼─┼─┼─┼─┤ ├─┼─┼─┤ ├─┼─┼─┤
│ │▓│▓│▓│ │ │ │ │ │ │ │ │ │
└─┴─┴─┴─┴─┘ └─┴─┴─┘ └─┴─┴─┘
▓ = parche bajo el filtro ► producto punto
Un ejemplo numérico chico
Hagamos una casilla a mano. Tomamos un parche 3×3 de la imagen y el kernel de borde vertical. El resultado de esa posición es la suma de los productos:
parche imagen kernel producto
┌────┬────┬────┐ ┌────┬────┬────┐
│ 10 │ 10 │ 90 │ │ -1 │ 0 │ +1 │ 10*(-1)=-10
├────┼────┼────┤ ├────┼────┼────┤ 90*(+1)=+90
│ 10 │ 10 │ 90 │ x │ -1 │ 0 │ +1 │ 10*(-1)=-10
├────┼────┼────┤ ├────┼────┼────┤ 90*(+1)=+90
│ 10 │ 10 │ 90 │ │ -1 │ 0 │ +1 │ 10*(-1)=-10
└────┴────┴────┘ └────┴────┴────┘ 90*(+1)=+90
suma = +240
Los píxeles del medio (columna con 0) no cuentan; la columna oscura (10) resta y la clara (90) suma. Resultado alto y positivo: aquí hay un borde vertical claro-a-la-derecha. Si el parche fuera todo parejo (10, 10, 10), la suma daría exactamente 0.
Las tres ideas clave
1. Campo receptivo local. Cada valor del feature map depende solo del parche que tenía debajo (un vecindario chico), no de toda la imagen. Eso se llama campo receptivo (receptive field): la región de la entrada que “ve” una neurona de salida. Al apilar capas (módulo 3), ese campo crece y la red pasa de ver detalles a ver objetos completos.
2. Compartir pesos (weight sharing). El mismo filtro —los mismos 9 números— se usa en todas las posiciones de la imagen. Esto es weight sharing (compartir pesos) y tiene dos consecuencias enormes:
- Poquísimos parámetros. Un filtro 3×3 sobre una imagen RGB son 3×3×3 = 27 pesos. Comparado con los 150 millones de la red densa del módulo 1, es otra galaxia.
- Invariancia a traslación. Como el mismo detector recorre toda la imagen, un borde se detecta igual esté arriba o abajo. La red no tiene que reaprender el patrón en cada posición.
3. Los filtros se aprenden solos. El kernel de borde vertical lo escribimos a mano para ilustrar, pero en una CNN real nadie los diseña: arrancan con números aleatorios y el entrenamiento (módulo 4) va ajustando sus pesos hasta que detectan lo útil — bordes, texturas, colores. La red descubre por su cuenta qué vale la pena mirar.
Ojo: una capa convolucional no tiene un solo filtro, tiene muchos (32, 64, 128…). Cada uno detecta un patrón distinto y produce su propio feature map. Una capa con 64 filtros saca 64 mapas apilados — un tensor de salida con 64 canales.
Stride y padding: controlando el deslizamiento
Dos perillas gobiernan cómo se desliza el filtro:
Stride (paso): cuántos píxeles avanza el filtro en cada salto. Stride 1 = pisa todas las posiciones (salida casi del tamaño de la entrada). Stride 2 = salta de a dos, produce un mapa a la mitad de resolución. Sirve para achicar.
Padding (relleno): la convolución encoge la imagen (un filtro 3×3 con stride 1 recorta 1 píxel por lado, porque el filtro no puede salirse del borde). El padding rellena la orilla con ceros para que la salida conserve el tamaño de la entrada. padding=1 con un kernel 3×3 mantiene el alto y ancho intactos.
| Perilla | Valor | Efecto en la salida |
|---|---|---|
| Kernel size | 3×3 | Estándar: campo pequeño, pocos pesos |
| Kernel size | 5×5 / 7×7 | Campo más amplio, más pesos por filtro |
| Stride | 1 | Salida del mismo alto/ancho (con padding) |
| Stride | 2 | Salida a la mitad de resolución (submuestreo) |
| Padding | 0 (valid) | La imagen encoge en los bordes |
| Padding | 1 (con 3×3) | La imagen conserva su tamaño (same) |
En código
Primero la convolución “a mano” con numpy, para ver que no hay magia; después la versión real de PyTorch, que es la que usarás.
import numpy as np
def convolucion_2d(imagen, kernel):
"""Convolución simple, un solo filtro, stride 1, sin padding."""
kh, kw = kernel.shape
ih, iw = imagen.shape
# La salida encoge: cabe (ih-kh+1) x (iw-kw+1) posiciones
salida = np.zeros((ih - kh + 1, iw - kw + 1))
for i in range(salida.shape[0]):
for j in range(salida.shape[1]):
parche = imagen[i:i+kh, j:j+kw] # el trozo bajo el filtro
salida[i, j] = np.sum(parche * kernel) # producto punto
return salida
imagen = np.array([[10, 10, 90, 90],
[10, 10, 90, 90],
[10, 10, 90, 90],
[10, 10, 90, 90]], dtype="float32")
kernel_borde = np.array([[-1, 0, 1],
[-1, 0, 1],
[-1, 0, 1]], dtype="float32")
print(convolucion_2d(imagen, kernel_borde))
# El borde (salto 10 -> 90) se ilumina con valores altos (+240)
import torch
import torch.nn as nn
# En PyTorch la convolución es una capa: Conv2d(canales_entrada,
# canales_salida = nº de filtros, tamaño del kernel).
# in=3 (RGB), out=64 filtros, kernel 3x3, con padding para no encoger.
capa = nn.Conv2d(in_channels=3, out_channels=64,
kernel_size=3, stride=1, padding=1)
# Un batch de 1 imagen RGB de 224x224. PyTorch usa (N, C, H, W).
x = torch.randn(1, 3, 224, 224)
y = capa(x)
print(y.shape) # torch.Size([1, 64, 224, 224]) -> 64 feature maps
# Parámetros aprendidos: 3*3*3*64 pesos + 64 bias = 1.792 en total
print(sum(p.numel() for p in capa.parameters())) # 1792
Fíjate en el número final: 1.792 parámetros para una capa que procesa una imagen completa con 64 detectores distintos. La red densa del módulo 1 gastaba 150 millones. Ese es el poder de compartir pesos.
Errores comunes
- Confundir kernel con feature map. El kernel es el detector (los pesos que se aprenden); el feature map es el resultado (dónde se activó). Uno es la pregunta, el otro la respuesta.
- Creer que hay que diseñar los filtros. En una CNN real se aprenden solos durante el entrenamiento. Los kernels escritos a mano son solo para entender la idea.
- Olvidar el padding y no cuadrar los tamaños. Sin padding la imagen encoge en cada capa; es una causa típica de errores de forma al apilar capas.
- Pensar que una capa tiene un filtro. Tiene decenas o cientos, y cada uno saca su propio mapa. La salida es un tensor con tantos canales como filtros.
- Ignorar el stride al calcular tamaños. Stride 2 reduce la salida a la mitad; si no lo consideras, las dimensiones no calzan más adelante.
La frase para llevarse
“Un filtro es una pregunta chica —‘¿hay un borde aquí?’— que la convolución hace en cada rincón de la imagen usando siempre los mismos pocos números. Ahí está todo el ahorro y toda la potencia.”
Qué viene
Módulo 3: la arquitectura CNN. Un filtro detecta bordes; pero, ¿cómo se pasa de bordes a reconocer un gato? Apilando capas convolucionales se forma una jerarquía: bordes → partes → objetos. Veremos el pooling, la CNN completa para clasificar y las arquitecturas clásicas (LeNet, AlexNet, VGG, ResNet).
Material relacionado
Preguntas sobre este curso
Regístrate gratis para hacer preguntas y guardar tu progreso.
✉ Entérate de lo nuevo
Recibe un aviso cuando publique nuevos cursos, workshops o investigaciones. Sin spam.