"They gave me a bank of tiny filters and told me to find whatever repeats. Turns out a footstep repeats. So does a failing bearing. So does an atrial flutter."
A Pattern-Matching AI Agent
Why this section matters
A sensor window is a long, redundant array of numbers where the useful information lives in local, repeating shapes: the double bounce of a heel strike, the ring-down after a gearbox tooth meshes, the QRS complex of a heartbeat. The 1D convolution is the neural primitive that hunts for such shapes efficiently, sharing one small filter across every time step so the same detector fires wherever the pattern occurs. Temporal pooling is its partner: it collapses a long stream of detector responses into a compact, position-tolerant summary a classifier can use. Together they are the workhorse of sensor deep learning, and almost every architecture later in this part (temporal convolutional networks, the stem of a transformer, the encoder of a self-supervised model) begins with exactly this pair. This section is where you learn to read, size, and reason about them.
This section assumes you can already picture a sensor window as a tensor of shape (channels, time), which Section 13.1 laid out. It leans on the convolution and filterbank intuition from classical signal processing in Chapter 6, and it treats sampling rate and the causal constraint as given from Chapter 3. We keep the book's notation: a window is \(x \in \mathbb{R}^{C \times T}\) with \(C\) channels and \(T\) time steps, and a layer produces feature maps \(z \in \mathbb{R}^{C' \times T'}\).
The 1D convolution as a learned, sliding matched filter
A 1D convolution slides a small weight kernel along the time axis and, at each position, takes an inner product between the kernel and the window of input beneath it. For an input with \(C\) channels, a single output channel \(o\) computes
$$z_o[t] = b_o + \sum_{c=1}^{C} \sum_{k=0}^{K-1} w_{o,c}[k]\, x_c[t + k],$$where \(K\) is the kernel size (its length in samples), \(w_{o,c}\) are the learnable weights, and \(b_o\) is a bias. The what is a bank of \(C'\) such kernels, each learning to respond to a different local shape. The why is three properties the sensor setting rewards heavily. First, weight sharing: the same kernel is applied at every \(t\), so a heel-strike detector trained on one stride generalizes to every stride in the recording, and the parameter count depends on \(K\), not on \(T\). Second, locality: each output looks only at a short span, matching the fact that discriminative sensor events are short relative to the window. Third, translation equivariance: shift the input in time and the output shifts identically, so the network does not have to relearn a pattern for every possible arrival time. This is precisely the matched-filter idea from Chapter 6, except the filter coefficients are learned from labels rather than designed by hand (the tradeoff between the two is the subject of Section 13.4).
Sensors convolve across time, not space
The 2D convolutions that dominate vision slide a kernel across a spatial grid where up and left are interchangeable axes. A sensor stream has one privileged axis, time, and it runs one way. So the natural primitive is Conv1d: the kernel slides along time while spanning all input channels at once. A depth of accelerometer axes or EEG electrodes is not a spatial dimension to convolve over; it is a channel stack the first layer mixes in full, exactly as color channels are mixed at the input of a 2D network.
Sizing the layer: kernels, stride, padding, and receptive field
Three hyperparameters set the geometry, and getting them right is most of the practical skill. Kernel size \(K\) is a duration once you multiply by the sample period: at 50 Hz a \(K=15\) kernel spans 0.3 seconds, roughly one footstep, which is why you size kernels in milliseconds of physics rather than in abstract taps. Stride \(S\) is how many samples the kernel hops between evaluations; \(S=1\) keeps full temporal resolution, while \(S=2\) halves the output length and cheaply downsamples. Padding \(P\) adds zeros at the ends so the output length is controllable; the output length is
$$T' = \left\lfloor \frac{T + 2P - K}{S} \right\rfloor + 1.$$The quantity that ultimately decides what the network can see is the receptive field: the span of input samples that influence one output value. Stacking \(L\) convolutions of kernel \(K\) and stride 1 grows the receptive field to \(1 + L(K-1)\), which is linear in depth and often too slow to reach the seconds-long context a sensor task needs. That limitation is exactly what motivates dilation and multiresolution stacks, taken up in Section 13.3, and the long-context temporal convolutional networks of Chapter 14. One more choice matters for deployment: a causal convolution pads only on the left so \(z[t]\) never reads \(x[t{+}1]\), preserving the online constraint that a streaming model must obey.
Temporal pooling: from a stream of responses to a decision
A convolution turns one stream into many streams of detector responses, but a classifier usually needs a fixed-size vector, and it should not care where in the window a gesture happened. Temporal pooling supplies both. Max pooling over a length-\(m\) region reports the strongest response, answering "did this pattern appear at all?" and granting tolerance to small time shifts. Average pooling reports the mean, preserving how sustained a pattern was, which suits energy-like features. The most useful variant for windowed sensor classification is global pooling, which collapses the entire time axis to a single value per channel, so a variable-length or long window becomes a fixed \(C'\)-vector regardless of \(T\). Global max asks whether a transient ever fired; global average asks how prevalent it was. This is the neural echo of the summary statistics you computed by hand in Chapter 8, now learned end to end. When to use which: max for sparse, transient events (a fall, an arrhythmic beat); average for diffuse, ongoing states (walking versus standing); and strided convolutions when you want the network, not a fixed rule, to decide how to downsample.
A wrist-worn activity recognizer that fits on a coin cell
A wearables team building human activity recognition (see Chapter 26) feeds 2.56-second windows of 3-axis accelerometer data at 50 Hz, a tensor of shape (3, 128). The first Conv1d uses \(K=15\) (0.3 s, about one step) with 32 filters and stride 2, so the model learns 32 short gait-shape detectors and halves the length to 64 in one move. Two more conv-plus-pool blocks widen the receptive field to just over a second, enough to span a full stride cycle. A global average pool then crushes the time axis to a 128-vector fed to a small linear head. The whole network is well under 50k parameters, small enough to run on the watch's microcontroller (the quantization that gets it there is Chapter 59), and it beats the team's hand-crafted feature baseline because the filters adapt to how their users actually walk.
The code below builds exactly that stem in PyTorch and confirms how the tensor shape marches from (3, 128) down to a single vector per window. The shapes it prints are the receptive-field and pooling arithmetic above, made concrete.
import torch, torch.nn as nn
class HARStem(nn.Module):
def __init__(self, in_ch=3, width=32, n_classes=6):
super().__init__()
self.features = nn.Sequential(
nn.Conv1d(in_ch, width, kernel_size=15, stride=2, padding=7), # 0.3 s kernels
nn.BatchNorm1d(width), nn.ReLU(),
nn.Conv1d(width, width, kernel_size=9, stride=2, padding=4),
nn.BatchNorm1d(width), nn.ReLU(),
nn.Conv1d(width, 2 * width, kernel_size=5, padding=2),
nn.BatchNorm1d(2 * width), nn.ReLU(),
)
self.pool = nn.AdaptiveAvgPool1d(1) # global temporal pooling
self.head = nn.Linear(2 * width, n_classes)
def forward(self, x): # x: (batch, 3, 128)
z = self.features(x) # -> (batch, 64, 32)
z = self.pool(z).squeeze(-1) # -> (batch, 64)
return self.head(z) # -> (batch, 6)
x = torch.randn(8, 3, 128)
print(HARStem()(x).shape) # torch.Size([8, 6])
Conv1d layers downsample time while widening the receptive field, and AdaptiveAvgPool1d(1) performs global temporal pooling so any window length collapses to one vector per channel before the linear head.Global pooling in one line
Writing your own global temporal pool means reshaping, masking any padding, reducing over the time axis, and handling variable lengths, which is easily 15 to 20 lines of index-juggling that quietly breaks on the edge cases. PyTorch's nn.AdaptiveAvgPool1d(1) (or AdaptiveMaxPool1d(1)) does it in one line for any input length, and the library handles the reduction, the output-size bookkeeping, and the autograd path. Swapping average for max is a one-word change, which makes it trivial to A/B the two pooling semantics on your task.
What the filters learn, and how to keep it honest
Because the first-layer kernels operate directly on the raw signal, they are interpretable: trained on periodic motion they tend to converge to band-limited, oscillatory shapes that behave like a learned filterbank, a phenomenon we return to in Section 13.7 on inspecting learned filters. That interpretability is a gift and a trap. A convolution shares weights across time but not across channels or amplitude, so if one accelerometer axis is scaled differently or a channel drifts, the learned filters bake in that idiosyncrasy. This is why normalization and augmentation (Section 13.5) are not optional garnish but part of making convolutional features transfer across units. It is also why evaluation must respect the arrow of time: pooling and striding do not excuse you from subject-wise, chronological splits, or the leakage-safe discipline of Chapter 5 will hand you an accuracy the field never reproduces.
Exercise: size the receptive field to the physics
You are classifying gait from a shin-mounted gyroscope sampled at 100 Hz, and domain knowledge says a full stride cycle lasts about 1.1 seconds. (1) Using only stride-1 convolutions with kernel \(K=7\), how many stacked layers do you need before one output can see a whole stride? (2) Now allow a stride-2 downsample after every conv. Recompute the depth needed to cover 1.1 s, and state the output length starting from a 256-sample window. (3) Which design would you ship to a battery-powered device, and why? Justify each step with the receptive-field and output-length formulas from this section.
Self-check
- Weight sharing gives translation equivariance. Explain why that property is worth more for a footstep detector than for a fixed-position feature, and name the pooling operation that converts equivariance into the translation invariance a window classifier wants.
- A colleague sets kernel size \(K=3\) at 200 Hz and cannot understand why the network misses a 40 ms transient. What does \(K=3\) span in milliseconds here, and what two knobs would you change to let a single output see the whole transient?
- Why does a causal convolution pad only on the left, and what breaks in a deployed streaming model if you use symmetric padding instead?
What's Next
In Section 13.3, we confront the receptive-field bottleneck head on: stacking plain convolutions to reach seconds of context is slow and parameter-hungry, so we introduce dilated convolutions and multiresolution stacks that grow the receptive field exponentially with depth, letting a compact network see both a single heartbeat and the rhythm it belongs to at once.