A la lliçó anterior vam descobrir que la SimpleRNN té memòria de peix: el gradient que viatja cap enrere a través del temps s'esvaeix i, amb ell, la capacitat d'aprendre dependències a més d'uns pocs passos de distància. Aquesta lliçó presenta les dues cel·les que van resoldre aquest problema i que sostenen la majoria d'aplicacions reals de seqüències: la LSTM (Long Short-Term Memory, 1997) i la seva simplificació moderna, la GRU (Gated Recurrent Unit, 2014). Entendràs la LSTM peça a peça —el seu estat de cel·la i les seves tres portes—, en veuràs el paral·lelisme amb les skip connections de ResNet, en comptaràs els paràmetres, i comprovaràs experimentalment en Keras com una LSTM resol una tasca de memòria llarga en què la SimpleRNN fracassa. Tancarem amb dues eines d'arquitectura que faràs servir constantment: capes apilades (return_sequences) i capes bidireccionals.

Contingut

  1. El problema de la memòria a llarg termini
  2. La cel·la LSTM: l'estat de cel·la com a cinta transportadora
  3. Les tres portes, una a una
  4. L'autopista del gradient: paral·lelisme amb ResNet
  5. GRU: la simplificació que gairebé sempre basta
  6. Comparativa SimpleRNN / LSTM / GRU
  7. Recompte de paràmetres d'una LSTM
  8. Experiment en Keras: SimpleRNN vs. LSTM en memòria llarga
  9. Apilar capes recurrents i Bidirectional

El problema de la memòria a llarg termini

Llegeix aquesta ressenya de l'estil que rep TecnoMarket:

"Vaig comprar aquesta nevera fa tres mesos. El procés de compra va ser còmode, el transportista va ser puntual, l'embalatge va arribar impecable i la instal·lació va ser senzilla... però des de la setmana passada no refreda."

Per classificar-la correctament, el model ha de connectar "nevera" (paraula 3) i "no refreda" (paraules 30+) saltant per sobre d'una llarga llista de comentaris positius. Una SimpleRNN reescriu el seu estat ocult sencer a cada pas (h_t = tanh(...)): cada paraula nova trepitja l'anterior, i després de 25 passos d'elogis logístics, la "nevera" del principi s'ha diluït. I durant l'entrenament passa el fenomen simètric: el gradient de l'error ("has dit positiva i era negativa") s'esvaeix abans d'arribar als primers passos, així que la xarxa ni tan sols aprèn que havia de recordar.

La solució de la LSTM no és "recordar més fort", sinó una cosa més elegant: separar la memòria del processament i posar l'accés a aquesta memòria sota el control de portes apreses.

La cel·la LSTM: l'estat de cel·la com a cinta transportadora

Una LSTM manté dos vectors d'estat en lloc d'un:

  • h_t — l'estat ocult, com a la SimpleRNN: la "sortida de treball" de cada pas.
  • C_t — l'estat de cel·la: la memòria a llarg termini.

La imatge clàssica per a C_t és una cinta transportadora que recorre tota la seqüència: la informació hi viatja d'un pas al següent gairebé sense tocar-la — només dues operacions suaus (una multiplicació i una suma) la modifiquen. Res no la reescriu del tot. Als costats de la cinta hi ha tres portes: petits mecanismes que decideixen què es retira de la cinta, què s'hi puja i què s'hi consulta.

Una porta és simplement una minicapa amb activació sigmoide:

porta = σ(W · [h_{t-1}, x_t] + b)

La sigmoide (02-02) produeix valors entre 0 i 1 per cada component de la memòria: 0 significa "bloqueja el pas" i 1 "deixa-ho passar tot". És el mateix paper que una aixeta — l'analogia del curs —: la xarxa aprèn quant obrir cada aixeta segons el context (h_{t-1} i x_t), no amb un valor fix.

graph LR
    subgraph "Cel·la LSTM al pas t"
        Cprev["C_{t-1}<br>(cinta: memòria)"] --> MUL1(("×"))
        F["Porta d'oblit f_t<br>σ: què esborro?"] --> MUL1
        MUL1 --> ADD(("+"))
        I["Porta d'entrada i_t<br>σ: què escric?"] --> MUL2(("×"))
        CAND["Candidat C̃_t<br>tanh: contingut nou"] --> MUL2
        MUL2 --> ADD
        ADD --> Cnew["C_t<br>(cinta actualitzada)"]
        Cnew --> TANH["tanh"]
        TANH --> MUL3(("×"))
        O["Porta de sortida o_t<br>σ: què mostro?"] --> MUL3
        MUL3 --> Hnew["h_t<br>(estat ocult)"]
    end
    X["x_t i h_{t-1}"] -.alimenten.-> F
    X -.-> I
    X -.-> CAND
    X -.-> O

