Gated Recurrent Unit (GRU)
La alternativa elegante al LSTM: misma potencia contra el vanishing gradient, menos parámetros y más velocidad. Reset/update gates, ecuaciones formales, implementación en PyTorch y TensorFlow, y una comparación exhaustiva con LSTM para saber cuándo usar cada uno.
⚡ ¿Por qué simplificar el LSTM?
El LSTM resolvió el problema del vanishing gradient de forma brillante, pero su arquitectura con 3 puertas, un cell state separado y 4 conjuntos de matrices de pesos es relativamente pesada. En 2014, Kyunghyun Cho et al. se preguntaron: ¿podemos lograr un rendimiento similar con una arquitectura más simple?
| Aspecto | LSTM | GRU |
|---|---|---|
| Puertas | 3 (forget, input, output) | 2 (reset, update) |
| Estados | 2 (h_t + C_t) | 1 (solo h_t) |
| Matrices de pesos | 4 conjuntos | 3 conjuntos |
| Parámetros (d_h=256, d_x=100) | ~367K | ~275K (~25% menos) |
| Velocidad de entrenamiento | Referencia | ~15-25% más rápido |
La idea del GRU: Combinar la forget gate y la input gate en una sola «update gate», eliminar el cell state separado, y usar directamente el hidden state con un mecanismo de actualización controlada. Menos parámetros, entrenamiento más rápido, rendimiento comparable en la mayoría de tareas.
📜 Origen del GRU
El Gated Recurrent Unit fue propuesto por Cho et al. en 2014, en el contexto de traducción automática neuronal. Los autores querían una unidad recurrente eficiente para su sistema encoder-decoder:
Curiosamente, Dzmitry Bahdanau — coautor del paper del GRU — publicó en el mismo año su célebre paper sobre el mecanismo de atención, que eventualmente llevaría a los Transformers. El GRU y la atención nacieron de la misma necesidad: hacer que los modelos secuenciales fueran más eficientes y capaces.
🔄 Las dos puertas del GRU
A diferencia del LSTM con sus 3 puertas + cell state, el GRU opera con solo 2 puertas y un único estado (el hidden state \(h_t\)):
Reset Gate \(r_t\)
«¿Cuánto del pasado ignorar?» Controla cuánta información del hidden state anterior se utiliza para calcular el nuevo candidato. Con \(r_t \approx 0\), el candidato se calcula casi como si no hubiera historia previa — un «reset» de la memoria.
r_t = 1 → usar h_{t-1} completamente
Update Gate \(z_t\)
«¿Cuánto actualizar?» Combina las funciones de la forget gate y la input gate del LSTM en una sola puerta. Controla el balance entre mantener el estado anterior y adoptar el nuevo candidato.
z_t ≈ 0 → reemplazar con h̃_t (candidato nuevo)
La clave del GRU: La update gate \(z_t\) implementa un mecanismo de «coupled gate» — lo que no se actualiza se mantiene, y lo que se actualiza reemplaza. No hay decisión independiente de «qué olvidar» y «qué escribir» como en LSTM; es un único trade-off: \(h_t = z_t \odot h_{t-1} + (1-z_t) \odot \tilde{h}_t\)
🔬 Anatomía de una celda GRU
El GRU es más compacto que el LSTM. Cada paso temporal recibe \(h_{t-1}\) y \(x_t\), y produce \(h_t\) — sin cell state separado:
Paso a paso: flujo de datos en una celda GRU
Nota elegante: Cuando \(z_t = 1\), el GRU simplemente copia \(h_{t-1}\) sin ninguna transformación — el gradiente fluye directamente a través del tiempo, como una skip connection. Cuando \(z_t = 0\), el GRU ignora completamente el pasado y usa solo el candidato nuevo. Los valores intermedios interpolan suavemente.
🔍 GRU vs LSTM: comparación visual
📐 Ecuaciones del GRU
Las ecuaciones del GRU son más compactas que las del LSTM. En cada paso temporal:
Diferencia clave con LSTM: En el LSTM, la forget gate \(f_t\) y la input gate \(i_t\) son independientes — pueden ambas estar en 1 o ambas en 0. En el GRU, la update gate \(z_t\) fuerza un trade-off: \(z_t\) controla cuánto mantener y \((1-z_t)\) controla cuánto actualizar. Siempre suman 1.
📏 Dimensiones y conteo de parámetros
El GRU tiene 3 conjuntos de matrices de pesos (vs 4 del LSTM):
| Componente | Matriz | Dimensiones | Bias |
|---|---|---|---|
| Reset gate | \(W_r\) | \(d_h \times (d_h + d_x)\) | \(b_r \in \mathbb{R}^{d_h}\) |
| Update gate | \(W_z\) | \(d_h \times (d_h + d_x)\) | \(b_z \in \mathbb{R}^{d_h}\) |
| Candidato | \(W_h\) | \(d_h \times (d_h + d_x)\) | \(b_h \in \mathbb{R}^{d_h}\) |
📈 ¿Por qué el GRU también resuelve el vanishing gradient?
El gradiente del hidden state a través del tiempo:
Cuando la update gate \(z_t \approx 1\), tenemos \(h_t \approx h_{t-1}\) y el gradiente es aproximadamente la identidad — la información fluye sin atenuación, exactamente como en el cell state del LSTM:
Observación: Tanto el LSTM como el GRU resuelven el vanishing gradient mediante el mismo principio: crear un camino directo (highway) para el gradiente con multiplicación por valores cercanos a 1. En el LSTM ese camino es el cell state; en el GRU es directamente el hidden state con la interpolación de la update gate.
🎛️ Explorador interactivo del GRU
Ajusta los valores de las gates y observa cómo el GRU interpola entre mantener el estado anterior y adoptar el candidato:
🔥 GRU en PyTorch
La API de PyTorch para GRU es prácticamente idéntica a la de LSTM. La principal diferencia
es que nn.GRU retorna (output, h_n) en vez de
(output, (h_n, c_n)) — no hay cell state.
import torch
import torch.nn as nn
class GRUClassifier(nn.Module):
def __init__(self, vocab_size, embed_dim, hidden_dim, num_classes,
num_layers=2, dropout=0.3, bidirectional=True):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0)
self.gru = nn.GRU(
input_size=embed_dim,
hidden_size=hidden_dim,
num_layers=num_layers,
batch_first=True,
dropout=dropout,
bidirectional=bidirectional
)
direction_factor = 2 if bidirectional else 1
self.fc = nn.Linear(hidden_dim * direction_factor, num_classes)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
embedded = self.dropout(self.embedding(x))
# GRU retorna (output, h_n) — sin cell state
output, h_n = self.gru(embedded)
if self.gru.bidirectional:
h_final = torch.cat([h_n[-2], h_n[-1]], dim=1)
else:
h_final = h_n[-1]
return self.fc(self.dropout(h_final))
# Comparación directa: misma tarea, GRU vs LSTM
gru_model = GRUClassifier(vocab_size=10000, embed_dim=128,
hidden_dim=256, num_classes=5)
x = torch.randint(0, 10000, (32, 50))
logits = gru_model(x)
# Contar parámetros
gru_params = sum(p.numel() for p in gru_model.parameters())
print(f"GRU params: {gru_params:,}")
import torch
import torch.nn as nn
class ManualGRU(nn.Module):
"""GRU paso a paso con GRUCell."""
def __init__(self, input_dim, hidden_dim):
super().__init__()
self.hidden_dim = hidden_dim
self.cell = nn.GRUCell(input_dim, hidden_dim)
def forward(self, x):
batch, seq_len, _ = x.shape
h = torch.zeros(batch, self.hidden_dim, device=x.device)
outputs = []
for t in range(seq_len):
h = self.cell(x[:, t, :], h) # Solo h, sin cell state
outputs.append(h)
return torch.stack(outputs, dim=1), h
# Ventaja del GRU: inicialización más simple
model = ManualGRU(input_dim=64, hidden_dim=128)
x = torch.randn(1, 20, 64)
outputs, h_final = model(x)
print(f"Hidden state final: norma={h_final.norm():.4f}")
# No necesitas gestionar cell state — una variable menos
🟧 GRU en TensorFlow/Keras
import tensorflow as tf
def build_gru_model(vocab_size=10000, embed_dim=128, hidden_dim=128,
max_len=200, num_classes=2):
model = tf.keras.Sequential([
tf.keras.layers.Embedding(vocab_size, embed_dim,
input_length=max_len),
tf.keras.layers.SpatialDropout1D(0.2),
# GRU bidireccional
tf.keras.layers.Bidirectional(
tf.keras.layers.GRU(hidden_dim, return_sequences=True,
dropout=0.2, recurrent_dropout=0.2)
),
tf.keras.layers.Bidirectional(
tf.keras.layers.GRU(hidden_dim // 2,
dropout=0.2, recurrent_dropout=0.2)
),
tf.keras.layers.Dense(32, activation='relu'),
tf.keras.layers.Dropout(0.3),
tf.keras.layers.Dense(num_classes, activation='softmax')
])
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
return model
model = build_gru_model()
model.summary()
# Comparar el total de parámetros con un LSTM equivalente:
# GRU tendrá ~25% menos parámetros en las capas recurrentes
import tensorflow as tf
import numpy as np
def build_gru_time_series(seq_len=60, n_features=3, hidden_dim=64):
"""GRU para predicción multivariante de series temporales."""
inputs = tf.keras.Input(shape=(seq_len, n_features))
x = tf.keras.layers.GRU(hidden_dim, return_sequences=True)(inputs)
x = tf.keras.layers.Dropout(0.2)(x)
x = tf.keras.layers.GRU(hidden_dim // 2)(x)
x = tf.keras.layers.Dense(16, activation='relu')(x)
outputs = tf.keras.layers.Dense(1)(x)
model = tf.keras.Model(inputs, outputs)
model.compile(optimizer='adam', loss='mse')
return model
# Ejemplo: 3 features (temperatura, humedad, presión)
model = build_gru_time_series()
X = np.random.randn(1000, 60, 3).astype(np.float32)
y = np.random.randn(1000).astype(np.float32)
model.fit(X, y, epochs=5, batch_size=32, verbose=0)
print(f"GRU time series model params: {model.count_params():,}")
Consejo práctico: En TensorFlow, el GRU con reset_after=True
(por defecto desde TF 2.x) es compatible con la implementación cuDNN optimizada por GPU,
lo que lo hace significativamente más rápido. El LSTM también tiene optimización cuDNN,
pero el GRU se beneficia más por tener menos operaciones.
⚖️ LSTM vs GRU: comparación sistemática
¿Cuándo usar LSTM y cuándo GRU? No hay una respuesta universal. La investigación empírica (Chung et al. 2014, Jozefowicz et al. 2015, Greff et al. 2016) muestra que depende de la tarea y los datos. Aquí tienes una guía práctica:
| Criterio | LSTM | GRU | Ganador |
|---|---|---|---|
| Parámetros | 4 × d_h(d_h+d_x+1) | 3 × d_h(d_h+d_x+1) | GRU (~25% menos) |
| Velocidad | Referencia | ~15-25% más rápido | GRU |
| Memoria GPU | 2 estados (h_t + C_t) | 1 estado (h_t) | GRU |
| Dependencias muy largas | Cell state separado | Interpolación directa | LSTM (ligeramente) |
| Datasets pequeños | Puede overfittear | Menos parámetros = menos overfit | GRU |
| Datasets grandes | Mayor capacidad | Puede ser insuficiente | LSTM |
| Tareas de NLP complejas | Más flexible (gates indep.) | Suficiente en muchos casos | ~Empate |
| Series temporales | Bueno | Igual de bueno, más rápido | GRU (eficiencia) |
| Interpretabilidad | Cell state analizable | Hidden state directo | ~Empate |
Regla general:
- Empieza con GRU si no tienes razones específicas para usar LSTM. Es más rápido de entrenar y suele dar resultados comparables.
- Usa LSTM si necesitas máxima expresividad (datasets grandes, dependencias muy largas) o si el rendimiento del GRU no es suficiente.
- Prueba ambos — la diferencia en rendimiento suele ser pequeña, y el mejor modelo depende del problema concreto.
📊 Simulador comparativo: RNN vs LSTM vs GRU
Observa cómo los tres modelos propagan un gradiente a lo largo de una secuencia. Ajusta los parámetros y compara el comportamiento:
🌍 Aplicaciones destacadas del GRU
🔮 RNN, LSTM, GRU y Transformers: el panorama completo
Para cerrar el módulo de redes recurrentes, veamos cómo encajan las tres arquitecturas en el panorama general del deep learning para secuencias:
Lo que has aprendido en el módulo RNN:
- Fundamentos: Datos secuenciales, recurrencia, hidden state, BPTT, vanishing/exploding gradients.
- LSTM: Cell state, forget/input/output gates, cómo resuelve el vanishing gradient, la «neurona sintiente».
- GRU: Simplificación elegante, reset/update gates, cuándo usar cada arquitectura.
Estas ideas son fundamentales para entender por qué los Transformers funcionan como funcionan — los mecanismos de atención y las skip connections heredan conceptos directos del LSTM y GRU.