Skip to content

Commit 8080be5

Browse files
committed
[ENH] Implement NBeatsKAN in v2 interface
1 parent 243bdbc commit 8080be5

5 files changed

Lines changed: 416 additions & 1 deletion

File tree

pytorch_forecasting/models/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@
1111
from pytorch_forecasting.models.baseline import Baseline
1212
from pytorch_forecasting.models.deepar import DeepAR
1313
from pytorch_forecasting.models.mlp import DecoderMLP
14-
from pytorch_forecasting.models.nbeats import NBeats, NBeatsKAN
14+
from pytorch_forecasting.models.nbeats import (
15+
NBeats,
16+
NBeatsKAN,
17+
NBeatsKAN_pkg_v2,
18+
NBeatsKAN_v2,
19+
)
1520
from pytorch_forecasting.models.nhits import NHiTS
1621
from pytorch_forecasting.models.nn import GRU, LSTM, MultiEmbedding, get_rnn
1722
from pytorch_forecasting.models.rnn import RecurrentNetwork
@@ -26,6 +31,8 @@
2631
__all__ = [
2732
"NBeats",
2833
"NBeatsKAN",
34+
"NBeatsKAN_v2",
35+
"NBeatsKAN_pkg_v2",
2936
"NHiTS",
3037
"TemporalFusionTransformer",
3138
"RecurrentNetwork",

pytorch_forecasting/models/nbeats/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,14 @@
1616
from pytorch_forecasting.models.nbeats._nbeats_pkg import NBeats_pkg
1717
from pytorch_forecasting.models.nbeats._nbeatskan import NBeatsKAN
1818
from pytorch_forecasting.models.nbeats._nbeatskan_pkg import NBeatsKAN_pkg
19+
from pytorch_forecasting.models.nbeats._nbeatskan_pkg_v2 import NBeatsKAN_pkg_v2
20+
from pytorch_forecasting.models.nbeats._nbeatskan_v2 import NBeatsKAN_v2
1921

2022
__all__ = [
2123
"NBeats",
2224
"NBeatsKAN",
25+
"NBeatsKAN_v2",
26+
"NBeatsKAN_pkg_v2",
2327
"NBeats_pkg",
2428
"NBeatsKAN_pkg",
2529
"NBEATSGenericBlock",
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""NBeatsKAN v2 package container."""
2+
3+
from typing import Any
4+
5+
from pytorch_forecasting.base._base_pkg import Base_pkg
6+
7+
8+
class NBeatsKAN_pkg_v2(Base_pkg):
9+
"""NBeatsKAN v2 package container."""
10+
11+
_tags: dict[str, Any] = {
12+
"info:name": "NBeatsKAN",
13+
"info:compute": 2,
14+
"info:y_type": ["numeric"],
15+
"authors": ["Dev10-sys", "kalpanagola9897"],
16+
"capability:exogenous": False,
17+
"capability:multivariate": False,
18+
"capability:pred_int": True,
19+
"capability:flexible_history_length": False,
20+
"capability:cold_start": False,
21+
"python_dependencies": [],
22+
}
23+
24+
@classmethod
25+
def get_cls(cls):
26+
"""Get the model class."""
27+
from pytorch_forecasting.models.nbeats._nbeatskan_v2 import NBeatsKAN_v2
28+
29+
return NBeatsKAN_v2
30+
31+
@classmethod
32+
def get_datamodule_cls(cls):
33+
"""Get the compatible DataModule class."""
34+
from pytorch_forecasting.data.data_module import (
35+
EncoderDecoderTimeSeriesDataModule,
36+
)
37+
38+
return EncoderDecoderTimeSeriesDataModule
39+
40+
@classmethod
41+
def get_test_train_params(cls) -> list[dict[str, Any]]:
42+
"""Return testing parameter settings for trainer fixtures."""
43+
from pytorch_forecasting.metrics import MAE
44+
45+
params: list[dict[str, Any]] = [
46+
{},
47+
{"stack_types": ["generic"], "num_blocks": [1], "widths": [32]},
48+
{"loss": MAE()},
49+
]
50+
51+
default_dm_cfg: dict[str, Any] = {
52+
"max_encoder_length": 8,
53+
"max_prediction_length": 3,
54+
}
55+
56+
for param in params:
57+
current_dm_cfg = param.get("datamodule_cfg", {})
58+
if isinstance(current_dm_cfg, dict):
59+
default_dm_cfg.update(current_dm_cfg)
60+
param["datamodule_cfg"] = default_dm_cfg.copy()
61+
62+
return params
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
"""NBeatsKAN model for PyTorch Forecasting v2."""
2+
3+
from collections.abc import Callable
4+
from typing import Any
5+
6+
import torch
7+
from torch import nn
8+
from torch.optim import Optimizer
9+
10+
from pytorch_forecasting.layers._nbeats._blocks import (
11+
NBEATSGenericBlockKAN,
12+
NBEATSSeasonalBlockKAN,
13+
NBEATSTrendBlockKAN,
14+
)
15+
from pytorch_forecasting.metrics import MAE, Metric
16+
from pytorch_forecasting.models.base._base_model_v2 import BaseModel
17+
18+
19+
class NBeatsKAN_v2(BaseModel):
20+
"""N-BEATS model with Kolmogorov-Arnold Network (KAN) spline layers for v2.
21+
22+
Parameters
23+
----------
24+
loss : Metric, default=MAE()
25+
Loss metric used for training and evaluation.
26+
stack_types : list of str, optional
27+
List of stack types: ``"generic"``, ``"trend"``, or ``"seasonality"``.
28+
num_blocks : list of int, optional
29+
Number of blocks per stack.
30+
num_block_layers : list of int, optional
31+
Number of KAN layers per block.
32+
widths : list of int, optional
33+
Widths of layers in blocks.
34+
sharing : list of bool, optional
35+
Whether blocks share weights per stack.
36+
expansion_coefficient_lengths : list of int, optional
37+
Expansion lengths or polynomial degrees per stack.
38+
dropout : float, default=0.1
39+
Dropout rate.
40+
logging_metrics : list of nn.Module, optional
41+
Logged evaluation metrics.
42+
optimizer : Optimizer or str, default="adam"
43+
Optimizer used for training.
44+
optimizer_params : dict, optional
45+
Optimizer parameters.
46+
lr_scheduler : str, optional
47+
Learning rate scheduler name.
48+
lr_scheduler_params : dict, optional
49+
Parameters for the learning rate scheduler.
50+
num : int, default=5
51+
KAN grid intervals.
52+
k : int, default=3
53+
KAN spline polynomial order.
54+
noise_scale : float, default=0.5
55+
KAN noise scale at initialization.
56+
scale_base_mu : float, default=0.0
57+
KAN base scale mean.
58+
scale_base_sigma : float, default=1.0
59+
KAN base scale std.
60+
scale_sp : float, default=1.0
61+
KAN spline scale.
62+
base_fun : Callable, optional
63+
KAN residual base activation function.
64+
grid_eps : float, default=0.02
65+
KAN grid interpolation parameter.
66+
grid_range : list of int, optional
67+
KAN grid range boundaries.
68+
sp_trainable : bool, default=True
69+
Whether spline scale is trainable.
70+
sb_trainable : bool, default=True
71+
Whether base scale is trainable.
72+
sparse_init : bool, default=False
73+
Whether sparse initialization is used.
74+
metadata : dict, optional
75+
Metadata from DataModule.
76+
"""
77+
78+
@classmethod
79+
def _pkg(cls):
80+
"""Package container for the model."""
81+
from pytorch_forecasting.models.nbeats._nbeatskan_pkg_v2 import (
82+
NBeatsKAN_pkg_v2,
83+
)
84+
85+
return NBeatsKAN_pkg_v2
86+
87+
def __init__(
88+
self,
89+
loss: Metric = MAE(),
90+
stack_types: list[str] | None = None,
91+
num_blocks: list[int] | None = None,
92+
num_block_layers: list[int] | None = None,
93+
widths: list[int] | None = None,
94+
sharing: list[bool] | None = None,
95+
expansion_coefficient_lengths: list[int] | None = None,
96+
dropout: float = 0.1,
97+
logging_metrics: list[nn.Module] | None = None,
98+
optimizer: Optimizer | str | None = "adam",
99+
optimizer_params: dict | None = None,
100+
lr_scheduler: str | None = None,
101+
lr_scheduler_params: dict | None = None,
102+
num: int = 5,
103+
k: int = 3,
104+
noise_scale: float = 0.5,
105+
scale_base_mu: float = 0.0,
106+
scale_base_sigma: float = 1.0,
107+
scale_sp: float = 1.0,
108+
base_fun: Callable | None = None,
109+
grid_eps: float = 0.02,
110+
grid_range: list[int] | None = None,
111+
sp_trainable: bool = True,
112+
sb_trainable: bool = True,
113+
sparse_init: bool = False,
114+
metadata: dict[str, Any] | None = None,
115+
**kwargs,
116+
):
117+
super().__init__(
118+
loss=loss,
119+
logging_metrics=logging_metrics,
120+
optimizer=optimizer,
121+
optimizer_params=optimizer_params,
122+
lr_scheduler=lr_scheduler,
123+
lr_scheduler_params=lr_scheduler_params,
124+
)
125+
self.save_hyperparameters(
126+
ignore=["loss", "logging_metrics", "metadata", "base_fun"]
127+
)
128+
self.metadata = metadata or {}
129+
130+
self.max_encoder_length = self.metadata.get("max_encoder_length", 10)
131+
self.max_prediction_length = self.metadata.get("max_prediction_length", 1)
132+
133+
if base_fun is None:
134+
base_fun = nn.SiLU()
135+
if grid_range is None:
136+
grid_range = [-1, 1]
137+
if expansion_coefficient_lengths is None:
138+
expansion_coefficient_lengths = [3, 7]
139+
if sharing is None:
140+
sharing = [True, True]
141+
if widths is None:
142+
widths = [32, 512]
143+
if num_block_layers is None:
144+
num_block_layers = [3, 3]
145+
if num_blocks is None:
146+
num_blocks = [3, 3]
147+
if stack_types is None:
148+
stack_types = ["trend", "seasonality"]
149+
150+
kan_params = {
151+
"num": num,
152+
"k": k,
153+
"noise_scale": noise_scale,
154+
"scale_base_mu": scale_base_mu,
155+
"scale_base_sigma": scale_base_sigma,
156+
"scale_sp": scale_sp,
157+
"base_fun": base_fun,
158+
"grid_eps": grid_eps,
159+
"grid_range": grid_range,
160+
"sp_trainable": sp_trainable,
161+
"sb_trainable": sb_trainable,
162+
"sparse_init": sparse_init,
163+
}
164+
165+
self.net_blocks = nn.ModuleList()
166+
for stack_id, stack_type in enumerate(stack_types):
167+
for _ in range(num_blocks[stack_id]):
168+
net_block: nn.Module
169+
if stack_type == "generic":
170+
net_block = NBEATSGenericBlockKAN(
171+
units=widths[stack_id],
172+
thetas_dim=expansion_coefficient_lengths[stack_id],
173+
num_block_layers=num_block_layers[stack_id],
174+
backcast_length=self.max_encoder_length,
175+
forecast_length=self.max_prediction_length,
176+
dropout=dropout,
177+
**kan_params,
178+
)
179+
elif stack_type == "seasonality":
180+
net_block = NBEATSSeasonalBlockKAN(
181+
units=widths[stack_id],
182+
thetas_dim=expansion_coefficient_lengths[stack_id],
183+
num_block_layers=num_block_layers[stack_id],
184+
backcast_length=self.max_encoder_length,
185+
forecast_length=self.max_prediction_length,
186+
nb_harmonics=None, # type: ignore[arg-type]
187+
min_period=expansion_coefficient_lengths[stack_id],
188+
dropout=dropout,
189+
**kan_params,
190+
)
191+
192+
elif stack_type == "trend":
193+
net_block = NBEATSTrendBlockKAN(
194+
units=widths[stack_id],
195+
thetas_dim=expansion_coefficient_lengths[stack_id],
196+
num_block_layers=num_block_layers[stack_id],
197+
backcast_length=self.max_encoder_length,
198+
forecast_length=self.max_prediction_length,
199+
dropout=dropout,
200+
**kan_params,
201+
)
202+
else:
203+
raise ValueError(f"Unknown stack_type: {stack_type}")
204+
205+
self.net_blocks.append(net_block)
206+
207+
def forward(
208+
self,
209+
x: dict[str, torch.Tensor],
210+
) -> dict[str, torch.Tensor]:
211+
"""Forward pass for NBeatsKAN_v2.
212+
213+
Parameters
214+
----------
215+
x : dict[str, torch.Tensor]
216+
Input dictionary containing ``target_past``.
217+
218+
Returns
219+
-------
220+
dict[str, torch.Tensor]
221+
Dictionary containing predicted output tensor under key ``prediction``.
222+
"""
223+
target_past = x.get("target_past")
224+
if target_past is None:
225+
raise KeyError("Input dictionary must contain key 'target_past'.")
226+
227+
if target_past.ndim == 3 and target_past.size(-1) == 1:
228+
target_past = target_past.squeeze(-1)
229+
230+
backcast = target_past
231+
forecast = torch.zeros(
232+
backcast.size(0),
233+
self.max_prediction_length,
234+
device=backcast.device,
235+
dtype=backcast.dtype,
236+
)
237+
238+
for block in self.net_blocks:
239+
b, f = block(backcast)
240+
backcast = backcast - b
241+
forecast = forecast + f
242+
243+
prediction = forecast.unsqueeze(-1)
244+
return {"prediction": prediction}

0 commit comments

Comments
 (0)