LSTMs y GRUs: Dándole memoria a las redes
En la Parte 5, Lina vio por qué las Redes Neuronales Recurrentes (RNN) estándar son como peces de colores. Tienen un problema de memoria a corto plazo. Debido al Gradiente Desvaneciente, la señal del inicio de una sesión larga se atenúa antes de llegar al final.
Si estás rastreando una sesión de 500 páginas vistas, necesitas que esa rara vista de un libro de primera edición de $800 desde la página 1 persista — es lo que te permite marcar a un cliente de alto valor al final. Una RNN estándar generalmente olvida esa señal en las primeras decenas de páginas. Por eso, estamos construyendo un mejor sistema de memoria.
1. El problema del pez dorado: Por qué las redes estándar olvidan
Una RNN estándar funciona como un registro de sesión de cliente. La primera vista de página dice “este cliente hizo clic en un libro de primera edición raro de $800.” Para cuando el registro pasa por veinte vistas de página más, el modelo ha perdido esencialmente el rastro de ello.
Esto es el desvanecimiento del gradiente (vanishing gradient) en términos matemáticos. La red pierde el hilo de sus pensamientos. Así que veamos qué sucede cuando una RNN simple intenta recordar una señal desde el principio mismo de una secuencia larga.
import torch
import torch.nn as nn
# A simple sequence: the first number is a high-value signal (1.0 = viewed an $800 handbag)
# The rest are 'filler' page views (0s). Can the RNN remember that first signal at the end?
sequence_length = 20
input_data = torch.zeros(1, sequence_length, 1)
input_data[0, 0, 0] = 1.0 # The high-value view happens at the very first page
model = nn.RNN(input_size=1, hidden_size=1, batch_first=True)
# We initialize weights to 0.5 to simulate the 'fading' effect
for name, param in model.named_parameters():
if 'weight' in name:
nn.init.constant_(param, 0.5)
output, hidden = model(input_data)
print(f"Value at step 1: {output[0, 0, 0]:.4f}")
print(f"Value at step 20: {output[0, 19, 0]:.4f}")
input_data = torch.zeros(1, sequence_length, 1)— Crea un lote de 1 secuencia, de 20 pasos de longitud, 1 característica por paso, todos en cero. La forma es(batch, seq, feature).input_data[0, 0, 0] = 1.0— Establece el primer paso en 1.0: la señal de “alto valor” en la página 1. Todo lo demás permanece en cero.model = nn.RNN(input_size=1, hidden_size=1, batch_first=True)— Una RNN mínima con 1 característica de entrada y 1 unidad oculta.batch_first=Truesignifica que el tensor de entrada tiene la forma(batch, seq, feature).for name, param in model.named_parameters()— Itera sobre todos los parámetros nombrados (pesos y sesgos) en el modelo.if 'weight' in name: nn.init.constant_(param, 0.5)— Establece cada peso en 0.5 para simular el efecto de “desvanecimiento”: cada paso multiplica la señal por 0.5, por lo que después de 20 pasos la señal original se ha multiplicado por .output, hidden = model(input_data)—outputcontiene el estado oculto en cada paso;hiddenes solo el estado final. Usamosoutput[0, 0, 0](paso 1) youtput[0, 19, 0](paso 20) para comparar.
Lo que esto muestra: El valor en el paso 1 comienza significativamente por encima de cero. Para el paso 20, ha decaído a esencialmente 0.0000. La señal de alto valor ha desaparecido. Si el modelo necesita esa información para decidir si este cliente merece un anuncio de retargeting premium al final de la sesión, falla. Está adivinando a ciegas.
(Nota: los pesos de este modelo no tienen una semilla fija (seed), por lo que el valor exacto del paso 1 variará ligeramente cada vez que lo ejecute — el patrón de “decae a ~0 para el paso 20” es lo que importa, no el primer número preciso.)
2. La LSTM: Un archivador para tus datos
Los investigadores crearon la LSTM (Long Short-Term Memory) para resolver esto. En lugar de una sola “maleta” que se desordena, imagina una LSTM como una persona que administra un archivador.
Esa persona sigue tres reglas estrictas, llamadas Compuertas:
- La compuerta de olvido: Revisa los archivos antiguos en busca de algo obsoleto. Si un cliente cierra la pestaña de un producto, no necesitamos seguir rastreando su precio. Deséchalo.
- La compuerta de entrada: Revisa la nueva información y decide si vale la pena guardarla. Si el cliente ve un producto premium, anótalo y archívalo.
- La compuerta de salida: Según lo que hay en el archivador y lo que acaba de entrar, ¿qué decimos ahora mismo?
Así es como se ve esa lógica de compuertas en el código. Usamos una función sigmoid (que genera un número entre 0 y 1) como una “válvula”. 0 significa cerrado — olvida todo — y 1 significa abierto, guarda todo.
import numpy as np
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def lstm_cell_logic(input_val, prev_cell_state):
# Forget gate: Should we keep the old memory?
# Let's say we decide to keep 90% of it
forget_gate = 0.9
# Input gate: Should we add the new info?
# Let's say this info is very important (1.0)
input_gate = 1.0
new_info = np.tanh(input_val)
# The magic happens here: The Cell State update
# We multiply old memory by the forget gate and ADD the new info
updated_cell_state = (prev_cell_state * forget_gate) + (new_info * input_gate)
return updated_cell_state
memory = 10.0 # Our high-value signal from earlier
for i in range(20):
memory = lstm_cell_logic(0.0, memory) # Adding 'filler' info
print(f"LSTM memory after 20 steps: {memory:.4f}")
def sigmoid(x): return 1 / (1 + np.exp(-x))— Sigmoide manual: comprime cualquier número real al rango . En una LSTM real, cada puerta aprende sus propios pesos sigmoide; aquí fijamos los valores para mayor claridad.forget_gate = 0.9— Fijado en “mantener el 90%”. Una LSTM real calcula esto a partir de la entrada actual y el estado oculto anterior.input_gate = 1.0— Completamente abierta: acepta toda la información nueva. De nuevo, normalmente se aprende.new_info = np.tanh(input_val)— El valor “candidato”: comprime la señal entrante en para que pueda sumarse limpiamente al estado de la celda.updated_cell_state = (prev_cell_state * forget_gate) + (new_info * input_gate)— La actualización central de la LSTM: reduce la memoria antigua mediante la puerta de olvido y luego añade la nueva información filtrada. Esta suma — no solo la multiplicación — es lo que mantiene viva la señal a lo largo de muchos pasos.memory = 10.0— El valor inicial que representa la señal de alto valor de la página 1.for i in range(20): memory = lstm_cell_logic(0.0, memory)— Alimenta 20 pasos de “relleno” (entrada 0.0) para ver si la señal original de 10 sobrevive. Lo hace: después de 20 pasos de decaimiento 0.9×, , sigue siendo distinto de cero.
Interpretación: La RNN llegó a cero, pero la memoria de la LSTM sigue en 1.22. Protegió el valor original porque las matemáticas utilizan suma en lugar de multiplicación constante. La suma evita que la señal se reduzca a la nada.
Ecuaciones de las compuertas de la LSTM
En cada paso de tiempo , la LSTM calcula tres puertas y un valor candidato, luego actualiza el estado de celda y el estado oculto:
La puerta de olvido decide cuánto del estado de celda anterior conservar. La puerta de entrada decide cuánta información candidata nueva escribir. La puerta de salida decide cuánto del estado de celda actualizado exponer como el estado oculto . El símbolo es el producto de Hadamard (elemento a elemento).
| Explicación simple | Símbolo estadístico | Equivalente en Python |
|---|---|---|
| Puerta de olvido (cuánta memoria anterior conservar) | forget_gate | |
| Puerta de entrada (cuánta información nueva agregar) | input_gate | |
| Valor candidato de celda (nueva entrada comprimida) | new_info | |
| Estado de celda (memoria a largo plazo) | updated_cell_state | |
| Puerta de salida (cuánto revelar ahora) | (no mostrado en código simplificado) | |
| Estado oculto (salida a corto plazo) | hn | |
| Estado oculto anterior | prev_hidden_state | |
| Entrada actual | input_val | |
| Activación sigmoide | sigmoid | |
| Producto de Hadamard (elemento a elemento) | * | |
| Tangente hiperbólica | np.tanh |
3. Esta es la parte más difícil: Estado de celda vs. estado oculto
El código de la LSTM devuelve dos cosas: h (Estado oculto) y c (Estado de la celda). Esto confunde a muchos principiantes.
Aquí tienes el modelo mental:
- El Estado de la celda (c): Tu libreta. El registro a largo plazo. La información fluye a través de ella con muy poca interferencia. Esa señal temprana de alto valor se queda aquí durante 50 pasos.
- El Estado oculto (h): Lo que estás pensando actualmente. Una versión filtrada de la libreta, combinada con la palabra actual, utilizada para hacer una predicción ahora mismo.
Entonces, ¿cómo difieren durante una secuencia?
lstm = nn.LSTM(input_size=1, hidden_size=1, batch_first=True)
input_seq = torch.randn(1, 5, 1)
output, (hn, cn) = lstm(input_seq)
print(f"Hidden State (Short-term): {hn.item():.4f}")
print(f"Cell State (Long-term): {cn.item():.4f}")
lstm = nn.LSTM(input_size=1, hidden_size=1, batch_first=True)— Una LSTM de una sola capa con 1 característica de entrada y 1 unidad oculta.batch_first=Truesignifica que la forma del tensor de entrada es(batch, seq, feature).input_seq = torch.randn(1, 5, 1)— Entrada aleatoria: 1 lote, 5 pasos de tiempo, 1 característica por paso. Cada valor se extrae de una distribución normal estándar.output, (hn, cn) = lstm(input_seq)— La LSTM devuelve una tupla:outputtiene el estado oculto en cada paso, y(hn, cn)es una tupla del estado oculto final y el estado de la celda final.hnes la memoria a corto plazo;cnes la “libreta” a largo plazo.hn.item()/cn.item()—.item()extrae un valor flotante de Python de un tensor de un solo elemento. Ambos son escalares aquí porquehidden_size=1.
Lo que esto significa en realidad: El Estado de la celda suele ser más grande o más complejo porque contiene el historial sin procesar. El Estado oculto es la versión refinada, lista para la siguiente capa de la red.
4. La GRU: La versión ‘lite’ de la memoria
Las LSTM son excelentes, pero son pesadas. Todos esos parámetros ralentizan el entrenamiento. En 2014, los investigadores introdujeron la GRU (Unidad Recurrente con Puertas).
Piensa en la GRU como el hermano menor eficiente. Omite el archivero separado (el Estado de Celda). En su lugar, depende de una “nota adhesiva” (un único Estado Oculto). Combina las puertas de Olvido y de Entrada en una sola Puerta de Actualización.
- LSTM: “¿Debo olvidar lo viejo? Bien. Ahora, ¿debo añadir lo nuevo?”
- GRU: “¿Cuánto de lo nuevo debería reemplazar lo viejo?”
Entonces, ¿cómo se comparan en una tarea pequeña?
import time
def benchmark(model_type, data):
model = model_type(1, 64, batch_first=True)
start = time.time()
for _ in range(100):
_ = model(data)
return time.time() - start
data = torch.randn(32, 100, 1)
lstm_time = benchmark(nn.LSTM, data)
gru_time = benchmark(nn.GRU, data)
print(f"LSTM time: {lstm_time:.4f}s")
print(f"GRU time: {gru_time:.4f}s")
def benchmark(model_type, data)— Una función genérica de medición de tiempo que funciona tanto connn.LSTMcomo connn.GRUpasadas como el argumentomodel_type.model = model_type(1, 64, batch_first=True)— Crea tanto una LSTM como una GRU coninput_size=1yhidden_size=64. Ambas clases aceptan los mismos argumentos de constructor.for _ in range(100): _ = model(data)— Ejecuta 100 pasadas hacia adelante (sin entrenamiento) para obtener una medición de tiempo de reloj estable. El_descarta cada salida.data = torch.randn(32, 100, 1)— 32 secuencias de longitud 100 con 1 característica cada una — un tamaño de lote y una longitud de secuencia realistas para realizar pruebas de rendimiento.lstm_time = benchmark(nn.LSTM, data)/gru_time = benchmark(nn.GRU, data)— Mide el tiempo de ambas arquitecturas con datos idénticos para que la comparación sea justa.
Interpretación: La GRU es casi siempre más rápida (generalmente de un 15-20% más rápida). Funciona igual de bien que una LSTM en muchas tareas. Si tienes pocos recursos de cómputo, yo me inclinaría por empezar con una GRU.
LSTM vs. GRU: ¿Qué arquitectura de memoria deberías elegir?
| Enfoque | Qué hace | Ideal para | Compromiso |
|---|---|---|---|
| LSTM | Mantiene un estado de celda separado con tres puertas (olvido, entrada, salida). El estado de celda actúa como un “cuaderno” protegido que transporta la señal de largo alcance a través de muchos pasos. | Secuencias largas donde proteger una señal temprana es crítico; tareas donde se necesita control granular sobre qué recordar y qué olvidar. | Más parámetros (~33% más que una GRU para el mismo tamaño oculto), más lento de entrenar. |
| GRU | Usa un único estado oculto con dos puertas (actualización, reinicio). Sin estado de celda separado: la puerta de actualización fusiona “olvidar lo antiguo” y “añadir lo nuevo” en una sola decisión. | La mayoría de las tareas; secuencias cortas a medianas; entornos con limitaciones de cómputo; cuando se desea una línea base rápida. | Menos parámetros y más rápido, pero sin almacenamiento a largo plazo dedicado. Puede tener dificultades en secuencias muy largas donde el estado de celda de la LSTM proporciona una ventaja. |
Regla general: Comienza con una GRU por velocidad y simplicidad — se entrena más rápido y a menudo iguala la precisión de la LSTM. Opta por una LSTM cuando necesites que el estado de celda proteja una señal de largo alcance específica (como la vista de un libro raro en la página 1 que debe sobrevivir 100+ pasos para influir en una predicción al final). Empíricamente, la brecha se estrecha en secuencias más cortas pero se amplía en las más largas.
5. Poniéndolo en práctica: Prediciendo la intención de compra
¿Puede una red con memoria resolver una tarea de predicción simple? Le daremos una sesión: el cliente ve un libro raro de primera edición de $800 en el paso 1, luego explora cuatro artículos económicos y no relacionados. Queremos que prediga una puntuación alta de valor de compra a partir de esa primera señal, cinco pasos después.
class Predictor(nn.Module):
def __init__(self, mode='LSTM'):
super().__init__()
if mode == 'LSTM':
self.rnn = nn.LSTM(1, 10, batch_first=True)
else:
self.rnn = nn.GRU(1, 10, batch_first=True)
self.fc = nn.Linear(10, 1)
def forward(self, x):
out, _ = self.rnn(x)
return self.fc(out[:, -1, :]) # Predict based on last state
# Imagine 0.8 represents 'viewed the $800 handbag' and we want to predict 0.9 (high purchase-value likelihood)
model = Predictor(mode='LSTM')
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
x = torch.tensor([[[0.8], [0.1], [0.1], [0.1], [0.1]]]) # 'handbag view' followed by 4 filler page views
y = torch.tensor([[0.9]])
for epoch in range(100):
pred = model(x)
loss = criterion(pred, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
print(f"Final Prediction: {model(x).item():.4f} (Target: 0.9)")
class Predictor(nn.Module)— Un modelo que puede ser LSTM o GRU según el argumentomode, lo que facilita el intercambio de arquitecturas.if mode == 'LSTM': self.rnn = nn.LSTM(1, 10, batch_first=True)— Crea una capa LSTM con 1 característica de entrada y 10 unidades ocultas. El estado oculto de 10 dimensiones captura la “memoria” de la sesión.else: self.rnn = nn.GRU(1, 10, batch_first=True)— Recurre a una GRU con las mismas dimensiones (1 de entrada, 10 ocultas). Tantonn.LSTMcomonn.GRUcomparten la misma firma de llamada, por lo que intercambiarlas es trivial.self.fc = nn.Linear(10, 1)— Una capa lineal que mapea el estado oculto final de 10 dimensiones a una única salida escalar (la puntuación de valor de compra predicha).out, _ = self.rnn(x)— Pasa la secuencia completa de 5 pasos por la capa recurrente.outtiene la forma(1, 5, 10)— el estado oculto en cada paso. El_descartado es la tupla final(h_n, c_n)(o soloh_npara GRU).return self.fc(out[:, -1, :])— Toma solo el estado oculto del último paso de tiempo (out[:, -1, :]tiene la forma(1, 10)) y lo proyecta a través de la capa lineal para obtener una única predicción.criterion = nn.MSELoss()— Error cuadrático medio: penaliza la diferencia al cuadrado entre la predicción y el objetivo. Bueno para salidas de tipo regresión.optimizer = torch.optim.Adam(model.parameters(), lr=0.01)— Optimizador Adam con una tasa de aprendizaje de 0.01. Adam adapta las tasas de aprendizaje por parámetro y es un valor predeterminado seguro para la mayoría de los modelos pequeños.optimizer.zero_grad()— Borra los gradientes acumulados de la época anterior. Sin esto, los gradientes se sumarían a lo largo de las iteraciones.loss.backward()— Calcula los gradientes mediante retropropagación a través del tiempo (BPTT), fluyendo el error desde el paso 5 hasta el paso 1.optimizer.step()— Actualiza todos los pesos utilizando los gradientes calculados porbackward().
Interpretación: El modelo acierta el objetivo casi perfectamente. Como es un LSTM, la señal “0.8” de la primera vista de página no desapareció. La pérdida (el factor de “sorpresa”) cayó casi a cero—el modelo llevó la memoria de esa vista temprana de libros raros a través de los pasos intermedios e hizo una predicción precisa al final.
Resumen:
- Las RNN estándar olvidan porque los gradientes desaparecen durante la multiplicación.
- Los LSTM usan compuertas como un archivador, protegiendo la información importante.
- El Estado de Celda es el cuaderno a largo plazo; el Estado Oculto es el pensamiento a corto plazo.
- Las GRU son versiones más rápidas y simplificadas de los LSTM que funcionan bien para la mayoría de las tareas.
Incluso con los LSTM, hay un límite. Si una sesión tiene 1,000 vistas de página, incluso un archivador se llena. Para resolver eso, necesitamos dejar de intentar recordar todo y empezar a prestar Atención.
Con un sistema de memoria funcional, el modelo de Lina ahora puede recordar toda la sesión, pero procesar cada vista de página una por una es lento. Hay una forma más rápida de enfocarse en lo que importa.
Comprueba tu comprensión
Las preguntas a continuación avanzan desde el simple recuerdo hasta el diseño abierto, siguiendo aproximadamente la Taxonomía de Bloom.
Recordar ¿Cuáles son las tres puertas en una LSTM y qué pregunta responde cada una?
Comprender En tus propias palabras, explica por qué la actualización del estado de la celda de la LSTM usa adición en lugar de solo multiplicación, y por qué eso específicamente evita que la señal se reduzca a cero.
Aplicar
Usando la fórmula lstm_cell_logic del artículo (updated_cell_state = prev_cell_state * forget_gate + new_info * input_gate), calcula el estado de la celda después de 2 pasos partiendo de memory = 5.0, con forget_gate = 0.8, input_gate = 1.0, y new_info = tanh(1.0) añadido en cada paso.
Analizar El artículo dice que el Estado de la Celda es un “cuaderno” (largo plazo) y el Estado Oculto es “lo que estás pensando actualmente” (corto plazo). Analiza paso a paso qué saldría mal si un modelo solo tuviera un Estado Oculto y ningún Estado de Celda separado; ¿por qué la versión a corto plazo no es suficiente por sí sola?
Evaluar El artículo recomienda las GRU cuando “tienes poca capacidad de cómputo”, ya que son 15-20% más rápidas con un rendimiento similar. Critica ese consejo: describe un escenario donde el Estado de Celda adicional de la LSTM (que la GRU no tiene) probablemente importaría lo suficiente como para valer la pena el entrenamiento más lento.
Crear Diseña un escenario (una secuencia específica de señales de vistas de página) donde esperarías que la Puerta de Olvido de la LSTM importara mucho; donde olvidar la información antigua en el momento adecuado es tan importante como recordar el principio.
Nos vemos en la Parte 7, donde construimos el mecanismo que cambió a la IA para siempre: la Atención.
Artículos relacionados
- Por qué las RNN olvidan: la intuición detrás del problema del gradiente desvanecido
- El mecanismo de atención, finalmente explicado sin matemáticas
Referencias y lecturas adicionales
- Hochreiter, S., & Schmidhuber, J. (1997). “Long Short-Term Memory.” Neural Computation, 9(8), 1735–1780. — El artículo fundamental que introdujo la arquitectura LSTM y el estado de la celda con compuertas que resolvió el problema del gradiente evanescente en las redes recurrentes.
- Cho, K., van Merriënboer, B., Gulcehre, C., Bahdanau, D., Bougares, F., Schwenk, H., & Bengio, Y. (2014). “Learning Phrase Representations using RNN Encoder–Decoder for Statistical Machine Translation.” Proceedings of EMNLP 2014. — El artículo que introdujo la GRU, una arquitectura recurrente con compuertas simplificada que tiene menos parámetros que la LSTM.
Esta traducción fue generada automáticamente y puede contener errores. Si el idioma inglés es tu preferencia, puedes leer el artículo original en inglés .
«Aplica lo que aprendiste» es para suscriptores Supporter e Insider.
Suscríbete para desbloquear los ejercicios de este artículo.
Ver planesArtículos relacionados
- Aprendizaje Profundo En revisión
Retropropagación intuitiva: cómo las redes aprenden de sus errores
Aprende cómo funciona la retropropagación de forma intuitiva, sin necesidad de cálculo. Descubre cómo las redes neuronales asignan la culpa a los pesos mediante la regla de la cadena y aprenden de sus errores.
- Aprendizaje Profundo En revisión
Atención de múltiples cabezas y codificación posicional: Dándole al Transformer un sentido de dirección
Descubre cómo la codificación posicional le otorga a los Transformers noción del orden de las palabras y la atención de múltiples cabezas rastrea múltiples patrones a la vez en esta guía práctica.
- Aprendizaje Profundo En revisión
Por qué las computadoras ven mejor con las CNN: una guía intuitiva para el reconocimiento de imágenes
Aprende por qué las CNN superan a las redes densas en el reconocimiento de imágenes utilizando filtros deslizantes, pesos compartidos y pooling para detectar patrones espaciales de manera eficiente.
- Aprendizaje Profundo En revisión
Referencia: Optimizadores
Una referencia que cubre los optimizadores de redes neuronales desde GD hasta AdamW, con programación de la tasa de aprendizaje, árboles de decisión y guía práctica para cada arquitectura.
¿Buscas otra cosa?
Busca en todos los artículos por título, resumen o tema.