Exercise: Forecasting with HALNet (All Inputs Required)¶
Welcome to this exercise on using the Hybrid Attentive LSTM
Network, HALNet, available in
fusionlab-learn. This model is a powerful data-driven
architecture that requires static, dynamic (past observed),
and known future features to be provided as inputs.
We will perform a multi-step point forecast to illustrate the specific data preparation and model interaction required for this advanced forecasting model.
Learning Objectives:
Generate synthetic multi-item time series data with distinct static, dynamic, and future features.
Understand how to define feature roles and prepare the data for a model with three separate input streams.
Utilize a data preparation utility (like
reshape_xtft_data()) to create the three required input arrays.Correctly structure the input list
[static_features, dynamic_features, future_features]for training and prediction withHALNet.Define, compile, and train the
HALNetmodel.Make predictions and visualize the results.
Let’s get started!
Prerequisites¶
Ensure you have fusionlab-learn and its common dependencies
installed. For visualizations, matplotlib is also needed.
pip install fusionlab-learn matplotlib scikit-learn
Step 1: Imports and Setup¶
First, we import all necessary libraries and set up our environment for reproducibility.
1import os
2import numpy as np
3import pandas as pd
4import tensorflow as tf
5import matplotlib.pyplot as plt
6from sklearn.preprocessing import StandardScaler, LabelEncoder
7import warnings
8
9# FusionLab imports
10from fusionlab.nn.models import HALNet
11from fusionlab.nn.utils import reshape_xtft_data
12
13# Suppress warnings and TF logs for cleaner output
14warnings.filterwarnings('ignore')
15tf.get_logger().setLevel('ERROR')
16
17# Directory for saving any output images from this exercise
18EXERCISE_OUTPUT_DIR = "./halnet_exercise_outputs"
19os.makedirs(EXERCISE_OUTPUT_DIR, exist_ok=True)
20
21print("Libraries imported and setup complete for HALNet exercise.")
Expected Output:
Libraries imported and setup complete for HALNet exercise.
Step 2: Generate Synthetic Data¶
We’ll create a synthetic dataset for multiple items. Each item will
have static features, dynamic past features, and known future
features, which is the structure HALNet is designed for.
1N_ITEMS = 3
2N_TIMESTEPS_PER_ITEM = 100
3SEED = 42
4np.random.seed(SEED)
5tf.random.set_seed(SEED)
6
7date_rng = pd.date_range(
8 start='2022-01-01', periods=N_TIMESTEPS_PER_ITEM, freq='D'
9)
10df_list = []
11
12for item_id in range(N_ITEMS):
13 time_idx = np.arange(N_TIMESTEPS_PER_ITEM)
14 # Create a base signal with trend, seasonality, and noise
15 value = (
16 30 + item_id * 20 + time_idx * 0.4
17 + np.sin(time_idx / 7) * 10
18 + np.random.normal(0, 3, N_TIMESTEPS_PER_ITEM)
19 )
20 # Static feature unique to each item
21 static_category = f"Category_{'ABC'[item_id]}"
22 # Known future feature (e.g., promotional event on weekends)
23 future_event = (date_rng.dayofweek >= 5).astype(int)
24
25 item_df = pd.DataFrame({
26 'Date': date_rng,
27 'ItemID': f'item_{item_id}',
28 'StaticCategory': static_category,
29 'DayOfWeek': date_rng.dayofweek,
30 'FutureEvent': future_event,
31 'Value': value
32 })
33 # Dynamic feature (lagged value)
34 item_df['ValueLag1'] = item_df['Value'].shift(1)
35 df_list.append(item_df)
36
37df_raw = pd.concat(df_list).dropna().reset_index(drop=True)
38print(f"Generated raw data shape: {df_raw.shape}")
39print("Sample of generated data:")
40print(df_raw.head())
Expected Output:
Generated raw data shape: (297, 7)
Sample of generated data:
Date ItemID StaticCategory ... FutureEvent Value ValueLag1
0 2022-01-02 item_0 Category_A ... 1 31.408924 31.490142
1 2022-01-03 item_0 Category_A ... 0 35.561494 31.408924
2 2022-01-04 item_0 Category_A ... 0 39.924808 35.561494
3 2022-01-05 item_0 Category_A ... 0 36.305882 39.924808
4 2022-01-06 item_0 Category_A ... 0 37.848368 36.305882
[5 rows x 7 columns]
Step 3: Define Features and Preprocess Data¶
We assign columns to their roles (static, dynamic, future). Since
HALNet requires numerical inputs, we encode categorical static
features and scale the numerical features.
1TARGET_COL = 'Value'
2DT_COL = 'Date'
3
4# Define feature roles
5static_cols = ['ItemID', 'StaticCategory']
6dynamic_cols = ['DayOfWeek', 'ValueLag1']
7future_cols = ['FutureEvent', 'DayOfWeek']
8# Use the original ItemID for grouping the time series
9grouping_cols = ['ItemID']
10
11df_processed = df_raw.copy()
12
13# --- Encode Categorical Static Features ---
14static_encoders = {}
15for col in static_cols:
16 le = LabelEncoder()
17 df_processed[f"{col}_encoded"] = le.fit_transform(df_processed[col])
18 static_encoders[col] = le
19print("\nEncoded static categorical features.")
20
21# --- Update feature lists to use encoded/scaled versions ---
22static_cols_for_model = [f"{c}_encoded" for c in static_cols]
23
24# --- Scale Numerical Features ---
25scaler = StandardScaler()
26num_cols_to_scale = ['Value', 'ValueLag1']
27df_processed[num_cols_to_scale] = scaler.fit_transform(
28 df_processed[num_cols_to_scale]
29)
30print("Scaled numerical features.")
Expected Output:
Encoded static categorical features.
Scaled numerical features.
Step 4: Prepare Sequences for HALNet¶
We use the reshape_xtft_data utility to transform our flat
DataFrame into the three distinct sequence arrays required by
HALNet: static, dynamic past, and known future.
1TIME_STEPS = 14 # Lookback window
2FORECAST_HORIZON = 7 # Prediction window
3
4static_data, dynamic_data, future_data, target_data = reshape_xtft_data(
5 df=df_processed,
6 dt_col=DT_COL,
7 target_col=TARGET_COL,
8 dynamic_cols=dynamic_cols,
9 static_cols=static_cols_for_model, # Use encoded static cols
10 future_cols=future_cols,
11 spatial_cols=grouping_cols, # Use for grouping items
12 time_steps=TIME_STEPS,
13 forecast_horizons=FORECAST_HORIZON,
14 verbose=0
15)
16targets = target_data.astype(np.float32)
17
18print(f"\nReshaped Data Shapes for HALNet:")
19print(f" Static data: {static_data.shape}")
20print(f" Dynamic data: {dynamic_data.shape}")
21print(f" Future data: {future_data.shape}")
22print(f" Target data: {targets.shape}")
Expected Output:
Reshaped Data Shapes for HALNet:
Static data: (237, 2)
Dynamic data: (237, 14, 2)
Future data: (237, 21, 2)
Target data: (237, 7, 1)
Step 5: Define, Compile, and Train HALNet¶
Now we instantiate HALNet with the correct input dimensions
derived from our prepared data, compile it, and train for a few epochs.
1OUTPUT_DIM =1
2# Split data into training and validation sets
3train_inputs = [arr[:-20] for arr in [static_data, dynamic_data, future_data]]
4val_inputs = [arr[-20:] for arr in [static_data, dynamic_data, future_data]]
5train_targets, val_targets = targets[:-20], targets[-20:]
6
7# Instantiate HALNet
8halnet_model = HALNet(
9 static_input_dim=static_data.shape[-1],
10 dynamic_input_dim=dynamic_data.shape[-1],
11 future_input_dim=future_data.shape[-1],
12 output_dim=OUTPUT_DIM,
13 forecast_horizon=FORECAST_HORIZON,
14 max_window_size=TIME_STEPS,
15 quantiles=None, # Point forecast for this exercise
16 embed_dim=16,
17 hidden_units=16,
18 lstm_units=16,
19 attention_units=16,
20 num_heads=2,
21 use_vsn=False
22)
23
24# Compile the model
25halnet_model.compile(optimizer=Adam(learning_rate=1e-3), loss='mse')
26
27# Train the model
28print("\nStarting HALNet model training...")
29history = halnet_model.fit(
30 train_inputs,
31 train_targets,
32 validation_data=(val_inputs, val_targets),
33 epochs=50,
34 batch_size=32,
35 verbose=1
36)
37print("Training complete.")
Expected Output:
Starting HALNet model training...
Epoch 1/10
7/7 [==============================] - 15s 391ms/step - loss: 1.0506 - val_loss: 0.8172
Epoch 2/10
7/7 [==============================] - 0s 19ms/step - loss: 0.3957 - val_loss: 0.5841
...
Epoch 50/50
7/7 [==============================] - 0s 14ms/step - loss: 0.1322 - val_loss: 0.4004
Training complete.
Step 6: Visualize Training History¶
Use the plot_history_in utility to visualize the loss curves.
1from fusionlab.nn.models.utils import plot_history_in
2
3print("\\nPlotting training history...")
4plot_history_in(
5 history,
6 metrics={"Loss": ["loss"]},
7 layout='single',
8 title="HALNet Training and Validation History"
9)
Example Output Plot:
An example plot showing the training and validation loss over epochs.¶
Step 7: Visualize the Forecast¶
Finally, we make predictions on the validation set and plot the results against the actual values for a single item.
1# Make predictions on the validation set
2val_predictions_scaled = halnet_model.predict(val_inputs)
3
4# Reshape for inverse transform (we only scaled 'Value' and 'ValueLag1')
5val_predictions_flat = val_predictions_scaled.flatten()
6val_actuals_flat = val_targets.flatten()
7dummy_shape = (len(val_predictions_flat), len(num_cols_to_scale))
8target_idx = num_cols_to_scale.index('Value')
9
10dummy_preds = np.zeros(dummy_shape)
11dummy_preds[:, target_idx] = val_predictions_flat
12val_preds_inv = scaler.inverse_transform(dummy_preds)[:, target_idx]
13
14dummy_actuals = np.zeros(dummy_shape)
15dummy_actuals[:, target_idx] = val_actuals_flat
16val_actuals_inv = scaler.inverse_transform(dummy_actuals)[:, target_idx]
17
18# --- Visualization for one validation item ---
19# Let's find the first sample in the validation set for item_2
20val_static_df = pd.DataFrame(val_inputs[0], columns=static_cols_for_model)
21item_2_encoded_val = static_encoders['ItemID'].transform(['item_2'])[0]
22first_item_2_idx = val_static_df[
23 val_static_df['ItemID_encoded'] == item_2_encoded_val
24].index[0]
25
26# Plot the forecast for this single sequence
27plt.figure(figsize=(12, 6))
28plt.plot(
29 val_actuals_inv.reshape(val_targets.shape)[first_item_2_idx, :, 0],
30 label='Actual Values', marker='o', linestyle='--'
31)
32plt.plot(
33 val_preds_inv.reshape(val_predictions_scaled.shape)[first_item_2_idx, :, 0],
34 label='HALNet Predictions', marker='x'
35)
36plt.title('HALNet Forecast vs. Actual (Validation Set - Item 2)')
37plt.xlabel(f'Forecast Step (Horizon = {FORECAST_HORIZON} steps)')
38plt.ylabel('Value (Inverse Transformed)')
39plt.legend()
40plt.grid(True)
41plt.tight_layout()
42fig_path = os.path.join(EXERCISE_OUTPUT_DIR, "halnet_exercise_forecast.png")
43# plt.savefig(fig_path)
44plt.show()
Expected Plot:
Visualization of the multi-step point forecast from the HALNet
model against actual validation data for a specific item.¶
Discussion of Exercise¶
Congratulations! In this exercise, you have learned the end-to-end
workflow for using the data-driven HALNet model:
- You successfully structured a dataset with the three required
input types (static, dynamic past, and known future).
- You used a data preparation utility to create the correctly shaped
sequence arrays needed by the model.
You instantiated, trained, and made predictions with
HALNet.- You visualized the multi-step forecast, demonstrating the model’s
ability to predict a sequence of future values.
This forms a strong basis for applying HALNet to your own complex,
multi-feature forecasting problems.