A la lliçó anterior vas fer una convolució a mà i vas veure que un filtre 3×3 sobre una imatge 5×5 produïa un mapa de 3×3, però vam deixar diverses preguntes obertes: per què exactament aquesta mida? Com es convoluciona una imatge en color amb 3 canals? Què fa el pooling que apareixia al diagrama d'anatomia? Aquesta lliçó ho respon tot: és la lliçó d'"enginyeria" de les CNN, on aprendràs a configurar una capa convolucional (filtres, stride, padding, canals), a predir la mida de la seva sortida i a comptar-ne els paràmetres —habilitats imprescindibles per llegir i dissenyar qualsevol arquitectura, incloses les famoses que veurem a 03-03. Tancarem construint capes Conv2D i MaxPooling2D reals a Keras, interpretant el seu summary() línia a línia i visualitzant què "veuen" els filtres sobre una imatge tipus producte.

Contingut

  1. La capa convolucional en detall: filtres, stride i padding
  2. Canals d'entrada i sortida
  3. Mida de sortida i recompte de paràmetres
  4. Pooling: max i average
  5. Conv2D i MaxPooling2D a Keras: summary() línia a línia
  6. Visualització de feature maps amb matplotlib

La capa convolucional en detall: filtres, stride i padding

Una capa convolucional queda definida per unes poques decisions de disseny (els seus hiperparàmetres):

  • Nombre de filtres: quants detectors de patrons diferents té la capa. Cada filtre produeix el seu propi mapa de característiques.
  • Mida del filtre (kernel size): la finestra que es desplaça, típicament 3×3 o 5×5. Finestres petites capturen patrons locals fins; avui dia 3×3 és l'estàndard (a 03-03 veuràs per què VGG el va consagrar).
  • Stride (pas): quants píxels es desplaça el filtre a cada salt.
  • Padding (farciment): si s'afegeix o no una vora de zeros al voltant de la imatge abans de convolucionar.

Stride: la mida del pas

A 03-01 vam desplaçar el filtre d'una posició en una posició: stride = 1. Amb stride = 2, el filtre salta de dos en dos, avaluant la meitat de posicions per dimensió, amb la qual cosa el mapa de sortida surt aproximadament de la meitat d'ample i d'alt (una quarta part de posicions en total).

Stride 1 sobre 5 columnes:      Stride 2 sobre 5 columnes:
posicions 0,1,2  (3 columnes)   posicions 0,2  (2 columnes)
  • Stride 1: màxima resolució d'anàlisi; és l'habitual en capes convolucionals.
  • Stride 2: redueix la mida espacial (de vegades es fa servir en lloc del pooling per "encongir" els mapes).

Padding: valid vs. same

En convolucionar sense més, el mapa s'encongeix (de 5×5 passem a 3×3) i, pitjor encara, els píxels de la vora participen en menys finestres que els del centre: la cantonada superior esquerra només apareix en 1 finestra, mentre que un píxel central apareix en 9. Si el logotip del producte està enganxat a la vora de la foto, s'analitza "menys".

La solució és el padding: envoltar la imatge amb un marc de zeros abans de convolucionar.

Mode Què fa Mida de sortida (stride 1) Quan fer-lo servir
valid Sense farciment: el filtre només es col·loca on cap sencer S'encongeix: n - f + 1 Quan no importa perdre la vora o es vol reduir mida
same Farceix amb zeros just el necessari Igual que l'entrada: n Quan vols apilar moltes capes sense que la imatge s'evapori

Exemple amb la nostra imatge 5×5 i filtre 3×3: amb same s'afegeix un marc d'1 píxel de zeros (la imatge passa a 7×7) i la sortida torna a ser 5×5. Sense padding, després de 2 capes 3×3 la imatge hauria passat de 5×5 a 1×1: amb imatges petites i xarxes profundes, valid es menja la imatge de seguida.

Canals d'entrada i sortida

