Entrenando un modelo de visión (II): la convolución, cómo una IA aprende a ver

Entrenando un modelo de visión (II): la convolución, cómo una IA aprende a ver

En el primer artículo de esta serie montamos el esqueleto: la estructura del proyecto, el script de entrenamiento y la clase `Net` con sus capas `conv1`, `pool`, `conv2`, `fc1` y `fc2`. Dijimos que `conv1` y `conv2` "detectan patrones" — pero no explicamos qué significa eso realmente.

En esta segunda entrega abrimos esa caja negra: qué es una convolución, qué es un filtro (o kernel), y cómo se ve todo esto en la práctica sobre una imagen real.


1. ¿Qué es una convolución?

Una convolución es una operación matemática que se aplica sobre una imagen usando una ventana pequeña llamada filtro o kernel. Ese filtro se va moviendo por toda la imagen, y en cada posición hace un cálculo sencillo que genera un nuevo valor. El resultado final es una imagen nueva, más pequeña, donde ciertas características (como bordes o texturas) quedan resaltadas.

Piénsalo así:

  • Tu imagen es una matriz de píxeles (por ejemplo, 32×32 números).
  • Tu filtro es una matriz mucho más pequeña, normalmente de 3×3.
  • Vas deslizando ese filtro de 3×3 por toda la imagen, casilla a casilla.
  • En cada posición, multiplicas cada número del filtro por el número de la imagen que tiene debajo, y sumas todos esos resultados en un único valor.
  • Ese valor se guarda en una nueva imagen (llamada mapa de activación o feature map).

Photoshop: Filter > Other > Custom

Photoshop: Filter>Other>Custom

Ejemplo numérico paso a paso

Imagina este trozo de imagen en escala de grises (3×3 píxeles):


[1, 2, 0]
[0, 1, 3]
[1, 0, 2]

Y este filtro, diseñado para detectar bordes verticales:


[ 1, 0, -1]
[ 1, 0, -1]
[ 1, 0, -1]

Para aplicar la convolución en esta posición: multiplicas cada número del filtro por el número que tiene justo debajo en la imagen, y sumas todo:


(1*1) + (2*0) + (0*-1) +
(0*1) + (1*0) + (3*-1) +
(1*1) + (0*0) + (2*-1)
= 1 + 0 + 0 + 0 + 0 - 3 + 1 + 0 - 2
= -3

Ese `-3` es un único número del mapa de activación resultante. El filtro se desliza una casilla y repite el cálculo, hasta cubrir toda la imagen. Este filtro concreto reacciona fuerte cuando hay una diferencia grande entre la columna izquierda y la derecha — es decir, cuando hay un borde vertical (un cambio brusco de claro a oscuro, o al revés, en horizontal). Por eso decimos que es un "detector de bordes verticales".


2. Filtros manuales vs. filtros aprendidos

Hay dos formas muy distintas de conseguir un filtro:

  1. Tú lo diseñas a mano — como el ejemplo de arriba. Sabes de antemano qué patrón quieres detectar y escribes los números del filtro para conseguirlo.
  2. La red lo aprende sola durante el entrenamiento — no le dices qué buscar; ella ajusta los números del filtro poco a poco, mediante *backpropagation*, hasta que ese filtro resulta útil para clasificar bien.
Importante: Importante: Backpropagation is the process by which a neural network calculates how much each weight contributed to the prediction error by propagating backward from the output to the input. Using this information, it adjusts the weights to reduce the error and improve future predictions.

Vamos a ver las dos en código.

Parte 1: Filtros manuales sobre una imagen real


