En la lección anterior hiciste una convolución a mano y viste que un filtro 3×3 sobre una imagen 5×5 producía un mapa de 3×3, pero dejamos varias preguntas abiertas: ¿por qué ese tamaño exactamente? ¿Cómo se convoluciona una imagen en color con 3 canales? ¿Qué hace el pooling que aparecía en el diagrama de anatomía? Esta lección responde todo eso: es la lección de "ingeniería" de las CNN, donde aprenderás a configurar una capa convolucional (filtros, stride, padding, canales), a predecir el tamaño de su salida y a contar sus parámetros —habilidades imprescindibles para leer y diseñar cualquier arquitectura, incluidas las famosas que veremos en 03-03. Cerraremos construyendo capas Conv2D y MaxPooling2D reales en Keras, interpretando su summary() línea a línea y visualizando qué "ven" los filtros sobre una imagen tipo producto.

Contenido

  1. La capa convolucional en detalle: filtros, stride y padding
  2. Canales de entrada y salida
  3. Tamaño de salida y recuento de parámetros
  4. Pooling: max y average
  5. Conv2D y MaxPooling2D en Keras: summary() línea a línea
  6. Visualización de feature maps con matplotlib

La capa convolucional en detalle: filtros, stride y padding

Una capa convolucional queda definida por unas pocas decisiones de diseño (sus hiperparámetros):

  • Número de filtros: cuántos detectores de patrones distintos tiene la capa. Cada filtro produce su propio mapa de características.
  • Tamaño del filtro (kernel size): la ventana que se desliza, típicamente 3×3 o 5×5. Ventanas pequeñas capturan patrones locales finos; hoy en día 3×3 es el estándar (en 03-03 verás por qué VGG lo consagró).
  • Stride (paso): cuántos píxeles se desplaza el filtro en cada salto.
  • Padding (relleno): si se añade o no un borde de ceros alrededor de la imagen antes de convolucionar.

Stride: el tamaño del paso

En 03-01 deslizamos el filtro de una posición en una posición: stride = 1. Con stride = 2, el filtro salta de dos en dos, evaluando la mitad de posiciones por dimensión, con lo que el mapa de salida sale aproximadamente a la mitad de ancho y de alto (una cuarta parte de posiciones en total).

Stride 1 sobre 5 columnas:      Stride 2 sobre 5 columnas:
posiciones 0,1,2  (3 columnas)  posiciones 0,2  (2 columnas)
  • Stride 1: máxima resolución de análisis; es lo habitual en capas convolucionales.
  • Stride 2: reduce el tamaño espacial (a veces se usa en lugar del pooling para "encoger" los mapas).

Padding: valid vs. same

Al convolucionar sin más, el mapa encoge (de 5×5 pasamos a 3×3) y, peor aún, los píxeles del borde participan en menos ventanas que los del centro: la esquina superior izquierda solo aparece en 1 ventana, mientras que un píxel central aparece en 9. Si el logo del producto está pegado al borde de la foto, se analiza "menos".

La solución es el padding: rodear la imagen con un marco de ceros antes de convolucionar.

Modo Qué hace Tamaño de salida (stride 1) Cuándo usarlo
valid Sin relleno: el filtro solo se coloca donde cabe entero Encoge: n - f + 1 Cuando no importa perder el borde o se quiere reducir tamaño
same Rellena con ceros justo lo necesario Igual que la entrada: n Cuando quieres apilar muchas capas sin que la imagen se evapore

Ejemplo con nuestra imagen 5×5 y filtro 3×3: con same se añade un marco de 1 píxel de ceros (la imagen pasa a 7×7) y la salida vuelve a ser 5×5. Sin padding, tras 2 capas 3×3 la imagen habría pasado de 5×5 a 1×1: con imágenes pequeñas y redes profundas, valid se come la imagen enseguida.

Canales de entrada y salida

