Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
e12a751
Add tsmixer-basic
eschibli Sep 3, 2024
1bd17da
Implimented project_first
eschibli Sep 8, 2024
34e431f
Merge branch 'master' of https://github.com/unit8co/darts
eschibli Oct 8, 2024
bb72922
Updated changelog
eschibli Oct 8, 2024
1934928
Linting
eschibli Oct 9, 2024
c5de724
Additional linting
eschibli Oct 9, 2024
b8a2e88
Further updated changelog
eschibli Oct 11, 2024
c6c7e97
More linting?
eschibli Oct 11, 2024
79612f0
Removed unnecessary layer init
eschibli Oct 23, 2024
6cdc9db
linting
eschibli Oct 23, 2024
171dc34
Reverted example
eschibli Oct 24, 2024
f9797a3
linting????
eschibli Oct 24, 2024
e528f85
auto formatting
eschibli Oct 25, 2024
133547e
Merge branch 'master' of https://github.com/unit8co/darts into Imp/ts…
eschibli Oct 25, 2024
5afff01
Merge branch 'master' of https://github.com/unit8co/darts into Imp/ts…
eschibli Oct 27, 2024
3a392a3
Added test
eschibli Oct 27, 2024
ebe02d1
Improved test coverage
eschibli Oct 28, 2024
0a90f24
Docustring tweak
eschibli Oct 31, 2024
2674e1c
Merge branch 'master' of https://github.com/unit8co/darts into Imp/ts…
eschibli Nov 3, 2024
2bf09ce
Merge branch 'master' into Imp/tsmixer-basic
dennisbader Nov 7, 2024
245ae09
Merge branch 'master' into Imp/tsmixer-basic
madtoinou Nov 12, 2024
f9c0d15
Merge branch 'master' into Imp/tsmixer-basic
eschibli Dec 17, 2024
51e2b11
Merge branch 'master' into Imp/tsmixer-basic
eschibli Feb 11, 2025
c86b559
Merge branch 'master' into Imp/tsmixer-basic
madtoinou Mar 5, 2025
e6647c0
Merge branch 'unit8co:master' into Imp/tsmixer-basic
eschibli Mar 10, 2025
1483096
Merge branch 'master' into Imp/tsmixer-basic
eschibli Mar 25, 2025
97c83b2
Added project_after_n_layers to tsmixer
eschibli Mar 25, 2025
bf912d9
Merge branch 'unit8co:master' into Imp/tsmixer-basic
eschibli Apr 19, 2025
46cf888
Relaxed TSMixer performance theshold
eschibli Apr 20, 2025
c374724
Try again
eschibli Apr 20, 2025
67a89f1
Merge branch 'master' into Imp/tsmixer-basic
dennisbader Apr 20, 2025
ef1a322
Try again
eschibli Apr 21, 2025
0dd0c30
Corrected changelog
eschibli Apr 25, 2025
a4891e2
Added additional test
eschibli Apr 29, 2025
a46645a
Merge branch 'master' into Imp/tsmixer-basic
eschibli Apr 29, 2025
c5a5fe4
Merge branch 'master' into Imp/tsmixer-basic
eschibli May 9, 2025
af74b5b
Merge branch 'master' into Imp/tsmixer-basic
eschibli May 29, 2025
8550314
Merge branch 'master' into Imp/tsmixer-basic
eschibli Jun 8, 2025
df935cc
Merge branch 'master' into Imp/tsmixer-basic
eschibli Jun 17, 2025
2c156f5
Merge branch 'master' into Imp/tsmixer-basic
madtoinou Jun 27, 2025
694ca4a
Merge branch 'master' into Imp/tsmixer-basic
eschibli Jul 15, 2025
94c6d71
Merge branch 'master' into Imp/tsmixer-basic
eschibli Jul 21, 2025
f31af84
Merge branch 'master' into Imp/tsmixer-basic
eschibli Jul 30, 2025
784e9ea
Merge branch 'master' into Imp/tsmixer-basic
eschibli Jul 31, 2025
7ca964b
Merge branch 'master' into Imp/tsmixer-basic
eschibli Aug 1, 2025
176dd5c
Merge branch 'unit8co:master' into Imp/tsmixer-basic
eschibli Jan 7, 2026
7ad36bd
Corrected some code comments
Jan 8, 2026
552c92b
Merge branch 'master' into Imp/tsmixer-basic
eschibli Feb 11, 2026
83264d4
Merge branch 'master' into Imp/tsmixer-basic
dennisbader Feb 15, 2026
1e48108
minor updates
dennisbader Feb 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ but cannot always guarantee backwards compatibility. Changes that may **break co
- Updated the Ray Tune Hyperparameter Optimization example in the [user guide](https://unit8co.github.io/darts/userguide/hyperparameter_optimization.html) to work with the latest `ray` versions (`>=2.31.0`). [#2459](https://github.com/unit8co/darts/pull/2459) by [He Weilin](https://github.com/cnhwl).
- Indicate that `multi_models=False` induce a lags shift for each step in `output_chunk_length` in `RegressionModel` and `LinearRegressionModel`. [#2511](https://github.com/unit8co/darts/pull/2511) by [Antoine Madrona](https://github.com/madtoinou).
- Added reference to `timeseries_generation.datetime_attribute_timeseries` in `TimeSeries.add_datetime_attribute` (0-indexing of encoding is enforced). [#2511](https://github.com/unit8co/darts/pull/2511) by [Antoine Madrona](https://github.com/madtoinou).
- Added `project_first_layer` keyword to `TSMixerModel` (default False), allowing the option of projecting to `output_chunk_length` at the end, rather than the beginning, of the model. This could improve performance when past covariates are more important the future covariates.

**Fixed**

Expand Down
100 changes: 80 additions & 20 deletions darts/models/forecasting/tsmixer_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ def __init__(
super().__init__()

mixing_input = input_dim

if static_cov_dim != 0:
self.feature_mixing_static = _FeatureMixing(
sequence_length=sequence_length,
Expand Down Expand Up @@ -328,6 +329,7 @@ def __init__(
dropout: float,
norm_type: Union[str, nn.Module],
normalize_before: bool,
project_first_layer: bool = True,
**kwargs,
) -> None:
"""
Expand Down Expand Up @@ -362,18 +364,28 @@ def __init__(
Type of normalization to use.
normalize_before
Whether to apply normalization before or after mixing.
project_first_layer
Whether to project to the output time dimension at the first layer (default),
or at the end of the module. False is recommended if there are
no future covariates, while True is recommended if there are
important future covariates.
"""
super().__init__(**kwargs)
self.input_dim = input_dim
self.output_dim = output_dim
self.future_cov_dim = future_cov_dim
self.static_cov_dim = static_cov_dim
self.nr_params = nr_params
self.project_first_layer = project_first_layer

self.sequence_length = (
self.output_chunk_length if project_first_layer else self.input_chunk_length
)

if activation not in ACTIVATIONS:
raise_log(
ValueError(
f"Invalid `activation={activation}`. Must be on of {ACTIVATIONS}."
f"Invalid `activation={activation}`. Must be one of {ACTIVATIONS}."
),
logger=logger,
)
Expand All @@ -383,7 +395,7 @@ def __init__(
if norm_type not in NORMS:
raise_log(
ValueError(
f"Invalid `norm_type={norm_type}`. Must be on of {NORMS}."
f"Invalid `norm_type={norm_type}`. Must be one of {NORMS}."
),
logger=logger,
)
Expand All @@ -402,24 +414,33 @@ def __init__(
"normalize_before": normalize_before,
}

# Projects from the input time dimension to the output time dimension

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.fc_hist is not used anymore (I guess replaced by the new encoder_to_decoder?). We should remove one of the two

self.fc_hist = nn.Linear(self.input_chunk_length, self.output_chunk_length)

# Projects from the output time dimension to the input time dimension
# (if we are keeping the input time dimension/project_first_layer=False)
if not self.project_first_layer:
self.fc_future = nn.Linear(
self.output_chunk_length, self.input_chunk_length
)

self.feature_mixing_hist = _FeatureMixing(
sequence_length=self.output_chunk_length,
sequence_length=self.sequence_length,
input_dim=input_dim + past_cov_dim + future_cov_dim,
output_dim=hidden_size,
**mixer_params,
)
if future_cov_dim:
self.feature_mixing_future = _FeatureMixing(
sequence_length=self.output_chunk_length,
sequence_length=self.sequence_length,
input_dim=future_cov_dim,
output_dim=hidden_size,
**mixer_params,
)
else:
self.feature_mixing_future = None
self.conditional_mixer = self._build_mixer(
prediction_length=self.output_chunk_length,
sequence_length=self.sequence_length,
num_blocks=num_blocks,
hidden_size=hidden_size,
future_cov_dim=future_cov_dim,
Expand All @@ -430,7 +451,7 @@ def __init__(

@staticmethod
def _build_mixer(
prediction_length: int,
sequence_length: int,
num_blocks: int,
hidden_size: int,
future_cov_dim: int,
Expand All @@ -448,7 +469,7 @@ def _build_mixer(
layer = _ConditionalMixerLayer(
input_dim=input_dim_block,
output_dim=hidden_size,
sequence_length=prediction_length,
sequence_length=sequence_length,
static_cov_dim=static_cov_dim,
**kwargs,
)
Expand Down Expand Up @@ -480,6 +501,7 @@ def forward(
# B: batch size
# L: input chunk length
# T: output chunk length
# SL: Residual block time dimension (T if project_first_layer, L otherwise)
# C: target components
# P: past cov features
# F: future cov features
Expand All @@ -491,31 +513,62 @@ def forward(
# `x`: (B, L, H), `x_future`: (B, T, F), `x_static`: (B, C or 1, S)
x, x_future, x_static = x_in

# swap feature and time dimensions (B, L, H) -> (B, H, L)
x = _time_to_feature(x)
# linear transformations to horizon (B, H, L) -> (B, H, T)
x = self.fc_hist(x)
# (B, H, T) -> (B, T, H)
x = _time_to_feature(x)

# feature mixing for historical features (B, T, H) -> (B, T, H_S)
# If project_first_layer, decoder style model with residual blocks in output time dimension
# (B, L, H) -> (B, SL, H)
if self.project_first_layer:
# swap feature and time dimensions (B, L, H) -> (B, H, L)
x = _time_to_feature(x)
# linear transformations to SL (T in this case)
# (B, H, L) -> (B, H, SL)
x = self.fc_hist(x)
# Transpose back
# (B, H, T) -> (B, T, H)
x = _time_to_feature(x)

# Otherwise, encoder-style model with residual blocks in input time dimension
# In the original paper this was not implimented for future covariates,
Comment thread
eschibli marked this conversation as resolved.
Outdated
# but rather than ignoring them or raising an error we remap them to the input time dimension.
# Suboptimal but may be useful in some cases.
elif self.future_cov_dim:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to make it a bit more intuitive, I would move this code below, inside the if self.future_cov_dim and change the condition to if not self.project_first_layer in order to group the operation on each kind of features:

  1. "target"; project to output time dimension in the first layer if project_first_layer = True otherwise we stay in input time dimension
  2. "target"; do the feature_mixing_hist (not changed)
  3. "fut_cov"; project the future covariates to input time dimension if project_first_layer=False (the logic you added)
  4. concatenate the future covariates to the target features (not changed)
  5. static covariates (not changed)
  6. "target"; projection to the output time dimension if it did not occur earlier
  7. "target"; application of fc_out, critical for probabilistic forecasts

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation is out-of-date with the current version of the PR, and I think the current version makes more sense.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I set it up this way originally because the TSMixer performance test was failing if I didn't generate the layers in exactly the same order. In this PR I have relaxed the tolerance as it wasn't stable.

# swap feature and time dimensions (B, L, F) -> (B, F, L)
x_future = _time_to_feature(x_future)
# linear transformations to SL (L in this case)
# (B, F, T) -> (B, F, SL)
x_future = self.fc_future(x_future)
# Transpose back (B, L, F) -> (B, F, L)
x_future = _time_to_feature(x_future)

# feature mixing for historical features (B, SL, H) -> (B, SL, H_S)
x = self.feature_mixing_hist(x)
if self.future_cov_dim:
# feature mixing for future features (B, T, F) -> (B, T, H_S)
# feature mixing for future features (B, SL, F) -> (B, SL, H_S)
x_future = self.feature_mixing_future(x_future)
# (B, T, H_S) + (B, T, H_S) -> (B, T, 2*H_S)
# (B, SL, H_S) + (B, SL, H_S) -> (B, T, 2*H_S)
x = torch.cat([x, x_future], dim=-1)

if self.static_cov_dim:
# (B, C, S) -> (B, 1, C * S)
x_static = x_static.reshape(x_static.shape[0], 1, -1)
# repeat to match horizon (B, 1, C * S) -> (B, T, C * S)
x_static = x_static.repeat(1, self.output_chunk_length, 1)
# repeat to match time dim: (B, 1, C * S) -> (B, SL, C * S)
x_static = x_static.repeat(1, self.sequence_length, 1)

for mixing_layer in self.conditional_mixer:
# conditional mixer layers with static covariates (B, T, 2 * H_S), (B, T, C * S) -> (B, T, H_S)
# conditional mixer layers with static covariates (B, SL, 2 * H_S), (B, SL, C * S) -> (B, SL, H_S)
x = mixing_layer(x, x_static=x_static)

# If we are in the input time dimension, we need to project to the output time dimension.
# The original paper did not a fc_out layer (as hidden_size == output_dim)
Comment thread
eschibli marked this conversation as resolved.
Outdated
# (so we needed to decide where to put it)
# We put the projection first as it as while both operations may be very compressive,
# we felt it more likely that output_dim << hidden_size than output_chunk_length << input_chunk_length.
if not self.project_first_layer:
# (B, SL, H_S) -> (B, H_S, SL)
x = _time_to_feature(x)
# (B, H_S, SL) -> (B, H_S, T)
x = self.fc_hist(x)
# (B, H_S, T) -> (B, T, H_S)
x = _time_to_feature(x)

# linear transformation to generate the forecast (B, T, H_S) -> (B, T, C * N_P)
x = self.fc_out(x)
# (B, T, C * N_P) -> (B, T, C, N_P)
Expand All @@ -537,6 +590,7 @@ def __init__(
norm_type: Union[str, nn.Module] = "LayerNorm",
normalize_before: bool = False,
use_static_covariates: bool = True,
project_first_layer: bool = True,
**kwargs,
) -> None:
"""Time-Series Mixer (TSMixer): An All-MLP Architecture for Time Series.
Expand Down Expand Up @@ -591,6 +645,10 @@ def __init__(
`"LayerNormNoBias", "LayerNorm", "TimeBatchNorm2d"`. Otherwise, must be a custom `nn.Module`.
normalize_before
Whether to apply layer normalization before or after mixer layer.
project_first_layer
Whether to project to the output time dimension at the first layer (default), or at the end of the module.
Projecting last is recommended if there are no future covariates, while projecting first is recommended if
there are important future covariates.
use_static_covariates
Whether the model should use static covariate information in case the input `series` passed to ``fit()``
contain static covariates. If ``True``, and static covariates are available at fitting time, will enforce
Expand Down Expand Up @@ -774,6 +832,7 @@ def encode_year(idx):
self.normalize_before = normalize_before
self.norm_type = norm_type
self.hidden_size = hidden_size
self.project_first_layer = project_first_layer
self._considers_static_covariates = use_static_covariates

def _create_model(self, train_sample: MixedCovariatesTrainTensorType) -> nn.Module:
Expand Down Expand Up @@ -825,6 +884,7 @@ def _create_model(self, train_sample: MixedCovariatesTrainTensorType) -> nn.Modu
dropout=self.dropout,
norm_type=self.norm_type,
normalize_before=self.normalize_before,
project_first_layer=self.project_first_layer,
**self.pl_module_params,
)

Expand Down
22 changes: 22 additions & 0 deletions darts/tests/models/forecasting/test_tsmixer.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,3 +362,25 @@ def test_time_batch_norm_2d_gradients(self):
output.mean().backward()

assert input_tensor.grad is not None

@pytest.mark.parametrize("project_first_layer", [True, False])
def test_project_first(self, project_first_layer):
ts = tg.sine_timeseries(length=36, freq="h")
input_len = 12
output_len = 6

model = TSMixerModel(
input_chunk_length=input_len,
output_chunk_length=output_len,
n_epochs=1,
project_first_layer=project_first_layer,
# Cover case of projecting future covs back to input dims
add_encoders={"cyclic": {"future": "hour"}},
**tfm_kwargs,
)
model.fit(ts)

if project_first_layer:
assert model.model.sequence_length == output_len
else:
assert model.model.sequence_length == input_len
Comment thread
eschibli marked this conversation as resolved.
Outdated