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 (ht + Ct) | 1 (solo ht) |
| Matrices de pesos | 4 conjuntos | 3 conjuntos |
| Parámetros (dh=256, dx=100) | ~367K | ~275K (~25% menos) |
| Velocidad de entrenamiento | Referencia | ~15-25% más rápido |
Esta reducción del ~25% en parámetros puede parecer modesta, pero tiene implicaciones prácticas significativas. Menos parámetros significa entrenamiento más rápido, menor consumo de memoria GPU, y menor riesgo de sobreajuste en datasets pequeños o medianos. Además, con menos operaciones por paso temporal, el GRU permite procesar secuencias más largas en el mismo tiempo — una ventaja crucial en aplicaciones de tiempo real como reconocimiento de voz en streaming o predicción de texto en teclados móviles.
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 junio de 2014 en el paper «Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation», dentro del contexto de traducción automática neuronal. Los autores buscaban una unidad recurrente eficiente para su sistema encoder-decoder que pudiera capturar dependencias a largo plazo sin la complejidad computacional del LSTM. El resultado fue una arquitectura que demostraba que la simplificación podía lograrse sin sacrificar rendimiento.
Hochreiter & Schmidhuber publican el LSTM, resolviendo el vanishing gradient con cell state y puertas. Arquitectura compleja pero eficaz.
Cho, van Merriënboer, Gulcehre, Bahdanau et al. proponen el Gated Recurrent Unit como alternativa más simple al LSTM. Dos puertas en lugar de tres, un solo estado en lugar de dos. Rendimiento comparable con ~25% menos parámetros.
Bahdanau, Cho & Bengio publican el mecanismo de atención para traducción neuronal. El GRU y la atención comparten coautores y la misma motivación: eficiencia.
Greff et al. comparan exhaustivamente LSTM, GRU y variantes, concluyendo que el GRU rinde de forma comparable al LSTM en la mayoría de tareas.
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. De hecho, el paper original del GRU fue uno de los primeros en utilizar una arquitectura encoder-decoder para traducción automática neuronal — un paradigma que dominaría el campo durante los siguientes años.
Referencias fundamentales:
- Cho, K. et al. (2014). «Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation». EMNLP 2014. — Paper original del GRU.
- Bahdanau, D., Cho, K. & Bengio, Y. (2014). «Neural Machine Translation by Jointly Learning to Align and Translate». ICLR 2015. — Mecanismo de atención.
- Chung, J. et al. (2014). «Empirical Evaluation of Gated Recurrent Neural Networks on Sequence Modeling». — Comparación directa GRU vs LSTM.
🔄 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.
rt = 1 → usar ht−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.
zt ≈ 0 → reemplazar con h̃t (candidato nuevo)
La diferencia fundamental entre las puertas del GRU y las del LSTM radica en el acoplamiento. En el LSTM, la forget gate y la input gate son independientes: pueden ambas estar abiertas simultáneamente (añadir información nueva sin borrar la vieja) o ambas cerradas. En el GRU, la update gate \(z_t\) implementa un trade-off obligatorio: si \(z_t\) es alta (actualizar mucho), automáticamente \(1 - z_t\) es baja (retener poco del pasado), y viceversa. Esta restricción simplifica la dinámica del modelo y reduce la cantidad de configuraciones posibles que la red necesita aprender.
La reset gate, por su parte, juega un papel que no tiene equivalente directo en el LSTM. Al controlar cuánto del hidden state previo se utiliza para calcular el candidato, permite a la celda «ignorar» su historia cuando detecta un cambio de contexto abrupto — por ejemplo, al cruzar un límite de oración en un texto, o al iniciar un nuevo patrón en una serie temporal.
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
La mejor forma de entender las diferencias arquitectónicas entre GRU y LSTM es verlas en paralelo. Aunque ambas resuelven el mismo problema (capturar dependencias a largo plazo evitando el vanishing gradient), lo hacen con mecanismos distintos:
🧠 LSTM
- 3 gates + 1 candidato
- 2 estados (Ct + ht)
- 4 × dh(dh + dx + 1) parámetros
- Ct: camino lineal separado para el gradiente
- Forget e input gates independientes
⚡ GRU
- 2 gates + 1 candidato
- 1 estado (solo ht)
- 3 × dh(dh + dx + 1) parámetros
- ht: interpolación directa como skip connection
- Update gate = coupled forget + input
La reducción de una gate y un estado completo no es cosmética: implica ~25% menos de parámetros y, en consecuencia, menos cómputo por paso temporal, menor riesgo de sobreajuste en datasets pequeños, y tiempos de entrenamiento más cortos. La pregunta práctica es si esa simplificación sacrifica expresividad — tema que exploraremos en la sección de comparación empírica.
📐 Ecuaciones del GRU
Las ecuaciones del GRU son notablemente más compactas que las del LSTM. Mientras que el LSTM requiere cuatro conjuntos de ecuaciones (forget, input, output gates más candidato), el GRU las condensa en tres, y la ecuación final de actualización del estado es una simple interpolación lineal. 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}\) |
COMPARACIÓN DE PARÁMETROS (dh=256, dx=100)
El GRU tiene ~25% menos parámetros que el LSTM → entrenamiento más rápido y menor riesgo de sobreajuste.
📈 ¿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. El mecanismo es análogo a una residual connection adaptativa: en lugar de sumar un residuo fijo, la red aprende cuánto del residuo (el candidato) añadir en cada paso temporal.
Para secuencias largas, si la update gate permanece alta en múltiples pasos, el gradiente se propaga casi sin cambios a lo largo de cientos de timesteps:
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. (Más detalles en Fundamentos de RNN y LSTM en profundidad.)
🎛️ 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. Esto simplifica notablemente
el código: no necesitas desempaquetar tuplas al recibir la salida, la función
forward es más limpia, y la inicialización del estado oculto requiere
una sola variable en vez de dos.
El primer ejemplo muestra un clasificador bidireccional completo usando
nn.GRU de alto nivel. Observa que la única diferencia con un LSTM
equivalente está en la línea de desempaquetado de la salida (línea 25) y en el
constructor (línea 11). Todo lo demás — embeddings, dropout, capa de clasificación —
es idéntico.
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:,}")
El segundo ejemplo muestra nn.GRUCell, la versión paso a paso que te da
control total sobre el bucle temporal. Esto es útil cuando necesitas lógica condicional
dentro del bucle (por ejemplo, teacher forcing, atención, o beam search) o cuando
implementas variantes del GRU como el GRU-D para datos con tiempos
irregulares. Compara con el
equivalente LSTMCell,
donde necesitarías gestionar dos variables de estado en cada paso.
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
En Keras, la transición de LSTM a GRU es igualmente directa: basta con reemplazar
tf.keras.layers.LSTM por tf.keras.layers.GRU. La ventaja
adicional en TensorFlow es la compatibilidad automática con cuDNN: si
usas reset_after=True (el valor por defecto desde TF 2.x) y no activas
recurrent_dropout, Keras delega el cálculo completo a la implementación
optimizada de NVIDIA, consiguiendo speed-ups de 3× a 10×.
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 × dh(dh+dx+1) | 3 × dh(dh+dx+1) | GRU (~25% menos) |
| Velocidad | Referencia | ~15-25% más rápido | GRU |
| Memoria GPU | 2 estados (ht + Ct) | 1 estado (ht) | 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:
EVOLUCIÓN DEL PROCESAMIENTO DE SECUENCIAS
RNN Vanilla
~1986Elman & Jordan introducen la recurrencia: ht = f(ht−1, xt). El concepto fundamental de redes con memoria, pero limitado por el vanishing gradient en secuencias largas.
Recurrencia • Memoria implícita • Vanishing gradientLSTM
1997Hochreiter & Schmidhuber resuelven el vanishing gradient con gates y un cell state separado (Ct). Forget, input y output gates controlan el flujo de información de forma independiente.
+ Gates + Cell State • Memoria a largo plazoGRU
2014Cho et al. simplifican el LSTM: 2 gates (reset + update) en vez de 3, sin cell state separado. Rendimiento comparable con ~25% menos parámetros y entrenamiento más rápido.
+ Simplificación • Eficiencia ≈ rendimientoTransformers
2017Vaswani et al. eliminan la recurrencia por completo. Self-attention permite procesar toda la secuencia en paralelo, escalando a billones de parámetros. Heredan conceptos de gates (skip connections) del LSTM/GRU.
Sin recurrencia • Self-attention paralelo📌 RESUMEN DEL MÓDULO RNN
- RNN Vanilla → concepto de recurrencia, limitado por vanishing gradient
- LSTM → resuelve vanishing gradient con gates y cell state separado
- GRU → simplificación del LSTM, rendimiento similar con menos parámetros
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.
Referencias y lecturas recomendadas
- Cho et al. (2014). Learning Phrase Representations using RNN Encoder–Decoder for Statistical Machine Translation. arXiv:1406.1078 — Paper original del GRU.
- Chung et al. (2014). Empirical Evaluation of Gated Recurrent Neural Networks on Sequence Modeling. arXiv:1412.3555 — Comparación empírica GRU vs LSTM.
- Jozefowicz et al. (2015). An Empirical Exploration of Recurrent Network Architectures. ICML 2015 — Estudio exhaustivo de variantes de RNN.
- Greff et al. (2016). LSTM: A Search Space Odyssey. arXiv:1503.04069 — Análisis de ablación de componentes LSTM/GRU.
- Vaswani et al. (2017). Attention Is All You Need. arXiv:1706.03762 — Los Transformers que sucedieron a LSTM/GRU.
📖 Ver también: Fundamentos de RNN | LSTM en profundidad