Recuperación de series temporales: cómo mirar hacia atrás mejora los pronósticos

Ayuda en la previsión de series temporales

Todos sabemos cómo son las cosas: los datos de series temporales son complicados.

Los modelos de pronóstico tradicionales no están preparados para incidentes como caídas repentinas del mercado, eventos de cisne negro o patrones climáticos raros.

Incluso los modelos grandes y elegantes como Chronos a veces tienen dificultades porque nunca antes se habían ocupado de ese tipo de patrón.

Podemos mitigar esto con la recuperación. Con la recuperación, podemos preguntar: ¿Ha sucedido algo como esto antes? y luego usar ese ejemplo pasado para guiar el pronóstico.

Como todos sabemos ahora, en el procesamiento del lenguaje natural (PNL), esta idea se llama generación aumentada de recuperación (RAG). También se está volviendo popular en el mundo del pronóstico de series temporales.

Luego, el modelo considera situaciones pasadas que se parecen a la actual y, a partir de ahí, puede hacer predicciones más confiables.

¿En qué se diferencia este RAF de las series temporales tradicionales? El pronóstico de recuperación agrega un paso explícito de acceso a la memoria.

En lugar de:

Pasado -> parámetros -> pronóstico

Con recuperación tenemos:

Situación actual -> búsqueda de similitudes -> episodios pasados ​​concretos-> previsión

Ciclo de pronóstico aumentado de recuperación. Imagen del autor | Servilleta AI.

En lugar de simplemente utilizar lo que el modelo aprendió durante el entrenamiento, la idea es darle acceso a una variedad de situaciones similares.

Es como dejar que un modelo meteorológico compruebe: "¿Cómo eran antes inviernos como este?".

Hola, soy Sara Nóbrega, ingeniera en IA. Si está trabajando en problemas similares o desea comentarios sobre cómo aplicar estas ideas, recopilo mis escritos, recursos y enlaces de tutoría aquí.

En este artículo, exploro el pronóstico de recuperación aumentada desde los primeros principios y muestro, con ejemplos concretos y ejemplos de código, cómo se puede utilizar la recuperación en procesos de pronóstico reales.

¿Qué es el pronóstico de recuperación aumentada (RAF)?

¿Qué es la RAF? Desde una vista de muy alto nivel, en lugar de basarse únicamente en lo que un modelo aprendió en el entrenamiento, RAF permite que el modelo busque activamente situaciones pasadas concretas similares a la actual y utilice sus resultados para guiar su predicción.

Veámoslo más en detalle:

Convierte la situación actual (por ejemplo, las últimas semanas de un conjunto de datos bursátiles de series temporales) en una consulta. Luego, esta consulta se utiliza para buscar en una base de datos de segmentos de series temporales históricas para encontrar los patrones más similares. No es necesario que estas cerillas provengan del mismo stock; El sistema también debería mostrar movimientos similares de otras acciones o productos financieros.

Recupera esos patrones y lo que sucedió después.

Posteriormente, esta información se incorpora al modelo de pronóstico para ayudarlo a realizar mejores predicciones.

Esta técnica es poderosa en:

Escenarios de disparo cero: cuando el modelo se enfrenta a algo en lo que no fue entrenado. Eventos raros o anómalos: como COVID, crisis financieras repentinas, etc. Tendencias estacionales en evolución: donde los datos pasados ​​contienen patrones útiles, pero cambian con el tiempo.

RAF no reemplaza su modelo de pronóstico, sino que lo aumenta brindándole sugerencias adicionales y basándolo en ejemplos históricos relevantes.

Otro ejemplo: digamos que desea pronosticar el consumo de energía durante una semana inusualmente calurosa.

En lugar de esperar que su modelo recuerde cómo las olas de calor afectan el uso, la recuperación encuentra olas de calor pasadas similares y permite que el modelo considere lo que sucedió en ese momento.

¿Qué recuperan realmente estos modelos?

El “conocimiento” recuperado no son sólo datos sin procesar. Es el contexto el que da pistas al modelo.

A continuación se muestran algunos ejemplos comunes:

Ejemplos de recuperación de datos. Imagen del autor | Servilleta AI.
Ejemplos de recuperación de datos. Imagen del autor | Servilleta AI.

Como puede ver, la recuperación se centra en situaciones históricas significativas, como shocks raros, efectos estacionales y patrones que tienen estructuras similares. Estos brindan un contexto procesable para el pronóstico actual.

¿Cómo se recuperan estos modelos?

