Si te gusta Skforecast , ayúdanos dándonos una estrella en GitHub! ⭐️
Más sobre forecasting en: cienciadedatos.net
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:
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.
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.
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)
Problemas N:1 - Modelar una única serie utilizando múltiples series (multi-series, single-output)
Problemas N:M - Modelar múltiples series utilizando múltiples series (multi-series, multiple-outputs)
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.
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.
💡 Tip: Configuración del backend
Desde la versión0.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.# 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.deep_learning import ForecasterRnn
from skforecast.deep_learning.utils import create_and_compile_model
from sklearn.preprocessing import MinMaxScaler
from skforecast.model_selection import TimeSeriesFold
from skforecast.model_selection 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__}")
⚠ 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
# Descarga y procesado de datos
# ==============================================================================
air_quality = fetch_dataset(name="air_quality_valencia_no_missing")
air_quality.head()
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.
# Comprobación de índice y frecuencia
# ==============================================================================
print(f"Tipo de índice: {air_quality.index.dtype}")
print(f"Frecuencia: {air_quality.index.freq}")
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.
# Split train-validation-test
# ==============================================================================
air_quality = air_quality.loc[:'2021-12-31 23:00:00', :].copy()
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)})"
)
# 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();
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óncreate_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, ...).
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.
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.
# 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()
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.
# 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
⚠ 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 argumentolags
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
.
# Entrenamiento del forecaster
# ==============================================================================
forecaster.fit(data_train)
# 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.
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
).
# Predicción
# ==============================================================================
predictions = forecaster.predict()
predictions
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.
# Backtesting con datos de test
# ==============================================================================
cv = TimeSeriesFold(
steps=forecaster.max_step,
initial_train_size=len(data.loc[:end_validation, :]), # Datos de entrenamiento + validación
refit=False,
)
metrics, predictions = backtesting_forecaster_multiseries(
forecaster=forecaster,
series=data,
levels=forecaster.levels,
cv=cv,
metric="mean_absolute_error",
verbose=False,
)
# Predicciones de backtesting
# ==============================================================================
predictions
# 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()
# Métricas de backtesting
# ==============================================================================
metrics
# 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} %")
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.
# 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()
# 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.# Entrenamiento del forecaster
# ==============================================================================
forecaster.fit(data_train)
# 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
).
# Predicción
# ==============================================================================
predictions = forecaster.predict()
predictions
También se pueden predecir steps
especificos, siempre y cuando se encuentren dentro del horizonte de predicción definido en el modelo.
# Predicción steps especificos
# ==============================================================================
predictions = forecaster.predict(steps=[1, 3])
predictions
# Backtesting con datos de test
# ==============================================================================
cv = TimeSeriesFold(
steps=forecaster.max_step,
initial_train_size=len(data.loc[:end_validation, :]), # Datos de entrenamiento + validación
refit=False,
)
metrics, predictions = backtesting_forecaster_multiseries(
forecaster=forecaster,
series=data,
levels=forecaster.levels,
cv=cv,
metric="mean_absolute_error",
verbose=False,
)
# Predicciones de backtesting
# ==============================================================================
predictions
# 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()
# Métricas de backtesting
# ==============================================================================
metrics
# 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} %")
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.
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.
# 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()
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.
# 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
# Entrenamiento del Modelo
# ==============================================================================
forecaster.fit(data_train)
# Seguimiento del entrenamiento y overfitting
# ==============================================================================
fig, ax = plt.subplots(figsize=(5, 2.5))
forecaster.plot_history(ax=ax)
# Predicción
# ==============================================================================
predictions = forecaster.predict()
predictions
# Backtesting con datos de test
# ==============================================================================
cv = TimeSeriesFold(
steps=forecaster.max_step,
initial_train_size=len(data.loc[:end_validation, :]), # Datos de entrenamiento + validación
refit=False,
)
metrics, predictions = backtesting_forecaster_multiseries(
forecaster=forecaster,
series=data,
levels=forecaster.levels,
cv=cv,
metric="mean_absolute_error",
verbose=False,
)
# Métricas de error de backtesting
# ==============================================================================
metrics
# 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} %")
# Predicciones de backtesting
# ==============================================================================
predictions
# 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.
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.
# 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()
# 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
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 19. Estos resultados pueden variar en función del hardware utilizado.
# Entrenamiento del forecaster
# ==============================================================================
forecaster.fit(data_train)
# 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 14. 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 19, 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.
# Predicción
# ==============================================================================
predictions = forecaster.predict()
predictions
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.
# Predicción de steps especificos (1 y 5) para la serie o3
# ==============================================================================
forecaster.predict(steps=[1, 5], levels="o3")
# Backtesting con datos de test
# ==============================================================================
cv = TimeSeriesFold(
steps=forecaster.max_step,
initial_train_size=len(data.loc[:end_validation, :]), # Datos de entrenamiento + validación
refit=False,
)
metrics, predictions = backtesting_forecaster_multiseries(
forecaster=forecaster,
series=data,
levels=forecaster.levels,
cv=cv,
metric="mean_absolute_error",
verbose=False,
)
# Métricas de error de backtesting para cada serie
# ==============================================================================
metrics
# 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.
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 la sesión
import session_info
session_info.show(html=False)
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.
¿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.14.0). Zenodo. https://doi.org/10.5281/zenodo.8382788
APA:
Amat Rodrigo, J., & Escobar Ortiz, J. (2023). skforecast (Version 0.14.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.14.0}, month = {11}, 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! 😊
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.