"I was trained on a metronome and deployed at a jazz club. Nobody told the samples to arrive on the beat."
A Rhythmically Challenged AI Agent
The clock is a modeling assumption, and sensors break it
Every architecture so far in this chapter, the LSTM of Section 14.1, the temporal convolution of Section 14.2, and the dilation stacks of Section 14.3, silently assumes that the step from index \(t\) to \(t+1\) is a fixed slice of wall-clock time. Real sensor streams violate this constantly: a duty-cycled accelerometer sleeps to save battery, a BLE packet is dropped, an ICU nurse charts a lab value only when it is ordered, and three sensors on the same bus each run their own free clock. When the gap between samples carries information, treating the index as time throws that information away and, worse, forces you to interpolate data you never measured. The wall-clock gap between samples is not an implementation detail to normalize away, it is a measurement in its own right, and this section is about treating it as one.
This section assumes you understand sampling, jitter, and clock synchronization from Chapter 3, and that you can build the recurrent and convolutional encoders from the earlier sections of this chapter. It also leans on the leakage-safe splitting discipline of Chapter 5, because irregular data offers new ways to leak the future into the past.
Where irregularity comes from, and why you cannot ignore it
Irregular sampling is not one problem; it is a family. Event-driven sensors emit a reading only when something crosses a threshold, the extreme case being the event cameras of Chapter 46. Power-managed sensors duty-cycle, sleeping for a variable interval so the gap itself encodes a scheduling decision. Lossy transport, typical of Bluetooth Low Energy wearables, drops packets under radio contention, leaving ragged holes in an otherwise uniform stream. Asynchronous multi-rate fusion combines a 100 Hz IMU, a 1 Hz GPS fix, and an event-triggered barometer, each on its own clock, with no single grid onto which all channels naturally fall. And human-triggered records, most clinical data, appear only when a measurement was clinically warranted, so the sampling pattern is itself a signal about patient state.
The tempting fix, resample everything to a uniform grid and pretend, is dangerous for two reasons. First, upsampling by interpolation fabricates measurements: a linear fill between two blood-pressure readings twelve hours apart invents a smooth trend the patient may never have had, and a forward-fill can carry a stale value across a state change. Second, the interpolation kernel can straddle the split boundary and leak future information backward, exactly the trap flagged in the forecasting discussion of Section 14.4. The gap length is often the most predictive feature you own, and resampling deletes it.
Missingness is a measurement, not a hole to be filled
In human-triggered and event-driven streams, the decision to sample is correlated with the latent state: a lab drawn every hour signals a sicker patient than one drawn daily. So the informative representation is not just the imputed value; it is the triple of value, elapsed time since the last observation of that channel, and a binary mask marking whether this timestep is real or a placeholder. Feed all three. Models that see the mask and \(\Delta t\) routinely beat models handed a cleanly interpolated series, because imputation destroyed the signal the gap pattern carried.
Three ways to make a model time-aware
The cheapest intervention is time as a feature: append the elapsed interval \(\Delta t_i = s_i - s_{i-1}\) (where \(s_i\) is the timestamp of sample \(i\)) as an extra input channel, and often the observation mask alongside it. A vanilla GRU or TCN can then learn to modulate its update on the gap. This is a one-line change and a strong baseline, though the model must discover the right functional form of the gap on its own.
Appending \(\Delta t\) is not the same as being time-aware
It is easy to assume that once elapsed time sits in the input tensor, the model has "solved" irregular sampling, the same way adding a feature column solves an ordinary tabular problem. It has not. A plain GRU or TCN still applies the same weights at every step regardless of how large the gap is; it must learn, from data alone, that a 30-second gap should be treated differently from a 30-millisecond one, and with limited data or a noisy gap distribution it often does not learn this reliably. \(\Delta t\) as a feature is a good, cheap baseline, but the decay and continuous-time approaches below make the gap-dependence structural rather than something the optimizer has to discover on its own.
The principled recurrent answer is a time-decayed state. The GRU-D and T-LSTM families make the hidden state forget as a function of elapsed time: between two observations the memory decays toward a learned baseline, fast for channels that go stale quickly (heart rate) and slow for those that persist (a chronic diagnosis). A common decay is
$$\gamma_i = \exp\!\big(-\max(0,\; w\,\Delta t_i + b)\big), \qquad h_i^{-} = \gamma_i \odot h_{i-1},$$with the decayed state \(h_i^{-}\) fed into the usual gated update. When \(\Delta t_i\) is small, \(\gamma_i \approx 1\) and nothing changes; when the gap is long, the state relaxes toward its prior, which is exactly the behavior a Kalman prediction step exhibits between measurements in Chapter 9.
The most expressive answer models the hidden state as a continuous-time trajectory. ODE-RNNs and latent-ODE models evolve \(h(t)\) by a learned ordinary differential equation \(\mathrm{d}h/\mathrm{d}t = f_\theta(h)\) between observations, then jump the state at each arrival with a standard recurrent update. Because the dynamics are defined for every real \(t\), the model is genuinely grid-free: you can query it at any timestamp, interpolate and extrapolate within one framework, and handle wildly uneven spacing without a single imputed value. This continuous-time viewpoint connects directly to the linear-time state-space models of Chapter 16, several of which are discretizations of exactly this kind of continuous system.
import torch, torch.nn as nn
class GRUDecayCell(nn.Module):
"""Minimal GRU-D-style cell: hidden state decays with the elapsed gap."""
def __init__(self, in_dim, hid):
super().__init__()
self.gru = nn.GRUCell(in_dim, hid)
self.w = nn.Parameter(torch.zeros(hid)) # per-unit decay rate
self.b = nn.Parameter(torch.zeros(hid))
def forward(self, x_t, dt_t, h):
# dt_t: (batch, 1) elapsed time since previous sample, in seconds
gamma = torch.exp(-torch.relu(self.w * dt_t + self.b)) # (batch, hid)
h_decayed = gamma * h # forget by gap
return self.gru(x_t, h_decayed)
cell = GRUDecayCell(in_dim=4, hid=16)
h = torch.zeros(2, 16)
# two irregular samples: a 0.1 s gap, then a 30 s gap
for x, dt in [(torch.randn(2, 4), torch.full((2, 1), 0.1)),
(torch.randn(2, 4), torch.full((2, 1), 30.0))]:
h = cell(x, dt, h)
print(h.shape) # torch.Size([2, 16])
gamma = exp(-relu(w*dt + b)): it shrinks the retained state as the gap grows, per hidden unit, so channels that go stale fast decay fast. Passing the raw index instead of dt_t would recover the ordinary GRU and discard all timing information.The code above shows the decay applied to two samples spaced 0.1 s and 30 s apart; the second update starts from a heavily faded memory, which is the whole point.
Continuous-time RNNs without hand-writing the solver
Implementing an ODE-RNN from scratch means writing an adaptive-step solver and its adjoint for backpropagation, easily 200-plus lines of careful numerics. The torchdiffeq library reduces the state evolution to one call, odeint(f, h0, t), which integrates the learned dynamics between observation timestamps and returns gradients through the solver automatically. You supply the ~15-line dynamics module \(f_\theta\) and the arrival times; the library owns step-size control, stiffness handling, and the adjoint. A numerics project becomes a modeling choice.
The duty-cycled fall detector that woke up confused
A wearable team built a fall detector on a 50 Hz accelerometer and, to stretch battery life, added an aggressive sleep mode that dropped the rate to 5 Hz after ten seconds of stillness. Their TCN, trained on continuous 50 Hz windows, treated post-sleep samples as if they were 20 ms apart when they were 200 ms apart, so a genuine stumble looked like an implausibly violent spike and tripped false alarms every morning as users got out of bed. The fix was not a bigger model: they fed \(\Delta t\) per sample and switched the recurrent core to a time-decayed GRU, so the network learned that a large gap means "I have been asleep" rather than "the world just accelerated." False positives fell by more than half with no firmware change, and the design generalized to the human-activity pipelines of Chapter 26.
Asynchronous multi-sensor streams, and the leakage traps
When several channels each fire on their own clock, you have two structural choices. Union-of-timestamps builds one merged sequence over every arrival time across all sensors; at each row, exactly one channel (or a few) is real and the rest are masked, and per-channel \(\Delta t\) tells the model how stale each masked entry is. This preserves the true event order and is the natural input for a decay or ODE model. Set-based encoders drop the grid entirely: the record becomes an unordered set of \((t, \text{channel}, \text{value})\) tuples fed to a permutation-invariant model. This works because self-attention scores every pair of tokens by content and position directly, so nothing forces the input onto a shared clock. Each token's position is a continuous encoding of its raw timestamp, typically a sinusoidal or learned embedding in the style of Time2Vec, rather than an integer index, so a 50 ms gap and a 5 s gap remain distinguishable to the attention weights even though both pairs sit next to each other in the tuple list. This is the door into the attention-based sensor models of Chapter 15, and it earns its cost when channels are numerous and sparse enough that no shared grid is natural, dozens of independently ordered clinical labs, for instance, rather than the two- or three-sensor case where a union-of-timestamps table suffices.
Two evaluation traps deserve a warning label. First, any imputation statistic, a channel mean, a decay baseline, a normalization scale, must be estimated on the training split only, or the test set's future bleeds into training exactly as it would for the continuous-health monitors of Chapter 33. Second, and subtler: if sampling times themselves correlate with the label (a lab ordered because the clinician already suspected the outcome), the timestamp pattern can be a shortcut feature absent at prediction time. Audit whether \(\Delta t\) is a legitimate predictor or a leak of the decision you are trying to anticipate.
Foundation models that never resample
Most time-series foundation models still assume a regular grid internally and ask you to resample before tokenizing. An active line of work builds tokenizers that consume raw \((t, \text{channel}, \text{value})\) triples directly, so a pretrained backbone can ingest a duty-cycled wearable stream, a multi-rate ICU record, and an event-triggered log without a shared preprocessing pipeline. The open question is statistical more than architectural: whether one pretrained model can learn a sampling-invariant notion of "how much has changed since the last observation" across domains with wildly different missingness mechanisms, a live thread in the foundation models of Chapter 19.
Exercise: prove the gap carries signal
Take any duty-cycled or packet-dropped sensor stream (or simulate one by randomly deleting 40% of a uniform series). Train three models on a downstream label: (a) linear-interpolated to a uniform grid with no gap feature, (b) the same grid plus a per-timestep observation mask and \(\Delta t\) channel, and (c) a GRU-D-style decay cell like the one above. Report the metric gap between (a) and (b), then between (b) and (c). Then deliberately shuffle the \(\Delta t\) values across timesteps and retrain (c); if performance collapses, you have empirically shown the timing pattern, not just the values, drove the model.
Self-check
- Why can forward-filling a slowly-sampled clinical variable actively hurt a model rather than merely add noise?
- In the decay \(\gamma_i = \exp(-\max(0, w\,\Delta t_i + b))\), what does the network's behavior reduce to as \(\Delta t_i \to 0\), and why is that the desired limit?
- Give one scenario where the sampling timestamps are a leakage shortcut rather than a legitimate feature.
What's Next
In Section 14.7, we assemble everything from this chapter, the recurrent and convolutional cores, the streaming state, and the irregular-time inputs, into practical training recipes: how to window, normalize, batch ragged sequences, schedule learning rates, and regularize sensor models so they actually train stably and survive contact with a live deployment.