Curso
Las redes neuronales recurrentes estándar tienen problemas de memoria. A medida que las secuencias superan los 20-30 pasos temporales, los gradientes o bien se encogen hasta casi cero o bien explotan durante la retropropagación, y la red pierde la capacidad de conectar partes distantes de la entrada. Hochreiter y Schmidhuber abordaron este problema en su artículo de 1997 al introducir las redes de memoria a corto y largo plazo (LSTM).
En esta guía, repasaré el funcionamiento interno de un LSTM antes de pasar a su implementación práctica en Python. En las secciones finales comparo LSTM con Transformers para que puedas elegir la arquitectura adecuada para tu caso de uso.
Te recomiendo encarecidamente nuestro curso Introduction to Deep Learning in PyTorch para conseguir experiencia práctica con PyTorch entrenando redes neuronales, que son la base de los modelos LSTM.
¿Qué es un modelo LSTM?
LSTM significa Long Short-Term Memory, un tipo de red neuronal recurrente (RNN) diseñada para manejar secuencias donde el contexto de mucho antes en la entrada sigue siendo relevante.
La idea central de los LSTM es un mecanismo de compuertas que regula el flujo de información a través de la red. En lugar de dejar pasar todas las señales por igual, las compuertas aprenden cuándo conservar el contexto y cuándo soltarlo.
Esta memoria selectiva permite a los LSTM seguir dependencias a lo largo de cientos de pasos temporales, algo muy útil en modelado del lenguaje, reconocimiento de voz y predicción de series temporales. Para entender por qué existe esta arquitectura, conviene ver qué falla en las RNN estándar.
El problema del desvanecimiento del gradiente en RNN
Las redes recurrentes transmiten información de un paso temporal al siguiente a través de un estado oculto. En cada paso se multiplica este estado por pesos aprendidos, se actualiza y se pasa adelante. Para entrenar, hay que enviar las señales de error hacia atrás a través de todos esos pasos, y en cada pasada hacia atrás la señal se encoge un poco. Tras suficientes pasos, no queda nada.
Es como hacer una fotocopia de una fotocopia. Unas pocas veces y la copia se ve bien. Cincuenta veces y apenas se puede leer. Las RNN chocan con este muro alrededor de 20-30 pasos temporales, por eso les cuestan las secuencias largas.
Cómo resuelven los LSTM las dependencias a largo plazo
Los LSTM añaden una vía de memoria separada, llamada estado de celda, que corre en paralelo al estado oculto estándar. La diferencia es estructural: la información en el estado de celda fluye por suma en lugar de por multiplicaciones repetidas, así que no se degrada del mismo modo.
Para controlar qué entra y sale de esta memoria, los LSTM usan compuertas. Cada compuerta es una pequeña red neuronal que aprende a dar valores entre 0 y 1, actuando como filtro. Un valor cercano a 0 bloquea información; uno cercano a 1 la deja pasar. La red aprende durante el entrenamiento qué patrones merece la pena recordar a lo largo de secuencias largas y cuáles se pueden descartar.
Este mecanismo de compuertas es lo que separa a los LSTM de las RNN básicas. En lugar de forzar toda la información por el mismo camino, los LSTM pueden preservar selectivamente el contexto durante cientos de pasos. En la siguiente sección desglosamos exactamente cómo funcionan estas compuertas y cómo se combinan para actualizar el estado de celda.
Casos de uso de los modelos LSTM
Los LSTM aparecen allá donde importan los datos secuenciales. Algunos ejemplos más allá de las aplicaciones típicas de PLN y forecasting:
- Los bancos analizan secuencias de transacciones con LSTM para detectar fraudes que se escapan a sistemas basados en reglas
- Los hospitales monitorizan trazas de ECG con LSTM bidireccionales, detectando anomalías cardiacas más rápido que las alertas por umbral
- Las plataformas de smart city predicen tráfico y demanda energética a partir de sensores, ajustando recursos antes de que lleguen los picos
- Las fábricas siguen la vibración de equipos a lo largo del tiempo para programar mantenimiento antes de que algo falle
Arquitectura LSTM: compuertas y celdas de memoria
Esta sección cubre en detalle el interior de un LSTM. Si prefieres ir directo a la implementación, salta a la siguiente sección. Los detalles de arquitectura no son imprescindibles para usar LSTM, pero ayudan al depurar o ajustar.
Para entender cómo funcionan los LSTM, seguiremos un ejemplo concreto: procesar la secuencia "The clouds are dark..." para predecir la siguiente palabra. Cada componente tiene más sentido cuando se ve qué hace con datos reales.
Estado de celda y estado oculto en LSTM
Un LSTM mantiene dos vectores separados que fluyen por la red: el estado de celda y el estado oculto.
El estado de celda actúa como almacenamiento a largo plazo. Transporta información entre pasos temporales con modificaciones controladas en cada paso. Al procesar nuestra secuencia, el estado de celda podría guardar "apareció un sustantivo relacionado con el tiempo" tras ver "clouds" y mantener ese contexto durante "are", hasta que "dark" refuerza el patrón.
El estado oculto es la salida de la celda en cada paso. Alimenta las predicciones y los cálculos del siguiente paso. Tras procesar "dark", el estado oculto codifica algo como "adjetivo que describe condiciones meteorológicas" que puede usar una capa de predicción.

