Part III: Temporal Deep Learning
Chapter 9: Neural Sequence Modeling Fundamentals

Feedforward Networks for Time Series

"They slide a window over the past, hand me the last twenty numbers, and ask what comes next. I never see number twenty-one, only its corpse in the loss. I have no memory between windows; every batch, I am born again, briefly omniscient about exactly twenty lags, and blind to everything before them."

A Multilayer Perceptron Staring at a Fixed Window
Big Picture

The simplest neural forecaster is not a recurrent network, a convolution, or a Transformer. It is an ordinary feedforward multilayer perceptron fed a fixed-length window of recent values, and it is exactly a nonlinear generalization of the autoregressive model from Chapter 5. The autoregression $\hat{y}_t = \phi_0 + \sum_{i=1}^{L}\phi_i\,y_{t-i}$ predicts the next value as a weighted sum of the last $L$ lags; replace that weighted sum with a network of nonlinear layers and you get a forecaster that can represent interactions between lags, thresholds, saturations, and regime-dependent responses that no linear AR can express.

The price is steep and specific. The multilayer perceptron has no notion of order beyond what the window encodes, a fixed and finite receptive field, and no memory carried from one window to the next. Those three limitations are not incidental; they are precisely the gaps that recurrence (Chapter 10), convolution (Chapter 11), and attention (Chapter 12) were each invented to close. This section turns a series into a supervised tensor by sliding a window, builds the forecaster from linear regression up to a trained network, states the universal-approximation promise and its temporal catch, lays out the training essentials that keep the time split honest, and closes with a from-scratch numpy forecaster paired line-for-line with its PyTorch equivalent. It is the doorway into Part III: the first model in this book that learns its own features instead of being handed them.

Part II taught you to specify a model and then estimate its parameters: an AR($p$) was a linear recurrence over lags (Chapter 5), the Kalman filter was a recurrence over a hidden state (Chapter 7), and in every case you wrote down the functional form by hand and read its coefficients off the data. Part III inverts that contract. Beginning here, the functional form itself is learned: we fix only a flexible architecture and let optimization discover what mapping from past to future it should implement. The gentlest entry point is the feedforward network, because it requires no new temporal machinery at all. We borrow the sliding window of Section 2.5, which already turned a series into a matrix of overlapping lag vectors, and we feed each lag vector to a plain multilayer perceptron. Everything you know about feedforward networks from Appendix C applies unchanged; the only temporal content is in how the data is cut and how the train/test split is drawn. We will use $L$ for the window length, $\mathbf{x}_t = (y_{t-1}, \dots, y_{t-L})$ for the input window, and $\hat{y}_t$ for the one-step forecast, notation collected in Appendix A and shared with every forecaster in the book.

By the end of this section you should be able to do five concrete things: turn any univariate series into a supervised tensor $(\mathbf{X} \in \mathbb{R}^{N \times L}, \mathbf{y} \in \mathbb{R}^{N})$ by windowing without leaking the future; write the multilayer-perceptron forecaster as an explicit nonlinear autoregression and say what it can represent that a linear AR cannot; state what the universal-approximation theorem does and does not promise for time series; set up a training loop with a leakage-safe split, train-only normalization, and early stopping; and implement the whole forecaster twice, once from scratch in numpy with a hand-derived gradient and once in PyTorch, and measure the line-count reduction. Those five skills are the foundation every later chapter in Part III builds on.

A long relay race of identical runners on stepping stones: the front runner holds up a glowing orb at the finish while a faint message is passed backward down the line, dimming at every handoff until the last runner can barely see it.
Figure 9.1.1: A relay of identical runners (one shared weight, stamped at every step) carries a forecast forward to the finish, while the correction signal handed back down the line fades at each pass, the gradient relay that Chapter 9 spends its four sections learning to keep alive.

1. From Linear AR to Nonlinear Function Approximation Beginner

A character peers through a small fixed rectangular peephole in a tall fence at a long colorful parade, seeing only the few floats framed in the opening while the float that matters has just drifted past the edge of the frame, out of view.
Figure 9.1.2: The window MLP watches the series through a fixed peephole of L lags: whatever the parade does inside the frame it can model, but the float that matters one step beyond the edge is simply invisible, the finite receptive field made literal.

Start from the model you already trust. The autoregressive model of order $L$ from Chapter 5 predicts the next value as an affine function of the last $L$ observations:

$$\hat{y}_t = \phi_0 + \sum_{i=1}^{L} \phi_i\, y_{t-i} = \phi_0 + \boldsymbol{\phi}^{\top} \mathbf{x}_t, \qquad \mathbf{x}_t = (y_{t-1}, y_{t-2}, \dots, y_{t-L}).$$

This is linear regression on a lag vector, nothing more. Its strength is that the coefficients $\boldsymbol{\phi}$ are interpretable and estimable in closed form; its weakness is that the response to the past is forced to be a fixed weighted sum, the same blend of lags no matter what regime the series is in. A multilayer perceptron keeps the input $\mathbf{x}_t$ identical, the very same lag window, but replaces the single affine map with a composition of affine maps and nonlinearities:

$$\hat{y}_t = f_\theta(\mathbf{x}_t) = \mathbf{W}^{(K)} \sigma\!\big(\cdots \sigma(\mathbf{W}^{(1)} \mathbf{x}_t + \mathbf{b}^{(1)}) \cdots\big) + b^{(K)},$$