Hasta ahora convolucionamos imágenes en escala de grises (1 canal). Una foto de producto real tiene 3 canales (RGB), es decir, es un bloque de alto × ancho × 3. ¿Cómo se adapta el filtro?

Regla de oro: el filtro siempre tiene tantos canales como su entrada. Un "filtro 3×3" sobre una entrada RGB es en realidad un bloque de pesos 3×3×3 (27 pesos): tiene una rebanada 3×3 para el canal rojo, otra para el verde y otra para el azul. En cada posición se multiplican y suman los 27 productos (más el sesgo) y sale un único número. Por eso:

  • 1 filtro → 1 mapa de características 2D, da igual cuántos canales tenga la entrada.
  • Una capa con 32 filtros → salida con 32 canales (un bloque alto × ancho × 32).

Y aquí viene el encadenamiento que hace profundas a las CNN: la siguiente capa convolucional recibe ese bloque de 32 canales, así que sus filtros serán de 3×3×32. Los "canales" dejan de ser colores y pasan a ser mapas de patrones detectados: el canal 7 puede ser "bordes verticales", el 21 "textura metálica". Cada capa combina los patrones de la anterior — la jerarquía de características de 03-01 en acción.

flowchart LR
    A["Entrada RGB<br/>64x64x3"] -->|"32 filtros de 3x3x3"| B["Bloque<br/>64x64x32"]
    B -->|"64 filtros de 3x3x32"| C["Bloque<br/>64x64x64"]

Tamaño de salida y recuento de parámetros

La fórmula del tamaño de salida

Con entrada de tamaño n × n, filtro f × f, padding p (píxeles de relleno por lado) y stride s:

tamaño de salida = ⌊(n + 2p − f) / s⌋ + 1

(⌊·⌋ = redondear hacia abajo.) Comprobaciones y ejemplos habituales:

Entrada (n) Filtro (f) Padding Stride (s) Cálculo Salida
5 3 valid (p=0) 1 (5+0−3)/1 + 1 3
5 3 same (p=1) 1 (5+2−3)/1 + 1 5
28 3 valid (p=0) 1 (28+0−3)/1 + 1 26
28 3 same (p=1) 1 (28+2−3)/1 + 1 28
64 3 same (p=1) 2 ⌊(64+2−3)/2⌋ + 1 32
224 7 p=3 2 ⌊(224+6−7)/2⌋ + 1 112

La primera fila es exactamente nuestro ejemplo a mano de 03-01: de 5×5 a 3×3. La última es la primera capa de ResNet, que verás en 03-03: ya sabes de dónde sale su "112".

Para same con stride 1, el padding necesario es p = (f − 1) / 2: con filtro 3×3, p=1; con 5×5, p=2 (por eso los filtros suelen ser de tamaño impar).

Parámetros de una capa convolucional

parámetros = (f × f × canales_entrada + 1) × número_de_filtros

El +1 es el sesgo de cada filtro. Ejemplo: Conv2D de 32 filtros 3×3 sobre entrada RGB:

  • (3 × 3 × 3 + 1) × 32 = 28 × 32 = 896 parámetros.

Comparemos con una capa densa "equivalente" que conectara la misma entrada con la misma salida, para una imagen 64×64×3 y salida 64×64×32 (padding same):

Capa Cálculo Parámetros
Conv2D, 32 filtros 3×3 (3·3·3 + 1) × 32 896
Densa de 64·64·3 = 12 288 entradas a 64·64·32 = 131 072 salidas 12 288 × 131 072 + 131 072 ~1 610 743 808

896 frente a 1 610 millones: un factor de casi dos millones. Es la consecuencia directa de la conectividad local y los pesos compartidos de 03-01. Además, fíjate en que los parámetros de la conv no dependen del tamaño de la imagen, solo del filtro y de los canales: la misma capa de 896 parámetros procesa fotos de 64×64 o de 1024×768.

Pooling: max y average

