"A per-sensor model asked whether each tag was surprising on its own. The graph asked a harder question: does this tag still agree with the neighbours it is wired to? Half my false alarms were tags that were merely lonely, not lying."
A Relationally-Minded AI Agent
Prerequisites
This section assumes the message-passing machinery of Chapter 54, where graph neural networks for sensor arrays are developed from first principles; here we specialize that toolkit to industrial-control anomaly detection. It continues directly from the residual detectors of Section 38.3: a GNN is one more way to produce an expected value, and the scoring, whitening, and thresholding of that section apply unchanged. The forecasting components reuse the temporal models of Chapter 14 and the attention of Chapter 15, and the plant-as-a-graph intuition rests on the process wiring of Chapter 35. Leakage discipline is assumed throughout (Chapter 65).
The Big Picture
A per-tag forecaster treats each sensor as an island: it asks whether a channel is temporally surprising given its own past. But a plant is not a bag of independent signals; it is a web of physically coupled quantities. The flow out of a pump constrains the level in the tank it fills, which constrains the pressure at the valve downstream. The stealthy attacks that defeat a per-tag detector are precisely those that keep every individual channel inside its normal range while quietly breaking the relationships between channels. A graph neural network turns that structure into the model. Each sensor becomes a node, learned edges encode which sensors normally explain each other, and message passing lets every node forecast itself from its neighbours. The residual then means something new and sharper: not "this value is odd for this tag" but "this value is inconsistent with the tags it is coupled to." That relational residual catches on-manifold tampering a univariate model waves through, and, because the model knows which neighbours it consulted, it hands the next stage a map for localizing where the anomaly began.
Why the graph, and where the edges come from
The premise is that anomalies in a cyber-physical process are relational. When an adversary spoofs a level transmitter to a plausible constant, the reading itself is unremarkable; what betrays the attack is that the level no longer moves the way the inflow and the pump command say it should. To detect that, the model must represent inter-sensor dependence explicitly. Stacking all channels into one vector and feeding a dense network can in principle learn these couplings, but it wastes capacity modelling every pair equally and offers no handle on which pair broke. A graph makes the dependence structure a first-class object: node \(i\) is sensor \(i\), and a directed edge \(j \to i\) says "sensor \(j\) helps predict sensor \(i\)."
Where do the edges come from? Three sources, in increasing order of how much a modern detector leans on them. You can hand-build the graph from the piping-and-instrumentation diagram, wiring tags that share a physical pipe or control loop; this is faithful but laborious and brittle to undocumented couplings. You can derive it from data by correlation or mutual information, thresholding a similarity matrix. Or, in the approach that now dominates, you can learn the graph jointly with the detector. Each sensor gets a trainable embedding vector \(\mathbf{v}_i \in \mathbb{R}^d\), and the affinity between two sensors is their embedding similarity; for every node you keep only its top-\(k\) most similar candidates as incoming edges,
\[ e_{ji} = \frac{\mathbf{v}_i^{\top}\mathbf{v}_j}{\lVert \mathbf{v}_i\rVert\,\lVert \mathbf{v}_j\rVert}, \qquad \mathcal{N}(i) = \operatorname{TopK}_{j\neq i}\, e_{ji}. \]This is the core idea of the Graph Deviation Network (GDN), and it means the model discovers the plant's dependency structure from healthy data instead of demanding a schematic. The learned graph is also a deliverable in its own right: an engineer can inspect the top edges and confirm that stage-three level really does depend on the stage-two flow, a sanity check no black-box detector offers.
Key Insight
The value of a GNN detector is not that it is a bigger network; it is that its residual answers a different, harder question. A univariate forecaster fires when a channel breaks its temporal pattern. A graph detector fires when a channel breaks its relational pattern, staying plausible on its own axis while disagreeing with the neighbours it is coupled to. That is exactly the failure mode of the stealthy, on-manifold attacks (a spoof pinned to a valid value, a slow setpoint nudge) that Section 38.3 flagged as the blind spot of per-tag residuals. The relational view does not replace the temporal one; the strongest detectors carry both, forecasting each node from the recent history of its neighbourhood so a residual can spike on either kind of departure.
Message passing, forecasting, and the deviation score
With a graph in hand, a detection step runs in three moves. First, embed: each node's recent window \(\mathbf{x}^{(i)}_{t-w:t-1}\) is encoded into a feature vector. Second, pass messages: every node aggregates its neighbours' features, weighted by learned attention, producing a representation that fuses a sensor with the tags it depends on. A single graph-attention layer computes, for node \(i\),
\[ \mathbf{h}_i = \operatorname{ReLU}\!\Big( \sum_{j\in\mathcal{N}(i)\cup\{i\}} \alpha_{ij}\, \mathbf{W}\,\mathbf{g}_j \Big), \qquad \sum_j \alpha_{ij}=1, \]where \(\mathbf{g}_j\) fuses sensor \(j\)'s window with its embedding \(\mathbf{v}_j\) and the attention weight \(\alpha_{ij}\) says how much node \(i\) trusts neighbour \(j\) right now. Third, predict: a small output head maps \(\mathbf{h}_i\) to the forecast \(\hat{x}^{(i)}_t\) for the next value of that sensor. Train the whole stack (embeddings, attention, head) to minimize forecasting error on a contiguous clean run, and the per-sensor deviation is the familiar residual \(r^{(i)}_t = \lvert x^{(i)}_t - \hat{x}^{(i)}_t\rvert\), normalized per channel by its healthy median and inter-quartile range so no single loud tag dominates. The plant-wide score at time \(t\) is the maximum normalized deviation across sensors, and the tag achieving that maximum is the model's first guess at the culprit. Thresholding that score reuses the calibration of Section 38.3 exactly: a high quantile on a held-out clean stream, smoothed to demand sustained rather than single-sample exceedance.
import torch, torch.nn.functional as F
class GraphAnomalyDetector(torch.nn.Module):
def __init__(self, n_sensors, window, emb_dim=64, topk=15):
super().__init__()
self.topk = topk
self.embed = torch.nn.Embedding(n_sensors, emb_dim) # learned node embeddings
self.enc = torch.nn.Linear(window, emb_dim) # per-sensor window encoder
self.attn = torch.nn.Linear(2 * emb_dim, 1) # graph-attention scorer
self.head = torch.nn.Linear(emb_dim, 1) # one-step forecast head
def learned_edges(self): # top-k cosine graph
v = F.normalize(self.embed.weight, dim=1)
sim = v @ v.t()
sim.fill_diagonal_(-1e9) # no self-loop in the ranking
return sim.topk(self.topk, dim=1).indices # [n_sensors, topk]
def forward(self, x): # x: [batch, n_sensors, window]
g = torch.relu(self.enc(x)) + self.embed.weight # fuse window with node identity
nbr = self.learned_edges() # who each node listens to
msg = g[:, nbr] # [batch, n_sensors, topk, emb]
pair = torch.cat([g.unsqueeze(2).expand_as(msg), msg], dim=-1)
a = torch.softmax(self.attn(pair).squeeze(-1), dim=-1) # attention over neighbours
h = torch.relu((a.unsqueeze(-1) * msg).sum(dim=2)) # aggregated neighbour message
return self.head(h).squeeze(-1) # x_hat: [batch, n_sensors]
learned_edges() exposes the discovered dependency structure for inspection.Listing 38.4 shows why the graph is not just an architectural flourish. The learned_edges method returns, for every sensor, the handful of tags it consults, and that structure is what makes the detector both sharper on relational attacks and readable afterward: when the deviation on a level transmitter spikes, the neighbours in its row tell you which upstream tags it should have agreed with and did not.
Right Tool: message passing without the index gymnastics
The neighbour-gather and attention bookkeeping in Listing 38.4 is the part that hides bugs (wrong broadcast dimension, a self-loop that leaks the answer, softmax over the wrong axis). PyTorch Geometric supplies a batched GATConv and edge-index plumbing that replace roughly 40 lines of hand-written gather-and-attention with two:
from torch_geometric.nn import GATConv
gat = GATConv(in_channels=emb_dim, out_channels=emb_dim, heads=4)
h = gat(node_feats, edge_index) # edge_index: [2, E] from the learned top-k graph
torch_geometric, replacing about 40 lines of manual neighbour indexing. The library owns the message-passing kernel; you still own the learned edge_index, the normal-only fit, and the leakage-safe chronological split, none of which a library can decide for you.What the graph buys, and what it costs
The gain is concentrated where per-tag detectors are weakest. On a spoof pinned to a plausible value, the target channel forecasts fine on its own history but wildly given its neighbours, so its normalized deviation spikes even though a univariate model stays flat. Because the score localizes to the offending node, the graph detector feeds root-cause analysis (Section 38.5) directly, and its learned edges align with interpretability tooling for time series (Chapter 67). The costs are real and worth naming. A learned graph can overfit spurious correlations in a short clean run, wiring sensors that co-move by coincidence; a top-\(k\) that is too large dilutes attention, too small starves nodes of context. And the graph is still learned only from normal data, so a coordinated multi-point attack that moves several coupled sensors together in a way the graph considers normal can slip through, the relational cousin of the replay blind spot. The honest evaluation discipline from Section 38.3 carries over without change: report event-level detection at a fixed clean false-alarm budget and median detection delay on a chronological split, never point-adjusted F1 in isolation, and never tune the graph or threshold on the attack set.
In Practice: catching a stealthy tamper on the WADI water testbed
The Water Distribution (WADI) testbed extends the SWaT plant with a larger, more loosely coupled network of about 120 sensors and actuators, which is where per-tag forecasters start to drown in false alarms. A team trains a GDN-style detector on fourteen days of the logged normal operation, learning a top-15 graph from the sensor embeddings and holding out the final clean day to set a 99.5th-percentile threshold. During the attack campaign, an adversary opens a motorized valve just enough to divert flow while spoofing the downstream flow transmitter to its expected value, so every individual channel reads normal and no limit alarm trips. The univariate baseline stays silent. The graph detector does not: the tank-level node upstream had learned to depend on that flow transmitter, and with the real flow diverted the level falls faster than the spoofed flow predicts, so the level node's normalized deviation crosses threshold within a minute. When the operators inspect the alarm, the node's learned neighbourhood points straight at the tampered valve and its flow tag, turning a bare detection into a localized lead. On a separate replay-style attack that moves a coupled cluster in a jointly plausible way, the graph deviation stays muted, and the team logs it as a known relational blind spot to be covered by the physical invariants of Section 38.1 rather than by lowering the threshold.
Research Frontier
The Graph Deviation Network (Deng and Hooi, AAAI 2021) established the learn-the-graph recipe, and MTAD-GAT paired a feature-relation graph with a temporal-relation graph so a node attends across both sensors and time. More recent lines fuse the two views (GTA learns the graph and runs a temporal transformer over it; FuSAGNet couples a sparse learned graph with a reconstruction objective), and hypergraph and dynamic-graph variants let the dependency structure change with the operating regime rather than staying fixed. The sobering counter-current from Section 38.3 applies here in full force: once the point-adjust metric is dropped and splits are made strictly chronological, several graph detectors barely edge out a well-tuned forecasting-residual baseline, and some reported gains trace to evaluation artefacts rather than the graph. The live frontiers are graphs whose residuals resist coordinated multi-sensor manipulation, dynamic graphs that track regime changes without relearning spurious edges, and evaluation honest enough that a relational gain on paper survives a control room; the adversarial and transductive-leakage traps that break naive graph benchmarks are the subject of Section 38.6.
Exercise
Take a multivariate ICS stream with a labeled stealthy attack (a spoof pinned to a plausible value). (1) Train the GDN-style detector of Listing 38.4 on a contiguous clean prefix only, and separately train a per-tag univariate forecaster from Section 38.3. (2) On the attack window, plot both detectors' normalized scores and confirm that the graph detector spikes while the univariate one stays flat; explain in terms of relational versus temporal residuals. (3) Print the learned neighbourhood of the spoofed sensor and check whether the tags it points to are the ones physically coupled to it. (4) Sweep the top-\(k\) from 5 to 40 and report how event-level detection and clean false-alarm rate change, then argue for an operating point.
Self-Check
1. What kind of attack does a graph deviation score catch that a per-tag forecasting residual misses, and why does the relational residual spike when the univariate one does not?
2. In a learned-graph detector, what do the per-sensor embeddings and the top-\(k\) selection actually produce, and why is that structure useful even after a detection fires?
3. Name one attack a normal-only graph detector remains blind to, and explain why it is the relational analogue of the replay blind spot from Section 38.3.
What's Next
In Section 38.5, we turn the detector's finger-pointing into a discipline. A graph model already nominates the node with the largest deviation and the neighbours it disagreed with, but a single tamper propagates through the plant and lights up a cluster of downstream tags, so the loudest alarm is not always the origin. Root-cause localization walks the learned dependency structure backward, separates the primary fault from its physical consequences, and hands the operator not just "something is wrong" but "it started here."