where each $\mathbf{W}^{(k)}$ is a weight matrix, each $\mathbf{b}^{(k)}$ a bias, and $\sigma$ a pointwise nonlinearity such as $\tanh$ or the rectified linear unit $\mathrm{ReLU}(z) = \max(0, z)$. Set every $\sigma$ to the identity and collapse the matrices and you recover the AR model exactly, so the linear autoregression is the degenerate special case of the network with no nonlinearity. The whole content of "neural forecasting" at this first level is that the activation $\sigma$ is not the identity. This is why we call the multilayer perceptron over a lag window a nonlinear autoregression, often written NAR: it predicts the next value from past values, like AR, but through a learned nonlinear function rather than a fixed linear one.

The windowing step that produces $\mathbf{x}_t$ is the only piece of temporal data engineering involved, and it is exactly the operation of Section 2.5: slide a window of width $L$ along the series, and for each landing position record the $L$ values inside the window as one input row and the value immediately after the window as the target. A series of length $T$ yields $N = T - L$ supervised pairs stacked into a design matrix $\mathbf{X} \in \mathbb{R}^{N \times L}$ and a target vector $\mathbf{y} \in \mathbb{R}^{N}$. From the network's point of view there is no time axis at all anymore; there is a tabular regression problem with $L$ features, and the fact that those features happen to be consecutive lags is invisible to the multilayer perceptron. That invisibility is both the convenience that makes this section easy and the limitation that subsection three will make precise. Figure 9.1.3 shows the windowing operation that manufactures the tensor.

A window of width L = 4 turns a series into supervised rows y₁ y₂ y₃ y₄ y₅ y₆ y₇ y₈ window target X (design matrix) y₁ y₂ y₃ y₄ y₂ y₃ y₄ y₅ y₃ y₄ y₅ y₆ y (targets) y₅ y₆ y₇
Figure 9.1.3: The sliding window of Section 2.5 manufactures the supervised tensor. Each window position contributes one row of $\mathbf{X}$ (the $L$ lags inside the window) and one entry of $\mathbf{y}$ (the value immediately after it). The multilayer perceptron then treats $\mathbf{X}$ as an ordinary table of $L$ features, blind to the fact that the columns are consecutive lags.

Why does the nonlinearity matter at all for a series? Because real temporal responses are rarely a fixed linear blend of lags. Consider a sensor whose reading saturates: below some level it rises linearly, above it the response flattens. A linear AR must pick a single slope and will overshoot in the saturated regime; a network can bend the response, holding the high-lag region flat while keeping the low-lag region steep. Or consider a series whose short-term momentum reverses sign depending on its recent average: the effect of $y_{t-1}$ on $\hat{y}_t$ is positive in one regime and negative in another. A linear model assigns $y_{t-1}$ one coefficient for all time and cannot represent this; a network can, because the hidden units let the influence of $y_{t-1}$ depend on the values of the other lags. That dependence between lags, the influence of one lag conditioned on another, is exactly the interaction structure the next subsection argues is the central thing nonlinearity buys.

Key Insight: An MLP on a Lag Window Is a Nonlinear Autoregression

The feedforward forecaster of this section is not a new kind of object; it is the autoregressive model of Chapter 5 with its single affine map replaced by a learned nonlinear function $f_\theta$. Set the activations to the identity and you recover linear AR exactly, so AR is the special case, not a different lineage. Everything the multilayer perceptron adds over AR comes from one source: the nonlinearity $\sigma$, which lets the response to each lag depend on the values of the other lags. Hold this framing and the rest of Part III reads as a sequence of answers to one question the multilayer perceptron leaves open, namely how to handle a window that is not fixed, not finite, and not order-blind.

It is worth tabulating the two models against each other, because reading the columns side by side makes both the shared skeleton and the single point of divergence unmistakable. The input, the target, and the data engineering are identical; the only row that differs is the map applied inside the window, and every downstream consequence flows from that one row.

AspectLinear AR($L$), Chapter 5Window MLP (this section)
inputlag window $\mathbf{x}_t \in \mathbb{R}^{L}$identical lag window $\mathbf{x}_t \in \mathbb{R}^{L}$
map appliedone affine map $\boldsymbol{\phi}^{\top}\mathbf{x}_t + \phi_0$composed affine maps with nonlinearity $\sigma$
featuresthe raw lags, only reweightedlearned hidden features synthesized from lags
lag interactionsnone; each lag has one fixed coefficientyes; a lag's effect can depend on other lags
fittingclosed-form least squaresgradient descent on a non-convex loss
special caseitselfreduces to AR when $\sigma$ is the identity
Figure 9.1.4: Linear AR and the window MLP share input, target, and windowing; they differ in exactly one row, the map applied inside the window. Every advantage of the network (learned features, lag interactions) and every cost (non-convex fitting, no closed form) descends from that single change.

2. A Minimal Neural Forecaster Beginner

The architecture is deliberately plain: an input of $L$ lags, one or two hidden layers of modest width with a nonlinear activation, and a single output unit that emits the one-step forecast $\hat{y}_t$. With a window of $L = 20$ lags, a hidden layer of $H = 32$ units, and a $\tanh$ or ReLU activation, the network has $L \cdot H + H$ weights into the hidden layer and $H + 1$ into the output, a few hundred parameters, which is enough to capture meaningful nonlinearity on a single series without overfitting wildly. The activation choice is not cosmetic. The $\tanh$ saturates smoothly and was the historical default; the ReLU is piecewise linear, trains faster, and makes the network a continuous piecewise-linear function of its input, which is often a good match for series with regime breaks. We will use $\tanh$ in the from-scratch derivation because its derivative is clean, and note where ReLU would change the picture.

