Entrenando un modelo de visión (III): pooling y capas densas, de píxeles a decisión

Entrenando un modelo de visión (III): pooling y capas densas, de píxeles a decisión

En el primer artículo de esta serie montamos el esqueleto de la red neuronal. En el segundo abrimos la caja negra de `conv1` y `conv2`: qué es una convolución y cómo un filtro aprende a detectar patrones visuales.

Nos quedan dos piezas por explicar, y son justo las que suelen generar más dudas porque ya no se pueden "ver" tan fácilmente como los filtros: el pooling (`self.pool`) y las capas densas (`fc1`, `fc2`). Cerramos con ellas la serie.


1. ¿Qué es el pooling?

El pooling es una operación que se aplica justo después de una convolución. Su trabajo es simple: reducir el tamaño del mapa de activación quedándose solo con la información más relevante de cada zona.

Piensa en el pooling como si tomaras una foto de alta resolución y la comprimieras a propósito, quedándote solo con lo esencial de cada bloque de píxeles, en vez de intentar conservarlo todo.

Cómo funciona: una ventana deslizante

Al igual que la convolución, el pooling se mueve por la imagen con una ventana, pero sin multiplicaciones ni filtros complejos: simplemente toma el valor más representativo de cada zona.

Comparativa visual entre MaxPooling y Average Pooling
TipoQué hace
MaxPoolingToma el valor máximo de la ventana
AvgPoolingToma el valor promedio de la ventana

Ejemplo numérico

Imagina este trozo de mapa de activación (2×2 valores):


[1, 3]
[4, 2]

Un MaxPooling de 2×2 sobre este bloque devuelve un único valor: `4` — el más alto de los cuatro.

Un AvgPooling de 2×2 sobre el mismo bloque devuelve `2.5` — la media de (1+3+4+2)/4.

En nuestro modelo usamos MaxPooling porque, para clasificar imágenes, suele interesarnos más "¿dónde hay una activación fuerte de este patrón?" que "¿cuál es el promedio de esta zona?" — un borde muy marcado en una esquina de la ventana es más informativo que diluirlo con la media del resto.

En nuestro código


# Definimos el pooling una sola vez en __init__, y lo reutilizamos
# tanto después de conv1 como después de conv2.
self.pool = nn.MaxPool2d(2, 2)   # ventana de 2x2, se queda con el valor más alto de cada una

# En forward, el pooling se aplica siempre justo después de la convolución + ReLU:
x = self.pool(torch.relu(self.conv1(x)))

Ejemplo concreto con números reales: si la imagen que entra mide 32×32 píxeles, tras esta línea el resultado mide 16×16. Al pasar otra vez por `conv2` y `self.pool` de nuevo, pasamos de 16×16 a 8×8. Por eso en el primer artículo la capa densa esperaba `32 * 8 * 8` valores de entrada — el `8×8` viene exactamente de aplicar pooling dos veces sobre una imagen de 32×32.


2. Por qué el pooling importa (no es solo "para ahorrar espacio")

  • Reduce el tamaño → menos números que procesar en las siguientes capas, entrenamiento más rápido.
  • Se queda con lo importante → descarta detalles finos poco relevantes y conserva los patrones principales de cada zona.
  • Hace el modelo más robusto → si en una foto la gallina está un poco más a la derecha que en otra, el pooling ayuda a que el modelo la reconozca igual, porque no depende de la posición exacta de cada píxel.
  • Ayuda a evitar overfitting → al simplificar la información, es más difícil que el modelo "memorice" detalles irrelevantes de las fotos de entrenamiento en vez de aprender el patrón general.
Analogía: imagina que le describes una foto a alguien por teléfono. No le dices el color exacto de cada píxel — le dices "hay una gallina marrón en el centro, mirando a la izquierda". Eso es, en esencia, lo que hace el pooling: quedarse con el resumen útil y descartar el ruido.

3. Las capas densas: donde se toma la decisión final

Después de las convoluciones y el pooling, ya no tenemos una imagen en el sentido habitual — tenemos un vector de números, una lista larga que resume lo que la red ha detectado. Algo como:


[0.1, 2.3, 0.0, 5.4, 1.2, 0.7, ...]

Aquí es donde entran las capas densas (también llamadas *fully connected*, de ahí el nombre `fc1`, `fc2`):

Visualización del vector de embedding pasando de convoluciones a capas densas

self.fc1 = nn.Linear(32 * 8 * 8, 128)   # de 2048 valores de entrada a un vector de 128
self.fc2 = nn.Linear(128, num_classes)  # de 128 valores a una neurona por clase

`nn.Linear(entrada, salida)` conecta cada número de entrada con cada neurona de salida — de ahí lo de "densa": no se salta ninguna conexión posible, a diferencia de la convolución, que solo mira una ventana pequeña a la vez.

¿Por qué se habla menos de las capas densas que de los filtros?

Es una pregunta muy razonable, y tiene una respuesta clara: porque no se pueden "ver" tan fácilmente. Un filtro de convolución es una imagen pequeña — lo puedes dibujar y mirar. Una capa densa, en cambio, trabaja con un vector abstracto de números, sin ninguna estructura espacial (no hay "arriba", "abajo" o "vecino" en una lista de 2048 números). No hay una forma tan directa de dibujar "qué está pensando" esa capa.

