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

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, TensorFlow y Skforecast.

  • Keras-TensorFlow proporciona, a traves de su modulo Keras, una interfaz amigable 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

In [49]:
# Procesado de datos
# ==============================================================================
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
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)

# Tensorflow and Keras
# ==============================================================================
import tensorflow
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.losses import MeanSquaredError
from tensorflow.keras.callbacks import EarlyStopping

# Modelado series temporales
# ==============================================================================
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')

print(f"skforecast version: {skforecast.__version__}")
print(f"tensorflow version: {tensorflow.__version__}")
skforecast version: 0.12.0
tensorflow version: 2.15.1
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
# ==============================================================================
fig, ax = plt.subplots(figsize=(8, 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()
Model: "model"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_1 (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 Byte)
Trainable params: 193 (772.00 Byte)
Non-trainable params: 0 (0.00 Byte)
_________________________________________________________________

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_12_py11/lib/python3.11/site-packages/skforecast/ForecasterRnn/ForecasterRnn.py:227: UserWarning:

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

/home/ubuntu/anaconda3/envs/skforecast_12_py11/lib/python3.11/site-packages/skforecast/ForecasterRnn/ForecasterRnn.py:257: UserWarning:

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

Out[8]:
============= 
ForecasterRnn 
============= 
Regressor: <keras.src.engine.functional.Functional object at 0x7f4f6c7070d0> 
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': 'model', 'trainable': True, 'layers': [{'module': 'keras.layers', 'class_name': 'InputLayer', 'config': {'batch_input_shape': (None, 32, 1), 'dtype': 'float32', 'sparse': False, 'ragged': False, 'name': 'input_1'}, 'registered_name': None, 'name': 'input_1', 'inbound_nodes': []}, {'module': 'keras.layers', 'class_name': 'LSTM', 'config': {'name': 'lstm', 'trainable': True, 'dtype': 'float32', 'return_sequences': False, 'return_state': False, 'go_backwards': False, 'stateful': False, 'unroll': False, 'time_major': 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': 'Orthogonal', '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, 'implementation': 2}, 'registered_name': None, 'build_config': {'input_shape': (None, 32, 1)}, 'name': 'lstm', 'inbound_nodes': [[['input_1', 0, 0, {}]]]}, {'module': 'keras.layers', 'class_name': 'Dense', 'config': {'name': 'dense', 'trainable': True, 'dtype': 'float32', '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, 'activity_regularizer': None, 'kernel_constraint': None, 'bias_constraint': None}, 'registered_name': None, 'build_config': {'input_shape': (None, 4)}, 'name': 'dense', 'inbound_nodes': [[['lstm', 0, 0, {}]]]}, {'module': 'keras.layers', 'class_name': 'Dense', 'config': {'name': 'dense_1', 'trainable': True, 'dtype': 'float32', '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, 'activity_regularizer': None, 'kernel_constraint': None, 'bias_constraint': None}, 'registered_name': None, 'build_config': {'input_shape': (None, 16)}, 'name': 'dense_1', 'inbound_nodes': [[['dense', 0, 0, {}]]]}, {'module': 'keras.layers', 'class_name': 'Reshape', 'config': {'name': 'reshape', 'trainable': True, 'dtype': 'float32', 'target_shape': (1, 1)}, 'registered_name': None, 'build_config': {'input_shape': (None, 1)}, 'name': 'reshape', 'inbound_nodes': [[['dense_1', 0, 0, {}]]]}], 'input_layers': [['input_1', 0, 0]], 'output_layers': [['reshape', 0, 0]]} 
Compile parameters: {'optimizer': {'module': 'keras.optimizers', 'class_name': 'Adam', 'config': {'name': 'Adam', 'weight_decay': None, 'clipnorm': None, 'global_clipnorm': None, 'clipvalue': None, 'use_ema': False, 'ema_momentum': 0.99, 'ema_overwrite_frequency': None, 'jit_compile': False, 'is_legacy_optimizer': False, 'learning_rate': 0.009999999776482582, 'beta_1': 0.9, 'beta_2': 0.999, 'epsilon': 1e-07, 'amsgrad': False}, 'registered_name': None}, 'loss': {'module': 'keras.losses', 'class_name': 'MeanSquaredError', 'config': {'reduction': 'auto', 'name': 'mean_squared_error', 'fn': 'mean_squared_error'}, 'registered_name': None}, 'metrics': None, 'loss_weights': None, 'weighted_metrics': None, 'run_eagerly': None, 'steps_per_execution': None, 'jit_compile': None} 
fit_kwargs: {'epochs': 10, 'batch_size': 32, 'callbacks': [<keras.src.callbacks.EarlyStopping object at 0x7f4f34a48150>]} 
Creation date: 2024-05-06 12:20:11 
Last fit date: None 
Skforecast version: 0.12.0 
Python version: 3.11.8 
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 15ms/step - loss: 0.0088 - val_loss: 0.0055
Epoch 2/10
615/615 [==============================] - 9s 14ms/step - loss: 0.0054 - val_loss: 0.0053
Epoch 3/10
615/615 [==============================] - 8s 14ms/step - loss: 0.0053 - val_loss: 0.0052
Epoch 4/10
615/615 [==============================] - 9s 14ms/step - loss: 0.0051 - val_loss: 0.0052
Epoch 5/10
615/615 [==============================] - 9s 14ms/step - loss: 0.0053 - val_loss: 0.0059
Epoch 6/10
615/615 [==============================] - 9s 14ms/step - loss: 0.0052 - val_loss: 0.0052
Epoch 7/10
615/615 [==============================] - 9s 14ms/step - loss: 0.0053 - val_loss: 0.0078
Epoch 8/10
615/615 [==============================] - 8s 14ms/step - loss: 0.0051 - val_loss: 0.0063
Epoch 9/10
615/615 [==============================] - 8s 14ms/step - loss: 0.0052 - val_loss: 0.0052
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 46.653629

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 [==============================] - 10s 12ms/step - loss: 0.0051 - val_loss: 0.0055
Epoch 2/10
752/752 [==============================] - 8s 11ms/step - loss: 0.0051 - val_loss: 0.0051
Epoch 3/10
752/752 [==============================] - 8s 11ms/step - loss: 0.0050 - val_loss: 0.0050
Epoch 4/10
752/752 [==============================] - 8s 11ms/step - loss: 0.0049 - val_loss: 0.0054
Epoch 5/10
752/752 [==============================] - 8s 11ms/step - loss: 0.0049 - val_loss: 0.0049
Epoch 6/10
752/752 [==============================] - 8s 11ms/step - loss: 0.0048 - val_loss: 0.0051
Epoch 7/10
752/752 [==============================] - 8s 11ms/step - loss: 0.0050 - val_loss: 0.0049
Epoch 8/10
752/752 [==============================] - 8s 11ms/step - loss: 0.0048 - val_loss: 0.0051
Epoch 9/10
752/752 [==============================] - 9s 12ms/step - loss: 0.0048 - val_loss: 0.0050
Epoch 10/10
752/752 [==============================] - 8s 11ms/step - loss: 0.0047 - val_loss: 0.0047
In [13]:
# Predicciones de backtesting
# ==============================================================================
predictions
Out[13]:
o3
2021-10-01 00:00:00 56.057663
2021-10-01 01:00:00 59.355564
2021-10-01 02:00:00 62.447186
2021-10-01 03:00:00 61.635063
2021-10-01 04:00:00 50.839520
... ...
2021-12-31 19:00:00 16.706299
2021-12-31 20:00:00 14.079927
2021-12-31 21:00:00 15.304923
2021-12-31 22:00:00 15.317797
2021-12-31 23:00:00 16.606569

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=800,
    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.587501
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.25 %

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()
Model: "model_1"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_2 (InputLayer)        [(None, 32, 1)]           0         
                                                                 
 lstm_1 (LSTM)               (None, 50)                10400     
                                                                 
 dense_2 (Dense)             (None, 32)                1632      
                                                                 
 dense_3 (Dense)             (None, 5)                 165       
                                                                 
 reshape_1 (Reshape)         (None, 5, 1)              0         
                                                                 
=================================================================
Total params: 12197 (47.64 KB)
Trainable params: 12197 (47.64 KB)
Non-trainable params: 0 (0.00 Byte)
_________________________________________________________________
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 [==============================] - 14s 21ms/step - loss: 0.0182 - val_loss: 0.0134
Epoch 2/10
615/615 [==============================] - 12s 20ms/step - loss: 0.0129 - val_loss: 0.0139
Epoch 3/10
615/615 [==============================] - 12s 20ms/step - loss: 0.0121 - val_loss: 0.0123
Epoch 4/10
615/615 [==============================] - 12s 20ms/step - loss: 0.0117 - val_loss: 0.0113
Epoch 5/10
615/615 [==============================] - 13s 20ms/step - loss: 0.0115 - val_loss: 0.0117
Epoch 6/10
615/615 [==============================] - 13s 21ms/step - loss: 0.0114 - val_loss: 0.0117
Epoch 7/10
615/615 [==============================] - 13s 21ms/step - loss: 0.0113 - val_loss: 0.0120
Epoch 8/10
615/615 [==============================] - 13s 21ms/step - loss: 0.0111 - val_loss: 0.0117
Epoch 9/10
615/615 [==============================] - 13s 21ms/step - loss: 0.0112 - val_loss: 0.0113
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.703911
2021-04-01 01:00:00 43.698830
2021-04-01 02:00:00 41.273136
2021-04-01 03:00:00 35.935780
2021-04-01 04:00:00 32.158550

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.703911
2021-04-01 02:00:00 41.273136
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 [==============================] - 17s 21ms/step - loss: 0.0110 - val_loss: 0.0108
Epoch 2/10
752/752 [==============================] - 15s 20ms/step - loss: 0.0110 - val_loss: 0.0116
Epoch 3/10
752/752 [==============================] - 15s 20ms/step - loss: 0.0109 - val_loss: 0.0110
Epoch 4/10
752/752 [==============================] - 15s 20ms/step - loss: 0.0108 - val_loss: 0.0120
Epoch 5/10
752/752 [==============================] - 15s 20ms/step - loss: 0.0108 - val_loss: 0.0107
Epoch 6/10
752/752 [==============================] - 15s 19ms/step - loss: 0.0106 - val_loss: 0.0106
Epoch 7/10
752/752 [==============================] - 15s 20ms/step - loss: 0.0107 - val_loss: 0.0128
Epoch 8/10
752/752 [==============================] - 15s 20ms/step - loss: 0.0106 - val_loss: 0.0109
Epoch 9/10
752/752 [==============================] - 15s 19ms/step - loss: 0.0106 - val_loss: 0.0109
Epoch 10/10
752/752 [==============================] - 15s 20ms/step - loss: 0.0105 - val_loss: 0.0115
In [24]:
# Predicciones de backtesting
# ==============================================================================
predictions
Out[24]:
o3
2021-10-01 00:00:00 52.867039
2021-10-01 01:00:00 51.072506
2021-10-01 02:00:00 47.524994
2021-10-01 03:00:00 45.858742
2021-10-01 04:00:00 42.227062
... ...
2021-12-31 19:00:00 17.402279
2021-12-31 20:00:00 14.372124
2021-12-31 21:00:00 11.699747
2021-12-31 22:00:00 13.389835
2021-12-31 23:00:00 13.597195

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=800,
    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.657512
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.71 %

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()
Model: "model_2"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_3 (InputLayer)        [(None, 32, 10)]          0         
                                                                 
 lstm_2 (LSTM)               (None, 32, 100)           44400     
                                                                 
 lstm_3 (LSTM)               (None, 50)                30200     
                                                                 
 dense_4 (Dense)             (None, 64)                3264      
                                                                 
 dense_5 (Dense)             (None, 32)                2080      
                                                                 
 dense_6 (Dense)             (None, 5)                 165       
                                                                 
 reshape_2 (Reshape)         (None, 5, 1)              0         
                                                                 
=================================================================
Total params: 80109 (312.93 KB)
Trainable params: 80109 (312.93 KB)
Non-trainable params: 0 (0.00 Byte)
_________________________________________________________________

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: <keras.src.engine.functional.Functional object at 0x7f4eef4cbdd0> 
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': 'model_2', 'trainable': True, 'layers': [{'module': 'keras.layers', 'class_name': 'InputLayer', 'config': {'batch_input_shape': (None, 32, 10), 'dtype': 'float32', 'sparse': False, 'ragged': False, 'name': 'input_3'}, 'registered_name': None, 'name': 'input_3', 'inbound_nodes': []}, {'module': 'keras.layers', 'class_name': 'LSTM', 'config': {'name': 'lstm_2', 'trainable': True, 'dtype': 'float32', 'return_sequences': True, 'return_state': False, 'go_backwards': False, 'stateful': False, 'unroll': False, 'time_major': 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': 'Orthogonal', '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, 'implementation': 2}, 'registered_name': None, 'build_config': {'input_shape': (None, 32, 10)}, 'name': 'lstm_2', 'inbound_nodes': [[['input_3', 0, 0, {}]]]}, {'module': 'keras.layers', 'class_name': 'LSTM', 'config': {'name': 'lstm_3', 'trainable': True, 'dtype': 'float32', 'return_sequences': False, 'return_state': False, 'go_backwards': False, 'stateful': False, 'unroll': False, 'time_major': 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': 'Orthogonal', '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, 'implementation': 2}, 'registered_name': None, 'build_config': {'input_shape': (None, 32, 100)}, 'name': 'lstm_3', 'inbound_nodes': [[['lstm_2', 0, 0, {}]]]}, {'module': 'keras.layers', 'class_name': 'Dense', 'config': {'name': 'dense_4', 'trainable': True, 'dtype': 'float32', '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, 'activity_regularizer': None, 'kernel_constraint': None, 'bias_constraint': None}, 'registered_name': None, 'build_config': {'input_shape': (None, 50)}, 'name': 'dense_4', 'inbound_nodes': [[['lstm_3', 0, 0, {}]]]}, {'module': 'keras.layers', 'class_name': 'Dense', 'config': {'name': 'dense_5', 'trainable': True, 'dtype': 'float32', '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, 'activity_regularizer': None, 'kernel_constraint': None, 'bias_constraint': None}, 'registered_name': None, 'build_config': {'input_shape': (None, 64)}, 'name': 'dense_5', 'inbound_nodes': [[['dense_4', 0, 0, {}]]]}, {'module': 'keras.layers', 'class_name': 'Dense', 'config': {'name': 'dense_6', 'trainable': True, 'dtype': 'float32', '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, 'activity_regularizer': None, 'kernel_constraint': None, 'bias_constraint': None}, 'registered_name': None, 'build_config': {'input_shape': (None, 32)}, 'name': 'dense_6', 'inbound_nodes': [[['dense_5', 0, 0, {}]]]}, {'module': 'keras.layers', 'class_name': 'Reshape', 'config': {'name': 'reshape_2', 'trainable': True, 'dtype': 'float32', 'target_shape': (5, 1)}, 'registered_name': None, 'build_config': {'input_shape': (None, 5)}, 'name': 'reshape_2', 'inbound_nodes': [[['dense_6', 0, 0, {}]]]}], 'input_layers': [['input_3', 0, 0]], 'output_layers': [['reshape_2', 0, 0]]} 
Compile parameters: {'optimizer': {'module': 'keras.optimizers', 'class_name': 'Adam', 'config': {'name': 'Adam', 'weight_decay': None, 'clipnorm': None, 'global_clipnorm': None, 'clipvalue': None, 'use_ema': False, 'ema_momentum': 0.99, 'ema_overwrite_frequency': None, 'jit_compile': False, 'is_legacy_optimizer': False, 'learning_rate': 0.009999999776482582, 'beta_1': 0.9, 'beta_2': 0.999, 'epsilon': 1e-07, 'amsgrad': False}, 'registered_name': None}, 'loss': {'module': 'keras.losses', 'class_name': 'MeanSquaredError', 'config': {'reduction': 'auto', 'name': 'mean_squared_error', 'fn': 'mean_squared_error'}, 'registered_name': None}, 'metrics': None, 'loss_weights': None, 'weighted_metrics': None, 'run_eagerly': None, 'steps_per_execution': None, 'jit_compile': None} 
fit_kwargs: {'epochs': 4, 'batch_size': 128} 
Creation date: 2024-05-06 12:31:46 
Last fit date: None 
Skforecast version: 0.12.0 
Python version: 3.11.8 
Forecaster id: None 
In [30]:
# Entrenamiento del Modelo
# ==============================================================================
forecaster.fit(data_train)
Epoch 1/4
154/154 [==============================] - 20s 110ms/step - loss: 0.0350 - val_loss: 0.0231
Epoch 2/4
154/154 [==============================] - 16s 102ms/step - loss: 0.0164 - val_loss: 0.0213
Epoch 3/4
154/154 [==============================] - 16s 105ms/step - loss: 0.0124 - val_loss: 0.0135
Epoch 4/4
154/154 [==============================] - 16s 107ms/step - loss: 0.0114 - val_loss: 0.0145
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 49.586761
2021-04-01 01:00:00 45.700237
2021-04-01 02:00:00 41.210724
2021-04-01 03:00:00 35.471233
2021-04-01 04:00:00 29.925587
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 [==============================] - 23s 109ms/step - loss: 0.0108 - val_loss: 0.0123
Epoch 2/4
188/188 [==============================] - 19s 103ms/step - loss: 0.0104 - val_loss: 0.0110
Epoch 3/4
188/188 [==============================] - 19s 104ms/step - loss: 0.0100 - val_loss: 0.0110
Epoch 4/4
188/188 [==============================] - 19s 102ms/step - loss: 0.0097 - val_loss: 0.0107
In [34]:
# Métricas de error de backtesting
# ==============================================================================
metrics
Out[34]:
levels mean_absolute_error
0 o3 10.313728
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: 18.92 %
In [36]:
# Predicciones de backtesting
# ==============================================================================
predictions
Out[36]:
o3
2021-10-01 00:00:00 49.175896
2021-10-01 01:00:00 47.345818
2021-10-01 02:00:00 43.658485
2021-10-01 03:00:00 38.892246
2021-10-01 04:00:00 33.842121
... ...
2021-12-31 19:00:00 19.217308
2021-12-31 20:00:00 12.979877
2021-12-31 21:00:00 4.234653
2021-12-31 22:00:00 7.419445
2021-12-31 23:00:00 8.199465

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=800,
    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()