
简介这是一份基于时空图神经网络的交通预测PDF资料适合智慧城市、交通数据分析与深度学习研究者阅读。资料以阿里巴巴达摩院城市大脑实践为背景系统梳理了从数据接入、数据挖掘、预测到干预的动态闭环重点讲解如何利用时空图神经网络处理交通流量、事故与天气数据构建短长期交通流预测模型并涉及低延时高并发计算及开源平台等工程落地问题。资源为单个PDF文档大小约1.48MB内容紧凑完整可作为技术报告或行业综述快速查阅。目前已有292人学习/浏览是快速了解城市大脑交通预测技术框架与模型思路的入门参考。1. 先搞清楚这张 PDF 在解决什么问题假设你在做某城市主干道未来 15 分钟平均车速预测。LSTM 跑出来的 baseline 在白天还能看一到早高峰就出问题A 路预测 60 km/h但它的上游 B 路已经堵成深红这个结果显然离谱——因为你把每条路段当独立时间序列了。路网是图车辆会流动堵车会传递结构信息一旦被丢掉再强的时序模型也无从补救。基于时空图神经网络的交通预测就是要把“结构 时序”放进同一个模型路段是图上节点连接是边节点特征随时间变化图卷积抽空间依赖时序模块抽时间依赖。这份 PDF 的核心是把两件事联合建模而不是分别在两个模型里做完再拼结果。适合谁手里有路网拓扑或卡口数据想摆脱“拆开预测”的算法工程师以及做智慧交通方案选型的技术负责人。2. 把路网变成图数据原始数据到模型输入的三步预处理很多人拿到 PDF 第一件事是抄模型结构结果代码抄得一模一样训练出来却是一团糟。问题几乎都出在数据侧路网和检测器数据不是天生就能喂给图神经网络的。在模型跑起来之前你要完成三件事构图、建特征、切样本。2.1 邻接矩阵的三种构造法与关键参数STGNN 的输入图一般用邻接矩阵 A 描述A[i][j] 表示节点 i 和节点 j 之间的连接权重。同一个路网至少有三套常见构图法基于空间距离、基于路网拓扑、基于历史相关性。先说我用得最多的空间距离法。把每个检测器或路段取一个经纬度坐标点按两两之间的欧氏距离构造高斯核函数。这套做法不依赖路网拓扑数据只要有点位坐标就能建图缺点是当两个检测器物理上很近但被高架或河流隔开时拓扑上并不相通空间距离会制造一条虚边。所以实际工程里一定要加阈值把权重太小的边直接去掉。基于路网拓扑的构图法更贴近真实世界做法是把路口或路段当作节点用道路连接关系直接生成 0/1 邻接矩阵。问题是很多检测器点位不在路口需要先匹配到最近节点矩阵很容易变稀疏而且部分节点可能完全孤立图卷积在孤立节点上只能学到自身特征。基于历史相关性的做法更“数据驱动”用速度或流量序列的相关系数当边权例如 A[i][j] |Pearson(i, j)|。它能捕捉真正同步变化的道路对但前提是每条序列足够长、质量足够好而且相关系数很容易受节假日和异常事件污染训练时容易过拟合。我的建议是第一版用空间距离法跑通链路再融合拓扑掩码和历史相关性做迭代。下面是最常用的距离阈值高斯核import numpy as np from scipy.spatial.distance import cdist def build_adj_from_coords(coords, sigma20.1, threshold0.7): coords: (N, 2) 数组每一行是节点的 (经度, 纬度) 返回: (N, N) float32 邻接矩阵 n len(coords) dist cdist(coords, coords, metriceuclidean) adj np.exp(-dist ** 2 / sigma2) adj[adj threshold] 0.0 np.fill_diagonal(adj, 0.0) # 自环在 GCN 里单独加 return adj.astype(np.float32)sigma2 和 threshold 是两个要反复试的参数。sigma2 控制距离衰减速度设太小则只有自环还保留权重设太大则整个矩阵全是接近 1 的稠密连接。threshold 决定稀疏度经验上让每行平均非零元素数落在 5 到 15 比较合适太稀疏聚合不到邻居信息太稠密输出会向邻居均值收敛预测结果全部被磨平。我习惯先画一行权重分布再看整个图的稀疏度两个参数一起定。如果手里有路网拓扑关系可以在距离矩阵上乘一个连通掩码这样一个矩阵里同时保留拓扑可达约束和空间权重def build_adj_with_mask(coords, topo_mask, sigma20.1, threshold0.7): dist cdist(coords, coords, metriceuclidean) adj np.exp(-dist ** 2 / sigma2) adj adj * topo_mask # topo_mask 是 0/1 矩阵1 表示拓扑可达 adj[adj threshold] 0.0 np.fill_diagonal(adj, 0.0) return adj这里有个隐藏前提所有节点坐标必须是同一坐标系。不同供应商的数据有的是经纬度有的是平面投影坐标直接混合计算距离会得到一张完全不可用的图。我见过不止一次指标比 baseline 还差排查半天发现是坐标系混用。2.2 节点特征先做缺失值插值再做归一化邻接矩阵描述“谁和谁相连”节点特征描述“这条路上正在发生什么”。交通预测里常用特征有三类历史观测值速度、流量、占有率、时间编码星期几、是否节假日、一天中的小时、外部事件天气、临时施工。PDF 里的模型骨干一般只用第一类但产品化时建议把第二类也拼进去。真实数据有三类问题缺失、噪声、量纲不一致。先处理缺失。检测器损坏、网络传输断链是常态经常出现一整夜没有数据的横沟。短间隙用时间维度的线性插值长间隙超过一小时不要硬插直接把对应样本的 mask 置 0让模型学会忽略缺失位置。再处理量纲。速度是 0 到 120 的连续值流量可能从 0 到 6000 辆每小时如果不做归一化直接进模型图卷积结果会被流量一个维度主导。速度、流量、占有率各自做 z-score 比较稳妥输出端最后再还原。如果未来要做分位数回归也可以统一用 min-max 压到 [0,1]。def preprocess_features(df, feature_cols, methodzscore): df: 包含 node_id, speed, flow, occupancy 的 DataFrame feature_cols: 要标准化的特征列 result {} for node in df[node_id].unique(): sub df[df[node_id] node].sort_index() if method zscore: mean sub[feature_cols].mean() std sub[feature_cols].std().replace(0, 1) sub[feature_cols] (sub[feature_cols] - mean) / std elif method minmax: lo sub[feature_cols].min() hi sub[feature_cols].max() sub[feature_cols] (sub[feature_cols] - lo) / (hi - lo 1e-6) result[node] sub return pd.concat(result.values())这里必须强调两点。第一标准化统计量只能在训练集上计算验证集和测试集要用训练集的均值标准差去转换一旦把验证集混进统计过程验证指标会虚高上线后立刻现原形。第二不同节点的速度分布差异很大高速路和城区小路的标准差可能差好几倍。按节点分别标准化通常收敛更快但推理还原时也要按同样方式做。提示如果你发现训练集和验证集 MAPE 差异很小但线上效果一塌糊涂先查标准化统计量是不是混了验证集这是最常见的隐性泄漏。2.3 时间滑窗切样本时最容易犯的错模型输入的常见形态是取过去 P 个时间步的图特征预测未来 Q 个时间步。P 由预测目标定义比如 5 分钟粒度、预测未来 15 分钟一般取过去 6 到 12 步预测未来 1 小时可能需要过去 1 小时以上的上下文。滑窗本身不复杂复杂在“样本不能乱切”。默认流程是先按时间排序固定步长滑动滑出来的样本要检查有没有跨越大段缺失切完后按时间顺序划分训练、验证、测试集而不是随机划分。交通数据有强周期性随机抽样会把高峰样本同时放进训练和测试指标看起来不错部署却完全不是那回事。def make_sliding_samples(features, seq_len12, pred_len3, stride1): features: (T, N, F) 已标准化的特征序列 返回样本和标签 samples, labels [], [] for t in range(0, len(features) - seq_len - pred_len 1, stride): x features[t:tseq_len] # (seq_len, N, F) y features[tseq_len:tseq_lenpred_len, :, 0:1] # 预测速度 samples.append(x) labels.append(y) return np.stack(samples), np.stack(labels)这段代码把 T 长度序列切成若干个 (12, N, F) 输入和 (3, N, 1) 标签。注意取标签时直接取原特征矩阵里的速度列这样不容易出现标签平移错位。更重点的问题是内存T 是三万步、N 是三百节点、F 是三维时原始矩阵只有 27M 浮点数还能撑但滑窗展开后变成几百万个 (12, 300, 3) 样本全量 numpy 化会直接内存爆掉。常见做法是写一个 Dataset 类只记录每个样本的起始索引训练时才按索引切片取数不要一开始就把所有样本堆进内存。3. 时空图神经网络的三条主线和最小可跑骨架数据就位后模型要怎么选。我见过不少 STGNN 论文落到代码上其实只有三条主线谱域图卷积、空域图卷积、时空交替框架。理解差别之后选型就顺理成章了。3.1 谱域图卷积与空域图卷积工程上该选哪条谱域方法基于图信号处理把图上信号变换到谱域做频谱滤波。最经典的 GCN 就是谱域思想加切比雪夫近似的结果。谱域方法数学性质清晰、实现简单缺点也很明显滤波器参数和具体图结构强绑定换一张拓扑图就得重新训练节点数量到大几千后计算成本迅速上升。空域方法直接在节点邻居上做消息传递和聚合每个节点的新表示由它的邻居表示加权求和得到。Graph WaveNet、STGCN 这类实际效果突出的模型基本都是空域路线。交通场景节点数量常是几百到几千图相对稳定谱域还够用要是做到全市级几万节点谱域基本撑不住。我第一次做选型时直接抄了空域 GCN也因此避开了很多谱域实现的坑。我的建议是第一版别纠结学术流派用空域 GCN 或 GraphSAGE 式聚合快速验证链路再根据效果决定是否换成更重的时空模型。3.2 时间模块选型从 GRU 到膨胀时间卷积图卷积解决空间聚合时间维度还需要专门模块。常见有两种。第一种是循环结构。把每个时间步的图卷积结果输入 GRU 或 LSTM由门控控制时间步之间的信息保留与遗忘。实现简单、训练稳定但循环结构必须按时间顺序计算推理速度慢无法并行预测 12 步以内还行预测 48 步以上误差累积会非常明显。第二种是时间卷积结构用一维卷积或膨胀因果卷积在时间维度抽取依赖。一次可以并行处理全部时间步训练速度快靠膨胀系数扩大感受野不需要堆很多层。Graph WaveNet 里的时间模块就是这么做的。凡是预测长度超过 12 步的任务我一般优先选时间卷积而不是循环结构。3.3 最小可跑模型一层 GCN 接一层 GRU我这边最简结构输入 (batch, time, node, feature) 先 reshape 成 (batch*time, node, feature)过一层 GCN再 reshape 回 (batch, node, time, hidden)接一层 GRU输出未来序列。这个骨架是用来回答“这套方案在我数据上行不行”最快的路径。下面用 PyTorch 和 PyTorch Geometric 写最小实现import torch import torch.nn as nn import torch.nn.functional as F from torch_geometric.nn import GCNConv class GCNGRU(nn.Module): def __init__(self, in_dim, hidden_dim, out_dim, seq_len, pred_len, num_nodes): super().__init__() self.gcn GCNConv(in_dim, hidden_dim) self.gru nn.GRU(input_sizehidden_dim, hidden_sizehidden_dim, batch_firstTrue) self.pred nn.Linear(hidden_dim, out_dim) self.num_nodes num_nodes self.seq_len seq_len self.pred_len pred_len def forward(self, x, edge_index): x: (batch, seq_len, num_nodes, in_dim) edge_index: (2, E) COO 格式 b, t, n, f x.shape x x.reshape(b * t, n, f) x self.gcn(x, edge_index) # (b*t, n, hidden) x F.relu(x) x x.reshape(b, t, n, -1) x x.permute(0, 2, 1, 3) # (b, n, t, hidden) x x.reshape(b * n, t, -1) out, _ self.gru(x) # (b*n, t, hidden) out out[:, -self.pred_len:, :] out self.pred(out) # (b*n, pred_len, out_dim) out out.reshape(b, n, self.pred_len, -1) out out.permute(0, 2, 1, 3) # (b, pred_len, n, out_dim) return out这段代码有几个地方值得解释。第一个是 reshape 手法GCN 之前把 (batch, time, node, feature) 压成二维 (batch*time, node, feature)让每个时间步共享同一套图卷积参数这是 PyG 里的标准操作。时间维度的依赖完全交给 GRU不会被 GCN 破坏。第二个是 GRU 输入用 (batch*node, time, hidden)这不是唯一写法也可以换成 (batch, node, time, hidden) 然后合并维度效果差别不大。我保留这个写法是因为显存占用更友好而且 GRU 内部迭代的 batch 更大训练更稳定。第三个是输出端只取 GRU 最后 pred_len 个时间步而不是输出整个序列再接全连接。这样预测维度只跟输出长度相关模型在切换预测时长时更灵活不会受 seq_len 影响。注意PyG 的 GCNConv 要求传 edge_index 而不是邻接矩阵 A。你可以用from torch_geometric.utils import dense_to_sparse把 NumPy 邻接矩阵转成 COO 格式转的时候去掉自环GCNConv 内部会自己加。参数上我一般先用 hidden_dim64、GCN 一层、GRU 一层跑通。如果验证 loss 下降太慢先加 hidden_dim不要急着加图卷积层数如果训练 loss 很低但验证 loss 不降再加 Dropout 并减小 hidden_dim。第一版模型的目的是验证链路不是追指标。4. 训练、评估与调参三个直接影响结果的关键决策模型能跑只是第一步。交通预测真正耗时间的是三个决策loss 怎么选、指标怎么对比、哪些超参数值得花时间调。4.1 loss 选型MAE、MAPE 还是分位数损失绝大多数复现代码盯着 MSE 不放但做交通预测的都知道 MSE 会被少数极端事件主导。比如一次事故造成的短时拥堵速度从 80 跌到 10MSE 会产生非常大梯度的损失模型为了压掉这个样本会把正常时段的预测整体拉低。MAE 对极端值不敏感不容易被突发拥堵带偏是工程上最稳的默认选择。MAPE 是无量纲百分比方便向业务解释但速度接近 0 时会爆炸需要给分母加一个小常数或者把速度下限截到 5 km/h。如果目标是做拥堵预警、给调度部门发区间预报我建议直接用 pinball loss 做分位数回归预测的是一个区间而不是单点决策价值完全不同。def quantile_loss(y_true, y_pred, tau0.5): tau0.5 时等价于 MAEtau0.9 时得到高分量预测 err y_true - y_pred loss torch.where(err 0, tau * err, (tau - 1) * err) return loss.mean()pinball loss 的一个附加好处是它天然不鼓励模型向均值回归长序列预测时不容易把曲线磨平。我最近一个项目正是靠这个方案让 30 分钟以上的预测保留了高峰形态。4.2 三个必调超参数历史窗口、隐藏维度、学习率与 batch size第一是历史窗口长度 seq_len。窗口太短模型看不到早高峰的起势预测拐点会滞后窗口太长输入维度和计算量上升反而引入凌晨无害噪声。经验上是 5 分钟粒度预测 15 分钟seq_len 取 6 到 12预测 1 小时seq_len 取 12 到 24。这个值不要只靠验证集选最好画不同 seq_len 下的预测曲线重点看拐点位置拟合。第二是隐藏维度 hidden_dim。节点数少于 500 时 64 是合理起步节点数多就上 128。但 hidden_dim 不是越大越好交通数据信噪比有限翻倍很快会被过拟合吞掉。一个笨办法是跑 20 个 epoch 看训练 loss 是否明显低于验证 loss如果是先降 hidden_dim别急着加正则化。第三是学习率和 batch size 的搭配。一般先把 batch 固定到 32 或 64学习率从 1e-3 起步如果训练 loss 下降后期剧烈抖动降到 1e-4 重跑。学习率调度用 cosine 衰减比 Step 衰减省心Step 的下降点需要反复试cosine 只要设总轮数。4.3 评估指标和 baseline 的设置方法交通预测最容易掉进“自嗨陷阱”模型 MAE 看着不错却不知道相对 baseline 提升了多少。常用 baseline 有历史平均同一时段历史均值、上一时刻复制用 t 时刻值直接当 t1 预测、线性回归、XGBoost、LSTM。我的习惯是新模型必须先跑 LSTM 和 XGBoost 两条把它们的指标当及格线。STGNN 如果打不过这两条大概率不是模型问题而是图构造或数据预处理出了问题。下面这段代码输出三个核心指标def evaluate_metrics(y_true, y_pred): y_true np.asarray(y_true).flatten() y_pred np.asarray(y_pred).flatten() mae np.mean(np.abs(y_true - y_pred)) rmse np.sqrt(np.mean((y_true - y_pred) ** 2)) mape np.mean(np.abs((y_true - y_pred) / (y_true 1e-3))) * 100 return {MAE: mae, RMSE: rmse, MAPE: mape}这里我要加一个容易忽略的对比条件用 RMSE 或 MAPE 对不同模型做比较时必须保证预测时段一致。高峰期误差普遍是非高峰期的 2 到 3 倍如果模型 A 平均预测 12 步、模型 B 只预测 6 步指标差异根本没有可比性。另一个关键是验证集划分必须按时间顺序比如 60 天数据前 42 天训练、中间 9 天验证、最后 9 天测试。随机抽样等于开卷考试模型已经见过几乎同一天所有时段的模式测试指标完全没有参考价值。在一组城市级路网的示意性数据上简单对比关系大致如下表注意数值只用于说明相对差距和对比基准不是某篇 PDF 的官方结果方法MAEkm/hRMSEkm/hMAPE%备注历史平均 HA8.612.122.4无结构信息LSTM6.910.317.8全局趋势可用GCNGRU最小骨架6.19.215.6加入路网结构带膨胀卷积的时空模型5.48.013.9长序列更稳5. 避坑与常见问题排查复现 STGNN 最容易翻车的四个位置这一章专治玄学。以下四条是我在项目里真实踩过的按“现象 → 原因 → 解决”来写。5.1 邻接矩阵没归一化训练一轮就 NaN现象训练刚开始 loss 就变成 NaN或者预测值永远落在一个常数附近调小学习率也没用。原因邻接矩阵没有做对称归一化。如果直接用原始 0/1 矩阵和特征相乘每个节点聚合的邻居数量不同特征整体量级会被拉偏图卷积输出越来越大最后溢出成 NaN。另一个常见原因是矩阵里有 NaN 值没清干净混合进稀疏矩阵计算后直接污染梯度。解决用对称归一化 D^(-1/2) A D^(-1/2)或者退一步做行归一化。PyG 的 GCNConv 内部做了归一化但如果你是自己实现图卷积这一步不能省。另外构建邻接矩阵后加一句np.isnan(adj).any()做检查成本极低能省下半天排错时间。5.2 训练 loss 下降了验证 MAPE 却比 HA baseline 还差现象模型训练曲线很正常loss 稳步下降结果在验证集上 MAPE 比历史平均还高怎么调参都没用。原因大概率是数据泄漏。标准化统计量混入了验证集或者滑窗切样本时随机打乱了时间顺序模型在训练时已经见过相近时间片段。还有一个常见原因是评估时段没有排除夜间低流量夜间速度接近零MAPE 会变成巨大值淹没了真实的预测能力。解决把标准化统计量严格固定到训练集验证集和测试集只做转换不参与统计切分样本严格按时间顺序评估时单独跑一遍“仅白天 6:00-22:00”和“全天”两组指标避免夜间零值污染。这两组数字差异很大出现异常就知道问题在哪。5.3 预测 15 分钟还行预测 30 分钟以上曲线全被磨平现象短时预测的曲线形状和真实值对得上时间拉长到 30 分钟或 1 小时所有预测值向均值收缩高峰肩部消失。原因这是两个因素叠加。一是回归型输出天然有向均值回归的趋势预测长度越大越明显二是模型没有学到足够强的动态模式时间卷积感受野不够输出趋近于平稳估计。解决改用分位数损失或预测残差。残差做法是把预测目标从“绝对速度”改成“相对当前时刻的变化量”让模型保留突变方向感受野不够时把时间卷积的膨胀系数从 [1,2,4,8] 做起这个组合在 5 分钟粒度下能覆盖约 75 分钟时序上下文。改完之后重点看高峰前后各两个步长的误差是否明显下降。5.4 图卷积感受野不足高峰拐点系统性滞后现象预测速度和真实值的拐点对不上模型预测的堵车开始时间和结束时间都晚了 10 到 15 分钟。原因图卷积层数太少。一层 GCN 只能看到一跳邻居如果两个强相关路段中间隔了一个路口信息传不过去。空间感受野不足时模型只能靠时间序列自己猜拐点猜不准就滞后。解决把图卷积从 1 层加到 2 到 3 层但要注意加层之后参数量上升配合 Dropout 0.3 左右同时检查邻接矩阵最远有效传播距离用A_power A A看二阶邻居是否真的连上了。很多人加层无效是因为邻接矩阵太稀疏二跳邻居已经全变成了零加多少层都是白搭。6. 验证、轻量化与进一步优化最后说点让方案真正落地的做法。模型效果好只算完成三分之一后端还要解决推理速度、模型体积和线上稳定性。这里三个技巧都是我做交通预测项目时沉淀下来的。6.1 用误差空间分布判断模型是否学到结构前面提过 baseline我再给一个判断模型是否学到时空结构的方法。把每个节点的预测误差展开到路网上画一张空间分布图。如果差值在相邻节点上成片出现说明这些节点之间的边没有起作用模型只学到了全局趋势。正常情况下误差应该是随机散落在各节点而不是聚成一团。这一步肉眼看起来简单却比任何指标都能更快暴露构图问题。6.2 模型压缩与推理部署交通预测经常要实时更新真实线上服务通常不需要 GPU。GCNGRU 这类结构可以直接把权重导出成 TorchScript 或 ONNX 格式用 CPU 推理。常见的优化有两点一是把 GCN 里的 D^(-1/2) A D^(-1/2) 归一化矩阵预计算成常量避免每次 forward 重复算二是把 GRU 的时间步剪到实际需要的最小长度推理时按 causal 顺序只算最后几个时间步。剪完之后模型体积通常能从几十 MB 降到几 MB单条预测延迟压到毫秒级。6.3 定期重训与再校准路网会变模型不能一次训练终身使用。我的习惯是每两周用最近八周数据做一次增量微调保留原有权重只在新数据上继续训少量 epoch。每次重训之后在测试集上对比新旧模型 MAPE 并记录下来。连续三次重训都没有正的提升就停止重训回头检查窗口长度和特征组成而不是继续空转。这篇笔记读到最后最想让你带走的一条经验是时空图模型的收益必须靠 baseline 对比和误差空间分布来证明而不是只看训练 loss。希望这些踩坑经历能帮你少走一段弯路。本文还有配套的精品资源点击获取