Módulo 7 · Visión por computadora
Convoluciones y clasificación
En esta páginaEl problema
El problema
En Misión de entrenamiento, un MLP 64 → 64 → 10 con dropout 0.2 clasificó los dígitos 8 × 8 de load_digits. Con las semillas 0, 1 y 2 falló 9, 7 y 9 de las 360 imágenes de validación.
Ahora cada imagen de validación se desplaza una columna a la derecha. La columna izquierda se rellena con ceros. Este es el 0 de la segunda imagen de validación, antes y después del desplazamiento:
original desplazada
0 0 10 15 6 0 0 0 0 0 0 10 15 6 0 0
0 2 16 14 16 7 0 0 0 0 2 16 14 16 7 0
0 4 16 8 5 16 1 0 0 0 4 16 8 5 16 1
0 5 12 0 0 12 8 0 0 0 5 12 0 0 12 8
0 8 9 0 0 12 8 0 0 0 8 9 0 0 12 8
0 5 12 0 1 15 3 0 0 0 5 12 0 1 15 3
0 4 13 4 12 13 0 0 0 0 4 13 4 12 13 0
0 0 9 16 13 4 0 0 0 0 0 9 16 13 4 0
Una persona reconoce el mismo 0. Los mismos tres MLP, entrenados solo con imágenes originales, fallan 183, 194 y 200 de las 360 imágenes desplazadas. Aplanada, la imagen es una lista de 64 características, y el píxel (3, 2) ocupa la posición 26. Después del desplazamiento, ese valor pasa a la posición 27, que el MLP multiplica por otros pesos. Nada en el MLP indica que las posiciones 26 y 27 son vecinas en la imagen.
Imagen como tensor
PyTorch guarda una imagen como un tensor (C, H, W): canales, alto y ancho. Una imagen en escala de grises tiene un canal. Una imagen en color RGB tiene tres: rojo, verde y azul. Un batch añade un eje al inicio: (N, C, H, W).
load_digits ofrece los mismos datos en dos formas. images conserva cada imagen como una matriz 8 × 8. data guarda cada imagen aplanada fila por fila. unsqueeze(i) añade un eje de tamaño 1 en la posición i. np.ravel_multi_index convierte un índice (fila, columna) de una matriz en su posición dentro del vector aplanado.
Pregunta. Escribe la forma de digitos.images, la de digitos.data y la de imagenes después de unsqueeze(1). El píxel de la fila 2 y la columna 5 de la primera imagen, ¿en qué posición de digitos.data[0] aparece?
import numpy as np
import torch
from torch import nn
from sklearn.datasets import load_digits
digitos = load_digits()
print(digitos.images.shape)
print(digitos.data.shape)
# Los píxeles van de 0 a 16; se escalan a [0, 1].
imagenes = torch.tensor(digitos.images / 16.0, dtype=torch.float32).unsqueeze(1)
print(imagenes.shape)
posicion = np.ravel_multi_index((2, 5), (8, 8))
print(posicion, digitos.images[0][2, 5], digitos.data[0][posicion])
La salida esperada está oculta.
NumPy y Matplotlib suelen guardar imágenes en color como (H, W, C). Para pasar a (C, H, W) hay que reordenar ejes, no solo cambiar la forma. La imagen siguiente mide 2 × 3 píxeles. La fila superior es rojo, rojo y azul. La inferior es rojo, verde y azul.
Pregunta. permute(2, 0, 1) y reshape(3, 2, 3) producen la misma forma. ¿Cuál de los dos deja en el canal 0 los valores del rojo de cada píxel?
rgb_hwc = torch.tensor(
[
[[255, 0, 0], [255, 0, 0], [0, 0, 255]],
[[255, 0, 0], [0, 255, 0], [0, 0, 255]],
]
)
chw = rgb_hwc.permute(2, 0, 1)
mezclado = rgb_hwc.reshape(3, 2, 3)
print(rgb_hwc.shape, chw.shape, mezclado.shape)
print(chw[0])
print(mezclado[0])
La salida esperada está oculta.
Convolución a mano
Una convolución desliza un filtro pequeño sobre la imagen. En cada posición multiplica el filtro por la ventana de píxeles que cubre y suma los productos. El resultado es un mapa de activación: un número por posición.
El filtro de abajo resta la columna izquierda de la ventana y suma la columna derecha. La imagen de 5 × 5 tiene un trazo vertical en la columna 2, de la fila 1 a la 3.
imagen = torch.zeros(5, 5)
imagen[1:4, 2] = 1.0 # filas 1 a 3 de la columna 2
filtro = torch.tensor([[-1.0, 0.0, 1.0]] * 3) # tres filas iguales
print(imagen)
La salida esperada está oculta.
convolucion_a_mano recorre con dos bucles todas las posiciones donde cabe la ventana completa. En cada una, ventana * filtro multiplica elemento a elemento y .sum() suma los nueve productos.
Pregunta. Sin un borde que rellene la imagen, ¿cuántas posiciones caben para una ventana 3 × 3 en cada eje? Calcula a mano la salida en (1, 0), (1, 1) y (1, 2).
def convolucion_a_mano(imagen, filtro):
k = filtro.shape[0]
alto = imagen.shape[0] - k + 1
ancho = imagen.shape[1] - k + 1
salida = torch.zeros(alto, ancho)
for i in range(alto):
for j in range(ancho):
ventana = imagen[i : i + k, j : j + k]
salida[i, j] = (ventana * filtro).sum()
return salida
print(convolucion_a_mano(imagen, filtro))
La salida esperada está oculta.
nn.Conv2d es la capa de PyTorch para esta operación. Sus pesos tienen forma (canales de salida, canales de entrada, alto, ancho) y su entrada es un batch (N, C, H, W) o una sola imagen (C, H, W). PyTorch no invierte el filtro antes de deslizarlo. En matemáticas, esa operación se llama correlación cruzada, pero en redes neuronales se le llama convolución.
La función auxiliar desplazar mueve el contenido una columna a la derecha y rellena con ceros, como en el problema.
Mostrar códigoOcultar código
def desplazar(x):
salida = torch.zeros_like(x)
salida[..., 1:] = x[..., :-1]
return salida
bias=False quita el sesgo, así que la capa solo suma productos. weight.copy_ sobrescribe los pesos aleatorios con el filtro. Va dentro de torch.no_grad() porque los pesos son parámetros que registran gradientes. .detach() separa la salida del grafo para imprimirla sin grad_fn. torch.equal devuelve True solo si dos tensores tienen la misma forma y los mismos valores.
Pregunta. ¿nn.Conv2d con el mismo filtro da exactamente la misma salida? Si el trazo se mueve a la columna 3, ¿qué valores quedan en la fila 1 de la salida?
conv = nn.Conv2d(1, 1, kernel_size=3, bias=False)
with torch.no_grad():
conv.weight.copy_(filtro.reshape(1, 1, 3, 3)) # (salida, entrada, alto, ancho)
entrada = imagen.reshape(1, 1, 5, 5) # batch de una imagen de un canal
print(conv.weight.shape)
print(conv(entrada).shape)
print(torch.equal(conv(entrada)[0, 0], convolucion_a_mano(imagen, filtro)))
desplazada = desplazar(entrada)
print(conv(desplazada)[0, 0].detach())
La salida esperada está oculta.
Tamaño de salida
El padding añade un borde de ceros alrededor de la imagen antes de deslizar el filtro. Con padding=1, la imagen 5 × 5 se convierte en 7 × 7 y las ventanas también se centran en los píxeles del borde. La capa conv_padding copia el filtro de conv.
Pregunta. Con padding=1, ¿qué forma tiene la salida para la imagen desplazada? ¿Aparece la respuesta negativa del borde derecho del trazo?
conv_padding = nn.Conv2d(1, 1, kernel_size=3, padding=1, bias=False)
with torch.no_grad():
conv_padding.weight.copy_(conv.weight)
print(conv_padding(desplazada)[0, 0].detach())
La salida esperada está oculta.
El stride es el salto entre dos posiciones consecutivas del filtro. Con stride=2, la ventana avanza de dos en dos y la salida tiene aproximadamente la mitad de posiciones por eje. Para una entrada de tamaño n, un filtro k, padding p y stride s, el tamaño de salida en cada eje es:
salida = ⌊(n + 2p − k) / s⌋ + 1
⌊ ⌋ redondea hacia abajo. En Python, la fórmula se escribe con división entera: (n + 2 * p - k) // s + 1. En nn.Conv2d, el stride se pasa como argumento junto al tamaño del filtro y el padding:
nn.Conv2d(8, 16, kernel_size=3, padding=1, stride=2) # la ventana avanza de dos en dos
La tabla siguiente compara la fórmula con la forma que produce nn.Conv2d sobre una entrada (1, 1, 8, 8).
Pregunta. Para una entrada 8 × 8, calcula la salida de las cinco combinaciones (k, p, s): (3, 0, 1), (3, 1, 1), (3, 1, 2), (5, 0, 1) y (2, 0, 2).
Ver resultado
Cada fila es una combinación para una entrada 8 × 8. k, p y s son el tamaño del filtro, el padding y el stride. La columna «fórmula» da el tamaño por eje según ⌊(n + 2p − k) / s⌋ + 1, y la última da el alto y el ancho de la salida de nn.Conv2d. La fila 3, 1, 2 dice que la fórmula da 4 y que nn.Conv2d produce una salida de (4, 4).
| k | p | s | fórmula | alto y ancho con nn.Conv2d |
|---|---|---|---|---|
| 3 | 0 | 1 | 6 | (6, 6) |
| 3 | 1 | 1 | 8 | (8, 8) |
| 3 | 1 | 2 | 4 | (4, 4) |
| 5 | 0 | 1 | 4 | (4, 4) |
| 2 | 0 | 2 | 4 | (4, 4) |
nn.Conv2d en las cinco combinaciones. Con (3, 1, 2) hay 4 posiciones porque ⌊(8 + 2 − 3) / 2⌋ + 1 = 3 + 1. La división entera descarta la última ventana cuando no cabe completa. La fórmula no depende de los valores de la imagen ni del número de canales.Ventana de convolución
Avanza la ventana paso a paso y compara los nueve productos con la celda que se llena en la salida.
Qué probar
Cambia el filtro o haz clic en un píxel de la entrada y observa qué celdas de la salida se marcan.
Editar pesos
Tamaño de salida por eje⌊(8 + 2·0 − 3) / 1⌋ + 1 = 6
Haz clic en un píxel de la entrada para cambiarlo entre 0 y 1. Con el teclado, las flechas pasan de un píxel a otro y Enter lo cambia.
El pooling reduce el tamaño de un mapa de activación. nn.MaxPool2d(2) divide el mapa en bloques 2 × 2 sin solapamiento y conserva el máximo de cada bloque. nn.AvgPool2d(2) conserva el promedio. Los dos usan kernel_size=2 y stride=2, así que la fórmula anterior también da su tamaño de salida. parameters() recorre los tensores de pesos de una capa y numel() cuenta los elementos de cada uno.
Pregunta. Escribe la salida 2 × 2 de cada pooling para este mapa 4 × 4. ¿Cuántos parámetros tiene nn.MaxPool2d(2)?
mapa = torch.tensor(
[
[1.0, 3.0, 0.0, 2.0],
[4.0, 2.0, 1.0, 0.0],
[0.0, 1.0, 5.0, 2.0],
[2.0, 0.0, 3.0, 1.0],
]
).reshape(1, 1, 4, 4)
max_pool = nn.MaxPool2d(2)
print(max_pool(mapa)[0, 0])
print(nn.AvgPool2d(2)(mapa)[0, 0])
print(sum(p.numel() for p in max_pool.parameters()))
La salida esperada está oculta.
CNN sobre dígitos
Una CNN (red neuronal convolucional) apila capas convolucionales, activaciones y pooling, y termina con una capa densa que produce los logits. La partición es la misma de Misión de entrenamiento: 360 imágenes de prueba y, del resto, 360 de validación, con stratify y random_state=0. La comparación usa validación. La prueba queda reservada, como en Misión de entrenamiento.
Mostrar códigoOcultar código
from sklearn.model_selection import train_test_split
from torch.utils.data import DataLoader, TensorDataset
X = digitos.data / 16.0
y = digitos.target
X_resto, X_prueba, y_resto, y_prueba = train_test_split(
X, y, test_size=360, stratify=y, random_state=0
)
X_ent, X_val, y_ent, y_val = train_test_split(
X_resto, y_resto, test_size=360, stratify=y_resto, random_state=0
)
X_ent_t = torch.tensor(X_ent, dtype=torch.float32).reshape(-1, 1, 8, 8)
X_val_t = torch.tensor(X_val, dtype=torch.float32).reshape(-1, 1, 8, 8)
y_ent_t = torch.tensor(y_ent)
y_val_t = torch.tensor(y_val)
X_val_desplazada = desplazar(X_val_t)
X_ent_t tiene forma (1077, 1, 8, 8). X_val_t y X_val_desplazada tienen forma (360, 1, 8, 8). Son 1077 imágenes de entrenamiento y 360 de validación, con píxeles entre 0 y 1. X_val_desplazada contiene las mismas 360 imágenes de validación desplazadas una columna con desplazar.
crear_cnn usa dos bloques convolución–ReLU–pooling. La primera convolución produce 8 mapas y la segunda 16. nn.Flatten aplana los mapas finales antes de la capa densa.
def crear_cnn(semilla):
torch.manual_seed(semilla) # mismos pesos iniciales para la misma semilla
return nn.Sequential(
nn.Conv2d(1, 8, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Conv2d(8, 16, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Flatten(),
nn.Linear(16 * 2 * 2, 10),
)
contar_parametros suma los elementos de todos los tensores de pesos de un modelo. Un nn.Sequential se puede recorrer capa por capa, y type(capa).__name__ da el nombre de la clase de cada capa.
Pregunta. Con la fórmula de tamaño de salida, escribe la forma después de cada capa para un batch de una imagen (1, 1, 8, 8). Calcula los parámetros de la primera convolución: 8 filtros de 1 × 3 × 3 más un sesgo por filtro.
def contar_parametros(modelo):
return sum(p.numel() for p in modelo.parameters())
h = X_val_t[:1] # batch de una imagen: (1, 1, 8, 8)
print(f"{'entrada':<10}{tuple(h.shape)}")
for capa in crear_cnn(0):
h = capa(h)
print(f"{type(capa).__name__:<10}{str(tuple(h.shape)):<16}{contar_parametros(capa)}")
La salida esperada está oculta.
El MLP es el de Misión de entrenamiento, con nn.Flatten al inicio para que reciba la misma entrada (N, 1, 8, 8).
def crear_mlp(semilla):
torch.manual_seed(semilla)
return nn.Sequential(
nn.Flatten(),
nn.Linear(64, 64),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(64, 10),
)
Las funciones auxiliares siguientes repiten Misión de entrenamiento. entrenar minimiza perdida_fn, la entropía cruzada, con Adam, lr=0.01, batches de 32 y 30 épocas. errores compara el argmax de los logits con la etiqueta y cuenta las imágenes mal clasificadas.
Mostrar códigoOcultar código
perdida_fn = nn.CrossEntropyLoss()
def entrenar(modelo, semilla, epocas=30):
generador = torch.Generator().manual_seed(semilla)
cargador = DataLoader(
TensorDataset(X_ent_t, y_ent_t),
batch_size=32,
shuffle=True,
generator=generador,
)
optimizador = torch.optim.Adam(modelo.parameters(), lr=0.01)
for _ in range(epocas):
modelo.train()
for xb, yb in cargador:
optimizador.zero_grad()
perdida_fn(modelo(xb), yb).backward()
optimizador.step()
modelo.eval()
return modelo
def errores(modelo, X_t, y_t):
with torch.no_grad():
return (modelo(X_t).argmax(dim=1) != y_t).sum().item()
Cada modelo se entrena con las semillas 0, 1 y 2, y se cuentan sus errores en la validación original y en la desplazada. Cada error de la tabla sale de dos llamadas como estas:
modelo = entrenar(crear_cnn(semilla), semilla)
errores(modelo, X_val_desplazada, y_val_t) # o X_val_t para las originales
Pregunta. ¿Qué modelo tiene más parámetros? ¿La CNN comete menos errores que el MLP en las imágenes originales? ¿Y en las desplazadas?
Ver resultado
Cada fila es un modelo entrenado con una semilla. «parámetros» cuenta sus pesos y sesgos. Las dos últimas columnas cuentan las imágenes mal clasificadas de las 360 de validación, sin desplazar y desplazadas una columna. La fila CNN 1 dice que la CNN entrenada con la semilla 1 tiene 1898 parámetros, falla 12 imágenes originales y falla 97 desplazadas.
| modelo | semilla | parámetros | errores originales | errores desplazadas |
|---|---|---|---|---|
| MLP | 0 | 4810 | 9 | 183 |
| MLP | 1 | 4810 | 7 | 194 |
| MLP | 2 | 4810 | 9 | 200 |
| CNN | 0 | 1898 | 10 | 149 |
| CNN | 1 | 1898 | 12 | 97 |
| CNN | 2 | 1898 | 8 | 128 |
Bloque residual
Apilar más capas convolucionales permite detectar patrones más grandes y más compuestos. En la práctica, las redes profundas sin atajos entrenaban peor. En 2015, He y coautores reportaron que una red simple de 56 capas tenía más error de entrenamiento que una de 20 en CIFAR-10. Una red más profunda puede representar lo mismo que una menos profunda: bastaría que las capas extra copiaran su entrada. El optimizador no encontraba esa solución.
Su propuesta, ResNet, suma la entrada de cada bloque a su salida: salida = ReLU(x + F(x)). En ResNet-18 y ResNet-34, F son dos convoluciones 3 × 3. Esa suma es una conexión residual. Los bloques de ResNet también llevan BatchNorm después de cada convolución. ResNet-18, ResNet-50 y ResNet-152 son versiones con 18, 50 y 152 capas.
Para sumar x y F(x), las dos formas deben coincidir. Con padding=1, stride=1 y el mismo número de canales de entrada y de salida, cada convolución conserva la forma.
BloqueResidual hereda de nn.Module. __init__ declara las dos convoluciones, residuo calcula F(x) y forward define la salida del bloque.
class BloqueResidual(nn.Module):
def __init__(self, canales):
super().__init__()
self.conv1 = nn.Conv2d(canales, canales, kernel_size=3, padding=1)
self.conv2 = nn.Conv2d(canales, canales, kernel_size=3, padding=1)
def residuo(self, x):
return self.conv2(torch.relu(self.conv1(x)))
def forward(self, x):
return torch.relu(x + self.residuo(x))
La prueba usa BloqueResidual(1), creado con semilla 0, y las 4 primeras imágenes de dígitos, x = imagenes[:4]. torch.equal(bloque(x), x) compara la salida con la entrada, primero con los pesos aleatorios y después con los pesos y el sesgo de conv2 en cero. zero_() pone en cero un tensor sin crear otro:
with torch.no_grad():
bloque.conv2.weight.zero_()
bloque.conv2.bias.zero_()
Pregunta. Si los pesos y el sesgo de conv2 valen cero, ¿qué devuelve el bloque para una imagen de dígitos? ¿Y con los pesos iniciales aleatorios?
Ver resultado
Cada fila es un estado de conv2. La segunda columna da la forma de la salida del bloque y la tercera dice si la salida es idéntica a la entrada. La fila «aleatorios» dice que, con los pesos iniciales, la salida tiene forma (4, 1, 8, 8) y no es igual a x.
pesos y sesgo de conv2 | forma de bloque(x) | torch.equal(bloque(x), x) |
|---|---|---|
| aleatorios | (4, 1, 8, 8) | False |
| en cero | (4, 1, 8, 8) | True |
(4, 1, 8, 8). Con pesos aleatorios, la salida difiere de la entrada. Con conv2 en cero, F(x) = 0 y el bloque devuelve ReLU(x). Los píxeles de los dígitos son no negativos, así que ReLU(x) = x. Añadir un bloque residual con residuo cero no cambia lo que la red ya calculaba.crear_profunda construye una red con la misma entrada y la misma salida para cualquier tipo de bloque: una convolución inicial de 8 canales, n bloques y una capa densa. BloquePlano hereda de BloqueResidual y solo reemplaza forward: es el mismo bloque sin la suma de x.
class BloquePlano(BloqueResidual):
def forward(self, x):
return torch.relu(self.residuo(x))
def crear_profunda(bloque, n):
torch.manual_seed(0)
return nn.Sequential(
nn.Conv2d(1, 8, kernel_size=3, padding=1),
nn.ReLU(),
*[bloque(8) for _ in range(n)], # n bloques sueltos dentro de Sequential
nn.Flatten(),
nn.Linear(8 * 8 * 8, 10), # 8 mapas de 8 × 8, sin pooling
)
Para cada red se calcula la pérdida en 64 imágenes de entrenamiento. Después de backward(), red[0].weight.grad guarda el gradiente de la primera convolución y norm() mide su tamaño:
red = crear_profunda(BloquePlano, 10)
perdida_fn(red(X_ent_t[:64]), y_ent_t[:64]).backward()
red[0].weight.grad.norm() # primera convolución
Pregunta. Con 1, 5 y 10 bloques, ¿qué red conserva un gradiente de tamaño parecido en la primera capa?
Ver resultado
La tabla da la norma del gradiente de la primera convolución. Cada fila es un número de bloques y cada columna un tipo de bloque. La fila 5 dice que, con 5 bloques, la norma vale 7.6e-07 en la red plana y 7.6e-02 en la residual.
| bloques | BloquePlano | BloqueResidual |
|---|---|---|
| 1 | 7.6e-03 | 8.4e-02 |
| 5 | 7.6e-07 | 7.6e-02 |
| 10 | 2.2e-11 | 1.2e-01 |
step() casi no cambia la primera capa. En la red residual se mantiene entre 7.6 × 10⁻² y 1.2 × 10⁻¹ con las tres profundidades. El gradiente de x + F(x) respecto a x es 1 + F'(x). El término 1 deja pasar el gradiente hacia las primeras capas sin multiplicarlo por los pesos del bloque.Práctica
Traza guiada
Antes de procesar, escribe la forma después de cada capa y los parámetros de cada convolución para un batch de 4 imágenes RGB de 32 × 32.
torch.manual_seed(0)
red_traza = nn.Sequential(
nn.Conv2d(3, 6, kernel_size=5),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Conv2d(6, 16, kernel_size=5),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Flatten(),
)
lote = torch.rand(4, 3, 32, 32)
Comprueba recorriendo red_traza capa por capa, como en «CNN sobre dígitos». Si tu conteo de la segunda convolución no coincide, revisa que cada filtro cubre los 6 canales de entrada.
Stride en lugar de pooling
Copia crear_cnn, quita cada nn.MaxPool2d(2) y pon stride=2 en la convolución anterior. Antes de procesar, predice con la fórmula la forma después de cada convolución, el número de parámetros y si los errores con imágenes desplazadas suben o bajan. Comprueba con assert que la entrada de la capa densa sigue teniendo 64 valores. Entrena con las semillas 0, 1 y 2 y compara la tabla con la de la CNN original.
Red residual para dígitos
Construye crear_cnn_residual(semilla) que:
- empiece con
nn.Conv2d(1, 8, kernel_size=3, padding=1)y ReLU - añada un
BloqueResidual(8) - termine con
nn.MaxPool2d(2), la segunda convolución, ReLU,nn.MaxPool2d(2),nn.Flatten()ynn.Linear(64, 10), comocrear_cnn
Antes de entrenar, calcula cuántos parámetros añade el bloque y compruébalo con assert contar_parametros(...) == contar_parametros(crear_cnn(0)) + …. Entrena con entrenar y las semillas 0, 1 y 2. Escribe si la diferencia de errores frente a crear_cnn es mayor que la diferencia entre semillas.
Reto opcional
Elige una permutación fija de los 64 píxeles con torch.randperm(64, generator=torch.Generator().manual_seed(0)) y aplícala a todas las imágenes de entrenamiento y de validación. Entrena el MLP y la CNN sobre las imágenes permutadas con las semillas 0, 1 y 2. Predice antes qué modelo cambia poco y cuál empeora, y explícalo con la vecindad entre píxeles.
Errores que revisar
- Si
nn.Conv2d(1, 8, 3)lanzaexpected input[1, 5, 8, 8] to have 1 channels, but got 5 channels insteadcon un batch(5, 8, 8), falta el eje de canales. La capa interpreta un tensor de tres ejes como una sola imagen(C, H, W)y toma el batch como canales. Añade el eje conunsqueeze(1). - Si la imagen en color queda con franjas o colores mezclados después de pasar de
(H, W, C)a(C, H, W), usastereshape. Reordena los ejes conpermute(2, 0, 1). - Si la capa densa lanza
mat1 and mat2 shapes cannot be multiplied, el número de valores después denn.Flattenno coincide con su entrada. Calcula el tamaño con la fórmula o pasa un batch de una imagen capa por capa. - Si la suma de un bloque residual lanza un error de tamaños,
xyF(x)tienen formas distintas. Revisa que cada convolución del bloque usepadding=1,stride=1y el mismo número de canales de entrada y de salida.
Cierre
Con los números de la comparación, explica por qué la CNN usa menos parámetros que el MLP y por qué aun así falla con muchas imágenes desplazadas una columna.
Lista de comprobación
- ¿Qué forma tiene un batch de 16 imágenes RGB de 32 × 32 en PyTorch?
- ¿Qué tamaño de salida da una capa con
k=3,p=0ys=2sobre una entrada de 9 × 9? - ¿Por qué el número de parámetros de
nn.Conv2dno depende del alto ni del ancho de la imagen? - ¿Qué conserva
nn.MaxPool2d(2)de cada bloque 2 × 2 y cuántos parámetros tiene? - ¿Qué devuelve un bloque residual cuando
F(x) = 0y por qué eso facilita entrenar redes profundas?
Documentación oficial
- torch.nn.Conv2d: forma de entrada y de pesos,
padding,stridey fórmula del tamaño de salida. - torch.nn.MaxPool2d: pooling por máximo,
kernel_size,stridey tamaño de salida. - Training a Classifier: CNN pequeña para imágenes RGB de 32 × 32 con el loop de entrenamiento de PyTorch.
- Deep Residual Learning for Image Recognition: artículo original de ResNet, con el problema de degradación y los bloques residuales.