Fins ara hem convolucionat imatges en escala de grisos (1 canal). Una foto de producte real té 3 canals (RGB), és a dir, és un bloc d'alt × ample × 3. Com s'hi adapta el filtre?

Regla d'or: el filtre sempre té tants canals com la seva entrada. Un "filtre 3×3" sobre una entrada RGB és en realitat un bloc de pesos 3×3×3 (27 pesos): té una llesca 3×3 per al canal vermell, una altra per al verd i una altra per al blau. A cada posició es multipliquen i sumen els 27 productes (més el biaix) i en surt un únic número. Per això:

  • 1 filtre → 1 mapa de característiques 2D, tant se val quants canals tingui l'entrada.
  • Una capa amb 32 filtres → sortida amb 32 canals (un bloc alt × ample × 32).

I aquí ve l'encadenament que fa profundes les CNN: la capa convolucional següent rep aquest bloc de 32 canals, així que els seus filtres seran de 3×3×32. Els "canals" deixen de ser colors i passen a ser mapes de patrons detectats: el canal 7 pot ser "vores verticals", el 21 "textura metàl·lica". Cada capa combina els patrons de l'anterior — la jerarquia de característiques de 03-01 en acció.

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

Mida de sortida i recompte de paràmetres

La fórmula de la mida de sortida

Amb entrada de mida n × n, filtre f × f, padding p (píxels de farciment per costat) i stride s:

mida de sortida = ⌊(n + 2p − f) / s⌋ + 1

(⌊·⌋ = arrodonir cap avall.) Comprovacions i exemples habituals:

Entrada (n) Filtre (f) Padding Stride (s) Càlcul Sortida
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 és exactament el nostre exemple a mà de 03-01: de 5×5 a 3×3. L'última és la primera capa de ResNet, que veuràs a 03-03: ja saps d'on surt el seu "112".

Per a same amb stride 1, el padding necessari és p = (f − 1) / 2: amb filtre 3×3, p=1; amb 5×5, p=2 (per això els filtres solen ser de mida senar).

Paràmetres d'una capa convolucional

paràmetres = (f × f × canals_entrada + 1) × nombre_de_filtres

L'+1 és el biaix de cada filtre. Exemple: Conv2D de 32 filtres 3×3 sobre entrada RGB:

  • (3 × 3 × 3 + 1) × 32 = 28 × 32 = 896 paràmetres.

Comparem-ho amb una capa densa "equivalent" que connectés la mateixa entrada amb la mateixa sortida, per a una imatge 64×64×3 i sortida 64×64×32 (padding same):

Capa Càlcul Paràmetres
Conv2D, 32 filtres 3×3 (3·3·3 + 1) × 32 896
Densa de 64·64·3 = 12 288 entrades a 64·64·32 = 131 072 sortides 12 288 × 131 072 + 131 072 ~1 610 743 808

896 davant d'1 610 milions: un factor de gairebé dos milions. És la conseqüència directa de la connectivitat local i els pesos compartits de 03-01. A més, fixa't que els paràmetres de la conv no depenen de la mida de la imatge, només del filtre i dels canals: la mateixa capa de 896 paràmetres processa fotos de 64×64 o de 1024×768.

Pooling: max i average

El pooling és l'operació de "resum" que vam veure al diagrama de 03-01. Divideix cada mapa de característiques en finestres (típicament 2×2, amb stride 2, sense solapament) i substitueix cada finestra per un sol valor:

  • Max pooling: es queda amb el màxim de la finestra. Interpretació: "s'ha detectat el patró en algun punt d'aquesta zona?" — conserva la detecció més forta i descarta la resta.
  • Average pooling: la mitjana de la finestra. Resumeix l'activació general de la zona; és més suau, i la seva variant global (fer la mitjana de tot el mapa) es fa servir al final d'arquitectures modernes com a substitut del Flatten (ho veuràs a 03-03).

Exemple a mà de max pooling 2×2 amb stride 2 sobre un mapa 4×4:

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