Les tres portes, una a una

  1. Porta d'oblit (forget gate): què esborro de la cinta?

f_t = σ(Wf · [h_{t-1}, x_t] + bf)

Mira l'entrada actual i el context, i decideix quins components de C_{t-1} conservar (valors a prop d'1) i quins buidar (a prop de 0). S'aplica multiplicant: f_t ⊙ C_{t-1} (⊙ = component a component).

Intuïció TecnoMarket: si la ressenya diu "D'altra banda, el segon producte...", convé oblidar els detalls del primer producte per fer lloc.

  1. Porta d'entrada (input gate): què escric a la cinta?

Treballa en parella amb un candidat a memòria nova:

i_t  = σ(Wi · [h_{t-1}, x_t] + bi)       # quant escriure (0-1)
C̃_t = tanh(Wc · [h_{t-1}, x_t] + bc)    # quin contingut escriure (-1 a 1)

El candidat C̃_t proposa informació nova (és, de fet, la fórmula de la SimpleRNN); la porta i_t decideix quines parts d'aquesta proposta mereixen entrar a la memòria. L'actualització completa de la cinta és:

C_t = f_t ⊙ C_{t-1}  +  i_t ⊙ C̃_t
      (el que conservo)  (el que afegeixo)

Intuïció: en llegir "nevera", la porta d'entrada escriu "el producte és una nevera" a la cinta; durant els elogis al transportista, i_t pot mantenir-se gairebé tancada i f_t gairebé oberta: la cinta passa intacta.

  1. Porta de sortida (output gate): què consulto de la cinta?

o_t = σ(Wo · [h_{t-1}, x_t] + bo)
h_t = o_t ⊙ tanh(C_t)

L'estat ocult h_t (el que la cel·la "mostra" a l'exterior i al pas següent) és una vista filtrada de la memòria: la porta de sortida decideix quines parts de la cinta són rellevants ara mateix. No tot el que s'ha memoritzat cal a cada pas: pots recordar que el producte és una nevera sense mencionar-ho fins que arribi el "no refreda".

Resum del flux: oblido allò obsolet (f_t), incorporo el que és nou i rellevant (i_t ⊙ C̃_t), i exposo la part útil de la meva memòria (o_t). Tots els pesos de les portes s'aprenen per retropropagació, exactament igual que la resta de la xarxa: ningú no programa a mà què cal oblidar.

L'autopista del gradient: paral·lelisme amb ResNet

Torna a mirar l'equació de la cinta: C_t = f_t ⊙ C_{t-1} + i_t ⊙ C̃_t. La memòria anterior arriba al present mitjançant una suma, no travessant una tanh i una multiplicació per Wh com a la SimpleRNN. Et sona? És la mateixa jugada que les skip connections de ResNet (03-03): allà, sortida = F(x) + x creava una drecera perquè el gradient creués desenes de capes en l'espai; aquí, la cinta crea una drecera perquè el gradient creui desenes de passos en el temps. Mentre la porta d'oblit estigui raonablement oberta (f_t ≈ 1), el gradient flueix cap enrere per la cinta gairebé sense atenuar-se: una autopista del gradient temporal.

És una idea recurrent en deep learning que convé fixar: quan el gradient mor pel camí (vanishing, 02-03), la solució sol ser donar-li un camí additiu directe. ResNet ho va fer en profunditat, la LSTM en el temps, i les connexions residuals dels transformers (05-05) repeteixen el patró.

GRU: la simplificació que gairebé sempre basta

La GRU es pregunta: de debò calen dos estats i tres portes? La seva resposta: fusiona C_t i h_t en un únic estat h_t, i fa servir dues portes:

  • Porta d'actualització (z_t, update): combina en una de sola les funcions d'oblidar i d'escriure. L'actualització és una interpolació:

    h_t = (1 - z_t) ⊙ h_{t-1} + z_t ⊙ h̃_t
    

    Si z_t ≈ 0, l'estat passa intacte (memòria conservada); si z_t ≈ 1, se substitueix pel candidat nou. Fixa't que continua sent una via additiva: l'autopista del gradient es conserva.

  • Porta de reinici (r_t, reset): en calcular el candidat h̃_t = tanh(W · [r_t ⊙ h_{t-1}, x_t]), decideix quant context passat fer servir per proposar contingut nou. Amb r_t ≈ 0, el candidat ignora el passat i "comença de zero" localment.

Menys portes significa ~25 % menys paràmetres i un entrenament una mica més ràpid, amb un rendiment habitualment comparable al de la LSTM. A la pràctica: prova la GRU quan el dataset és petit o el temps de còmput importa; prova la LSTM quan la tasca exigeix una memòria molt fina o hi ha dades de sobres. No hi ha un guanyador universal — és una decisió empírica.

Comparativa SimpleRNN / LSTM / GRU

