Skip to content

Commit 9c2797b

Browse files
authored
Merge branch 'main' into nidhi_nn_losses
2 parents 352b3a3 + 033169a commit 9c2797b

12 files changed

Lines changed: 366 additions & 58 deletions

File tree

docs/source/m_layer_v2.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,4 @@ See the detailed API documentation for the V2 base classes and specific model im
4747
models.samformer._samformer_v2.Samformer
4848
models.tide._tide_dsipts._tide_v2.TIDE
4949
models.timexer._timexer_v2.TimeXer
50+
models.mlp._decodermlp_v2.DecoderMLP_v2

docs/source/pkg_v2.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,4 @@ See the detailed API documentation for the available V2 Package classes below:
9999
models.samformer._samformer_v2_pkg.Samformer_pkg_v2
100100
models.tide._tide_dsipts._tide_v2_pkg.TIDE_pkg_v2
101101
models.timexer._timexer_pkg_v2.TimeXer_pkg_v2
102+
models.mlp._decodermlp_pkg_v2.DecoderMLP_pkg_v2

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ dependencies = [
3333
"scipy >=1.8,<2.0",
3434
"pandas >=1.3.0,<3.1.0",
3535
"scikit-learn >=1.2,<2.0",
36-
"scikit-base <1.1.0",
36+
"scikit-base <1.2.0",
3737
]
3838

3939
[project.optional-dependencies]

pytorch_forecasting/layers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
Encoder,
2020
EncoderLayer,
2121
)
22+
from pytorch_forecasting.layers._mlp import FullyConnectedModule
2223
from pytorch_forecasting.layers._normalization import RevIN
2324
from pytorch_forecasting.layers._output._flatten_head import (
2425
FlattenHead,
@@ -54,4 +55,5 @@
5455
"RevIN",
5556
"ResidualBlock",
5657
"embedding_cat_variables",
58+
"FullyConnectedModule",
5759
]
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""
2+
Fully connected (MLP) layers.
3+
"""
4+
5+
from pytorch_forecasting.layers._mlp._fully_connected import FullyConnectedModule
6+
7+
__all__ = ["FullyConnectedModule"]
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""
2+
Fully connected (MLP) module.
3+
"""
4+
5+
import torch
6+
from torch import nn
7+
8+
9+
class FullyConnectedModule(nn.Module):
10+
def __init__(
11+
self,
12+
input_size: int,
13+
output_size: int,
14+
hidden_size: int,
15+
n_hidden_layers: int,
16+
activation_class: nn.ReLU,
17+
dropout: float = None,
18+
norm: bool = True,
19+
):
20+
super().__init__()
21+
self.input_size = input_size
22+
self.output_size = output_size
23+
self.hidden_size = hidden_size
24+
self.n_hidden_layers = n_hidden_layers
25+
self.activation_class = activation_class
26+
self.dropout = dropout
27+
self.norm = norm
28+
29+
# input layer
30+
module_list = [nn.Linear(input_size, hidden_size), activation_class()]
31+
if dropout is not None:
32+
module_list.append(nn.Dropout(dropout))
33+
if norm:
34+
module_list.append(nn.LayerNorm(hidden_size))
35+
# hidden layers
36+
for _ in range(n_hidden_layers):
37+
module_list.extend(
38+
[nn.Linear(hidden_size, hidden_size), activation_class()]
39+
)
40+
if dropout is not None:
41+
module_list.append(nn.Dropout(dropout))
42+
if norm:
43+
module_list.append(nn.LayerNorm(hidden_size))
44+
# output layer
45+
module_list.append(nn.Linear(hidden_size, output_size))
46+
47+
self.sequential = nn.Sequential(*module_list)
48+
49+
def forward(self, x: torch.Tensor) -> torch.Tensor:
50+
# x of shape: batch_size x n_timesteps_in
51+
# output of shape batch_size x n_timesteps_out
52+
return self.sequential(x)

