Deep Learning para la predicción de series temporales: Redes Neuronales Recurrentes (RNN) y Long Short-Term Memory (LSTM)

Si te gusta  Skforecast , ayúdanos dándonos una estrella en  GitHub! ⭐️

Deep Learning para la predicción de series temporales: Redes Neuronales Recurrentes (RNN) y Long Short-Term Memory (LSTM)

Fernando Carazo Melo, Joaquín Amat Rodrigo
Enero, 2024 (última actualización Agosto 2024)

Introdución

El Deep Learning es un campo de la inteligencia artificial enfocado en crear modelos basados en redes neuronales que permiten aprender representaciones no lineales de manera jerárquica. Las redes neuronales recurrentes (RNN) son un tipo de arquitectura de deep learning diseñada para trabajar con datos secuenciales, donde la información se propaga a través de conexiones recurrentes, permitiendo a la red aprender dependencias temporales.

Este artículo describe cómo entrenar modelos de redes neuronales recurrentes -específicamente RNN y LSTM- para la predicción de series temporales (forecasting) empleando Python, Keras y Skforecast.

  • Keras3 proporciona una interfaz sencilla para construir y entrenar modelos de redes neuronales. Gracias a su API de alto nivel, los desarrolladores pueden implementar fácilmente arquitecturas LSTM, aprovechando las ventajas de la eficiencia computacional y la escalabilidad que ofrece el deep learning.

  • Skforecast permite generalizar de forma sencilla la implementación y uso de modelos de machine learning -entre ellos LSTMs y RNNs- a problemas de forecasting. De esta forma, el usuario puede definir el problema y abstraerse de la arquitectura. Para usuarios avanzados, skforecast también permite ejecutar una arquitectura de deep learning previamente definida.

✎ Nota

Para comprender plenamente este artículo, se presupone cierto conocimiento sobre redes neuronales y deep learning. No obstante, si este no es el caso, y mientras trabajamos en la creación de nuevo material, te proporcionamos algunos enlaces de referencia para comenzar:

Redes Neuronales Recurrentes (RNN)

Las Redes Neuronales Recurrentes (RNN) son un tipo de redes neuronales diseñadas para procesar datos que siguen un orden secuencial. En las redes neuronales convencionales, como las redes feedforward, la información fluye en una dirección, desde la entrada hasta la salida pasando por las capas ocultas, sin considerar la estructura secuencial de los datos. En cambio, las RNN mantienen estados internos o memorias, lo que les permite recordar información pasada y utilizarla para predecir datos futuros en la secuencia.

La unidad básica de una RNN es la célula recurrente. Esta célula toma dos entradas: la entrada actual y el estado oculto previo. El estado oculto puede entenderse como una "memoria" que retiene información de las iteraciones previas. La entrada actual y el estado oculto anterior se combinan para calcular la salida actual y el nuevo estado oculto. Esta salida se utiliza como entrada para la próxima iteración, junto con la siguiente entrada en la secuencia de datos.

A pesar de los avances que se han conseguido con las arquitecturas RNN, tienen limitaciones para capturar patrones a largo plazo. Es por esto que se han desarrollado variantes como las LSTM (Memorias a Corto y Largo Plazo) y las GRU (Unidades Recurrentes Gated), que abordan estos problemas y permiten retener información a largo plazo de manera más efectiva.

Diagrama de una red RNN simple. Fuente: James, G., Witten, D., Hastie, T., & Tibshirani, R. (2013). An introduction to statistical learning (1st ed.) [PDF]. Springer.

Long Short-Term Memory (LSTM)

Las redes neuronales Long Short-Term Memory (LSTM) constituyen un tipo especializado de RNNs diseñadas para superar las limitaciones asociados con la captura de dependencias temporales a largo plazo. A diferencia de las RNN tradicionales, las LSTMs incorporan una arquitectura más compleja, introduciendo unidades de memoria y mecanismos de puertas para mejorar la gestión de la información a lo largo del tiempo.

Estructura de las LSTMs

Las LSTMs presentan una estructura modular que consta de tres puertas (gates) fundamentales: la puerta de olvido (forget gate), la puerta de entrada (input gate), y la puerta de salida (output gate). Estas puertas trabajan en conjunto para regular el flujo de información a través de la unidad de memoria, permitiendo un control más preciso sobre qué información retener y cuál olvidar.

  • Puerta de Olvido (Forget Gate): Regula cuánta información se debe olvidar y cuánta se mantiene, combinando la entrada actual y la salida anterior mediante una función sigmoide.

  • Puerta de Entrada (Input Gate): Decide cuánta nueva información debe añadirse a la memoria a largo plazo.

  • Puerta de Salida (Output Gate): Determina cuánta información de la memoria actual se utilizará para la salida final, combinando la entrada actual y la información de la memoria mediante una función sigmoide.

Diagrama de la entradas y dalidas de una LSTM. Fuente: codificandobits https://databasecamp.de/wp-content/uploads/lstm-architecture-1024x709.png.

Tipos de problemas en el modelado de series temporales

La complejidad de un problema de series temporarles suele estar definida por tres factores clave: primero, decidir qué serie o series temporales utilizar para entrenar el modelo; segundo, determinar qué o cuántas series temporales se quieren predecir; y tercero, definir el número de pasos a futuro que se desea predecir. Estos tres aspectos pueden ser un verdadero desafío al abordar problemas de series temporales.

Las redes neuronales recurrentes, gracias a su ámplia variedad de arquitecturas, permiten modelar los siguientes escenarios:

  • Problemas 1:1 - Modelar una única serie y predecir esa misma serie (single-serie, single-output)

    • Descripción: Este tipo de problemas implica modelar una serie temporal utilizando únicamente su pasado. Es un problema típico autoregresivo.
    • Ejemplo: Predicción de la temperatura diaria en base a la temperatura de los últimos días.

  • Problemas N:1 - Modelar una única serie utilizando múltiples series (multi-series, single-output)

    • Descripción: Se trata de problemas en los que se utilizan varias series temporales para predecir una única serie. Cada serie puede representar una entidad o variable diferente, pero la variable salida es solo una de las series.
    • Ejemplo: Predicción de la temperatura diaria en base a múltiples series como: temperatura, humedad y presión atmosférica.

  • Problemas N:M - Modelar múltiples series utilizando múltiples series (multi-series, multiple-outputs)

    • Descripción: Estos problemas consisten en modelar y predecir valores futuros de varias series temporales al mismo tiempo.
    • Ejemplo: Pronosticar los valores en bolsa de varias acciones en función del histórico de la bolsa, del precio de la energía y materias primas.

En todos estos encenarios, la predicción puede realizarse single-step forecasting (un paso a futuro) o multi-step forecasting (múltiples pasos a futuro). En el primer caso, el modelo solo predice un único valor, mientras que en el segundo, el modelo predice múltiples valores a futuro.

En algunas situaciones, puede resultar complicado definir y crear la arquitectura de Deep Learning adecuada para abordar un problema concreto. La librería skforecast dispone de funcionalidades que permiten determinar la arquitectura de Tensorflow adecuada para cada problema, simplificando y acelerando el proceso de modelado para una amplia variedad de problemas. A continuación, se muestra un ejemplo de cómo utilizar skforecast para resolver cada uno de los problemas de series temporales descritos, utilizando redes neuronales recurrentes.

Datos

Los datos empleados en este artículo contienen información detallada sobre la calidad del aire en la ciudad de Valencia (España). La colección de datos abarca desde el 1 de enero de 2019 hasta el 31 de diciembre de 2021, proporcionando mediciones horarias de diversos contaminantes atmosféricos, como partículas PM2.5 y PM10, monóxido de carbono (CO), dióxido de nitrógeno (NO2), entre otros. Los datos se han obtenido de plataforma Red de Vigilancia y Control de la Contaminación Atmosférica, 46250054-València - Centre, https://mediambient.gva.es/es/web/calidad-ambiental/datos-historicos.

Librerías

💡 Tip: Configuración del backend

Desde la versión 0.13.0 de Skforecast, se ha añadido soporte para el backend de PyTorch. Puedes configurar el backend exportando la variable de entorno KERAS_BACKEND o editando tu archivo de configuración local en ~/.keras/keras.json. Las opciones de backend disponibles son: "tensorflow" y "torch". Ejemplo: ```python import os os.environ["KERAS_BACKEND"] = "torch" import keras ```

Warning

El backend debe configurarse antes de importar Keras, y no se puede cambiar el backend después de que se haya importado la librería.
In [1]:
# Procesado de datos
# ==============================================================================
import os
import pandas as pd
import numpy as np
from skforecast.datasets import fetch_dataset

# Gráficos
# ==============================================================================
import matplotlib.pyplot as plt
from skforecast.plot import set_dark_theme
import plotly.express as px
import plotly.graph_objects as go
import plotly.io as pio
import plotly.offline as poff
pio.templates.default = "seaborn"
poff.init_notebook_mode(connected=True)

# Keras
# ==============================================================================
os.environ["KERAS_BACKEND"] = "tensorflow" # 'tensorflow', 'jax´ or 'torch'
import keras
from keras.optimizers import Adam
from keras.losses import MeanSquaredError
from keras.callbacks import EarlyStopping

if keras.__version__ > "3.0":
    if keras.backend.backend() == "tensorflow":
        import tensorflow
    elif keras.backend.backend() == "torch":
        import torch
    else:
        print("Backend not recognized. Please use 'tensorflow' or 'torch'.")

# Modelado 
# ==============================================================================
import skforecast
from skforecast.ForecasterRnn import ForecasterRnn
from skforecast.ForecasterRnn.utils import create_and_compile_model
from sklearn.preprocessing import MinMaxScaler
from skforecast.model_selection_multiseries import backtesting_forecaster_multiseries

# Configuración warnings
# ==============================================================================
import warnings
warnings.filterwarnings('once')

color = '\033[1m\033[38;5;208m' 
print(f"{color}Version skforecast: {skforecast.__version__}")
print(f"{color}Version Keras: {keras.__version__}")
print(f"{color}Using backend: {keras.backend.backend()}")
print(f"{color}Version pandas: {pd.__version__}")
print(f"{color}Version numpy: {np.__version__}")
if keras.__version__ > "3.0":
    if keras.backend.backend() == "tensorflow":
        print(f"{color}Version tensorflow: {tensorflow.__version__}")
    elif keras.backend.backend() == "torch":
        print(f"{color}Version torch: {torch.__version__}")
    else:
        print(f"{color}Version torch: {jax.__version__}")
Version skforecast: 0.13.0
Version Keras: 3.4.1
Using backend: tensorflow
Version pandas: 2.2.2
Version numpy: 1.26.4
Version tensorflow: 2.17.0

Warning

En el momento de escribir este documento, tensorflow solo es compatible con versiones de numpy inferiores a 2.0. Si tienes una versión superior, puedes reducirla ejecutando el siguiente comando: pip install numpy==1.26.4
In [2]:
# Descarga y procesado de datos
# ==============================================================================
air_quality = fetch_dataset(name="air_quality_valencia")
air_quality_valencia
--------------------
Hourly measures of several air chemical pollutant (pm2.5, co, no, no2, pm10,
nox, o3, so2) at Valencia city.
 Red de Vigilancia y Control de la Contaminación Atmosférica, 46250054-València