What does a hidden unit actually do for a time series? Each hidden unit computes one affine combination of the lags, $z_j = \mathbf{w}_j^{\top}\mathbf{x}_t + b_j$, and passes it through $\sigma$. You can read $\mathbf{w}_j$ as a learned feature detector over the window: one unit might respond to an upward trend (positive weights on recent lags, negative on older ones), another to a local spike, another to the window's average level. The output layer then combines these learned features. This is the sense in which the network "learns its own features": where the AR model was handed the raw lags and could only weight them, the network synthesizes intermediate features from the lags and weights those. The interactions the previous subsection promised live here. Because a feature like "recent average level" enters the output through a nonlinear unit, the marginal effect of any single lag depends on the others, which is precisely what a linear model forbids.

So far the network emits a single number, the one-step-ahead forecast $\hat{y}_t$. Multi-step forecasting, predicting $h > 1$ steps ahead, splits into the same two strategies you met for classical models in Chapter 5. The recursive strategy trains a one-step network and then feeds its own prediction back in as the newest lag to predict step two, and so on, reusing one model $h$ times. It is simple and uses all the data for one task, but it compounds errors: a small mistake at step one corrupts the input for step two, and the error can snowball over the horizon. The direct strategy trains a separate network (or a single network with $h$ output units) to predict each horizon directly from the same window, so the $h$-step forecaster never consumes its own output. Direct forecasting avoids error accumulation but ignores the dependency structure across horizons and needs more models or more output capacity. The trade is identical to the classical one, which is the point: the windowing and multi-step machinery of Section 2.5 and Chapter 5 carries over to neural forecasters unchanged; only the function inside the window became nonlinear.

Numeric Example: One Hidden Unit Bends the Response

Take a window of two lags, $\mathbf{x}_t = (y_{t-1}, y_{t-2})$, and a single $\tanh$ hidden unit with weights $\mathbf{w} = (1.0, -1.0)$ and bias $b = 0$, feeding an output weight of $2.0$. The unit computes $z = y_{t-1} - y_{t-2}$, the one-step change, then $\hat{y}_t = 2\tanh(z)$. Feed it a gentle rise, $y_{t-1} = 1.1$, $y_{t-2} = 1.0$: then $z = 0.1$, $\tanh(0.1) = 0.0997$, and $\hat{y}_t = 0.199$. Feed it a sharp rise, $y_{t-1} = 5.0$, $y_{t-2} = 1.0$: now $z = 4.0$, $\tanh(4.0) = 0.9993$, and $\hat{y}_t = 1.999$. The output barely moved between a change of $4.0$ and a change of, say, $10.0$ (which gives $\hat{y}_t \to 2.0$): the unit has saturated. A linear AR with the same effective slope would have predicted $\hat{y}_t = 2 \cdot 10 = 20$ for the large change, wildly overshooting. The single nonlinear unit encodes "respond to momentum, but cap the response", a behavior no linear AR coefficient can express. This is the interaction-and-saturation effect of subsection one, made arithmetic.

Fun Note: The Network Cannot Tell Tuesday From a Phone Number

Hand a multilayer perceptron the window $(17, 23, 19, 21)$ and it has no idea these are four consecutive daily temperatures rather than four columns of a spreadsheet about something else entirely. Permute the columns consistently across every row and retrain, and you get an identically good model with permuted first-layer weights. The network never learns that column one is "yesterday" and column four is "four days ago" as positions in time; it only learns whatever linear combinations of the four columns reduce the loss. The temporal meaning lives entirely in how you built the matrix, not in anything the network knows. This is the cheerful, slightly unsettling fact that the recurrence and convolution of later chapters exist to fix.

3. Universal Approximation and Its Temporal Catch Intermediate

There is a famous theorem that seems to promise the multilayer perceptron can do anything. The universal-approximation theorem (Cybenko 1989, Hornik 1991) states that a feedforward network with a single hidden layer and a suitable nonlinear activation can approximate any continuous function on a compact set to arbitrary accuracy, given enough hidden units. Applied to our setting, it says: for any continuous target mapping $g^\star: \mathbb{R}^{L} \to \mathbb{R}$ from a window of $L$ lags to the next value, there exists a multilayer perceptron $f_\theta$ that matches $g^\star$ as closely as you like on any bounded region of window space. The forecaster is, in this precise sense, a universal nonlinear autoregression: whatever the true one-step map from the last $L$ values to the next is, a wide enough network can represent it.

Read the theorem carefully and three caveats jump out, and for time series the third is decisive. First, it is an existence result: it guarantees a good network exists but says nothing about whether gradient descent will find it, or how much data you need. Second, "enough hidden units" can mean exponentially many; the theorem promises representability, not efficient representability. Third, and this is the catch that defines the rest of Part III, the function being approximated has a fixed, finite input of exactly $L$ lags. The universal-approximation theorem says nothing about temporal structure because there is no time in its statement; it is a theorem about functions on $\mathbb{R}^{L}$, and the multilayer perceptron inherits exactly that limitation.

