Entrenant un model de visió (II): la convolució, com una IA aprèn a veure

Entrenant un model de visió (II): la convolució, com una IA aprèn a veure

En el primer article d'aquesta sèrie vàrem muntar l'esquelet: l'estructura del projecte, el script d'entrenament i la classe `Net` amb les seves capes `conv1`, `pool`, `conv2`, `fc1` i `fc2`. Vàrem dir que `conv1` i `conv2` "detecten patrons" — però no us vaig explicar què significa això realment.

En aquesta segona entrega anem a obrir la caixa negra: aprenerem què és una convolució, què és un filtre (o kernel), i com es veu tot això a la pràctica sobre una imatge real.


1. Què és una convolució?

Una convolució és una operació matemàtica que s'aplica sobre una imatge utilitzant una finestra petita anomenada filtre o kernel. Aquest filtre es va movent per tota la imatge, i en cada posició fa un càlcul senzill que genera un nou valor. El resultat final és una imatge nova, més petita, on certes característiques (com vores o textures) queden ressaltades.

Per a veure-ho fàcil:

  • Imagina que la teva imatge és una quadrícula gran de números (píxels).
  • El teu filtre és una matriu molt més petita, normalment de 3x3.
  • En cada posició, multipliques cada número del filtre pel número de la imatge que té a sota, i sumes tots aquests resultats en un únic valor.

Photoshop: Filter > Other > Custom

Photoshop: Filter>Other>Custom

Exemple numèric pas a pas