- Centre, https://mediambient.gva.es/es/web/calidad-ambiental/datos-historicos.
Shape of the dataset: (26304, 10)
In [3]:
# Imputación de datos ausentes mediante interpolación lineal
# ==============================================================================
air_quality = air_quality.interpolate(method="linear")
air_quality = air_quality.sort_index()
air_quality.head()
Out[3]:
pm2.5 co no no2 pm10 nox o3 veloc. direc. so2
datetime
2019-01-01 00:00:00 19.0 0.2 3.0 36.0 22.0 40.0 16.0 0.5 262.0 8.0
2019-01-01 01:00:00 26.0 0.1 2.0 40.0 32.0 44.0 6.0 0.6 248.0 8.0
2019-01-01 02:00:00 31.0 0.1 11.0 42.0 36.0 58.0 3.0 0.3 224.0 8.0
2019-01-01 03:00:00 30.0 0.1 15.0 41.0 35.0 63.0 3.0 0.2 220.0 10.0
2019-01-01 04:00:00 30.0 0.1 16.0 39.0 36.0 63.0 3.0 0.4 221.0 11.0

Se verifica que el conjunto de datos tiene un índice de tipo DatetimeIndex con frecuencia horaria. Si bien no es necesario que los datos tengan este tipo de índice para utilizar skforecast, es más ventajoso para el posterior uso de las predicciones.

In [4]:
# Comprobación de índice y frecuencia
# ==============================================================================
print(f"Tipo de índice: {air_quality.index.dtype}")
print(f"Frecuencia: {air_quality.index.freq}")
Tipo de índice: datetime64[ns]
Frecuencia: <Hour>

Para facilitar el entrenamiento de los modelos, la búsqueda de hiperparámetros óptimos y la evaluación de su capacidad predictiva, los datos se dividen en tres conjuntos separados: entrenamiento, validación y test.

In [5]:
# Split train-validation-test
# ==============================================================================
end_train = "2021-03-31 23:59:00"
end_validation = "2021-09-30 23:59:00"
air_quality_train = air_quality.loc[:end_train, :].copy()
air_quality_val = air_quality.loc[end_train:end_validation, :].copy()
air_quality_test = air_quality.loc[end_validation:, :].copy()

print(
    f"Fechas train      : {air_quality_train.index.min()} --- " 
    f"{air_quality_train.index.max()}  (n={len(air_quality_train)})"
)
print(
    f"Fechas validation : {air_quality_val.index.min()} --- " 
    f"{air_quality_val.index.max()}  (n={len(air_quality_val)})"
)
print(
    f"Fechas test       : {air_quality_test.index.min()} --- " 
    f"{air_quality_test.index.max()}  (n={len(air_quality_test)})"
)
Fechas train      : 2019-01-01 00:00:00 --- 2021-03-31 23:00:00  (n=19704)
Fechas validation : 2021-04-01 00:00:00 --- 2021-09-30 23:00:00  (n=4392)
Fechas test       : 2021-10-01 00:00:00 --- 2021-12-31 23:00:00  (n=2208)
In [6]:
# Gráfico de la serie temporal del contaminante pm2.5
# ==============================================================================
set_dark_theme()
fig, ax = plt.subplots(figsize=(7, 3))
air_quality_train["pm2.5"].rolling(100).mean().plot(ax=ax, label="entrenamiento")
air_quality_val["pm2.5"].rolling(100).mean().plot(ax=ax, label="validación")
air_quality_test["pm2.5"].rolling(100).mean().plot(ax=ax, label="test")
ax.set_title("pm2.5")
ax.legend();

Modelo LSTM y ForecasterRnn

Si bien tensorflow-keras facilita el proceso de crear arquitecturas de deep learning, no siempre es trivial determinar las dimensiones que debe tener un modelo LSTM para forecasting ya que estas dependen de cuantas series temporales se estén modelando, cuantas prediciendo y la longitud del horicento de predicción.

Para tratar de mejorar la experiencia del usuario y acelerar el proceso de propotipado, desarollo y puesta en producción, skforecast dispone de la función create_and_compile_model, con la que, indicando apenas unos pocos argumentos, se infiere la arquitectura y se crea el modelo.

  • series: Series temporales que se utilizarán para entrenar el modelo

  • levels: Series temporales que se quieren predecir

  • lags: Número de pasos de tiempo que se utilizarán para predecir el siguiente valor.

  • steps: Número de pasos de tiempo que se quieren predecir.

  • recurrent_layer: Tipo de capa recurrente a utilizar. Por defecto, se utiliza una capa LSTM.

  • recurrent_units: Número de unidades de la capa recurrente. Por defecto, se utiliza 100. Si se pasa una lista, se creará una capa recurrente por cada elemento de la lista.

  • dense_units: Número de unidades de la capa densa. Por defecto, se utiliza 64. Si se pasa una lista, se creará una capa densa por cada elemento de la lista.

  • optimizer: Optimizador a utilizar. Por defecto, se utiliza Adam con learning rate de 0.01.

  • loss: Función de pérdida a utilizar. Por defecto, se utiliza Mean Squared Error.

✎ Nota

La función create_and_compile_model está pensada para facilitar la creación del modelo Tensorflow, sin embargo, usuarios más avanzados pueden crear sus propias arquitecturas siempre y cuado las dimensiones de entrada y salida coincidan con el caso de uso al que se va a aplicar el modelo.

Una vez que el modelo se ha creado y compilado, el siguiente paso es crear una instancia del ForecasterRnn. Esta clase se encarga de añadir al modelo de deep learning todas las funcionalidades necesarias para que pueda utilizarse en problemas de forecasting. Además es compatible con el resto de funcionalidades que ofrece skforecast (backtesting, busqueda de hiperparámetros, ...).

Problema 1:1 - Modelar una única serie

En este priemr escenario, se desea predecir la concentracion de $O_3$ de los próximos 1 y 5 días utilizando únciamente sus propios datos históricos. Se trata por lo tanto de un escenario en el que una única serie temporal se modela utilizando únicamente sus valores pasados. Este problema también se denomina predicción autoregresiva.

Predicción de un día a futuro (Single step forecasting)

En primer lugar, se realiza un pronóstico de un solo paso a futuro. Par ello, se creará un modelo utilizando la función create_and_compile_model, que se pasa como argumento a la clase ForecasterRnn.

Este es el ejemplo más sencillo de forecasting con redes neuronales recurrentes. El modelo solo necesita una serie temporal para entrenar y predecir. Por lo tanto, el argumento series de la función create_and_compile_model solo necesita una serie temporal, la mismo que se quiere predecir (levels). Además, como solo se quiere predecir un único valor a futuro, el argumento steps es igual a 1.

In [7]:
# Creación del modelo
# ==============================================================================
series = ["o3"] # Series temporales que se utilizarán para entrenar el modelo.
levels = ["o3"] # Serie que se quiere predecir
lags = 32 # Valores pasados a utilizar en la predicción
steps = 1 # Pasos a futuro a predecir

# Selección de las series temporales utilizadas
data = air_quality[series].copy()
data_train = air_quality_train[series].copy()
data_val = air_quality_val[series].copy()
data_test = air_quality_test[series].copy()

model = create_and_compile_model(
    series=data_train,
    levels=levels, 
    lags=lags,
    steps=steps,
    recurrent_layer="LSTM",
    recurrent_units=4,
    dense_units=16,
    optimizer=Adam(learning_rate=0.01), 
    loss=MeanSquaredError()
)
model.summary()
keras version: 3.4.1
Using backend: tensorflow
tensorflow version: 2.17.0
Model: "functional"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type)                     Output Shape                  Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ input_layer (InputLayer)        │ (None, 32, 1)          │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ lstm (LSTM)                     │ (None, 4)              │            96 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense (Dense)                   │ (None, 16)             │            80 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_1 (Dense)                 │ (None, 1)              │            17 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ reshape (Reshape)               │ (None, 1, 1)           │             0 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 193 (772.00 B)
 Trainable params: 193 (772.00 B)
 Non-trainable params: 0 (0.00 B)

Se utilizará un modelo sencillo, una red LSTM con una única capa recurrente con 4 neuronas y una capa oculta densa de 16 neuronas. La siguiente tabla muestra una descripción detallada de cada capa:

Capa Tipo Forma de salida Parámetros Descripción
Capa de Entrada (InputLayer) InputLayer (None, 32, 1) 0 Esta es la capa de entrada del modelo. Recibe secuencias de longitud 32, correspondiente al número de lags con una dimensión en cada paso de tiempo.
Capa LSTM (Long Short-Term Memory) LSTM (None, 4) 96 La capa LSTM es una capa de memoria a corto y largo plazo que procesa la secuencia de entrada. Tiene 4 unidades LSTM y se conecta a la capa siguiente.
Primera Capa Densa (Dense) Dense (None, 16) 80 Esta es una capa completamente conectada con 16 unidades y utiliza una función de activación por defecto (relu) en la arquitectura proporcionada.
Segunda Capa Densa (Dense) Dense (None, 1) 17 Otra capa densa completamente conectada, esta vez con una sola unidad de salida. También utiliza una función de activación por defecto.
Capa de Remodelación (Reshape) Reshape (None, 1, 1) 0 Esta capa remodela la salida de la capa densa anterior para tener una forma específica (None, 1, 1). Esta capa no es estrictamente necesaria, pero se incluye para que el módulo sea generalizable a otros problemas de forecasting multi-output. La dimensión de esta capa de salida es (None, pasos_a_futuro_a_predecir, series_a_predecir). En este caso se tiene steps=1 y levels="o3", por lo que la dimensión es (None, 1, 1)
Total de Parámetros y Entrenables - - 193 Total de Parámetros: 193, Parámetros Entrenables: 193, Parámetros No Entrenables: 0

Una vez que el modelo se ha creado y compilado, el siguiente paso es crear una instancia del ForecasterRnn. Esta clase se encarga de añadir al modelo de regresión todas las funcionalidades necesarias para que pueda utilizarse en problemas de forecasting. Además es compatible con el resto de funcionalidades que ofrece skforecast.

El forecaster se crea a partir del modelo y se le pasan los datos de validación para que pueda evaluar el modelo en cada época. Además, se le pasa un objeto MinMaxScaler para que estandarice los datos de entrada y salida. Este objeto se encargará de escalar los datos de entrada y de desescalar las predicciones.

Por otro lado, los fit_kwargs son los argumentos que se le pasan al método fit del modelo. En este caso, se le pasa el número de épocas, el tamaño del batch, los datos de validación y un callback para detener el entrenamiento cuando la pérdida de validación deje de disminuir.

In [8]:
# Creación del forecaster
# ==============================================================================
forecaster = ForecasterRnn(
    regressor=model,
    levels=levels,
    transformer_series=MinMaxScaler(),
    fit_kwargs={
        "epochs": 10,  # Número de épocas para entrenar el modelo.
        "batch_size": 32,  # Tamaño del batch para entrenar el modelo.
        "callbacks": [
            EarlyStopping(monitor="val_loss", patience=5)
        ],  # Callback para detener el entrenamiento cuando ya no esté aprendiendo más.
        "series_val": data_val,  # Datos de validación para el entrenamiento del modelo.
    },
)
forecaster
/home/ubuntu/anaconda3/envs/skforecast_13_py12/lib/python3.12/site-packages/skforecast/ForecasterRnn/ForecasterRnn.py:229: UserWarning:

Setting `lags` = 'auto'. `lags` are inferred from the regressor architecture. Avoid the warning with lags=lags.