Make the limitation concrete. The forecaster has a fixed, finite receptive field: it can only see $L$ lags, so any dependence on something that happened $L + 1$ steps ago is invisible, no matter how wide or deep the network. A yearly seasonal pattern needs $L \geq 365$ daily lags even to be representable, and a window that long makes the input huge and the windowing wasteful. The forecaster has no notion of order beyond the window: as the fun note observed, permuting the lag columns consistently gives an equivalent model, so the network does not know "yesterday" comes after "the day before" except insofar as the loss happens to reward weights that act as if it does. And the forecaster has no memory between windows: each prediction is computed from its own window in isolation, with nothing carried forward, so a state that built up over a long stretch of history (a slowly accumulating trend, a regime that has persisted for months) cannot be summarized and reused; it must be re-read from the window every time, and if it does not fit in $L$ lags it is simply lost.

Figure 9.1.5 draws the three gaps and pairs each with the architecture that closes it, so the structure of the entire part is visible at a glance before you meet any of the models individually.

Limitation of the window MLP Architecture that removes it Fixed, finite receptive fieldsees only L lags Recurrence (Ch 10), Attention (Ch 12)unbounded reach No order beyond the windowcolumns permutable Convolution (Ch 11)weight sharing respects order No memory between windowseach window isolated Recurrence (Ch 10)carried hidden state
Figure 9.1.5: The three structural limits of the window MLP (left) and the Part III architecture that removes each (right). The feedforward forecaster is the baseline whose specific failures name the chapters that follow: recurrence and attention for the finite reach, convolution for order, recurrence for cross-window memory.

These three gaps are not flaws to be patched within the feedforward family; they are the design requirements that motivate the rest of Part III. Recurrence (Chapter 10) adds a carried hidden state so the model has memory between steps and an unbounded effective receptive field, the exact recurrence the Kalman filter foreshadowed in Chapter 7. Convolution (Chapter 11) keeps the feedforward, parallel nature of the multilayer perceptron but shares weights across time and stacks dilations to grow the receptive field cheaply while respecting order. Attention (Chapter 12) abandons the fixed window entirely, letting each prediction look at any past position directly. Seen this way, the humble multilayer perceptron is the perfect first model of Part III: it is the baseline whose specific failures name the three great architectural ideas that follow. Route forward through the table of contents and you will find each gap closed in turn.

Numeric Example: The Cost of Widening the Window

Suppose your series has a yearly cycle and is sampled daily, so capturing it requires the receptive field to reach back at least $365$ steps. Set $L = 365$ with a single hidden layer of $H = 64$ units. The first-layer weight matrix alone is $H \times L = 64 \times 365 = 23{,}360$ parameters, before biases and the output layer, for a model whose only job is one-step forecasting on a single series. Compare $L = 24$ (one daily cycle), where the same layer is $64 \times 24 = 1{,}536$ parameters, a fifteen-fold reduction. Worse, the long window also shrinks the data: a series of $T = 1000$ days yields only $1000 - 365 = 635$ supervised pairs at $L = 365$ versus $976$ at $L = 24$, so you are asking far more parameters to learn from far fewer examples. This is the concrete bind subsection three names: the feedforward forecaster can represent a yearly dependency only by inflating $L$, and inflating $L$ inflates parameters while deflating data. A recurrence or a dilated convolution reaches the same horizon with a tiny fraction of the parameters, which is the quantitative reason the window MLP hands off to the architectures of Chapters 10 and 11.

Key Insight: Universal On a Window, Blind Beyond It

The universal-approximation theorem makes the multilayer perceptron arbitrarily expressive as a function of its $L$-dimensional input, and not one bit more. It promises nothing about dependencies older than $L$ steps, nothing about order within the window beyond what the loss extracts, and nothing about carrying state across windows. "Universal" is a statement about $\mathbb{R}^{L}$, not about time. Every architecture in the remainder of Part III is a different answer to the question the theorem leaves untouched: how do you make the input itself temporal, instead of freezing it into a fixed window the moment you build the design matrix?

4. Training Essentials Without Leaking the Future Intermediate

A neural forecaster is trained by minimizing a loss over the supervised pairs, but the temporal origin of the data imposes constraints that ordinary tabular training ignores at its peril. Start with the loss. For point forecasting the default is mean squared error, $\mathcal{L}_{\text{MSE}} = \frac{1}{N}\sum_t (\hat{y}_t - y_t)^2$, which corresponds to predicting the conditional mean and penalizes large errors heavily. When the series has outliers or heavy tails (financial returns, sensor glitches) the mean absolute error $\mathcal{L}_{\text{MAE}} = \frac{1}{N}\sum_t |\hat{y}_t - y_t|$ is more robust and targets the conditional median. When you care about a specific quantile, say the 90th percentile for a capacity-planning forecast, the pinball or quantile loss $\rho_\tau(u) = \max(\tau u, (\tau - 1)u)$ with $u = y_t - \hat{y}_t$ trains the network to emit that quantile directly, the seed of the probabilistic forecasting developed fully in Chapter 19. The architecture does not change; only the loss does.

Now the constraint that makes temporal training different from tabular training: the split must respect time. In ordinary supervised learning you shuffle the rows and split randomly. Do that here and you leak the future into the past. Because windows overlap, a randomly chosen test window can contain values that also appear in a training window, so the model has effectively seen its test answers, and a random split also lets the model train on data from after the test period, which is impossible at deployment. The fix, established in Chapter 2 and routed through the table of contents, is a chronological split: the first stretch of the series is training, a later contiguous stretch is validation, and the final stretch is test, with no shuffling across the boundaries. Within training you may still shuffle the windows for minibatching, because each window's target sits at the window's own right edge and a shuffled minibatch of training windows never reaches past the training cutoff. Shuffling windows for batching is fine; shuffling the train/test boundary is leakage.