Para encontrar patrones relevantes del pasado, estos modelos utilizan mecanismos estructurados que representan la situación actual de una manera que facilita la búsqueda en grandes bases de datos y encontrar las coincidencias más cercanas.

Los fragmentos de código de esta sección son una ilustración simplificada destinada a generar intuición; no representan código de producción.

Métodos de recuperación para la previsión de series temporales. Imagen del autor | Servilleta AI.
Métodos de recuperación para la previsión de series temporales. Imagen del autor | Servilleta AI.

Algunos de estos métodos son:

Similitud basada en incrustación

Este convierte series de tiempo (o parches/ventanas de una serie) en vectores compactos y luego los compara con métricas de distancia como la similitud euclidiana o coseno.

En palabras simples: el modelo convierte fragmentos de datos de series temporales en breves resúmenes y luego verifica qué resúmenes pasados ​​se parecen más a lo que está sucediendo ahora.

Algunos pronosticadores de recuperación aumentada (por ejemplo, RAFT) recuperan los parches históricos más similares de los datos de entrenamiento/series completas y luego agregan los valores recuperados con ponderaciones similares a las de atención.

En palabras simples: encuentra situaciones similares del pasado y las promedia, prestando más atención a las mejores coincidencias.

importar numpy como np # Ejemplo: recuperación basada en incrustaciones para parches de series temporales # Este es un ejemplo de juguete para mostrar la *idea* detrás de la recuperación. # En la práctica: # – las incrustaciones se aprenden mediante redes neuronales # – la búsqueda de similitud se ejecuta en millones de vectores # – esta lógica vive dentro de un proceso de pronóstico más grande def embed_patch(patch: np.ndarray) -> np.ndarray: """ Convierte una ventana de serie temporal corta ("parche") en un vector compacto. Aquí utilizamos estadísticas simples (media, estándar, mínimo, máximo) únicamente a modo ilustrativo. Los sistemas del mundo real pueden usar: – una red de codificadores entrenada – representaciones basadas en formas – características de dominio de frecuencia – vectores latentes de una columna vertebral de pronóstico """ return np.array([ patch.mean(), # nivel promedio patch.std(), # volatilidad patch.min(), # punto más bajo patch.max() # punto más alto ]) def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float: """ Mide qué tan similares son dos vectores con focos de similitud. en *dirección* en lugar de magnitud, lo cual suele ser útil para comparar patrones o formas """ return float(a @ b) / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-9) # Paso 1: Representar la situación actual # Una ventana breve que representa el comportamiento actual de la serie temporal query_patch = np.array([10, 12, 18, 25, 14, 11]) # Convertirlo en una incrustación query_embedding = embed_patch(query_patch) # Paso 2: Representar situaciones históricas # Ventanas pasadas extraídas de datos históricos historic_patches = [ np.array([9, 11, 17, 24, 13, 10]), # se parece a np.array([2, 2, 2, 2, 2, 2]), # plano, no relacionado np.array([10, 13, 19, 26, 15, 12]) # muy similar ] # Convertir todos los parches históricos en incrustaciones historic_embeddings = [ embed_patch(patch) for patch in historic_patches ] # Paso 3: comparar y recuperar los casos pasados más similares # Calcular puntuaciones de similitud entre la situación actual # y cada ejemplo histórico similitudes = [ cosine_similarity(query_embedding, hist_emb) for hist_emb in historic_embeddings ] # Clasificar los parches históricos por similitud top_k_indices = np.argsort(similitudes)[::-1][:2] print("Parches históricos más similares:", top_k_indices) # Paso 4 (conceptual): # En un pronosticador de recuperación aumentada, el modelo ahora: # – recuperar los *resultados futuros* de estos parches similares # – ponderarlos por similitud (ponderación similar a la atención) # – utilizarlos para guiar el pronóstico final # Este paso de integración es específico del modelo y no se muestra aquí.

Herramientas y bibliotecas de recuperación

1. FAISS
FAISS es una biblioteca súper rápida y compatible con GPU para búsqueda de similitudes en vectores densos. Los mejores conjuntos de datos para esta biblioteca son los que son grandes y están en memoria, aunque su estructura hace que las actualizaciones en tiempo real sean más difíciles de implementar.

