N-BEATS: neural basis expansion analysis for time series forecasting.

Classes

NBEATSForecaster

class algorithms.timeseries.deep.nbeats.NBEATSForecaster(DeepForecaster)

N-BEATS: doubly-residual stacks of fully connected blocks.

N-BEATS forecasts a series from a lookback window using nothing but dense layers — no recurrence, no convolution, no attention — and still beat the winner of the M4 competition. Its one structural idea is the double residual: every block emits both a backcast (its reconstruction of the input it saw) and a forecast. The backcast is subtracted from the block's input before the next block runs, so each block only ever works on the part of the signal its predecessors could not explain, and the final forecast is the plain sum of the block forecasts.

Two basis configurations are available. The generic one learns the backcast and forecast vectors directly and is the stronger forecaster. The interpretable one constrains the first stack to a low-order polynomial (trend) and the second to a Fourier series (seasonality), which costs a little accuracy and buys a decomposition you can plot — see decompose.

Constructor
__init__(
    self,
    lookback: int = 24,
    horizon: int = 8,
    stack_type: str = 'generic',
    n_stacks: int = 2,
    n_blocks: int = 2,
    n_layers: int = 2,
    hidden_size: int = 64,
    trend_polynomial_degree: int = 2,
    n_harmonics: int = 4,
    backcast_loss_weight: float = 0.5,
    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

  1. Slide a (lookback, horizon) window over the series to build a
supervised dataset.
  1. Normalise each window by its own mean and standard deviation.
  2. Pass the window through the first block: a stack of ReLU layers
producing expansion coefficients \theta, projected onto a backcast basis and a forecast basis.
  1. Subtract the backcast from the block input and hand the residual to the
next block; accumulate the forecasts.
  1. Denormalise the summed forecast with the window's own statistics.

Theory

Block \ell receives residual x_\ell and produces

\hat{x}_\ell = V^b \theta^b_\ell, \qquad \hat{y}_\ell = V^f \theta^f_\ell,

where \theta_\ell = g_\ell(x_\ell) is the output of the fully connected trunk and V^b, V^f are the backcast and forecast bases. The residual recursion and the forecast aggregation are

x_{\ell+1} = x_\ell - \hat{x}_\ell, \qquad \hat{y} = \sum_{\ell=1}^{L} \hat{y}_\ell .

For the generic configuration V^b and V^f are identity — the trunk emits the vectors themselves. For the interpretable configuration the trend stack uses a polynomial basis of order p,

\hat{y}^{\text{tr}}_\ell = \sum_{i=0}^{p} \theta_{\ell,i}\, t^i, \qquad t = 0, \tfrac{1}{H}, \dots, \tfrac{H-1}{H},

and the seasonality stack a Fourier basis with fundamental period H,

\hat{y}^{\text{se}}_\ell = \sum_{k=1}^{K} \left[ \alpha_{\ell,k} \cos(2\pi k t) + \beta_{\ell,k} \sin(2\pi k t) \right].

The constant harmonic k = 0 is omitted so the seasonal component has exactly zero mean and cannot absorb the level.

Parameters

lookback
int = 24
Length of the input window. Automatically shrunk when the series is too short to yield training windows.
horizon
int = 8
Number of steps the network is trained to emit at once. Forecasts longer than this are produced by autoregressive rollout.
stack_type
{"generic", "interpretable"} = "generic"
"generic" learns the bases; "interpretable" uses a trend stack and a seasonality stack and enables decompose.
n_stacks
int = 2
Number of stacks in the generic configuration. Ignored when stack_type="interpretable", which always has exactly two.
n_blocks
int = 2
Blocks per stack.
n_layers
int = 2
Fully connected layers in each block's trunk.
hidden_size
int = 64
Width of the trunk. The paper uses 512; the default here is sized so the generic algorithm contract, which fits every model on sixty points, stays fast. Real use wants 256-512.
trend_polynomial_degree
int = 2
Polynomial order of the interpretable trend stack.
n_harmonics
int = 4
Harmonics in the interpretable seasonality stack.
backcast_loss_weight
float = 0.5
Weight on a penalty applied to the residual leaving the last block. The paper's objective supervises only the forecast, which leaves the backcast heads free to grow the residual; a small weight here is what makes the doubly-residual cascade real. Set to 0 for the paper's exact objective.
n_epochs
int = 100
Maximum passes over the window dataset. Real use wants several hundred to a few thousand.
batch_size
int = 32
Windows per gradient step.
learning_rate
float = 0.001
Adam step size.
patience
int = 15
Epochs without an improvement in training loss before stopping early.
device
{"cpu", "auto", "cuda", "mps"} = "cpu"
Where to run. The default is "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
int = None
Seed for weight initialisation and batch shuffling. With a seed and device="cpu" the forecast is reproducible.

Attributes

module_
torch.nn.Module
The trained network.
lookback_
int
Lookback actually used, after any shrinking.
horizon_
int
Horizon actually used, after any shrinking.
n_windows_
int
Number of training windows built from the series.
offset_
float
Mean of the training series, removed before fitting.
scale_
float
Standard deviation of the training series, divided out before fitting.
series_
np.ndarray of shape (n_samples,)
The globally scaled training series, kept for the rollout.
loss_curve_
np.ndarray of shape (n_epochs_run_,)
Mean training loss per epoch.
n_epochs_run_
int
Epochs actually run before early stopping.
device_
str
Resolved device string.

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 L \cdot B \cdot h) for E
epochs, W windows, L blocks, B trunk layers of width h.
  • Prediction: O(\lceil s / H \rceil \cdot L \cdot B \cdot h)
