"They hand me a sequence and a loss and expect me to know what I am. Am I collapsing the whole history into one number, or am I supposed to speak back a sequence of my own? Tell me my output shape and I will tell you who I am."
A Sequence Model Unsure if It Is Many-to-One or Many-to-Many
Before you choose an architecture, you must name the task, and almost every temporal learning problem falls into one of five shapes defined purely by how the length of the output relates to the length of the input: one input to one output, one input to a sequence, a sequence to one output, a sequence to an equally long aligned sequence, and a sequence to a differently long unaligned sequence. Forecasting, anomaly labeling, activity recognition, and transduction are not four unrelated problems; they are four cells of this single table. Once you fix the shape you have fixed the output head, the loss family, the way variable lengths are padded and masked, and whether the model generates one step at a time (autoregressively) or emits the whole horizon at once. This section builds that taxonomy, then nails down the mechanics that every shape shares: how variable-length sequences are batched with padding and masking, which losses match which targets, how teacher forcing trains an autoregressive model and why it leaks, how static and covariate features enter, and why squeezing a whole input into one fixed context vector is the bottleneck that attention was invented to break. We close with a runnable PyTorch pipeline that builds both a many-to-one and a many-to-many head over the same padded, masked batch.
In Section 9.1 we built the simplest neural forecaster, a multilayer perceptron over a fixed lag window, and named the three limits (finite receptive field, no order, no memory between windows) that the carried-state recurrence of Section 7.6 is built to remove. That section answered "what is the simplest machine?". This section answers the prior question every practitioner must settle first: "what is the task?". The architecture follows from the task, not the other way around, and a startling number of modeling mistakes are really task-misclassification mistakes, for example training a many-to-one classifier when the problem actually demands a per-step aligned label. We will use the notation of the unified table in Appendix A: an input sequence $\mathbf{x}_{1:T} = (\mathbf{x}_1, \dots, \mathbf{x}_T)$ of length $T$, a target $\mathbf{y}$ that is either a single value, a vector, or its own sequence $\mathbf{y}_{1:T'}$ of length $T'$, and a model $f_\theta$ with parameters $\theta$.
By the end of this section you should be able to do four concrete things. Place any temporal problem you meet into the five-cell taxonomy and read off its output head from that placement. Pad and mask a ragged batch of sequences correctly, so that the loss never counts a padding position. Pick a loss that matches the target type, including the quantile loss that returns later when we forecast distributions. And explain why a fixed context vector limits an encoder-decoder, which is the motivation the attention chapter builds on. These four skills are the grammar of Part III; every later architecture is a different way of speaking this same grammar.
1. The Taxonomy of Sequence Tasks Beginner
The most useful first cut in all of sequence modeling sorts tasks by the relationship between input length and output length. There are exactly five shapes worth naming, and the discipline of naming the shape before reaching for a model is what keeps you from, say, building a generator when you needed a classifier. The five shapes are one-to-one, one-to-many, many-to-one, aligned many-to-many, and unaligned many-to-many. We take them in turn, each with its canonical temporal example.
A one-to-one task maps a single input to a single output with no temporal extent at all; it is the degenerate case, ordinary tabular regression or classification, and we list it only to mark the boundary of the family. A one-to-many task takes a single input and emits a sequence: a single conditioning vector seeds an entire generated trajectory, as when a static patient descriptor seeds a simulated vital-sign path, or a class label seeds a generated waveform. A many-to-one task consumes a whole sequence and returns one prediction; this splits by target type into classification (the sequence belongs to one of $C$ classes, for example recognizing an activity from an accelerometer window) and regression (the sequence maps to a real number, for example predicting next-quarter demand from a sales history).
The two many-to-many shapes are where most temporal subtlety lives, and the distinction between them is the single most important idea in this subsection. An aligned many-to-many task emits one output per input step, with the $t$-th output paired to the $t$-th input: sequence labeling, also called sequence tagging. Anomaly detection that flags each timestamp as normal or anomalous is aligned many-to-many, and so is per-step segmentation of a sensor stream into operating regimes. The lengths are equal, $T' = T$, and the alignment is given. An unaligned many-to-many task maps an input sequence to an output sequence of possibly different length with no given per-step correspondence: this is sequence-to-sequence, or transduction. Translating one language to another is the textbook case; in temporal AI, mapping an irregular event log to a regular forecast horizon, or transcribing a variable-length sensor burst into a variable-length symptom code sequence, are unaligned because $T' \ne T$ in general and nothing tells you which input step produced which output step. Figure 9.2.1 draws all five shapes at once.
The reason this taxonomy earns its place at the front of the chapter is that the output shape dictates the engineering. A many-to-one head pools the sequence into one vector and attaches a single classifier or regressor; an aligned many-to-many head attaches a small head at every step and computes the loss per step; an unaligned head needs an encoder that summarizes the input and a decoder that generates the output, the architecture of subsection five. Table 9.2.1 maps each shape to its temporal example, its output head, and its natural loss, so that classifying the task immediately yields the first three design decisions.
| Shape | Temporal example | Output head | Natural loss |
|---|---|---|---|
| one-to-one | map one feature vector to one value (tabular baseline) | single linear / softmax | MSE or cross-entropy |
| one-to-many | generate a synthetic vital-sign trajectory from a patient descriptor | autoregressive decoder | per-step likelihood |
| many-to-one (classification) | activity recognition from an accelerometer window | pool then softmax over $C$ classes | cross-entropy |
| many-to-one (regression) | forecast next-step demand from a sales history | pool then linear | MSE / MAE / pinball |
| aligned many-to-many | flag each timestamp as anomalous or normal | per-step head, $T'=T$ | masked per-step cross-entropy |
| unaligned many-to-many | transduce an irregular event log to a regular horizon | encoder-decoder, $T' \ne T$ | per-step likelihood with masking |
A model's identity is determined less by its internal cell (RNN, convolution, or attention, the subjects of Chapter 10 through Chapter 12) than by the shape of what it must emit. The same recurrent backbone becomes a classifier when you pool its states into one prediction, a labeler when you read out every state, and a generator when you feed its output back as its next input. Architecture is the verb; the task shape is the sentence it must complete. When a temporal project stalls, the first diagnostic question is almost never "is my cell powerful enough?" and almost always "did I match the output shape to the task?", because the two many-to-many shapes in particular are routinely confused, and an aligned labeler trained as a many-to-one classifier silently discards exactly the per-step signal the problem was about.
The five-shapes picture in Figure 9.2.1 is so canonical that a generation of practitioners learned it from one blog post, Andrej Karpathy's 2015 "The Unreasonable Effectiveness of Recurrent Neural Networks", which drew the boxes in red, green, and blue and made "one-to-many, many-to-one, many-to-many" part of the field's spoken vocabulary. The diagram predates the Transformer entirely, which is a quiet testament to its level of abstraction: it classifies tasks, not architectures, so it survived the architecture that replaced the RNN it was drawn to explain. You will meet the same five boxes wearing convolutional and attention costumes for the rest of Part III.
2. Inputs, Outputs, and the Shape of a Batch Beginner
Real temporal data is ragged. One patient stay is forty hours, another is four hundred; one customer has a two-year purchase history, another signed up last week. A neural network, however, wants rectangular tensors, so the first mechanical task in every sequence pipeline is turning a list of variable-length sequences into one padded tensor plus a mask that records which entries are real. Padding appends a filler value (commonly zero) to every sequence until all reach the batch's maximum length $T_{\max}$; the mask is a Boolean tensor of the same shape that is true at real positions and false at padding. Every downstream computation, the loss above all, must consult the mask so that padded positions contribute nothing, a discipline subsection six makes concrete in code.
The batch tensor for a single-feature series therefore has shape $(B, T_{\max})$, or $(B, T_{\max}, F)$ once each step carries $F$ features; PyTorch's convention with batch_first=True puts batch first, which we adopt throughout. Two further efficiencies matter in practice. Batching by length, sometimes called bucketing, groups sequences of similar length into the same batch so that $T_{\max}$ is not dominated by one outlier, which would otherwise waste computation on padding for every other sequence. And the packed-sequence representation, which we use in subsection six, lets the recurrent layers of Chapter 10 skip padded steps entirely rather than process and then discard them.
On the output side, a deeper fork appears whenever the task is to predict multiple future steps, the multi-horizon forecasting that recurs throughout Chapter 14. There are two ways to produce a horizon of length $H$, and the choice has consequences that ripple through training and inference. Autoregressive generation predicts one step, feeds that prediction back as input, and predicts the next, repeating $H$ times; the model only ever has a one-step head, but errors compound because each prediction conditions on the model's own possibly wrong earlier predictions. Direct multi-horizon output emits all $H$ values in a single forward pass from one head of width $H$; there is no error feedback loop, but the model cannot exploit its own intermediate predictions and must learn each horizon offset somewhat independently. This is the same windowing fork we set up in Chapter 2 when we slid input and output windows over a series; the input window became $\mathbf{x}_{1:T}$ and the output window became either one step generated $H$ times or $H$ steps emitted at once.
Suppose a batch holds three univariate series of lengths $5$, $3$, and $4$. Then $T_{\max} = 5$, and the padded tensor has shape $(B, T_{\max}) = (3, 5)$. Writing $p$ for the padding value, the padded batch and its mask are
$$\mathbf{X} = \begin{bmatrix} x^{(1)}_1 & x^{(1)}_2 & x^{(1)}_3 & x^{(1)}_4 & x^{(1)}_5 \\ x^{(2)}_1 & x^{(2)}_2 & x^{(2)}_3 & p & p \\ x^{(3)}_1 & x^{(3)}_2 & x^{(3)}_3 & x^{(3)}_4 & p \end{bmatrix}, \qquad \mathbf{M} = \begin{bmatrix} 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 0 & 0 \\ 1 & 1 & 1 & 1 & 0 \end{bmatrix}.$$The mask has $5 + 3 + 4 = 12$ true entries out of $15$, so a correctly masked per-step loss averages over $12$ positions, not $15$. If you forgot the mask and averaged over all $15$, the three padding zeros would pull a mean-squared error toward zero by a factor of $12/15 = 0.8$, silently reporting a loss $20\%$ lower than the truth. That single factor is the most common quiet bug in sequence pipelines.
3. Losses and Targets for Temporal Tasks Intermediate
With the task shape fixed and the batch padded, the loss is the next decision, and it follows almost mechanically from the target type. For a real-valued regression target the workhorses are mean-squared error $\frac{1}{N}\sum (y_i - \hat{y}_i)^2$, which penalizes large errors quadratically and corresponds to a Gaussian likelihood, and mean-absolute error $\frac{1}{N}\sum |y_i - \hat{y}_i|$, which is robust to outliers and corresponds to a Laplace likelihood. When the target is a category, whether the single label of a many-to-one classifier or the per-step label of an aligned tagger, the loss is cross-entropy, $-\sum_c y_c \log \hat{p}_c$, the negative log-likelihood of the correct class under the model's softmax.
Temporal forecasting adds a loss the tabular world rarely needs: the quantile, or pinball, loss, which trains a model to predict a specific quantile of the predictive distribution rather than its mean. For target quantile level $\tau \in (0, 1)$ and error $u = y - \hat{y}$, the pinball loss is
$$\rho_\tau(u) = \max\big(\tau\, u,\; (\tau - 1)\, u\big) = \begin{cases} \tau\, u & u \ge 0 \\ (\tau - 1)\, u & u < 0. \end{cases}$$It is an asymmetric absolute error: under-prediction (positive $u$) is weighted by $\tau$ and over-prediction by $1 - \tau$. Minimizing $\rho_\tau$ over the data drives $\hat{y}$ toward the conditional $\tau$-quantile, so fitting several $\tau$ values at once (say $0.1, 0.5, 0.9$) yields a predictive interval directly, which is why the pinball loss is the bread-and-butter objective of the probabilistic forecasting we develop fully in Chapter 19. We meet it again there as the bridge to conformal calibration; here it is enough to recognize it as the loss that turns a point forecaster into an interval forecaster.
Take the upper-tail quantile $\tau = 0.9$ and a true value $y = 100$. If the model under-predicts with $\hat{y} = 90$, then $u = y - \hat{y} = 10 \ge 0$, so $\rho_{0.9} = 0.9 \times 10 = 9.0$. If instead it over-predicts by the same magnitude, $\hat{y} = 110$, then $u = -10 < 0$, so $\rho_{0.9} = (0.9 - 1)\times(-10) = 0.1 \times 10 = 1.0$. The same ten-unit miss costs nine times as much when the model falls short as when it overshoots, which is exactly what you want from a $0.9$ quantile: it should sit high enough that the truth rarely exceeds it, so the optimizer is taught to fear under-prediction. Set $\tau = 0.5$ and both directions cost $0.5 \times 10 = 5.0$, recovering symmetric (half) absolute error and confirming that the median pinball loss is just scaled MAE.
The final loss-side topic is specific to autoregressive (one-to-many and unaligned many-to-many) models: how to train a model that, at inference, consumes its own previous outputs. The standard answer is teacher forcing: during training, feed the model the ground-truth previous token rather than its own prediction, so that step $t$ always conditions on the correct $\mathbf{y}_{t-1}$ from the data. This makes training stable and fully parallelizable over steps, because every step's input is known in advance. It also creates a discrepancy known as exposure bias: the model is only ever exposed to correct histories during training, but at inference it must consume its own imperfect generations, so a single early mistake leads it into a region of history it never saw in training, and errors compound. Figure 9.2.2 contrasts the two regimes.
Several remedies for exposure bias appear later in the book (see Chapter 14 and the table of contents) and are worth flagging now so the term is familiar: scheduled sampling mixes ground-truth and predicted inputs on a curriculum, and sequence-level objectives optimize the generated sequence as a whole rather than step by step. The non-autoregressive direct-output head of subsection two sidesteps exposure bias entirely by never feeding outputs back, which is one reason direct multi-horizon heads are popular in Chapter 14.
4. Conditioning and Covariates Intermediate
A sequence model rarely sees only the target series. Real forecasting problems come with side information of three distinct temporal kinds. Keeping them straight matters because each enters the model at a different point, and respecting when each is actually available is what prevents the leakage of Chapter 2. Static features do not vary with time: the store's region, the patient's baseline diagnosis, the product category. Known-future covariates are time-varying but known in advance for the entire forecast horizon: calendar effects (day of week, month), scheduled holidays, planned promotions, known price changes. Past covariates (also called observed covariates) are time-varying and known only up to the present: a related sensor reading, realized weather, an exogenous market index whose future values you do not have.
The availability distinction is not pedantic. A known-future covariate may legitimately inform the prediction at horizon step $h$, because you genuinely know next Tuesday is a holiday when you forecast it. A past covariate may not, because using its future value at training time would leak information the model will not have at deployment, manufacturing an optimistic backtest that collapses in production. Mechanically, static features are typically broadcast across all time steps and concatenated to every step's input, or used to initialize the recurrent state; known-future covariates are concatenated to the decoder inputs over the horizon; past covariates are concatenated only to the encoder inputs over the observed window. This three-way routing is the organizing principle of the Temporal Fusion Transformer we build in Chapter 14, which devotes separate processing paths to exactly these three covariate types and even learns per-feature importance weights, so the categories you are learning here are the literal inputs of that architecture.
The temptation is to throw every available column at the model and let it sort out what matters. For temporal data this is a trap, because the question is not only "is this feature predictive?" but "will its value be known at the moment of prediction?". A feature that is informative in the training table but unavailable at inference for the horizon you are predicting is a leakage hazard, not a help: the model learns to lean on a value it will never receive in deployment. The discipline of sorting every covariate into static, known-future, or past-only, and routing each to the part of the model where its information is legitimately available, is what separates a backtest that holds up in production from one that does not. This is the deployment-facing twin of the leakage discussion in Chapter 2, now expressed in the vocabulary of sequence models.
5. Encoder-Decoder and the Bottleneck of a Fixed Context Advanced
The unaligned many-to-many shape demands a structure that the other four do not: a way to read a whole input sequence of length $T$ and then write a whole output sequence of length $T' \ne T$ with no given alignment between them. The classical answer is the encoder-decoder. An encoder, typically a recurrence of the kind Section 7.6 derived, consumes $\mathbf{x}_{1:T}$ and compresses it into a single fixed-width context vector $\mathbf{c}$, usually its final hidden state. A decoder, another recurrence, is initialized from $\mathbf{c}$ and generates the output sequence one step at a time, autoregressively, until it emits an end-of-sequence marker or reaches the target length. Formally,
$$\mathbf{c} = \text{Encoder}(\mathbf{x}_{1:T}), \qquad \mathbf{y}_t = \text{Decoder}(\mathbf{y}_{t-1}, \mathbf{s}_{t-1}, \mathbf{c}), \quad t = 1, \dots, T',$$where $\mathbf{s}_{t-1}$ is the decoder's own carried state. This factorization is elegant and was the dominant sequence-to-sequence design for years, but it hides a structural flaw that becomes acute as $T$ grows. Every detail the decoder will ever need about the input must pass through the single fixed-width vector $\mathbf{c}$. A vector of, say, 512 numbers is asked to summarize an input that might be hundreds of steps long, and it cannot grow with $T$. The earliest inputs, in particular, must survive the entire encoder recurrence without being overwritten, the precise compression limit that Section 7.6 named when it contrasted a recurrence (which compresses into a bounded state) with attention (which keeps everything). This is the information bottleneck of the fixed context vector.
The bottleneck has a memorable empirical signature: encoder-decoder translation quality degrades sharply as input length increases past what the context vector can hold, while a model that lets the decoder look back at all encoder states does not degrade the same way. That observation, made by Bahdanau, Cho, and Bengio in 2014, is precisely what motivated attention: rather than force the decoder to rely on one summary vector, give it a mechanism to attend to all the encoder's per-step states and form a fresh, query-dependent context at every output step. The fixed $\mathbf{c}$ becomes a time-varying $\mathbf{c}_t$ computed by a weighted look back over the whole input. We develop attention in full in Chapter 12; for now the load-bearing idea is that the bottleneck of this subsection is the problem statement that the next-but-one chapter answers. The encoder-decoder is not wrong; it is the structure whose one weakness defined the agenda of modern sequence modeling.
The task taxonomy of this section is not a historical artifact; it is the schema that the 2024 to 2026 temporal foundation models are built to span. Moirai (Salesforce, 2024) is explicitly a many-input, multi-horizon forecaster with a unified head that handles arbitrary numbers of variates, and TimesFM (Google, 2024) is a decoder-only autoregressive forecaster, the one-to-many and many-to-one cells of this table scaled to a pretrained model. Chronos (Amazon, 2024) reframes forecasting as unaligned sequence-to-sequence by tokenizing the series and reusing a language-model decoder, a direct application of the seq2seq shape of subsection five, and MOMENT (2024) targets the aligned many-to-many cell for per-step tasks like anomaly detection and imputation. The selective state-space models Mamba and Mamba-2 (Gu and Dao, 2023 to 2024) and the linear-attention revival are, in the framing of Section 7.6, attempts to keep the recurrence's bounded state while recovering attention's freedom from the fixed-context bottleneck this subsection diagnosed. The practitioner's takeaway: the first decision when prompting or fine-tuning any 2026 temporal foundation model is still "which of the five shapes is my task?", because that determines which model family and which head you reach for.
6. Worked Example: A Padded, Masked seq2seq Pipeline in PyTorch Intermediate
The cleanest way to make the previous five subsections concrete is to build the data plumbing once and then hang two different heads, a many-to-one and an aligned many-to-many, on the same padded, masked batch. We will assemble a tiny variable-length dataset, write a collate function that pads and builds a mask from scratch, run a recurrent encoder, and show that the many-to-one head pools to one prediction while the many-to-many head reads out every step. Then we replace the hand-written masking with PyTorch's nn.utils.rnn pack and pad utilities, the library pair, and demonstrate a teacher-forcing decoder step. Throughout, watch the tensor shapes; they are the running proof that the abstractions of this section are real objects. Code 9.2.1 starts with the dataset and the from-scratch collate.
import torch
from torch.utils.data import Dataset, DataLoader
class RaggedSeqDataset(Dataset):
"""Variable-length univariate sequences with a per-sequence class label
(for many-to-one) and a per-step target (for aligned many-to-many)."""
def __init__(self, sequences, labels):
self.sequences = sequences # list of 1D float tensors, ragged
self.labels = labels # list of int class labels
def __len__(self):
return len(self.sequences)
def __getitem__(self, i):
x = self.sequences[i] # shape (T_i,) variable length
return x, self.labels[i]
def collate_pad(batch):
"""FROM SCRATCH: pad a ragged batch to T_max and build a boolean mask.
Returns padded inputs (B, T_max), a mask (B, T_max), labels (B,),
and the true lengths (B,)."""
seqs, labels = zip(*batch)
lengths = torch.tensor([len(s) for s in seqs]) # true length per seq
T_max = int(lengths.max())
B = len(seqs)
padded = torch.zeros(B, T_max) # pad value = 0.0
mask = torch.zeros(B, T_max, dtype=torch.bool) # False everywhere
for i, s in enumerate(seqs):
padded[i, :len(s)] = s # copy real values
mask[i, :len(s)] = True # mark real positions
return padded, mask, torch.tensor(labels), lengths
# three ragged sequences of lengths 5, 3, 4 (the numeric example above)
seqs = [torch.arange(1., 6.), torch.arange(1., 4.), torch.arange(1., 5.)]
ds = RaggedSeqDataset(seqs, labels=[0, 1, 0])
loader = DataLoader(ds, batch_size=3, collate_fn=collate_pad)
padded, mask, labels, lengths = next(iter(loader))
print("padded shape:", tuple(padded.shape), " mask true count:", int(mask.sum()))
print(padded)
print(mask)
collate_pad turns a ragged batch into a rectangular (B, T_max) tensor and builds the boolean mask by hand, the explicit version of the padding shown in the subsection-two numeric example.padded shape: (3, 5) mask true count: 12
tensor([[1., 2., 3., 4., 5.],
[1., 2., 3., 0., 0.],
[1., 2., 3., 4., 0.]])
tensor([[ True, True, True, True, True],
[ True, True, True, False, False],
[ True, True, True, True, False]])
False so the loss will ignore them.With the batch in hand, Code 9.2.2 runs a recurrent encoder and attaches the two heads. The many-to-one head must pool over time, and pooling correctly means averaging only over real positions using the mask, the masked mean that prevents the padding bug quantified earlier. The many-to-many head instead keeps a prediction at every step. The shapes printed at the end are the point: one head collapses the time axis, the other preserves it.
import torch.nn as nn
class TwoHeadEncoder(nn.Module):
def __init__(self, hidden=8, n_classes=2):
super().__init__()
self.rnn = nn.RNN(input_size=1, hidden_size=hidden, batch_first=True)
self.cls_head = nn.Linear(hidden, n_classes) # many-to-one: one label
self.step_head = nn.Linear(hidden, 1) # aligned m-to-m: per step
def forward(self, x, mask):
h_all, _ = self.rnn(x.unsqueeze(-1)) # (B, T, hidden)
# many-to-one: masked mean pool over real steps only
m = mask.unsqueeze(-1).float() # (B, T, 1)
pooled = (h_all * m).sum(1) / m.sum(1) # (B, hidden) masked mean
logits = self.cls_head(pooled) # (B, n_classes)
# aligned many-to-many: a prediction at every step
per_step = self.step_head(h_all).squeeze(-1) # (B, T)
return logits, per_step
model = TwoHeadEncoder()
logits, per_step = model(padded, mask)
print("many-to-one logits:", tuple(logits.shape)) # (B, n_classes)
print("many-to-many out: ", tuple(per_step.shape)) # (B, T)
# masked per-step regression loss: never count a padding position
target_steps = padded # toy target = the input
sq_err = (per_step - target_steps) ** 2
masked_loss = (sq_err * mask).sum() / mask.sum() # divide by 12, not 15
print("masked per-step MSE: %.4f" % masked_loss.item())
mask.sum() (twelve), so padding never dilutes it.many-to-one logits: (3, 2)
many-to-many out: (3, 5)
masked per-step MSE: 6.8123
Now the library pair. Our hand-written collate_pad plus the manual masked mean ran about a dozen lines and silently processed padding through the RNN before discarding it. PyTorch's nn.utils.rnn utilities do better: pad_sequence pads, and pack_padded_sequence compresses the padded batch into a packed form that the RNN steps over without ever computing on padding, which is both faster and removes any chance of padding contaminating the hidden states. Code 9.2.3 shows the same pipeline through these utilities and then performs one teacher-forcing decoder step, feeding the ground-truth previous value rather than the model's own output.
from torch.nn.utils.rnn import pad_sequence, pack_padded_sequence, pad_packed_sequence
# LIBRARY PAIR: pad_sequence replaces the hand-written padding loop.
padded_lib = pad_sequence(seqs, batch_first=True) # (B, T_max), zero-padded
lengths = torch.tensor([len(s) for s in seqs])
# pack so the RNN skips padding entirely (lengths must be on CPU)
packed = pack_padded_sequence(padded_lib.unsqueeze(-1), lengths,
batch_first=True, enforce_sorted=False)
enc = nn.RNN(input_size=1, hidden_size=8, batch_first=True)
packed_out, h_n = enc(packed) # RNN never sees padding
h_all, out_lengths = pad_packed_sequence(packed_out, batch_first=True)
print("unpacked hidden shape:", tuple(h_all.shape)) # (B, T_max, 8)
# --- one TEACHER-FORCING decoder step ---
dec = nn.RNNCell(input_size=1, hidden_size=8)
s = h_n.squeeze(0) # decoder init = encoder final state
y_prev_truth = padded_lib[:, 0:1] # GROUND TRUTH y_0, not a prediction
s = dec(y_prev_truth, s) # teacher-forced step
print("decoder state after one teacher-forced step:", tuple(s.shape))
pack_padded_sequence makes the RNN skip padding outright, and the decoder is fed the ground-truth previous value y_prev_truth, the training-time regime of Figure 9.2.2 whose mismatch with inference is exposure bias.unpacked hidden shape: (3, 5, 8)
decoder state after one teacher-forced step: (3, 8)
The from-scratch collate_pad plus the manual masked mean pool of Code 9.2.1 and 9.2.2 ran about a dozen lines and still paid to push padding through the RNN before masking it out. The torch.nn.utils.rnn trio (pad_sequence, pack_padded_sequence, pad_packed_sequence) collapses the padding-and-masking logic to three calls and, crucially, makes the recurrent layer skip padded steps entirely rather than compute and discard them, handling the variable-length bookkeeping, the per-sequence length tracking, and the contiguity reordering internally. The line count drops from roughly twelve to three, and the result is both faster and immune to the padding-contamination bug, which is why production sequence pipelines in Chapter 10 use packing rather than hand masking for recurrent models.
Who: A reliability-engineering team at a wind-farm operator instrumenting turbines with vibration, temperature, and rotor-speed sensors, the sensor and IoT series threaded through Chapter 8 and beyond.
Situation: Each turbine emits a multivariate stream, and maintenance logs record both whole-turbine failure events and the exact timestamps of incipient fault onsets within a stream.
Problem: The first modeling attempt framed everything as many-to-one, classifying each fixed window as "will fail soon" or "healthy". It missed slow-developing faults whose signature was a gradual per-step drift, because collapsing the window to one label discarded the timing the maintenance crew actually needed.
Dilemma: Three framings competed. Keep the many-to-one window classifier, which is simple but timing-blind. Switch to an aligned many-to-many tagger that labels each timestamp's fault state, recovering timing but needing per-step labels. Or an unaligned seq2seq that transduces a raw burst into a variable-length fault-code sequence for the work order.
Decision: They split the system by the taxonomy of this section. An aligned many-to-many tagger produced the per-step fault-onset signal the crew scheduled on, while a separate many-to-one regressor estimated remaining useful life for the dashboard, each with the head and loss Table 9.2.1 prescribes.
How: Both heads sat on one shared recurrent encoder over the same padded, masked batches built exactly as in Code 9.2.2, with rotor-speed and temperature as past covariates and scheduled-maintenance windows as known-future covariates routed per subsection four.
Result: The aligned tagger caught the slow-drift faults the window classifier had missed, and because the loss was masked per step, ragged sensor bursts of different lengths trained together without the padding diluting the fault-onset signal.
Lesson: The failure was a task-classification error, not a model-capacity error. Naming the shape first, aligned many-to-many for timing, many-to-one for a scalar, dictated the heads and losses and fixed the problem without a fancier cell.
For a tensor that holds nothing but ones and zeros, the mask does astonishing amounts of work and causes astonishing amounts of grief. Forget it in the loss and your metrics flatter you by the padding ratio. Forget it in an attention layer and the model peers at positions that do not exist. Forget it in a pooling layer and your "average" is an average over imaginary timesteps. Veteran sequence engineers develop a reflex: the moment a sequence becomes a rectangular tensor, the very next question is "where is the mask, and is everything that should consult it consulting it?". The mask is the bookkeeping that lets a ragged world fit into rectangular hardware, and respecting it is half of getting sequence models right.
Four mistakes recur when readers first set up a temporal learning task, and naming them now saves debugging later:
- Confusing the two many-to-many shapes. An aligned tagger ($T'=T$, given correspondence) and an unaligned transducer ($T' \ne T$, no correspondence) need different heads and architectures. Building one when the task is the other is the single most common task-classification error.
- Letting padding into the loss. Any per-step loss, pool, or attention that does not consult the mask quietly counts padding positions, flattering metrics by the padding ratio and corrupting gradients.
- Leaking a past covariate. Feeding a past-only covariate's future value at training time manufactures an optimistic backtest that collapses in deployment. Sort every covariate into static, known-future, or past-only first.
- Ignoring exposure bias. A model trained only with teacher forcing can look excellent on validation yet drift at free-running inference. If generations degrade over the horizon, suspect exposure bias before suspecting capacity.
For each of the following, name the shape from the five-cell taxonomy of subsection one and state the output head and loss it implies, justifying your choice against Table 9.2.1: (a) predicting tomorrow's electricity demand from the past 168 hourly readings; (b) flagging each minute of an ECG as normal or arrhythmic; (c) generating a 24-step synthetic load profile from a building's static metadata; (d) transcribing a variable-length keystroke-timing burst into a variable-length command-token sequence; (e) deciding whether a 30-second accelerometer clip is "walking", "running", or "still". For one of the two many-to-many cases, explain precisely why it is aligned or unaligned.
A batch contains sequences of lengths $8$, $2$, $6$, and $4$, padded to $T_{\max}$ with zeros, and the targets at padding positions are also zero. Suppose the model's per-step squared error at every real position is exactly $4.0$. Compute the correct masked mean-squared error, then compute the unmasked mean over the full $(B, T_{\max})$ tensor, and report the ratio. Generalize: show that the unmasked loss equals the masked loss times the fraction of real positions, and state the one condition under which the bug happens to vanish.
Extend Code 9.2.2 with a third head that performs direct multi-horizon forecasting: from the masked-pooled encoder state, emit a vector of $H = 3$ future values in one linear layer, and train it against a toy target with a masked-aware MSE. Then implement the autoregressive alternative using the nn.RNNCell decoder of Code 9.2.3, generating the same three steps one at a time with teacher forcing during training. Compare the two heads' output shapes and write two sentences on when each is preferable, referencing the autoregressive-versus-direct fork of subsection two.
You are forecasting daily store sales 14 days ahead. Available columns: store region and store size (fixed), day-of-week and a public-holiday calendar (known for the whole horizon), yesterday's local weather and a realized competitor-price index (known only up to today). Sort each column into static, known-future, or past-only, and design, on paper, how each enters an encoder-decoder: which features initialize the state, which feed the encoder over the observed window, and which feed the decoder over the horizon. Argue why misrouting the competitor-price index as known-future would leak, and connect your routing to the Temporal Fusion Transformer of Chapter 14.