En la lección anterior descubrimos que la SimpleRNN tiene memoria de pez: el gradiente que viaja hacia atrás a través del tiempo se desvanece y, con él, la capacidad de aprender dependencias a más de unos pocos pasos de distancia. Esta lección presenta las dos células que resolvieron ese problema y que sostienen la mayoría de aplicaciones reales de secuencias: la LSTM (Long Short-Term Memory, 1997) y su simplificación moderna, la GRU (Gated Recurrent Unit, 2014). Entenderás la LSTM pieza a pieza —su estado de célula y sus tres puertas—, verás su paralelismo con las skip connections de ResNet, contarás sus parámetros, y comprobarás experimentalmente en Keras cómo una LSTM resuelve una tarea de memoria larga en la que la SimpleRNN fracasa. Cerraremos con dos herramientas de arquitectura que usarás constantemente: capas apiladas (return_sequences) y capas bidireccionales.

Contenido

  1. El problema de la memoria a largo plazo
  2. La célula LSTM: el estado de célula como cinta transportadora
  3. Las tres puertas, una a una
  4. La autopista del gradiente: paralelismo con ResNet
  5. GRU: la simplificación que casi siempre basta
  6. Comparativa SimpleRNN / LSTM / GRU
  7. Recuento de parámetros de una LSTM
  8. Experimento en Keras: SimpleRNN vs. LSTM en memoria larga
  9. Apilar capas recurrentes y Bidirectional

El problema de la memoria a largo plazo

Lee esta reseña real del estilo que recibe TecnoMarket:

"Compré este frigorífico hace tres meses. El proceso de compra fue cómodo, el transportista fue puntual, el embalaje llegó impecable y la instalación fue sencilla... pero desde la semana pasada no enfría."

Para clasificarla correctamente, el modelo debe conectar "frigorífico" (palabra 3) y "no enfría" (palabras 30+) saltando por encima de una larga lista de comentarios positivos. Una SimpleRNN reescribe su estado oculto entero en cada paso (h_t = tanh(...)): cada palabra nueva pisa lo anterior, y tras 25 pasos de elogios logísticos, el "frigorífico" del principio se ha diluido. Y durante el entrenamiento ocurre lo simétrico: el gradiente del error ("dijiste positiva y era negativa") se desvanece antes de llegar a los primeros pasos, así que la red ni siquiera aprende que debía recordar.

La solución de la LSTM no es "recordar más fuerte", sino algo más elegante: separar la memoria del procesamiento y poner el acceso a esa memoria bajo el control de puertas aprendidas.

La célula LSTM: el estado de célula como cinta transportadora

Una LSTM mantiene dos vectores de estado en lugar de uno:

  • h_t — el estado oculto, como en la SimpleRNN: la "salida de trabajo" de cada paso.
  • C_t — el estado de célula: la memoria a largo plazo.

La imagen clásica para C_t es una cinta transportadora que recorre toda la secuencia: la información viaja sobre ella de un paso al siguiente casi sin tocar — solo dos operaciones suaves (una multiplicación y una suma) la modifican. Nada la reescribe por completo. A los lados de la cinta hay tres puertas: pequeños mecanismos que deciden qué se retira de la cinta, qué se sube a ella y qué se consulta.

Una puerta es simplemente una mini-capa con activación sigmoide:

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

La sigmoide (02-02) produce valores entre 0 y 1 por cada componente de la memoria: 0 significa "bloquea el paso" y 1 "deja pasar todo". Es el mismo papel que un grifo — la analogía del curso —: la red aprende cuánto abrir cada grifo según el contexto (h_{t-1} y x_t), no con un valor fijo.

graph LR
    subgraph "Célula LSTM en el paso t"
        Cprev["C_{t-1}<br>(cinta: memoria)"] --> MUL1(("×"))
        F["Puerta de olvido f_t<br>σ: ¿qué borro?"] --> MUL1
        MUL1 --> ADD(("+"))
        I["Puerta de entrada i_t<br>σ: ¿qué escribo?"] --> MUL2(("×"))
        CAND["Candidato C̃_t<br>tanh: contenido nuevo"] --> MUL2
        MUL2 --> ADD
        ADD --> Cnew["C_t<br>(cinta actualizada)"]
        Cnew --> TANH["tanh"]
        TANH --> MUL3(("×"))
        O["Puerta de salida o_t<br>σ: ¿qué muestro?"] --> MUL3
        MUL3 --> Hnew["h_t<br>(estado oculto)"]
    end
    X["x_t y h_{t-1}"] -.alimentan.-> F
    X -.-> I
    X -.-> CAND
    X -.-> O