pytorch_forecasting/metrics/base_metrics/_base_metrics.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -703,18 +703,22 @@ def to_quantiles(self, y_pred: torch.Tensor, **kwargs) -> torch.Tensor:
703703
return self._metrics[0].to_quantiles(y_pred, **kwargs)
704704

705705
def __add__(self, metric: LightningMetric):
706+
new_metrics = list(self._metrics)
707+
new_weights = list(self._weights)
706708
if isinstance(metric, self.__class__):
707-
self._metrics.extend(metric._metrics)
708-
self._weights.extend(metric._weights)
709+
new_metrics.extend(metric._metrics)
710+
new_weights.extend(metric._weights)
709711
else:
710-
self._metrics.append(metric)
711-
self._weights.append(1.0)
712+
new_metrics.append(metric)
713+
new_weights.append(1.0)
712714

713-
return self
715+
result = CompositeMetric(metrics=new_metrics, weights=new_weights)
716+
return result
714717

715718
def __mul__(self, multiplier: float):
716-
self._weights = [w * multiplier for w in self._weights]
717-
return self
719+
new_weights = [w * multiplier for w in self._weights]
720+
result = CompositeMetric(metrics=list(self._metrics), weights=new_weights)
721+
return result
718722

719723
__rmul__ = __mul__
720724

pytorch_forecasting/models/mlp/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
from pytorch_forecasting.models.mlp._decodermlp import DecoderMLP
44
from pytorch_forecasting.models.mlp._decodermlp_pkg import DecoderMLP_pkg
5+
from pytorch_forecasting.models.mlp._decodermlp_pkg_v2 import DecoderMLP_pkg_v2
6+
from pytorch_forecasting.models.mlp._decodermlp_v2 import DecoderMLP_v2
57
from pytorch_forecasting.models.mlp.submodules import FullyConnectedModule
68

