|
| 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