Las tres puertas, una a una

  1. Puerta de olvido (forget gate): ¿qué borro de la cinta?

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

Mira la entrada actual y el contexto, y decide qué componentes de C_{t-1} conservar (valores cerca de 1) y cuáles vaciar (cerca de 0). Se aplica multiplicando: f_t ⊙ C_{t-1} (⊙ = componente a componente).

Intuición TecnoMarket: si la reseña dice "Por otro lado, el segundo producto...", conviene olvidar los detalles del primer producto para hacer sitio.

  1. Puerta de entrada (input gate): ¿qué escribo en la cinta?

Trabaja en pareja con un candidato a memoria nueva:

i_t  = σ(Wi · [h_{t-1}, x_t] + bi)       # cuánto escribir (0-1)
C̃_t = tanh(Wc · [h_{t-1}, x_t] + bc)    # qué contenido escribir (-1 a 1)

El candidato C̃_t propone información nueva (es, de hecho, la fórmula de la SimpleRNN); la puerta i_t decide qué partes de esa propuesta merecen entrar en la memoria. La actualización completa de la cinta es:

C_t = f_t ⊙ C_{t-1}  +  i_t ⊙ C̃_t
      (lo que conservo)  (lo que añado)

Intuición: al leer "frigorífico", la puerta de entrada escribe "el producto es un frigorífico" en la cinta; durante los elogios al transportista, i_t puede mantenerse casi cerrada y f_t casi abierta: la cinta pasa intacta.

  1. Puerta de salida (output gate): ¿qué consulto de la cinta?

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

El estado oculto h_t (lo que la célula "muestra" al exterior y al paso siguiente) es una vista filtrada de la memoria: la puerta de salida decide qué partes de la cinta son relevantes ahora mismo. No todo lo memorizado hace falta en cada paso: puedes recordar que el producto es un frigorífico sin mencionarlo hasta que llegue el "no enfría".

Resumen del flujo: olvido lo obsoleto (f_t), incorporo lo nuevo relevante (i_t ⊙ C̃_t), y expongo la parte útil de mi memoria (o_t). Todos los pesos de las puertas se aprenden por retropropagación, exactamente igual que el resto de la red: nadie programa a mano qué olvidar.

La autopista del gradiente: paralelismo con ResNet

Mira otra vez la ecuación de la cinta: C_t = f_t ⊙ C_{t-1} + i_t ⊙ C̃_t. La memoria anterior llega al presente mediante una suma, no atravesando una tanh y una multiplicación por Wh como en la SimpleRNN. ¿Te suena? Es la misma jugada que las skip connections de ResNet (03-03): allí, salida = F(x) + x creaba un atajo para que el gradiente cruzara decenas de capas en el espacio; aquí, la cinta crea un atajo para que el gradiente cruce decenas de pasos en el tiempo. Mientras la puerta de olvido esté razonablemente abierta (f_t ≈ 1), el gradiente fluye hacia atrás por la cinta casi sin atenuarse: una autopista del gradiente temporal.

Es una idea recurrente en deep learning que conviene fijar: cuando el gradiente muere por el camino (vanishing, 02-03), la solución suele ser darle un camino aditivo directo. ResNet lo hizo en profundidad, la LSTM en el tiempo, y las conexiones residuales de los transformers (05-05) repiten el patrón.

GRU: la simplificación que casi siempre basta

La GRU se pregunta: ¿de verdad hacen falta dos estados y tres puertas? Su respuesta: fusiona C_t y h_t en un único estado h_t, y usa dos puertas:

  • Puerta de actualización (z_t, update): combina en una sola las funciones de olvidar y escribir. La actualización es una interpolación:

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

    Si z_t ≈ 0, el estado pasa intacto (memoria conservada); si z_t ≈ 1, se sustituye por el candidato nuevo. Fíjate en que sigue siendo una vía aditiva: la autopista del gradiente se conserva.

  • Puerta de reinicio (r_t, reset): al calcular el candidato h̃_t = tanh(W · [r_t ⊙ h_{t-1}, x_t]), decide cuánto contexto pasado usar para proponer contenido nuevo. Con r_t ≈ 0, el candidato ignora el pasado y "empieza de cero" localmente.

Menos puertas significa ~25 % menos parámetros y entrenamiento algo más rápido, con un rendimiento habitualmente comparable al de la LSTM. En la práctica: prueba GRU cuando el dataset es pequeño o el tiempo de cómputo importa; prueba LSTM cuando la tarea exige memoria muy fina o hay datos de sobra. No hay un ganador universal — es una decisión empírica.