¿Por qué dos vías separadas? Un único estado tendría que sostener a la vez el contexto a largo plazo y producir salidas útiles. Estos objetivos chocan: la memoria estable requiere mínimos cambios, mientras que generar salidas útiles requiere transformar. Separarlos permite que cada vía optimice su propósito.
En cada paso temporal, la celda recibe tres entradas y produce dos salidas:
Entradas:
-
Estado de celda previo (
C_{t-1}): lo que la red recuerda hasta ahora -
Estado oculto previo (
h_{t-1}): la salida del último paso -
Entrada actual (
x_t): el nuevo token, como "clouds" o "dark"
Salidas:
-
Estado de celda actualizado (
C_t): memoria tras procesar este token -
Nuevo estado oculto (
h_t): la salida de este paso
Cómo calculan las compuertas de un LSTM
Cuatro componentes controlan lo que ocurre dentro de la celda. Tres son compuertas que devuelven valores entre 0 y 1, actuando como filtros. El cuarto es el candidato, que propone contenido nuevo.
Cuando llega "are" después de "clouds", cada componente tiene un cometido:
-
Compuerta de olvido (
f_t): ¿Debemos mantener el contexto de "clouds"? Los sustantivos meteorológicos seguidos de verbos copulativos suelen importar, así que la compuerta de olvido da valores cercanos a 1 para preservar la mayor parte del estado de celda. -
Compuerta de entrada (
i_t): ¿Cuánto debe aportar "are" a la memoria? Los verbos copulativos llevan menos señal predictiva, así que la compuerta de entrada devuelve valores más bajos. -
Candidato (
C̃_t): ¿Qué nueva información podría añadir "are"? El candidato propone contenido, pero la compuerta de entrada lo atenúa. -
Compuerta de salida (
o_t): ¿Qué debe mostrar el estado oculto? La compuerta de salida filtra el estado de celda para producirh_t.

Cada componente calcula su salida del mismo modo:
-
Concatenar
h_{t-1}yx_ten un único vector -
Multiplicar por una matriz de pesos aprendida
-
Sumar un término de sesgo
-
Aplicar una función de activación
Las compuertas usan activación sigmoide, que comprime los valores al rango 0-1. El candidato usa tanh, devolviendo valores entre -1 y 1, para poder proponer aumentos y disminuciones al estado de celda.
Las ecuaciones
Compuerta de olvido (sigmoide → 0 a 1):
![]()
Compuerta de entrada (sigmoide → 0 a 1):
![]()
Compuerta de salida (sigmoide → 0 a 1):
![]()
Candidato (tanh → -1 a +1):
![]()
Cada componente tiene su propia matriz de pesos (W_f, W_i, W_c, W_o), por lo que aprenden a responder a distintos patrones en la entrada.

Cómo funciona el filtrado
Las salidas de las compuertas se multiplican contra vectores posición por posición (multiplicación elemento a elemento, escrita como ⊙). Cada dimensión se filtra de forma independiente.

Si una compuerta da 0,9 para la posición 1, esa dimensión conserva el 90% de su valor. Si da 0,0 para la posición 4, esa dimensión se borra. Al procesar "dark" tras "clouds are", la compuerta de olvido podría dar valores altos para dimensiones que guardan el contexto de sustantivo/tiempo y valores más bajos para dimensiones con contexto previo irrelevante.
Flujo de información entre compuertas de olvido, adición y salida
Los componentes se combinan en tres pasos para actualizar el estado de celda y producir el estado oculto.

Paso 1: olvidar
La compuerta de olvido filtra el estado de celda previo: ![]()
Procesando "dark", la compuerta de olvido preserva el contexto meteorológico de "clouds" mientras limpia las dimensiones que seguían el artículo "The".
Paso 2: añadir
La compuerta de entrada escala la propuesta del candidato y luego la suma al estado de celda filtrado:
![]()
El candidato podría proponer "adjetivo que modifica a sustantivo meteorológico", y la compuerta de entrada decide con qué fuerza escribirlo en memoria. Esta actualización aditiva (no multiplicativa) es lo que ayuda a que los gradientes fluyan durante el entrenamiento.
Paso 3: salida
El estado de celda pasa por tanh y después la compuerta de salida filtra lo que se convierte en estado oculto:
![]()
La tanh mantiene los valores acotados. La compuerta de salida selecciona qué aspectos de la memoria acumulada deben ser visibles para las predicciones en este paso.
La secuencia completa:

Aquí tienes un seguimiento simplificado con un estado de celda de 4 dimensiones:
|
Paso |
Entrada |
Estado de celda (simplificado) |
Qué ocurre |
|
1 |
"The" |
[0.1, 0.0, 0.2, 0.0] |
Almacenamiento mínimo; los artículos aportan poca señal predictiva |
|
2 |
"clouds" |
[0.1, 0.7, 0.2, 0.3] |
La compuerta de entrada se abre en las dimensiones 2 y 4; guarda patrón de sustantivo |
|
3 |
"are" |
[0.1, 0.6, 0.2, 0.3] |
La compuerta de olvido ≈ 0.9 preserva el contexto; el verbo copulativo aporta poco |
|
4 |
"dark" |
[0.1, 0.8, 0.5, 0.6] |
El adjetivo refuerza la señal en las dimensiones 2, 3 y 4 |
En el paso 4, las dimensiones 2-4 han acumulado un patrón. El estado oculto h_4 alimenta una capa de predicción (no parte del LSTM), que produce probabilidades sobre el vocabulario. Ese patrón acumulado ayuda al modelo a favorecer palabras como "rain" o "sky".
La red no entiende de meteorología. Aprende correlaciones estadísticas: ciertas secuencias de entrada tienden a preceder a ciertas salidas.
Flujo de gradientes durante el entrenamiento
La estructura aditiva del estado de celda proporciona una vía de gradiente más directa que las RNN estándar. Si no estás familiarizado con cómo se propagan los gradientes por las redes neuronales, el tutorial de backpropagation cubre los fundamentos.

En las RNN estándar, los gradientes fluyen a través de matrices de pesos en cada paso. La multiplicación repetida se compone: tras 100 pasos con pesos que promedian 0,9, el gradiente se reduce a 0.9^100 ≈ 0.00003.
Los LSTM ofrecen una vía alternativa a lo largo del estado de celda. La derivada de C_t con respecto a C_{t-1}:
∂C_t/∂C_{t-1} = f_t
Este es el valor de la compuerta de olvido, no una matriz de pesos. Cuando las compuertas de olvido se mantienen cerca de 1, los gradientes pasan con menos decaimiento. Si las compuertas promedian 0,95, tras 100 pasos: 0.95^100 ≈ 0.006. Aún reducido, pero útil.
|
Aspecto |
RNN |
LSTM |
|
Vía de gradiente |
Solo matrices de pesos |
Vía adicional a lo largo del estado de celda |
|
Gradiente a 100 pasos |
~10^-5 (desvanecido) |
~10^-2 a 10^-1 (utilizable) |
|
Memoria práctica |
~10-20 pasos temporales |
~50-200 pasos temporales |
Nota práctica: a menudo se inicializan los sesgos de la compuerta de olvido en 1.0 o más, empujando las salidas iniciales hacia "recuerda todo". Esto ayuda a la red a aprender patrones a largo plazo al inicio del entrenamiento.
Los LSTM mitigan el problema del desvanecimiento del gradiente más que eliminarlo. La mejora viene de la actualización aditiva del estado de celda, que proporciona una autopista de gradientes junto a las vías estándar.
Cómo implementar modelos LSTM en Python
La sección anterior cubrió lo que ocurre dentro de una celda LSTM: compuertas filtrando información, estado de celda preservando contexto y estado oculto produciendo salidas. Ahora veremos cómo nn.LSTM de PyTorch envuelve todo eso en un único módulo y lo usa para clasificar reseñas de películas como positivas o negativas.
Consejo para ejecutar el código: entrenar 50 épocas en una CPU tarda bastante. Usa Google Colab o Kaggle Notebooks con aceleración GPU para resultados más rápidos. El script completo está disponible como GitHub Gist.
Construir un clasificador LSTM con PyTorch
La clasificación de texto con LSTM sigue un patrón: convertir palabras en vectores, dejar que el LSTM las procese secuencialmente y después usar la salida final para predecir.