Aspecte SimpleRNN LSTM GRU
Estats 1 (h) 2 (h i C) 1 (h)
Portes 0 3 (oblit, entrada, sortida) 2 (update, reset)
Paràmetres (entrada d, unitats u) u(d+u+1) 4·u(d+u+1) 3·u(d+u+1)
Memòria efectiva ~5-10 passos Centenars de passos Centenars de passos
Cost per pas Baix Alt (×4) Mitjà (×3)
Vanishing en el temps Greu Mitigat (cinta additiva) Mitigat (interpolació)
Quan usar-la Seqüències molt curtes, demos didàctiques Dependències llargues i complexes, datasets grans Opció per defecte: bon equilibri qualitat/cost

Recompte de paràmetres d'una LSTM

Una LSTM calcula 4 blocs amb la mateixa estructura (les 3 portes + el candidat), cadascun amb la seva matriu per a l'entrada, la seva matriu recurrent i el seu biaix. Amb entrada de dimensió d i u unitats:

paràmetres = 4 × (d·u + u·u + u) = 4·u·(d + u + 1)

Exemple: LSTM(64) amb entrades de 32 característiques per pas:

4 × 64 × (32 + 64 + 1) = 4 × 64 × 97 = 24 832 paràmetres

Exactament 4 vegades els que tindria una SimpleRNN(64) equivalent (6 208), i com sempre en les recurrents, independent de la longitud de la seqüència. Verifica-ho amb model.summary(), com vam fer amb les CNN a 03-02.

Experiment en Keras: SimpleRNN vs. LSTM en memòria llarga

Dissenyem una tasca que exigeix memòria llarga: en una seqüència de 50 números aleatoris, l'objectiu és recuperar la suma dels 2 primers valors. Tota la informació útil és al principi; els 48 passos restants són soroll que la xarxa ha de travessar sense oblidar.

import numpy as np
from tensorflow import keras
from tensorflow.keras import layers

# Dataset sintètic de memòria llarga
rng = np.random.default_rng(0)
PASSOS = 50
X = rng.uniform(0, 1, size=(5000, PASSOS, 1))
y = X[:, :2, 0].sum(axis=1)          # objectiu: suma dels DOS PRIMERS passos

def entrenar(capa_recurrent, nom):
    model = keras.Sequential([
        layers.Input(shape=(PASSOS, 1)),
        capa_recurrent,
        layers.Dense(1)
    ])
    model.compile(optimizer="adam", loss="mse", metrics=["mae"])
    h = model.fit(X, y, epochs=25, batch_size=64,
                  validation_split=0.2, verbose=0)
    print(f"{nom:>10}: MAE validació = {h.history['val_mae'][-1]:.3f}")

entrenar(layers.SimpleRNN(32), "SimpleRNN")
entrenar(layers.LSTM(32),      "LSTM")

Resultats típics (variaran lleugerament a la teva màquina):

 SimpleRNN: MAE validació = 0.39   # ≈ predir sempre la mitjana: NO ha après
      LSTM: MAE validació = 0.05   # memòria intacta després de 48 passos de soroll

Interpretació: la suma de dues uniformes a [0,1] té mitjana 1.0 i un predictor "babau" que sempre digués 1.0 aconseguiria un MAE de ~0.4 — just on es queda la SimpleRNN. El gradient no va aconseguir mai arribar als primers passos, així que la xarxa es va rendir i prediu la mitjana. La LSTM, gràcies a la cinta, escriu els dos primers valors a C_t, manté la porta d'oblit oberta durant 48 passos i els recupera al final. Aquest experiment en miniatura és la raó que gairebé ningú no faci servir SimpleRNN en producció.

Apilar capes recurrents i Bidirectional

Capes apilades: return_sequences=True

Igual que apilàvem capes Conv2D per passar de vores a textures i objectes (03-01), podem apilar capes recurrents perquè la segona processi representacions més abstractes de la seqüència. El detall tècnic: una capa recurrent per defecte retorna només el seu últim estat, però la capa següent necessita una seqüència completa com a entrada. La solució és return_sequences=True a totes les capes menys l'última:

model = keras.Sequential([
    layers.Input(shape=(50, 1)),
    layers.LSTM(64, return_sequences=True),   # retorna els 50 estats: (50, 64)
    layers.LSTM(32),                          # retorna només l'últim: (32,)
    layers.Dense(1)
])

Regla mnemotècnica: return_sequences=True = "passa-li la pel·lícula sencera a la capa següent"; False (per defecte) = "queda't amb el fotograma final".

Bidirectional: llegir en els dos sentits

En moltes tasques, el context futur també ajuda: a "no m'ha arribat encara", l'"encara" suavitza la queixa, però arriba després. Bidirectional duplica la capa: una còpia llegeix la seqüència d'esquerra a dreta i una altra de dreta a esquerra, i les seves sortides es concatenen (duplicant la dimensió de sortida i els paràmetres):