Cada bloc 2×2 es redueix al seu màxim: max(1,3,4,2)=4, max(2,0,1,1)=2, max(0,6,1,2)=6, max(5,8,3,0)=8.

Efectes del pooling 2×2:

  • Redueix l'ample i l'alt a la meitat → la capa següent processa 4 vegades menys posicions → menys còmput i memòria. Els canals no canvien.
  • Té 0 paràmetres: no hi ha res a aprendre, és una operació fixa.
  • Aporta una petita invariància a translacions locals: si la vora detectada es mou 1 píxel dins de la finestra, el màxim no canvia. A la CNN tant li fa que la nansa de la cafetera sigui 2 píxels més a l'esquerra a la foto del venedor.
  • En reduir el mapa, cada píxel de capes posteriors "veu" una zona més gran de la imatge original (creix el camp receptiu), cosa que ajuda a passar de detectar vores a detectar parts i objectes.

El patró clàssic que ja vam avançar a 03-01 és alternar: conv (extreure) → pool (resumir i encongir) → conv → pool... duplicant habitualment el nombre de filtres després de cada pooling (16 → 32 → 64...): a mesura que perdem resolució espacial, guanyem riquesa de patrons.

Conv2D i MaxPooling2D a Keras: summary() línia a línia

Construïm a Keras l'etapa convolucional per a fotos de producte de TecnoMarket reduïdes a 64×64 RGB, amb 4 categories de sortida (portàtil, cafetera, monitor, torradora). Encara no entrenarem (el projecte guiat complet és 07-01); l'objectiu aquí és llegir el model.

from tensorflow import keras
from tensorflow.keras import layers

model = 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 categories de producte
])

model.summary()

Desglossament dels arguments de Conv2D(32, (3, 3), padding="same", activation="relu"):

  • 32: nombre de filtres → la sortida tindrà 32 canals.
  • (3, 3): mida del filtre.
  • padding="same": la sortida conserva l'ample i l'alt de l'entrada (l'stride per defecte és 1; es canviaria amb strides=2).
  • activation="relu": l'activació després de cada convolució + biaix, com vam recomanar per a capes ocultes a 02-02.

MaxPooling2D((2, 2)) fa servir finestra 2×2 i, per defecte, stride igual a la finestra (2): redueix a la meitat.

El summary() produeix (formatat):

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

