22. Construcción de una CNN en PyTorch

22.1 Introducción

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.

22.2 ¿Qué significa construir una CNN?

Construir una CNN significa definir la estructura del modelo. Esto incluye decidir:

  • Cuántas capas convolucionales tendrá.
  • Cuántos filtros usará cada capa.
  • Qué activaciones y pooling se aplicarán.
  • Cómo se pasará de mapas de características a una salida de clasificación.
  • Cuántas clases tendrá la salida final.

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á.

Construir el modelo es diseñar cómo fluye la información. Entrenarlo será, más adelante, ajustar sus pesos con datos.

22.3 ¿Por qué PyTorch?

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:

  • Permite definir modelos de forma muy expresiva.
  • Integra tensores, autodiferenciación y GPU de manera coherente.
  • Tiene un ecosistema muy fuerte junto a torchvision.
  • Es ideal para aprender porque el código suele reflejar bien la lógica del modelo.

22.4 Bloques básicos que usaremos

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.

22.5 La entrada de una CNN en PyTorch

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.

22.6 La dimensión batch

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:

  • Una imagen RGB sola: (3, 64, 64)
  • La misma imagen preparada para el modelo: (1, 3, 64, 64)

22.7 El papel de nn.Module

En 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.

22.8 ¿Qué va en __init__ y qué va en forward?

Esta distinción es fundamental:

  • En __init__ se definen las capas del modelo.
  • En 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.

22.9 Primera capa convolucional

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:

  • La entrada tiene 3 canales, por ejemplo RGB.
  • La capa aprenderá 16 filtros.
  • Cada filtro tendrá tamaño 3x3.
  • El padding=1 ayuda a conservar el tamaño espacial cuando el stride es 1.

22.10 Activación y no linealidad

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:

  • Convolución.
  • Activación.
  • Eventualmente pooling.

22.11 Pooling para reducir dimensió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.

22.12 Un bloque convolucional típico

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.

22.13 De mapas de características a clasificación

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.

22.14 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).

22.15 Capas lineales de salida

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.

22.16 La importancia de las dimensiones

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.

En PyTorch, muchos problemas al construir una CNN no vienen de la teoría, sino de una mala cuenta de dimensiones.

22.17 Ejemplo de flujo de tamaños

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:

  • Entrada: 64 x 64
  • Después de conv1: 64 x 64
  • Después de pool1: 32 x 32
  • Después de conv2: 32 x 32
  • Después de pool2: 16 x 16

Como 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.

22.18 Definición paso a paso de una CNN pequeña

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.

22.19 ¿Qué devuelve el modelo?

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.

22.20 Instanciar el modelo

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__.

22.21 Probar el modelo con un batch ficticio

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).

22.22 Adaptar la CNN al problema real

La arquitectura concreta depende del problema. Algunas decisiones cambian según los datos:

  • Si la imagen es en escala de grises, in_channels será 1 en lugar de 3.
  • Si hay 2 clases, la salida no necesita 10 neuronas.
  • Si el tamaño de entrada cambia, también cambiará el tamaño esperado en la capa lineal.

Por eso conviene pensar la red como una plantilla adaptable, no como una receta rígida.

22.23 ¿Conviene usar 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.

22.24 Modelo en CPU o GPU

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.

22.25 Errores comunes al construir una CNN

Algunos errores muy frecuentes son:

  • Confundir el orden de dimensiones y pasar tensores como (batch, alto, ancho, canales).
  • Definir mal in_channels.
  • No calcular correctamente el tamaño antes de la capa lineal.
  • Usar una cantidad de salidas final que no coincide con las clases del problema.
  • Olvidar la dimensión batch al probar una sola imagen.

La mayoría de estos errores se detecta rápido si se prueba el modelo con tensores sintéticos antes de entrenarlo.

22.26 Relación con el entrenamiento

Construir la red es solo una parte del proceso. Para que el modelo aprenda harán falta, además:

  • Un dataset cargado correctamente.
  • Una función de pérdida.
  • Un optimizador.
  • Un loop de entrenamiento.
  • Métricas de validación.

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.

22.27 Código completo y mínimo

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.

22.28 Qué debes recordar de este tema

  • Construir una CNN en PyTorch significa definir sus capas y el flujo de datos entre ellas.
  • Las imágenes suelen entrar con forma (batch, canales, alto, ancho).
  • Los modelos se definen normalmente como clases que heredan de nn.Module.
  • En __init__ se declaran las capas y en forward se define el recorrido de la información.
  • Después de las convoluciones y el pooling, suele aplicarse flatten para pasar a capas lineales.
  • Las dimensiones deben calcularse con cuidado, especialmente antes de la primera capa densa.
  • Probar la red con tensores aleatorios es una práctica excelente antes de entrenar.

22.29 Conclusión

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.