Cinco aplicaciones prácticas del modelo LSTM para series temporales, con código

5 aplicaciones prácticas de LSTM para series temporales, con código

Cómo implementar un modelo avanzado de redes neuronales en varios contextos de series temporales

Foto de Andrew Svk en Unsplash

Cuando escribí Explorando el modelo de redes neuronales LSTM para series temporales en enero de 2022, mi objetivo era mostrar cómo se podría implementar fácilmente el modelo avanzado de redes neuronales en Python utilizando scalecast, una biblioteca de series temporales que desarrollé para facilitar mi propio trabajo y proyectos. No pensé que sería visto más de decenas de miles de veces y aparecería como el primer resultado en Google cuando se busca “lstm forecasting python” durante más de un año después de que lo publiqué (cuando lo revisé hoy, todavía estaba en el segundo lugar).

No he tratado de llamar mucha atención a ese artículo porque nunca pensé, y todavía no pienso, que sea muy bueno. Nunca pretendió ser una guía sobre la mejor manera de implementar el modelo LSTM, sino más bien una simple exploración de su utilidad para la predicción de series temporales. Traté de responder preguntas como: ¿qué sucede cuando se ejecuta el modelo con parámetros predeterminados, qué sucede cuando se ajustan sus parámetros de esta manera o de aquella, qué tan fácilmente puede ser superado por otros modelos en ciertos conjuntos de datos, etc. Sin embargo, a juzgar por las publicaciones de blogs, los cuadernos de Kaggle e incluso el curso de Udemy que sigo viendo con el código de ese artículo copiado textualmente, está claro que muchas personas estaban tomando el artículo por su valor anterior, no por el último. Ahora entiendo que no dejé en claro mis intenciones.

Hoy, para ampliar ese artículo, quiero mostrar cómo se debe aplicar el modelo de redes neuronales LSTM, o al menos cómo lo aplicaría yo, para aprovechar al máximo su valor para problemas de predicción de series temporales. Desde que escribí el primer artículo, hemos podido agregar muchas características nuevas e innovadoras a la biblioteca de scalecast que hacen que el uso del modelo LSTM sea mucho más fluido y aprovecharé este espacio para explorar algunas de mis favoritas. Hay cinco aplicaciones para LSTM que creo que funcionarán fantásticamente utilizando la biblioteca: predicción univariable, predicción multivariable, predicción probabilística, predicción probabilística dinámica y transfer learning.

Antes de comenzar, asegúrate de ejecutar en la terminal o línea de comandos:

pip install --upgrade scalecast

El cuaderno completo desarrollado para este artículo se encuentra aquí.

Una nota final: en cada ejemplo, puedo usar los términos “RNN” y “LSTM” indistintamente. Alternativamente, RNN puede aparecer en un gráfico dado de una predicción LSTM. La red neuronal de memoria a corto y largo plazo (LSTM) es un tipo de red neuronal recurrente (RNN), con parámetros adicionales relacionados con la memoria. En scalecast, la clase de modelo rnn se puede usar para ajustar tanto celdas RNN simples como LSTM en modelos portados desde tensorflow.

1. Predicción univariable

La forma más común y más obvia de usar el modelo LSTM es al hacer un problema simple de predicción univariable. Aunque el modelo ajusta muchos parámetros que deberían hacerlo suficientemente sofisticado para aprender tendencias, estacionalidad y dinámicas a corto plazo en cualquier serie temporal dada de manera efectiva, he descubierto que funciona mucho mejor con datos estacionarios (datos que no muestran tendencias o estacionalidad). Entonces, con el conjunto de datos de pasajeros aéreos, que está disponible en Kaggle con una licencia de base de datos abierta, podemos crear fácilmente una predicción precisa y confiable utilizando hiperparámetros bastante simples, si simplemente eliminamos la tendencia y la estacionalidad de los datos:

transformer = Transformer(    transformers = [        ('DetrendTransform',{'poly_order':2}),        'DeseasonTransform',    ],)

También queremos asegurarnos de revertir los resultados a su nivel original cuando hayamos terminado:

