N-BEATS › Implementation in PyTorch

Michael BrenndoerferAugust 20, 20259 min read
Machine Learning from ScratchAll chapters
Part 8 of 9Time Series
Chapter 60N-BEATS › Implementation in PyTorch
Reading section 5 of 8 · Implementation in PyTorch · 9 minSection 5 of 8 · 9 min

Book contents

Section 5 of 8 · 9 min read

N-BEATS › Implementation in PyTorchOne section · Sections in this chapter5 / 8 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Now that we understand the mathematical framework and have seen a working example, let's build a complete, production-ready implementation. This implementation will translate the formulas we've discussed into actual PyTorch code that you can use for your own time series forecasting projects.

What we'll implement: We'll create a full N-BEATS model that includes:

  • The neural network architecture that learns coefficients (θ=NN(xt)\boldsymbol{\theta} = \text{NN}(\mathbf{x}_t))
  • The trend basis functions (gktg_k^t with polynomial terms)
  • The seasonal basis functions (gksg_k^s with trigonometric terms)
  • The training loop that minimizes the loss function (L=1T∑t=1T(yt−y^t)2\mathcal{L} = \frac{1}{T} \sum_{t=1}^{T} (y_t - \hat{y}_t)^2)

This implementation follows the mathematical framework we've established, making it easy to see how the formulas translate into code.

Step 1: Import Required Libraries

Step 2: Define the N-BEATS Architecture

In[18]:
Code
class NBEATSBlock(nn.Module):
    """Single N-BEATS block for trend or seasonal decomposition"""

    def __init__(self, input_size, theta_size, hidden_size=64):
        super(NBEATSBlock, self).__init__()

        # Neural network to learn coefficients
        self.network = nn.Sequential(
            nn.Linear(input_size, hidden_size),
            nn.ReLU(),
            nn.Linear(hidden_size, hidden_size),
            nn.ReLU(),
            nn.Linear(hidden_size, theta_size),
        )

        self.theta_size = theta_size

    def forward(self, x):
        return self.network(x)


class NBEATS(nn.Module):
    """Complete N-BEATS model"""

    def __init__(self, input_size=12, forecast_horizon=6, hidden_size=64):
        super(NBEATS, self).__init__()

        # Trend block
        self.trend_block = NBEATSBlock(
            input_size=input_size,
            theta_size=forecast_horizon
            * 2,  # Linear and quadratic coefficients
            hidden_size=hidden_size,
        )

        # Seasonal block
        self.seasonal_block = NBEATSBlock(
            input_size=input_size,
            theta_size=forecast_horizon * 2,  # Sine and cosine coefficients
            hidden_size=hidden_size,
        )

        self.forecast_horizon = forecast_horizon

    def forward(self, x):
        # Get trend coefficients
        trend_coeffs = self.trend_block(x)
        trend_coeffs = trend_coeffs.view(-1, self.forecast_horizon, 2)

        # Get seasonal coefficients
        seasonal_coeffs = self.seasonal_block(x)
        seasonal_coeffs = seasonal_coeffs.view(-1, self.forecast_horizon, 2)

        # Generate forecasts
        forecasts = []
        for h in range(self.forecast_horizon):
            # Trend component
            trend_h = trend_coeffs[:, h, 0] * (h + 1) + trend_coeffs[
                :, h, 1
            ] * ((h + 1) ** 2)

            # Seasonal component
            seasonal_h = seasonal_coeffs[:, h, 0] * torch.sin(
                2 * torch.pi * torch.tensor(h + 1) / 12
            ) + seasonal_coeffs[:, h, 1] * torch.cos(
                2 * torch.pi * torch.tensor(h + 1) / 12
            )

            forecast_h = trend_h + seasonal_h
            forecasts.append(forecast_h)

        return torch.stack(forecasts, dim=1)


