¿Menos es más? ¿Los modelos de pronóstico de aprendizaje profundo necesitan una reducción de funciones?

El Transformadores de Fusión Temporal (TFT) es un modelo avanzado para el pronóstico de series de tiempo. Incluye la Red de Selección de Variables (VSN), que es un componente clave del modelo. Está diseñado específicamente para identificar y centrarse automáticamente en las características más relevantes dentro de un conjunto de datos. Lo logra asignando pesos aprendidos a cada variable de entrada, resaltando efectivamente qué características contribuyen más a la tarea predictiva.

Este enfoque basado en VSN será nuestra segunda técnica de reducción. Lo implementaremos usando Pronóstico de PyTorchlo que nos permite aprovechar la Red de Selección Variable del modelo TFT.

Usaremos una configuración básica. Nuestro objetivo no es crear el modelo de mayor rendimiento posible, sino identificar las características más relevantes utilizando recursos mínimos.

from pytorch_forecasting import TemporalFusionTransformer, TimeSeriesDataSet
from pytorch_forecasting.metrics import QuantileLoss
from lightning.pytorch.callbacks import EarlyStopping
import lightning.pytorch as pl
import torch

pl.seed_everything(42)
max_encoder_length = 32
max_prediction_length = 1
VAL_SIZE = .2
VARIABLES_IMPORTANCE = .8
model_data_feature_sel = initial_model_train.join(stationary_df_train)
model_data_feature_sel = model_data_feature_sel.join(pca_df_train)
model_data_feature_sel['price'] = model_data_feature_sel['price'].astype(float)
model_data_feature_sel['y'] = model_data_feature_sel['price'].pct_change()
model_data_feature_sel = model_data_feature_sel.iloc[1:].reset_index(drop=True)

model_data_feature_sel['group'] = 'spy'
model_data_feature_sel['time_idx'] = range(len(model_data_feature_sel))

train_size_vsn = int((1-VAL_SIZE)*len(model_data_feature_sel))
train_data_feature = model_data_feature_sel[:train_size_vsn]
val_data_feature = model_data_feature_sel[train_size_vsn:]
unknown_reals_origin = [col for col in model_data_feature_sel.columns if col.startswith('value_')] + ['y']

timeseries_config = {
"time_idx": "time_idx",
"target": "y",
"group_ids": ["group"],
"max_encoder_length": max_encoder_length,
"max_prediction_length": max_prediction_length,
"time_varying_unknown_reals": unknown_reals_origin,
"add_relative_time_idx": True,
"add_target_scales": True,
"add_encoder_length": True
}

training_ts = TimeSeriesDataSet(
train_data_feature,
**timeseries_config
)

El VARIABLES_IMPORTANCE El umbral está establecido en 0,8, lo que significa que conservaremos las características en el percentil 80 superior de importancia según lo determinado por la Red de selección variable (VSN). Para obtener más información sobre los Transformadores de Fusión Temporal (TFT) y sus parámetros, consulte la documentación.

A continuación, entrenaremos el modelo TFT.

if torch.cuda.is_available():
accelerator = 'gpu'
num_workers = 2
else :
accelerator = 'auto'
num_workers = 0

validation = TimeSeriesDataSet.from_dataset(training_ts, val_data_feature, predict=True, stop_randomization=True)
train_dataloader = training_ts.to_dataloader(train=True, batch_size=64, num_workers=num_workers)
val_dataloader = validation.to_dataloader(train=False, batch_size=64*5, num_workers=num_workers)

tft = TemporalFusionTransformer.from_dataset(
training_ts,
learning_rate=0.03,
hidden_size=16,
attention_head_size=2,
dropout=0.1,
loss=QuantileLoss()
)

early_stop_callback = EarlyStopping(monitor="val_loss", min_delta=1e-5, patience=5, verbose=False, mode="min")

trainer = pl.Trainer(max_epochs=20, accelerator=accelerator, gradient_clip_val=.5, callbacks=[early_stop_callback])
trainer.fit(
tft,
train_dataloaders=train_dataloader,
val_dataloaders=val_dataloader

)

