-
Notifications
You must be signed in to change notification settings - Fork 885
[ENH] softs_v2 Model added #2232
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 25 commits
cd931ac
c90e494
6026e0e
fcb7cad
2db264b
5026e82
4d19fa8
a44150c
927c50e
b2aa1f6
a18a499
0087cb8
8fb4616
0afece8
b3f75a9
ed02f22
7a33e11
b460ae8
d7b34b9
dae93e8
c0f6f17
c8ca76f
f1a2614
d35353c
5c75c71
9a71c1d
f35af9a
c8528ed
844d82b
371fca3
aa653fa
dd94e8b
55a1bc5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,11 @@ | ||
| from pytorch_forecasting.layers._blocks._residual_block_dsipts import ResidualBlock | ||
| from pytorch_forecasting.layers._blocks._softs_block import ( | ||
| SOFTSEncoderLayer, | ||
| STADModule, | ||
| ) | ||
|
|
||
| __all__ = ["ResidualBlock"] | ||
| __all__ = [ | ||
| "ResidualBlock", | ||
| "STADModule", | ||
| "SOFTSEncoderLayer", | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| """ | ||
| SOFTS Blocks for Star Aggregate-Dispatch Network. | ||
| """ | ||
|
|
||
| import torch | ||
| import torch.nn as nn | ||
|
|
||
|
|
||
| class STADModule(nn.Module): | ||
| """ | ||
| Star Aggregate-Dispatch (STAD) Module for capturing inter-series dependencies. | ||
|
|
||
| Uses a star-topology to aggregate all channels into a central node, | ||
| process it via an MLP, and dispatch back — achieving O(C) cross-channel | ||
| mixing instead of O(C²) self-attention. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| d_model : int | ||
| Embedding dimension per channel per time step. | ||
| d_core : int | ||
| Dimension of the central star node (information bottleneck). | ||
| dropout : float, default=0.0 | ||
| Dropout probability inside the channel-mixing MLP. | ||
| """ | ||
|
|
||
| def __init__(self, d_model: int, d_core: int, dropout: float = 0.0): | ||
| super().__init__() | ||
| self.channel_mixing = nn.Sequential( | ||
| nn.Linear(d_model, d_model), | ||
| nn.GELU(), | ||
| nn.Dropout(dropout), | ||
| nn.Linear(d_model, d_model), | ||
| ) | ||
| self.gen_weight = nn.Linear(d_model, d_core) | ||
|
|
||
| def forward(self, x: torch.Tensor) -> torch.Tensor: | ||
| """Aggregate channel features into a star node and dispatch back. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| x : torch.Tensor | ||
| Shape ``(batch_size, n_channels, seq_len, d_model)``. | ||
|
|
||
| Returns | ||
| ------- | ||
| torch.Tensor | ||
| Same shape as input, enriched with cross-channel context. | ||
| """ | ||
|
|
||
| B, C, L, D = x.shape | ||
|
|
||
| w = self.gen_weight(x).mean(dim=2) | ||
| w = torch.softmax(w, dim=1) | ||
|
|
||
| x_pooled = x.mean(dim=2) | ||
| core_node = torch.einsum("bcd,bce->bed", x_pooled, w) | ||
| core_node = self.channel_mixing(core_node) | ||
| dispatch_out = torch.einsum("bed,bce->bcd", core_node, w) | ||
|
|
||
| dispatch_out = dispatch_out.unsqueeze(2).repeat(1, 1, L, 1) | ||
| return x + dispatch_out | ||
|
|
||
|
|
||
| class SOFTSEncoderLayer(nn.Module): | ||
| """ | ||
| Single encoder layer for SOFTS, combining STAD and a Feed-Forward Network. | ||
|
|
||
| Applies Pre-LayerNorm STAD (cross-channel) then FFN (within-channel) | ||
| with residual connections, following the Pre-LN Transformer convention. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| d_model : int | ||
| Embedding dimension per channel per time step. | ||
| d_core : int | ||
| Dimension of the central star node in the STAD sub-layer. | ||
| d_ff : int | ||
| Hidden dimension of the feed-forward network (typically 4 x d_model). | ||
| dropout : float, default=0.0 | ||
| Dropout probability applied after the STAD and FFN sub-layers. | ||
| """ | ||
|
|
||
| def __init__(self, d_model: int, d_core: int, d_ff: int, dropout: float = 0.0): | ||
| super().__init__() | ||
| self.stad = STADModule(d_model=d_model, d_core=d_core, dropout=dropout) | ||
| self.ffn = nn.Sequential( | ||
| nn.Linear(d_model, d_ff), | ||
| nn.GELU(), | ||
| nn.Dropout(dropout), | ||
| nn.Linear(d_ff, d_model), | ||
| ) | ||
| self.norm1 = nn.LayerNorm(d_model) | ||
| self.norm2 = nn.LayerNorm(d_model) | ||
| self.dropout = nn.Dropout(dropout) | ||
|
|
||
| def forward(self, x: torch.Tensor) -> torch.Tensor: | ||
| """ | ||
| Apply one SOFTS encoder layer: STAD sub-layer then FFN sub-layer. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| x : torch.Tensor | ||
| Input tensor of shape ``(batch_size, n_channels, seq_len, d_model)``. | ||
|
|
||
| Returns | ||
| ------- | ||
| torch.Tensor | ||
| Output tensor of shape ``(batch_size, n_channels, seq_len, d_model)``. | ||
| """ | ||
| x = x + self.dropout(self.stad(self.norm1(x))) | ||
| x = x + self.dropout(self.ffn(self.norm2(x))) | ||
|
|
||
| return x | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| """ | ||
| SOFTS Model for Multivariate Time Series Forecasting. | ||
| """ | ||
|
|
||
| from pytorch_forecasting.models.softs._softs_pkg_v2 import SOFTS_pkg_v2 | ||
| from pytorch_forecasting.models.softs._softs_v2 import SOFTS | ||
|
|
||
| __all__ = ["SOFTS", "SOFTS_pkg_v2"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| """ | ||
| Packages container for SOFTS model. | ||
| """ | ||
|
|
||
| from pytorch_forecasting.base._base_pkg import Base_pkg | ||
|
|
||
|
|
||
| class SOFTS_pkg_v2(Base_pkg): | ||
| """ | ||
| SOFTS package container. | ||
| Reference : https://arxiv.org/abs/2404.14197 | ||
| """ | ||
|
|
||
| _tags = { | ||
| "info:name": "SOFTS", | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the name should be same as the class here:
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it should be
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Then you would have to update the class name. The tag |
||
| "info:y_type": ["numeric"], | ||
| "info:compute": 2, | ||
| "authors": ["Secilia-Cxy", "Muhammad-Rebaal"], | ||
| "capability:exogenous": True, | ||
| "capability:multivariate": True, | ||
| "capability:pred_int": True, | ||
| "capability:flexible_history_length": True, | ||
| "capability:cold_start": False, | ||
| } | ||
|
|
||
| @classmethod | ||
| def get_cls(cls): | ||
| """Get model class.""" | ||
| from pytorch_forecasting.models.softs._softs_v2 import SOFTS | ||
|
|
||
| return SOFTS | ||
|
|
||
| @classmethod | ||
| def get_datamodule_cls(cls): | ||
| """Get the underlying DataModule class.""" | ||
| from pytorch_forecasting.data.data_module import ( | ||
| EncoderDecoderTimeSeriesDataModule, | ||
| ) | ||
|
|
||
| return EncoderDecoderTimeSeriesDataModule | ||
|
|
||
| @classmethod | ||
| def get_test_train_params(cls): | ||
| """Return testing parameter settings for the trainer. | ||
|
|
||
| Returns | ||
| ------- | ||
| list of dict | ||
| Each dict is a valid set of constructor arguments for ``SOFTS``. | ||
| The key ``datamodule_cfg`` is passed to the DataModule, not the model. | ||
| """ | ||
| from pytorch_forecasting.metrics import MAE, MAPE, RMSE, SMAPE | ||
|
|
||
| params = [ | ||
| {}, | ||
| dict(hidden_size=64, d_core=64, d_ff=256, n_layers=1), | ||
| dict(hidden_size=128, n_layers=1, use_revin=False), | ||
| dict( | ||
| hidden_size=64, | ||
| n_layers=1, | ||
| loss=MAE(), | ||
| ), | ||
| dict( | ||
| hidden_size=64, | ||
| n_layers=1, | ||
| loss=MAPE(), | ||
| ), | ||
| dict( | ||
| hidden_size=64, | ||
| n_layers=1, | ||
| loss=RMSE(), | ||
| ), | ||
| dict( | ||
| hidden_size=64, | ||
| n_layers=1, | ||
| use_revin=False, | ||
| loss=MAE(), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be good if we could add other point prediction losses here as well - just to increase the coverage
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for letting me know I've updated that |
||
| ), | ||
| dict(hidden_size=64, dropout=0.0, n_layers=1), | ||
| dict(datamodule_cfg=dict(max_encoder_length=16, max_prediction_length=4)), | ||
| dict( | ||
| optimizer="adamw", | ||
| lr_scheduler="cosine_annealing", | ||
| lr_scheduler_params={"T_max": 5}, | ||
| ), | ||
| dict( | ||
| optimizer="adagrad", | ||
| optimizer_params={"lr": 1e-3}, | ||
| ), | ||
| dict(hidden_size=64, n_layers=1, logging_metrics=[SMAPE()]), | ||
| ] | ||
|
|
||
| default_dm_cfg = {"max_encoder_length": 8, "max_prediction_length": 2} | ||
|
|
||
| for param in params: | ||
| current_dm_cfg = param.get("datamodule_cfg", {}) | ||
| param["datamodule_cfg"] = {**default_dm_cfg, **current_dm_cfg} | ||
|
|
||
| return params | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should it go to
_encodersfolder?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I considered this during implementation. Looking at the current
_encoders/contents, bothEncoderandEncoderLayerare TimeXer-specific (they takecross,tau,deltaparams and have global-token logic).Whereas
SOFTSEncoderLayerhas a fundamentally different interface it takes a 4D tensor(B, C, L, D)and uses STAD instead of attention, so there's no shared contract between them._blocks/currently houses model-specific building blocks likeResidualBlock(for DSIPTs), andSOFTSEncoderLayer+STADModulefollow the same pattern self-contained blocks specific to one model. Should I move it to a new_softs/subfolder underlayers/to make the model association clear.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, but we can have multiple implementations of encoder layers, no? and that is why we created an
_encodersfolder that can host multiple implementations. It is not necessary that this encoder layer is just used bySOFTS, what if we see some new model that is derived fromSOFTS, that could also use this layer. THe name -SOFTSEncoderLayeralready makes the association pretty clear.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it can be possible that in the future we'd reuse it. I've adjusted that.