Fa dues lliçons que ajornem la gran pregunta: com aprèn els seus pesos una xarxa amb capes ocultes? La regla del perceptró no serveix (ningú no diu a una neurona oculta quina era la seva sortida "correcta") i a la lliçó anterior vam preparar l'ingredient que faltava: activacions amb derivada útil. En aquesta lliçó tanquem el cercle. Primer formalitzarem el forward pass (propagació cap endavant) com una cadena de multiplicacions matricials, reprenent la nostra xarxa 3-4-2-1 de 29 paràmetres. Després construirem, de manera accessible, la noció de gradient i la regla de la cadena, i amb elles entendrem backpropagation: l'algorisme de 1986 que reparteix la culpa de l'error entre totes les capes. El traçarem a mà amb un exemple numèric petit, veurem per què d'aquesta mecànica neixen els problemes de vanishing i exploding gradients, i acabarem entrenant en numpy, des de zero, una xarxa de 2 capes sobre dades fictícies de TecnoMarket.
Contingut
- Forward pass amb matrius: la xarxa 3-4-2-1 en acció
- El gradient: la brúixola de l'error
- La regla de la cadena, explicada sense dolor
- Backpropagation: un exemple numèric traçat a mà
- Vanishing i exploding gradients
- Implementació completa en numpy: xarxa de 2 capes per a TecnoMarket
Forward pass amb matrius: la xarxa 3-4-2-1 en acció
El forward pass és el recorregut de les dades des de l'entrada fins a la sortida. Ja el vam fer a petita escala amb l'MLP de XOR; ara l'escrivim en general. Per a una capa amb matriu de pesos $W$ (una columna per neurona), biaixos $b$ i activació $f$:
$$z = xW + b \qquad a = f(z)$$
on $z$ és la pre-activació (la suma ponderada de cada neurona) i $a$ l'activació (el que surt cap a la capa següent). Una xarxa sencera és simplement aquesta operació repetida capa a capa. Recuperem la xarxa 3-4-2-1 que vam comptar a mà al mòdul 1 (29 paràmetres: 16 + 10 + 3) i donem-li vida:
import numpy as np
rng = np.random.default_rng(0)
# Pesos aleatoris petits (encara sense entrenar), amb les formes de la xarxa 3-4-2-1
W1, b1 = rng.normal(0, 0.5, (3, 4)), np.zeros(4) # entrada(3) -> oculta1(4)
W2, b2 = rng.normal(0, 0.5, (4, 2)), np.zeros(2) # oculta1(4) -> oculta2(2)
W3, b3 = rng.normal(0, 0.5, (2, 1)), np.zeros(1) # oculta2(2) -> sortida(1)
def relu(z): return np.maximum(0, z)
def sigmoide(z): return 1 / (1 + np.exp(-z))
# Una comanda de TecnoMarket: [import_norm, antiguitat_norm, discrepancia_adreces]
x = np.array([[0.9, 0.1, 1.0]]) # forma (1, 3): 1 exemple, 3 característiques
a1 = relu(x @ W1 + b1) # forma (1, 4)
a2 = relu(a1 @ W2 + b2) # forma (1, 2)
y_hat = sigmoide(a2 @ W3 + b3) # forma (1, 1): probabilitat de frau
print(y_hat) # p. ex. [[0.55]] -> encara no sap resTres observacions clau:
- Les formes encaixen com a fitxes de dòmino: (1,3)·(3,4) → (1,4); (1,4)·(4,2) → (1,2); (1,2)·(2,1) → (1,1). Verificar
.shapea cada pas és el millor detector d'errors. - Processar un batch surt gratis: si
xté forma (32, 3) —32 comandes alhora—, el mateix codi retorna (32, 1) sense canviar ni una línia. Aquesta és la raó pràctica d'organitzar les dades en batches (vocabulari de 01-04) i que les GPU brillin aquí. - La sortida amb pesos aleatoris ronda 0.5: la xarxa encara és una moneda a l'aire. Entrenar consisteix a moure aquests 29 paràmetres fins que les sortides coincideixin amb les etiquetes.
graph LR
X[x - 1x3] -->|"W1 (3x4)"| A1[a1 - 1x4]
A1 -->|"W2 (4x2)"| A2[a2 - 1x2]
A2 -->|"W3 (2x1)"| Y[y_hat - 1x1]
El gradient: la brúixola de l'error
Per entrenar necessitem mesurar l'error amb un nombre. Farem servir de moment l'error quadràtic $L = (\hat{y} - y)^2$ (les funcions de pèrdua en detall són el tema de la propera lliçó; amb aquesta en tenim prou).
La pregunta central de l'aprenentatge és: si moc un pes concret $w$ una miqueta, l'error puja o baixa, i quant? La resposta és la derivada parcial $\frac{\partial L}{\partial w}$. El vector amb totes aquestes derivades —una per paràmetre— és el gradient, i apunta en la direcció en què l'error creix més de pressa. Per tant, per reduir l'error caminem en direcció contrària:
$$w \leftarrow w - \eta \frac{\partial L}{\partial w}$$
Això és el descens del gradient, i és la versió matemàtica exacta de l'analogia de l'aixeta de la dutxa: la derivada diu cap a on girar (surt massa calenta o massa freda?) i $\eta$ (el learning rate) quant girar cada vegada. Compara-la amb la regla del perceptró de 02-01: aquella era un cas particular rígid; aquesta funciona per a qualsevol xarxa derivable.
El problema pràctic: la nostra petita xarxa 3-4-2-1 té 29 derivades a calcular, i una xarxa real en té milions. Calcular cadascuna per separat seria inviable. Backpropagation és, simplement, la manera eficient de calcular-les totes d'una passada, reutilitzant càlculs. I el seu motor és la regla de la cadena.
La regla de la cadena, explicada sense dolor
Pensa en una cadena de causes i efectes dins de la xarxa:
el pes $w_1$ afecta la pre-activació $z_1$, que afecta l'activació $h$, que afecta la sortida $\hat{y}$, que afecta l'error $L$.
La regla de la cadena diu que l'efecte total de $w_1$ sobre $L$ és el producte dels efectes de cada baula:
$$\frac{\partial L}{\partial w_1} = \frac{\partial L}{\partial \hat{y}} \cdot \frac{\partial \hat{y}}{\partial z_2} \cdot \frac{\partial z_2}{\partial h} \cdot \frac{\partial h}{\partial z_1} \cdot \frac{\partial z_1}{\partial w_1}$$
Una analogia TecnoMarket: si el departament de compres negocia malament un preu (causa), el cost del producte puja, el marge baixa, el benefici trimestral cau. Quanta culpa té compres del resultat trimestral? La culpa es propaga multiplicant-se baula a baula per la cadena. Backpropagation fa exactament això: comença per l'error final i va repartint la culpa cap enrere, capa per capa — d'aquí el nom, propagació cap enrere. I és eficient perquè els primers factors de la cadena (els propers a la sortida) es calculen un cop i es reutilitzen per a tots els pesos anteriors.
Només necessites tres derivades elementals, totes ja conegudes:
| Baula | Derivada | Lectura |
|---|---|---|
| $L = (\hat{y}-y)^2$ respecte a $\hat{y}$ | $2(\hat{y}-y)$ | Com més lluny de l'objectiu, més culpa entra |
| Sigmoide $\sigma(z)$ respecte a $z$ | $\sigma(z)(1-\sigma(z))$ | La vas calcular a l'exercici 1 de la lliçó anterior |
| $z = wh + b$ respecte a $w$, $b$, $h$ | $h$, $1$, $w$ | La culpa d'un pes és proporcional al senyal que li va arribar |
Backpropagation: un exemple numèric traçat a mà
Farem una iteració completa, amb nombres, sobre la xarxa més petita possible amb capa oculta: 1 entrada → 1 neurona oculta (sigmoide) → 1 sortida (sigmoide). Dades: entrada $x = 1.0$, etiqueta $y = 1$ (és frau). Pesos inicials: $w_1 = 0.6$, $b_1 = 0$, $w_2 = 0.4$, $b_2 = 0$. Learning rate $\eta = 0.5$.
Forward pass (guardem tots els valors intermedis: els necessitarem a la tornada):
| Pas | Càlcul | Valor |
|---|---|---|
| $z_1 = w_1 x + b_1$ | $0.6 \times 1.0$ | $0.600$ |
| $h = \sigma(z_1)$ | $\sigma(0.6)$ | $0.646$ |
| $z_2 = w_2 h + b_2$ | $0.4 \times 0.646$ | $0.258$ |
| $\hat{y} = \sigma(z_2)$ | $\sigma(0.258)$ | $0.564$ |
| $L = (\hat{y}-y)^2$ | $(0.564-1)^2$ | $0.190$ |
La xarxa diu "56 % de probabilitat de frau" quan hauria de dir gairebé 100 %. Repartim la culpa.
Backward pass, de la sortida cap a l'entrada:
- Culpa de la sortida: $\frac{\partial L}{\partial \hat{y}} = 2(\hat{y}-y) = 2(0.564-1) = -0.872$ (negativa: cal pujar $\hat{y}$).
- Travessem la sigmoide de sortida: $\frac{\partial \hat{y}}{\partial z_2} = \hat{y}(1-\hat{y}) = 0.564 \times 0.436 = 0.246$. Acumulat: $\delta_2 = -0.872 \times 0.246 = -0.214$.
- Culpes dels paràmetres de la capa 2: $\frac{\partial L}{\partial w_2} = \delta_2 \cdot h = -0.214 \times 0.646 = -0.139$; $\frac{\partial L}{\partial b_2} = \delta_2 = -0.214$.
- La culpa que baixa cap a la capa oculta viatja pel pes: $\frac{\partial L}{\partial h} = \delta_2 \cdot w_2 = -0.214 \times 0.4 = -0.086$.
- Travessem la sigmoide oculta: $\frac{\partial h}{\partial z_1} = h(1-h) = 0.646 \times 0.354 = 0.229$. Acumulat: $\delta_1 = -0.086 \times 0.229 = -0.020$.
- Culpes de la capa 1: $\frac{\partial L}{\partial w_1} = \delta_1 \cdot x = -0.020$; $\frac{\partial L}{\partial b_1} = \delta_1 = -0.020$.
Actualització ($w \leftarrow w - \eta \cdot \text{gradient}$):
$$w_2 = 0.4 - 0.5(-0.139) = 0.470 \qquad b_2 = 0 - 0.5(-0.214) = 0.107$$ $$w_1 = 0.6 - 0.5(-0.020) = 0.610 \qquad b_1 = 0 - 0.5(-0.020) = 0.010$$
Comprovació: repetint el forward amb els pesos nous, $\hat{y}$ puja de $0.564$ a $\approx 0.594$ i la pèrdua baixa de $0.190$ a $\approx 0.165$. La xarxa ha après una miqueta. Repetir això milers de vegades (sobre molts exemples) és entrenar.
Fixa't en un detall revelador del pas 3 davant del pas 6: el gradient de $w_2$ ($-0.139$) és unes 7 vegades més gran que el de $w_1$ ($-0.020$). La capa més allunyada de la sortida rep menys senyal de correcció. Aquest detall és la porta de l'apartat següent.
Vanishing i exploding gradients
Cada vegada que la culpa retrocedeix una capa, es multiplica per dues coses: la derivada de l'activació i els pesos. En el nostre traçat, travessar cada sigmoide va multiplicar el senyal per ~0.23–0.25. Recorda de la lliçó anterior que la derivada de la sigmoide mai no supera 0.25. Llavors, en una xarxa de 10 capes sigmoides:
$$0.25^{10} \approx 0.00000095$$
La culpa que arriba a les primeres capes és pràcticament zero: no aprenen. És el vanishing gradient (esvaïment del gradient), i és la raó històrica que durant els 90 fos gairebé impossible entrenar xarxes profundes, i una de les raons del triomf de ReLU (la seva derivada és 1 a la zona positiva: el senyal no s'encongeix en travessar-la).
El fenomen invers també existeix: si els pesos són grans (més grans que 1 en magnitud), el senyal s'amplifica a cada capa i els gradients es disparen a valors enormes — exploding gradient — produint actualitzacions gegants que destrossen l'entrenament (veuràs pèrdues que salten a nan). És especialment típic de les xarxes recurrents, on la "profunditat" és la longitud de la seqüència (ho reprendrem al mòdul 4), i les tècniques modernes de mitigació es tracten al mòdul 5.
| Problema | Causa | Símptoma | Mitigació (avanç) |
|---|---|---|---|
| Vanishing | Derivades < 1 multiplicades moltes vegades | Les primeres capes no canvien; la pèrdua s'estanca | ReLU, bona inicialització, arquitectures del mòdul 5 |
| Exploding | Pesos grans multiplicats moltes vegades | La pèrdua oscil·la salvatgement o es fa nan |
Baixar $\eta$, retall de gradient (mòdul 5) |
Implementació completa en numpy: xarxa de 2 capes per a TecnoMarket
Ajuntem-ho tot: una xarxa 3-8-1 (3 entrades, 8 ocultes amb sigmoide, 1 sortida sigmoide) entrenada amb descens del gradient per estimar la probabilitat de frau d'una comanda. És el salt de qualitat respecte al perceptró de 02-01: entrades contínues, sortida amb matisos i capes ocultes entrenades de debò.
Primer, dades fictícies de TecnoMarket generades sintèticament (400 comandes):
import numpy as np
rng = np.random.default_rng(7)
n = 400
import_ = rng.uniform(0, 1, n) # import normalitzat (0 = 0 EUR, 1 = 2000 EUR)
antiguitat = rng.uniform(0, 1, n) # antiguitat del compte (0 = nou, 1 = 10 anys)
discrep = rng.integers(0, 2, n) # 1 si les adreces d'enviament/facturacio difereixen
# Regla oculta (que la xarxa haura de descobrir): risc alt si la comanda es cara
# I el compte es nou, agreujat per la discrepancia d'adreces
risc = 2.2*import_ - 2.0*antiguitat + 1.2*discrep - 0.6
y = (risc + rng.normal(0, 0.3, n) > 0).astype(float).reshape(-1, 1)
X = np.column_stack([import_, antiguitat, discrep]) # forma (400, 3)Ara la xarxa. Tot el backward pass és la versió matricial exacta dels 6 passos que vam traçar a mà:
def sigmoide(z): return 1 / (1 + np.exp(-z))
# Inicialitzacio: valors petits aleatoris (mai zeros a les ocultes)
W1 = rng.normal(0, 0.5, (3, 8)); b1 = np.zeros((1, 8))
W2 = rng.normal(0, 0.5, (8, 1)); b2 = np.zeros((1, 1))
eta, epoques = 0.5, 3000
m = X.shape[0] # nombre d'exemples
for epoca in range(epoques):
# ---- FORWARD: guardem els intermedis per al backward ----
z1 = X @ W1 + b1 # (400, 8)
h = sigmoide(z1) # (400, 8)
z2 = h @ W2 + b2 # (400, 1)
y_hat = sigmoide(z2) # (400, 1)
perdua = np.mean((y_hat - y) ** 2) # error quadratic mitja
# ---- BACKWARD: els mateixos 6 passos del tracat a ma ----
d_yhat = 2 * (y_hat - y) / m # pas 1 (el /m fa la mitjana del batch)
delta2 = d_yhat * y_hat * (1 - y_hat) # pas 2: travessar la sigmoide de sortida
dW2 = h.T @ delta2 # pas 3: culpa de W2 (8, 1)
db2 = delta2.sum(axis=0, keepdims=True)
d_h = delta2 @ W2.T # pas 4: la culpa baixa pels pesos
delta1 = d_h * h * (1 - h) # pas 5: travessar la sigmoide oculta
dW1 = X.T @ delta1 # pas 6: culpa de W1 (3, 8)
db1 = delta1.sum(axis=0, keepdims=True)
# ---- ACTUALITZACIO: descens del gradient ----
W1 -= eta * dW1; b1 -= eta * db1
W2 -= eta * dW2; b2 -= eta * db2
if epoca % 500 == 0:
encert = np.mean((y_hat > 0.5) == y)
print(f"Epoca {epoca:4d} | perdua {perdua:.4f} | encert {encert:.2%}")Sortida típica:
Epoca 0 | perdua 0.2531 | encert 52.25%
Epoca 500 | perdua 0.1025 | encert 86.50%
Epoca 1000 | perdua 0.0668 | encert 91.75%
Epoca 2000 | perdua 0.0525 | encert 93.50%
Epoca 2500 | perdua 0.0499 | encert 94.00%De moneda a l'aire (52 %) a un classificador decent (94 %), sense que ningú programés la regla de risc: la xarxa l'ha extreta de les dades via forward + backward + actualització. Provem-la com ho faria l'equip antifrau:
def prob_frau(comanda):
h = sigmoide(comanda @ W1 + b1)
return sigmoide(h @ W2 + b2)[0, 0]
print(prob_frau(np.array([[0.95, 0.05, 1]]))) # cara, compte nou, adreces diferents -> ~0.98
print(prob_frau(np.array([[0.30, 0.90, 0]]))) # barata, client vetera -> ~0.02Les correspondències codi ↔ traçat a mà mereixen una segona lectura: h.T @ delta2 és "la culpa d'un pes és el senyal que li va arribar multiplicat per la culpa de la seva neurona", sumada sobre els 400 exemples del batch; delta2 @ W2.T és "la culpa baixa cap a la capa anterior viatjant pels pesos". Si entens aquestes dues línies, entens backpropagation.
Errors Comuns i Consells
- No guardar els valors intermedis del forward. El backward necessita
h,y_hat, etc. Si els recalcules o els perds, o el codi es complica o es torna lent. Estructura sempre: el forward guarda → el backward consumeix. - Inicialitzar tots els pesos a zero. Al perceptró funcionava; amb capes ocultes és fatal: totes les neurones d'una capa reben el mateix gradient, aprenen el mateix i la capa es comporta com una sola neurona (el problema de la simetria). Inicialitza amb valors aleatoris petits.
- Confondre el signe de l'actualització. És $w \mathrel{-}= \eta \cdot dW$ (restar: descendir). Un
+=accidental fa que la pèrdua pugi i és un despiste sorprenentment freqüent. - Errors de dimensions al backward. Regla d'or: el gradient
dWha de tenir exactament la mateixa forma queW. ComprovadW1.shape == W1.shapecom a test ràpid. - Pèrdua que es torna
nan. Gairebé sempre exploding gradient o learning rate massa alt. Baixa $\eta$ un ordre de magnitud i revisa la inicialització. - Verificar el gradient numèricament quan dubtis. Aproxima $\frac{\partial L}{\partial w} \approx \frac{L(w+\epsilon) - L(w-\epsilon)}{2\epsilon}$ amb $\epsilon = 10^{-5}$ per a un pes concret i compara'l amb el teu backward. Si no coincideixen en diverses xifres, hi ha un bug al backward.
Exercicis
- Segona iteració a mà. Continua l'exemple traçat a mà: amb els pesos actualitzats ($w_1=0.610$, $b_1=0.010$, $w_2=0.470$, $b_2=0.107$), fes el forward complet i calcula la nova pèrdua. Comprova que ha baixat respecte a 0.190.
- Sentir el vanishing gradient. A la xarxa numpy de TecnoMarket, canvia l'arquitectura a 4 capes ocultes sigmoides de 8 neurones (en lloc d'1) i entrena amb els mateixos hiperparàmetres. Imprimeix a l'època 0 la magnitud mitjana dels gradients de la primera i de l'última capa oculta (
np.abs(dW).mean()). Quantes vegades més petit és el de la primera capa? Què passa amb la velocitat d'aprenentatge? - Learning rate extrem. Torna a la xarxa 3-8-1 original i entrena amb
eta = 20. Descriu què observes a la pèrdua i explica-ho amb el vocabulari d'aquesta lliçó. I ambeta = 0.001?
Solucions
Exercici 1. Forward amb els pesos nous: $z_1 = 0.610 \times 1.0 + 0.010 = 0.620$; $h = \sigma(0.620) = 0.650$; $z_2 = 0.470 \times 0.650 + 0.107 = 0.413$; $\hat{y} = \sigma(0.413) = 0.602$; $L = (0.602 - 1)^2 = 0.158$. La pèrdua baixa de 0.190 a ≈0.158 (els decimals exactes poden variar lleument segons l'arrodoniment que arrosseguis): el descens del gradient ha funcionat.
Exercici 2. En apilar 4 capes sigmoides, a l'època 0 el gradient mitjà de la primera capa sol ser entre 20 i 100 vegades més petit que el de l'última (cada sigmoide travessada multiplica el senyal per ≤ 0.25, més l'efecte dels pesos petits). L'entrenament es torna molt més lent i la pèrdua pot quedar-se estancada a prop del seu valor inicial durant centenars d'èpoques: estàs veient el vanishing gradient en directe. Substituir les sigmoides ocultes per ReLU (i la seva derivada, (z > 0).astype(float)) millora la situació notablement.
Exercici 3. Amb eta = 20 la pèrdua oscil·la salvatgement o creix fins a estabilitzar-se en un valor dolent (o directament apareix nan si els gradients exploten): cada actualització és un gir d'aixeta tan gran que saltem de "gelada" a "bullint" sense passar pel punt bo. Amb eta = 0.001 la pèrdua baixa de manera monòtona però lentíssima: després de 3000 èpoques l'encert amb prou feines ha millorat. El learning rate és un compromís; a la propera lliçó veurem tècniques (i optimitzadors) que el gestionen millor que un valor fix.
Conclusió
Ja tens el cor del deep learning complet: el forward pass converteix entrades en prediccions mitjançant multiplicacions matricials encadenades; la pèrdua resumeix l'error en un nombre; el gradient diu cap a on moure cada paràmetre; la regla de la cadena permet calcular-lo capa a capa repartint la culpa cap enrere — això és backpropagation, l'algorisme que el 1986 va treure les xarxes del seu primer hivern; i el descens del gradient aplica la correcció, gir suau d'aixeta a gir suau d'aixeta. Ho has vist en fórmules, traçat a mà nombre a nombre, i funcionant en numpy sobre comandes de TecnoMarket. També has vist que la mateixa mecànica multiplicativa del backward engendra les seves patologies: gradients que s'esvaeixen o exploten.
En el nostre entrenament hem fet servir la pèrdua i l'optimitzador més simples possibles: error quadràtic i descens del gradient pur sobre totes les dades alhora. La propera lliçó examina aquestes dues eleccions amb lupa: quines funcions de pèrdua existeixen i quina correspon a cada problema, i quins optimitzadors (mini-batch, momentum, RMSprop, Adam) fan que el descens sigui més ràpid i estable. Amb això tindràs totes les peces per muntar la teva primera xarxa completa en Keras al final del mòdul.
Curs de Deep Learning
Mòdul 1: Introducció al Deep Learning
- Què és el Deep Learning?
- Història i evolució del Deep Learning
- Aplicacions del Deep Learning
- Conceptes bàsics de xarxes neuronals
- Preparació de l'entorn de treball
Mòdul 2: Fonaments de Xarxes Neuronals
- Perceptró i Perceptró Multicapa
- Funció d'activació
- Propagació cap endavant i cap enrere
- Optimització i funció de pèrdua
- La teva primera xarxa neuronal completa
Mòdul 3: Xarxes Neuronals Convolucionals (CNN)
- Introducció a les CNN
- Capes convolucionals i de pooling
- Arquitectures populars de CNN
- Aplicacions de CNN en reconeixement d'imatges
Mòdul 4: Xarxes Neuronals Recurrents (RNN)
- Introducció a les RNN
- LSTM i GRU
- Aplicacions de RNN en processament del llenguatge natural
- Seqüències i sèries temporals
Mòdul 5: Tècniques Avançades en Deep Learning
- Xarxes Generatives Adversàries (GAN)
- Autoencoders
- Transfer Learning
- Regularització i tècniques de millora
- Mecanismes d'atenció i Transformers
Mòdul 6: Eines i Frameworks
- Introducció a TensorFlow
- Introducció a PyTorch
- Comparació de frameworks
- Entorns de desenvolupament i recursos addicionals
- Desar, carregar i desplegar models
Mòdul 7: Projectes Pràctics
- Classificació d'imatges amb CNN
- Generació de text amb RNN
- Detecció d'anomalies amb Autoencoders
- Creació d'una GAN per a generació d'imatges
- Fine-tuning d'un model preentrenat