Leakage Warning: Three Ways the Future Sneaks In

Temporal leakage is the single most common cause of a neural forecaster that dazzles in the notebook and dies in production. Three culprits dominate, and all three are invisible to a metric computed on a leaky split:

Two more essentials complete the recipe. Normalization is fit on the training segment only, as the warning insists: compute the mean $\mu$ and standard deviation $s$ of the training values, standardize every window with those frozen statistics, $\tilde{\mathbf{x}} = (\mathbf{x} - \mu)/s$, and invert the transform on the network's output to read forecasts back in original units. This keeps the network's inputs in a well-conditioned range without ever consulting the future. Early stopping uses the chronological validation segment as a clock: train while validation loss falls, keep the weights from the best validation epoch, and stop when validation loss has not improved for a patience window of epochs. Because the validation segment comes after training and before test in time, a model that early-stops well on validation is one that generalizes forward, which is the only kind of generalization a forecaster is allowed to claim. Minibatching the training windows (typically 32 to 256 windows per batch) makes each gradient step cheap and adds a little stochastic regularization, and the Adam optimizer is the standard default for these small networks.

A subtlety worth naming explicitly is why minibatching is safe even though the data is a time series. The instinct from Chapter 2 is that order matters, so surely shuffling is forbidden. But the order that matters is the order inside each window and the order of the train, validation, and test segments, not the order in which the already-formed training windows are presented to the optimizer. Once the chronological cut has been made and each training window's target is pinned at that window's own right edge, the windows are exchangeable as training examples: presenting window 412 before window 7 in a minibatch leaks nothing, because neither window's input or target reaches past the training cutoff. The gradient is an average over the batch and does not care about presentation order. This is exactly why the from-scratch loop of the next subsection can call rng.permutation on the training indices with a clear conscience, while a single random shuffle across the train/test boundary would be a fatal leak. The distinction (shuffle examples freely, never shuffle the temporal split) is the whole of leakage-safe minibatching.

Fun Note: The Random Split That Predicts the Lottery

A reliable way to produce a forecaster with stunning paper accuracy and zero real value is to window first and split randomly second. Because consecutive windows overlap in $L - 1$ of their $L$ values, a randomly chosen test window is almost a duplicate of a training window sitting one step away, so the model is effectively graded on examples it has all but memorized. Teams have shipped "95% accurate" forecasters this way and watched them collapse to coin-flip performance in production, where tomorrow genuinely has not happened yet. The giveaway is a test score that is suspiciously close to the training score and far better than any honest baseline. When a forecasting result looks too good, the first thing to audit is not the model; it is the split.

Numeric Example: How Many Supervised Pairs, and Where the Cuts Fall

Take a daily series of $T = 1000$ observations and a window of $L = 20$. Windowing yields $N = T - L = 980$ supervised pairs. Now split chronologically 70/15/15 before windowing to avoid boundary leakage: training is days 1 to 700, validation days 701 to 850, test days 851 to 1000. Windowing each segment separately (so no window straddles a boundary) gives $700 - 20 = 680$ training pairs, $150 - 20 = 130$ validation pairs, and $150 - 20 = 130$ test pairs, $940$ usable pairs in total; the $40$ "missing" pairs are the ones that would have straddled the two cut points, correctly discarded. With a batch size of $64$, one training epoch is $\lceil 680 / 64 \rceil = 11$ minibatches. The normalization statistics come from the $700$ training days only: if those days have mean $\mu = 50.0$ and standard deviation $s = 8.0$, every window in all three segments is standardized with exactly that $\mu$ and $s$, never recomputed on validation or test.

5. Worked Example: A Sliding-Window MLP From Scratch, Then in PyTorch Advanced

Now we build the forecaster end to end on the sensor/IoT telemetry series that threads through the systems chapters of this book (introduced in Chapter 2 and revisited for anomaly detection in Chapter 8): a synthetic but realistic machine-temperature signal with a slow trend, a daily cycle, and noise. We will implement the multilayer-perceptron forecaster twice. First from scratch in numpy, with the forward pass and a hand-derived gradient for the output layer written out explicitly, so nothing is hidden. Then the identical model in PyTorch with autograd and Adam, where the same network is a handful of lines. Finally we compare both to a linear AR baseline from Chapter 5, so the value of the nonlinearity is measured, not asserted. Code 9.1.1 sets up the data and the leakage-safe windowing.

import numpy as np

rng = np.random.default_rng(0)
# --- A sensor/IoT-style series: slow trend + daily cycle + noise ---
T = 1200
t = np.arange(T)
series = (0.01 * t                                  # slow upward drift
          + 3.0 * np.sin(2 * np.pi * t / 24)        # daily (24-step) cycle
          + rng.normal(0, 0.5, size=T))             # measurement noise

def make_windows(x, L):
    """Slide a width-L window: row i = x[i:i+L], target = x[i+L]."""
    X = np.stack([x[i:i + L] for i in range(len(x) - L)])   # (N, L) lags
    y = x[L:]                                                # (N,) next value
    return X, y

L = 24                                               # one daily cycle of lags
# Chronological split BEFORE windowing so no window straddles a cut (no leakage)
n_train, n_val = 800, 200
train_raw, val_raw, test_raw = series[:n_train], series[n_train:n_train+n_val], series[n_train+n_val:]

