En los temas anteriores estudiamos las ideas fundamentales detrás de una CNN: convoluciones, mapas de características, pooling, arquitecturas clásicas, datasets y data augmentation. Ahora llega un paso decisivo: llevar todo eso a código real.
En este tema aprenderemos a construir una red convolucional en PyTorch. La idea no es todavía entrenar un clasificador completo de principio a fin, sino entender cómo se define estructuralmente el modelo: qué capas contiene, cómo se conectan y qué forma tienen los tensores que van circulando.
Si este paso queda claro, el tema siguiente, dedicado al entrenamiento, resultará mucho más natural. Construir una CNN no es memorizar una receta; es entender cómo traducir una arquitectura visual a una clase de Python bien organizada.
Construir una CNN significa definir la estructura del modelo. Esto incluye decidir:
En otras palabras, aquí no estamos enseñándole aún a la red qué hacer. Estamos definiendo el “esqueleto” computacional con el que después aprenderá.
PyTorch es uno de los frameworks más usados en Deep Learning porque combina flexibilidad, claridad y una sintaxis bastante natural para experimentar. En visión por computadora es especialmente popular tanto en investigación como en aplicaciones prácticas.
Algunas razones por las que se usa tanto son:
torchvision.Para construir una CNN pequeña en PyTorch suelen intervenir unos pocos elementos fundamentales:
torch para trabajar con tensores.torch.nn para definir capas y modelos.torch.nn.functional si queremos aplicar algunas operaciones funcionales.Un comienzo típico es este:
import torch
import torch.nn as nn
import torch.nn.functional as F
Con esto ya tenemos casi todo lo necesario para definir una CNN elemental.
Una de las primeras cosas que conviene fijar bien es la forma de los tensores de imagen. En PyTorch, para visión, la convención típica es:
(batch_size, canales, alto, ancho)
Es decir, primero el batch, luego los canales y después las dimensiones espaciales. Por ejemplo, un batch de 32 imágenes RGB de 64 x 64 se representaría como:
(32, 3, 64, 64)
Este orden es muy importante porque las capas convolucionales esperan justamente esa estructura.
Muchas veces cuando pensamos una imagen aislada imaginamos solo canales, alto y ancho. Sin embargo, durante el entrenamiento el modelo procesa grupos de imágenes a la vez. Por eso aparece la dimensión batch.
Incluso si queremos probar una sola imagen, normalmente PyTorch seguirá esperando una dimensión batch, aunque sea de tamaño 1.
Por ejemplo:
(3, 64, 64)(1, 3, 64, 64)nn.ModuleEn PyTorch, los modelos suelen definirse como clases que heredan de nn.Module. Esto le indica al framework que la clase representa un modelo entrenable, con parámetros que deben registrarse correctamente.
Una estructura mínima tiene esta forma:
class MiCNN(nn.Module):
def __init__(self):
super().__init__()
def forward(self, x):
return x
Aquí todavía no hace nada útil, pero ya muestra la forma general: una inicialización y un método forward.
__init__ y qué va en forward?Esta distinción es fundamental:
__init__ se definen las capas del modelo.forward se define cómo se aplican esas capas a la entrada.En otras palabras, __init__ declara los componentes permanentes del modelo, mientras que forward describe el recorrido de los datos a través de esos componentes.
Entender bien esta separación evita muchos errores al construir redes en PyTorch.
Una capa convolucional 2D se define típicamente con nn.Conv2d. Por ejemplo:
self.conv1 = nn.Conv2d(
in_channels=3,
out_channels=16,
kernel_size=3,
padding=1
)
Esto significa:
padding=1 ayuda a conservar el tamaño espacial cuando el stride es 1.Después de una convolución, lo habitual es aplicar una activación no lineal, generalmente ReLU. En PyTorch puede escribirse así:
x = F.relu(self.conv1(x))
La activación introduce no linealidad y permite que la red aprenda transformaciones más ricas que una simple combinación lineal de filtros.
Una CNN típica repite una y otra vez el patrón:
El pooling ayuda a reducir el tamaño espacial de los mapas de características. Una opción muy frecuente es nn.MaxPool2d.
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
Con esta operación, si una entrada tiene tamaño 64 x 64, después del pooling pasará a 32 x 32. El objetivo es resumir información local y reducir costo computacional.
Una CNN pequeña suele construirse a partir de bloques repetidos. Por ejemplo:
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
Cada bloque detecta patrones, aplica no linealidad y reduce tamaño. Las primeras capas suelen detectar rasgos simples; las siguientes combinan esos rasgos en representaciones más complejas.
Hasta cierto punto, la red trabaja con tensores tridimensionales por imagen: canales, alto y ancho. Pero al final, si queremos clasificar, necesitamos producir un vector de salida con puntajes para cada clase.
Por eso, después de las capas convolucionales, la red suele pasar a capas densas o lineales que operan sobre un vector plano.
Ese paso intermedio se logra con una operación llamada flatten.
Flatten significa convertir un bloque de características espaciales en un vector. En PyTorch, una forma habitual es:
x = torch.flatten(x, start_dim=1)
Se usa start_dim=1 para preservar la dimensión batch y aplanar todo lo demás. Así, si el tensor era (32, 32, 16, 16), pasará a algo como (32, 8192).
Una vez aplanado el tensor, ya podemos usar capas lineales con nn.Linear. Por ejemplo:
self.fc1 = nn.Linear(32 * 16 * 16, 128)
self.fc2 = nn.Linear(128, 10)
Aquí la primera capa transforma el vector de características en una representación intermedia de 128 neuronas, y la segunda produce 10 salidas, una por clase.
Uno de los errores más comunes al construir CNN en PyTorch es equivocarse con los tamaños. El modelo puede estar conceptualmente bien pensado, pero si el número esperado por una capa lineal no coincide con el tamaño real después de convoluciones y pooling, aparecerá un error.
Por eso conviene seguir cuidadosamente cómo cambian las dimensiones a lo largo de la red.
Supongamos una entrada de (3, 64, 64) y esta secuencia:
Conv2d(3, 16, kernel_size=3, padding=1)MaxPool2d(2, 2)Conv2d(16, 32, kernel_size=3, padding=1)MaxPool2d(2, 2)La evolución espacial sería:
64 x 6464 x 6432 x 3232 x 3216 x 16Como al final hay 32 canales, el tensor resultante por imagen tendrá tamaño 32 x 16 x 16, y por eso el flatten producirá 32 * 16 * 16 valores.
Con estas ideas, ya podemos escribir una CNN pequeña y clara:
class CNNPequena(nn.Module):
def __init__(self, num_clases=10):
super().__init__()
self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1)
self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding=1)
self.pool = nn.MaxPool2d(2, 2)
self.fc1 = nn.Linear(32 * 16 * 16, 128)
self.fc2 = nn.Linear(128, num_clases)
def forward(self, x):
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
x = torch.flatten(x, start_dim=1)
x = F.relu(self.fc1(x))
x = self.fc2(x)
return x
Este ya es un modelo funcional. Todavía no está entrenado, pero su estructura está completamente definida.
En clasificación, la última capa suele devolver logits, es decir, puntajes sin normalizar para cada clase. No son probabilidades todavía.
Por ejemplo, si hay 10 clases y el batch es de 32 imágenes, la salida tendrá forma:
(32, 10)
Cada fila representa una imagen y cada columna el puntaje asociado a una clase posible.
Una vez definida la clase, crear el modelo es directo:
modelo = CNNPequena(num_clases=10)
Desde ese momento PyTorch ya registra todos los parámetros aprendibles que aparecen en las capas definidas dentro de __init__.
Antes de entrenar, es muy buena práctica verificar que la red acepta tensores con la forma esperada y produce una salida coherente. Por ejemplo:
x = torch.randn(8, 3, 64, 64)
salida = modelo(x)
print(salida.shape)
Aquí generamos un batch aleatorio de 8 imágenes RGB de 64 x 64. Si todo está bien, la forma de salida será (8, 10).
La arquitectura concreta depende del problema. Algunas decisiones cambian según los datos:
in_channels será 1 en lugar de 3.Por eso conviene pensar la red como una plantilla adaptable, no como una receta rígida.
nn.Sequential?En algunos casos PyTorch permite escribir redes con nn.Sequential, lo cual puede ser cómodo para modelos lineales sencillos. Sin embargo, para aprender CNN suele ser mejor definir una clase completa con nn.Module.
Eso hace más explícita la arquitectura y facilita depuración, extensiones y modificaciones posteriores.
Una vez definido, el modelo puede moverse al dispositivo disponible:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
modelo = modelo.to(device)
Lo importante es recordar que, cuando llegue el entrenamiento, tanto el modelo como los tensores de entrada deberán estar en el mismo dispositivo.
Algunos errores muy frecuentes son:
(batch, alto, ancho, canales).in_channels.La mayoría de estos errores se detecta rápido si se prueba el modelo con tensores sintéticos antes de entrenarlo.
Construir la red es solo una parte del proceso. Para que el modelo aprenda harán falta, además:
Justamente eso será el foco del próximo tema. Aquí el objetivo es que la arquitectura quede clara antes de introducir la lógica de aprendizaje.
Este ejemplo reúne todo lo esencial para construir y probar una CNN pequeña en PyTorch:
import torch
import torch.nn as nn
import torch.nn.functional as F
class CNNPequena(nn.Module):
def __init__(self, num_clases=10):
super().__init__()
self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1)
self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding=1)
self.pool = nn.MaxPool2d(2, 2)
self.fc1 = nn.Linear(32 * 16 * 16, 128)
self.fc2 = nn.Linear(128, num_clases)
def forward(self, x):
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
x = torch.flatten(x, start_dim=1)
x = F.relu(self.fc1(x))
x = self.fc2(x)
return x
modelo = CNNPequena(num_clases=10)
x = torch.randn(8, 3, 64, 64)
salida = modelo(x)
print("Forma de entrada:", x.shape)
print("Forma de salida:", salida.shape)
Este programa no entrena la red, pero sí confirma que la arquitectura está bien construida y que la salida tiene la forma esperada para un problema de clasificación de 10 clases.
(batch, canales, alto, ancho).nn.Module.__init__ se declaran las capas y en forward se define el recorrido de la información.flatten para pasar a capas lineales.Construir una CNN en PyTorch es el punto donde la teoría de redes convolucionales empieza a transformarse en una herramienta concreta. A partir de aquí, ya no estamos solo describiendo convoluciones y pooling en abstracto: estamos definiendo un modelo real que puede recibir tensores, producir salidas y prepararse para aprender.
Lo importante no es memorizar una arquitectura fija, sino comprender la lógica de composición entre capas convolucionales, activaciones, pooling, flatten y capas finales de clasificación. Cuando esa lógica queda clara, resulta mucho más fácil diseñar variantes, adaptar redes a nuevos problemas y depurar errores.
En el próximo tema daremos el siguiente paso natural: el entrenamiento de un clasificador de imágenes, donde esta arquitectura dejará de ser una estructura estática y empezará a ajustarse con datos reales.