Verifiquem cada línia amb les nostres fórmules (el None és la dimensió del batch, com a 02-05):

  1. conv2d: padding same → continua en 64×64; 32 filtres → 32 canals. Paràmetres: (3·3·3 + 1) × 32 = 896. ✔ (el nostre exemple d'abans)
  2. max_pooling2d: 64/2 = 32 → (32, 32, 32). 0 paràmetres, com tota capa de pooling.
  3. conv2d_1: entrada amb 32 canals → filtres 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 valors. Aplanar aquí és raonable: són 8192 característiques abstractes, no píxels crus.
  6. dense: 8192 × 64 + 64 = 524 352. Observa que la capa densa després del Flatten concentra el 85 % dels paràmetres del model — el motiu pel qual les arquitectures modernes la substitueixen per global average pooling (03-03).
  7. dense_1: 64 × 4 + 4 = 260, amb softmax per a les 4 categories (02-02).

Total: ~618 000 paràmetres per processar imatges de 64×64×3. La xarxa densa de 02-05 en necessitava ~100 000 només per a MNIST de 28×28×1; una densa equivalent per a 64×64×3 es dispararia a milions. I d'aquests 618 000, només 93 248 són a la part convolucional: l'extractor de característiques és baratíssim.

Visualització de feature maps amb matplotlib

Per intuir què fa cada filtre, apliquem una convolució + ReLU i max pooling a una "foto de producte" sintètica (una caixa clara sobre fons fosc, com l'embalatge d'un producte de TecnoMarket) i pintem els mapes resultants. Fem servir filtres fixats a mà perquè el resultat sigui interpretable; en un model entrenat faries el mateix amb els filtres apresos.

import numpy as np
import matplotlib.pyplot as plt

# 1) Imatge sintetica 32x32: una "caixa de producte" clara sobre fons fosc
imatge = np.zeros((32, 32))
imatge[8:24, 10:22] = 1.0          # rectangle clar (el producte)

# 2) Dos filtres de deteccio de vores
filtre_vertical = np.array([[1, 0, -1]] * 3, dtype=float)      # vores verticals
filtre_horitzontal = filtre_vertical.T                         # vores horitzontals

def convolucio2d(imatge, filtre):
    fi, ci = imatge.shape
    ff, cf = filtre.shape
    sortida = np.zeros((fi - ff + 1, ci - cf + 1))
    for i in range(sortida.shape[0]):
        for j in range(sortida.shape[1]):
            sortida[i, j] = np.sum(imatge[i:i + ff, j:j + cf] * filtre)
    return sortida

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

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

# 3) Pipeline conv -> ReLU -> pooling per a cada filtre
mapes = {
    "Vores verticals": max_pooling(relu(convolucio2d(imatge, filtre_vertical))),
    "Vores horitzontals": max_pooling(relu(convolucio2d(imatge, filtre_horitzontal))),
}

# 4) Visualitzacio
fig, eixos = plt.subplots(1, 3, figsize=(12, 4))
eixos[0].imshow(imatge, cmap="gray")
eixos[0].set_title("Imatge original 32x32")
for eix, (nom, mapa) in zip(eixos[1:], mapes.items()):
    eix.imshow(mapa, cmap="gray")
    eix.set_title(f"{nom}\n{mapa.shape[0]}x{mapa.shape[1]} despres del pooling")
plt.tight_layout()
plt.show()

Què veuràs en executar-lo:

  • El mapa de vores verticals s'il·lumina només al llarg del costat esquerre de la caixa (transició fosc→clar amb aquest filtre; el costat dret dona valors negatius que la ReLU posa a zero — un segon filtre amb signes oposats capturaria aquest costat, i per això les capes porten molts filtres).
  • El mapa de vores horitzontals s'il·lumina al costat superior de la caixa.
  • Tots dos mapes són de 15×15: la convolució valid deixa 30×30 i el pooling 2×2 ho redueix a la meitat.

Cada filtre produeix un "mapa d'on és el seu patró", i el pooling condensa aquests mapes conservant les deteccions. Amb els filtres d'un model entrenat, aquesta mateixa tècnica (extreure la sortida d'una capa intermèdia i pintar-la) és una eina habitual per inspeccionar què ha après una CNN.

Errors Comuns i Consells

  • Oblidar els canals en comptar paràmetres. L'error més freqüent: comptar (3·3 + 1) × 64 en comptes de (3·3·32 + 1) × 64. El filtre sempre abasta tots els canals de la seva entrada.
  • Creure que el pooling aprèn alguna cosa. Té 0 paràmetres; és una operació fixa. Redueix mida, no "extreu característiques" per si mateix.
  • Confondre l'efecte de padding i stride. El padding decideix si es perd mida per les vores (same ho evita); l'stride decideix cada quants píxels s'avalua el filtre (stride 2 divideix la mida per 2). Són controls independents.
  • Apilar convs valid sobre imatges petites. Cada capa 3×3 valid resta 2 píxels per dimensió; deu capes sobre una imatge de 28×28 la deixen en 8×8 abans que hagis començat a fer pooling. Per apilar en profunditat, same és la teva amiga.
  • Ignorar on són els paràmetres. Mira sempre el summary(): si una densa després del Flatten concentra la majoria dels paràmetres, aquí tens el principal candidat a sobreajustar (recorda 02-05) i a engreixar el model.
  • Consell: davant de qualsevol summary(), reprodueix a mà les columnes Output Shape i Param # de les dues primeres capes. És l'exercici que consolida més ràpid aquesta lliçó.