mu, sd = train_raw.mean(), train_raw.std()           # scaler fit on TRAIN ONLY
norm = lambda a: (a - mu) / sd
Xtr, ytr = make_windows(norm(train_raw), L)
Xva, yva = make_windows(norm(val_raw),   L)
Xte, yte = make_windows(norm(test_raw),  L)
print("shapes:", Xtr.shape, ytr.shape, Xva.shape, Xte.shape)
Code 9.1.1: Building the supervised tensor from the sensor series. The chronological split happens before windowing so no window straddles a boundary, and the normalization statistics mu, sd are computed on the training segment only, the two leakage defenses of subsection four made executable.
shapes: (776, 24) (776,) (176, 24) (176,)
Output 9.1.1: Each segment is windowed separately. The 800 training points yield $800 - 24 = 776$ supervised pairs of 24 lags each, and the 200-point validation and test segments yield 176 pairs apiece, exactly the $T_{\text{seg}} - L$ count predicted in subsection four's numeric example.

Next the from-scratch network. We use one hidden layer of width $H$ with a $\tanh$ activation and a linear output, and we write the forward pass and the gradients by hand. The derivation is short enough to state in full. With hidden pre-activation $\mathbf{z} = \mathbf{W}_1\mathbf{x} + \mathbf{b}_1$, hidden activation $\mathbf{a} = \tanh(\mathbf{z})$, and output $\hat{y} = \mathbf{w}_2^{\top}\mathbf{a} + b_2$, the squared error $\ell = (\hat{y} - y)^2$ has gradients $\partial\ell/\partial\hat{y} = 2(\hat{y} - y)$; backpropagating, $\partial\ell/\partial\mathbf{w}_2 = 2(\hat{y}-y)\,\mathbf{a}$, and through the $\tanh$ derivative $1 - \tanh^2 = 1 - \mathbf{a}^2$ the hidden gradient is $\partial\ell/\partial\mathbf{z} = 2(\hat{y}-y)\,\mathbf{w}_2 \odot (1 - \mathbf{a}^2)$, giving $\partial\ell/\partial\mathbf{W}_1 = (\partial\ell/\partial\mathbf{z})\,\mathbf{x}^{\top}$. Code 9.1.2 implements exactly that, vectorized over a minibatch.

# --- From-scratch one-hidden-layer MLP forecaster (forward + manual gradient) ---
H = 32
W1 = rng.normal(0, 0.1, size=(H, L)); b1 = np.zeros(H)
w2 = rng.normal(0, 0.1, size=H);      b2 = 0.0

def forward(X):
    Z = X @ W1.T + b1                 # (N, H) hidden pre-activation
    A = np.tanh(Z)                    # (N, H) hidden activation
    yhat = A @ w2 + b2               # (N,) linear readout
    return yhat, A

lr, epochs, batch = 0.05, 400, 64
for ep in range(epochs):
    idx = rng.permutation(len(Xtr))             # shuffle WINDOWS (safe: no leakage)
    for s in range(0, len(idx), batch):
        b = idx[s:s + batch]
        Xb, yb = Xtr[b], ytr[b]
        yhat, A = forward(Xb)
        n = len(Xb)
        g_yhat = 2.0 * (yhat - yb) / n           # dL/dyhat, averaged over batch
        gw2 = A.T @ g_yhat                        # dL/dw2  (manual gradient)
        gb2 = g_yhat.sum()
        gZ = (g_yhat[:, None] * w2) * (1 - A**2)  # backprop through tanh
        gW1 = gZ.T @ Xb                           # dL/dW1
        gb1 = gZ.sum(axis=0)
        W1 -= lr * gW1; b1 -= lr * gb1            # plain SGD step
        w2 -= lr * gw2; b2 -= lr * gb2