Establecimos intencionalmente max_epochs=20 para que el modelo no entrene demasiado. Además, implementamos un early_stop_callback que detiene el entrenamiento si el modelo no muestra ninguna mejora durante 5 épocas consecutivas (patience=5).

Finalmente, utilizando el mejor modelo obtenido, seleccionamos el percentil 80 de las características más importantes determinadas por el VSN.

best_model_path = trainer.checkpoint_callback.best_model_path
best_tft = TemporalFusionTransformer.load_from_checkpoint(best_model_path)

raw_predictions = best_tft.predict(val_dataloader, mode="raw", return_x=True)

def get_top_encoder_variables(best_tft,interpretation):
encoder_importances = interpretation["encoder_variables"]
sorted_importances, indices = torch.sort(encoder_importances, descending=True)
cumulative_importances = torch.cumsum(sorted_importances, dim=0)
threshold_index = torch.where(cumulative_importances > VARIABLES_IMPORTANCE)[0][0]
top_variables = [best_tft.encoder_variables[i] for i in indices[:threshold_index+1]]
if 'relative_time_idx' in top_variables:
top_variables.remove('relative_time_idx')
return top_variables

interpretation= best_tft.interpret_output(raw_predictions.output, reduction="sum")
top_encoder_vars = get_top_encoder_variables(best_tft,interpretation)

print(f"\nOriginal number of features: {stationary_df_train.shape[1]}")
print(f"Number of features after Variable Selection Network (VSN): {len(top_encoder_vars)}\n")

Reducción de características mediante red de selección variable. Imagen del autor.

El conjunto de datos original contenía 438 características, que luego se redujeron a 1 característica solo después de aplicar el método VSN. Esta drástica reducción sugiere varias posibilidades:

  1. Es posible que muchas de las funciones originales hayan sido redundantes.
  2. Es posible que el proceso de selección de características haya simplificado demasiado los datos.
  3. Usar sólo los valores históricos de la variable objetivo (enfoque autorregresivo) podría funcionar tan bien, o posiblemente mejor, que los modelos que incorporan variables exógenas.

En esta sección final, comparamos las técnicas de reducción aplicadas a nuestro modelo. Cada método se prueba manteniendo configuraciones de modelo idénticas, variando solo las características sujetas a reducción.

usaremos Mareaun pequeño modelo basado en Transformer de última generación. Usaremos la implementación proporcionada por Pronóstico neuronal. Cualquier modelo de NeuralForecast aquí funcionaría siempre que permita variables históricas exógenas.

Entrenaremos y probaremos dos modelos utilizando datos diarios de SPY (S&P 500 ETF). Ambos modelos tendrán lo mismo:

  1. Relación de división de prueba de tren
  2. Hiperparámetros
  3. Serie temporal única (SPY)
  4. Horizonte de previsión de 1 paso por delante

La única diferencia entre los modelos será la técnica de reducción de características. ¡Eso es todo!

  1. Primer modelo: características originales (sin reducción de características)
  2. Segundo modelo: reducción de funciones mediante PCA
  3. Tercer modelo: reducción de funciones usando VSN

Esta configuración nos permite aislar el impacto de cada técnica de reducción de características en el rendimiento del modelo.

Primero entrenamos los 3 modelos con la misma configuración excepto por las características.

from neuralforecast.models import TiDE
from neuralforecast import NeuralForecast

train_data = initial_model_train.join(stationary_df_train)
train_data = train_data.join(pca_df_train)
test_data = initial_model_test.join(stationary_df_test)
test_data = test_data.join(pca_df_test)

hist_exog_list_origin = [col for col in train_data.columns if col.startswith('value_')] + ['y']
hist_exog_list_pca = [col for col in train_data.columns if col.startswith('PC')] + ['y']
hist_exog_list_vsn = top_encoder_vars

tide_params = {
"h": 1,
"input_size": 32,
"scaler_type": "robust",
"max_steps": 500,
"val_check_steps": 20,
"early_stop_patience_steps": 5
}

model_original = TiDE(
**tide_params,
hist_exog_list=hist_exog_list_origin,
)