Exercicis

Exercici 1: predir formes i paràmetres

Sense executar res, calcula l'Output Shape i el Param # de cada capa d'aquest model per a fotos de TecnoMarket de 32×32 RGB:

model = 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)),
])

Exercici 2: pooling a mà

Aplica max pooling i average pooling (finestra 2×2, stride 2) a aquest mapa de característiques, i explica quin dels dos conservaria millor la detecció d'un patró feble però real que només activa un píxel:

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

Exercici 3: dissenyar la reducció

Les fotos de producte arriben a 128×128×3. Vols una etapa convolucional que acabi en mapes de 16×16 fent servir blocs [Conv2D 3×3 same + MaxPooling2D 2×2]. Quants blocs necessites? Si comences amb 16 filtres i els dupliques a cada bloc, quants canals tindrà la sortida final i quants paràmetres tindrà l'última capa conv?

Solucions

Exercici 1:

Capa Output Shape Càlcul de paràmetres Param #
Conv2D 16 filtres 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 filtres 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àmetres.

Exercici 2:

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

(Màxims: max(2,0,0,0)=2, max(1,1,1,3)=3, max(8,1,2,1)=8, max(0,0,0,0)=0. Mitjanes: 2/4=0.5, 6/4=1.5, 12/4=3.0, 0/4=0.0.)

El max pooling conserva millor una detecció feble però real que activa un sol píxel: el màxim la manté intacta (el 8 sobreviu com a 8), mentre que la mitjana la dilueix entre els veïns inactius (8 → 3.0). Per això max pooling és l'opció per defecte a les etapes intermèdies: interessa saber si el patró ha aparegut, no quant ha activat la zona de mitjana.

Exercici 3:

Cada bloc divideix la mida per 2: 128 → 64 → 32 → 16. Calen 3 blocs. Els filtres: 16 → 32 → 64, així que la sortida final és de 16×16×64 canals. L'última capa conv rep 32 canals i té 64 filtres 3×3: (3·3·32 + 1) × 64 = 289 × 64 = 18 496 paràmetres.

Conclusió

Ja domines la mecànica completa dels dos blocs que formen l'etapa d'extracció d'una CNN. De la capa convolucional en saps configurar les quatre decisions (filtres, mida de kernel, stride, padding), saps que els filtres abasten tots els canals de la seva entrada i que la capa produeix un canal per filtre, i saps predir-ne la sortida amb la fórmula ⌊(n + 2p − f)/s⌋ + 1 i comptar-ne els paràmetres amb (f·f·canals + 1) × filtres — 896 paràmetres allà on una densa equivalent en necessitaria 1600 milions. Del pooling saps que resumeix finestres (max conserva deteccions, average fa la mitjana), que redueix el còmput a una quarta part per bloc sense afegir paràmetres i que regala invariància local. I has traduït tot això a Keras, verificant a mà cada línia d'un summary() i visualitzant feature maps amb matplotlib.

Amb aquestes peces ja pots llegir els plànols de qualsevol CNN. I això és just el que farem a la lliçó següent: recórrer les arquitectures populars que van marcar la història del camp —LeNet, AlexNet, VGG, Inception, ResNet— entenent quina idea va aportar cadascuna i comprovant, summary() en mà, que ja saps desxifrar-les.

Curs de Deep Learning

Mòdul 1: Introducció al Deep Learning

Mòdul 2: Fonaments de Xarxes Neuronals

Mòdul 3: Xarxes Neuronals Convolucionals (CNN)

Mòdul 4: Xarxes Neuronals Recurrents (RNN)

Mòdul 5: Tècniques Avançades en Deep Learning

Mòdul 6: Eines i Frameworks

Mòdul 7: Projectes Pràctics

Mòdul 8: Consideracions Ètiques i Futur del Deep Learning

© Copyright 2026. Tots els drets reservats