import faiss import numpy as np # Supongamos que ya tenemos incrustaciones para ventanas históricas d = 128 # dimensión de incrustación xb = np.random.randn(100_000, d).astype("float32") # incrustaciones históricas xq = np.random.randn(1, d).astype("float32") # índice de incrustación de consulta = faiss.IndexFlatIP(d) # producto interno (a menudo usado con vectores normalizados para un comportamiento similar al coseno) index.add(xb) k = 5 puntuaciones, ids = index.search(xq, k) print("Vecinos más cercanos (ids):", ids) print("Puntuaciones de similitud:", puntuaciones) # Algunos índices/algoritmos FAISS se pueden ejecutar en GPU.

Búsqueda de vecino más cercano (Molesto)
La biblioteca Annoy es relativamente liviana y fácil de trabajar.

Los mejores conjuntos de datos para esta biblioteca son los conjuntos de datos históricos que permanecen en su mayoría estáticos, ya que cualquier modificación del conjunto de datos requiere reconstruir el índice.

de molestar importar AnnoyIndex importar numpy como np # Número de valores en cada vector de incrustación. # La "longitud" de cada huella digital. f = 64 # Crea un índice de molestia. # Este objeto almacenará muchas incrustaciones pasadas y nos ayudará a encontrar rápidamente las más similares. ann = AnnoyIndex(f, "angular") # la distancia "angular" se usa comúnmente para comparar patrones # y se comporta de manera similar a la similitud del coseno. # Agregar incrustaciones históricas (situaciones pasadas). # Cada elemento representa una versión comprimida de una ventana de serie temporal pasada. # Aquí usamos números aleatorios solo como ejemplo. para i in range(10000): ann.add_item(i, np.random.randn(f).tolist()) # Construya la estructura de búsqueda. # Este paso organiza los datos para que las búsquedas de similitudes sean rápidas. # Después de esto, el índice pasa a ser de solo lectura. ann.build(10) # Guarda el índice en el disco. # Esto nos permite cargarlo más tarde sin reconstruir todo. ann.save("hist.ann") # Crea una incrustación de consulta. # Esto representa la situación actual que queremos comparar # con situaciones pasadas. q = np.random.randn(f).tolist() # Encuentra las 5 incrustaciones pasadas más similares. # Annoy devuelve los ID de las coincidencias más cercanas. vecinos = ann.get_nns_by_vector(q, 5) print("Vecinos más cercanos:", vecinos) # Nota importante: # Una vez creado el índice, no se pueden agregar nuevos elementos. # Si aparecen nuevos datos históricos, se debe reconstruir el índice.

Qdrant / Piña

Qdrant y Pinecone son como Google en cuanto a incrustaciones.

Almacenas muchas “huellas digitales” vectoriales (además de etiquetas adicionales como ciudad/temporada) y cuando tienes una nueva huella digital, preguntas:

Muéstrame los más similares pero solo de esta ciudad/temporada/tipo de tienda”.
Esto es lo que los hace más fáciles que realizar su propia recuperación: ¡manejan búsquedas y filtrado rápidos!

Qdrant llama a la carga útil de metadatos y puede filtrar los resultados de la búsqueda mediante condiciones.

# Sólo ejemplo (por intuición). El código real necesita una instancia de Qdrant en ejecución + incrustaciones reales. from qdrant_client import QdrantClient, models client = QdrantClient(url="http://localhost:6333") collection = "time_series_windows" # Supongamos que esta es la incrustación de la ventana de serie temporal *actual* query_vector = [0.12, -0.03, 0.98, 0.44] # abreviado para facilitar la lectura # Filter = "considerar solo ventanas pasadas de Nueva York en verano" # La documentación de Qdrant muestra filtros creados a partir de FieldCondition + MatchValue. :contentReference[oaicite:3]{index=3} query_filter = models.Filter( must=[ models.FieldCondition( key="city", match=models.MatchValue(value="New York"), ), models.FieldCondition( key="season", match=models.MatchValue(value="summer"), ), ] ) # En el uso real, llamarías a búsqueda/consulta y obtendrías las coincidencias más cercanas # más sus carga útil (metadatos) si lo solicita. resultados = client.search( collection_name=collection, query_vector=query_vector, query_filter=query_filter, limit=5, with_payload=True, # devolver metadatos para que puedas inspeccionar lo que recuperaste) print(resultados) # Lo que harías a continuación (conceptualmente): # – tomar los ID coincidentes # – cargar las ventanas históricas reales detrás de ellas # – introducir esas ventanas (o sus resultados) en tu modelo de pronóstico

Pinecone almacena pares clave-valor de metadatos junto con los vectores y le permite filtrar en el momento de la consulta (incluido $eq) y devolver metadatos.