El pooling es la operación de "resumen" que vimos en el diagrama de 03-01. Divide cada mapa de características en ventanas (típicamente 2×2, con stride 2, sin solapamiento) y sustituye cada ventana por un solo valor:

  • Max pooling: se queda con el máximo de la ventana. Interpretación: "¿se ha detectado el patrón en algún punto de esta zona?" — conserva la detección más fuerte y descarta el resto.
  • Average pooling: la media de la ventana. Resume la activación general de la zona; es más suave, y su variante global (promediar todo el mapa) se usa al final de arquitecturas modernas como sustituto del Flatten (lo verás en 03-03).

Ejemplo a mano de max pooling 2×2 con stride 2 sobre un mapa 4×4:

Mapa de características:      Max pooling 2x2:
 1  3 | 2  0
 4  2 | 1  1        ->         4  2
------+------                  6  8
 0  6 | 5  8
 1  2 | 3  0

Cada bloque 2×2 se reduce a su máximo: max(1,3,4,2)=4, max(2,0,1,1)=2, max(0,6,1,2)=6, max(5,8,3,0)=8.

Efectos del pooling 2×2:

  • Reduce el ancho y alto a la mitad → la siguiente capa procesa 4 veces menos posiciones → menos cómputo y memoria. Los canales no cambian.
  • Tiene 0 parámetros: no hay nada que aprender, es una operación fija.
  • Aporta una pequeña invarianza a traslaciones locales: si el borde detectado se mueve 1 píxel dentro de la ventana, el máximo no cambia. A la CNN le da igual que el asa de la cafetera esté 2 píxeles más a la izquierda en la foto del vendedor.
  • Al reducir el mapa, cada píxel de capas posteriores "ve" una zona mayor de la imagen original (crece el campo receptivo), lo que ayuda a pasar de detectar bordes a detectar partes y objetos.

El patrón clásico que ya adelantamos en 03-01 es alternar: conv (extraer) → pool (resumir y encoger) → conv → pool... duplicando habitualmente el número de filtros tras cada pooling (16 → 32 → 64...): a medida que perdemos resolución espacial, ganamos riqueza de patrones.

Conv2D y MaxPooling2D en Keras: summary() línea a línea

Construyamos en Keras la etapa convolucional para fotos de producto de TecnoMarket reducidas a 64×64 RGB, con 4 categorías de salida (portátil, cafetera, monitor, tostadora). Aún no vamos a entrenar (el proyecto guiado completo es 07-01); el objetivo aquí es leer el modelo.

from tensorflow import keras
from tensorflow.keras import layers

modelo = keras.Sequential([
    keras.Input(shape=(64, 64, 3)),                    # foto RGB 64x64
    layers.Conv2D(32, (3, 3), padding="same", activation="relu"),
    layers.MaxPooling2D((2, 2)),
    layers.Conv2D(64, (3, 3), padding="same", activation="relu"),
    layers.MaxPooling2D((2, 2)),
    layers.Conv2D(128, (3, 3), padding="same", activation="relu"),
    layers.MaxPooling2D((2, 2)),
    layers.Flatten(),
    layers.Dense(64, activation="relu"),
    layers.Dense(4, activation="softmax"),             # 4 categorias de producto
])

modelo.summary()

Desglose de los argumentos de Conv2D(32, (3, 3), padding="same", activation="relu"):

  • 32: número de filtros → la salida tendrá 32 canales.
  • (3, 3): tamaño del filtro.
  • padding="same": la salida conserva el ancho y alto de la entrada (el stride por defecto es 1; se cambiaría con strides=2).
  • activation="relu": la activación tras cada convolución + sesgo, como recomendamos para capas ocultas en 02-02.

MaxPooling2D((2, 2)) usa ventana 2×2 y, por defecto, stride igual a la ventana (2): reduce a la mitad.

El summary() produce (formateado):