for s steps.

When to use NBEATSForecaster:

  • Univariate series with a few hundred points or more, where a classical
model underfits the shape.
  • When you want a strong forecaster with no feature engineering.
  • When an explicit trend/seasonality split is wanted, via
stack_type="interpretable".
  • Not for very short series: with fewer than roughly a hundred points,
ExponentialSmoothing or ARIMA will usually win.

References

Oreshkin2020
Oreshkin, B. N., Carpov, D., Chapados, N., & Bengio, Y. (2020). N-BEATS: Neural basis expansion analysis for interpretable time series forecasting. International Conference on Learning Representations (ICLR). https://doi.org/10.48550/arXiv.1905.10437
python
>>> import numpy as np
>>> from tuiml.algorithms.timeseries.deep import NBEATSForecaster
>>> from tuiml.utils.torch_backend import has_torch
>>> model = NBEATSForecaster(lookback=24, horizon=6, random_state=0)
>>> sorted(model.get_parameter_schema())[:3]
['backcast_loss_weight', 'batch_size', 'device']
>>> if has_torch():
...     y = np.sin(np.arange(200) / 5.0)
...     _ = model.fit(y)
...     print(model.predict(steps=6).shape)
... else:
...     print("(6,)")
(6,)

Methods

decompose (self, steps: int=1) -> Dict[str, np.ndarray]

Split the forecast into its trend and seasonality components.

Parameters
steps
int = 1
Number of future points to decompose.
Returns
components
dict of str to np.ndarray
"trend", "seasonality" and "forecast", each of shape (steps,) and in the units of the training series.
Raises
RuntimeError
If called before fit.
ValueError
If the model is not the interpretable configuration.
get_parameter_schema (cls) -> Dict[str, Dict[str, Any]]

Return JSON Schema for algorithm parameters.

get_complexity (cls) -> str

Return complexity analysis.

get_references (cls) -> List[str]

Return academic citations.

Functions

Func

polynomial_basis

Line 16
polynomial_basis(degree: int, length: int) -> np.ndarray

Build the polynomial basis N-BEATS uses for its trend stack.

Row p holds t^p on a time grid normalised to [0, 1), so a linear combination of the rows is a polynomial of order degree — a deliberately low-order, slowly varying function.

Parameters

degree
int
Highest power in the basis. The basis has degree + 1 rows.
length
int
Number of time steps to evaluate the basis on.

Returns

basis
np.ndarray of shape (degree + 1, length)
The basis matrix.
python
>>> from tuiml.algorithms.timeseries.deep.nbeats import polynomial_basis
>>> polynomial_basis(1, 4).round(2)
array([[1.  , 1.  , 1.  , 1.  ],
       [0.  , 0.25, 0.5 , 0.75]])
Func

fourier_basis

Line 46
fourier_basis(n_harmonics: int, length: int) -> np.ndarray

Build the Fourier basis N-BEATS uses for its seasonality stack.

Harmonics run from 1 to n_harmonics over a fundamental period of exactly length. The zeroth (constant) harmonic is deliberately omitted: it would let the seasonality stack absorb the level, blurring the split against the trend stack. Because it is omitted, every row — and so every linear combination of them — has exactly zero mean over one period and repeats with period length.

Parameters

n_harmonics
int
Number of harmonics. The basis has 2 * n_harmonics rows, a cosine and a sine per harmonic.
length
int
Number of time steps to evaluate the basis on. Passing a multiple of the fundamental period yields the periodic extension.

Returns

basis
np.ndarray of shape (2 * n_harmonics, length)
The basis matrix.
python
>>> import numpy as np
>>> from tuiml.algorithms.timeseries.deep.nbeats import fourier_basis
>>> basis = fourier_basis(2, 8)
>>> basis.shape
(4, 8)
>>> bool(np.allclose(basis.mean(axis=1), 0.0))
True