7-
__all__ = ["DecoderMLP", "DecoderMLP_pkg", "FullyConnectedModule"]
9+
__all__ = [
10+
"DecoderMLP",
11+
"DecoderMLP_pkg",
12+
"DecoderMLP_v2",
13+
"DecoderMLP_pkg_v2",
14+
"FullyConnectedModule",
15+
]
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""DecoderMLP v2 package container."""
2+
3+
from pytorch_forecasting.base._base_pkg import Base_pkg
4+
5+
6+
class DecoderMLP_pkg_v2(Base_pkg):
7+
"""DecoderMLP v2 package container."""
8+
9+
_tags = {
10+
"info:name": "DecoderMLP_v2",
11+
"info:compute": 1,
12+
"authors": ["jdb78", "echo-xiao"],
13+
# TODO: the v2 datamodule supports categorical inputs but not
14+
# categorical targets yet; add "categorical" to y_type once
15+
# EncoderDecoderTimeSeriesDataModule supports categorical targets.
16+
"info:y_type": ["numeric"],
17+
"capability:exogenous": True,
18+
"capability:multivariate": False,
19+
"capability:pred_int": True,
20+
"capability:flexible_history_length": True,
21+
"capability:cold_start": True,
22+
}
23+
24+
@classmethod
25+
def get_cls(cls):
26+
"""Get model class."""
27+
from pytorch_forecasting.models.mlp._decodermlp_v2 import DecoderMLP_v2
28+
29+
return DecoderMLP_v2
30+
31+
@classmethod
32+
def get_datamodule_cls(cls):
33+
"""Get the underlying 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):
42+
"""Return testing parameter settings for the trainer.
43+
44+
Returns
45+
-------
46+
params : list of dict
47+
Parameters to create testing instances of the class. Each dict is passed
48+
as ``model_cfg`` to the package constructor; the ``"datamodule_cfg"`` key
49+
is forwarded to the datamodule constructor.
50+
"""
51+
from pytorch_forecasting.metrics import MAE, RMSE, SMAPE, QuantileLoss
52+
53+
params = [
54+
{},
55+
dict(
56+
hidden_size=64, n_hidden_layers=2, dropout=0.1, norm=True, loss=RMSE()
57+
),
58+
dict(
59+
hidden_size=128,
60+
n_hidden_layers=1,
61+
activation_class="ReLU",
62+
loss=SMAPE(),
63+
logging_metrics=[MAE()],
64+
),
65+
dict(hidden_size=32, n_hidden_layers=2, norm=False, loss=MAE()),
66+
dict(hidden_size=64, n_hidden_layers=1, loss=QuantileLoss()),
67+
dict(
68+
optimizer="adamw",
69+
lr_scheduler="cosine_annealing",
70+
lr_scheduler_params={"T_max": 5},
71+
loss=MAE(),
72+
),
73+
]
74+
75+
default_dm_cfg = {"max_encoder_length": 4, "max_prediction_length": 3}
76+
for param in params:
77+
dm_cfg = default_dm_cfg.copy()
78+
dm_cfg.update(param.get("datamodule_cfg", {}))
79+
param["datamodule_cfg"] = dm_cfg
80+
81+
return params
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
"""Decoder-only MLP for pytorch-forecasting v2."""
2+
3+
########################################################################################
4+
# Disclaimer: This implementation is based on the new v2 data pipeline and is
5+
# experimental, please use with care.
6+
########################################################################################
7+
8+
import torch
9+
import torch.nn as nn
10+
from torch.optim import Optimizer
11+
12+
from pytorch_forecasting.layers import FullyConnectedModule
13+
from pytorch_forecasting.metrics import QuantileLoss
14+
from pytorch_forecasting.models.base._base_model_v2 import BaseModel
15+
16+
17+
class DecoderMLP_v2(BaseModel):
18+
"""MLP on the decoder for pytorch-forecasting v2.
19+
20+
Predicts each future step purely from information known in the decoder
21+
(future-known covariates and static features). It intentionally does not use the
22+
encoder or target history -- it is a lightweight, covariate-driven baseline. The
23+
original v1 ``DecoderMLP`` was authored by the pytorch-forecasting team.
24+
25+
Parameters
26+
----------
27+
loss : nn.Module
28+
Loss function for training (required).
29+
hidden_size : int, default=300
30+
Hidden layer width of the MLP.
31+
n_hidden_layers : int, default=3
32+
Number of hidden layers.
33+
dropout : float, default=0.1
34+
Dropout probability.
35+
norm : bool, default=True
36+
Whether to apply ``LayerNorm`` in the MLP.
37+
activation_class : str, default="ReLU"
38+
Name of a ``torch.nn`` activation class.
39+
logging_metrics : Optional[list[nn.Module]], default=None
40+
Metrics to log during training, validation, and testing.
41+
optimizer : Optional[Union[Optimizer, str]], default="adam"
42+
Optimizer to use for training.
43+
optimizer_params : Optional[dict], default=None
44+
Parameters for the optimizer.
45+
lr_scheduler : Optional[str], default=None
46+
Learning rate scheduler to use.
47+
lr_scheduler_params : Optional[dict], default=None
48+
Parameters for the learning rate scheduler.
49+
metadata : Optional[dict], default=None
50+
Metadata from ``EncoderDecoderTimeSeriesDataModule``.
51+
"""
52+
53+
@classmethod
54+
def _pkg(cls):
55+
"""Package containing the model."""
56+
from pytorch_forecasting.models.mlp._decodermlp_pkg_v2 import DecoderMLP_pkg_v2
57+
58+
return DecoderMLP_pkg_v2
59+
60+
def __init__(
61+
self,
62+
loss: nn.Module,
63+
hidden_size: int = 300,
64+
n_hidden_layers: int = 3,
65+
dropout: float = 0.1,
66+
norm: bool = True,
67+
activation_class: str = "ReLU",
68+
logging_metrics: list[nn.Module] | None = None,
69+
optimizer: Optimizer | str | None = "adam",
70+
optimizer_params: dict | None = None,
71+
lr_scheduler: str | None = None,
72+
lr_scheduler_params: dict | None = None,
73+
metadata: dict | None = None,
74+
):
75+
super().__init__(
76+
loss=loss,
77+
logging_metrics=logging_metrics,
78+
optimizer=optimizer,
79+
optimizer_params=optimizer_params,
80+
lr_scheduler=lr_scheduler,
81+
lr_scheduler_params=lr_scheduler_params,
82+
)
83+
self.hidden_size = hidden_size
84+
self.n_hidden_layers = n_hidden_layers
85+
self.dropout = dropout
86+
self.norm = norm
87+
self.activation_class = activation_class
88+
self.metadata = metadata if metadata is not None else {}
89+
90+
self.save_hyperparameters(ignore=["loss", "logging_metrics", "metadata"])
91+
92+
# all dimensions are derived from metadata -- never hardcoded
93+
self.decoder_cont_dim = self.metadata.get("decoder_cont", 0)
94+
self.decoder_cat_dim = self.metadata.get("decoder_cat", 0)
95+
self.static_cat_dim = self.metadata.get("static_categorical_features", 0)
96+
self.static_cont_dim = self.metadata.get("static_continuous_features", 0)
97+
self.prediction_length = self.metadata.get("max_prediction_length", 1)
98+
self.target_dim = self.metadata.get("target", 1)
99+
100+
self._init_network()
101+
102+
def _init_network(self):
103+
"""Build the per-step MLP from the derived dimensions."""
104+
self.input_size = (
105+
self.decoder_cont_dim
106+
+ self.decoder_cat_dim
107+
+ self.static_cat_dim
108+
+ self.static_cont_dim
109+
)
110+
111+
self.n_quantiles = None
112+
if isinstance(self.loss, QuantileLoss):
113+
self.n_quantiles = len(self.loss.quantiles)
114+
self.output_size = self.n_quantiles if self.n_quantiles is not None else 1
115+
116+
self.mlp = FullyConnectedModule(
117+
input_size=max(1, self.input_size),
118+
output_size=self.output_size,
119+
hidden_size=self.hidden_size,
120+
n_hidden_layers=self.n_hidden_layers,
121+
activation_class=getattr(nn, self.activation_class),
122+
dropout=self.dropout,
123+
norm=self.norm,
124+
)
125+
126+
def forward(self, x: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
127+
"""Forward pass: per-step MLP over decoder and static features.
128+
129+
Parameters
130+
----------
131+
x : dict[str, torch.Tensor]
132+
Input dictionary containing at least ``decoder_cont`` and, when the
133+
corresponding metadata dimensions are non-zero, ``decoder_cat``,
134+
``static_continuous_features`` and ``static_categorical_features``.
135+
136+
Returns
137+
-------
138+
dict[str, torch.Tensor]
139+
``{"prediction": tensor}`` of shape
140+
``(batch_size, prediction_length, output_size)``.
141+
"""
142+
decoder_cont = x["decoder_cont"]
143+
batch_size = decoder_cont.shape[0]
144+
pred_len = self.prediction_length
145+
device = decoder_cont.device
146+
dtype = decoder_cont.dtype
147+
148+
features = []
149+
if self.decoder_cont_dim > 0:
150+
features.append(x["decoder_cont"])
151+
if self.decoder_cat_dim > 0:
152+
features.append(x["decoder_cat"].to(dtype))
153+
if self.static_cont_dim > 0:
154+
features.append(x["static_continuous_features"].expand(-1, pred_len, -1))
155+
if self.static_cat_dim > 0:
156+
features.append(
157+
x["static_categorical_features"].to(dtype).expand(-1, pred_len, -1)
158+
)
159+
160+
if features:
161+
network_input = torch.cat(features, dim=-1)
162+
else:
163+
network_input = torch.zeros(
164+
batch_size, pred_len, 1, device=device, dtype=dtype
165+
)
166+
167+
prediction = self.mlp(network_input.reshape(-1, self.mlp.input_size)).reshape(
168+
batch_size, pred_len, self.output_size
169+
)
170+
171+
return {"prediction": prediction}

0 commit comments

Comments
 (0)