model_pca = TiDE(
**tide_params,
hist_exog_list=hist_exog_list_pca,
)

model_vsn = TiDE(
**tide_params,
hist_exog_list=hist_exog_list_vsn,
)

nf = NeuralForecast(
models=[model_original, model_pca, model_vsn],
freq='D'
)

val_size = int(train_size*VAL_SIZE)
nf.fit(df=train_data,val_size=val_size,use_init_models=True)

Luego, hacemos las predicciones.

from tabulate import tabulate
y_hat_test_ret = pd.DataFrame()
current_train_data = train_data.copy()

y_hat_ret = nf.predict(current_train_data)
y_hat_test_ret = pd.concat([y_hat_test_ret, y_hat_ret.iloc[[-1]]])

for i in range(len(test_data) - 1):
combined_data = pd.concat([current_train_data, test_data.iloc[[i]]])
y_hat_ret = nf.predict(combined_data)
y_hat_test_ret = pd.concat([y_hat_test_ret, y_hat_ret.iloc[[-1]]])
current_train_data = combined_data

predicted_returns_original = y_hat_test_ret['TiDE'].values
predicted_returns_pca = y_hat_test_ret['TiDE1'].values
predicted_returns_vsn = y_hat_test_ret['TiDE2'].values

predicted_prices_original = []
predicted_prices_pca = []
predicted_prices_vsn = []

for i in range(len(predicted_returns_pca)):
if i == 0:
last_true_price = train_data['price'].iloc[-1]
else:
last_true_price = test_data['price'].iloc[i-1]
predicted_prices_original.append(last_true_price * (1 + predicted_returns_original[i]))
predicted_prices_pca.append(last_true_price * (1 + predicted_returns_pca[i]))
predicted_prices_vsn.append(last_true_price * (1 + predicted_returns_vsn[i]))

true_values = test_data['price']
methods = ['Original','PCA', 'VSN']
predicted_prices = [predicted_prices_original,predicted_prices_pca, predicted_prices_vsn]

results = []

for method, prices in zip(methods, predicted_prices):
mse = np.mean((np.array(prices) - true_values)**2)
rmse = np.sqrt(mse)
mae = np.mean(np.abs(np.array(prices) - true_values))

results.append([method, mse, rmse, mae])

headers = ["Method", "MSE", "RMSE", "MAE"]
table = tabulate(results, headers=headers, floatfmt=".4f", tablefmt="grid")

print("\nPrediction Errors Comparison:")
print(table)

with open("prediction_errors_comparison.txt", "w") as f:
f.write("Prediction Errors Comparison:\n")
f.write(table)

Pronosticamos los rendimientos diarios utilizando el modelo y luego los convertimos nuevamente a precios. Este enfoque nos permite calcular errores de predicción utilizando precios y comparar los precios reales con los precios pronosticados en un gráfico.

Comparación de errores de predicción con diferentes técnicas de reducción de características. Imagen del autor.

El rendimiento similar del modelo TiDE en los conjuntos de características originales y reducidos revela una idea crucial: la reducción de características no condujo a mejores predicciones como cabría esperar. Esto sugiere posibles problemas clave:

  • Pérdida de información: a pesar de intentar preservar datos esenciales, las técnicas de reducción de dimensionalidad descartaron información relevante para la tarea de predicción, lo que explica la falta de mejora con menos características.
  • Problemas de generalización: el rendimiento constante en todos los conjuntos de características indica la dificultad del modelo para capturar patrones subyacentes, independientemente del número de características.
  • Exceso de complejidad: resultados similares con menos funciones sugieren que la sofisticada arquitectura de TiDE puede ser innecesariamente compleja. Un modelo más simple, como ARIMA, podría funcionar igual de bien.

Luego, examinemos el gráfico para ver si podemos observar diferencias significativas entre los tres métodos de pronóstico y los precios reales.

import matplotlib.pyplot as plt

