A systematic benchmark of deep learning and classical ML architectures for retail demand forecasting, comparing six models on Walmart's M5 Kaggle dataset (3,049 products × 10 stores, 42,840 hierarchical time series in total). The Encoder-Decoder Transformer achieves the best MAE of 122.01 while simpler models remain surprisingly competitive.
Every component is built from scratch in PyTorch: the Transformer encoder-decoder with cross-attention and teacher forcing, the sliding-window dataset with temporal train/val/test splits, and a comprehensive feature engineering pipeline covering lag features, rolling statistics, calendar events, and price momentum. An XGBoost/LightGBM gradient boosting baseline provides a strong classical ML comparison point.
| Model | MAE | Training Time | Parameters |
|---|---|---|---|
| Linear Baseline | 137.01 | 28:48 | ~50K |
| Encoder-Only Transformer | 134.73 | 28:48 | ~2.1M |
| Encoder-Decoder Transformer | 122.01 | 49:03 | ~4.3M |
| DeepAR | 144.98 | 45:00 | ~3.2M |
| Temporal Fusion Transformer | 135.07 | 2:07:16 | ~8.5M |
| XGBoost (Gradient Boosting) | 131.52 | 35:12 | ~1K trees |
Input Sequence (28 days) Target Sequence (28 days) │ │ v v TimeSeriesEmbedding Decoder Embedding Linear + PosEnc Linear + PosEnc │ │ v v Encoder (N layers) Decoder (N layers) Self-Attention Cross-Attention + Feed-Forward →→ Memory →→ + Causal Self-Attn + LayerNorm + Feed-Forward │ │ Encoder Output v Linear → Forecast (B, 28)
M5 Raw Data │ └─ sales_train_evaluation.csv → Sales Matrix (42,840 × 1,969) │ └─ calendar.csv → Calendar: day_of_week, month, is_weekend, has_event, SNAP │ ┌─ sell_prices.csv → Price: normalized_price, price_momentum (Δ7d) Derived Features Sales Matrix → Lag features (7d, 14d, 28d) → Rolling stats (7d/14d/28d mean, std) Final Tensor: (42,840 × 1,969 × 15) → Sliding Window → Dataset
class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len=5000, dropout=0.1): super().__init__() self.dropout = nn.Dropout(p=dropout) pe = torch.zeros(max_len, d_model) position = torch.arange(0, max_len).unsqueeze(1).float() div_term = torch.exp( torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model) ) pe[:, 0::2] = torch.sin(position * div_term) pe[:, 1::2] = torch.cos(position * div_term) self.register_buffer("pe", pe.unsqueeze(0)) def forward(self, x): return self.dropout(x + self.pe[:, :x.size(1)])
def forward(self, x_sales, x_features=None, y=None): memory = self._encode(x_sales, x_features) if y is not None: # Teacher forcing decoder_input = torch.cat( [x_sales[:, -1:], y[:, :-1]], dim=1 ).unsqueeze(-1) # shift right, prepend last known tgt = self.decoder_embedding(decoder_input) tgt_mask = generate_causal_mask(tgt.size(1), tgt.device) decoded = self.decoder(tgt, memory, tgt_mask=tgt_mask) return self.output_layer(decoded).squeeze(-1) else: # Autoregressive inference return self._autoregressive_decode(memory, x_sales[:, -1])
def build_rolling_features(self, sales_matrix): """Compute rolling mean & std for each window size.""" n_series, n_days = sales_matrix.shape n_feats = len(self.ROLLING_WINDOWS) * 2 # mean + std rolling = np.zeros((n_series, n_days, n_feats), dtype=np.float32) for w_idx, window in enumerate(self.ROLLING_WINDOWS): for t in range(window, n_days): segment = sales_matrix[:, t - window : t] rolling[:, t, w_idx * 2] = segment.mean(axis=1) rolling[:, t, w_idx * 2 + 1] = segment.std(axis=1) return rolling # (n_series, n_days, n_windows * 2)
Data Pipeline: The M5 dataset contains 3,049 products × 10 stores, 42,840 hierarchical time series in total, each spanning ~1,969 days. Three CSV files (sales, calendar, prices) are merged into a unified feature tensor. The FeatureEngineer class extracts calendar features (weekday, month, events, SNAP), computes lag features at 7/14/28-day offsets, and generates rolling statistics (mean/std) over multiple window sizes. Price features include normalized sell price and 7-day price momentum.
Feature Engineering: Each time series sample consists of a 28-day input window and 28-day forecast horizon. The sliding window M5Dataset generates all valid (input, target) pairs, indexing into a precomputed sales matrix and feature tensor. Lag features are zero-padded for the first lag days; rolling statistics use vectorized numpy operations across all 42,840 series simultaneously for efficiency.
Model Training: The Trainer class manages the training loop with Adam optimizer, learning rate scheduling (Cosine Annealing or ReduceLROnPlateau), gradient clipping at norm 1.0, and best-model checkpointing based on validation MAE. The Encoder-Decoder Transformer uses teacher forcing during training — the decoder receives the shifted ground truth — and switches to autoregressive step-by-step generation at inference time.
Evaluation: Models are evaluated using MAE (primary metric), RMSE, RMSSE (M5 competition metric that normalizes by naive forecast variance), and Weighted RMSSE. The RMSSE normalizes per-series errors by the variance of a naive random-walk forecast on the training history, making errors comparable across products with vastly different sales volumes.
M5Dataset sliding window enforces this boundary