¿Qué hace que un dato sea secuencial?
En un dato secuencial, el orden importa: el elemento en la posición t depende (total o parcialmente) de lo que ocurrió antes. Esto aparece en:
- Texto: una frase es una secuencia de tokens; el significado de una palabra depende del contexto previo.
- Series temporales: sensores, finanzas o demanda eléctrica; el valor actual suele depender de valores pasados y estacionalidades.
- Eventos: clics, logs, historial de compras; la secuencia de acciones es informativa.
El reto central es que el modelo debe “recordar” información relevante del pasado para predecir o etiquetar el presente. En redes recurrentes, ese recuerdo se formaliza como un estado oculto.
Idea de estado oculto (hidden state)
En cada paso temporal t, el modelo recibe una entrada x_t y mantiene un vector h_t que resume lo visto hasta ese momento. Operacionalmente, h_t es una memoria comprimida: no guarda todo, sino una representación aprendida útil para la tarea.
Una forma típica de pensarlo es: h_t = f(h_{t-1}, x_t). Si necesitas clasificar una secuencia completa (por ejemplo, sentimiento), el modelo puede producir una salida final y a partir de h_T. Si necesitas etiquetar cada elemento (por ejemplo, POS tagging), produce y_t en cada paso.
RNN básica: cómo funciona y por qué falla en secuencias largas
Definición operacional
Una RNN “clásica” (Elman) actualiza su estado con una transformación afín y una no linealidad:
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
Descargar la aplicación
h_t = tanh(W_x x_t + W_h h_{t-1} + b)y puede producir una salida:
o_t = W_o h_t + cLo importante es que W_h se reutiliza en todos los pasos: el mismo bloque se aplica repetidamente a lo largo del tiempo (parámetros compartidos).
Entrenamiento: Backpropagation Through Time (BPTT)
Para entrenar, se “desenrolla” la RNN en el tiempo: se crea una copia conceptual del bloque por cada t, se calcula la pérdida (en el último paso o en todos) y se retropropaga el gradiente a través de esa cadena temporal. Eso hace que el gradiente hacia pasos tempranos sea un producto repetido de derivadas y matrices.
Limitaciones: gradientes que se desvanecen o explotan (visión práctica)
En la práctica, al retropropagar desde t=T hacia t=1, el gradiente se multiplica muchas veces por términos relacionados con W_h y la derivada de la activación (por ejemplo, tanh'), que suele ser menor que 1 en magnitud. Dos escenarios típicos:
- Gradiente que se desvanece: las multiplicaciones sucesivas reducen el gradiente hasta casi cero. Resultado operativo: el modelo no aprende dependencias lejanas; “olvida” lo que pasó hace muchos pasos.
- Gradiente que explota: si el producto crece, los gradientes se vuelven enormes. Resultado operativo: entrenamiento inestable, pérdidas que se disparan,
NaN, necesidad de recortar gradientes.
Señales comunes en entrenamiento: si al aumentar la longitud de secuencia el rendimiento cae drásticamente o el modelo solo usa información reciente, suele haber desvanecimiento; si aparecen picos en la norma del gradiente o inestabilidad, suele haber explosión.
LSTM y GRU: compuertas para conservar información
La idea clave es introducir mecanismos de compuertas (gates) que controlan qué información se guarda, qué se olvida y qué se expone como estado. Esto crea rutas de gradiente más estables y permite memorias más largas.
LSTM (Long Short-Term Memory)
LSTM mantiene dos estados: estado oculto h_t (lo que se “expone”) y estado de celda c_t (memoria interna). Usa compuertas sigmoides (valores entre 0 y 1) para controlar el flujo:
- Compuerta de olvido
f_t: cuánto dec_{t-1}se conserva. - Compuerta de entrada
i_ty candidatog_t: qué nueva información se escribe en la celda. - Compuerta de salida
o_t: qué parte de la celda se proyecta ah_t.
f_t = sigmoid(W_f x_t + U_f h_{t-1} + b_f) # olvidar/retener memoria previa (0..1)
i_t = sigmoid(W_i x_t + U_i h_{t-1} + b_i) # cuánto escribir
g_t = tanh( W_g x_t + U_g h_{t-1} + b_g) # contenido candidato
c_t = f_t * c_{t-1} + i_t * g_t # memoria acumulada
o_t = sigmoid(W_o x_t + U_o h_{t-1} + b_o) # cuánto exponer
h_t = o_t * tanh(c_t)Interpretación práctica: si f_t se acerca a 1 e i_t a 0 durante varios pasos, la celda mantiene información casi intacta, facilitando dependencias largas.
GRU (Gated Recurrent Unit)
GRU simplifica LSTM: combina memoria y estado en un solo vector h_t y usa dos compuertas principales:
- Update gate
z_t: cuánto mantener del estado anterior. - Reset gate
r_t: cuánto “resetear” al calcular el candidato.
z_t = sigmoid(W_z x_t + U_z h_{t-1} + b_z)
r_t = sigmoid(W_r x_t + U_r h_{t-1} + b_r)
h~_t = tanh(W_h x_t + U_h (r_t * h_{t-1}) + b_h)
h_t = (1 - z_t) * h_{t-1} + z_t * h~_tEn la práctica, GRU suele entrenar bien con menos parámetros y puede ser una buena primera opción si buscas eficiencia. LSTM puede ser preferible cuando necesitas una memoria más explícita (por su celda c_t), aunque depende del problema.
Esquemas de entrenamiento para tareas con secuencias
Many-to-one: clasificación de secuencia
Objetivo: una etiqueta por secuencia completa. Ejemplos: sentimiento de una reseña, detección de anomalía en una ventana temporal, clasificación de intención en una frase.
Patrón típico:
- Entrada:
x_1, x_2, ..., x_T - Modelo recurrente produce estados
h_1...h_T - Se toma
h_T(o un pooling sobre todos losh_t) y se pasa a una capa de clasificación.
# many-to-one (ejemplo conceptual)
for t in 1..T:
h_t = RNN(x_t, h_{t-1})
y_hat = softmax(W * h_T + b)Consejo operativo: si la señal relevante puede estar en cualquier parte de la secuencia, a veces funciona mejor usar un resumen como mean/max pooling sobre h_t en lugar de solo h_T, especialmente si T es grande.
Many-to-many: etiquetado por paso (sequence labeling)
Objetivo: una etiqueta por elemento. Ejemplos: etiquetado gramatical (POS), detección de entidades (NER), clasificación de cada instante en una serie temporal (normal/anómalo por tiempo).
Patrón típico:
- Entrada:
x_1...x_T - Salida:
y_1...y_T - Se aplica una capa de salida en cada
h_t.
# many-to-many (etiquetado)
for t in 1..T:
h_t = RNN(x_t, h_{t-1})
y_hat_t = softmax(W * h_t + b)En tareas de texto, es común usar variantes bidireccionales (procesar de izquierda a derecha y de derecha a izquierda) para que cada y_t use contexto pasado y futuro, pero incluso en ese caso siguen aplicando las mismas ideas de padding, máscaras y BPTT truncado.
Estrategias prácticas imprescindibles: padding, máscaras y BPTT truncado
1) Padding: hacer lotes (batches) con longitudes distintas
En la práctica, las secuencias tienen longitudes variables. Para entrenar eficientemente en GPU, se agrupan en lotes con una longitud común T_max y se rellena con un token/valor especial (PAD) hasta igualar longitudes.
Ejemplo (texto) con IDs:
| Secuencia | Original | Con padding a T_max=6 |
|---|---|---|
| A | [5, 18, 9] | [5, 18, 9, 0, 0, 0] |
| B | [4, 7, 2, 11, 3] | [4, 7, 2, 11, 3, 0] |
En series temporales, el padding puede ser ceros u otro valor; lo importante es que el modelo no “aprenda” del relleno como si fuera señal real.
2) Máscaras: evitar que el padding afecte la pérdida y (a veces) el estado
El padding debe ignorarse al calcular la pérdida. Para eso se usa una máscara m_t que vale 1 en posiciones reales y 0 en padding.
En many-to-many, la pérdida total suele ser una suma/medio ponderado por máscara:
# loss por paso (ejemplo)
loss = sum_t ( m_t * CE(y_t, y_hat_t) ) / sum_t m_tAdemás, en algunas implementaciones conviene enmascarar la actualización del estado para que, cuando m_t=0, el estado no cambie:
# actualización enmascarada (conceptual)
h_t = m_t * RNN(x_t, h_{t-1}) + (1 - m_t) * h_{t-1}Esto es especialmente útil si procesas lotes con padding al final y quieres evitar que el modelo “evolucione” sobre tokens inexistentes.
3) Truncated BPTT: entrenar en secuencias largas sin retropropagar hasta el inicio
En secuencias muy largas (por ejemplo, miles de pasos en series temporales), desenrollar toda la secuencia es costoso en memoria y puede empeorar la estabilidad. Truncated BPTT consiste en:
- Procesar la secuencia por fragmentos de longitud
K(por ejemplo, 50 o 100 pasos). - Retropropagar solo dentro de cada fragmento.
- Pasar el estado final del fragmento como estado inicial del siguiente, pero deteniendo el gradiente entre fragmentos (no se retropropaga a través del límite).
Guía paso a paso (operacional):
- Paso 1: elige
K(tamaño de truncamiento) según memoria y dependencia temporal esperada. - Paso 2: inicializa
h_0(cero o aprendido) para cada secuencia del lote. - Paso 3: para el fragmento 1, procesa
x_1...x_K, calcula la pérdida del fragmento y actualiza parámetros. - Paso 4: toma el estado
h_Kcomo inicio del siguiente fragmento, pero aplica “detach/stop_gradient” para que el grafo no crezca. - Paso 5: repite con
x_{K+1}...x_{2K}, etc., hasta cubrir la secuencia.
# esquema conceptual de truncated BPTT
h = h0
for chunk in chunks(x, K):
h = stop_gradient(h)
for x_t in chunk:
h = RNN(x_t, h)
... acumular pérdida ...
backprop_y_update()Trade-off: reduces coste y estabilizas entrenamiento, pero limitas cuánto puede “aprender” el gradiente sobre dependencias más allá de K. Aun así, el estado puede transportar información hacia adelante; lo que se recorta es la señal de aprendizaje a través de muchos pasos.
Checklist práctico para implementar un modelo recurrente en un proyecto real
- Define el formato de entrada: tokens (texto) o vectores numéricos por paso (series temporales). Asegura normalización/estandarización en series.
- Elige arquitectura: empieza con GRU si buscas simplicidad; usa LSTM si sospechas dependencias largas y quieres más control.
- Selecciona esquema de salida: many-to-one para etiqueta global; many-to-many para etiqueta por paso.
- Gestiona longitudes variables: aplica padding y usa máscaras en la pérdida (y opcionalmente en el estado).
- Si la secuencia es larga: usa truncated BPTT con un
Krazonable. - Estabilidad: si observas explosión de gradientes, aplica recorte (gradient clipping) y revisa la escala de entradas; si hay olvido, considera LSTM/GRU, ajustar
Ko usar representaciones más informativas.