plt.figure(figsize=(12, 6))
plt.plot(train_data['ds'], train_data['price'], label='Training Data', color='blue')
plt.plot(test_data['ds'], true_values, label='True Prices', color='green')
plt.plot(test_data['ds'], predicted_prices_original, label='Predicted Prices', color='red')
plt.legend()
plt.title('SPY Price Forecast Using All Original Feature')
plt.xlabel('Date')
plt.ylabel('SPY Price')
plt.savefig('spy_forecast_chart_original.png', dpi=300, bbox_inches='tight')
plt.close()

plt.figure(figsize=(12, 6))
plt.plot(train_data['ds'], train_data['price'], label='Training Data', color='blue')
plt.plot(test_data['ds'], true_values, label='True Prices', color='green')
plt.plot(test_data['ds'], predicted_prices_pca, label='Predicted Prices', color='red')
plt.legend()
plt.title('SPY Price Forecast Using PCA Dimensionality Reduction')
plt.xlabel('Date')
plt.ylabel('SPY Price')
plt.savefig('spy_forecast_chart_pca.png', dpi=300, bbox_inches='tight')
plt.close()

plt.figure(figsize=(12, 6))
plt.plot(train_data['ds'], train_data['price'], label='Training Data', color='blue')
plt.plot(test_data['ds'], true_values, label='True Prices', color='green')
plt.plot(test_data['ds'], predicted_prices_vsn, label='Predicted Prices', color='red')
plt.legend()
plt.title('SPY Price Forecast Using VSN')
plt.xlabel('Date')
plt.ylabel('SPY Price')
plt.savefig('spy_forecast_chart_vsn.png', dpi=300, bbox_inches='tight')
plt.close()

Previsión de precios SPY utilizando todas las funciones originales. Imagen creada por el autor.
Previsión de precios SPY utilizando PCA. Imagen creada por el autor.
Previsión de precios SPY utilizando VSN. Imagen creada por el autor.

La diferencia entre los precios reales y previstos parece constante en los tres modelos, sin variaciones notables en el rendimiento entre ellos.

¡Lo logramos! Exploramos la importancia de la reducción de características en el análisis de series temporales y proporcionamos una guía de implementación práctica:

  • La reducción de características tiene como objetivo simplificar los modelos manteniendo el poder predictivo. Los beneficios incluyen complejidad reducida, generalización mejorada, interpretación más sencilla y eficiencia computacional.
  • Demostramos dos técnicas de reducción utilizando datos FRED:
  1. El Análisis de Componentes Principales (PCA), un método de reducción de dimensionalidad lineal, redujo las características de 438 a 76 manteniendo el 90% de la varianza explicada.
  2. La Red de selección variable (VSN) de Temporal Fusion Transformers, un enfoque no lineal, redujo drásticamente las características a solo 1 utilizando un umbral de importancia del percentil 80.
  • La evaluación utilizando modelos TiDE mostró un rendimiento similar entre los conjuntos de características originales y reducidos, lo que sugiere que la reducción de características no siempre mejora el rendimiento del pronóstico. Esto podría deberse a la pérdida de información durante la reducción, la dificultad del modelo para capturar patrones subyacentes o la posibilidad de que un modelo más simple pueda ser igualmente efectivo para esta tarea de pronóstico en particular.

Como nota final, no exploramos todas las técnicas de reducción de características, como SHAP (SHapley Additive exPlanations), que proporciona una medida unificada de la importancia de las características en varios tipos de modelos. Incluso si no mejoramos nuestro modelo, es mejor realizar la selección de funciones y comparar el rendimiento entre diferentes métodos de reducción. Este enfoque ayuda a garantizar que no se descarte información valiosa y, al mismo tiempo, se optimiza la eficiencia y la interpretabilidad de su modelo.

En artículos futuros, aplicaremos estas técnicas de reducción de características a modelos más complejos, comparando su impacto en el rendimiento y la interpretabilidad. ¡Manténganse al tanto!

¿Listo para poner estos conceptos en acción? Puede encontrar la implementación completa del código. aquí.

👏 Aplaude hasta 50 veces

🤝 Envíame un LinkedIn solicitud de conexión para permanecer en contacto

¡Tu apoyo lo es todo! 🙏