import cv2                       # librería de visión por computador, para leer y procesar imágenes
import numpy as np               # para trabajar con matrices de números (arrays)
import matplotlib.pyplot as plt  # para dibujar los resultados en pantalla
# Cargamos una imagen y la convertimos a escala de grises (un único canal en vez de RGB,
# así es más fácil ver el efecto del filtro sin la distracción del color).
img = cv2.imread("tus_fotos/imagen1.jpg", cv2.IMREAD_GRAYSCALE)
# Redimensionamos la imagen a 128x128 píxeles, solo para que todos los ejemplos
# se vean con el mismo tamaño y el cálculo vaya rápido.
img = cv2.resize(img, (128, 128))
# Definimos tres filtros clásicos "a mano", cada uno como una matriz de 3x3.
# Estos números NO se han aprendido con entrenamiento: los hemos escrito nosotros
# a propósito, sabiendo qué patrón queremos que detecten.
filtro_horizontal = np.array([[1, 1, 1],
                               [0, 0, 0],
                               [-1, -1, -1]])   # reacciona a cambios de arriba a abajo
filtro_vertical = np.array([[1, 0, -1],
                             [1, 0, -1],
                             [1, 0, -1]])       # reacciona a cambios de izquierda a derecha
filtro_diagonal = np.array([[0, 1, -1],
                             [-1, 0, 1],
                             [1, -1, 0]])       # reacciona a cambios en diagonal
# cv2.filter2D aplica la convolución: desliza el filtro sobre "img" y genera
# la imagen resultante (el mapa de activación) de cada uno.
img_h = cv2.filter2D(img, -1, filtro_horizontal)
img_v = cv2.filter2D(img, -1, filtro_vertical)
img_d = cv2.filter2D(img, -1, filtro_diagonal)
# Dibujamos las 4 imágenes lado a lado para comparar el efecto de cada filtro.
plt.figure(figsize=(10, 4))
plt.subplot(1, 4, 1), plt.imshow(img, cmap="gray"), plt.title("Original")
plt.subplot(1, 4, 2), plt.imshow(img_h, cmap="gray"), plt.title("Bordes Horizontales")
plt.subplot(1, 4, 3), plt.imshow(img_v, cmap="gray"), plt.title("Bordes Verticales")
plt.subplot(1, 4, 4), plt.imshow(img_d, cmap="gray"), plt.title("Diagonales")
plt.tight_layout()
plt.show()
Importante: Este ejemplo no usa ningún tipo de aprendizaje. Tú decides el filtro, tú decides qué patrón detecta. Es la mejor forma de entender la mecánica antes de ver lo que hace una red neuronal por sí sola.
Resultado de aplicar filtros manuales de detección de bordes

Qué detecta cada tipo de filtro, en la práctica

FiltroQué resalta en la imagen
Bordes verticalesContornos de objetos que van de arriba a abajo (una botella, un poste, el lateral de una cara)
Bordes horizontalesLíneas de separación de arriba a abajo (una mesa, el suelo, un horizonte)
DiagonalFormas curvas y oblicuas — más sutil, capta cambios en varias direcciones a la vez

Sobre una imagen de gallinas, por ejemplo: el filtro vertical resalta muy bien el contorno de las cabezas, el horizontal marca la línea donde el cuerpo se separa del fondo, y el diagonal capta las curvas del plumaje — cada filtro es literalmente una "lente" distinta sobre la misma foto.


3. Parte 2: los filtros que aprende la propia CNN

Now vamos a mirar dentro de la red del artículo anterior. Recordemos que en `conv1` definimos 16 filtros:


self.conv1 = nn.Conv2d(3, 16, 3, padding=1)

Esto significa literalmente: *"quiero 16 filtros distintos, cada uno de 3×3, aplicados a una imagen de 3 canales (RGB)"*. Esos 16 filtros existen como números dentro del modelo — y podemos extraerlos y dibujarlos, igual que dibujamos los filtros manuales de antes.