Imagina que tenim una imatge petita (una secció d'una imatge gran) on hi ha un contrast fort de lluminositat (un tall net que va de zona blanca a zona negra). En valors numèrics de píxels, es veuria així (els valors alts representen blanc o llum, els baixos representen negre o ombra):


Imatge (secció 3x3):
[  0,  0, 255 ]
[  0,  0, 255 ]
[  0,  0, 255 ]

I utilitzarem aquest filtre de 3x3 dissenyat específicament per a detectar vores verticals:


Filtre (kernel):
[ -1,  0,  1 ]
[ -1,  0,  1 ]
[ -1,  0,  1 ]

Per a aplicar la convolució en aquesta posició: multipliques cada número del filtre pel número que té just a sota a la imatge, i ho sumes tot:


Càlcul:
(0 * -1) + (0 * 0) + (255 * 1) +
(0 * -1) + (0 * 0) + (255 * 1) +
(0 * -1) + (0 * 0) + (255 * 1)
= 0 + 0 + 255 + 0 + 0 + 255 + 0 + 0 + 255
= 765

Aquest valor resultant `765` és un número molt alt (activació forta). Això significa que el filtre ha detectat el patró que buscava en aquesta posició de la imatge.

Què passaria en una zona llisa de color? Per exemple, una zona completament blanca de la imatge:


Imatge (secció 3x3 llisa blanca):
[ 255, 255, 255 ]
[ 255, 255, 255 ]
[ 255, 255, 255 ]

Si apliquem el mateix filtre sobre aquesta secció llisa:


Càlcul:
(255 * -1) + (255 * 0) + (255 * 1) +
(255 * -1) + (255 * 0) + (255 * 1) +
(255 * -1) + (255 * 0) + (255 * 1)
= -255 + 0 + 255 - 255 + 0 + 255 - 255 + 0 + 255
= 0

El resultat és `0` (sense activació). El filtre no ha detectat cap canvi de llum, cap vora.


2. Filtres manuals vs. filtres apresos

La diferència fonamental entre la visió per computador clàssica i el Deep Learning està en com es decideixen els números d'aquest filtre:

  1. La persona els dissenya a mà (clàssic) — tu decideixes posar-hi `[-1, 0, 1]` perquè saps que això buscarà vores verticals.
  2. La xarxa neuronal els aprèn sola durant l'entrenament (Deep Learning) — no li dius què buscar; ella ajusta els números del filtre a poc a poc, mitjançant *backpropagation*, fins que aquest filtre és útil per a classificar bé.
Important: El backpropagation és el procés pel qual una xarxa neuronal calcula quant ha contribuït cada pes a l'error d'una predicció, recorrent la xarxa des de la sortida fins a l'entrada. Amb aquesta informació, ajusta els pesos per reduir l'error i millorar les prediccions futures.

Part 1: Filtres manuals sobre una imatge real

Podem programar un script ràpid en Python utilitzant OpenCV per a agafar una imatge del nostre ordinador i aplicar-li filtres dissenyats a mà per a comprovar el seu efecte:


import cv2                       # llibreria de visió per computador, per a llegir i processar imatges
import numpy as np               # per a treballar amb matrius de números (arrays)
import matplotlib.pyplot as plt  # per a dibuixar els resultats a la pantalla
# Carreguem una imatge i la convertim a escala de grisos (un únic canal en lloc de RGB,
# així és més fàcil veure l'efecte del filtre sense la distracció del color).
img = cv2.imread("images/entrenando-modelo-vision-cnn-parte2/fp-img.jpg", cv2.IMREAD_GRAYSCALE)
# Redimensionem la imatge a 128x128 píxels, només perquè tots els exemples
# es vegin amb la mateixa mida i el càlcul vagi ràpid.
img = cv2.resize(img, (128, 128))
# Definim tres filtres clàssics "a mà", cadascun com una matriu de 3x3.
# Aquests números NO s'han après amb entrenament: els hem escrit nosaltres
# a propòsit, sabent quin patró volem que detectin.
filtre_hor = np.array([[-1, -2, -1],
                         [ 0,  0,  0],
                         [ 1,  2,  1]])   # reacciona a canvis de dalt a baix (vores horitzontals)
filtre_vert = np.array([[-1,  0,  1],
                        [-2,  0,  2],
                        [-1,  0,  1]])   # reacciona a canvis d'esquerra a dreta (vores verticals)
filtre_diag = np.array([[-2, -1,  0],
                        [-1,  4,  1],
                        [ 0,  1,  2]])   # reacciona a canvis en diagonal
# cv2.filter2D aplica la convolució: llisca el filtre sobre "img" i genera
# la imatge resultant (el mapa d'activació) de cadascun.
res_horiz = cv2.filter2D(img, -1, filtre_hor)
res_vert  = cv2.filter2D(img, -1, filtre_vert)
res_diag  = cv2.filter2D(img, -1, filtre_diag)
# Dibuixem les 4 imatges costat a costat per a comparar l'efecte de cada filtre.
fig, axs = plt.subplots(1, 4, figsize=(15, 5))
axs[0].imshow(img, cmap='gray'); axs[0].set_title('Original (Grisos)')
axs[1].imshow(res_horiz, cmap='gray'); axs[1].set_title('Vores Horitzontals')
axs[2].imshow(res_vert, cmap='gray'); axs[2].set_title('Vores Verticals')
axs[3].imshow(res_diag, cmap='gray'); axs[3].set_title('Vores Diagonals')
plt.show()
Resultat d'aplicar filtres manuals de detecció de vores
Important: Aquest exemple no utilitza cap tipus d'aprenentatge. Tu decideixes el filtre, tu decideixes quin patró detecta. És la millor manera d'entendre la mecànica abans de veure el que fa una xarxa neuronal per si sola.

Què detecta cada tipus de filtre, a la pràctica

Tipus de FiltreQuè detecta (Característiques ressaltades)
Vores horitzontalsContorns d'objectes orientats de costat a costat (el capó d'un cotxe, l'horitzó, la vora superior d'una taula)
Vores verticalsContorns d'objectes que van de dalt a baix (una ampolla, un pal, el lateral d'una cara)
Vores diagonalsTextures en angle, cantonades i elements en diagonal o de transició

3. Part 2: els filtres que aprèn la pròpia CNN

Ara mirarem dins de la xarxa de l'article anterior. Recordem que a `conv1` vàrem definir 16 filtres:


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

Això significa literalment: *"vull 16 filtres diferents, cada un de 3x3, aplicats a una imatge de 3 canals (RGB)"*. Aquests 16 filtres existeixen com a números dins del model — i podem extreure'ls i dibuixar-los, igual que hem dibuixat els filtres manuals d'abans.


