📖 Teoría

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:

1997 LSTM 2014 GRU Cho, van Merriënboer, Gulcehre, Bahdanau, et al. 2014 Atención (Bahdanau et al.) 2016 LSTM Odyssey (Greff et al.)

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 = 0 → ignorar h_{t-1} en el candidato
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 ≈ 1 → mantener h_{t-1} (no actualizar)
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:

CELDA GRU — FLUJO DE DATOS h_{t-1} h_t σ reset gate r_t σ update gate z_t tanh candidato h̃_t × r_t ⊙ h_{t-1} × z_t ⊙ h_{t-1} × (1-z_t) ⊙ h̃_t + h_{t-1} , x_t [h_{t-1}, x_t] → entrada a ambas gates y al candidato (vía r_t ⊙ h_{t-1}) Reset gate Update gate Candidato h̃_t × multiplicación + interpolación final

Paso a paso: flujo de datos en una celda GRU

1
Reset Gate: Calcula \(r_t = \sigma(W_r \cdot [h_{t-1}, x_t] + b_r)\). Decide cuánto de \(h_{t-1}\) considerar al generar el candidato.
2
Update Gate: Calcula \(z_t = \sigma(W_z \cdot [h_{t-1}, x_t] + b_z)\). Determina el balance entre mantener y actualizar.
3
Candidato: Calcula \(\tilde{h}_t = \tanh(W_h \cdot [r_t \odot h_{t-1}, x_t] + b_h)\). Propone un nuevo estado usando el pasado filtrado por la reset gate.
4
Interpolación: \(h_t = z_t \odot h_{t-1} + (1 - z_t) \odot \tilde{h}_t\). Mezcla suavemente el estado anterior con el candidato nuevo.
💡

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

COMPARACIÓN ESTRUCTURAL LSTM forget σ input σ output σ cand tanh Cell state C_t Hidden h_t 3 gates + 1 candidato 2 estados (C_t + h_t) 4 × d_h(d_h+d_x+1) params C_t: camino lineal separado forget e input independientes GRU reset σ update σ cand tanh Hidden h_t 2 gates + 1 candidato 1 estado (solo h_t) 3 × d_h(d_h+d_x+1) params h_t: interpolación directa update = coupled forget+input

📐 Ecuaciones del GRU

Las ecuaciones del GRU son más compactas que las del LSTM. En cada paso temporal:

1. Reset Gate $$r_t = \sigma\!\bigl(W_r \cdot [h_{t-1}, x_t] + b_r\bigr)$$
2. Update Gate $$z_t = \sigma\!\bigl(W_z \cdot [h_{t-1}, x_t] + b_z\bigr)$$
3. Candidate Hidden State $$\tilde{h}_t = \tanh\!\bigl(W_h \cdot [r_t \odot h_{t-1}, x_t] + b_h\bigr)$$
4. Hidden State (interpolación) $$h_t = z_t \odot h_{t-1} + (1 - z_t) \odot \tilde{h}_t$$
🔑

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}\)
Parámetros del GRU $$\text{Params}_{\text{GRU}} = 3 \cdot d_h \cdot (d_h + d_x + 1)$$
COMPARACIÓN DE PARÁMETROS (d_h=256, d_x=100) RNN vanilla 91.9K (1×) GRU 275.7K (3×) LSTM 367.6K (4×) GRU tiene ~25% menos parámetros que LSTM → entrenamiento más rápido

📈 ¿Por qué el GRU también resuelve el vanishing gradient?

El gradiente del hidden state a través del tiempo:

Gradiente del GRU $$\frac{\partial h_t}{\partial h_{t-1}} = \text{diag}(z_t) + \text{términos de segundo orden}$$

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:

Gradiente a largo plazo $$\frac{\partial h_T}{\partial h_k} \approx \prod_{t=k+1}^{T} \text{diag}(z_t) \approx I \quad \text{si } z_t \approx 1$$
🎯

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:

0.50
0.70
0.60
-0.30

🔥 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:

30
0.90
0.90

🌍 Aplicaciones destacadas del GRU

🤖
Chatbots y diálogo
Modelos de conversación con GRU encoder-decoder. Rápidos de entrenar, ideales para producción ligera.
pregunta respuesta
📊
Detección de anomalías
En series temporales de sensores IoT, logs de servidores, tráfico de red. La eficiencia del GRU permite despliegue en edge.
serie temporal normal/anomalía
🎮
Aprendizaje por refuerzo
En entornos parcialmente observables, GRU como memoria del agente. Ligero y eficiente para RL online.
observaciones acción
🩺
Registros clínicos
Predicción de riesgo a partir de secuencias de visitas médicas. GRU-D maneja datos con tiempos irregulares.
historial riesgo
📱
Modelos en dispositivo
Teclados predictivos, reconocimiento de gestos, modelos on-device donde cada KB de RAM cuenta.
secuencia predicción
🧬
Secuencias biológicas
Análisis de ADN, predicción de estructura secundaria de proteínas. GRU bidireccional para contexto completo.
secuencia bio estructura

🔮 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 ~1986 Elman, Jordan LSTM 1997 Hochreiter & Schmidhuber GRU 2014 Cho et al. Transformers 2017 Vaswani et al. "Attention Is All You Need" Recurrencia h_t = f(h_{t-1}, x_t) + Gates + Cell State Memoria a largo plazo + Simplificación Eficiencia ≈ rendimiento 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:

  1. Fundamentos: Datos secuenciales, recurrencia, hidden state, BPTT, vanishing/exploding gradients.
  2. LSTM: Cell state, forget/input/output gates, cómo resuelve el vanishing gradient, la «neurona sintiente».
  3. 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.

🏭 Casos de uso