Convertir palabras en vectores
Las redes neuronales operan con números continuos, pero las palabras son símbolos discretos. No podemos meter "terrible" directamente en una multiplicación de matrices. La solución es una capa de embeddings: una tabla de búsqueda en la que cada palabra se asigna a un vector aprendible.
self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx=padding_idx)
Si eliges embedding_dim=32, cada palabra se convierte en un vector de 32 dimensiones. Al principio del entrenamiento, estos vectores son aleatorios. A medida que el modelo aprende, palabras con sentimiento similar se acercan en ese espacio vectorial. "Terrible", "awful" y "boring" podrían acabar cerca, mientras que "excellent" y "amazing" formarían otro grupo.
El argumento padding_idx indica a la capa que mantenga los tokens de relleno como vectores cero, para que no influyan en el modelo.
Definir la arquitectura LSTM
self.lstm = nn.LSTM(
input_size=embedding_dim,
hidden_size=hidden_size,
num_layers=num_layers,
batch_first=True,
dropout=dropout if num_layers > 1 else 0,
bidirectional=bidirectional,
)
Con esta sola línea creas toda la maquinaria de compuertas de la sección anterior. El hidden_size determina cuánta información pueden transportar el estado de celda y el estado oculto. Valores mayores permiten seguir más patrones, pero exigen más cómputo.
Configurar batch_first=True implica que las entradas tienen forma (batch, sequence_length, embedding_dim). El LSTM procesa cada posición de izquierda a derecha, actualizando sus estados internos en cada paso.
Procesar la secuencia
Tras procesar "This movie was absolutely terrible", el LSTM ha visto cada palabra. Su estado oculto final ha acumulado lo que el modelo ha aprendido a extraer de esa secuencia. Para sentimiento, esto podría codificar algo como "adjetivo fuertemente negativo al final, que anula contenido neutro anterior".
def forward(self, x):
embedded = self.embedding(x)
lstm_out, (h_n, c_n) = self.lstm(embedded)
h_final = h_n[-1]
return self.fc(h_final)
El LSTM devuelve salidas por paso (lstm_out) y estados finales (h_n, c_n). Para clasificar solo necesitamos h_n[-1], el estado oculto tras el último token. Una capa lineal lo mapea a puntuaciones de clase.
Trazar una reseña a través del modelo
Cuando "This movie was terrible" entra en el clasificador:
-
Cada palabra obtiene su embedding: cuatro vectores de forma
(32,) -
El LSTM los procesa secuencialmente. "This" inicializa cierto estado de celda. "Movie" lo actualiza ligeramente. "Was" es neutra y cambia poco. "Terrible" hace que la compuerta de olvido borre contexto irrelevante y que la compuerta de entrada escriba una señal negativa fuerte.
-
El estado oculto final captura el patrón acumulado.
-
La capa lineal lo mapea a dos puntuaciones: una para positivo y otra para negativo
El modelo no "entiende" el lenguaje. Aprende patrones estadísticos: ciertas secuencias de palabras se correlacionan con ciertas etiquetas.
La clase completa del modelo
A continuación puedes ver la clase LSTMClassifier completa que hemos creado paso a paso:
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset
from datasets import load_dataset
from collections import Counter
import numpy as np
torch.manual_seed(42)
np.random.seed(42)
class LSTMClassifier(nn.Module):
def __init__(
Self,
Vocab_size,
Embedding_dim,
Hidden_size,
Output_size,
num_layers=1,
dropout=0.0,
bidirectional=False,
padding_idx=0,
):
super().__init__()
self.hidden_size = hidden_size
self.num_layers = num_layers
self.bidirectional = bidirectional
self.embedding = nn.Embedding(
vocab_size, embedding_dim, padding_idx=padding_idx
)
self.lstm = nn.LSTM(
input_size=embedding_dim,
hidden_size=hidden_size,
num_layers=num_layers,
batch_first=True,
dropout=dropout if num_layers > 1 else 0,
bidirectional=bidirectional,
)
fc_input_size = hidden_size * 2 if bidirectional else hidden_size
self.fc = nn.Linear(fc_input_size, output_size)
def forward(self, x):
embedded = self.embedding(x)
lstm_out, (h_n, c_n) = self.lstm(embedded)
if self.bidirectional:
h_final = torch.cat([h_n[-2], h_n[-1]], dim=1)
else:
h_final = h_n[-1]
return self.fc(h_final)
La opción bidireccional ejecuta dos LSTM: uno de izquierda a derecha y otro de derecha a izquierda. Sus estados ocultos finales se concatenan, dando al modelo contexto de ambas direcciones. Esto ayuda cuando el significado depende de lo que viene después, como "not bad", donde "not" solo cobra sentido con "bad".
Preparar el texto para el modelo
Los LSTM necesitan secuencias de enteros, no cadenas de texto. El preprocesamiento convierte el texto bruto en tres pasos:
- Construir un vocabulario que mapee palabras a índices
- Convertir cada reseña en una secuencia de índices
- Rellenar (pad) las secuencias hasta una longitud uniforme
def build_vocab(texts, max_vocab_size=10000):
counter = Counter()
for text in texts:
counter.update(text.lower().split())
vocab = {"<pad>": 0, "<unk>": 1}
for word, _ in counter.most_common(max_vocab_size - 2):
vocab[word] = len(vocab)
return vocab
Reservamos el índice 0 para el relleno y el 1 para palabras desconocidas. Limitar el tamaño del vocabulario filtra palabras raras que no tienen suficientes ejemplos para aprender embeddings significativos.
def tokenize_and_pad(texts, vocab, max_length=200):
sequences = []
for text in texts:
tokens = text.lower().split()
ids = [vocab.get(t, vocab["<unk>"]) for t in tokens[:max_length]]
ids = ids + [vocab["<pad>"]] * (max_length - len(ids))
sequences.append(ids)
return torch.tensor(sequences, dtype=torch.long)
Truncar a max_length limita cuánto atrás debe recordar el LSTM. Secuencias más largas preservan más contexto, pero aumentan el coste computacional y de memoria. Para reseñas de cine, 100-200 tokens suelen capturar las señales clave de sentimiento.
El relleno garantiza que cada secuencia tenga la misma longitud, permitiendo a PyTorch agruparlas en un único tensor. Los tokens de relleno se convierten en vectores cero tras el embedding y aportan poco al estado final.
def load_imdb_data(max_samples=10000, max_length=200, max_vocab=10000):
dataset = load_dataset("imdb")
train_data = dataset["train"].shuffle(seed=42)
test_data = dataset["test"].shuffle(seed=42)
train_texts = train_data["text"][:max_samples]
train_labels = train_data["label"][:max_samples]
test_texts = test_data["text"][: max_samples // 4]
test_labels = test_data["label"][: max_samples // 4]
vocab = build_vocab(train_texts, max_vocab)
X_train = tokenize_and_pad(train_texts, vocab, max_length)
X_test = tokenize_and_pad(test_texts, vocab, max_length)
y_train = torch.tensor(train_labels, dtype=torch.long)
y_test = torch.tensor(test_labels, dtype=torch.long)
return X_train, y_train, X_test, y_test, vocab
Construir el vocabulario solo con datos de entrenamiento evita fugas de información. Si una palabra rara aparece solo en reseñas de test, el modelo debería tratarla como desconocida en lugar de haber aprendido su embedding durante el entrenamiento.
Entrenamiento de LSTM e hiperparámetros
El bucle de entrenamiento actualiza los pesos del modelo en función de cuánto se desvían sus predicciones de los valores correctos. En LSTM, un detalle importa más de lo habitual: el recorte de gradientes.
def train_model(model, train_loader, test_loader, n_epochs=5, lr=0.001):
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
for epoch in range(n_epochs):
model.train()
total_loss, correct, total = 0, 0, 0
for batch_texts, batch_labels in train_loader:
optimizer.zero_grad()
outputs = model(batch_texts)
loss = criterion(outputs, batch_labels)
loss.backward()
torch.nn.utils.clip_grad_norm_(
model.parameters(),
max_norm=5.0)
optimizer.step()
total_loss += loss.item()
_, predicted = outputs.max(1)
correct += (predicted == batch_labels).sum().item()
total += batch_labels.size(0)
Por qué importa el recorte de gradientes en LSTM
Antes explicamos cómo los LSTM mitigan los gradientes que se desvanecen mediante actualizaciones aditivas del estado de celda. Pero los gradientes que explotan siguen siendo un riesgo. Cuando los gradientes crecen demasiado, las actualizaciones de pesos se pasan de rosca y desestabilizan el entrenamiento.
clip_grad_norm_ limita la magnitud total del gradiente. Si los gradientes fueran a superar max_norm=5.0, se escalan proporcionalmente a la baja. Así se mantienen actualizaciones estables sin eliminar por completo la señal de gradiente.
model.eval()
test_correct, test_total = 0, 0
with torch.no_grad():
for batch_texts, batch_labels in test_loader:
outputs = model(batch_texts)
_, predicted = outputs.max(1)
test_correct += (predicted == batch_labels).sum().item()
test_total += batch_labels.size(0)
test_acc = test_correct / test_total
print(f"Epoch {epoch+1}: Loss={total_loss/len(train_loader):.4f}, "
f"Train={correct/total:.3f}, Test={test_acc:.3f}")
return test_acc
model.eval() desactiva el dropout durante la evaluación. torch.no_grad() evita calcular gradientes cuando no vamos a actualizar pesos, lo que ahorra memoria y acelera la inferencia.
Elegir hiperparámetros
X_train, y_train, X_test, y_test, vocab = load_imdb_data(
max_samples=10000, max_length=100, max_vocab=5000
)
train_loader = DataLoader(TensorDataset(X_train, y_train), batch_size=64, shuffle=True)
test_loader = DataLoader(TensorDataset(X_test, y_test), batch_size=64)
model = LSTMClassifier(
vocab_size=len(vocab),
embedding_dim=32,
hidden_size=32,
output_size=2,
padding_idx=vocab["<pad>"],
)
final_acc = train_model(model, train_loader, test_loader, n_epochs=50, lr=0.001)
Estos valores reflejan equilibrios:
-
hidden_size=32: Lo bastante pequeño para iterar rápido. En producción se suelen usar 128-512, pero a medida que crece el tamaño los beneficios disminuyen y el entrenamiento se ralentiza. -
embedding_dim=32: Igualar el tamaño oculto es común pero no obligatorio. Embeddings más grandes capturan matices más finos a costa de más parámetros. -
max_length=100: La mayoría de señales de sentimiento aparecen pronto en las reseñas. Secuencias más largas ayudan en casos sutiles, pero cuadruplican la memoria al doblarmax_length. -
batch_size=64: Lotes mayores dan estimaciones de gradiente más estables. Lotes más pequeños pueden ayudar a salir de mínimos locales. 32-128 es típico. -
lr=0.001: El valor por defecto de Adam funciona en la mayoría de tareas con LSTM. Si la pérdida oscila, prueba 0.0001. Si el aprendizaje se estanca, prueba 0.01. -
n_epochs=50: Suficiente para que el modelo converja. Vigila si la precisión en test se estanca o baja mientras la de train sigue subiendo: indica sobreajuste.
Qué aprende el modelo
Con esta configuración, verás algo como:
...
Epoch 46: Loss=0.4126, Train=0.812, Test=0.789
Epoch 47: Loss=0.4054, Train=0.815, Test=0.791
Epoch 48: Loss=0.3998, Train=0.818, Test=0.788
Epoch 49: Loss=0.3921, Train=0.821, Test=0.793
Epoch 50: Loss=0.3867, Train=0.824, Test=0.795
La precisión de entrenamiento alcanza alrededor del 82% y la de test se estabiliza cerca del 80%. El modelo aprende qué patrones de palabras predicen sentimiento. Los adjetivos fuertes al final pesan más. Negaciones como "not" invierten la polaridad. Ciertas frases ("waste of time", "highly recommend") se vuelven señales claras. Nada de esto está programado: emerge de las correlaciones del conjunto de entrenamiento.
Con un ajuste mínimo y un modelo pequeño, un 80% de exactitud es razonable. La pequeña brecha entre training y test muestra que el modelo generaliza bien sin sobreajustar en exceso. Añadir dropout, usar embeddings preentrenados como GloVe o entrenar con más datos podría llevar la precisión a finales de los 80.
Cómo mejorar el rendimiento de los modelos LSTM
La arquitectura LSTM básica de las secciones anteriores funciona, pero varias técnicas pueden aumentar la precisión sin cambiar de familia de modelos. Estas aproximaciones han sido validadas en miles de despliegues en producción y artículos de investigación.
LSTM bidireccionales
Los LSTM estándar procesan secuencias de izquierda a derecha, construyendo contexto solo desde el pasado. Los LSTM bidireccionales (BiLSTM) ejecutan dos pasadas en paralelo: una hacia adelante y otra hacia atrás. La representación final en cada paso combina lo que vino antes y lo que viene después.
self.lstm = nn.LSTM(
input_size=embedding_dim,
hidden_size=hidden_size,
bidirectional=True, # Run both directions
batch_first=True,
)
# Output size doubles: hidden_size * 2

En análisis de sentimiento, esto importa cuando la negación aparece después de la palabra que modifica: "the movie was not bad" necesita contexto hacia adelante para saber que "bad" existe, y hacia atrás para saber que "not" lo precede.
Los BiLSTM superan de forma consistente a los modelos unidireccionales en reconocimiento de entidades (NER), clasificación de sentimiento y reconocimiento de voz, con mejoras típicas del 5-15%.
La contrapartida: necesitas la secuencia completa en inferencia. Las aplicaciones en streaming que reciben tokens uno a uno no pueden usar procesamiento bidireccional.
Añadir atención
La atención permite al modelo ponderar qué pasos temporales importan más para una predicción. En lugar de comprimir toda una secuencia en un solo estado oculto final, la atención calcula una combinación ponderada de todos los estados ocultos.
class AttentionLayer(nn.Module):
def __init__(self, hidden_size):
super().__init__()
self.attention = nn.Linear(hidden_size, 1)
def forward(self, lstm_outputs):
# lstm_outputs: (batch, seq_len, hidden_size)
scores = self.attention(lstm_outputs) # (batch, seq_len, 1)
weights = torch.softmax(scores, dim=1)
context = torch.sum(weights * lstm_outputs, dim=1)
return context

En una reseña como "The acting was wooden, the plot made no sense, but the cinematography was stunning", la atención aprende a centrarse en las frases que portan sentimiento en lugar de tratar todas las palabras por igual. El modelo asigna pesos más altos a "wooden", "no sense" y "stunning" y reduce "the" y "was".
La atención añade poca sobrecarga computacional y suele aportar mejoras del 2-5% en clasificación. En problemas secuencia a secuencia como traducción, la atención entre codificador y decodificador es casi imprescindible para un buen rendimiento.
Apilar capas LSTM
Una sola capa LSTM capta patrones a un nivel de abstracción. Al apilar varias capas, la red aprende características jerárquicas: la primera capa puede detectar patrones locales de frases, mientras que las superiores combinan esos patrones en comprensión a nivel de documento.
self.lstm = nn.LSTM(
input_size=embedding_dim,
hidden_size=hidden_size,
num_layers=3, # Stack three layers
dropout=0.3, # Dropout between layers (not within)
batch_first=True,
)
Pautas para la profundidad:
- 1 capa: Tareas sencillas, conjuntos pequeños, iteración rápida
- 2 capas: La mayoría de aplicaciones en producción
- 3-4 capas: Dependencias temporales complejas, conjuntos grandes
- Más de 4: Rendimientos decrecientes, inestabilidad en entrenamiento
Cada capa adicional aproximadamente dobla el tiempo de entrenamiento. Empieza con poca profundidad y añádela solo si las métricas de validación se estancan.
Técnicas de regularización
Los LSTM sobreajustan con facilidad, especialmente en conjuntos pequeños. Tres técnicas funcionan bien juntas:
- Dropout
- Recorte de gradientes
- Early stopping
Dropout
El dropout pone a cero neuronas aleatoriamente durante el entrenamiento, obligando a la red a distribuir el aprendizaje por múltiples vías. Aplícalo entre capas LSTM, no dentro de las conexiones recurrentes (puede estropear el aprendizaje temporal).
# PyTorch aplica dropout entre capas apiladas automáticamente
self.lstm = nn.LSTM(..., num_layers=2, dropout=0.3)
# Para LSTM de una sola capa, añade dropout después
self.dropout = nn.Dropout(0.3)
Tasas de 0.2-0.5 funcionan en la mayoría de tareas. Valores más altos ayudan con sobreajuste severo pero ralentizan la convergencia.
Recorte de gradientes
El recorte de gradientes limita su magnitud durante la retropropagación. Aunque los LSTM mitigan los gradientes que se desvanecen, los que explotan siguen ocurriendo, sobre todo con secuencias largas o redes profundas.
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0)
Valores entre 1.0 y 5.0 son típicos. Si la pérdida de entrenamiento se dispara o produce NaN, baja el umbral.
Early stopping
El early stopping monitoriza la pérdida de validación y detiene el entrenamiento cuando deja de mejorar. Así evitas que el modelo memorice el entrenamiento a costa de la generalización.
patience = 5
best_val_loss = float('inf')
epochs_without_improvement = 0
for epoch in range(max_epochs):
train_loss = train_one_epoch(model, train_loader)
val_loss = evaluate(model, val_loader)
if val_loss < best_val_loss:
best_val_loss = val_loss
epochs_without_improvement = 0
torch.save(model.state_dict(), 'best_model.pt')
else:
epochs_without_improvement += 1
if epochs_without_improvement >= patience:
break
Para un recorrido completo por bucles de entrenamiento, métricas de evaluación y depuración de problemas comunes en LSTM, consulta la guía de NLP con PyTorch.
Embeddings preentrenados
Inicializar embeddings al azar funciona, pero embeddings preentrenados como GloVe o Word2Vec le dan al modelo una ventaja inicial. Estos embeddings codifican relaciones semánticas aprendidas de miles de millones de palabras: "king" y "queen" acaban cerca, igual que "terrible" y "awful".
import numpy as np
def load_glove_embeddings(glove_path, vocab, embedding_dim):
embeddings = np.random.randn(len(vocab), embedding_dim) * 0.01
with open(glove_path, 'r') as f:
for line in f:
parts = line.split()
word = parts[0]
if word in vocab:
embeddings[vocab[word]] = np.array(parts[1:], dtype=np.float32)
return torch.tensor(embeddings, dtype=torch.float32)
# Load and freeze or fine-tune
pretrained = load_glove_embeddings('glove.6B.100d.txt', vocab, 100)
model.embedding.weight.data.copy_(pretrained)
model.embedding.weight.requires_grad = True # Fine-tune during training
Los embeddings preentrenados ayudan sobre todo cuando los datos de entrenamiento son limitados. Con millones de ejemplos, los embeddings aleatorios acaban alcanzando. En aplicaciones prácticas con 10k-100k muestras, espera mejoras del 3-8% con inicialización preentrenada.
Combinar técnicas
Estas técnicas se pueden apilar. Un clasificador de texto de producción podría usar:
- BiLSTM para contexto bidireccional
- 2 capas apiladas para rasgos jerárquicos
- Pooling con atención en lugar del estado oculto final
- Embeddings GloVe ajustados durante el entrenamiento
- Dropout de 0.3 entre capas
- Recorte de gradiente a 5.0
- Early stopping con paciencia de 5 épocas
Cada añadido incrementa la complejidad y el tiempo de entrenamiento. Empieza con un LSTM sencillo de una capa, mide el rendimiento y luego añade técnicas una a una para ver qué te aporta en tu tarea concreta.
LSTM vs Transformers: cómo elegir la arquitectura adecuada
Los LSTM dominaron el modelado secuencial desde finales de los 90 hasta mediados de los 2010. El artículo de 2017 "Attention Is All You Need" cambió eso. Los Transformers impulsan la mayoría de LLM actuales, pero los LSTM no han desaparecido. Cubren otros nichos.
Limitaciones de LSTM
Ya vimos cómo los LSTM procesan secuencias de un token en un token y por qué su mecanismo de compuertas funciona. A gran escala, esa dependencia secuencial se convierte en cuello de botella. Tu GPU puede tener miles de núcleos, pero la mayoría quedan ociosos mientras se completa la cadena del estado oculto. Una entrada de 500 tokens implica 500 operaciones en serie.

Los Transformers no tienen esta restricción. La autoatención calcula relaciones entre todas las posiciones simultáneamente, así que esa secuencia de 500 tokens se procesa en paralelo. En hardware moderno con lotes grandes, esto se traduce en aceleraciones de entrenamiento de 10 a 100 veces.
También importa el límite de longitud de secuencia. Vimos cómo los LSTM preservan mejor los gradientes que las RNN básicas, pero "mejor" no significa "perfecto". El rendimiento sigue cayendo en torno a 200-500 tokens, según la tarea.
Los Transformers calculan atención entre cada par de posiciones directamente, así que el token 1 atiende al 1000 con la misma facilidad. La contrapartida es la memoria: las matrices de atención crecen cuadráticamente con la longitud de la secuencia.
Las activaciones de las compuertas también dificultan la interpretación. Los valores van de 0 a 1 sin límites claros. Los pesos de atención en Transformers tampoco son explicaciones perfectas, pero dan una señal más clara de qué tokens de entrada importaron para cada salida.
Cuándo usar LSTM o Transformers
Tiene sentido usar Transformers cuando:
- Los datos de entrenamiento superan 100K ejemplos y tienes GPU
- La tarea implica dependencias de cientos de tokens
- La velocidad de entrenamiento importa para iterar
- Puedes ajustar modelos preentrenados como GPT a tu caso
Los LSTM encajan cuando:
- El dispositivo objetivo tiene memoria muy limitada (edge, embebido, móvil)
- Hay pocos datos (menos de 10K ejemplos) y un Transformer sobreajustaría
- Las predicciones deben hacerse con secuencias parciales antes de recibir toda la entrada
- Los requisitos de latencia exigen modelos ligeros
La previsión de series temporales queda en terreno intermedio. Benchmarks recientes muestran un rendimiento similar en datos univariantes con patrones consistentes. Los Transformers destacan cuando interactúan múltiples variables de forma compleja, pero los LSTM aguantan bien cuando faltan datos o el cómputo es ajustado.
Los diseños híbridos combinan ambos enfoques. Algunas arquitecturas apilan capas LSTM con mecanismos de atención. xLSTM, publicado en 2024, revisita el diseño original con compuertas exponenciales y memoria matricial, mostrando resultados competitivos con Transformers a escalas más pequeñas.
|
Aspecto |
LSTM |
Transformer |
|
Procesamiento |
Secuencial (token a token) |
Paralelo (todas las posiciones a la vez) |
|
Velocidad de entrenamiento |
Más lenta (paralelización limitada) |
Más rápida en GPU |
|
Dependencias de largo alcance |
Degradan más allá de 200-500 tokens |
Maneja miles de tokens |
|
Escalado de memoria |
O(n) con la longitud |
O(n²) por matrices de atención |
|
Rendimiento con pocos datos |
Bueno (menos parámetros) |
Propenso al sobreajuste |
|
Despliegue en edge |
Adecuado (menor huella de memoria) |
Complicado (modelos grandes) |
|
Streaming/tiempo real |
Soporte nativo |
Requiere la secuencia completa |
|
Ecosistema preentrenado |
Limitado |
Extenso (BERT, GPT, etc.) |
La decisión suele depender de las restricciones. ¿Cómputo y datos sin límite? Los Transformers ganan en la mayoría de tareas de PLN. ¿Un microcontrolador con 10K ejemplos de entrenamiento? Los LSTM siguen siendo prácticos.
Conclusión
Los LSTM resolvieron un problema real cuando llegaron en 1997. El mecanismo de compuertas introducido por Hochreiter y Schmidhuber permitió que las redes por fin aprendieran de secuencias más largas que unas pocas decenas de tokens. Esa arquitectura impulsó la traducción automática, el reconocimiento de voz y la predicción de series temporales durante casi dos décadas.
Hoy, los Transformers manejan la mayoría de tareas de PLN a gran escala, pero los LSTM no han quedado obsoletos. Siguen teniendo sentido para despliegues en edge, aplicaciones en streaming y situaciones con datos de entrenamiento limitados.
Si vas a empezar un proyecto nuevo de modelado secuencial, prueba primero con un LSTM sencillo. Mide su rendimiento, entiende sus puntos débiles y decide luego si el problema realmente requiere un Transformer. Los conceptos de esta guía (compuertas, estado de celda, flujo de gradientes) te ayudarán a entender arquitecturas más complejas.
Para practicar, te recomiendo cursar nuestro curso de Recurrent Neural Networks, que recorre más aplicaciones de LSTM con ejercicios guiados.
Preguntas frecuentes sobre LSTM
¿Cuál es la diferencia entre un LSTM y una RNN normal?
Las RNN normales tienen dificultades con secuencias largas porque los gradientes se encogen durante la retropropagación y pierden la capacidad de aprender de contexto distante. Los LSTM añaden un estado de celda con mecanismos de compuertas que controlan el flujo de información, permitiendo a la red conservar contexto relevante a lo largo de cientos de pasos en lugar de solo 20-30.
¿Cuándo debería usar LSTM en lugar de Transformers?
Los LSTM funcionan mejor cuando tienes pocos datos de entrenamiento (menos de 10K ejemplos), necesitas ejecutar en dispositivos con memoria limitada como móviles o sistemas embebidos, requieres predicciones en tiempo real sobre datos en streaming antes de recibir la secuencia completa, o tienes presupuestos de latencia estrictos que descartan modelos más grandes.
¿Qué significa hidden_size en el LSTM de PyTorch?
El parámetro hidden_size determina la dimensionalidad de los vectores de estado oculto y de celda. Valores mayores permiten al modelo rastrear más patrones, pero aumentan el cómputo y la memoria. Entre 32 y 512 es común; 128-256 es típico en texto en producción.
¿Cómo evito que mi LSTM sobreajuste?
Usa dropout entre capas LSTM (tasas de 0.2-0.5), aplica recorte de gradiente para evitar explosiones, implementa early stopping para detener el entrenamiento cuando la validación deje de mejorar y valora usar embeddings preentrenados como GloVe si tu conjunto es pequeño.
¿Qué es un LSTM bidireccional y cuándo debo usarlo?
Un LSTM bidireccional lanza dos pasadas sobre la secuencia: una hacia adelante y otra hacia atrás. Así cada posición tiene contexto de ambas direcciones, lo que ayuda cuando el significado depende de tokens futuros (como "not bad", donde "not" necesita ver "bad"). Úsalo para clasificación y etiquetado cuando tengas la secuencia completa en inferencia, pero evítalo en streaming.
Soy creador de contenidos sobre ciencia de datos con más de 2 años de experiencia y uno de los mayores seguimientos en Medium. Me gusta escribir artículos detallados sobre IA y ML con un toque sarcástico, porque hay que darle algo de vidilla al tema. He publicado más de 130 artículos y un curso en DataCamp, y tengo otro en marcha. Mis contenidos han sido vistos por más de 5 millones de personas; 20.000 de ellas se convirtieron en seguidores tanto en Medium como en LinkedIn.