# net.conv1.weight conté els pesos (els números) dels 16 filtres de conv1.
# .data ho separa del graf de càlcul de PyTorch (no ho necessitem per a entrenar aquí, només per a mirar).
# .cpu() ho mou a la memòria normal de l'ordinador, per si el model estava a la GPU.
filtros = net.conv1.weight.data.cpu()
# Els valors dels filtres poden ser números negatius, positius, molt dispersos...
# Aquesta línia els reescala tots entre 0 i 1, per a poder dibuixar-los como una imatge normal.
filtros_norm = (filtros - filtros.min()) / (filtros.max() - filtros.min())
fig, axs = plt.subplots(2, 4, figsize=(10, 5))
for i in range(8):   # dibuixem només els primers 8 dels 16, per a no saturar la imatge
    ax = axs[i // 4, i % 4]
    filtro_img = filtros_norm[i]
    # Cada filtre té forma [canals, alt, ample] (per exemple [3, 3, 3]).
    # permute(1, 2, 0) reordena els eixos perquè sigui [alt, ample, canals],
    # que és el format que espera matplotlib per a pintar una imatge a color.
    filtro_img = filtro_img.permute(1, 2, 0).numpy()
    ax.imshow(filtro_img)
    ax.set_title(f'Filtre {i+1}')
    ax.axis('off')
plt.show()
Visualització dels primers 8 filtres de la capa convolucional conv1

Un matís important: si acabes de crear el model i encara no l'has entrenat, aquests filtres tenen valors aleatoris — així arranquen sempre les xarxes neuronals, com un full en blanc. Ja es pot intuir alguna estructura (línies diagonals, zones més clares o fosques), però no signifiquen res encara. És durant l'entrenament, mitjançant *backpropagation*, quan aquests números es van ajustant a poc a poc fins a convertir-se en detectors útils de veritat: un s'acabarà assemblant al filtre vertical que vam dissenyar a mà, un altre al horitzontal, un altre a textures més complexes — sense que ningú l'hi hagi programat explícitament.


4. Classificar una imatge amb un model sense entrenar

Com a tancament pràctic, passarem una imatge real pel model — encara que tingui els pesos aleatoris de fàbrica, sense entrenar. L'objectiu aquí no és que encerti (no pot, encara no ha après res), sinó comprovar que tot el flux funciona de principi a fi: carregar imatge -> convertir-la a tensor -> passar-la per la xarxa -> obtenir una predicció.


from PIL import Image
import torch
import torchvision.transforms as transforms
# Carreguem la imatge des de disc
img_pilot = Image.open("images/entrenando-modelo-vision-cnn-parte2/fp-img.jpg")
# Apliquem les mateixes transformacions que definim al script d'entrenament
transform = transforms.Compose([
    transforms.Resize((32, 32)),
    transforms.ToTensor(),
    transforms.Normalize((0.5,), (0.5,))
])
# Afegim la dimensió del "batch": PyTorch espera rebre un grup d'imatges,
# així que convertim una imatge de forma [3, 32, 32] en un grup d'una sola imatge: [1, 3, 32, 32].
img_tensor = transform(img_pilot).unsqueeze(0).to(device)
# Passem la imatge pel model
net.eval() # posem el model en mode d'avaluació (desactiva dropout, batchnorm, etc.)
with torch.no_grad(): # desactivem el càlcul de gradients, ja que no estem entrenant ara
    outputs = net(img_tensor)
# Simulem 3 classes possibles, en el mateix ordre en què les tindria un ImageFolder real.
clases = ["Gallina", "Ànec", "Oca"]
# Carreguem la imatge, la pasem pel model (net) i mirem quina neurona de sortida
# té el valor més alt: aquesta és "la classe triada" pel model.
# torch.max(outputs, 1) retorna l'índex de la neurona més activada.
_, predicted = torch.max(outputs, 1)
print(f"El model ha classificat la imatge com: {clases[predicted.item()]}")

Amb pesos aleatoris, el resultat és efectivament aleatori — el model podria dir "Ànec" igual que podria dir "Gallina" o "Oca", és igual la imatge que li posis davant. Però el fet que el flux complet funcioni (sense errors de forma, de tipus de dada, de dimensions...) és just el que necessitem verificar abans de llançar-nos a entrenar de veritat, que es el que veurem en un futur article d'aquesta sèrie.


5. Què hem après en aquesta entrega

  • Què és una convolució, amb un exemple numèric calculat a mà.
  • La diferència entre un filtre que dissenyes tu i un filtre que la xarxa neuronal aprèn sola.
  • Que els filtres d'una xarxa acabada de crear comencen amb valors aleatoris, i es van especialitzant durant l'entrenament.
  • Com comprovar que el flux de classificació funciona d'extrem a extrem, fins i tot abans d'entrenar.

Ens queda una peça per explicar del tot: què fa exactament el `pool` més enllà de "reduir mida", i què passa dins de les capes denses (`fc1`, `fc2`) que reben aquest resultat i prenen la decisió final. Això és el que abordem en la tercera i última entrega d'aquesta sèrie.


Article elaborat a partir de proves reals sobre infraestructura pròpia de ReparamiPC/Girtual (Girona).

Sobre l'autor: David Otero Verdaguer, Graduat en Multimèdia per la UOC i amb formació en Administració de Sistemes Informàtics en Xarxa (ASIX). Si t'interessa aquest tipus de contingut, al blog de ReparamiPC (www.reparamipc.com) escric amb més freqüència sobre xarxes, sistemes i els fonaments tècnics que sostenen projectes com aquest.

Comentaris