/home/ubuntu/anaconda3/envs/skforecast_13_py12/lib/python3.12/site-packages/skforecast/ForecasterRnn/ForecasterRnn.py:265: UserWarning:

`steps` default value = 'auto'. `steps` inferred from regressor architecture. Avoid the warning with steps=steps.

Out[8]:
============= 
ForecasterRnn 
============= 
Regressor: <Functional name=functional, built=True> 
Lags: [ 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
 25 26 27 28 29 30 31 32] 
Transformer for series: MinMaxScaler() 
Window size: 32 
Target series, levels: ['o3'] 
Multivariate series (names): None 
Maximum steps predicted: [1] 
Training range: None 
Training index type: None 
Training index frequency: None 
Model parameters: {'name': 'functional', 'trainable': True, 'layers': [{'module': 'keras.layers', 'class_name': 'InputLayer', 'config': {'batch_shape': (None, 32, 1), 'dtype': 'float32', 'sparse': False, 'name': 'input_layer'}, 'registered_name': None, 'name': 'input_layer', 'inbound_nodes': []}, {'module': 'keras.layers', 'class_name': 'LSTM', 'config': {'name': 'lstm', 'trainable': True, 'dtype': {'module': 'keras', 'class_name': 'DTypePolicy', 'config': {'name': 'float32'}, 'registered_name': None}, 'return_sequences': False, 'return_state': False, 'go_backwards': False, 'stateful': False, 'unroll': False, 'zero_output_for_mask': False, 'units': 4, 'activation': 'relu', 'recurrent_activation': 'sigmoid', 'use_bias': True, 'kernel_initializer': {'module': 'keras.initializers', 'class_name': 'GlorotUniform', 'config': {'seed': None}, 'registered_name': None}, 'recurrent_initializer': {'module': 'keras.initializers', 'class_name': 'OrthogonalInitializer', 'config': {'gain': 1.0, 'seed': None}, 'registered_name': None}, 'bias_initializer': {'module': 'keras.initializers', 'class_name': 'Zeros', 'config': {}, 'registered_name': None}, 'unit_forget_bias': True, 'kernel_regularizer': None, 'recurrent_regularizer': None, 'bias_regularizer': None, 'activity_regularizer': None, 'kernel_constraint': None, 'recurrent_constraint': None, 'bias_constraint': None, 'dropout': 0.0, 'recurrent_dropout': 0.0, 'seed': None}, 'registered_name': None, 'build_config': {'input_shape': (None, 32, 1)}, 'name': 'lstm', 'inbound_nodes': [{'args': ({'class_name': '__keras_tensor__', 'config': {'shape': (None, 32, 1), 'dtype': 'float32', 'keras_history': ['input_layer', 0, 0]}},), 'kwargs': {'training': False, 'mask': None}}]}, {'module': 'keras.layers', 'class_name': 'Dense', 'config': {'name': 'dense', 'trainable': True, 'dtype': {'module': 'keras', 'class_name': 'DTypePolicy', 'config': {'name': 'float32'}, 'registered_name': None}, 'units': 16, 'activation': 'relu', 'use_bias': True, 'kernel_initializer': {'module': 'keras.initializers', 'class_name': 'GlorotUniform', 'config': {'seed': None}, 'registered_name': None}, 'bias_initializer': {'module': 'keras.initializers', 'class_name': 'Zeros', 'config': {}, 'registered_name': None}, 'kernel_regularizer': None, 'bias_regularizer': None, 'kernel_constraint': None, 'bias_constraint': None}, 'registered_name': None, 'build_config': {'input_shape': (None, 4)}, 'name': 'dense', 'inbound_nodes': [{'args': ({'class_name': '__keras_tensor__', 'config': {'shape': (None, 4), 'dtype': 'float32', 'keras_history': ['lstm', 0, 0]}},), 'kwargs': {}}]}, {'module': 'keras.layers', 'class_name': 'Dense', 'config': {'name': 'dense_1', 'trainable': True, 'dtype': {'module': 'keras', 'class_name': 'DTypePolicy', 'config': {'name': 'float32'}, 'registered_name': None}, 'units': 1, 'activation': 'linear', 'use_bias': True, 'kernel_initializer': {'module': 'keras.initializers', 'class_name': 'GlorotUniform', 'config': {'seed': None}, 'registered_name': None}, 'bias_initializer': {'module': 'keras.initializers', 'class_name': 'Zeros', 'config': {}, 'registered_name': None}, 'kernel_regularizer': None, 'bias_regularizer': None, 'kernel_constraint': None, 'bias_constraint': None}, 'registered_name': None, 'build_config': {'input_shape': (None, 16)}, 'name': 'dense_1', 'inbound_nodes': [{'args': ({'class_name': '__keras_tensor__', 'config': {'shape': (None, 16), 'dtype': 'float32', 'keras_history': ['dense', 0, 0]}},), 'kwargs': {}}]}, {'module': 'keras.layers', 'class_name': 'Reshape', 'config': {'name': 'reshape', 'trainable': True, 'dtype': {'module': 'keras', 'class_name': 'DTypePolicy', 'config': {'name': 'float32'}, 'registered_name': None}, 'target_shape': (1, 1)}, 'registered_name': None, 'build_config': {'input_shape': (None, 1)}, 'name': 'reshape', 'inbound_nodes': [{'args': ({'class_name': '__keras_tensor__', 'config': {'shape': (None, 1), 'dtype': 'float32', 'keras_history': ['dense_1', 0, 0]}},), 'kwargs': {}}]}], 'input_layers': [['input_layer', 0, 0]], 'output_layers': [['reshape', 0, 0]]} 
Compile parameters: {'optimizer': {'module': 'keras.optimizers', 'class_name': 'Adam', 'config': {'name': 'adam', 'learning_rate': 0.009999999776482582, 'weight_decay': None, 'clipnorm': None, 'global_clipnorm': None, 'clipvalue': None, 'use_ema': False, 'ema_momentum': 0.99, 'ema_overwrite_frequency': None, 'loss_scale_factor': None, 'gradient_accumulation_steps': None, 'beta_1': 0.9, 'beta_2': 0.999, 'epsilon': 1e-07, 'amsgrad': False}, 'registered_name': None}, 'loss': {'module': 'keras.losses', 'class_name': 'MeanSquaredError', 'config': {'name': 'mean_squared_error', 'reduction': 'sum_over_batch_size'}, 'registered_name': None}, 'loss_weights': None, 'metrics': None, 'weighted_metrics': None, 'run_eagerly': False, 'steps_per_execution': 1, 'jit_compile': False} 
fit_kwargs: {'epochs': 10, 'batch_size': 32, 'callbacks': [<keras.src.callbacks.early_stopping.EarlyStopping object at 0x7f21b015c4a0>]} 
Creation date: 2024-08-08 09:20:48 
Last fit date: None 
Skforecast version: 0.13.0 
Python version: 3.12.4 
Forecaster id: None 

Warning

El warning indica que el número de lags se ha inferido de la arquitectura del modelo. En este caso, el modelo tiene una capa LSTM con 32 neuronas, por lo que el número de lags es 32. Si se desea utilizar un número diferente de lags, se puede especificar el argumento lags en la función create_and_compile_model. Para omitir el warning, se puede especificar el argumento lags=lags y steps=steps en la inicialización del ForecasterRnn.
In [9]:
# Entrenamiento del forecaster
# ==============================================================================
forecaster.fit(data_train)
Epoch 1/10
615/615 ━━━━━━━━━━━━━━━━━━━━ 11s 13ms/step - loss: 0.0114 - val_loss: 0.0063
Epoch 2/10
615/615 ━━━━━━━━━━━━━━━━━━━━ 8s 9ms/step - loss: 0.0056 - val_loss: 0.0056
Epoch 3/10
615/615 ━━━━━━━━━━━━━━━━━━━━ 8s 13ms/step - loss: 0.0054 - val_loss: 0.0053
Epoch 4/10
615/615 ━━━━━━━━━━━━━━━━━━━━ 8s 12ms/step - loss: 0.0055 - val_loss: 0.0054
Epoch 5/10
615/615 ━━━━━━━━━━━━━━━━━━━━ 11s 13ms/step - loss: 0.0053 - val_loss: 0.0055
Epoch 6/10
615/615 ━━━━━━━━━━━━━━━━━━━━ 7s 11ms/step - loss: 0.0054 - val_loss: 0.0053
Epoch 7/10
615/615 ━━━━━━━━━━━━━━━━━━━━ 6s 10ms/step - loss: 0.0050 - val_loss: 0.0058
Epoch 8/10
615/615 ━━━━━━━━━━━━━━━━━━━━ 8s 13ms/step - loss: 0.0054 - val_loss: 0.0055
Epoch 9/10
615/615 ━━━━━━━━━━━━━━━━━━━━ 10s 13ms/step - loss: 0.0051 - val_loss: 0.0053
Epoch 10/10
615/615 ━━━━━━━━━━━━━━━━━━━━ 9s 15ms/step - loss: 0.0051 - val_loss: 0.0053
In [10]:
# Seguimiento del entrenamiento y overfitting
# ==============================================================================
fig, ax = plt.subplots(figsize=(5, 2.5))
forecaster.plot_history(ax=ax)

En los modelos de deep learning es muy importante controlar el overfitting. Para ello, se utiliza un callback de Keras que detiene el entrenamiento cuando el valor de la función de coste, en los datos de validación, deja de disminuir. En este caso, el callback no llega a detener el entrenamiento, ya que únicamente hemos entrenado con 10 épocas. Si se aumenta el número de épocas, el callback detendrá el entrenamiento cuando la pérdida de validación deje de disminuir.

Por otro lado, otra herramienta muy util es el graficado de la pérdida de entrenamiento y validación en cada época. Esto permite visualizar el comportamiento del modelo y detectar posibles problemas de overfitting.

En el caso de nuestro modelo, se observa que la pérdida de entrenamiento disminuye rápidamente en las primera época, mientras que la pérdida de validación es baja desde la primera época. De esto se deduce lo siguiente:

  • El modelo no está haciendo overfitting, ya que la pérdida de validación es similar a la de entrenamiento.

  • El error de validación se calcula una vez se entrena el modelo, por ello el primer valor de la pérdida de validación en la primera época es parecido al de la pérdida de entrenamiento en la segunda época.

Explicación gráfica del overfitting. Fuente: https://datahacker.rs/018-pytorch-popular-techniques-to-prevent-the-overfitting-in-a-neural-networks/.

Una vez que el forecaster ha sido entrenado, se pueden obtener las predicciones. En este caso, es un único valor ya que solo se ha especificado un paso a futuro (step).

In [11]:
# Predicción
# ==============================================================================
predictions = forecaster.predict()
predictions
Out[11]:
o3
2021-04-01 45.644142

Para obtener una estimación robusta de la capacidad predictiva del modelo, se realiza un proceso de backtesting. El proceso de backtesting consiste en generar una predicción para cada observación del conjunto de test, siguiendo el mismo procedimiento que se seguiría si el modelo estuviese en producción, y finalmente comparar el valor predicho con el valor real.

