PatchTST: a patch-based Transformer forecaster with channel independence.
Classes
PatchTST: a Transformer that attends over patches, not time steps.
Point-wise Transformers on time series attend over individual time steps, which are semantically thin — a single reading carries almost no meaning on its own — and give attention a sequence as long as the lookback. PatchTST instead cuts the lookback into patches, short subseries of patch_len points, and treats each patch as one token. A patch carries local shape, so attention finally has something meaningful to relate, and the sequence length drops by roughly stride, making attention's quadratic cost about \text{stride}^2 cheaper.
The second idea is channel independence. A multivariate series is not mixed inside the model; every channel is pushed through the same shared backbone on its own. This helps for two reasons. It cuts parameters sharply — one backbone rather than one per channel pair, and a head sized by patches rather than by channels — and it removes the chance to overfit spurious cross-channel correlations, which are abundant and unstable in real series. Each channel also contributes its own training examples to the same weights, so the effective dataset grows with the channel count instead of the parameter count. The public API here is univariate, but the backbone implements this folding, so a channel dimension is handled by construction.
Instance normalisation (RevIN) wraps the whole thing: each window is standardised on its own mean and standard deviation and the forecast is mapped back with the same statistics, which is what lets one set of weights serve windows at wildly different levels.
__init__( self, lookback: int = 24, horizon: int = 8, patch_len: int = 8, stride: int = 4, d_model: int = 32, n_heads: int = 4, n_layers: int = 1, dim_feedforward: int = 64, dropout: float = 0.0, revin: bool = True, n_epochs: int = 100, batch_size: int = 32, learning_rate: float = 0.001, patience: int = 15, device: str = 'cpu', random_state: Optional[int] = None, )
Overview
-
Slide a
(lookback, horizon)window over the series. - Normalise each window by its own statistics (RevIN).
-
Cut the window into
n_patchespatches, padding the end by repeating
-
Embed each patch to
d_modeland add a learned positional encoding. - Run a Transformer encoder over the patch tokens.
- Flatten the encoded tokens and project linearly to the horizon.
- Denormalise with the window's own statistics.
Theory
A window x \in \mathbb{R}^{L} is normalised,
and cut into N = \lceil (L - P)/S \rceil + 1 patches of length P at stride S. Each patch is embedded and encoded,
where attention costs O(N^2 d) rather than the O(L^2 d) of a point-wise model. The head flattens and projects,
and the forecast is returned to the original units by \sigma \hat{y} + \mu, which is an exact inverse of the normalisation.
Parameters
lookback
horizon
patch_len
stride
patch_len gives disjoint patches, smaller gives overlap.
d_model
n_heads.
n_heads
n_layers
dim_feedforward
dropout
revin
n_epochs
batch_size
learning_rate
patience
device
"cpu" because it is the only setting that gives bit-identical forecasts across machines; use "auto" when speed matters more than exact reproducibility.
random_state
Attributes
module_
lookback_
horizon_
n_windows_
offset_
scale_
series_
loss_curve_
n_epochs_run_
device_
Notes
Requires PyTorch: pip install 'tuiml[torch]'. The class imports, constructs, registers and reports its schema without torch; only fit needs it.
Complexity:
- Training: O(E \cdot W \cdot (N^2 d + N d^2)) for E
- Prediction: O(\lceil s / H \rceil \cdot (N^2 d + N d^2)).
- Long lookbacks, where a point-wise Transformer's attention is both
- Series with repeating local shapes that patches can capture as units.
- Multivariate problems where channel mixing has been found to overfit.
- Not for very short series: with a few dozen points there are too few
ExponentialSmoothing will win.References
See Also
>>> import numpy as np
>>> from tuiml.algorithms.timeseries.deep import PatchTSTForecaster
>>> from tuiml.utils.torch_backend import has_torch
>>> model = PatchTSTForecaster(lookback=24, horizon=6, random_state=0)
>>> from tuiml.algorithms.timeseries.deep.patchtst import num_patches
>>> num_patches(24, model.patch_len, model.stride)
5
>>> if has_torch():
... y = np.sin(np.arange(200) / 5.0)
... _ = model.fit(y)
... print(model.predict(steps=6).shape)
... else:
... print("(6,)")
(6,)
Functions
Return how many patches a window of length is split into.
Parameters
length
patch_len
length when the window is shorter.
stride
patch_len gives disjoint patches; smaller gives overlap.
Returns
n_patches
>>> from tuiml.algorithms.timeseries.deep.patchtst import num_patches
>>> num_patches(24, patch_len=8, stride=4) # divides exactly
5
>>> num_patches(25, patch_len=8, stride=4) # padded to 29
6
>>> num_patches(5, patch_len=8, stride=4) # window shorter than a patch
1
Return the window length after end padding.
Parameters
length
patch_len
stride
Returns
padded
patch_len + (n_patches - 1) * stride, never less than length.
>>> from tuiml.algorithms.timeseries.deep.patchtst import padded_length
>>> padded_length(25, patch_len=8, stride=4)
28
Split each window into overlapping or disjoint patches.
Parameters
torch
torch module, passed in so this function never imports it.
x
patch_len
stride
Returns
patches