reverter = Reverter(    reverters = [        'DeseasonRevert',        'DetrendRevert',    ],    base_transformer = transformer,)

Ahora, podemos especificar los parámetros de la red. Para este ejemplo, utilizaremos 18 retardos, una capa, una función de activación tangente hiperbólica y 200 épocas. ¡Siéntete libre de explorar tus propios parámetros mejores!

def forecaster(f):    f.set_estimator('rnn')    f.manual_forecast(        lags = 18,        layers_struct = [            ('LSTM',{'units':36,'activation':'tanh'}),        ],        epochs=200,        call_me = 'lstm',    )

Combina todo en un pipeline, ejecuta el modelo y visualiza los resultados:

pipeline = Pipeline(    steps = [        ('Transformación',transformer),        ('Pronóstico',forecaster),        ('Revertir',reverter),    ])f = pipeline.fit_predict(f)f.plot()plt.show()
Imagen por el autor

Suficientemente bueno y mucho mejor que cualquier cosa que demostré en el otro artículo. Para ampliar esta aplicación, puedes intentar usar diferentes órdenes de rezago, agregar estacionalidad al modelo en forma de términos de Fourier, encontrar mejores transformaciones de series y ajustar los hiperparámetros del modelo con validación cruzada. Algunas formas de hacer esto se demostrarán en las secciones posteriores.

2. Pronóstico multivariable

Supongamos que tenemos dos series que esperamos que se muevan juntas. Podemos crear un modelo LSTM que tenga en cuenta ambas series al hacer predicciones con la esperanza de mejorar la precisión general del modelo. Esto es, por supuesto, un pronóstico multivariable.

Para este ejemplo, utilizaré el conjunto de datos de los aguacates, disponible en Kaggle con una licencia de base de datos abierta. Mide el precio y la cantidad vendida de aguacates a nivel semanal en diferentes regiones de Estados Unidos. Sabemos por teoría económica que el precio y la demanda están estrechamente relacionados, por lo que al usar el precio como un indicador líder, es posible que podamos pronosticar con mayor precisión la cantidad de aguacates vendidos que utilizando solo la demanda histórica en un contexto univariable.

Lo primero que haremos es transformar cada serie. Podemos buscar un conjunto “óptimo” de transformaciones (es decir, transformaciones que se puntúen fuera de muestra) ejecutando el siguiente código:

data = pd.read_csv('aguacate.csv')# demandavol = data.groupby('Fecha')['Volumen Total'].sum()# precioprecio = data.groupby('Fecha')['Precio Promedio'].sum()fvol = Pronosticador(    y = vol,    fechas_actuales = vol.index,    longitud_prueba = 13,    longitud_validación = 13,    fechas_futuras = 13,    métricas = ['rmse','r2'],)transformador, revertidor = encontrar_transformación_óptima(    fvol,    set_aside_test_set=True, # evita filtraciones para poder comparar los modelos resultantes de manera justa    return_train_only = True, # evita filtraciones para poder comparar los modelos resultantes de manera justa    verbose=True,    detrend_kwargs=[        {'loess':True},        {'poly_order':1},        {'ln_trend':True},    ],    m = 52, # ¿qué hace un ciclo estacional?    longitud_prueba = 4,)

La transformación recomendada de este proceso es un ajuste estacional, asumiendo que 52 períodos hacen una temporada, así como una escala robusta (escalado que es robusto a los valores atípicos). Luego podemos ajustar esa transformación en la serie y llamar a un modelo LSTM univariable para comparar el modelo multivariable. Esta vez, utilizaremos un proceso de ajuste de hiperparámetros generando una grilla de posibles funciones de activación, tamaños de capa y valores de deserción:

grilla_rnn = gen_rnn_grid(    intentos_capa = 10,    tamaño_capa_mínimo = 3,    tamaño_capa_máximo = 5,    unidades_pool = [100],    épocas = [25,50],    deserción_pool = [0,0.05],    callbacks=EarlyStopping(      monitor='val_loss',      patience=3,    ),    semilla_aleatoria = 20,) # crea una grilla de valores de hiperparámetros para ajustar el modelo LSTM