In [12]:
# Backtesting con datos de test
# ==============================================================================
metrics, predictions = backtesting_forecaster_multiseries(
    forecaster=forecaster,
    steps=forecaster.max_step,
    series=data,
    levels=forecaster.levels,
    initial_train_size=len(data.loc[:end_validation, :]), # Datos de entrenamiento + validación
    metric="mean_absolute_error",
    verbose=False,
    refit=False,
)
Epoch 1/10
752/752 ━━━━━━━━━━━━━━━━━━━━ 9s 11ms/step - loss: 0.0052 - val_loss: 0.0059
Epoch 2/10
752/752 ━━━━━━━━━━━━━━━━━━━━ 8s 10ms/step - loss: 0.0052 - val_loss: 0.0052
Epoch 3/10
752/752 ━━━━━━━━━━━━━━━━━━━━ 7s 10ms/step - loss: 0.0051 - val_loss: 0.0054
Epoch 4/10
752/752 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - loss: 0.0048 - val_loss: 0.0052
Epoch 5/10
752/752 ━━━━━━━━━━━━━━━━━━━━ 6s 8ms/step - loss: 0.0051 - val_loss: 0.0052
Epoch 6/10
752/752 ━━━━━━━━━━━━━━━━━━━━ 6s 8ms/step - loss: 0.0051 - val_loss: 0.0051
Epoch 7/10
752/752 ━━━━━━━━━━━━━━━━━━━━ 6s 8ms/step - loss: 0.0050 - val_loss: 0.0051
Epoch 8/10
752/752 ━━━━━━━━━━━━━━━━━━━━ 6s 8ms/step - loss: 0.0051 - val_loss: 0.0053
Epoch 9/10
752/752 ━━━━━━━━━━━━━━━━━━━━ 6s 8ms/step - loss: 0.0049 - val_loss: 0.0053
Epoch 10/10
752/752 ━━━━━━━━━━━━━━━━━━━━ 6s 8ms/step - loss: 0.0049 - val_loss: 0.0052
In [13]:
# Predicciones de backtesting
# ==============================================================================
predictions
Out[13]:
o3
2021-10-01 00:00:00 51.279484
2021-10-01 01:00:00 56.470856
2021-10-01 02:00:00 60.604416
2021-10-01 03:00:00 60.478226
2021-10-01 04:00:00 49.347538
... ...
2021-12-31 19:00:00 14.238775
2021-12-31 20:00:00 12.554365
2021-12-31 21:00:00 14.519808
2021-12-31 22:00:00 15.646739
2021-12-31 23:00:00 17.323700

2208 rows × 1 columns

In [14]:
# Gráfico de las predicciones vs valores reales en el conjunto de test
# ==============================================================================
fig = go.Figure()
trace1 = go.Scatter(x=data_test.index, y=data_test['o3'], name="test", mode="lines")
trace2 = go.Scatter(x=predictions.index, y=predictions['o3'], name="predicciones", mode="lines")
fig.add_trace(trace1)
fig.add_trace(trace2)
fig.update_layout(
    title="Predicciones vs valores reales en el conjunto de test",
    xaxis_title="Date time",
    yaxis_title="O3",
    width=750,
    height=350,
    margin=dict(l=20, r=20, t=35, b=20),
    legend=dict(
        orientation="h",
        yanchor="top",
        y=1.05,
        xanchor="left",
        x=0
    )
)
fig.show()
In [15]:
# Métricas de backtesting
# ==============================================================================
metrics
Out[15]:
levels mean_absolute_error
0 o3 5.841176
In [16]:
# Error en % respecto a la media de la serie
# ==============================================================================
rel_mse = 100 * metrics.loc[0, 'mean_absolute_error'] / np.mean(data["o3"])
print(f"Media de la serie: {np.mean(data['o3']):0.2f}")
print(f"Error (mae) relativo: {rel_mse:0.2f} %")
Media de la serie: 54.52
Error (mae) relativo: 10.71 %

El modelo consigue un error de backtesting (mae) de 5.9, lo que se correspondiente con error relativo respecto a la media de la serie del 10.88%.

Multi-step forecasting

En este caso, se desea predecir los próximos 5 valores de O3 utilizando únicamente sus datos históricos. Se trata por lo tanto de un escenario en el que múltiples pasos a futuro de una única serie temporal se modela utilizando únicamente sus valores pasados.

Para ello se utilizará una arquitectura similar a la anterior, pero con un mayor número de neuronas en la capa LSTM y en la primera capa densa. Esto permitirá al modelo tener mayor flexibilidad para modelar la serie temporal.

In [17]:
# Creación del modelo
# ==============================================================================
series = ["o3"] # Series temporales que se utilizarán para entrenar el modelo. 
levels = ["o3"] # Serie que se quiere predecir
lags = 32 # Valores pasados a utilizar en la predicción
steps = 5 # Pasos a futuro a predecir

model = create_and_compile_model(
    series=data_train,
    levels=levels, 
    lags=lags,
    steps=steps,
    recurrent_layer="LSTM",
    recurrent_units=50,
    dense_units=32,
    optimizer=Adam(learning_rate=0.01), 
    loss=MeanSquaredError()
)
model.summary()
keras version: 3.4.1
Using backend: tensorflow
tensorflow version: 2.17.0
Model: "functional_1"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type)                     Output Shape                  Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ input_layer_1 (InputLayer)      │ (None, 32, 1)          │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ lstm_1 (LSTM)                   │ (None, 50)             │        10,400 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_2 (Dense)                 │ (None, 32)             │         1,632 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_3 (Dense)                 │ (None, 5)              │           165 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ reshape_1 (Reshape)             │ (None, 5, 1)           │             0 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 12,197 (47.64 KB)
 Trainable params: 12,197 (47.64 KB)
 Non-trainable params: 0 (0.00 B)
In [18]:
# Creación del Forecaster
# ==============================================================================
forecaster = ForecasterRnn(
    regressor=model,
    levels=levels,
    steps=steps,
    lags=lags,
    transformer_series=MinMaxScaler(),
    fit_kwargs={
        "epochs": 10,  # Número de épocas para entrenar el modelo.
        "batch_size": 32,  # Tamaño del batch para entrenar el modelo.
        "callbacks": [
            EarlyStopping(monitor="val_loss", patience=5)
        ],  # Callback para detener el entrenamiento cuando ya no esté aprendiendo más.
        "series_val": data_val,  # Datos de validación para el entrenamiento del modelo.
    },
)

✎ Nota

El parámetro `fit_kwargs` es de gran utilidad ya que permite establecer cualquier configuración en el modelo, en este caso de Keras. En el código anterior se define el número de épocas de entrenamiento (10) con un batch size de 32. Se configura un callback de `EarlyStopping` que detiene el entrenamiento cuando la pérdida de validación deja de disminuir durante 5 épocas (`patience=5`). También se pueden cofigurar otros callbacks como `ModelCheckpoint` para guardar el modelo en cada época, o incluso Tensorboard para visualizar la pérdida de entrenamiento y validación en tiempo real.
In [19]:
# Entrenamiento del forecaster
# ==============================================================================
forecaster.fit(data_train)
Epoch 1/10
615/615 ━━━━━━━━━━━━━━━━━━━━ 11s 14ms/step - loss: 0.0294 - val_loss: 0.0120
Epoch 2/10
615/615 ━━━━━━━━━━━━━━━━━━━━ 8s 13ms/step - loss: 0.0127 - val_loss: 0.0116
Epoch 3/10
615/615 ━━━━━━━━━━━━━━━━━━━━ 8s 13ms/step - loss: 0.0121 - val_loss: 0.0115
Epoch 4/10
615/615 ━━━━━━━━━━━━━━━━━━━━ 8s 13ms/step - loss: 0.0118 - val_loss: 0.0132
Epoch 5/10
615/615 ━━━━━━━━━━━━━━━━━━━━ 8s 13ms/step - loss: 0.0117 - val_loss: 0.0120
Epoch 6/10
615/615 ━━━━━━━━━━━━━━━━━━━━ 8s 13ms/step - loss: 0.0112 - val_loss: 0.0174
Epoch 7/10
615/615 ━━━━━━━━━━━━━━━━━━━━ 8s 13ms/step - loss: 0.0115 - val_loss: 0.0113
Epoch 8/10
615/615 ━━━━━━━━━━━━━━━━━━━━ 8s 14ms/step - loss: 0.0115 - val_loss: 0.0120
Epoch 9/10
615/615 ━━━━━━━━━━━━━━━━━━━━ 9s 14ms/step - loss: 0.0112 - val_loss: 0.0118
Epoch 10/10
615/615 ━━━━━━━━━━━━━━━━━━━━ 8s 13ms/step - loss: 0.0111 - val_loss: 0.0119
In [20]:
# Seguimiento del entrenamiento y overfitting
# ==============================================================================
fig, ax = plt.subplots(figsize=(5, 2.5))
forecaster.plot_history(ax=ax)

Se intuye que la predicción va a ser de peor calidad que en el caso anterior, ya que el error observado en las distintas épocas es mayor. Esto tiene una explicación sencilla, y es que el modelo tiene que predecir 5 valores en lugar de 1. Por lo tanto, el error de validación es mayor ya que se está calculando la pérdida de 5 valores en lugar de 1.

Se realiza la predicción. En este caso son 5 valores ya que se ha especificado 5 pasos a futuro (step).

In [21]:
# Predicción
# ==============================================================================
predictions = forecaster.predict()
predictions
Out[21]:
o3
2021-04-01 00:00:00 45.708866
2021-04-01 01:00:00 42.369797
2021-04-01 02:00:00 38.885204
2021-04-01 03:00:00 35.853554
2021-04-01 04:00:00 32.612747

También se pueden predecir steps especificos, siempre y cuando se encuentren dentro del horizonte de predicción definido en el modelo.

In [22]:
# Predicción steps especificos
# ==============================================================================
predictions = forecaster.predict(steps=[1, 3])
predictions
Out[22]:
o3
2021-04-01 00:00:00 45.708866
2021-04-01 02:00:00 38.885204
In [23]:
# Backtesting con datos de test
# ==============================================================================
metrics, predictions = backtesting_forecaster_multiseries(
    forecaster=forecaster,
    steps=forecaster.max_step,
    series=data,
    levels=forecaster.levels,
    initial_train_size=len(data.loc[:end_validation, :]), # Datos de entrenamiento + validación
    metric="mean_absolute_error",
    verbose=False,
    refit=False,
)
Epoch 1/10
752/752 ━━━━━━━━━━━━━━━━━━━━ 12s 14ms/step - loss: 0.0111 - val_loss: 0.0119
Epoch 2/10
752/752 ━━━━━━━━━━━━━━━━━━━━ 10s 13ms/step - loss: 0.0112 - val_loss: 0.0115
Epoch 3/10
752/752 ━━━━━━━━━━━━━━━━━━━━ 10s 13ms/step - loss: 0.0113 - val_loss: 0.0115
Epoch 4/10
752/752 ━━━━━━━━━━━━━━━━━━━━ 10s 14ms/step - loss: 0.0109 - val_loss: 0.0110
Epoch 5/10
752/752 ━━━━━━━━━━━━━━━━━━━━ 10s 13ms/step - loss: 0.0108 - val_loss: 0.0112
Epoch 6/10
752/752 ━━━━━━━━━━━━━━━━━━━━ 11s 14ms/step - loss: 0.0109 - val_loss: 0.0110
Epoch 7/10
752/752 ━━━━━━━━━━━━━━━━━━━━ 10s 13ms/step - loss: 0.0108 - val_loss: 0.0113
Epoch 8/10
752/752 ━━━━━━━━━━━━━━━━━━━━ 10s 13ms/step - loss: 0.0109 - val_loss: 0.0127
Epoch 9/10
752/752 ━━━━━━━━━━━━━━━━━━━━ 10s 13ms/step - loss: 0.0108 - val_loss: 0.0108
Epoch 10/10
752/752 ━━━━━━━━━━━━━━━━━━━━ 10s 13ms/step - loss: 0.0108 - val_loss: 0.0121
In [24]:
# Predicciones de backtesting
# ==============================================================================
predictions
Out[24]:
o3
2021-10-01 00:00:00 54.971054
2021-10-01 01:00:00 54.714790
2021-10-01 02:00:00 53.022858
2021-10-01 03:00:00 51.783970
2021-10-01 04:00:00 50.159683
... ...
2021-12-31 19:00:00 20.751768
2021-12-31 20:00:00 21.181679
2021-12-31 21:00:00 12.413179
2021-12-31 22:00:00 15.483805
2021-12-31 23:00:00 17.339401