Aun así, es precisamente aquí donde se toma la decisión. El proceso, en resumen:

  1. Las capas convolucionales detectan patrones sueltos: bordes, curvas, texturas.
  2. Las capas densas combinan esos patrones. De forma simplificada: *"si hay algo redondo con dos líneas verticales al lado, probablemente sea un ojo"*.
  3. Si además se combinan activaciones relacionadas con cabeza, pico y plumas: *"con todo esto junto, es muy probable que sea una gallina"*.
Analogía: imagina un equipo de detectives. Los filtros convolucionales son los que recorren la escena buscando pistas sueltas (una huella, un pelo, una mancha). El pooling es el que descarta las pistas poco claras y se queda solo con las más evidentes. Y las capas densas son el detective jefe, que junta todas esas pistas sueltas y dice: *"con todo esto, yo creo que fue el mayordomo"* — o, en nuestro caso, *"con todo esto, yo creo que es una gallina"*.

4. El vector de embedding: la "huella digital" de la imagen

El resultado de `fc1` — ese vector de 128 números — tiene un nombre propio: se le llama embedding. Es una representación numérica y abstracta de la imagen, calculada por la red.

Si ya conoces Qdrant (una base de datos vectorial que usaremos en otros proyectos de esta web), esto te va a sonar familiar: es exactamente el mismo concepto. Cuando entrenas el modelo, imágenes parecidas (dos fotos de gallinas distintas, por ejemplo) acaban generando vectores de embedding parecidos entre si — igual que en Qdrant guardamos vectores para poder comparar significados, aquí el modelo genera vectores para poder comparar imágenes. La última capa (`fc2`) usa ese vector para decidir la clase final, casi como si consultara internamente una pequeña base vectorial.


# outputs es el resultado final de pasar la imagen por TODO el modelo (fc2 incluida).
# outputs.shape sería algo como [1, num_classes] -> una puntuación por clase.
# torch.max nos da el índice de la clase con la puntuación más alta:
# el "1" indica que buscamos el máximo a lo largo de la dimensión de las clases.
_, predicted = torch.max(outputs, 1)

Un matiz importante sobre el tamaño de este vector: el número de dimensiones del embedding no es fijo, lo decides tú al definir la capa. Si en tu modelo pusieras `nn.Linear(32 * 8 * 8, 128)`, el embedding tendría 128 dimensiones. Si en otro proyecto usaras `nn.Linear(16 * 32 * 32, 64)`, tendría 64. No hay un número "correcto" universal — es un parámetro de diseño más, como decidir cuántas cifras usar para resumir algo: más dimensiones pueden capturar matices más finos, pero también hacen el modelo más pesado de entrenar.


5. El pipeline completo, de un vistazo

El pipeline general de procesamiento de nuestra red neuronal se resume en las siguientes etapas:

Esquema visual del pipeline de la red neuronal convolucional
EtapaQué haceAnalogía
Convolución (`conv1`, `conv2`)Detecta patrones visuales sueltos: bordes, texturas, curvasUn detective buscando pistas sueltas en la escena
Pooling (`pool`)Reduce tamaño, se queda con lo más relevante de cada zonaResumir la escena por teléfono, sin describir cada píxel
Aplanado (`view`)Convierte el resultado en una lista de númerosDeshilar una manta doblada en un solo hilo
Capas densas (`fc1`, `fc2`)Combinan los patrones detectados y deciden la clase finalEl detective jefe uniendo todas las pistas y dando un veredicto

6. Cierre de la serie

Con esto completamos el recorrido de las tres entregas:

  1. Primera parte: la estructura del proyecto y la arquitectura completa de la red.
  2. Segunda parte: qué es una convolución y cómo se ven los filtros, tanto diseñados a mano como aprendidos por el modelo.
  3. Tercera parte: cómo el pooling simplifica la información, y cómo las capas densas la combinan para tomar la decisión final.

De aquí a un modelo entrenado de verdad y funcionando en producción hay principalmente una diferencia: repetir este mismo flujo miles de veces, con datos reales y etiquetados, dejando que el optimizador (`Adam`, del primer artículo) vaya ajustando poco a poco cada filtro y cada peso de las capas densas hasta que las predicciones se acerquen a la realidad. Ese proceso de entrenamiento a fondo — cuánto tarda, cómo se sabe cuándo parar, qué errores típicos aparecen — lo dejamos para una próxima serie.


Artículo elaborado a partir de pruebas reales sobre infraestructura propia de ReparamiPC/Girtual (Girona).

Sobre el autor: David Otero Verdaguer, Graduado en Multimedia por la UOC y con formación en Administración de Sistemas Informáticos en Red (ASIR). Si te interesa este tipo de contenido, en el blog de ReparamiPC (www.reparamipc.com) escribo con más frecuencia sobre redes, sistemas y los fundamentos técnicos que sostienen proyectos como este.

Comentarios