# Sólo ejemplo (por intuición). El código real necesita una clave API + un host de índice. from pinecone import Pinecone pc = Pinecone(api_key="YOUR_API_KEY") index = pc.Index(host="INDEX_HOST") # Imagina que esta es la incrustación de la ventana de serie temporal actual query_vector = [0.12, -0.03, 0.98, 0.44] # abreviada para facilitar la lectura # Pregunta por las ventanas pasadas más similares, pero solo donde: # ciudad == "Nueva York" Y temporada == "verano" # Los documentos de Pinecone muestran el filtrado en tiempo de consulta y `$eq`. :contentReference[oaicite:5]{index=5} res = index.query( namespace="windows", vector=query_vector, top_k=5, filter={ "city": {"$eq": "New York"}, "season": {"$eq": "summer"}, }, include_metadata=True, # devuelve etiquetas para que puedas verificar las coincidencias include_values=False ) print(res) # Conceptualmente a continuación: # – use los ID devueltos para obtener las ventanas/resultados históricos subyacentes # – condicione su pronóstico en esos ejemplos recuperados

¿Por qué ayudan las bases de datos vectoriales? Le permiten realizar búsquedas de similitud + “filtros WHERE similares a SQL” en un solo paso, lo cual es difícil de hacer de manera limpia con una configuración de bricolaje (tanto el filtrado de carga útil de Qdrant como el filtrado de metadatos de Pinecone son características de primera clase en sus documentos).

Cada herramienta tiene sus ventajas y desventajas. Por ejemplo, FAISS es excelente para el rendimiento, pero no es adecuado para actualizaciones frecuentes. Qdrant ofrece flexibilidad y filtrado en tiempo real. Pinecone es fácil de configurar pero solo SaaS.

Recuperación + Previsión: cómo combinarlos

Después de saber qué recuperar, el siguiente paso es combinar esa información con la entrada actual.

Puede variar según la arquitectura y la tarea. Existen varias estrategias para hacer esto (ver imagen a continuación).

Estrategias para combinar recuperación y previsión
Estrategias para combinar recuperación y previsión. Imagen del autor | Servilleta AI.

A. Concatenación
Idea: trate el contexto recuperado como “más entrada” agregándolo a la secuencia existente (muy común en configuraciones de generación de recuperación aumentada).

Funciona bien con modelos basados ​​en transformadores como Chronos y no requiere cambios de arquitectura.

importar antorcha # x_current: la secuencia de entrada habitual del modelo (por ejemplo, últimos N pasos de tiempo o tokens) # forma: [lote, tiempo, d_model] (o [lote, tiempo] si piensas en tokens) x_current = torch.randn(8, 128, 256) # x_retrived: contexto recuperado codificado en el MISMO espacio de representación # por ejemplo, incrustaciones para ventanas pasadas similares (o sus resúmenes) # forma: [batch, retrieved_time, d_model] x_retrieved = torch.randn(8, 32, 256) # Fusión simple: simplemente agrega el contexto recuperado al final de la secuencia de entrada # Ahora el modelo ve: [historial actual… + contexto recuperado…] x_fused = torch.cat([x_current, x_retrieved], dim=1) # En la práctica, también agregarías: # – una máscara de atención (para que el modelo sepa qué es real vs acolchado) # – incrustaciones de segmento/tipo (para que el modelo sepa qué parte es el contexto recuperado) # Luego alimenta x_fused a tu transformador.

B. Fusión de atención cruzada
Idea: mantener separados la "entrada actual" y el "contexto recuperado" y dejar que el modelo atienda el contexto recuperado cuando lo necesite. Este es el patrón central de “fusión en el decodificador mediante atención cruzada” utilizado por arquitecturas de recuperación aumentada como FiD.

import torch # current_repr: representación de la ventana de serie temporal actual # shape: [batch, time, d_model] current_repr = torch.randn(8, 128, 256) # retrieved_repr: representación de las ventanas recuperadas (se pueden concatenar) # shape: [batch, retrieved_time, d_model] retrieved_repr = torch.randn(8, 64, 256) # Piense en la atención cruzada como: # – La consulta (Q) proviene de la secuencia actual # – Las claves/valores (K/V) provienen del contexto recuperado Q = current_repr K = retrieved_repr V = retrieved_repr # Puntuaciones de atención: "¿Cuánto debe mirar cada paso de tiempo actual en cada paso de tiempo recuperado?" puntuaciones = torch.matmul(Q, K.transpose(-1, -2)) / (Q.size(-1) ** 0.5) # Convertir puntuaciones en pesos (para que sumen 1 en las posiciones recuperadas) pesos = torch.softmax(scores, dim=-1) # Suma ponderada de la información recuperada (esta es la señal recuperada "fusionada") retrieval_signal = torch.matmul(weights, V) # Representación fusionada final: información actual + información recuperada # (Algunos modelos agregan, algunos concatenan, algunos usan una proyección aprendida) fusionado = current_repr + recuperación_señal # Luego, el cabezal de pronóstico lee desde `fusionado` para predecir el futuro.