2208 rows × 1 columns

In [25]:
# Gráfico de las predicciones vs valores reales en el conjunto de test
# ==============================================================================
fig = go.Figure()
trace1 = go.Scatter(x=data_test.index, y=data_test['o3'], name="test", mode="lines")
trace2 = go.Scatter(x=predictions.index, y=predictions['o3'], name="predicciones", mode="lines")
fig.add_trace(trace1)
fig.add_trace(trace2)
fig.update_layout(
    title="Predicciones vs valores reales en el conjunto de test",
    xaxis_title="Date time",
    yaxis_title="O3",
    width=750,
    height=350,
    margin=dict(l=20, r=20, t=35, b=20),
    legend=dict(
        orientation="h",
        yanchor="top",
        y=1.05,
        xanchor="left",
        x=0
    )
)
fig.show()
In [26]:
# Métricas de backtesting
# ==============================================================================
metrics
Out[26]:
levels mean_absolute_error
0 o3 9.716864
In [27]:
# Error mse en % respecto a la media de la serie
# ==============================================================================
rel_mse = 100 * metrics.loc[0, 'mean_absolute_error'] / np.mean(data["o3"])
print(f"Media de la serie: {np.mean(data['o3']):0.2f}")
print(f"Error mse relativo: {rel_mse:0.2f} %")
Media de la serie: 54.52
Error mse relativo: 17.82 %

En este caso la predicción es empeora respecto al caso anterior. Esto es de esperar ya que el modelo tiene que predecir 5 valores en lugar de 1.

Problemas N:1 - Series temporales múltiples con salida única

En este caso se tratará de predecir la misma serie temporal, pero utilizando múltiples series temporales como predictores. Se trata, por lo tanto, de un escenario en el que valores pasados de múltiples series temporales se utilizan predecir una única serie temporal.

Este tipo de aproximaciones son muy útiles cuando se dispone de múltiples series temporales relacionadas entre sí. Por ejemplo, en el caso de la predicción de la temperatura, se pueden utilizar múltiples series temporales como la humedad, la presión atmosférica, la velocidad del viento, etc.

En este tipo de problemas, la arquitectura de la red neuronal es más compleja, se necesita una capa densa recurrente adicional para procesar las múltiples series de entrada. Además, se añade otra capa oculta densa para procesar la salida de la capa recurrente. Como se puede observar, la creación del modelo utilizando skforecast es muy sencilla, simplemente basta con pasar una lista de enteros al arguento recurrent_units y dense_units para crear múltiples capas recurrentes y densas.

In [28]:
# Creación del modelo
# ==============================================================================
# Series temporales utilizadas en el entrenamiento. Tiene que incluir la serie a predecir.
series = ['pm2.5', 'co', 'no', 'no2', 'pm10', 'nox', 'o3', 'veloc.', 'direc.','so2'] 
levels = ["o3"] # Serie que se quiere predecir
lags = 32 # Valores pasados a utilizar en la predicción
steps = 5 # Pasos a futuro a predecir

# Selección de las series temporales utilizadas
data = air_quality[series].copy()
data_train = air_quality_train[series].copy()
data_val = air_quality_val[series].copy()
data_test = air_quality_test[series].copy()

model = create_and_compile_model(
    series=data_train,
    levels=levels, 
    lags=lags,
    steps=steps,
    recurrent_layer="LSTM",
    recurrent_units=[100, 50],
    dense_units=[64, 32],
    optimizer=Adam(learning_rate=0.01), 
    loss=MeanSquaredError()
)
model.summary()
keras version: 3.4.1
Using backend: tensorflow
tensorflow version: 2.17.0
Model: "functional_2"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type)                     Output Shape                  Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ input_layer_2 (InputLayer)      │ (None, 32, 10)         │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ lstm_2 (LSTM)                   │ (None, 32, 100)        │        44,400 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ lstm_3 (LSTM)                   │ (None, 50)             │        30,200 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_4 (Dense)                 │ (None, 64)             │         3,264 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_5 (Dense)                 │ (None, 32)             │         2,080 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_6 (Dense)                 │ (None, 5)              │           165 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ reshape_2 (Reshape)             │ (None, 5, 1)           │             0 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 80,109 (312.93 KB)
 Trainable params: 80,109 (312.93 KB)
 Non-trainable params: 0 (0.00 B)

Una vez que el modelo se ha creado y compilado, el siguiente paso es crear una instancia del ForecasterRnn. Esta clase se encarga de añadir al modelo de regresión, todas las funcionalidades necesarias para que pueda utilizarse en problemas de forecasting.

In [29]:
# Creación del Forecaster
# ==============================================================================
forecaster = ForecasterRnn(
    regressor=model,
    levels=levels,
    steps=steps,
    lags=lags,
    transformer_series=MinMaxScaler(),
    fit_kwargs={
        "epochs": 4,  # Número de épocas para entrenar el modelo.
        "batch_size": 128,  # Tamaño del batch para entrenar el modelo.
        "series_val": data_val,  # Datos de validación para el entrenamiento del modelo.
    },
)
forecaster
Out[29]:
============= 
ForecasterRnn 
============= 
Regressor: <Functional name=functional_2, built=True> 
Lags: [ 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
 25 26 27 28 29 30 31 32] 
