kesma

ConfTimeTransformer.py

Jan 10th, 2025 (edited)
35
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 13.84 KB | Source Code | 0 0
  1. import os
  2. import random
  3. import yaml
  4. import numpy as np
  5. import pandas as pd
  6. import matplotlib.pyplot as plt
  7.  
  8. ###############################################################################
  9. # 0) Environment Variables & Logging
  10. ###############################################################################
  11. #  - Suppress most TF logs (errors only).
  12. #  - Attempt to force deterministic ops (if supported by your TF version).
  13. os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"  # 0=ALL, 1=INFO, 2=WARNING, 3=ERROR
  14. os.environ["TF_DETERMINISTIC_OPS"] = "1"
  15.  
  16. # If you want to disable GPU for maximum reproducibility (CPU only), uncomment:
  17. # os.environ["CUDA_VISIBLE_DEVICES"] = "-1"
  18.  
  19. ###############################################################################
  20. # 1) TensorFlow & Other Imports
  21. ###############################################################################
  22. import tensorflow as tf
  23. from tensorflow.keras import layers, models
  24. from sklearn.preprocessing import MinMaxScaler
  25.  
  26. # Try enabling TF deterministic ops in code (TF >= 2.8/2.9).
  27. try:
  28.     tf.config.experimental.enable_op_determinism()
  29. except Exception as e:
  30.     print("Note: Could not enable_op_determinism(). Some TF versions may not support it.", e)
  31.  
  32. ###############################################################################
  33. # 2) Set All Random Seeds
  34. ###############################################################################
  35. # For reproducibility across runs (not guaranteed 100%, but as close as you can get).
  36. random.seed(42)
  37. np.random.seed(42)
  38. tf.random.set_seed(42)
  39.  
  40. ###############################################################################
  41. # 3) Load Configuration
  42. ###############################################################################
  43.  
  44. def load_config(config_path: str = "config.yaml") -> dict:
  45.     """Load YAML config file and return it as a dictionary."""
  46.     with open(config_path, 'r') as f:
  47.         config = yaml.safe_load(f)
  48.     return config
  49.  
  50. ###############################################################################
  51. # 4) Main Script
  52. ###############################################################################
  53.  
  54. def main():
  55.     # Use dark background for all plots
  56.     plt.style.use('dark_background')
  57.  
  58.     # -------------------------------------------------------------------------
  59.     # 4.1) Load the config
  60.     # -------------------------------------------------------------------------
  61.     config = load_config("config.yaml")
  62.  
  63.     # Paths
  64.     input_csv = config["paths"]["input_csv"]
  65.     output_plots_dir = config["paths"].get("output_plots_dir", None)
  66.  
  67.     # Columns
  68.     date_col = config["columns"]["date_col"]
  69.     target_col = config["columns"]["target_col"]
  70.     feature_cols = config["columns"]["feature_cols"]
  71.  
  72.     # Training params
  73.     window_size = config["training"]["window_size"]
  74.     train_split = config["training"]["train_split"]
  75.     epochs = config["training"]["epochs"]
  76.     batch_size = config["training"]["batch_size"]
  77.     learning_rate = config["training"]["learning_rate"]
  78.  
  79.     # Transformer params
  80.     d_model = config["transformer"]["d_model"]
  81.     num_heads = config["transformer"]["num_heads"]
  82.     ff_dim = config["transformer"]["ff_dim"]
  83.     num_transformer_blocks = config["transformer"]["num_transformer_blocks"]
  84.     dropout_rate = config["transformer"]["dropout_rate"]
  85.  
  86.     # Forecast params
  87.     n_future = config["forecast"]["n_future"]
  88.  
  89.     # -------------------------------------------------------------------------
  90.     # 4.2) Load CSV & Create Day-of-Week Features
  91.     # -------------------------------------------------------------------------
  92.     df = pd.read_csv(input_csv, parse_dates=[date_col])
  93.     df.sort_values(date_col, inplace=True)
  94.     df.set_index(date_col, inplace=True)
  95.  
  96.     # Basic day-of-week feature: Monday=0, ..., Sunday=6
  97.     df['dow'] = df.index.dayofweek
  98.  
  99.     # Cyclical encoding for day-of-week: dow_sin, dow_cos in [-1, 1]
  100.     df['dow_sin'] = np.sin(2 * np.pi * df['dow'] / 7)
  101.     df['dow_cos'] = np.cos(2 * np.pi * df['dow'] / 7)
  102.  
  103.     # Weekend flag: 1 if Saturday/Sunday, else 0
  104.     df['is_weekend'] = (df['dow'] >= 5).astype(int)
  105.  
  106.     # Extract only the features we want (including the target)
  107.     data_df = df[feature_cols].copy()
  108.  
  109.     # -------------------------------------------------------------------------
  110.     # 4.3) Scale All Features
  111.     # -------------------------------------------------------------------------
  112.     scaler = MinMaxScaler()
  113.     data_scaled = scaler.fit_transform(data_df.values)
  114.     # data_scaled shape: (num_samples, num_features)
  115.  
  116.     # -------------------------------------------------------------------------
  117.     # 4.4) Create Sequences (Windowed) for Single-Step Forecast
  118.     # -------------------------------------------------------------------------
  119.     def create_sequences(data, window_size=30):
  120.         """
  121.        data: np array (num_samples, num_features)
  122.        window_size: how many past steps to include in each sample
  123.        Returns:
  124.            X -> (num_samples - window_size, window_size, num_features)
  125.            y -> (num_samples - window_size,)
  126.        """
  127.         X, y = [], []
  128.         for i in range(len(data) - window_size):
  129.             X_window = data[i : i + window_size, :]
  130.             # Assumes target_col is at index 0 in feature_cols
  131.             y_next = data[i + window_size, 0]
  132.             X.append(X_window)
  133.             y.append(y_next)
  134.         return np.array(X), np.array(y)
  135.  
  136.     X, y = create_sequences(data_scaled, window_size=window_size)
  137.  
  138.     # Split train/test
  139.     train_size = int(len(X) * train_split)
  140.     X_train, y_train = X[:train_size], y[:train_size]
  141.     X_test,  y_test = X[train_size:], y[train_size:]
  142.  
  143.     # -------------------------------------------------------------------------
  144.     # 4.5) Build a Simple Transformer (TimeTransformer)
  145.     # -------------------------------------------------------------------------
  146.     class PositionalEncoding(layers.Layer):
  147.         def __init__(self, max_len, d_model):
  148.             super().__init__()
  149.             self.max_len = max_len
  150.             self.d_model = d_model
  151.            
  152.             # Prepare positional encodings (shape: [max_len, d_model])
  153.             pos = np.arange(max_len)[:, np.newaxis].astype(np.float32)   # (max_len, 1)
  154.             i   = np.arange(d_model)[np.newaxis, :].astype(np.float32)   # (1, d_model)
  155.            
  156.             angle_rates = 1.0 / (10000.0 ** (2.0 * np.floor(i/2.0) / d_model))
  157.             angle_rads  = pos * angle_rates
  158.            
  159.             pos_enc = np.zeros_like(angle_rads, dtype=np.float32)
  160.             pos_enc[:, 0::2] = np.sin(angle_rads[:, 0::2])  # even indices
  161.             pos_enc[:, 1::2] = np.cos(angle_rads[:, 1::2])  # odd indices
  162.  
  163.             self.pos_encoding = tf.constant(pos_enc, dtype=tf.float32)
  164.  
  165.         def call(self, x):
  166.             seq_len = tf.shape(x)[1]
  167.             # Add positional encoding up to seq_len
  168.             return x + self.pos_encoding[:seq_len, :]
  169.  
  170.     def time_transformer_model(
  171.         input_shape,
  172.         d_model=128,
  173.         num_heads=4,
  174.         ff_dim=512,
  175.         num_transformer_blocks=4,
  176.         dropout_rate=0.0001
  177.     ):
  178.         inputs = layers.Input(shape=input_shape)
  179.  
  180.         # Project from 'num_features' -> d_model
  181.         x = layers.Dense(d_model)(inputs)
  182.  
  183.         # Add positional encoding
  184.         x = PositionalEncoding(max_len=input_shape[0], d_model=d_model)(x)
  185.  
  186.         # Stacked transformer blocks
  187.         for _ in range(num_transformer_blocks):
  188.             # Multi-head attention
  189.             attn_output = layers.MultiHeadAttention(
  190.                 num_heads=num_heads,
  191.                 key_dim=d_model,
  192.                 dropout=dropout_rate
  193.             )(x, x)
  194.             attn_output = layers.Dropout(dropout_rate)(attn_output)
  195.             out1 = layers.LayerNormalization(epsilon=1e-6)(x + attn_output)
  196.  
  197.             # Feed Forward network
  198.             ffn = layers.Dense(ff_dim, activation='relu')(out1)
  199.             ffn = layers.Dense(d_model)(ffn)
  200.             ffn = layers.Dropout(dropout_rate)(ffn)
  201.             out2 = layers.LayerNormalization(epsilon=1e-6)(out1 + ffn)
  202.             x = out2
  203.        
  204.         x = layers.GlobalAveragePooling1D()(x)
  205.         outputs = layers.Dense(1)(x)
  206.        
  207.         model = models.Model(inputs=inputs, outputs=outputs, name="TimeTransformer")
  208.         return model
  209.  
  210.     model = time_transformer_model(
  211.         input_shape=(window_size, len(feature_cols)),
  212.         d_model=d_model,
  213.         num_heads=num_heads,
  214.         ff_dim=ff_dim,
  215.         num_transformer_blocks=num_transformer_blocks,
  216.         dropout_rate=dropout_rate
  217.     )
  218.  
  219.     model.compile(
  220.         optimizer=tf.keras.optimizers.Adam(learning_rate=learning_rate),
  221.         loss='mse'
  222.     )
  223.     model.summary()
  224.  
  225.     # -------------------------------------------------------------------------
  226.     # 4.6) Train & Evaluate
  227.     # -------------------------------------------------------------------------
  228.     # Shuffle=False for reproducibility
  229.     history = model.fit(
  230.         X_train, y_train,
  231.         validation_data=(X_test, y_test),
  232.         epochs=epochs,
  233.         batch_size=batch_size,
  234.         shuffle=False
  235.     )
  236.  
  237.     # Plot training history
  238.     plt.figure(figsize=(7,4))
  239.     # We use bright colors to ensure visibility on dark background
  240.     plt.plot(history.history['loss'], label='Train Loss', color='cyan')
  241.     plt.plot(history.history['val_loss'], label='Val Loss', color='magenta')
  242.     plt.title("Training History", color='white')
  243.     plt.xlabel("Epoch", color='white')
  244.     plt.ylabel("Loss", color='white')
  245.     plt.legend(facecolor='black')
  246.     plt.tight_layout()
  247.     if output_plots_dir:
  248.         os.makedirs(output_plots_dir, exist_ok=True)
  249.         plt.savefig(os.path.join(output_plots_dir, "training_history.png"))
  250.     plt.show()
  251.  
  252.     # Predict on test data & invert scaling (for target only)
  253.     y_pred_scaled = model.predict(X_test).flatten()
  254.  
  255.     def inverse_target_values(pred_scaled):
  256.         arr_dummy = np.zeros((len(pred_scaled), len(feature_cols)))
  257.         arr_dummy[:, 0] = pred_scaled
  258.         arr_inv = scaler.inverse_transform(arr_dummy)
  259.         return arr_inv[:, 0]
  260.  
  261.     y_test_inverted = inverse_target_values(y_test)
  262.     y_pred_inverted = inverse_target_values(y_pred_scaled)
  263.  
  264.     plt.figure(figsize=(10,5))
  265.     # Use contrasting colors for lines
  266.     plt.plot(y_test_inverted, label='Actual ' + target_col, color='yellow')
  267.     plt.plot(y_pred_inverted, label='Predicted ' + target_col, color='lime')
  268.     plt.title("Test Set: Actual vs Predicted {} (Single-Step)".format(target_col), color='white')
  269.     plt.legend(facecolor='black')
  270.     plt.xlabel("Index", color='white')
  271.     plt.ylabel(target_col, color='white')
  272.     plt.tight_layout()
  273.     if output_plots_dir:
  274.         plt.savefig(os.path.join(output_plots_dir, "test_actual_vs_predicted.png"))
  275.     plt.show()
  276.  
  277.     # -------------------------------------------------------------------------
  278.     # 4.7) Forecast Next N Days (Iterative), Including Day-of-Week Logic
  279.     # -------------------------------------------------------------------------
  280.     last_date = df.index[-1]
  281.     last_window = X[-1].copy()
  282.     future_predictions = []
  283.     current_window = last_window
  284.     current_date = last_date
  285.  
  286.     def cyclical_dow_features(date):
  287.         dow = date.dayofweek
  288.         dow_sin = np.sin(2 * np.pi * dow / 7)
  289.         dow_cos = np.cos(2 * np.pi * dow / 7)
  290.         is_weekend = 1 if dow >= 5 else 0
  291.         return dow_sin, dow_cos, is_weekend
  292.  
  293.     for _ in range(n_future):
  294.         pred_scaled = model.predict(current_window[np.newaxis, ...])
  295.         pred_scaled_val = pred_scaled[0, 0]
  296.  
  297.         next_date = current_date + pd.Timedelta(days=1)
  298.         dow_sin, dow_cos, is_weekend = cyclical_dow_features(next_date)
  299.  
  300.         # Prepare new row (scaled)
  301.         arr_dummy = np.array([[0, dow_sin, dow_cos, is_weekend]], dtype=np.float32)
  302.         arr_dummy_scaled = scaler.transform(arr_dummy)
  303.         arr_dummy_scaled[0, 0] = pred_scaled_val
  304.        
  305.         current_window = np.concatenate([current_window[1:], arr_dummy_scaled], axis=0)
  306.  
  307.         predicted_val_unscaled = inverse_target_values(np.array([pred_scaled_val]))[0]
  308.         future_predictions.append((next_date, predicted_val_unscaled))
  309.         current_date = next_date
  310.  
  311.     print("\nNext {} days (predicted):".format(n_future))
  312.     for date, val in future_predictions:
  313.         print(f"{date.strftime('%Y-%m-%d')}: {val:.2f}")
  314.  
  315.     # -------------------------------------------------------------------------
  316.     # 4.8) Plot Last window_size Days of Actual + Next N Forecast
  317.     # -------------------------------------------------------------------------
  318.     recent_true = df[target_col][-window_size:].values
  319.     past_x = np.arange(len(recent_true))
  320.     future_x = np.arange(len(recent_true), len(recent_true) + len(future_predictions))
  321.     future_vals = [val for (_, val) in future_predictions]
  322.  
  323.     plt.figure(figsize=(10,5))
  324.     plt.plot(past_x, recent_true, label='Last {} Days (Actual)'.format(window_size), color='orange')
  325.     # 'ro-' means red line + 'o' marker. If you prefer a different color or style:
  326.     # plt.plot(future_x, future_vals, 'o-', color='red', label='Next {} Days (Forecast)'.format(n_future))
  327.     plt.plot(future_x, future_vals, 'ro-', label='Next {} Days (Forecast)'.format(n_future))
  328.     plt.title("Last {} Actual + {}-Day Forecast (With Day-of-Week Features)".format(window_size, n_future), color='white')
  329.     plt.xlabel("Day Index (relative)", color='white')
  330.     plt.ylabel(target_col, color='white')
  331.     plt.legend(facecolor='black')
  332.     plt.tight_layout()
  333.     if output_plots_dir:
  334.         plt.savefig(os.path.join(output_plots_dir, "forecast_plot.png"))
  335.     plt.show()
  336.  
  337. # -----------------------------------------------------------------------------
  338. # Run
  339. # -----------------------------------------------------------------------------
  340. if __name__ == "__main__":
  341.     main()
  342.  
Advertisement
Add Comment
Please, Sign In to add comment