Layer (type)                 Output Shape        Param #
=========================================================
conv2d (Conv2D)              (None, 64, 64, 32)      896
max_pooling2d (MaxPooling2D) (None, 32, 32, 32)        0
conv2d_1 (Conv2D)            (None, 32, 32, 64)   18,496
max_pooling2d_1 (MaxPooling) (None, 16, 16, 64)        0
conv2d_2 (Conv2D)            (None, 16, 16, 128)  73,856
max_pooling2d_2 (MaxPooling) (None, 8, 8, 128)         0
flatten (Flatten)            (None, 8192)              0
dense (Dense)                (None, 64)          524,352
dense_1 (Dense)              (None, 4)               260
=========================================================
Total params: 617,860

Verifiquemos cada línea con nuestras fórmulas (el None es la dimensión del batch, como en 02-05):

  1. conv2d: padding same → sigue en 64×64; 32 filtros → 32 canales. Parámetros: (3·3·3 + 1) × 32 = 896. ✔ (nuestro ejemplo de antes)
  2. max_pooling2d: 64/2 = 32 → (32, 32, 32). 0 parámetros, como toda capa de pooling.
  3. conv2d_1: entrada con 32 canales → filtros de 3×3×32. (3·3·32 + 1) × 64 = 289 × 64 = 18 496. ✔
  4. conv2d_2: (3·3·64 + 1) × 128 = 577 × 128 = 73 856. ✔
  5. flatten: 8 × 8 × 128 = 8192 valores. Aplanar aquí es razonable: son 8192 características abstractas, no píxeles crudos.
  6. dense: 8192 × 64 + 64 = 524 352. Observa que la capa densa tras el Flatten concentra el 85 % de los parámetros del modelo — el motivo por el que las arquitecturas modernas la sustituyen por global average pooling (03-03).
  7. dense_1: 64 × 4 + 4 = 260, con softmax para las 4 categorías (02-02).

Total: ~618 000 parámetros para procesar imágenes de 64×64×3. La red densa de 02-05 necesitaba ~100 000 solo para MNIST de 28×28×1; una densa equivalente para 64×64×3 dispararía millones. Y de esos 618 000, solo 93 248 están en la parte convolucional: el extractor de características es baratísimo.

Visualización de feature maps con matplotlib

Para intuir qué hace cada filtro, apliquemos una convolución + ReLU y max pooling a una "foto de producto" sintética (una caja clara sobre fondo oscuro, como el embalaje de un producto de TecnoMarket) y pintemos los mapas resultantes. Usamos filtros fijados a mano para que el resultado sea interpretable; en un modelo entrenado harías lo mismo con los filtros aprendidos.

import numpy as np
import matplotlib.pyplot as plt

# 1) Imagen sintetica 32x32: una "caja de producto" clara sobre fondo oscuro
imagen = np.zeros((32, 32))
imagen[8:24, 10:22] = 1.0          # rectangulo claro (el producto)

# 2) Dos filtros deteccion de bordes
filtro_vertical = np.array([[1, 0, -1]] * 3, dtype=float)      # bordes verticales
filtro_horizontal = filtro_vertical.T                          # bordes horizontales

def convolucion2d(imagen, filtro):
    fi, ci = imagen.shape
    ff, cf = filtro.shape
    salida = np.zeros((fi - ff + 1, ci - cf + 1))
    for i in range(salida.shape[0]):
        for j in range(salida.shape[1]):
            salida[i, j] = np.sum(imagen[i:i + ff, j:j + cf] * filtro)
    return salida

def relu(x):
    return np.maximum(0, x)        # la ReLU de 02-02