Transformer for series: MinMaxScaler() 
Window size: 32 
Target series, levels: ['o3'] 
Multivariate series (names): None 
Maximum steps predicted: [1 2 3 4 5] 
Training range: None 
Training index type: None 
Training index frequency: None 
Model parameters: {'name': 'functional_2', 'trainable': True, 'layers': [{'module': 'keras.layers', 'class_name': 'InputLayer', 'config': {'batch_shape': (None, 32, 10), 'dtype': 'float32', 'sparse': False, 'name': 'input_layer_2'}, 'registered_name': None, 'name': 'input_layer_2', 'inbound_nodes': []}, {'module': 'keras.layers', 'class_name': 'LSTM', 'config': {'name': 'lstm_2', 'trainable': True, 'dtype': {'module': 'keras', 'class_name': 'DTypePolicy', 'config': {'name': 'float32'}, 'registered_name': None}, 'return_sequences': True, 'return_state': False, 'go_backwards': False, 'stateful': False, 'unroll': False, 'zero_output_for_mask': False, 'units': 100, 'activation': 'relu', 'recurrent_activation': 'sigmoid', 'use_bias': True, 'kernel_initializer': {'module': 'keras.initializers', 'class_name': 'GlorotUniform', 'config': {'seed': None}, 'registered_name': None}, 'recurrent_initializer': {'module': 'keras.initializers', 'class_name': 'OrthogonalInitializer', 'config': {'gain': 1.0, 'seed': None}, 'registered_name': None}, 'bias_initializer': {'module': 'keras.initializers', 'class_name': 'Zeros', 'config': {}, 'registered_name': None}, 'unit_forget_bias': True, 'kernel_regularizer': None, 'recurrent_regularizer': None, 'bias_regularizer': None, 'activity_regularizer': None, 'kernel_constraint': None, 'recurrent_constraint': None, 'bias_constraint': None, 'dropout': 0.0, 'recurrent_dropout': 0.0, 'seed': None}, 'registered_name': None, 'build_config': {'input_shape': (None, 32, 10)}, 'name': 'lstm_2', 'inbound_nodes': [{'args': ({'class_name': '__keras_tensor__', 'config': {'shape': (None, 32, 10), 'dtype': 'float32', 'keras_history': ['input_layer_2', 0, 0]}},), 'kwargs': {'training': False, 'mask': None}}]}, {'module': 'keras.layers', 'class_name': 'LSTM', 'config': {'name': 'lstm_3', 'trainable': True, 'dtype': {'module': 'keras', 'class_name': 'DTypePolicy', 'config': {'name': 'float32'}, 'registered_name': None}, 'return_sequences': False, 'return_state': False, 'go_backwards': False, 'stateful': False, 'unroll': False, 'zero_output_for_mask': False, 'units': 50, 'activation': 'relu', 'recurrent_activation': 'sigmoid', 'use_bias': True, 'kernel_initializer': {'module': 'keras.initializers', 'class_name': 'GlorotUniform', 'config': {'seed': None}, 'registered_name': None}, 'recurrent_initializer': {'module': 'keras.initializers', 'class_name': 'OrthogonalInitializer', 'config': {'gain': 1.0, 'seed': None}, 'registered_name': None}, 'bias_initializer': {'module': 'keras.initializers', 'class_name': 'Zeros', 'config': {}, 'registered_name': None}, 'unit_forget_bias': True, 'kernel_regularizer': None, 'recurrent_regularizer': None, 'bias_regularizer': None, 'activity_regularizer': None, 'kernel_constraint': None, 'recurrent_constraint': None, 'bias_constraint': None, 'dropout': 0.0, 'recurrent_dropout': 0.0, 'seed': None}, 'registered_name': None, 'build_config': {'input_shape': (None, 32, 100)}, 'name': 'lstm_3', 'inbound_nodes': [{'args': ({'class_name': '__keras_tensor__', 'config': {'shape': (None, 32, 100), 'dtype': 'float32', 'keras_history': ['lstm_2', 0, 0]}},), 'kwargs': {'training': False, 'mask': None}}]}, {'module': 'keras.layers', 'class_name': 'Dense', 'config': {'name': 'dense_4', 'trainable': True, 'dtype': {'module': 'keras', 'class_name': 'DTypePolicy', 'config': {'name': 'float32'}, 'registered_name': None}, 'units': 64, 'activation': 'relu', 'use_bias': True, 'kernel_initializer': {'module': 'keras.initializers', 'class_name': 'GlorotUniform', 'config': {'seed': None}, 'registered_name': None}, 'bias_initializer': {'module': 'keras.initializers', 'class_name': 'Zeros', 'config': {}, 'registered_name': None}, 'kernel_regularizer': None, 'bias_regularizer': None, 'kernel_constraint': None, 'bias_constraint': None}, 'registered_name': None, 'build_config': {'input_shape': (None, 50)}, 'name': 'dense_4', 'inbound_nodes': [{'args': ({'class_name': '__keras_tensor__', 'config': {'shape': (None, 50), 'dtype': 'float32', 'keras_history': ['lstm_3', 0, 0]}},), 'kwargs': {}}]}, {'module': 'keras.layers', 'class_name': 'Dense', 'config': {'name': 'dense_5', 'trainable': True, 'dtype': {'module': 'keras', 'class_name': 'DTypePolicy', 'config': {'name': 'float32'}, 'registered_name': None}, 'units': 32, 'activation': 'relu', 'use_bias': True, 'kernel_initializer': {'module': 'keras.initializers', 'class_name': 'GlorotUniform', 'config': {'seed': None}, 'registered_name': None}, 'bias_initializer': {'module': 'keras.initializers', 'class_name': 'Zeros', 'config': {}, 'registered_name': None}, 'kernel_regularizer': None, 'bias_regularizer': None, 'kernel_constraint': None, 'bias_constraint': None}, 'registered_name': None, 'build_config': {'input_shape': (None, 64)}, 'name': 'dense_5', 'inbound_nodes': [{'args': ({'class_name': '__keras_tensor__', 'config': {'shape': (None, 64), 'dtype': 'float32', 'keras_history': ['dense_4', 0, 0]}},), 'kwargs': {}}]}, {'module': 'keras.layers', 'class_name': 'Dense', 'config': {'name': 'dense_6', 'trainable': True, 'dtype': {'module': 'keras', 'class_name': 'DTypePolicy', 'config': {'name': 'float32'}, 'registered_name': None}, 'units': 5, 'activation': 'linear', 'use_bias': True, 'kernel_initializer': {'module': 'keras.initializers', 'class_name': 'GlorotUniform', 'config': {'seed': None}, 'registered_name': None}, 'bias_initializer': {'module': 'keras.initializers', 'class_name': 'Zeros', 'config': {}, 'registered_name': None}, 'kernel_regularizer': None, 'bias_regularizer': None, 'kernel_constraint': None, 'bias_constraint': None}, 'registered_name': None, 'build_config': {'input_shape': (None, 32)}, 'name': 'dense_6', 'inbound_nodes': [{'args': ({'class_name': '__keras_tensor__', 'config': {'shape': (None, 32), 'dtype': 'float32', 'keras_history': ['dense_5', 0, 0]}},), 'kwargs': {}}]}, {'module': 'keras.layers', 'class_name': 'Reshape', 'config': {'name': 'reshape_2', 'trainable': True, 'dtype': {'module': 'keras', 'class_name': 'DTypePolicy', 'config': {'name': 'float32'}, 'registered_name': None}, 'target_shape': (5, 1)}, 'registered_name': None, 'build_config': {'input_shape': (None, 5)}, 'name': 'reshape_2', 'inbound_nodes': [{'args': ({'class_name': '__keras_tensor__', 'config': {'shape': (None, 5), 'dtype': 'float32', 'keras_history': ['dense_6', 0, 0]}},), 'kwargs': {}}]}], 'input_layers': [['input_layer_2', 0, 0]], 'output_layers': [['reshape_2', 0, 0]]} 
Compile parameters: {'optimizer': {'module': 'keras.optimizers', 'class_name': 'Adam', 'config': {'name': 'adam', 'learning_rate': 0.009999999776482582, 'weight_decay': None, 'clipnorm': None, 'global_clipnorm': None, 'clipvalue': None, 'use_ema': False, 'ema_momentum': 0.99, 'ema_overwrite_frequency': None, 'loss_scale_factor': None, 'gradient_accumulation_steps': None, 'beta_1': 0.9, 'beta_2': 0.999, 'epsilon': 1e-07, 'amsgrad': False}, 'registered_name': None}, 'loss': {'module': 'keras.losses', 'class_name': 'MeanSquaredError', 'config': {'name': 'mean_squared_error', 'reduction': 'sum_over_batch_size'}, 'registered_name': None}, 'loss_weights': None, 'metrics': None, 'weighted_metrics': None, 'run_eagerly': False, 'steps_per_execution': 1, 'jit_compile': False} 
fit_kwargs: {'epochs': 4, 'batch_size': 128} 
Creation date: 2024-08-08 09:29:51 
Last fit date: None 
Skforecast version: 0.13.0 
Python version: 3.12.4 
Forecaster id: None 
In [30]:
# Entrenamiento del Modelo
# ==============================================================================
forecaster.fit(data_train)
Epoch 1/4
154/154 ━━━━━━━━━━━━━━━━━━━━ 16s 77ms/step - loss: 1.3716 - val_loss: 0.0245
Epoch 2/4
154/154 ━━━━━━━━━━━━━━━━━━━━ 10s 66ms/step - loss: 0.0158 - val_loss: 0.0175
Epoch 3/4
154/154 ━━━━━━━━━━━━━━━━━━━━ 10s 65ms/step - loss: 0.0119 - val_loss: 0.0131
Epoch 4/4
154/154 ━━━━━━━━━━━━━━━━━━━━ 10s 65ms/step - loss: 0.0110 - val_loss: 0.0137
In [31]:
# Seguimiento del entrenamiento y overfitting
# ==============================================================================
fig, ax = plt.subplots(figsize=(5, 2.5))
forecaster.plot_history(ax=ax)
In [32]:
# Predicción
# ==============================================================================
predictions = forecaster.predict()
predictions
Out[32]:
o3
2021-04-01 00:00:00 53.205566
2021-04-01 01:00:00 51.525543
2021-04-01 02:00:00 46.486347
2021-04-01 03:00:00 39.784966
2021-04-01 04:00:00 35.652725
In [33]:
# Backtesting con datos de test
# ==============================================================================
metrics, predictions = backtesting_forecaster_multiseries(
    forecaster=forecaster,
    steps=forecaster.max_step,
    series=data,
    levels=forecaster.levels,
    initial_train_size=len(data.loc[:end_validation, :]), # Datos de entrenamiento + validación
    metric="mean_absolute_error",
    verbose=False,
    refit=False,
)
Epoch 1/4
188/188 ━━━━━━━━━━━━━━━━━━━━ 14s 66ms/step - loss: 0.0105 - val_loss: 0.0115
Epoch 2/4
188/188 ━━━━━━━━━━━━━━━━━━━━ 12s 64ms/step - loss: 0.0101 - val_loss: 0.0112
Epoch 3/4
188/188 ━━━━━━━━━━━━━━━━━━━━ 12s 64ms/step - loss: 0.0101 - val_loss: 0.0116
Epoch 4/4
188/188 ━━━━━━━━━━━━━━━━━━━━ 12s 64ms/step - loss: 0.0096 - val_loss: 0.0108
In [34]:
# Métricas de error de backtesting
# ==============================================================================
metrics
Out[34]:
levels mean_absolute_error
0 o3 10.802595
In [35]:
# Error mse en % respecto a la media de la serie
# ==============================================================================
rel_mse = 100 * metrics.loc[0, 'mean_absolute_error'] / np.mean(data["o3"])
print(f"Media de la serie: {np.mean(data['o3']):0.2f}")
print(f"Error mse relativo: {rel_mse:0.2f} %")
Media de la serie: 54.52
Error mse relativo: 19.81 %
In [36]:
# Predicciones de backtesting
# ==============================================================================
predictions
Out[36]:
o3
2021-10-01 00:00:00 51.704315
2021-10-01 01:00:00 48.319477
2021-10-01 02:00:00 42.242672
2021-10-01 03:00:00 36.411221
2021-10-01 04:00:00 31.373882
... ...
2021-12-31 19:00:00 23.917957
2021-12-31 20:00:00 20.650944
2021-12-31 21:00:00 6.379282
2021-12-31 22:00:00 9.216523
2021-12-31 23:00:00 6.723901

2208 rows × 1 columns

In [37]:
# Gráfico de las predicciones vs valores reales en el conjunto de test
# ==============================================================================
fig = go.Figure()
trace1 = go.Scatter(x=data_test.index, y=data_test['o3'], name="test", mode="lines")
trace2 = go.Scatter(x=predictions.index, y=predictions['o3'], name="predicciones", mode="lines")
fig.add_trace(trace1)
fig.add_trace(trace2)
fig.update_layout(
    title="Predicciones vs valores reales en el conjunto de test",
    xaxis_title="Date time",
    yaxis_title="O3",
    width=750,
    height=350,
    margin=dict(l=20, r=20, t=35, b=20),
    legend=dict(
        orientation="h",
        yanchor="top",
        y=1.05,
        xanchor="left",
        x=0
    )
)
fig.show()

Al utilizar múltiples series temporales como predictores cabría esperar que el modelo fuera capaz de predecir mejor la serie objetivo. Sin embargo, en este caso las predicciones son peores que en el caso anterior en el que solo se utilizaba una serie temporal como predictor. Esto puede deberse a que a que las series temporales utilizadas como predictores no están relacionadas con la serie objetivo. Por lo tanto, el modelo no es capaz de aprender ninguna relación entre ellas.

Problemas N:M - Series temporales múltiples con salidas múltiples

El siguiente y último escenario, consiste en predecir múltiples series temporales utilizando múltiples series temporales como predictores. Se trata, por lo tanto, de un escenario en el que se modelan múltiples series simultaneamente utilizando un único modelo. Esto tiene especial aplicación en muchos escenarios reales, como por ejemplo, la predicción de valores en bolsa de varias empresas en función del histórico de la bolsa, del precio de la energía y materias primas. O el caso del forecasting de múltiples productos en una tienda online, en función de las ventas de otros productos, el precio de los productos, etc.

In [38]:
# Creación del modelo
# ==============================================================================
series = ['pm2.5', 'co', 'no', 'no2', 'pm10', 'nox', 'o3', 'veloc.', 'direc.', 'so2'] 
levels = ['pm2.5', 'co', 'no', "o3"] # Características a predecir. Pueden ser todas las serires o menos
lags = 32 # Valores pasados a utilizar en la predicción
steps = 5 # Pasos a futuro a predecir

data = air_quality[series].copy()
data_train = air_quality_train[series].copy()
data_val = air_quality_val[series].copy()
data_test = air_quality_test[series].copy()

model = create_and_compile_model(
    series=data_train,
    levels=levels, 
    lags=lags,
    steps=steps,
    recurrent_layer="LSTM",
    recurrent_units=[100, 50],
    dense_units=[64, 32],
    optimizer=Adam(learning_rate=0.01), 
    loss=MeanSquaredError()
)
model.summary()
keras version: 3.4.1
Using backend: tensorflow
tensorflow version: 2.17.0
Model: "functional_3"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type)                     Output Shape                  Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ input_layer_3 (InputLayer)      │ (None, 32, 10)         │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ lstm_4 (LSTM)                   │ (None, 32, 100)        │        44,400 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ lstm_5 (LSTM)                   │ (None, 50)             │        30,200 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_7 (Dense)                 │ (None, 64)             │         3,264 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_8 (Dense)                 │ (None, 32)             │         2,080 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_9 (Dense)                 │ (None, 20)             │           660 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ reshape_3 (Reshape)             │ (None, 5, 4)           │             0 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 80,604 (314.86 KB)
 Trainable params: 80,604 (314.86 KB)
 Non-trainable params: 0 (0.00 B)
In [39]:
# Creación del forecaster
# ==============================================================================
forecaster = ForecasterRnn(
    regressor=model,
    levels=levels,
    steps=steps,
    lags=lags,
    transformer_series=MinMaxScaler(),
    fit_kwargs={
        "epochs": 100,  # Número de épocas para entrenar el modelo.
        "batch_size": 128,  # Tamaño del batch para entrenar el modelo.
        "callbacks": [
            EarlyStopping(monitor="val_loss", patience=5)
        ],  # Callback para detener el entrenamiento cuando ya no esté aprendiendo más.
        "series_val": data_val,  # Datos de validación para el entrenamiento del modelo.
    },
)
forecaster
Out[39]:
============= 
ForecasterRnn 
============= 
Regressor: <Functional name=functional_3, built=True> 
Lags: [ 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
 25 26 27 28 29 30 31 32] 