model = keras.Sequential([
    layers.Input(shape=(50, 8)),
    layers.Bidirectional(layers.LSTM(32)),    # sortida: 64 números (32 + 32)
    layers.Dense(1, activation="sigmoid")
])

Fes-la servir quan disposis de la seqüència completa abans de decidir (classificar una ressenya ja escrita: sí). Evita-la quan prediguis el futur en temps real (demanda de demà: el "sentit invers" llegiria dades que encara no existeixen). Reprendrem aquesta distinció a 04-04.

Errors Comuns i Consells

  • Apilar recurrents sense return_sequences=True. L'error expected ndim=3, found ndim=2 a la segona LSTM gairebé sempre és això: la primera capa ha retornat només l'últim estat. Activa'l a totes les capes recurrents excepte l'última (si el problema és many-to-one).
  • Confondre h_t amb C_t. L'estat ocult és la vista de treball (el que surt de la cel·la); l'estat de cel·la és la memòria interna de la LSTM. Keras gestiona tots dos automàticament: tu només veus h_t.
  • Triar LSTM "perquè és la bona" sense comparar. Amb seqüències curtes o poques dades, una GRU (o fins i tot una densa sobre estadístiques agregades) pot rendir igual amb menys cost. Compara sempre contra l'opció simple, com farem amb el baseline a 04-04.
  • Fer servir Bidirectional en predicció de futur. És una fuga d'informació conceptual: en producció no tindràs el "futur" per llegir-lo a l'inrevés. Reserva-la per a seqüències completes ja disponibles.
  • Pensar que la LSTM elimina el vanishing del tot. El mitiga enormement, però amb seqüències de milers de passos també en pateix. Per a aquests extrems van sorgir els mecanismes d'atenció, que mencionarem a 04-03 i desenvoluparem a 05-05.

Exercicis

  1. Portes en acció: per a la ressenya "La nevera va arribar ràpid i ben embalada, però no refreda", descriu què esperaries que fessin (obertes/tancades, què escriuen o esborren) les portes d'oblit i d'entrada en processar "nevera", "ràpid" i "però".
  2. Recompte de paràmetres: calcula els paràmetres de (a) LSTM(128) amb entrada de 64 característiques per pas; (b) la GRU equivalent fent servir la fórmula 3·u·(d+u+1); (c) en quin percentatge redueix la GRU els paràmetres?
  3. Diagnòstic d'arquitectura: aquest model llança un error en construir-se — explica per què i corregeix-lo: Sequential([Input(shape=(30, 4)), LSTM(64), LSTM(32), Dense(1)]).

Solucions

  1. A "nevera": porta d'entrada molt oberta (escriu a la cinta el subjecte de la ressenya), oblit oberta (no hi ha res previ a esborrar). A "ràpid": entrada moderada (registra un senyal positiu sobre l'enviament), oblit gairebé del tot oberta (conserva "nevera"). A "però": és un gir discursiu — la porta d'oblit pot tancar-se parcialment sobre els components de valoració positiva (el que segueix probablement els contradiu) mentre conserva el subjecte, i l'entrada es prepara per escriure la nova valoració. (És una interpretació idealitzada: en una LSTM real els components no són tan llegibles, però el mecanisme és aquest.)
  2. (a) 4·128·(64+128+1) = 4·128·193 = 98 816. (b) 3·128·193 = 74 112. (c) 1 − 74 112/98 816 = 25 % de reducció, la proporció 3/4 esperable per tenir una porta menys.
  3. La primera LSTM(64) retorna per defecte només l'últim estat (un tensor 2D (batch, 64)), però la segona LSTM necessita una seqüència 3D. Correcció: LSTM(64, return_sequences=True) a la primera capa.

Conclusió

La LSTM resol la memòria curta de la SimpleRNN separant la memòria (l'estat de cel·la, una cinta transportadora que creua la seqüència per una via additiva) del processament, i governant l'accés amb tres portes apreses: oblit, entrada i sortida. Aquesta via additiva és la mateixa medicina contra el vanishing que les skip connections de ResNet, aplicada al temps en lloc de a la profunditat. La GRU comprimeix la idea en dues portes i un sol estat, amb un 25 % menys de paràmetres i resultats gairebé sempre comparables. Ho has comprovat empíricament: davant d'una dependència a 48 passos, la SimpleRNN es rendeix i la LSTM la resol. Amb les eines de composició (return_sequences, Bidirectional) ja tens l'arsenal recurrent complet. A la propera lliçó el posarem a treballar en el primer dels dos projectes estrella del mòdul: ensenyar una xarxa a llegir les ressenyes dels clients de TecnoMarket — cosa que exigeix, abans de res, resoldre un problema nou: convertir text en números.

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