def max_pooling(mapa, tam=2):
    f, c = mapa.shape
    salida = np.zeros((f // tam, c // tam))
    for i in range(salida.shape[0]):
        for j in range(salida.shape[1]):
            salida[i, j] = np.max(mapa[i*tam:(i+1)*tam, j*tam:(j+1)*tam])
    return salida

# 3) Pipeline conv -> ReLU -> pooling para cada filtro
mapas = {
    "Bordes verticales": max_pooling(relu(convolucion2d(imagen, filtro_vertical))),
    "Bordes horizontales": max_pooling(relu(convolucion2d(imagen, filtro_horizontal))),
}

# 4) Visualizacion
fig, ejes = plt.subplots(1, 3, figsize=(12, 4))
ejes[0].imshow(imagen, cmap="gray")
ejes[0].set_title("Imagen original 32x32")
for eje, (nombre, mapa) in zip(ejes[1:], mapas.items()):
    eje.imshow(mapa, cmap="gray")
    eje.set_title(f"{nombre}\n{mapa.shape[0]}x{mapa.shape[1]} tras pooling")
plt.tight_layout()
plt.show()

Qué verás al ejecutarlo:

  • El mapa de bordes verticales se ilumina solo a lo largo del lado izquierdo de la caja (transición oscuro→claro con este filtro; el lado derecho da valores negativos que la ReLU pone a cero — un segundo filtro con signos opuestos capturaría ese lado, y por eso las capas llevan muchos filtros).
  • El mapa de bordes horizontales se ilumina en el lado superior de la caja.
  • Ambos mapas son de 15×15: la convolución valid deja 30×30 y el pooling 2×2 lo reduce a la mitad.

Cada filtro produce un "mapa de dónde está su patrón", y el pooling condensa esos mapas conservando las detecciones. Con los filtros de un modelo entrenado, esta misma técnica (extraer la salida de una capa intermedia y pintarla) es una herramienta habitual para inspeccionar qué ha aprendido una CNN.

Errores Comunes y Consejos

  • Olvidar los canales al contar parámetros. El error más frecuente: contar (3·3 + 1) × 64 en vez de (3·3·32 + 1) × 64. El filtro siempre abarca todos los canales de su entrada.
  • Creer que el pooling aprende algo. Tiene 0 parámetros; es una operación fija. Reduce tamaño, no "extrae características" por sí mismo.
  • Confundir el efecto de padding y stride. El padding decide si se pierde tamaño por los bordes (same lo evita); el stride decide cada cuántos píxeles se evalúa el filtro (stride 2 divide el tamaño por 2). Son controles independientes.
  • Apilar convs valid sobre imágenes pequeñas. Cada capa 3×3 valid resta 2 píxeles por dimensión; diez capas sobre una imagen de 28×28 la dejan en 8×8 antes de que hayas empezado a hacer pooling. Para apilar en profundidad, same es tu amiga.
  • Ignorar dónde están los parámetros. Mira siempre el summary(): si una densa tras el Flatten concentra la mayoría de los parámetros, ahí tienes al principal candidato a sobreajustar (recuerda 02-05) y a engordar el modelo.
  • Consejo: ante cualquier summary(), reproduce a mano las columnas Output Shape y Param # de las dos primeras capas. Es el ejercicio que más rápido consolida esta lección.

Ejercicios

Ejercicio 1: predecir formas y parámetros

Sin ejecutar nada, calcula la Output Shape y el Param # de cada capa de este modelo para fotos de TecnoMarket de 32×32 RGB:

modelo = keras.Sequential([
    keras.Input(shape=(32, 32, 3)),
    layers.Conv2D(16, (5, 5), padding="valid", activation="relu"),
    layers.MaxPooling2D((2, 2)),
    layers.Conv2D(32, (3, 3), padding="same", activation="relu"),
    layers.MaxPooling2D((2, 2)),
])

Ejercicio 2: pooling a mano

Aplica max pooling y average pooling (ventana 2×2, stride 2) a este mapa de características, y explica cuál de los dos conservaría mejor la detección de un patrón débil pero real que solo activa un píxel:

2  0  1  1
0  0  1  3
8  1  0  0
2  1  0  0

Ejercicio 3: diseñar la reducción

Las fotos de producto llegan a 128×128×3. Quieres una etapa convolucional que termine en mapas de 16×16 usando bloques [Conv2D 3×3 same + MaxPooling2D 2×2]. ¿Cuántos bloques necesitas? Si empiezas con 16 filtros y los duplicas en cada bloque, ¿cuántos canales tendrá la salida final y cuántos parámetros tendrá la última capa conv?

Soluciones

Ejercicio 1:

Capa Output Shape Cálculo de parámetros Param #
Conv2D 16 filtros 5×5 valid (None, 28, 28, 16) — (32−5)/1+1 = 28 (5·5·3 + 1) × 16 = 76 × 16 1216
MaxPooling2D 2×2 (None, 14, 14, 16) — 0
Conv2D 32 filtros 3×3 same (None, 14, 14, 32) (3·3·16 + 1) × 32 = 145 × 32 4640
MaxPooling2D 2×2 (None, 7, 7, 32) — 0

Total: 5856 parámetros.

Ejercicio 2:

Max pooling:     Average pooling:
2  3              0.5   1.5
8  0              3.0   0.0

(Máximos: max(2,0,0,0)=2, max(1,1,1,3)=3, max(8,1,2,1)=8, max(0,0,0,0)=0. Medias: 2/4=0.5, 6/4=1.5, 12/4=3.0, 0/4=0.0.)

El max pooling conserva mejor una detección débil pero real que activa un solo píxel: el máximo la mantiene intacta (el 8 sobrevive como 8), mientras que la media la diluye entre los vecinos inactivos (8 → 3.0). Por eso max pooling es la opción por defecto en las etapas intermedias: interesa saber si el patrón apareció, no cuánto activó la zona en promedio.

Ejercicio 3:

Cada bloque divide el tamaño por 2: 128 → 64 → 32 → 16. Se necesitan 3 bloques. Los filtros: 16 → 32 → 64, así que la salida final es de 16×16×64 canales. La última capa conv recibe 32 canales y tiene 64 filtros 3×3: (3·3·32 + 1) × 64 = 289 × 64 = 18 496 parámetros.

Conclusión

Ya dominas la mecánica completa de los dos bloques que forman la etapa de extracción de una CNN. De la capa convolucional sabes configurar sus cuatro decisiones (filtros, tamaño de kernel, stride, padding), sabes que los filtros abarcan todos los canales de su entrada y que la capa produce un canal por filtro, y sabes predecir su salida con la fórmula ⌊(n + 2p − f)/s⌋ + 1 y contar sus parámetros con (f·f·canales + 1) × filtros — 896 parámetros donde una densa equivalente necesitaría 1600 millones. Del pooling sabes que resume ventanas (max conserva detecciones, average promedia), que reduce el cómputo a una cuarta parte por bloque sin añadir parámetros y que regala invarianza local. Y has traducido todo esto a Keras, verificando a mano cada línea de un summary() y visualizando feature maps con matplotlib.

Con estas piezas ya puedes leer los planos de cualquier CNN. Y eso es justo lo que haremos en la siguiente lección: recorrer las arquitecturas populares que marcaron la historia del campo —LeNet, AlexNet, VGG, Inception, ResNet— entendiendo qué idea aportó cada una y comprobando, summary() en mano, que ya sabes descifrarlas.

Curso de Deep Learning

Módulo 1: Introducción a Deep Learning

Módulo 2: Fundamentos de Redes Neuronales

Módulo 3: Redes Neuronales Convolucionales (CNN)

Módulo 4: Redes Neuronales Recurrentes (RNN)

Módulo 5: Técnicas Avanzadas en Deep Learning

Módulo 6: Herramientas y Frameworks

Módulo 7: Proyectos Prácticos

Módulo 8: Consideraciones Éticas y Futuro del Deep Learning

© Copyright 2026. Todos los derechos reservados