Transformer for series: MinMaxScaler() 
Window size: 32 
Target series, levels: ['pm2.5', 'co', 'no', 'o3'] 
Multivariate series (names): None 
Maximum steps predicted: [1 2 3 4 5] 
Training range: None 
Training index type: None 
Training index frequency: None 
Model parameters: {'name': 'functional_3', 'trainable': True, 'layers': [{'module': 'keras.layers', 'class_name': 'InputLayer', 'config': {'batch_shape': (None, 32, 10), 'dtype': 'float32', 'sparse': False, 'name': 'input_layer_3'}, 'registered_name': None, 'name': 'input_layer_3', 'inbound_nodes': []}, {'module': 'keras.layers', 'class_name': 'LSTM', 'config': {'name': 'lstm_4', 'trainable': True, 'dtype': {'module': 'keras', 'class_name': 'DTypePolicy', 'config': {'name': 'float32'}, 'registered_name': None}, 'return_sequences': True, 'return_state': False, 'go_backwards': False, 'stateful': False, 'unroll': False, 'zero_output_for_mask': False, 'units': 100, 'activation': 'relu', 'recurrent_activation': 'sigmoid', 'use_bias': True, 'kernel_initializer': {'module': 'keras.initializers', 'class_name': 'GlorotUniform', 'config': {'seed': None}, 'registered_name': None}, 'recurrent_initializer': {'module': 'keras.initializers', 'class_name': 'OrthogonalInitializer', 'config': {'gain': 1.0, 'seed': None}, 'registered_name': None}, 'bias_initializer': {'module': 'keras.initializers', 'class_name': 'Zeros', 'config': {}, 'registered_name': None}, 'unit_forget_bias': True, 'kernel_regularizer': None, 'recurrent_regularizer': None, 'bias_regularizer': None, 'activity_regularizer': None, 'kernel_constraint': None, 'recurrent_constraint': None, 'bias_constraint': None, 'dropout': 0.0, 'recurrent_dropout': 0.0, 'seed': None}, 'registered_name': None, 'build_config': {'input_shape': (None, 32, 10)}, 'name': 'lstm_4', 'inbound_nodes': [{'args': ({'class_name': '__keras_tensor__', 'config': {'shape': (None, 32, 10), 'dtype': 'float32', 'keras_history': ['input_layer_3', 0, 0]}},), 'kwargs': {'training': False, 'mask': None}}]}, {'module': 'keras.layers', 'class_name': 'LSTM', 'config': {'name': 'lstm_5', 'trainable': True, 'dtype': {'module': 'keras', 'class_name': 'DTypePolicy', 'config': {'name': 'float32'}, 'registered_name': None}, 'return_sequences': False, 'return_state': False, 'go_backwards': False, 'stateful': False, 'unroll': False, 'zero_output_for_mask': False, 'units': 50, 'activation': 'relu', 'recurrent_activation': 'sigmoid', 'use_bias': True, 'kernel_initializer': {'module': 'keras.initializers', 'class_name': 'GlorotUniform', 'config': {'seed': None}, 'registered_name': None}, 'recurrent_initializer': {'module': 'keras.initializers', 'class_name': 'OrthogonalInitializer', 'config': {'gain': 1.0, 'seed': None}, 'registered_name': None}, 'bias_initializer': {'module': 'keras.initializers', 'class_name': 'Zeros', 'config': {}, 'registered_name': None}, 'unit_forget_bias': True, 'kernel_regularizer': None, 'recurrent_regularizer': None, 'bias_regularizer': None, 'activity_regularizer': None, 'kernel_constraint': None, 'recurrent_constraint': None, 'bias_constraint': None, 'dropout': 0.0, 'recurrent_dropout': 0.0, 'seed': None}, 'registered_name': None, 'build_config': {'input_shape': (None, 32, 100)}, 'name': 'lstm_5', 'inbound_nodes': [{'args': ({'class_name': '__keras_tensor__', 'config': {'shape': (None, 32, 100), 'dtype': 'float32', 'keras_history': ['lstm_4', 0, 0]}},), 'kwargs': {'training': False, 'mask': None}}]}, {'module': 'keras.layers', 'class_name': 'Dense', 'config': {'name': 'dense_7', 'trainable': True, 'dtype': {'module': 'keras', 'class_name': 'DTypePolicy', 'config': {'name': 'float32'}, 'registered_name': None}, 'units': 64, 'activation': 'relu', 'use_bias': True, 'kernel_initializer': {'module': 'keras.initializers', 'class_name': 'GlorotUniform', 'config': {'seed': None}, 'registered_name': None}, 'bias_initializer': {'module': 'keras.initializers', 'class_name': 'Zeros', 'config': {}, 'registered_name': None}, 'kernel_regularizer': None, 'bias_regularizer': None, 'kernel_constraint': None, 'bias_constraint': None}, 'registered_name': None, 'build_config': {'input_shape': (None, 50)}, 'name': 'dense_7', 'inbound_nodes': [{'args': ({'class_name': '__keras_tensor__', 'config': {'shape': (None, 50), 'dtype': 'float32', 'keras_history': ['lstm_5', 0, 0]}},), 'kwargs': {}}]}, {'module': 'keras.layers', 'class_name': 'Dense', 'config': {'name': 'dense_8', 'trainable': True, 'dtype': {'module': 'keras', 'class_name': 'DTypePolicy', 'config': {'name': 'float32'}, 'registered_name': None}, 'units': 32, 'activation': 'relu', 'use_bias': True, 'kernel_initializer': {'module': 'keras.initializers', 'class_name': 'GlorotUniform', 'config': {'seed': None}, 'registered_name': None}, 'bias_initializer': {'module': 'keras.initializers', 'class_name': 'Zeros', 'config': {}, 'registered_name': None}, 'kernel_regularizer': None, 'bias_regularizer': None, 'kernel_constraint': None, 'bias_constraint': None}, 'registered_name': None, 'build_config': {'input_shape': (None, 64)}, 'name': 'dense_8', 'inbound_nodes': [{'args': ({'class_name': '__keras_tensor__', 'config': {'shape': (None, 64), 'dtype': 'float32', 'keras_history': ['dense_7', 0, 0]}},), 'kwargs': {}}]}, {'module': 'keras.layers', 'class_name': 'Dense', 'config': {'name': 'dense_9', 'trainable': True, 'dtype': {'module': 'keras', 'class_name': 'DTypePolicy', 'config': {'name': 'float32'}, 'registered_name': None}, 'units': 20, 'activation': 'linear', 'use_bias': True, 'kernel_initializer': {'module': 'keras.initializers', 'class_name': 'GlorotUniform', 'config': {'seed': None}, 'registered_name': None}, 'bias_initializer': {'module': 'keras.initializers', 'class_name': 'Zeros', 'config': {}, 'registered_name': None}, 'kernel_regularizer': None, 'bias_regularizer': None, 'kernel_constraint': None, 'bias_constraint': None}, 'registered_name': None, 'build_config': {'input_shape': (None, 32)}, 'name': 'dense_9', 'inbound_nodes': [{'args': ({'class_name': '__keras_tensor__', 'config': {'shape': (None, 32), 'dtype': 'float32', 'keras_history': ['dense_8', 0, 0]}},), 'kwargs': {}}]}, {'module': 'keras.layers', 'class_name': 'Reshape', 'config': {'name': 'reshape_3', 'trainable': True, 'dtype': {'module': 'keras', 'class_name': 'DTypePolicy', 'config': {'name': 'float32'}, 'registered_name': None}, 'target_shape': (5, 4)}, 'registered_name': None, 'build_config': {'input_shape': (None, 20)}, 'name': 'reshape_3', 'inbound_nodes': [{'args': ({'class_name': '__keras_tensor__', 'config': {'shape': (None, 20), 'dtype': 'float32', 'keras_history': ['dense_9', 0, 0]}},), 'kwargs': {}}]}], 'input_layers': [['input_layer_3', 0, 0]], 'output_layers': [['reshape_3', 0, 0]]} 
Compile parameters: {'optimizer': {'module': 'keras.optimizers', 'class_name': 'Adam', 'config': {'name': 'adam', 'learning_rate': 0.009999999776482582, 'weight_decay': None, 'clipnorm': None, 'global_clipnorm': None, 'clipvalue': None, 'use_ema': False, 'ema_momentum': 0.99, 'ema_overwrite_frequency': None, 'loss_scale_factor': None, 'gradient_accumulation_steps': None, 'beta_1': 0.9, 'beta_2': 0.999, 'epsilon': 1e-07, 'amsgrad': False}, 'registered_name': None}, 'loss': {'module': 'keras.losses', 'class_name': 'MeanSquaredError', 'config': {'name': 'mean_squared_error', 'reduction': 'sum_over_batch_size'}, 'registered_name': None}, 'loss_weights': None, 'metrics': None, 'weighted_metrics': None, 'run_eagerly': False, 'steps_per_execution': 1, 'jit_compile': False} 
fit_kwargs: {'epochs': 100, 'batch_size': 128, 'callbacks': [<keras.src.callbacks.early_stopping.EarlyStopping object at 0x7f2164cb7da0>]} 
Creation date: 2024-08-08 09:32:14 
Last fit date: None 
Skforecast version: 0.13.0 
Python version: 3.12.4 
Forecaster id: None 

Se entrena el modelo durante 100 épocas con un callback de EarlyStopping que detiene el entrenamiento cuando la pérdida de validación deja de disminuir durante 5 épocas (patience=5).

Warning

El entrenamiento del modelo dura aproximadamente 3 minutos en un ordenador con 8 cores, y el EarlyStopping detiene el entrenamiento en la época 11. Estos resultados pueden variar en función del hardware utilizado.

