"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 most later architectures in this part, from temporal convolutional networks to transformer stems, begin 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 the matched-filter idea from Chapter 6 with the pencil handed to gradient descent: the same sliding inner product, except the template is learned from labeled examples 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\) skips every other position outright, cheaper than convolving every step and pooling, but risking aliasing if a transient falls inside a skipped gap. Padding \(P\) adds \(P\) zero-valued samples to each end of the input before the kernel slides over it; without it, a valid convolution shrinks the output by \(K-1\) samples per layer, eroding the window's edges as layers stack. Padding is therefore the choice of whether a layer shrinks the sequence ("valid", \(P=0\)), preserves its length ("same", \(P=(K-1)/2\) for odd \(K\)), or grows it; same-padding is the default in sensor stems because it keeps a later prediction's time index aligned with the timestamp that produced it. In general, 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.
Zero-padding is not free
It is tempting to treat padding as bookkeeping that keeps the output length convenient, but the kernel cannot tell a manufactured zero from a genuine sensor reading of zero. Near the edges of every window, a wide kernel therefore convolves against an artificial flat region that never occurred in the physical signal, which can produce a spurious edge response or suppress a real event that straddles the boundary. The effect compounds with depth, since each layer's padded edge feeds directly into the next layer's receptive field. The practical fix is to leave real margin around the region of interest, or to use reflect or replicate padding instead of zeros where the library supports it.
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\). 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, learning 32 short gait-shape detectors while halving the length to 64. Two more conv blocks widen the receptive field past 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 for 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, trivial to A/B 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; skip the leakage-safe discipline of Chapter 5 and it will hand you an accuracy the field never reproduces.
Research frontier: constraining what the first layer learns
A fully free first-layer kernel can converge to almost any shape, powerful but unpredictable and costly to store. One active research direction constrains each kernel to a small parametric family, typically a band-pass filter defined by only a center frequency and a bandwidth, so training tunes classical filter parameters instead of raw taps. This shrinks the first layer to a few dozen numbers and keeps every filter individually auditable as a named frequency band, which has shown promise on speech and EEG streams where domain experts want to inspect what the network is listening to. The open question is how well this transfers to transient events, like the footsteps this section builds around, where no single frequency band captures the shape.
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.