Antes de empezar
Varios ejercicios usan las variables y funciones de la teoría: convolucion2d, pooling2d, contar, sobel_x, prenda, entrenamiento, idx_ent, media, desv, aumentar, cargador_val y la función entrenar. Abre un cuaderno nuevo en Colab, pega y ejecuta en orden los bloques de la ficha Código y después trabaja debajo. Intenta cada ejercicio con lápiz y papel antes de abrir la respuesta: en este tema, hacer las cuentas a mano es lo que fija la idea.
Ejercicios
1. Una convolución a mano
Esta imagen I de 4 × 4 y este filtro K de 3 × 3:
a) Sin padding y con stride 1, ¿de qué tamaño es la salida? Calcula cada casilla a mano.
b) ¿Qué tamaño tendría la salida con padding 1 y stride 1? ¿Y sin padding y con stride 2?
c) Comprueba todo con convolucion2d.
Respuesta
a) Con n = 4 y k = 3, la salida mide (4 − 3) / 1 + 1 = 2, es decir, 2 × 2. El filtro suma la columna izquierda de la ventana y le resta la derecha:
- Arriba a la izquierda (filas 0 a 2, columnas 0 a 2): (1 + 0 + 2) − (0 + 3 + 0) = 0.
- Arriba a la derecha (columnas 1 a 3): (2 + 1 + 1) − (1 + 1 + 0) = 2.
- Abajo a la izquierda (filas 1 a 3, columnas 0 a 2): (0 + 2 + 1) − (3 + 0 + 2) = −2.
- Abajo a la derecha (columnas 1 a 3): (1 + 1 + 0) − (1 + 0 + 1) = 0.
b) Con padding 1: (4 + 2 − 3) / 1 + 1 = 4, la salida es de 4 × 4, como la entrada. Sin padding y con stride 2: ⌊(4 − 3) / 2⌋ + 1 = 0 + 1 = 1, una sola casilla (la de arriba a la izquierda).
imagen_4 = np.array([[1, 2, 0, 1],
[0, 1, 3, 1],
[2, 1, 0, 0],
[1, 0, 2, 1]])
filtro_3 = np.array([[1, 0, -1],
[1, 0, -1],
[1, 0, -1]])
print(convolucion2d(imagen_4, filtro_3))
print(convolucion2d(imagen_4, filtro_3, padding=1).shape)
print(convolucion2d(imagen_4, filtro_3, stride=2))
[[ 0. 2.]
[-2. 0.]]
(4, 4)
[[0.]]
2. Tamaños de salida en cinco capas
Una red recibe fotos en color de 32 × 32, es decir, tensores de forma (N, 3, 32, 32). Predice la forma de la salida de cada capa, una tras otra:
nn.Conv2d(3, 8, kernel_size=5)nn.MaxPool2d(2)nn.Conv2d(8, 16, kernel_size=3, padding=1)nn.Conv2d(16, 16, kernel_size=3, stride=2)nn.MaxPool2d(2)
¿Cuántos números le llegan a la capa lineal si después se aplana? Comprueba con PyTorch pasando un tensor de ceros capa por capa.
Respuesta
- (32 − 5) / 1 + 1 = 28, con 8 filtros: (N, 8, 28, 28).
- El pooling de 2 × 2 divide el lado por dos: (N, 8, 14, 14).
- Con un filtro de 3 y padding 1 el tamaño se conserva: (N, 16, 14, 14).
- ⌊(14 − 3) / 2⌋ + 1 = ⌊5,5⌋ + 1 = 6: (N, 16, 6, 6).
- (N, 16, 3, 3).
Al aplanar quedan 16 × 3 × 3 = 144 números por imagen. Fíjate en que el número de canales lo decide solo out_channels, y el alto y el ancho, solo el filtro, el padding, el stride y el pooling.
capas = nn.Sequential(
nn.Conv2d(3, 8, kernel_size=5),
nn.MaxPool2d(2),
nn.Conv2d(8, 16, kernel_size=3, padding=1),
nn.Conv2d(16, 16, kernel_size=3, stride=2),
nn.MaxPool2d(2),
)
x = torch.zeros(1, 3, 32, 32)
for capa in capas:
x = capa(x)
print(f"{type(capa).__name__:10} -> {tuple(x.shape)}")
print("Números al aplanar:", x.flatten(1).shape[1])
Conv2d -> (1, 8, 28, 28)
MaxPool2d -> (1, 8, 14, 14)
Conv2d -> (1, 16, 14, 14)
Conv2d -> (1, 16, 6, 6)
MaxPool2d -> (1, 16, 3, 3)
Números al aplanar: 144
3. Pooling a mano
Para este mapa de activación A de 4 × 4, calcula a mano el max-pooling y el average-pooling de 2 × 2. Después compruébalo con pooling2d.
Respuesta
Los cuatro bloques de 2 × 2 son {1, 3, 4, 2}, {2, 0, 1, 1}, {0, 1, 2, 2} y {5, 2, 1, 0}.
- Max-pooling: 4, 2, 2 y 5.
- Average-pooling: 10 / 4 = 2,5; 4 / 4 = 1; 5 / 4 = 1,25; 8 / 4 = 2.
mapa_4 = np.array([[1, 3, 2, 0],
[4, 2, 1, 1],
[0, 1, 5, 2],
[2, 2, 1, 0]])
print(pooling2d(mapa_4, modo="max"))
print(pooling2d(mapa_4, modo="promedio"))
[[4 2]
[2 5]]
[[2.5 1. ]
[1.25 2. ]]
4. Contar parámetros: CNN contra MLP
a) Una capa nn.Conv2d(C_ent, C_sal, kernel_size=k) tiene un filtro por canal de salida, y cada filtro tiene k × k pesos por cada canal de entrada, más un sesgo. Escribe la fórmula de su número de parámetros.
b) Usa la fórmula para contar los parámetros de las tres convoluciones del ejercicio 2, más una capa nn.Linear(144, 10) al final.
c) Compáralo con un MLP que recibe la misma foto aplanada (3 × 32 × 32 = 3072 números), con una capa oculta de 64 neuronas y una salida de 10.
d) Comprueba los dos totales con contar.
Respuesta
a) (k · k · C_ent + 1) · C_sal.
b) Las tres convoluciones y la capa lineal:
Conv2d(3, 8, 5): (5 · 5 · 3 + 1) · 8 = 76 · 8 = 608.Conv2d(8, 16, 3): (3 · 3 · 8 + 1) · 16 = 73 · 16 = 1168.Conv2d(16, 16, 3): (3 · 3 · 16 + 1) · 16 = 145 · 16 = 2320.Linear(144, 10): 144 · 10 + 10 = 1450.
En total, 5546 parámetros. Los pooling no tienen parámetros: solo eligen o promedian.
c) El MLP: 3072 · 64 + 64 = 196 672 en la capa oculta y 64 · 10 + 10 = 650 en la salida, en total 197 322, unas 36 veces más que la CNN. Y eso que tiene una sola capa oculta, bastante angosta.
cnn_32 = nn.Sequential(*capas, nn.Flatten(), nn.Linear(144, 10))
mlp_32 = nn.Sequential(nn.Flatten(), nn.Linear(3 * 32 * 32, 64),
nn.ReLU(), nn.Linear(64, 10))
print("CNN:", contar(cnn_32), " MLP:", contar(mlp_32))
CNN: 5546 MLP: 197322
5. Encuentra el error
Este modelo quería ser la CNN de la teoría, pero al pasarle un lote falla. Lee el mensaje de error, explica qué pasó y corrígelo de dos formas distintas.
modelo_roto = nn.Sequential(
nn.Conv2d(1, 16, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Conv2d(16, 32, kernel_size=3, padding=1),
nn.ReLU(),
nn.Flatten(),
nn.Linear(32 * 7 * 7, 10),
)
lote = torch.zeros(64, 1, 28, 28)
print(modelo_roto(lote).shape)
Traceback (most recent call last):
...
RuntimeError: mat1 and mat2 shapes cannot be multiplied (64x6272 and 1568x10)
Respuesta
Falta el segundo nn.MaxPool2d(2). Con un solo pooling, los mapas quedan de 14 × 14 y no de 7 × 7, así que Flatten entrega 32 × 14 × 14 = 6272 números por imagen. La capa lineal espera 1568 (= 32 × 7 × 7) y no puede multiplicar: el mensaje dice exactamente eso, una matriz de 64 × 6272 (el lote) contra una de 1568 × 10 (los pesos).
Las dos correcciones: agregar el nn.MaxPool2d(2) que falta después de la segunda ReLU, o dejar el modelo como está y cambiar la capa final a nn.Linear(32 * 14 * 14, 10). La segunda funciona, pero la capa lineal queda con cuatro veces más pesos.
Para encontrar estos errores, pasa un lote de ceros capa por capa e imprime las formas, como en el ejercicio 2:
x = torch.zeros(64, 1, 28, 28)
for capa in modelo_roto[:-1]:
x = capa(x)
print("Llegan a la capa lineal:", tuple(x.shape))
Llegan a la capa lineal: (64, 6272)
6. Tres filtros de la actividad
a) ¿Qué filtro de 3 × 3 deja la imagen exactamente igual (salvo el borde que se pierde)? Compruébalo con prenda.
b) El filtro de desenfoque tiene los nueve pesos iguales a 1/9. ¿Qué calcula en cada casilla?
c) El filtro de nitidez de la actividad tiene un 5 en el centro, −1 arriba, abajo, a la izquierda y a la derecha, y 0 en las esquinas. ¿Cuánto da sobre una zona pareja, donde todos los píxeles valen lo mismo? ¿Y por qué hace más marcados los bordes?
Respuesta
a) El filtro con un 1 en el centro y 0 en todo lo demás: cada casilla de la salida es el píxel del centro de la ventana. Como sin padding se pierde un píxel en cada borde, la salida es igual a la imagen sin su marco.
b) El promedio de los nueve píxeles de la ventana. Cada píxel se mezcla con sus vecinos y los bordes se difuminan.
c) Sobre una zona pareja de valor v da 5v − 4v = v: la deja igual, porque los pesos suman 1. Cerca de un borde, en cambio, el píxel del centro pesa 5 y se le restan sus vecinos: si el centro es más claro que ellos, queda todavía más claro, y si es más oscuro, más oscuro. La diferencia entre los dos lados del borde crece.
identidad = np.array([[0, 0, 0],
[0, 1, 0],
[0, 0, 0]])
copia = convolucion2d(prenda, identidad)
print(copia.shape, np.allclose(copia, prenda[1:-1, 1:-1]))
nitidez = np.array([[0, -1, 0],
[-1, 5, -1],
[0, -1, 0]])
pareja = np.full((5, 5), 0.6)
print(convolucion2d(pareja, nitidez).round(2))
(26, 26) True
[[0.6 0.6 0.6]
[0.6 0.6 0.6]
[0.6 0.6 0.6]]
7. ¿Qué aumentaciones tienen sentido?
Para cada problema, di si usarías el volteo horizontal, el volteo vertical y una rotación pequeña (de unos 10°), y por qué:
- Reconocer dígitos escritos a mano.
- Reconocer señales de tránsito.
- Clasificar cultivos en fotos tomadas desde un satélite, mirando hacia abajo.
- Reconocer prendas de FashionMNIST.
Respuesta
- Dígitos: volteos no, porque cambian la respuesta (un 2 en espejo no es un 2; un 6 de cabeza parece un 9). Una rotación pequeña sí: la gente escribe un poco inclinado.
- Señales de tránsito: volteos no. Una flecha de «gire a la izquierda» volteada es «gire a la derecha», y el texto de un letrero queda al revés. Una rotación pequeña sí, porque las fotos se toman desde ángulos distintos.
- Cultivos desde un satélite: los dos volteos y cualquier rotación, incluso de 90°. Desde arriba no hay «arriba» ni «abajo»: un campo de soya es soya en cualquier orientación.
- Prendas: volteo horizontal sí (una camisa en espejo sigue siendo una camisa) y una rotación pequeña también; volteo vertical no, porque las fotos del conjunto siempre muestran las prendas derechas y la red nunca va a ver un botín de cabeza.
La pregunta de fondo es siempre la misma: ¿la etiqueta sigue siendo correcta después de transformar la imagen? Si la respuesta es no, esa aumentación enseña algo falso.
Mini-proyecto: una CNN con aumentación
Vas a entrenar una CNN para FashionMNIST con aumentación de datos y a superar al MLP que entrenaste en los temas 18 y 19.
Los datos. Los mismos de la teoría. Si tu computadora o Colab lo aguantan, usa más de 10 000 imágenes de entrenamiento: con las 60 000 cada época tarda unas seis veces más. Para medir de verdad al final, usa las 10 000 imágenes de prueba completas, no solo las 2000 de validación.
Cómo aplicar la aumentación en el entrenamiento. TensorDataset entrega las imágenes tal cual. Para que cada época vea versiones distintas, escribe un Dataset propio que aplique aumentar cada vez que se le pide una imagen. Este es un punto de partida que ya funciona: aumenta primero, sobre la imagen entre 0 y 1, y normaliza después, con la misma media y desviación de la teoría.
from torch.utils.data import Dataset
class ConAumentacion(Dataset):
"""Imágenes de 0 a 1 que se aumentan y normalizan al pedirlas."""
def __init__(self, imagenes, etiquetas, transformacion=None):
self.imagenes = imagenes # (N, 1, 28, 28)
self.etiquetas = etiquetas
self.transformacion = transformacion
def __len__(self):
return len(self.etiquetas)
def __getitem__(self, i):
x = self.imagenes[i]
if self.transformacion is not None:
x = self.transformacion(x)
return (x - media) / desv, self.etiquetas[i]
imagenes_ent = entrenamiento.data[idx_ent].float().unsqueeze(1) / 255
datos_aumentados = ConAumentacion(imagenes_ent,
entrenamiento.targets[idx_ent],
transformacion=aumentar)
cargador_aum = DataLoader(datos_aumentados, batch_size=64, shuffle=True,
generator=torch.Generator().manual_seed(0))
x, y = next(iter(cargador_aum))
print(tuple(x.shape), tuple(y.shape))
(64, 1, 28, 28) (64,)
Pasos.
- La línea de base. Anota la exactitud del mejor MLP de los temas 18 y 19 y la de la CNN de la teoría. Esos son los números a superar.
- Tu CNN. Prueba una CNN un poco más grande que la de la teoría: por ejemplo, 32 y 64 filtros, o un tercer bloque convolución → ReLU → pooling. Cuenta sus parámetros con
contary predice todas las formas antes de entrenar. - Con y sin aumentación. Entrena la misma arquitectura dos veces, con la misma semilla y el mismo número de épocas: una con
cargador_enty otra concargador_aum. Usa la funciónentrenary guarda los dos historiales. - Compara las curvas. Dibuja la exactitud de entrenamiento y la de validación de las dos corridas. ¿Cuál tiene más distancia entre entrenamiento y validación? ¿Cuál mejora más en las últimas épocas?
- Prueba otras aumentaciones. Quita el ruido, o cambia el padding del recorte, o agrega
transforms.RandomRotation(10). Cambia una sola cosa a la vez y anota el efecto en una tabla. - Mide al final. Con la mejor versión, mide la exactitud sobre las 10 000 imágenes de prueba. Hazlo una sola vez, al final, para que ese número sea honesto.
- Mira por dentro. Dibuja los filtros de la primera capa de tu mejor red y los mapas de activación de una prenda que haya clasificado mal. ¿Se parecen a los de la teoría?
- Concluye. Escribe tres conclusiones de una o dos frases: ¿cuánto mejoró la CNN al MLP? ¿Ayudó la aumentación, y a partir de qué época se notó? ¿Qué prendas confunde todavía tu red? (La matriz de confusión del tema 18 ayuda con esta última.)
Una pista: la aumentación hace el entrenamiento más difícil a propósito. Con pocas épocas es normal que una red con aumentación vaya por detrás de la misma red sin ella; la ventaja, si la hay, aparece cuando la red sin aumentación empieza a sobreajustar. Por eso conviene comparar las curvas completas y no solo el número final.
Errores comunes
- Olvidar el eje del canal.
nn.Conv2despera (N, C, H, W). Un lote de imágenes en gris de forma (N, 28, 28) necesitaunsqueeze(1)para volverse (N, 1, 28, 28). - Poner los canales al final. Las imágenes de NumPy, matplotlib y PIL suelen venir como (H, W, C); PyTorch las quiere como (C, H, W). Se arregla con
permute(2, 0, 1), o contransforms.ToTensor(), que además divide por 255. - Calcular mal el tamaño que llega a la capa lineal. Si te sale un error de «shapes cannot be multiplied», pasa un tensor de ceros capa por capa e imprime las formas (ejercicio 5).
- Confundir canales con filtros.
in_channelses cuántos mapas entran;out_channelses cuántos filtros tiene la capa y cuántos mapas salen. Elin_channelsde una capa tiene que ser igual alout_channelsde la anterior. - Aumentar la validación o la prueba. La aumentación es solo para entrenar. Si transformas también los datos con que mides, los números dejan de ser comparables.
- Aumentaciones que cambian la etiqueta. Voltear dígitos, señales con flechas o textos enseña a la red algo falso (ejercicio 7).
- Normalizar con la media de todos los datos. La media y la desviación se calculan con el entrenamiento y se usan igual en la validación y la prueba, como con cualquier otra transformación (tema 10).
- Comparar redes de distinto tamaño o con distinto entrenamiento. Para decir que una arquitectura es mejor, hay que igualar los parámetros, los datos, el optimizador y las épocas, como hicimos con la CNN y el MLP.
¿Lo lograste?
Al terminar este tema deberías poder hacer lo siguiente. Al lado, dónde lo practicaste.
- Calcular a mano una convolución 2D pequeña y el tamaño de su salida con padding y stride (ejercicios 1 y 2, y la actividad).
- Explicar el max-pooling y el average-pooling y por qué dan cierta tolerancia a movimientos pequeños (ejercicio 3 y el paso 5 de la actividad).
- Construir y entrenar una CNN en PyTorch y compararla con un MLP de forma justa (ejercicios 4 y 5, y pasos 2 a 4 del mini-proyecto).
- Aplicar aumentación de imágenes con
torchvision.transformsy elegir las que tienen sentido (ejercicio 7 y pasos 3 y 5 del mini-proyecto). - Visualizar los filtros y los mapas de activación de una red (paso 7 del mini-proyecto, con el código de la teoría).