-
Notifications
You must be signed in to change notification settings - Fork 689
[ENH] [WIP] Standardize model output to 4d tensor #1895
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 214 commits
b3644a6
a1d64c6
4b2486e
02b0ce6
41cbf66
cef62d3
bc2e93b
eee1c86
164fe0d
20e88d0
318c1fb
012ab3d
86365a0
f6dee46
9b0e4ec
7de5285
57dfe3a
c9f12db
fa8144e
232a510
1c8d4b5
f632e32
252598d
d0d1c3e
80e64d2
6364780
82b3dc7
257183c
9cdcb19
a83bf32
ac56d4f
0e7e36f
4bfff21
ef98273
8a53ed6
9f9df31
cdecb77
86360fd
20aafb7
043820d
1720a15
af44474
a3cb8b7
75d7fb5
1b946e6
3edb08b
a4bc9d8
4c0d570
ef37f55
a669134
d78bf5d
e350291
f90c94f
3099691
77cb979
28df3c3
f8c94e6
c289255
9467f38
c3b40ad
c007310
2e25052
38c28dc
9d80eb8
a8ccfe3
f900ba5
ed1b799
fd59bac
c947910
c0ceb8a
3828c26
0cb7df6
6d6d18e
b32ed0e
cb55578
2218336
3144865
89231d9
30b541b
3f1e11f
5cc3ff1
1bcf181
f18e09d
e1e360e
1fd0594
168e16a
b25ced9
f4afe90
ae12c69
04e1a45
a27905d
e87c25b
92c12bf
1831bcb
a896b3f
420de37
3b07263
010298e
d0aa444
fef4113
1d478d5
f9992f2
d049019
e72486b
8daeb95
5142d52
efbbc09
7443b0b
a734f26
7f466b2
8a680df
0ccb078
826ac31
d70b07c
5ce4553
7b41140
e3e5bb8
d67ccae
0968452
525bbb9
4267da6
4e8f863
5f79e25
7fb048b
4bfec1b
7510509
1a52579
693fbd2
10b1e4a
0fab57a
668c901
7c2855c
9d62ff0
8cb1484
c117092
4845c9b
7f0495d
943151b
228ebed
b101e2e
913c418
8990e8b
f6d39fe
2c518ee
7a5c58f
cb3e944
d0009ff
c9d3c26
33a99d1
927ee49
1829de5
8fc1865
ebe8d22
01b2d78
8fd608b
3b9de6d
032a7b0
4d9a19a
cef0292
35c6973
1749cd2
2d86134
cd477e6
7f8fca8
953214e
bdaecc2
8e5864c
327919c
3809ad5
10c9290
212e01d
12a79a8
75220be
03c06e8
8b0087e
d328fae
6e4e692
0003c54
79b5682
b422af6
2546410
55e1869
baf9a61
0352465
7c6385c
66a006c
5871bfa
eb0dfa5
6fac509
ee1edf5
ff85e69
377f416
14db380
7eaee38
06ae102
faf86ce
65e171e
3786a4c
d3e6ce2
e8d72c1
56ea5af
590beff
081b840
ffe6983
6882e23
87ceb10
fbb234a
cc10dd5
369ed49
9d94548
2d45ac6
2b5b702
ed71413
b5aab41
8a0e673
9a2979c
fa8cc3b
7e7688e
ea827a8
06b898b
5994af3
e82ab68
1f60883
ce4eeae
a33b9ba
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 |
---|---|---|
@@ -0,0 +1,9 @@ | ||
""" | ||
Decomposition layers for PyTorch Forecasting. | ||
""" | ||
|
||
from pytorch_forecasting.layers.decomposition._series_decomp import SeriesDecomposition | ||
|
||
__all__ = [ | ||
"SeriesDecomposition", | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
""" | ||
Series Decomposition Block for time series forecasting models. | ||
""" | ||
|
||
import torch | ||
import torch.nn as nn | ||
import torch.nn.functional as F | ||
|
||
from pytorch_forecasting.layers.filter._moving_avg_filter import MovingAvg | ||
|
||
|
||
class SeriesDecomposition(nn.Module): | ||
""" | ||
Series decomposition block from Autoformer. | ||
|
||
Decomposes time series into trend and seasonal components using | ||
moving average filtering. | ||
|
||
Args: | ||
kernel_size (int): | ||
Size of the moving average kernel for trend extraction. | ||
""" | ||
|
||
def __init__(self, kernel_size): | ||
super().__init__() | ||
self.moving_avg = MovingAvg(kernel_size, stride=1) | ||
|
||
def forward(self, x): | ||
""" | ||
Forward pass for series decomposition. | ||
|
||
Args: | ||
x (torch.Tensor): | ||
Input time series tensor of shape (batch_size, seq_len, features). | ||
|
||
Returns: | ||
tuple: | ||
- trend (torch.Tensor): Trend component of the time series. | ||
- seasonal (torch.Tensor): Seasonal component of the time series. | ||
""" | ||
trend = self.moving_avg(x) | ||
seasonal = x - trend | ||
return seasonal, trend |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
""" | ||
Filtering layers for time series forecasting models. | ||
""" | ||
|
||
from pytorch_forecasting.layers.filter._moving_avg_filter import MovingAvg | ||
|
||
__all__ = [ | ||
"MovingAvg", | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
""" | ||
Moving Average Filter Block | ||
""" | ||
|
||
import torch | ||
import torch.nn as nn | ||
import torch.nn.functional as F | ||
|
||
|
||
class MovingAvg(nn.Module): | ||
""" | ||
Moving Average block for smoothing and trend extraction from time series data. | ||
|
||
A moving average is a smoothing technique that creates a series of average from | ||
different subsets of a time series. | ||
|
||
For example: Given a time series ``x = [x_1, x_2, ..., x_n]``, the moving average | ||
with a kernel size of `k` calculates the average of each subset of `k` consecutive | ||
elements, resulting in a new series of averages. | ||
|
||
Args: | ||
kernel_size (int): | ||
Size of the moving average kernel. | ||
stride (int): | ||
Stride for the moving average operation, typically set to 1. | ||
""" | ||
|
||
def __init__(self, kernel_size, stride): | ||
super().__init__() | ||
self.kernel_size = kernel_size | ||
self.avg = nn.AvgPool1d(kernel_size, stride=stride, padding=0) | ||
|
||
def forward(self, x): | ||
if self.kernel_size % 2 == 0: | ||
self.padding_left = self.kernel_size // 2 - 1 | ||
self.padding_right = self.kernel_size // 2 | ||
else: | ||
self.padding_left = self.kernel_size // 2 | ||
self.padding_right = self.kernel_size // 2 | ||
|
||
front = x[:, 0:1, :].repeat(1, self.padding_left, 1) | ||
end = x[:, -1:, :].repeat(1, self.padding_right, 1) | ||
|
||
x_padded = torch.cat([front, x, end], dim=1) | ||
x_transposed = x_padded.permute(0, 2, 1) | ||
x_smoothed = self.avg(x_transposed) | ||
x_out = x_smoothed.permute(0, 2, 1) | ||
return x_out |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -294,3 +294,175 @@ | |
prog_bar=True, | ||
logger=True, | ||
) | ||
|
||
def standardize_model_output( | ||
self, | ||
prediction: torch.Tensor, | ||
expected_dims: tuple[int, Optional[int], Optional[int], Optional[int]] = ( | ||
None, | ||
None, | ||
None, | ||
None, | ||
), # noqa: E501 | ||
) -> torch.Tensor: | ||
""" | ||
Standardize model outputs to a 4-dimensional tensor, with shape | ||
(batch_size, timesteps, num_features, last_dim). | ||
|
||
Parameters | ||
---------- | ||
prediction : torch.Tensor | ||
The raw prediction tensor from the model. | ||
- Must be a torch.Tensor (in the future, also accept a list of tensors for | ||
multi-target forecasting). | ||
- Supported dims: 2D, 3D or 4D tensors. | ||
- if 2D: (batch_size, timesteps) - univariate forecasting | ||
- if 3D: | ||
a) (batch_size, timesteps, n_targets) - multivariate forecasting | ||
b) (batch_size, timesteps, last_dim) - univariate forecasting with quantiles or distribution. | ||
c) (batch_size, timesteps, n_targets * last_dim) - multivariate | ||
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. can we think of a better name than 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. Does |
||
forecasting with quantiles, where features and quantiles are flattened in dim 2. | ||
- if 4D: (batch_size, timesteps, n_targets, last_dim) - multivariate | ||
forecasting with quantiles or distribution parameters. | ||
- In the future, once multi-target forecasting is supported, this | ||
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. wait, not sure if I understand this. Is multi-target forcasting not just the case 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. yes, you are right. I think there is a slight confusion in the docstring. What I was actually referring to is multi-target forecasting |
||
will also accept a list of tensors, where each tensor inside the list | ||
is treated as above. | ||
- If anything apart from the above dimensions is provided, an error is raised. | ||
|
||
expected_dims : tuple[int, Optional[int], Optional[int], Optional[int]], default=(None, None, None, None) | ||
A tuple specifying the dimensions: (n_targets, batch_size, timesteps, last_dim) | ||
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. this is very confusing since the dimensions are not in the same order as for the tensor. Can we change that? 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 have changed it to the order of the output 4d tensor |
||
|
||
n_targets : int | ||
- Position 0: Number of target features | ||
- Must be provided explicitly (cannot be None) | ||
- Used for reshaping 2D and 3D tensors to 4D. | ||
|
||
batch_size : Optional[int], default=None | ||
- Position 1: Expected batch size | ||
- When specified: Validates prediction.shape[0] | ||
- When None: Uses actual tensor dimension | ||
|
||
timesteps : Optional[int], default=None | ||
- Position 2: Expected number of timesteps | ||
- When specified: Validates prediction.shape[1] | ||
- When None: Uses actual tensor dimension | ||
|
||
last_dim : Optional[int], default=None | ||
- Position 3: Size of the last dimension. | ||
- Common use case - quantile, sample, distribution params. | ||
- When it is specified, it is used to directly reshape. | ||
- When None and model uses QuantileLoss: It is set to the number of quantiles | ||
- When None and no quantile information is available: It defaults to 1. | ||
- If required, this can be extended to handle other cases where the last_dim is None | ||
but its value can be inferred from the loss function or model configuration (apart from | ||
the existing QuantileLoss case, of course). | ||
Returns | ||
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. minor formatting remark:
(not consequential now, but this is an assumption when generating rst autodocs) |
||
------- | ||
torch.Tensor | ||
The standardized prediction tensor with shape (batch_size, timesteps, n_targets, last_dim). | ||
The prediction tensor is obtained by reshaping the input tensor. There are | ||
several cases to consider: | ||
|
||
- If the input tensor is 2D, it is reshaped to (batch_size, timesteps, 1, 1). | ||
- If the input tensor is 3D, it is reshaped to (batch_size, timesteps, 1, 1) for a | ||
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 is unclear how the reshaping happens for 3D, can you be more precise? 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 have made changes, you can take a look now.. |
||
multivariate single-target forecast, or (batch_size, timesteps, 1, last_dim) for a univariate quantile forecast. | ||
- If the input tensor is 4D, it is assumed to be in the shape | ||
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. for 4D, you need to be explicit about how it is getting reshaped. |
||
(batch_size, timesteps, n_targets, last_dim) or (batch_size, timesteps, last_dim, n_targets). | ||
Notes | ||
----- | ||
[1] The fourth dimension (last_dim) commonly represents: | ||
|
||
* Quantiles: For quantile regression (e.g., 0.1, 0.5, 0.9) | ||
* Distribution parameters: For parametric forecasts (e.g., mean, variance) | ||
* Samples: For sample-based uncertainty estimates | ||
|
||
The current implementation assumes the most common case of quantile forecasts | ||
when automatically inferring this dimension from the loss function, | ||
but any value can be explicitly provided. A fallback of 1 is used in case where | ||
PranavBhatP marked this conversation as resolved.
Show resolved
Hide resolved
|
||
no information is available on ``last_dim``. | ||
|
||
[2] This can currently handle situations where a single target is used | ||
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. can you explain that and give an example? Can you give the simplest example that shows a 4D tensor is not possible in this case? Is this, for instance, that we use squared loss for variable 1 but parametric log-loss on mean/variance (of normal) on variable 2? 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. maybe using 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 can try looking at this.. |
||
either in a univariate or multivariate situation. In case of multi-target | ||
forecasting, where each target has its own loss function, a list of tensors is | ||
returned, where each tensor corresponds to a target. This requires some change | ||
to the existing code. | ||
""" # noqa: E501 | ||
|
||
n_targets, batch_size, timesteps, last_dim = expected_dims | ||
|
||
if not isinstance(prediction, torch.Tensor): | ||
raise TypeError( | ||
f"Expected prediction to be a torch.Tensor, but got {type(prediction)}" | ||
) | ||
|
||
if n_targets is None: | ||
raise ValueError( | ||
"Expected n_targets to be a positive integer, but got `None`." | ||
) | ||
|
||
if last_dim is None: | ||
if hasattr(self.loss, "quantiles") and self.loss.quantiles is not None: | ||
last_dim = len(self.loss.quantiles) # Quantile regression case | ||
# we can add more cases here in the future, where we refer to the specific | ||
# loss function to determine the last dimension. For now we are sticking | ||
# to the quantile regression case. | ||
else: | ||
last_dim = 1 | ||
|
||
if batch_size is not None: | ||
if prediction.shape[0] != batch_size: | ||
raise ValueError( | ||
f"Expected batch size {batch_size}, but got {prediction.shape[0]}." | ||
) | ||
|
||
if timesteps is not None: | ||
if prediction.shape[1] != timesteps: | ||
raise ValueError( | ||
f"Expected timesteps {timesteps}, but got {prediction.shape[1]}." | ||
) | ||
|
||
if prediction.ndim == 2: | ||
# reshape to (batch_size, timsteps, 1, 1) | ||
prediction = prediction.unsqueeze(-1).unsqueeze(-1) | ||
|
||
elif prediction.ndim == 3: | ||
if prediction.shape[2] == n_targets: | ||
# reshape to (batch_size, timesteps, n_targets, 1) | ||
prediction = prediction.unsqueeze(-1) | ||
elif prediction.shape[2] == last_dim: | ||
# reshape to (batch_size, timesteps, 1, last_dim) | ||
prediction = prediction.unsqueeze(2) | ||
elif prediction.shape[2] == n_targets * last_dim: | ||
# multivariate forecast with quantiles | ||
# where features and quantiles are flattened in dim 2. | ||
# reshape to (batch_size, timesteps, n_targets, last_dim) | ||
prediction = prediction.reshape( | ||
prediction.shape[0], prediction.shape[1], n_targets, last_dim | ||
) | ||
else: | ||
# reshape to (batch_size, timesteps, n_targets, last_dim) | ||
prediction = prediction.unsqueeze(-1) | ||
|
||
elif prediction.ndim == 4: | ||
# assuming only a single case where n_targets and last_dim are swapped. | ||
if prediction.shape[2] == last_dim and prediction.shape[3] == n_targets: | ||
# reshape to (batch_size, timesteps, n_targets, last_dim) | ||
warn( | ||
"Prediction tensor has shape (batch_size, timesteps, last_dim, n_targets). " # noqa: E501 | ||
"This is not the expected shape. Transposing the last two dimensions." # noqa: E501 | ||
) | ||
prediction = prediction.permute(0, 1, 3, 2) | ||
|
||
else: | ||
raise ValueError( | ||
f"Expected prediction tensor to have 2, 3, or 4 dimensions, " | ||
f"but got {prediction.ndim} dimensions." | ||
) | ||
|
||
# final check to ensure the output is 4D | ||
if prediction.ndim != 4: | ||
raise ValueError( | ||
f"Failed to standardize output to 4D tensor. Current shape: {prediction.shape}" # noqa: E501 | ||
) | ||
|
||
return prediction | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
""" | ||
Decomposition-Linear model for time series forecasting. | ||
""" | ||
|
||
from pytorch_forecasting.models.dlinear._dlinear_pkg_v2 import DLinearModel_pkg_v2 | ||
from pytorch_forecasting.models.dlinear._dlinear_v2 import DLinearModel | ||
|
||
__all__ = [ | ||
"DLinearModel" "DLinearModel_pkg_v2", | ||
] |
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.
hard to read - why not simply
tuple[int]
?