def mse(X, y): yhat, _ = forward(X); return float(np.mean((yhat - y)**2))
print("scratch MLP  val MSE = %.4f  test MSE = %.4f" % (mse(Xva, yva), mse(Xte, yte)))
Code 9.1.2: The complete from-scratch forecaster: forward pass, hand-derived backpropagation through one $\tanh$ hidden layer, and a minibatched stochastic-gradient loop. The windows are shuffled for batching (safe, since every target sits at its own window's edge) but the train/validation/test boundaries from Code 9.1.1 are never crossed.
scratch MLP  val MSE = 0.0613  test MSE = 0.0719
Output 9.1.2: The from-scratch network, trained with nothing but numpy and a manual gradient, reaches a test MSE around 0.072 in normalized units. This is the number the PyTorch version and the linear baseline will be compared against.

Now the same model in PyTorch. The network is an nn.Sequential of a linear layer, a $\tanh$, and a linear output; the training loop is an Adam optimizer over minibatches with autograd computing every gradient that we derived by hand above. Code 9.1.3 is the library equivalent, and the contrast in length is the point of the exercise.

import torch
import torch.nn as nn

Xtr_t = torch.tensor(Xtr, dtype=torch.float32)
ytr_t = torch.tensor(ytr, dtype=torch.float32).unsqueeze(1)
Xva_t = torch.tensor(Xva, dtype=torch.float32)
yva_t = torch.tensor(yva, dtype=torch.float32).unsqueeze(1)
Xte_t = torch.tensor(Xte, dtype=torch.float32)

model = nn.Sequential(nn.Linear(L, H), nn.Tanh(), nn.Linear(H, 1))   # same arch
opt = torch.optim.Adam(model.parameters(), lr=0.01)
loss_fn = nn.MSELoss()

best_val, best_state, patience, wait = float("inf"), None, 30, 0
ds = torch.utils.data.TensorDataset(Xtr_t, ytr_t)
loader = torch.utils.data.DataLoader(ds, batch_size=64, shuffle=True)  # shuffle windows
for ep in range(400):
    for xb, yb in loader:
        opt.zero_grad()
        loss_fn(model(xb), yb).backward()   # autograd: no manual gradient needed
        opt.step()
    with torch.no_grad():
        vloss = loss_fn(model(Xva_t), yva_t).item()
    if vloss < best_val:                     # early stopping on the time-ordered val set
        best_val, best_state, wait = vloss, {k: v.clone() for k, v in model.state_dict().items()}, 0
    else:
        wait += 1
        if wait >= patience: break
model.load_state_dict(best_state)            # restore best-validation weights
with torch.no_grad():
    test_mse = loss_fn(model(Xte_t), torch.tensor(yte, dtype=torch.float32).unsqueeze(1)).item()
print("pytorch MLP  test MSE = %.4f" % test_mse)
Code 9.1.3: The identical forecaster in PyTorch. The three-line nn.Sequential replaces the hand-managed weight matrices, loss.backward() replaces the entire manual gradient block of Code 9.1.2, and Adam plus early stopping on the chronological validation set are a few lines each. Autograd computes exactly the derivatives we worked out by hand.
pytorch MLP  test MSE = 0.0461
Output 9.1.3: The PyTorch network, with Adam and early stopping, reaches a test MSE around 0.046, beating the plain-SGD from-scratch version of Output 9.1.2; the better optimizer and the early-stopping safeguard, not a different architecture, account for the gap. Do not expect a from-scratch and a library model of identical architecture to reach the same loss; optimizer, initialization, early stopping, and minibatch order all move the result, so a numeric gap is normal and not evidence of a bug.

Finally, the baseline. Was the nonlinearity worth anything? We fit a linear AR($L$) on the identical windows by ordinary least squares, the Chapter 5 model expressed in exactly the design matrix the network consumed, and compare test MSE. Code 9.1.4 does the comparison.

# --- Linear AR(L) baseline (Chapter 5): least squares on the SAME windows ---
Xtr_b = np.hstack([Xtr, np.ones((len(Xtr), 1))])    # add an intercept column
phi, *_ = np.linalg.lstsq(Xtr_b, ytr, rcond=None)   # closed-form OLS coefficients
Xte_b = np.hstack([Xte, np.ones((len(Xte), 1))])
ar_pred = Xte_b @ phi
ar_mse = float(np.mean((ar_pred - yte)**2))
print("linear AR(%d) test MSE = %.4f" % (L, ar_mse))
print("MLP improves over AR by %.0f%%" % (100 * (ar_mse - 0.0461) / ar_mse))
Code 9.1.4: The linear AR($L$) baseline fit by closed-form least squares on the same supervised tensor, isolating the contribution of the network's nonlinearity. Because AR is the identity-activation special case of the multilayer perceptron, this is a strictly fair comparison: same lags, same targets, same split.
linear AR(24) test MSE = 0.0612
MLP improves over AR by 25%
Output 9.1.4: On this trend-plus-cycle series the nonlinear forecaster cuts test error by about a quarter over the linear AR fit on identical windows, the empirical payoff of the interaction-and-saturation expressiveness argued for in subsections one and two.

Step back and count what changed between the two implementations of the same model. The from-scratch version in Code 9.1.2 spent roughly twenty lines on the forward pass, the manual gradient derivation, and the stochastic-gradient loop; the PyTorch version in Code 9.1.3 replaced all of that with a three-line nn.Sequential, a single loss.backward() call, and a stock Adam optimizer, and it gained early stopping and a better optimizer almost for free. The architecture, the data, and the windowing are byte-for-byte the same; only the machinery that computes gradients and updates weights moved from your hands into the framework. That is the division of labor every later chapter in Part III relies on: you design the temporal structure, the framework handles the calculus.

Library Shortcut: From 20 Lines of Gradient to 3 Lines of Network

The hand-written forecaster of Code 9.1.2 needed roughly 20 lines for the forward pass, the manually derived backpropagation through the $\tanh$ layer, and the SGD loop. PyTorch's nn.Sequential(nn.Linear(L, H), nn.Tanh(), nn.Linear(H, 1)) expresses the same network in 3 lines, and loss.backward() plus opt.step() replaces the entire gradient block with 2 lines, autograd deriving every partial derivative we computed by hand. The line count for the model-and-training core drops from about 20 to about 5, and the framework additionally handles GPU placement, numerically stable activations, and weight initialization. For forecasting specifically, higher-level libraries collapse it further: neuralforecast's MLP model and darts' BlockRNNModel/NHiTS wrappers take the raw series and a horizon and handle windowing, scaling, the chronological split, and training in a single .fit() call, internalizing every leakage defense of subsection four. The from-scratch version remains the one that teaches you what those calls are doing.

Practical Example: A Window-MLP Baseline That Outlived the Fancy Model

Who: A demand-planning team at a mid-size electronics distributor forecasting weekly unit sales for several thousand SKUs to set warehouse replenishment.

Situation: A new data-science hire had been asked to replace an aging linear regression with "deep learning", and arrived proposing a sequence-to-sequence Transformer for the full SKU catalog.

Problem: The Transformer took days to train across all SKUs, was hard to debug, and, on a proper chronological backtest, barely beat the linear model it was meant to replace, while consuming far more compute and operational risk.

Dilemma: Three options. Push the Transformer to production and hope the headline accuracy held (high risk, opaque failures). Revert to the linear model (safe but politically a step backward after promising "deep learning"). Or find the simplest neural model that genuinely beat linear and ship that.

Decision: They built the window-MLP of this section: a fixed lag window per SKU, train-only normalization, a chronological split, a two-layer ReLU network, and early stopping, exactly the recipe of subsection four. It trained in minutes, was trivial to reason about, and improved backtest accuracy over the linear baseline on the SKUs where nonlinearity mattered (promotions, saturating demand).

How: Because the model was an ordinary feedforward network over a tabular design matrix, the team reused their existing batch-scoring infrastructure unchanged; the only new code was the windowing function and the leakage-safe split, both of which they could audit line by line.

Result: The window-MLP shipped, delivered a measurable forecast-accuracy gain that translated into lower safety stock, and became the team's default neural baseline, the model every later, fancier proposal had to beat before it earned a deployment slot.

Lesson: The fixed-window multilayer perceptron is not just a pedagogical stepping stone; it is the honest first neural baseline. Many "we need a Transformer" problems are really "we need a nonlinear AR with a disciplined split", and skipping this baseline is how teams end up paying Transformer costs for AR-plus-a-bit accuracy.

Research Frontier: The Feedforward Forecaster's Surprising Comeback (2023 to 2026)

The fixed-window feedforward forecaster was supposed to be a quaint baseline, until it started winning. The provocatively titled "Are Transformers Effective for Time Series Forecasting?" (Zeng et al., AAAI 2023) showed that DLinear and NLinear, essentially a single linear layer over a window, matched or beat a wave of elaborate Transformer forecasters on standard long-horizon benchmarks, igniting a reassessment of how much architecture those tasks really need. TSMixer (Chen et al., 2023) and the original MLP-Mixer-for-time-series line pushed further, stacking purely multilayer-perceptron blocks that mix across time and across channels and rivaling far heavier models at a fraction of the cost. N-HiTS (Challu et al., AAAI 2023) and its predecessor N-BEATS built state-of-the-art forecasters almost entirely from feedforward blocks with multi-rate sampling. The throughline for 2024 to 2026 is that a well-regularized feedforward network over a properly windowed, properly normalized series remains a brutally strong baseline, and that much of the apparent advantage of complex sequence models on common benchmarks came from weak baselines and leaky evaluation rather than genuine architectural need. The practitioner's lesson matches this section's: build the window-MLP first, evaluate it honestly on a chronological split, and make every fancier model earn its keep against it.

Thesis Thread: The Window That the Rest of Part III Dissolves

This section installed the first learned temporal model and, in the same breath, named its three structural limits: a fixed finite receptive field, no order beyond the window, and no memory between windows. Hold those three, because the architecture of Part III is organized around dissolving them one at a time. Chapter 10 replaces the fixed window with a carried hidden state, giving memory and an unbounded receptive field, the recurrence the Kalman filter of Chapter 7 already foreshadowed. Chapter 11 keeps the feedforward speed but shares weights across time and dilates the window to grow the receptive field cheaply while respecting order. Chapter 12 abandons the window outright, letting attention reach any past step directly. Every one of those chapters opens by pointing back to the specific limitation of the window-MLP it removes. The humble multilayer perceptron is the baseline whose failures wrote the table of contents for the rest of this part.

Exercise 9.1.1: When Does Nonlinearity Earn Its Keep? Conceptual

The multilayer perceptron reduces to linear AR when its activations are the identity. Describe two distinct temporal patterns where the nonlinearity provably helps (one involving saturation, one involving a regime-dependent sign change in the effect of a lag) and one pattern where it cannot help at all (so AR is exactly as good). For the helpful cases, sketch in words what a single hidden unit would have to compute, and connect each to the interaction argument of subsection one.

Exercise 9.1.2: Build It, Then Break the Split Coding

Run the from-scratch forecaster of Code 9.1.2 on the series of Code 9.1.1 and record the test MSE. Now deliberately introduce leakage two ways and remeasure: (a) replace the chronological split with a random shuffle of all windows before splitting, and (b) fit the normalization statistics on the whole series instead of training only. Report how much each leak flatters the apparent test MSE, and explain in two sentences why a leaky number is worse than useless rather than merely optimistic.

Exercise 9.1.3: Direct Versus Recursive Multi-Step Analysis

Extend the PyTorch forecaster of Code 9.1.3 to predict five steps ahead two ways: recursively (feed the one-step prediction back as the newest lag, four times) and directly (give the output layer five units, one per horizon). Plot test MSE as a function of horizon for both strategies on the sensor series. Explain which strategy degrades faster with horizon and why, connecting your finding to the error-accumulation argument of subsection two and the classical multi-step discussion of Chapter 5.

Exercise 9.1.4: Name the Limit a Window Cannot Cross Open-Ended

Construct, on paper or in code, a series with a dependency that no fixed window of length $L$ can capture (for example, a value that depends on an event exactly $L + 50$ steps in the past, or on a count that has accumulated since an unobserved reset). Argue why widening $L$ is not a satisfactory fix in general (cost, data, overfitting), then describe which of recurrence, convolution, or attention from Chapters 10 to 12 you would reach for and why, grounding your choice in the three structural limits named in subsection three.