Comparativa SimpleRNN / LSTM / GRU

Aspecto SimpleRNN LSTM GRU
Estados 1 (h) 2 (h y C) 1 (h)
Puertas 0 3 (olvido, entrada, salida) 2 (update, reset)
Parámetros (entrada d, unidades u) u(d+u+1) 4·u(d+u+1) 3·u(d+u+1)
Memoria efectiva ~5-10 pasos Cientos de pasos Cientos de pasos
Coste por paso Bajo Alto (×4) Medio (×3)
Vanishing en el tiempo Grave Mitigado (cinta aditiva) Mitigado (interpolación)
Cuándo usarla Secuencias muy cortas, demos didácticas Dependencias largas y complejas, datasets grandes Opción por defecto: buen equilibrio calidad/coste

Recuento de parámetros de una LSTM

Una LSTM calcula 4 bloques con la misma estructura (las 3 puertas + el candidato), cada uno con su matriz para la entrada, su matriz recurrente y su sesgo. Con entrada de dimensión d y u unidades:

parámetros = 4 × (d·u + u·u + u) = 4·u·(d + u + 1)

Ejemplo: LSTM(64) con entradas de 32 características por paso:

4 × 64 × (32 + 64 + 1) = 4 × 64 × 97 = 24 832 parámetros

Exactamente 4 veces los que tendría una SimpleRNN(64) equivalente (6 208), y como siempre en las recurrentes, independiente de la longitud de la secuencia. Verifícalo con model.summary(), como hicimos con las CNN en 03-02.

Experimento en Keras: SimpleRNN vs. LSTM en memoria larga

Diseñemos una tarea que exige memoria larga: en una secuencia de 50 números aleatorios, el objetivo es recuperar la suma de los 2 primeros valores. Toda la información útil está al principio; los 48 pasos restantes son ruido que la red debe atravesar sin olvidar.

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

# Dataset sintético de memoria larga
rng = np.random.default_rng(0)
PASOS = 50
X = rng.uniform(0, 1, size=(5000, PASOS, 1))
y = X[:, :2, 0].sum(axis=1)          # objetivo: suma de los DOS PRIMEROS pasos

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

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

Resultados típicos (variarán ligeramente en tu máquina):

 SimpleRNN: MAE validación = 0.39   # ≈ predecir siempre la media: NO ha aprendido
      LSTM: MAE validación = 0.05   # memoria intacta tras 48 pasos de ruido

Interpretación: la suma de dos uniformes en [0,1] tiene media 1.0 y un predictor "tonto" que siempre dijera 1.0 lograría un MAE de ~0.4 — justo donde se queda la SimpleRNN. El gradiente nunca consiguió llegar a los primeros pasos, así que la red se rindió y predice la media. La LSTM, gracias a la cinta, escribe los dos primeros valores en C_t, mantiene la puerta de olvido abierta durante 48 pasos y los recupera al final. Este experimento en miniatura es la razón de que casi nadie use SimpleRNN en producción.

Apilar capas recurrentes y Bidirectional

Capas apiladas: return_sequences=True

Igual que apilamos capas Conv2D para pasar de bordes a texturas y objetos (03-01), podemos apilar capas recurrentes para que la segunda procese representaciones más abstractas de la secuencia. El detalle técnico: una capa recurrente por defecto devuelve solo su último estado, pero la capa siguiente necesita una secuencia completa como entrada. La solución es return_sequences=True en todas las capas menos la última:

modelo = keras.Sequential([
    layers.Input(shape=(50, 1)),
    layers.LSTM(64, return_sequences=True),   # devuelve los 50 estados: (50, 64)
    layers.LSTM(32),                          # devuelve solo el último: (32,)
    layers.Dense(1)
])

Regla mnemotécnica: return_sequences=True = "pásale la película entera a la siguiente capa"; False (por defecto) = "quédate con el fotograma final".

Bidirectional: leer en los dos sentidos

En muchas tareas, el contexto futuro también ayuda: en "no me ha llegado aún", el "aún" suaviza la queja, pero llega después. Bidirectional duplica la capa: una copia lee la secuencia de izquierda a derecha y otra de derecha a izquierda, y sus salidas se concatenan (duplicando la dimensión de salida y los parámetros):

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

Úsala cuando dispongas de la secuencia completa antes de decidir (clasificar una reseña ya escrita: sí). Evítala cuando predices el futuro en tiempo real (demanda de mañana: el "sentido inverso" leería datos que aún no existen). Retomaremos esta distinción en 04-04.