# Initialize model
model = NBEATS(input_size=12, forecast_horizon=6, hidden_size=64)
total_params = sum(p.numel() for p in model.parameters())
Out[19]:
Console
Model parameters: 11544

The model contains a moderate number of parameters, which balances model capacity with computational efficiency. For this configuration with input_size=12, forecast_horizon=6, and hidden_size=64, the parameter count is reasonable for most time series forecasting tasks. This size allows the model to learn complex patterns without requiring excessive computational resources or risking overfitting on smaller datasets.

Step 3: Data Preparation and Training

In[20]:
Code
def prepare_data(data, input_size=12, output_size=6):
    """Prepare data for N-BEATS training"""
    X, y = [], []

    for i in range(len(data) - input_size - output_size + 1):
        X.append(data[i : i + input_size])
        y.append(data[i + input_size : i + input_size + output_size])

    return np.array(X), np.array(y)


# Generate sample data
np.random.seed(42)
months = 100
t = np.arange(months)

# Create time series with trend and seasonality
trend = 100 + 0.5 * t
seasonal = 10 * np.sin(2 * np.pi * t / 12) + 5 * np.cos(2 * np.pi * t / 12)
noise = np.random.normal(0, 2, months)
data = trend + seasonal + noise

# Prepare training data
X, y = prepare_data(data, input_size=12, output_size=6)

# Convert to PyTorch tensors
X_tensor = torch.FloatTensor(X)
y_tensor = torch.FloatTensor(y)
Out[21]:
Console
Training data shape: torch.Size([83, 12])
Target data shape: torch.Size([83, 6])
Number of training samples: 83

The training data consists of input sequences of length 12 (one year of monthly data) and target sequences of length 6 (6-month forecasts). The number of training samples is determined by the sliding window approach, which creates overlapping sequences from the historical data. With 100 months of data, the sliding window creates multiple training examples, allowing the model to learn from different segments of the time series. This approach maximizes the use of available data and helps the model generalize better.

Step 4: Training Loop

In[22]:
Code
# Training setup
optimizer = optim.Adam(model.parameters(), lr=0.001)
criterion = nn.MSELoss()

# Training loop
epochs = 1000
model.train()
training_losses = []

for epoch in range(epochs):
    optimizer.zero_grad()

    # Forward pass
    predictions = model(X_tensor)

    # Calculate loss
    loss = criterion(predictions, y_tensor)

    # Backward pass
    loss.backward()
    optimizer.step()

    training_losses.append(loss.item())
Out[23]:
Console
Epoch 0, Loss: 19513.5293
Epoch 200, Loss: 40.3587
Epoch 400, Loss: 19.7800
Epoch 600, Loss: 10.7850
Epoch 800, Loss: 7.3420
Final training loss: 6.0384
Out[24]:
Visualization
Training loss curve showing decreasing loss over epochs during N-BEATS model training.
Training curve showing the loss decreasing over epochs during N-BEATS training. The steady decrease in loss indicates that the model is successfully learning to capture the trend and seasonal patterns in the time series data. The convergence behavior helps diagnose training effectiveness and potential overfitting.

The training loss decreases steadily over epochs, indicating that the model is successfully learning to capture the trend and seasonal patterns in the data. The steady decline suggests the model is converging rather than overfitting, as the loss continues to decrease without signs of instability. The final loss value provides a baseline for comparison. Lower values indicate better fit to the training data, though you should also evaluate on validation data to assess generalization performance.

Step 5: Making Predictions

In[25]:
Code
# Make prediction on last window
model.eval()
with torch.no_grad():
    last_window = data[-12:].reshape(1, -1)
    last_window_tensor = torch.FloatTensor(last_window)

    forecast = model(last_window_tensor)
    forecast_np = forecast.numpy().flatten()

