|
| 1 | +"""Base classes for the TFTS benchmark system.""" |
| 2 | + |
| 3 | +from abc import ABC, abstractmethod |
| 4 | +from dataclasses import dataclass, field |
| 5 | +import logging |
| 6 | +from typing import Any, Callable, Dict, List, Optional, Tuple, Union |
| 7 | + |
| 8 | +import numpy as np |
| 9 | +import pandas as pd |
| 10 | +import tensorflow as tf |
| 11 | + |
| 12 | +logger = logging.getLogger(__name__) |
| 13 | + |
| 14 | + |
| 15 | +@dataclass |
| 16 | +class BenchmarkConfig: |
| 17 | + """Configuration for a benchmark run. |
| 18 | +
|
| 19 | + Attributes: |
| 20 | + models: List of model names to evaluate (e.g., ["rnn", "transformer"]). |
| 21 | + Use ``["all"]`` to run all registered models. |
| 22 | + datasets: List of dataset names to evaluate (e.g., ["sine", "grocery_sales"]). |
| 23 | + Use ``["all"]`` to run all registered datasets. |
| 24 | + metrics: List of metric names to compute. |
| 25 | + Available: ``"mae"``, ``"mse"``, ``"rmse"``, ``"mape"``, ``"smape"``, ``"r2"``. |
| 26 | + runs: Number of runs per model-dataset pair (for statistical significance). |
| 27 | + epochs: Number of training epochs per run. |
| 28 | + batch_size: Batch size for training. |
| 29 | + learning_rate: Learning rate for the optimizer. |
| 30 | + train_length: Lookback window length. If None, each dataset provides its own. |
| 31 | + predict_sequence_length: Forecast horizon. If None, each dataset provides its own. |
| 32 | + seed: Base seed. Each run uses ``seed + run_idx`` for reproducibility. |
| 33 | + output_dir: Directory to save results. |
| 34 | + save_models: Whether to save trained model weights. |
| 35 | + verbose: Verbosity level (0=silent, 1=progress, 2=detailed). |
| 36 | + device: Device to run on (e.g., ``"/gpu:0"`` or ``"/cpu:0"``). |
| 37 | + per_dataset_config: Optional per-dataset configuration overrides. |
| 38 | + Keys are dataset names, values are dicts with keys like ``train_length``, |
| 39 | + ``predict_sequence_length``, ``epochs``, etc. |
| 40 | + """ |
| 41 | + |
| 42 | + models: List[str] = field(default_factory=lambda: ["all"]) |
| 43 | + datasets: List[str] = field(default_factory=lambda: ["all"]) |
| 44 | + metrics: List[str] = field(default_factory=lambda: ["mae", "rmse", "mape"]) |
| 45 | + runs: int = 1 |
| 46 | + epochs: int = 50 |
| 47 | + batch_size: int = 32 |
| 48 | + learning_rate: float = 1e-3 |
| 49 | + train_length: Optional[int] = None |
| 50 | + predict_sequence_length: Optional[int] = None |
| 51 | + seed: int = 42 |
| 52 | + output_dir: str = "benchmark_results" |
| 53 | + save_models: bool = False |
| 54 | + verbose: int = 1 |
| 55 | + device: str = "" |
| 56 | + per_dataset_config: Dict[str, Dict[str, Any]] = field(default_factory=dict) |
| 57 | + |
| 58 | + def __post_init__(self): |
| 59 | + if self.runs < 1: |
| 60 | + raise ValueError("runs must be >= 1") |
| 61 | + if self.epochs < 1: |
| 62 | + raise ValueError("epochs must be >= 1") |
| 63 | + |
| 64 | + def get_dataset_config(self, dataset_name: str) -> Dict[str, Any]: |
| 65 | + """Get configuration for a specific dataset, merging with defaults.""" |
| 66 | + config = { |
| 67 | + "epochs": self.epochs, |
| 68 | + "batch_size": self.batch_size, |
| 69 | + "learning_rate": self.learning_rate, |
| 70 | + "train_length": self.train_length, |
| 71 | + "predict_sequence_length": self.predict_sequence_length, |
| 72 | + } |
| 73 | + if dataset_name in self.per_dataset_config: |
| 74 | + config.update(self.per_dataset_config[dataset_name]) |
| 75 | + return config |
| 76 | + |
| 77 | + |
| 78 | +class Dataset(ABC): |
| 79 | + """Abstract base class for benchmark datasets. |
| 80 | +
|
| 81 | + Each dataset subclass implements ``prepare_data()`` to load and format |
| 82 | + the data. The class also provides metadata about the dataset. |
| 83 | +
|
| 84 | + Attributes: |
| 85 | + name: Unique identifier for the dataset. |
| 86 | + description: Human-readable description. |
| 87 | + train_length: Default lookback window length (can be overridden by config). |
| 88 | + predict_sequence_length: Default forecast horizon (can be overridden by config). |
| 89 | + """ |
| 90 | + |
| 91 | + name: str = "" |
| 92 | + description: str = "" |
| 93 | + train_length: int = 24 |
| 94 | + predict_sequence_length: int = 8 |
| 95 | + num_features: int = 1 |
| 96 | + is_multivariate: bool = False |
| 97 | + is_grouped: bool = False |
| 98 | + target_column: str = "target" |
| 99 | + time_column: str = "time" |
| 100 | + group_column: Optional[str] = None |
| 101 | + |
| 102 | + @abstractmethod |
| 103 | + def prepare_data(self, **kwargs) -> Union[ |
| 104 | + Tuple[np.ndarray, np.ndarray], |
| 105 | + Tuple[Tuple[np.ndarray, np.ndarray], Tuple[np.ndarray, np.ndarray]], |
| 106 | + tf.data.Dataset, |
| 107 | + ]: |
| 108 | + """Prepare and return the dataset. |
| 109 | +
|
| 110 | + Returns: |
| 111 | + Either (x_train, y_train), ((x_train, y_train), (x_valid, y_valid)), |
| 112 | + or a tf.data.Dataset. |
| 113 | + """ |
| 114 | + raise NotImplementedError |
| 115 | + |
| 116 | + @abstractmethod |
| 117 | + def get_train_valid_split(self, **kwargs) -> Tuple[Tuple[np.ndarray, np.ndarray], Tuple[np.ndarray, np.ndarray]]: |
| 118 | + """Return train and validation splits. |
| 119 | +
|
| 120 | + Returns: |
| 121 | + Tuple of (x_train, y_train), (x_valid, y_valid). |
| 122 | + """ |
| 123 | + raise NotImplementedError |
| 124 | + |
| 125 | + @classmethod |
| 126 | + def list_params(cls) -> Dict[str, Any]: |
| 127 | + """Return dataset parameters for display.""" |
| 128 | + return { |
| 129 | + "name": cls.name, |
| 130 | + "description": cls.description, |
| 131 | + "train_length": cls.train_length, |
| 132 | + "predict_sequence_length": cls.predict_sequence_length, |
| 133 | + "num_features": cls.num_features, |
| 134 | + "is_multivariate": cls.is_multivariate, |
| 135 | + "is_grouped": cls.is_grouped, |
| 136 | + } |
0 commit comments