In [40]:
# Entrenamiento del forecaster
# ==============================================================================
forecaster.fit(data_train)
Epoch 1/100
154/154 ━━━━━━━━━━━━━━━━━━━━ 14s 68ms/step - loss: 0.0151 - val_loss: 0.0105
Epoch 2/100
154/154 ━━━━━━━━━━━━━━━━━━━━ 10s 65ms/step - loss: 0.0048 - val_loss: 0.0089
Epoch 3/100
154/154 ━━━━━━━━━━━━━━━━━━━━ 10s 65ms/step - loss: 0.0041 - val_loss: 0.0088
Epoch 4/100
154/154 ━━━━━━━━━━━━━━━━━━━━ 10s 65ms/step - loss: 0.0040 - val_loss: 0.0085
Epoch 5/100
154/154 ━━━━━━━━━━━━━━━━━━━━ 10s 65ms/step - loss: 0.0038 - val_loss: 0.0084
Epoch 6/100
154/154 ━━━━━━━━━━━━━━━━━━━━ 10s 65ms/step - loss: 0.0037 - val_loss: 0.0082
Epoch 7/100
154/154 ━━━━━━━━━━━━━━━━━━━━ 10s 65ms/step - loss: 0.0035 - val_loss: 0.0070
Epoch 8/100
154/154 ━━━━━━━━━━━━━━━━━━━━ 10s 65ms/step - loss: 0.0033 - val_loss: 0.0071
Epoch 9/100
154/154 ━━━━━━━━━━━━━━━━━━━━ 10s 65ms/step - loss: 0.0033 - val_loss: 0.0071
Epoch 10/100
154/154 ━━━━━━━━━━━━━━━━━━━━ 10s 65ms/step - loss: 0.0032 - val_loss: 0.0071
Epoch 11/100
154/154 ━━━━━━━━━━━━━━━━━━━━ 10s 65ms/step - loss: 0.0031 - val_loss: 0.0072
Epoch 12/100
154/154 ━━━━━━━━━━━━━━━━━━━━ 10s 65ms/step - loss: 0.0031 - val_loss: 0.0069
Epoch 13/100
154/154 ━━━━━━━━━━━━━━━━━━━━ 10s 65ms/step - loss: 0.0030 - val_loss: 0.0070
Epoch 14/100
154/154 ━━━━━━━━━━━━━━━━━━━━ 10s 65ms/step - loss: 0.0030 - val_loss: 0.0070
Epoch 15/100
154/154 ━━━━━━━━━━━━━━━━━━━━ 10s 65ms/step - loss: 0.0030 - val_loss: 0.0068
Epoch 16/100
154/154 ━━━━━━━━━━━━━━━━━━━━ 10s 65ms/step - loss: 0.0030 - val_loss: 0.0073
Epoch 17/100
154/154 ━━━━━━━━━━━━━━━━━━━━ 10s 65ms/step - loss: 0.0029 - val_loss: 0.0072
Epoch 18/100
154/154 ━━━━━━━━━━━━━━━━━━━━ 10s 66ms/step - loss: 0.0028 - val_loss: 0.0071
Epoch 19/100
154/154 ━━━━━━━━━━━━━━━━━━━━ 10s 65ms/step - loss: 0.0028 - val_loss: 0.0073
Epoch 20/100
154/154 ━━━━━━━━━━━━━━━━━━━━ 10s 65ms/step - loss: 0.0028 - val_loss: 0.0075
In [41]:
# Seguimiento del entrenamiento y overfitting
# ==============================================================================
fig, ax = plt.subplots(figsize=(5, 2.5))
forecaster.plot_history(ax=ax)

Se observan inicios de overfitting a partir de la época 11. Esto se puede deber a que el modelo es muy complejo para el problema que se está tratando de resolver. Gracias a el callback de Keras, el modelo se detiene en la época 16, evitando así que el modelo entre más en overfitting. Una buena práctica sería modificar la arquitectura del modelo para evitar el overfitting. Por ejemplo, se podría reducir el número de neuronas en las capas recurrentes y densas, o añadir una capa de dropout para regularizar el modelo.

Cuando el forecaster modela múltiples series temporales, por defecto, las predicciones se calculan para todas ellas.

In [42]:
# Predicción
# ==============================================================================
predictions = forecaster.predict()
predictions
Out[42]:
pm2.5 co no o3
2021-04-01 00:00:00 12.266407 0.122383 1.935437 39.916119
2021-04-01 01:00:00 11.731569 0.118779 1.469646 41.193314
2021-04-01 02:00:00 11.075222 0.117986 2.539756 39.170151
2021-04-01 03:00:00 10.109291 0.116276 2.837049 37.890045
2021-04-01 04:00:00 9.273896 0.113071 2.554243 39.002895

Tamibén se pueden predecir steps especificos, siempre y cuado se encuentren dentro del horizonte de predicción definido en el modelo, para series temporales concretas.

In [43]:
# Predicción de steps especificos (1 y 5) para la serie o3
# ==============================================================================
forecaster.predict(steps=[1, 5], levels="o3")
Out[43]:
o3
2021-04-01 00:00:00 39.916119
2021-04-01 04:00:00 39.002895
In [44]:
# Backtesting con datos de test
# ==============================================================================
metrics, predictions = backtesting_forecaster_multiseries(
    forecaster=forecaster,
    steps=forecaster.max_step,
    series=data,
    levels=forecaster.levels,
    initial_train_size=len(data.loc[:end_validation, :]),
    metric="mean_absolute_error",
    verbose=False,
    refit=False,
)
Epoch 1/100
188/188 ━━━━━━━━━━━━━━━━━━━━ 15s 67ms/step - loss: 0.0028 - val_loss: 0.0076
Epoch 2/100
188/188 ━━━━━━━━━━━━━━━━━━━━ 12s 64ms/step - loss: 0.0027 - val_loss: 0.0071
Epoch 3/100
188/188 ━━━━━━━━━━━━━━━━━━━━ 12s 64ms/step - loss: 0.0027 - val_loss: 0.0073
Epoch 4/100
188/188 ━━━━━━━━━━━━━━━━━━━━ 12s 65ms/step - loss: 0.0026 - val_loss: 0.0070
Epoch 5/100
188/188 ━━━━━━━━━━━━━━━━━━━━ 12s 64ms/step - loss: 0.0027 - val_loss: 0.0074
In [45]:
# Métricas de error de backtesting para cada serie
# ==============================================================================
metrics
Out[45]:
levels mean_absolute_error
0 pm2.5 3.750846
1 co 0.025974
2 no 2.960482
3 o3 13.134379
4 average 4.967920
5 weighted_average 4.967920
6 pooling 4.967920
In [46]:
# Gráfico de las predicciones vs valores reales en el conjunto de test
# =============================================================================
fig = px.line(
    data_frame = pd.concat([
                    predictions.melt(ignore_index=False).assign(group="predicciones"),
                    data_test[predictions.columns].melt(ignore_index=False).assign(group="test")
                ]).reset_index().rename(columns={"index": "date_time"}),
    x="date_time",
    y="value",
    facet_row="variable",
    color="group",
    title="Predicciones vs valores reales en el conjunto de test"
)

fig.update_layout(
    title="Predicciones vs valores reales en el conjunto de test",
    width=750,
    height=850,
    margin=dict(l=20, r=20, t=35, b=20),
    legend=dict(
        orientation="h",
        yanchor="top",
        y=1,
        xanchor="left",
        x=0
    )
)

fig.update_yaxes(matches=None)

Los resultados de backtesting muestran que el modelo es capaz de captar el patrón de las series temporales pm2.5 y O3, pero no el de las series CO y NO. Esto puede deberse a que las primeras tienen un mayor comportamiento autoregresivo, mientras que los valores futuros de las segundas no parecen depender de los valores pasados.

De nuevo, se observa que el error de backtesting para la serie O3 es mayor que el obtenido cuando se modelaba únicamente esa serie. Esto puede deberse a que el modelo está intentando modelar múltiples series temporales a la vez, lo que hace que el problema sea más complejo.

Conclusion

  • Las redes neuronales recurrentes permiten resolver una amplia variedad de problemas de forecasting.

  • En el caso 1:1 y N:1, el modelo es capaz de aprender patrones de la serie temporal y predecir valores futuros con un error relativo respecto a la media proximo al 5.8%.

  • En el caso N:M, el modelo predice algunas de las series temporales con un error mayor que en los casos anteriores. Esto puede deberse a que algunas series temporales son más difíciles de predecir que otras, o simplemente que el modelo no es lo suficientemente bueno para el problema que se está tratando de resolver.

  • Los modelos de deep learning tienen altos requerimeintos computacionales.

  • Para conseguir un buen modelo de deep learning es necesario encontrar la arquitectura adecuada, lo que requiere de conocimiento y experiencia.

  • Cuantas más series se modelen, más fácil es que el modelo aprenda las relaciones entre las series pero puede perder precision en la predicción individual de cada una de ellas.

  • El uso de skforecast permite simplificar el proceso de modelado y acelerar el proceso de prototipado y desarrollo.

Información de sesión

In [47]:
# Información de la sesión
import session_info
session_info.show(html=False)
-----
keras               3.4.1
matplotlib          3.9.1
numpy               1.26.4
pandas              2.2.2
plotly              5.23.0
session_info        1.0.0
skforecast          0.13.0
sklearn             1.5.1
tensorflow          2.17.0
-----
IPython             8.26.0
jupyter_client      8.6.2
jupyter_core        5.7.2
notebook            6.4.12
-----
Python 3.12.4 | packaged by Anaconda, Inc. | (main, Jun 18 2024, 15:12:24) [GCC 11.2.0]
Linux-5.15.0-1066-aws-x86_64-with-glibc2.31
-----
Session information updated at 2024-08-08 09:37

Bibliografía

Dive into Deep Learning. Zhang, Aston and Lipton, Zachary C. and Li, Mu and Smola, Alexander J. (2023). Cambridge University Press. https://D2L.ai

© 2024 Codificando Bits https://www.codificandobits.com/blog/redes-neuronales-recurrentes-explicacion-detallada/

Hyndman, R.J., & Athanasopoulos, G. (2021) Forecasting: principles and practice, 3rd edition, OTexts: Melbourne, Australia.

Time Series Analysis and Forecasting with ADAM Ivan Svetunkov.

Joseph, M. (2022). Modern time series forecasting with Python: Explore industry-ready time series forecasting using modern machine learning and Deep Learning. Packt Publishing.

Instrucciones para citar

¿Cómo citar este documento?

Si utilizas este documento o alguna parte de él, te agradecemos que lo cites. ¡Muchas gracias!

Deep Learning para la predicción de series temporales: Redes Neuronales Recurrentes (RNN) y Long Short-Term Memory (LSTM) por Fernando Carazo y Joaquín Amat Rodrigo, disponible bajo licencia Attribution-NonCommercial-ShareAlike 4.0 International en https://www.cienciadedatos.net/documentos/py54-forecasting-con-deep-learning.html

¿Cómo citar skforecast?

Zenodo:

Amat Rodrigo, Joaquin, & Escobar Ortiz, Javier. (2023). skforecast (v0.13.0). Zenodo. https://doi.org/10.5281/zenodo.8382788

APA:

Amat Rodrigo, J., & Escobar Ortiz, J. (2023). skforecast (Version 0.13.0) [Computer software]. https://doi.org/10.5281/zenodo.8382788

BibTeX:

@software{skforecast, author = {Amat Rodrigo, Joaquin and Escobar Ortiz, Javier}, title = {skforecast}, version = {0.13.0}, month = {8}, year = {2024}, license = {BSD-3-Clause}, url = {https://skforecast.org/}, doi = {10.5281/zenodo.8382788} }


¿Te ha gustado el artículo? Tu ayuda es importante

Mantener un sitio web tiene unos costes elevados, tu contribución me ayudará a seguir generando contenido divulgativo gratuito. ¡Muchísimas gracias! 😊


Creative Commons Licence
Este documento creado por Fernando Carazo y Joaquín Amat Rodrigo tiene licencia Attribution-NonCommercial-ShareAlike 4.0 International.

Se permite:

  • Compartir: copiar y redistribuir el material en cualquier medio o formato.

  • Adaptar: remezclar, transformar y crear a partir del material.

Bajo los siguientes términos:

  • Atribución: Debes otorgar el crédito adecuado, proporcionar un enlace a la licencia e indicar si se realizaron cambios. Puedes hacerlo de cualquier manera razonable, pero no de una forma que sugiera que el licenciante te respalda o respalda tu uso.

  • NoComercial: No puedes utilizar el material para fines comerciales.

  • CompartirIgual: Si remezclas, transformas o creas a partir del material, debes distribuir tus contribuciones bajo la misma licencia que el original.