Esta función proporciona una buena manera de ingresar una grilla manejable en nuestro objeto, pero también tiene suficiente aleatoriedad para tener una buena selección de parámetros para elegir. Ahora ajustamos el modelo univariable:

fvol.add_ar_terms(13) # el modelo usará 13 rezagos de seriesfvol.set_estimator('rnn')fvol.ingest_grid(grilla_rnn)fvol.tune() # utiliza un conjunto de validación de 13 períodosfvol.auto_forecast(call_me='lstm_univariable')

Para ampliar esto a un contexto multivariable, podemos transformar la serie de tiempo de precios con el mismo conjunto de transformaciones que usamos en la otra serie. Luego, ingresamos 13 rezagos de precios en el objeto Pronosticador y ajustamos un nuevo modelo LSTM:

fprecio = Pronosticador(    y = precio,    fechas_actuales = precio.index,    fechas_futuras = 13,)fprecio = transformador.fit_transform(fprecio)fvol.add_series(fprecio.y,denominada='precio')fvol.add_lagged_terms('precio',rezagos=13,drop=True)fvol.ingest_grid(grilla_rnn)fvol.tune()fvol.auto_forecast(call_me='lstm_multivariable')

También podemos hacer una prueba de referencia con un modelo ingenuo y mostrar los resultados a nivel de la serie original, junto con el conjunto de prueba fuera de muestra:

# previsión ingenua para hacer una prueba de referencia
fvol.set_estimator('ingenua')
fvol.manual_forecast()
fvol = reverter.fit_transform(fvol)
fvol.plot_test_set(order_by='TestSetRMSE')
plt.show()
Imagen por autor

Juzgando por cómo los tres modelos se agruparon visualmente, lo que condujo a la mayor precisión en esta serie en particular fueron las transformaciones aplicadas, así es como el modelo ingenuo terminó siendo tan comparable a ambos modelos LSTM. Sin embargo, los modelos LSTM son una mejora, con el modelo multivariable obteniendo un r-cuadrado de 38.37% y el modelo univariable de 26.35%, en comparación con la línea de base de -6.46%.

Imagen por autor

Una cosa que podría haber dificultado que los modelos LSTM tuvieran un mejor rendimiento en esta serie es lo corta que es. Con solo 169 observaciones, es posible que no haya suficiente historial para que el modelo aprenda suficientemente los patrones. Sin embargo, cualquier mejora sobre un modelo ingenuo o simple puede considerarse un éxito.

3. Previsión probabilística

La previsión probabilística se refiere a la capacidad de un modelo de no solo hacer predicciones puntuales, sino de proporcionar estimaciones de cuán lejos en cualquier dirección es probable que estén las predicciones. La previsión probabilística es similar a la previsión con intervalos de confianza, un concepto que ha existido durante mucho tiempo. Una forma rápidamente emergente de producir previsiones probabilísticas es aplicando un intervalo de confianza conformal al modelo, utilizando un conjunto de calibración para determinar la probable dispersión de los puntos futuros reales. Este enfoque tiene la ventaja de ser aplicable a cualquier modelo de aprendizaje automático, independientemente de las suposiciones que el modelo haga sobre la distribución de sus entradas o residuos. También proporciona ciertas garantías de cobertura que son extremadamente útiles para cualquier profesional de ML. Podemos aplicar el intervalo de confianza conformal al modelo LSTM para producir previsiones probabilísticas.

Para este ejemplo, utilizaremos el conjunto de datos de inicio de viviendas mensuales disponible en FRED, una base de datos de series temporales económicas de código abierto. Utilizaré datos desde enero de 1959 hasta diciembre de 2022 (768 observaciones). Primero, buscaremos una vez más el conjunto óptimo de transformaciones, pero esta vez utilizando un modelo LSTM con 10 épocas para evaluar cada intento de transformación:

transformador, revertidor = encontrar_transformacion_optima(    f,    estimador = 'lstm',    épocas = 10,    establecer_test_set_a_un_lado=True, # evita fugas para poder comparar los modelos resultantes de manera justa    solo_retorno_entrenamiento = True, # evita fugas para poder comparar los modelos resultantes de manera justa    verbose=True,    m = 52, # ¿qué constituye un ciclo estacional?    longitud_prueba = 24,    num_conjuntos_prueba = 3,    espacio_entre_conjuntos = 12,    detrend_kwargs=[        {'loess':True},        {'poly_order':1},        {'ln_trend':True},    ],)

Generaremos aleatoriamente una cuadrícula de hiperparámetros nuevamente, pero esta vez podemos hacer que su espacio de búsqueda sea muy grande, luego lo limitaremos manualmente a 10 intentos cuando se ajuste el modelo más adelante para poder validar cruzadamente los parámetros en un tiempo razonable:

rnn_grid = gen_rnn_grid(    intentos_capa = 100,    tamaño_mínimo_capa = 1,    tamaño_máximo_capa = 5,    unidades_pool = [100],    épocas = [100],    dropout_pool = [0,0.05],    validación_dividida=.2,    callbacks=EarlyStopping(      monitor='val_loss',      paciencia=3,    ),    semilla_aleatoria = 20,) # hacer una cuadrícula realmente grande y limitarla manualmente

Ahora podemos construir y ajustar el pipeline:

def pronosticador(f,grid):    f.auto_seleccionar_variable_X(        try_trend=False,        try_seasonalities=False,        max_ar=100    )    f.establecer_estimador('rnn')    f.ingerir_grid(grid)    f.limitar_tamaño_grid(10) # reducir aleatoriamente la cuadrícula grande a 10    f.validar_cruzado(k=3,longitud_prueba=24) # validación cruzada de tres pliegues    f.auto_pronosticar()pipeline = Pipeline(    steps = [        ('Transformar',transformador),        ('Pronosticar',pronosticador),        ('Revertir',revertidor),    ])f = pipeline.fit_predict(f,grid=rnn_grid)

Porque reservamos un conjunto de pruebas de tamaño suficiente en el objeto Forecaster, los resultados automáticamente nos dan las distribuciones probabilísticas del 90% para cada estimación puntual:

f.plot(ci=True)plt.show()
Imagen por autor

4. Pronóstico probabilístico dinámico

Los ejemplos anteriores proporcionan una predicción probabilística estática, donde cada límite superior e inferior a lo largo del pronóstico está igualmente alejado de la estimación puntual que cualquier otro límite superior e inferior adjunto a cualquier otro punto. Al predecir el futuro, es intuitivo que cuanto más se intente pronosticar, más se dispersará el error, una sutileza que no se captura con el intervalo estático. Hay una manera de lograr un pronóstico probabilístico más dinámico con el modelo LSTM utilizando el backtesting.

El backtesting es el proceso de ajustar iterativamente el modelo, predecirlo en diferentes horizontes de pronóstico y probar su rendimiento en cada iteración. Tomemos el pipeline especificado en el último ejemplo y hagamos un backtest 10 veces. Necesitamos al menos 10 iteraciones de backtest para construir intervalos de confianza al 90%:

backtest_results = backtest_for_resid_matrix(    f,    pipeline=pipeline,    alpha = .1,    jump_back = 12,    params = f.best_params,)backtest_resid_matrix = get_backtest_resid_matrix(backtest_results)

Podemos analizar visualmente los valores absolutos de los residuos en cada iteración:

Imagen por autor

Lo interesante de este ejemplo en particular es que los errores más grandes no suelen estar en los últimos pasos del pronóstico, sino en los pasos 14-17. Esto puede suceder con series que tienen patrones estacionales extraños. La presencia de valores atípicos también puede afectar este patrón. De cualquier manera, podemos usar estos resultados para reemplazar ahora los intervalos de confianza estáticos con intervalos dinámicos que sean conformes en cada paso:

overwrite_forecast_intervals(    f,    backtest_resid_matrix=backtest_resid_matrix,    alpha=.1, # intervalos del 90%)f.plot(ci=True)plt.show()
Imagen por autor

5. Transfer learning

El aprendizaje por transferencia es útil cuando deseamos utilizar un modelo fuera del contexto en el que se ajustó. Hay dos escenarios específicos donde demostraré su utilidad: hacer predicciones cuando hay nuevos datos disponibles en una serie de tiempo dada y hacer predicciones en una serie de tiempo relacionada con tendencias y estacionalidad similares.

Escenario 1: Nuevos datos de la misma serie

Podemos utilizar el mismo conjunto de datos de viviendas que en los dos ejemplos anteriores, pero digamos que ha pasado algún tiempo y ahora tenemos datos disponibles hasta junio de 2023.

df = pdr.get_data_fred(    'CANWSCNDW01STSAM',    start = '2010-01-01',    end = '2023-06-30',)f_new = Forecaster(    y = df.iloc[:,0],    current_dates = df.index,    future_dates = 24, # horizonte de pronóstico de 2 años)

Remodelaremos nuestro pipeline con las mismas transformaciones, pero esta vez, usaremos un pronóstico de transferencia en lugar del procedimiento de pronóstico de escala normal, que también ajusta el modelo:

def transfer_forecast(f_new,transfer_from):    f_new = infer_apply_Xvar_selection(infer_from=transfer_from,apply_to=f_new)    f_new.transfer_predict(transfer_from=transfer_from,model='rnn',model_type='tf')pipeline_can = Pipeline(    steps = [        ('Transform',transformer),        ('Transfer Forecast',transfer_forecast),        ('Revert',reverter),    ])f_new = pipeline_can.fit_predict(f_new,transfer_from=f)

Aunque el nombre de la función relevante sigue siendo fit_predict(), en realidad no hay ajuste y solo predicción en el pipeline tal como está escrito. Esto reduce en gran medida la cantidad de tiempo que necesitamos para volver a ajustar y reoptimizar un modelo. Luego vemos los resultados:

f_new.plot()plt.show('Pronóstico de Inicio de Viviendas con Datos Reales hasta Junio de 2023')plt.show()
Imagen del autor

Escenario 2: Un nuevo time series con características similares

Para el segundo escenario, podemos usar la situación hipotética de querer usar el modelo entrenado en la dinámica de viviendas en los Estados Unidos para predecir el inicio de viviendas en Canadá. Descargo de responsabilidad: no sé si esto es realmente una buena idea, es solo un escenario que se me ocurrió para demostrar cómo se haría esto. Pero imagino que podría ser útil y el código involucrado se puede transferir a otras situaciones (tal vez para situaciones en las que tienes series cortas que exhiben dinámicas similares a una serie más larga a la que ya le has ajustado un modelo de buen rendimiento). En ese caso, el código es en realidad exactamente el mismo que el código del Escenario 1; la única diferencia es los datos que cargamos en el objeto:

df = pdr.get_data_fred(    'CANWSCNDW01STSAM',    start = '2010-01-01',    end = '2023-06-30',)f_new = Forecaster(    y = df.iloc[:,0],    current_dates = df.index,    future_dates = 24, # 2 años de pronóstico)def transfer_forecast(f_new,transfer_from):    f_new = infer_apply_Xvar_selection(infer_from=transfer_from,apply_to=f_new)    f_new.transfer_predict(transfer_from=transfer_from,model='rnn',model_type='tf')pipeline_can = Pipeline(    steps = [        ('Transformar',transformador),        ('Transferir Pronóstico',transfer_forecast),        ('Revertir',revertir),    ])f_new = pipeline_can.fit_predict(f_new,transfer_from=f)f_new.plot()plt.show('Pronóstico de Inicio de Viviendas en Canadá')plt.show()
Imagen del autor

Creo que el pronóstico parece lo suficientemente creíble como para ser una aplicación interesante de transfer learning con LSTM.

Conclusión

Para muchos casos de uso de pronóstico, el modelo LSTM puede ser una solución interesante. En esta publicación, demostré cómo aplicar el modelo LSTM para cinco propósitos diferentes con código en Python. Si te resultó útil, dale una estrella a scalecast en GitHub y asegúrate de seguirme aquí en VoAGI para estar actualizado sobre lo último y lo mejor con el paquete. Para proporcionar comentarios, críticas constructivas o si tienes preguntas sobre este código, no dudes en enviarme un correo electrónico a: [email protected].