# net.conv1.weight contiene los pesos (los números) de los 16 filtros de conv1.
# .data lo separa del grafo de cálculo de PyTorch (no lo necesitamos para entrenar aquí, solo para mirar).
# .cpu() lo mueve a la memoria normal del ordenador, por si el modelo estaba en la GPU.
filtros = net.conv1.weight.data.cpu()
# Los valores de los filtros pueden ser números negativos, positivos, muy dispersos...
# Esta línea los reescala todos entre 0 y 1, para poder dibujarlos como una imagen normal.
filtros = (filtros - filtros.min()) / (filtros.max() - filtros.min())
import matplotlib.pyplot as plt
plt.figure(figsize=(12, 3))
for i in range(8):   # dibujamos solo los primeros 8 de los 16, para no saturar la imagen
    plt.subplot(1, 8, i + 1)
    filtro = filtros[i]
    # Cada filtro tiene forma [canales, alto, ancho] (por ejemplo [3, 3, 3]).
    # permute(1, 2, 0) reordena los ejes para que sea [alto, ancho, canales],
    # que es el formato que espera matplotlib para pintar una imagen a color.
    img = filtro.permute(1, 2, 0).numpy()
    plt.imshow(img)
    plt.axis('off')
    plt.title(f"Filtro {i}")
plt.tight_layout()
plt.show()
Visualización de los primeros 8 filtros de la capa convolucional conv1

Un matiz importante: si acabas de crear el modelo y todavía no lo has entrenado, estos filtros tienen valores aleatorios — así arrancan siempre las redes neuronales, como una hoja en blanco. Ya se puede intuir alguna estructura (líneas diagonales, zonas más claras u oscuras), pero no significan nada todavía. Es durante el entrenamiento, mediante *backpropagation*, cuando esos números se van ajustando poco a poco hasta convertirse en detectores útiles de verdad: uno acabará pareciéndose al filtro vertical que diseñamos a mano, otro al horizontal, otro a texturas más complejas — sin que nadie se lo haya programado explícitamente.


4. Clasificar una imagen con un modelo sin entrenar

Como cierre práctico, vamos a pasar una imagen real por el modelo — aunque todavía tenga los pesos aleatorios de fábrica, sin entrenar. El objetivo aquí no es que acierte (no puede, todavía no ha aprendido nada), sino comprobar que todo el flujo funciona de principio a fin: cargar imagen → convertirla a tensor → pasarla por la red → obtener una predicción.


# Simulamos 3 clases posibles, en el mismo orden en que las tendría un ImageFolder real.
clases = ["Gallina", "Pato", "Ganso"]
# Cargamos la imagen, la pasamos por el modelo (net) y miramos qué neurona de salida
# tiene el valor más alto: esa es "la clase elegida" por el modelo.
# torch.max(outputs, 1) devuelve el índice de la neurona más activada.
_, predicted = torch.max(outputs, 1)
print(f"El modelo ha clasificado la imagen como: {clases[predicted.item()]}")

Con pesos aleatorios, el resultado es efectivamente aleatorio — el modelo podría decir "Pato" igual que podría decir "Gallina" o "Ganso", da igual la imagen que le pongas delante. Pero el hecho de que el flujo completo funcione (sin errores de forma, de tipo de dato, de dimensiones...) es justo lo que necesitamos verificado antes de lanzarnos a entrenar de verdad, que es lo que veremos en un futuro artículo de esta serie.


5. Qué hemos aprendido en esta entrega

  • Qué es una convolución, con un ejemplo numérico calculado a mano.
  • La diferencia entre un filtro que diseñas tú y un filtro que la red aprende sola.
  • Que los filtros de una red recién creada empiezan con valores aleatorios, y se van especializando durante el entrenamiento.
  • Cómo comprobar que el flujo de clasificación funciona de extremo a extremo, incluso antes de entrenar.

Nos queda una pieza por explicar del todo: qué hace exactamente el `pool` más allá de "reducir tamaño", y qué pasa dentro de las capas densas (`fc1`, `fc2`) que reciben ese resultado y toman la decisión final. Eso es lo que abordamos en la tercera y última entrega de esta 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