C. Mezcla de Expertos (MoE)
Idea: combinar dos “expertos”:

el pronosticador basado en recuperación (no paramétrico, basado en casos) el pronosticador base (conocimiento paramétrico)

Una "puerta" decide en cuál confiar más en cada paso del tiempo.

import torch # base_pred: pronóstico del modelo principal (lo que "aprendió en pesos") # forma: [lote, horizonte] base_pred = torch.randn(8, 24) # retrieval_pred: pronóstico sugerido por casos similares recuperados # forma: [lote, horizonte] retrieval_pred = torch.randn(8, 24) # context_for_gate: resumen de la situación actual (podría ser el último estado oculto) # forma: [batch, d_model] context_for_gate = torch.randn(8, 256) # gate: un número entre 0 y 1 que dice "cuánto confiar en la recuperación" # (En modelos reales, esta es una red neuronal diminuta.) gate = torch.sigmoid(torch.randn(8, 1)) # Mezcla: combinación convexa # – si puerta ~ 1 -> confía en la recuperación más # – si puerta ~ 0 -> confianza el modelo base más final_pred = gate * retrieval_pred + (1 – gate) * base_pred # En la práctica: # – la puerta puede depender del paso del tiempo: forma [lote, horizonte, 1] # – también puede agregar pérdidas de entrenamiento para estabilizar el enrutamiento/uso (común en MoE)

D. Solicitud de canal
Idea: tratar las series recuperadas como canales/características de entrada adicionales (especialmente natural en series temporales multivariadas, donde cada variable es un "canal").

importar antorcha # x: entrada de serie temporal multivariada # forma: [lote, tiempo, canales] # Ejemplo: los canales podrían ser [ventas, precio, bandera_promoción, temperatura, …] x = torch.randn(8, 128, 5) # series_recuperadas_alineadas: señal recuperada alineada a la misma cuadrícula de tiempo # Ejemplo: promedio de las k ventanas pasadas similares (o un vecino representativo) # forma: [lote, tiempo, canales_recuperados] retrieved_series_aligned = torch.randn(8, 128, 2) # Solicitud de canal = agregar canales recuperados como características adicionales # Ahora el modelo obtiene "canales normales + canales recuperados" x_prompted = torch.cat([x, retrieved_series_aligned], dim=-1) # En la práctica, probablemente también incluirías: # – una máscara o puntaje de confianza para los canales recuperados # – normalización para que las señales recuperadas estén en una escala comparable # Luego alimenta x_prompted en el pronosticador.

Algunos modelos incluso combinan varios métodos.

Un enfoque común es recuperar varias series similares, fusionarlas utilizando la atención para que el modelo pueda centrarse en las partes más relevantes y luego enviárselas a un experto.

Resumen

El pronóstico de recuperación aumentada (RAF) permite que su modelo aprenda del pasado de una manera que el modelado de series de tiempo tradicional no logra.

Actúa como una memoria externa que ayuda al modelo a navegar situaciones desconocidas con más confianza.

Es sencillo experimentar con él y ofrece mejoras significativas en las tareas de previsión.

La recuperación ya no es una moda académica; ya está dando resultados en sistemas del mundo real.

¡Gracias por leer!

Mi nombre es Sara Nóbrega. Soy un ingeniero de inteligencia artificial enfocado en MLOps y en la implementación de sistemas de aprendizaje automático en producción.

Referencias

[1]J. Liu, Y. Zhang, Z. Wang et al., Pronóstico de series temporales aumentadas de recuperación (2025), preimpresión de arXiv
Fuente: https://arxiv.org/html/2505.04163v1

[2]UConn DSIS, TS-RAG: generación aumentada de recuperación de series temporales (nd), repositorio de GitHub
Fuente: https://github.com/UConn-DSIS/TS-RAG

[3]Y. Zhang, H. Xu, X. Chen et al., Pronóstico con memoria aumentada para series temporales con eventos raros (2024), preimpresión de arXiv
Fuente: https://arxiv.org/abs/2412.20810