时空图神经网络实战:交通预测中的图建模与模型部署
发布时间:2026/9/19 2:13:28来源:尧图网络
简介一份面向智能交通与时空数据建模的学习资料系统讲解城市大脑如何结合时空图神经网络对交通流量、事故和天气数据进行实时监测与未来趋势预测并探讨如何通过预测干预避免拥堵和事故适合从事深度学习、机器学习、数据建模或智慧城市相关工作的读者参考。资源为PDF格式包内共1个文件压缩包约1.48MB内容精炼、便于离线阅读目前已有292人学习下载。文档以达摩院城市大脑实践为背景按“数据接入—数据挖掘—预测干预—动态调整”的完整链路展开覆盖交通流预测模型、时空图神经网络原理、低延时高并发技术及开源平台设计等关键点为理解实时交通系统提供了清晰的技术脉络。读者可借此建立从大规模数据汇聚、分析挖掘到智能决策的全局认知快速把握交通预测领域的前沿应用思路与工程落地要点。1. 为什么交通预测要走向时空图神经网络城市道路上的拥堵不是凭空出现的。一个路口在早高峰突然排起长队往往不是这个路口本身出问题而是上游几个路口的流量和速度在十分钟前就变了。达摩院城市大脑团队在演讲中把这种关系比喻成水波一个波纹推动另一个波纹。理解这条传播链传统时间序列模型很难做到因为路口之间的依赖是图结构、非欧几里得的。时空图神经网络STGNN把路网建模成图把交通流量当作图上随时间变化的信号用图卷积与序列模型一起刻画“空间传播时间演化”。这份PDF把这个思路放进城市大脑完整链路从数据接入到数据挖掘再到预测干预给做交通预测或时空建模的工程师提供了一个可拆解的样本。一个反直觉的结论是要预测某个路口真正值得分析的不是该路口的历史而是它周围路网的联合状态。2. 从路口到图节点交通预测的数据建模与邻接矩阵构造2.1 为什么路网必须用图而不是网格先把PDF里的城市大脑场景拆成建模问题。城市大脑实时监测每个路口的车辆流量和速度这些数据天然就落在路口节点上节点之间由道路连接。如果像处理图片那样把路网切成网格会切断路口的连通顺序如果只按距离把路口聚在一起又会忽略道路方向。图结构不一样节点就是路口边就是道路或相关性邻接矩阵决定了一个节点的状态如何被邻居影响。这个选择不是建模偏好而是交通传播规律的直接映射流量和速度从上游节点往下游节点传递图卷积正好能把这个传播过程参数化。2.2 节点特征、边特征和时间片组织构建STGNN输入时每个时间片对应一个图图上每个节点有一组特征。结合PDF中提到的流量、事故、天气数据我一般会这样组织特征类型具体字段说明节点流量特征车流量、平均速度、占有率直接来自感应线圈、微波或摄像头计算节点事件特征是否事故、事件等级、是否管控由计算机视觉识别和交警上报融合得到外部特征天气、温度、是否节假日、是否大型活动用于修正预测分布尤其是活动带来的波动边的空间特征道路长度、车道数、方向、通行能力可选用来给图的边加权节点特征要做归一化和时间对齐。比如流量和速度量纲不同速度要是0到120km/h车流量可能是0到2000辆/小时直接拼接会让梯度被大数值特征主导。常见的做法是分别做Z-score归一化保存均值和方差在推理时用同一套统计量。时间对齐则要控制在同一个时间片内比如每5分钟一条记录所有路口数据都落到整5分钟的时间戳上缺失的片段先做线性插值再做滑窗切片。2.3 邻接矩阵的三种常见构造方式PDF中强调“路口之间受到周围交通流影响”这个影响如何变成矩阵有讲究。最简单的是把路网拓扑转成0-1邻接矩阵两个路口有路段直接相连就为1否则为0。但这种方式对距离不敏感而且路网不完整时矩阵会很稀疏。另一种是用高斯核根据路口距离构造加权矩阵A_ij exp(-d_ij^2 / sigma^2)这样近距离节点在聚合时权重更高。最后一种是基于数据计算相关矩阵用历史流量序列算Pearson相关系数把相关系数大于阈值的路口连接起来适合拓扑未知但流量模式相似的场景。实际项目中我会把拓扑矩阵和数据相关矩阵相加再归一化避免单一来源失真。下面是构造高斯核邻接矩阵的代码import numpy as np from scipy.spatial.distance import cdist def gaussian_adjacency(coords, sigma0.1, threshold0.01): # coords: N x 2每个路口的经纬度或投影坐标 dist cdist(coords, coords, metriceuclidean) adj np.exp(-dist**2 / (2 * sigma**2)) # 去掉自连接并剪掉过小的权重保持邻接矩阵稀疏 np.fill_diagonal(adj, 0) adj[adj threshold] 0 return adj这里的sigma是空间衰减系数越大表示影响范围越广。我一般会把sigma设置为路口平均间距的0.5到1倍再观察验证集误差。threshold设为0.01是为了在训练时避免让每个节点都成为全图的全局邻居否则图卷积退化成全局平均池化。构造完之后最好做一次行归一化让每个节点的邻居权重和为1这样数值更稳定。如果路网节点超过几千个建议把邻接矩阵转成scipy.sparse.csr_matrix后面图卷积可以直接用稀疏矩阵参与乘法内存占用从O(N^2)降到O(E)。还有一点容易忽略邻接矩阵的稀疏化会影响训练速度。N个路口全连结矩阵是N平方的存储500个路口就是25万个浮点数还算能接受如果扩展到5000个路口就是2500万个数必须转稀疏。我通常用KNN把每个节点只保留k个最近邻居k一般取10到20这样既能保留空间传播结构又能让GCN在mini-batch下跑得动。并行处理时也方便直接把稀疏邻接矩阵转成torch.sparse_tensor在GPU上做稀疏乘法速度能快一个数量级。3. 时空图卷积网络落地用图卷积和GRU搭建交通预测基线3.1 图卷积是怎么把邻居信息聚合起来的交通预测的输入格式是(X, A)X的形状是[N, T, F]N是路口数T是历史时间步长F是特征数A是N×N邻接矩阵。图卷积要做的是在空间维度上聚合邻居特征。最常见的定义是Kipf提出的GCNH D^-0.5 * A_hat * D^-0.5 * H * W其中A_hat是加了自环的邻接矩阵D是度矩阵。这一层做的事情很简单每个节点把邻居的特征加权求和再过一个线性层。这对路网来说很合理因为一个路口的流量变化本来就受邻居影响。不过PDF里提到的影响是“流量”和“速度”两个方向这更适合用扩散卷积不只考虑一跳邻居还要考虑多跳传播。扩散卷积把状态转移分解成前向和后向随机游走参数上分开权重公式上相当于在A的两侧都做聚合。在PyTorch里实现时我并不直接写稀疏矩阵乘法而是先把邻接矩阵转成归一化的转移矩阵再通过torch.sparse.mm做聚合内存和速度都合适。实际使用中我只做两跳扩散超过两跳的信息增益很小但参数量翻倍。3.2 时空模块组合GCN层放在GRU之前还是之后空间和时间两个维度都要建模常见的做法有两种。一种是STGCN那种“时间门控卷积GCN”交替堆叠另一种是把图卷积嵌进GRU的更新门和重置门里也就是GraphGRU。我推荐先把问题做简单用GCN对每个时间步做空间聚合再输入GRU做时间建模。这个结构参数少、训练快作为基线足够用来验证数据质量。下面是一个可直接跑的模型定义输入是连续15个时间步的流量预测未来6个时间步的路口流量import torch import torch.nn as nn class STGCNBaseline(nn.Module): def __init__(self, in_dim, hidden_dim, horizon): super().__init__() self.gcn nn.Linear(in_dim, hidden_dim) # 基线上先用全连接模拟GCN self.gru nn.GRU(input_sizehidden_dim, hidden_sizehidden_dim, num_layers2, batch_firstTrue) self.regressor nn.Linear(hidden_dim, horizon) def forward(self, x, adj): # x: [B, T, N, F]adj: [N, N] 已经是归一化邻接矩阵 B, T, N, F x.shape h None outputs [] for t in range(T): xt x[:, t, :, :] # [B, N, F] xt torch.einsum(bij,bjk-bik, adj, xt) # 空间聚合 xt self.gcn(xt) # [B, N, hidden] xt, h self.gru(xt, h) outputs.append(xt) out torch.stack(outputs, dim1) # 用最后一个时刻的隐状态映射到未来horizon步 return self.regressor(out[:, -1, :, :])上面这个版本把adj直接作为因子乘进去省去复杂的GCN实现但在真实项目中GCN需要做对称归一化和权重训练。我一般会在基线版本里使用PyTorch Geometric的GCNConv参数更准确。这里的要点是时间循环那块GRU每次接收的输入先做了空间聚合等于让“空间注意力”先发生再让GRU学习时间依赖。隐藏维度hidden_dim在交通数据上取64到128比较合适太小学不到空间传播太大会在数据量小时过拟合。3.3 训练预设时间窗口、隐藏维度和早期停止参数推荐值说明历史时间步T12或15覆盖早高峰前15分钟太短抓不住趋势预测步长horizon6到12对应未来6到12个5分钟间隔太长误差爆炸GCN隐藏维度64与GRU隐藏维度保持一致方便残差连接GRU层数21层欠拟合3层在小数据集上退化学习率1e-3起步Adam优化器每10轮衰减0.5batch size32路网小于500个节点时够用我一般会把输入数据按5分钟一个时间片切分预测未来30分钟内的流量。训练时先做早停验证集MAPE连续10轮不下降就回滚到最优权重。这里特别要注意的是预测时不能把未来值泄漏到输入里。GRU的循环输入只来自历史窗口滚动预测时要把预测值作为下一个时刻的输入而不是把真实值喂回去否则在测试集上的误差会看着很小实际部署时完全失灵。对于更复杂的场景也可以在GRU之后再加一个空间注意力层但要以基线模型的误差作为参照。我见过不少项目一上来就上Transformer和注意力结果在小数据集上误差反而比GRU基线高。交通流预测的时序依赖相对平稳用循环结构配合图卷积已经能抓住大部分模式真正提升效果的是特征质量和邻接矩阵构造而不是一味加大模型。4. 从训练到上线城市大脑交通预测的评估指标和低延迟部署4.1 数据接入和实时特征对齐城市大脑场景里数据不是存在一个整齐的离线文件里而是从微波、线圈、摄像头以及天气服务实时汇聚的。对于训练集我们需要把不同数据源按时间对齐到同一分钟粒度比如把每5分钟的流量做均值降采样同时把摄像头识别出来的事件数据按路口和时间匹配到节点特征上。这块常见的问题是路口ID不统一。A系统把路口叫“32001”B系统叫“西湖大道-延安路”直接match会丢失一半数据。我一般会在建模前先做一份标准路口表把所有来源的ID映射到一个统一的node_id再构造邻接矩阵。对实时推理来说低延时不是把模型压得越薄越好而是要让数据在被预测的时刻已经完整到达。比如要预测9:00的流量8:55到9:00的流量数据必须在这5分钟内完成清洗和特征拼接。如果某个线圈采集器故障就会出现missing值这时不能用历史均值硬填而是要把缺失节点在邻接矩阵里的聚合权重重新归一化并用它的邻居流量近似补全。下面这段代码处理这种图上的缺失值def fill_missing_with_neighbors(adj, flow, missing_nodes): # flow: [N, F]邻接矩阵已经行归一化 filled flow.copy() adj adj.copy() for node in missing_nodes: neighbors np.nonzero(adj[node])[0] if len(neighbors) 0: filled[node] 0.0 else: weights adj[node][neighbors] weights weights / weights.sum() filled[node] (flow[neighbors] * weights[:, None]).sum(axis0) return filled这个方式相比全局均值的好处是保持了空间连续性某个路口的流量异常时它的邻居通常也有同方向的变化。注意adj传入前要先做归一化否则邻居权重和不为1补出来的数值会偏大或偏小。对于事件类特征如果缺失我会用上一个时间片的值填充因为交通事故状态不会频繁跳变。4.2 损失函数和评估指标的选择交通预测通常用MAE、RMSE、MAPE三件套。RMSE对峰值流量更敏感适合衡量拥堵场景下的误差MAPE对零流量很敏感夜间数据会让它变得很大所以很多论文里会对MAPE加一个最小值限制或者只统计流量大于某个阈值的样本。我复现这份PDF里的场景时会按工作日早高峰7:00到9:00单独切片评估因为城市大脑最关心的是拥堵时段的表现。损失函数我用Huber损失替代MSE。原因是流量数据会偶发极端值比如交通事故后的流量突然归零MSE会对这些异常样本给过大梯度Huber在误差大于delta时退化为L1对异常有更好的抗性。delta通常设为RMSE的0.1倍在训练初期用RMSE估计一个初值也可以。4.3 训练循环、梯度裁剪与断点续训下面给出一个完整的训练循环骨架里面穿进了评估、早停和checkpoint保存def train_model(model, train_loader, val_loader, adj, epochs100): optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size10, gamma0.5) best_loss float(inf) bad_epochs 0 for epoch in range(epochs): model.train() for batch in train_loader: x, y batch optimizer.zero_grad() pred model(x, adj) loss huber_loss(pred, y, delta0.1) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0) optimizer.step() scheduler.step() val_loss evaluate(model, val_loader, adj) if val_loss best_loss: best_loss val_loss bad_epochs 0 torch.save(model.state_dict(), best_model.pt) else: bad_epochs 1 if bad_epochs 10: print(fearly stop at epoch {epoch}) break这里clip_grad_norm很重要。STGNN里的GCN对邻接矩阵做矩阵乘法梯度在深层时容易爆炸特别是用GRU展开15步之后梯度范数经常超过10。clip到5.0能稳定训练。早停看验证集而不是训练集避免把模型保存到过拟合的点上。断点续训也很实用城市大脑的实时数据集会持续增加我会每一天把新的观测追加到训练集然后用上一次的checkpoint继续训练而不是从随机初始化开始否则前面学到的空间相关性会被冲掉。为了理清上线时的资源分配我常用下面这张表做容量评估部署项配置建议说明模型推理框架TorchScript或ONNX Runtime去掉Python的前后处理开销节点数规模500以下用稠密矩阵500以上建议稀疏邻接矩阵推理频率每5分钟一轮与数据产生节奏一致即可不必每秒钟跑并发通路单实例配4线程模型小瓶颈在特征拼接和邻接矩阵读取这张表说明5分钟推理周期下CPU和内存压力都不大真正的瓶颈几乎都在数据处理管道上。5. 从预测到干预动态闭环验证和模型封装的两个细节5.1 预测干预的动态模型怎么验证这个PDF强调了“预测-干预-反馈”动态环路。交通预测模型在真实环境中不能只静止地预测因为信号灯调整后会改变车流的时空分布历史分布和未来分布会偏移。所以模型上线时我一般会先做影子模式不做干预只让模型跟真实观测对比。等误差稳定后再把模型的预测结果交给信号机优化模块并记录干预前后的流量变化。干预反馈的数据不能直接进训练集要先做漂移检测比如用PSI检验干预前后的流量分布差异差异超过阈值时就要触发模型重训练。这个闭环看起来简单但它解释了为什么PDF的链路要把预测和干预放在一起设计。5.2 把模型导出成隐藏路网结构的推理包PDF里提到第三方算法接入时要保护模型细节。当前开源平台普遍支持ONNX或TorchScript导出。在导出时要把邻接矩阵做成模型常量而不是每次推理时重新传进去这样一方面更快另一方面也把路网结构隐藏起来。ONNX导出的关键是把动态维度固定住一个batch预测数据输入序列长度、节点数和特征数都不变这样算子融合更彻底。下面这段是TorchScript导出时的一个注意点model.eval() traced torch.jit.trace(model, (example_input, adj)) traced.save(traffic_stgnn.pt)如果模型里有依赖batch维度的循环比如用GRU递归展开trace时要把输入batch固定为1。导出后在实际推理里我一般把推理进程做成一个常驻服务通过共享内存读实时流量数据再以固定间隔触发推理。低延时的关键不是把模型压缩得多小而是把模型输出结果直接写到信号机系统的输入队列里省掉中间文件的落地。最后还有一个容易踩的坑TorchScript的trace是执行导向的如果代码里有if分支依赖张量数值trace会漏掉分支需要改用torch.jit.script或者把分支移到图卷积的前后处理里。本文还有配套的精品资源点击获取
网站建设高端定制企业官网