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 with HALNet.

  • Define, compile, and train the HALNet model.

  • 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:

HALNet Training History 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:

HALNet Point Forecast Exercise Results

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.