Errores Comunes y Consejos

  • Apilar recurrentes sin return_sequences=True. El error expected ndim=3, found ndim=2 en la segunda LSTM casi siempre es esto: la primera capa devolvió solo el último estado. Actívalo en todas las capas recurrentes excepto la última (si el problema es many-to-one).
  • Confundir h_t con C_t. El estado oculto es la vista de trabajo (lo que sale de la célula); el estado de célula es la memoria interna de la LSTM. Keras gestiona ambos automáticamente: tú solo ves h_t.
  • Elegir LSTM "porque es la buena" sin comparar. Con secuencias cortas o pocos datos, una GRU (o incluso una densa sobre estadísticas agregadas) puede rendir igual con menos coste. Compara siempre contra la opción simple, como haremos con el baseline en 04-04.
  • Usar Bidirectional en predicción de futuro. Es fuga de información conceptual: en producción no tendrás el "futuro" para leerlo al revés. Resérvala para secuencias completas ya disponibles.
  • Pensar que la LSTM elimina el vanishing por completo. Lo mitiga enormemente, pero con secuencias de miles de pasos también sufre. Para esos extremos surgieron los mecanismos de atención, que mencionaremos en 04-03 y desarrollaremos en 05-05.

Ejercicios

  1. Puertas en acción: para la reseña "El frigorífico llegó rápido y bien embalado, pero no enfría", describe qué esperarías que hicieran (abiertas/cerradas, qué escriben o borran) las puertas de olvido y entrada al procesar "frigorífico", "rápido" y "pero".
  2. Recuento de parámetros: calcula los parámetros de (a) LSTM(128) con entrada de 64 características por paso; (b) la GRU equivalente usando la fórmula 3·u·(d+u+1); (c) ¿en qué porcentaje reduce la GRU los parámetros?
  3. Diagnóstico de arquitectura: este modelo lanza un error al construirse — explica por qué y corrígelo: Sequential([Input(shape=(30, 4)), LSTM(64), LSTM(32), Dense(1)]).

Soluciones

  1. En "frigorífico": puerta de entrada muy abierta (escribe en la cinta el sujeto de la reseña), olvido abierta (no hay nada previo que borrar). En "rápido": entrada moderada (registra señal positiva sobre el envío), olvido casi totalmente abierta (conserva "frigorífico"). En "pero": es un giro discursivo — la puerta de olvido puede cerrarse parcialmente sobre las componentes de valoración positiva (lo que sigue probablemente las contradice) mientras conserva el sujeto, y la entrada se prepara para escribir la nueva valoración. (Es una interpretación idealizada: en una LSTM real las componentes no son tan legibles, pero el mecanismo es este.)
  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ón, la proporción 3/4 esperable por tener una puerta menos.
  3. La primera LSTM(64) devuelve por defecto solo el último estado (un tensor 2D (batch, 64)), pero la segunda LSTM necesita una secuencia 3D. Corrección: LSTM(64, return_sequences=True) en la primera capa.

Conclusión

La LSTM resuelve la memoria corta de la SimpleRNN separando la memoria (el estado de célula, una cinta transportadora que cruza la secuencia por una vía aditiva) del procesamiento, y gobernando el acceso con tres puertas aprendidas: olvido, entrada y salida. Esa vía aditiva es la misma medicina contra el vanishing que las skip connections de ResNet, aplicada al tiempo en vez de a la profundidad. La GRU comprime la idea en dos puertas y un solo estado, con un 25 % menos de parámetros y resultados casi siempre comparables. Lo has comprobado empíricamente: ante una dependencia a 48 pasos, la SimpleRNN se rinde y la LSTM la resuelve. Con las herramientas de composición (return_sequences, Bidirectional) ya tienes el arsenal recurrente completo. En la próxima lección lo pondremos a trabajar en el primero de los dos proyectos estrella del módulo: enseñar a una red a leer las reseñas de los clientes de TecnoMarket — lo que exige, antes de nada, resolver un problema nuevo: convertir texto en números.

Curso de Deep Learning

Módulo 1: Introducción a Deep Learning

Módulo 2: Fundamentos de Redes Neuronales

Módulo 3: Redes Neuronales Convolucionales (CNN)

Módulo 4: Redes Neuronales Recurrentes (RNN)

Módulo 5: Técnicas Avanzadas en Deep Learning

Módulo 6: Herramientas y Frameworks

Módulo 7: Proyectos Prácticos

Módulo 8: Consideraciones Éticas y Futuro del Deep Learning

© Copyright 2026. Todos los derechos reservados