# Generate "actual" future values for comparison (using the same generating process)
np.random.seed(43)  # Different seed for future data
t_future = np.arange(months, months + 6)
trend_future = 100 + 0.5 * t_future
seasonal_future = 10 * np.sin(2 * np.pi * t_future / 12) + 5 * np.cos(
    2 * np.pi * t_future / 12
)
noise_future = np.random.normal(0, 2, 6)
actual_future = trend_future + seasonal_future + noise_future

# Calculate performance metrics
mae = np.mean(np.abs(forecast_np - actual_future))
rmse = np.sqrt(np.mean((forecast_np - actual_future) ** 2))
Out[26]:
Console
N-BEATS Forecast:
Month 1: 156.45
Month 2: 151.93
Month 3: 147.65
Month 4: 141.39
Month 5: 140.69
Month 6: 144.73

Performance Metrics:
MAE: 1.74
RMSE: 2.06
Out[27]:
Visualization
Time series forecast chart with the final 40 training observations, six circular N-BEATS forecast markers, six dashed square actual markers, and a dotted boundary before the forecast period.
N-BEATS PyTorch implementation predictions compared to actual values. The plot shows the last 40 training months, the model's 6-month forecast, and the generated future values. The dotted vertical line separates the training and forecast periods.

The forecast shows the predicted values for the next 6 months, extrapolating the learned trend and seasonal patterns from the training data. The MAE (Mean Absolute Error) measures the average absolute difference between predictions and actual values, while RMSE (Root Mean Squared Error) penalizes larger errors more heavily. Lower values for both metrics indicate better forecast accuracy. For this synthetic dataset, the relatively low MAE and RMSE values suggest the model has successfully learned the underlying trend and seasonal patterns. In practice, compare these metrics to baseline methods (such as naive forecasts or seasonal averages) to assess whether N-BEATS provides meaningful improvements.

Key Parameters

Below are some of the main parameters that affect how N-BEATS works and performs.

  • input_size: Number of historical time series values used as input (default: 12). Should match or exceed the seasonal period. Use 12 for monthly data with yearly seasonality, 7 or 14 for daily data with weekly patterns. Larger windows capture more historical context but may include irrelevant distant patterns.

  • forecast_horizon: Number of future periods to forecast (default: 6). Shorter horizons (1-6 periods) are typically more accurate but may not provide sufficient lead time. Longer horizons (12-24 periods) are useful for strategic planning but require more historical data. Keep the forecast horizon to at most half the length of your training data.

  • hidden_size: Size of hidden layers in the neural network (default: 64). Controls model complexity and capacity. Larger values (128-256) can capture more complex patterns but increase computation time and risk overfitting with limited data. Smaller values (32-64) work well for simpler patterns and smaller datasets.

  • learning_rate: Learning rate for the Adam optimizer (default: 0.001). Controls how quickly the model learns. Lower values (0.0001-0.001) provide more stable training but may require more epochs. Higher values (0.01-0.1) train faster but may be unstable. Start with 0.001 and adjust based on training behavior.

  • epochs: Number of training iterations (default: 1000). More epochs allow the model to learn better but increase training time. Monitor training and validation loss to determine when to stop. Use early stopping if validation loss stops improving.

  • random_state: Seed for reproducibility (default: None). Set to an integer to ensure consistent results across runs. Important for debugging and comparing different model configurations.

Key Methods

The following are the most commonly used methods for interacting with N-BEATS models.

  • forward(x): Generates forecasts for input sequences. Takes a tensor of shape (batch_size, input_size) containing historical time series values and returns predictions of shape (batch_size, forecast_horizon). This is called automatically when using model(x).

  • train(): Sets the model to training mode, enabling dropout and batch normalization updates. Call this before training to ensure the model learns from the data.

  • eval(): Sets the model to evaluation mode, disabling dropout and freezing batch normalization statistics. Call this before making predictions to ensure consistent, deterministic outputs.

  • parameters(): Returns an iterator over all model parameters (weights and biases). Used by optimizers to update parameters during training. Can also be used to inspect or modify specific parameters.