ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

图卷积神经网络交通流量预测实战:从建图到调参

图卷积神经网络交通流量预测实战:从建图到调参 简介面向智能交通、深度学习与图神经网络方向的论文级资源聚焦城市交通流量预测难题提出基于图卷积神经网络GCN的建模思路。利用图结构表达道路网络通过聚合节点局部邻居信息同时捕捉路网拓扑关系与车流量、车速等时空特征为智慧交通管理和相关课题研究提供参考。压缩包内含1个PDF文件大小1.18MB收录了哈尔滨师范大学团队2019年发表的完整论文包含模型原理、图卷积公式、网络结构示意图以及真实数据集上的车流量和车速预测实验。全文内容完整便于直接引用或作为入门GCN交通预测的参考资料。已有318人学习下载适合智能交通、数据建模领域的研究者和工程师快速掌握GCN预测方法。1. 图卷积神经网络对交通流量预测意味着什么交通流量预测有一个让人别扭的事实道路交叉口和路段天然是图结构可大多数工程师上手就把它当作图像或序列去建模。十字路口不能做 padding两条路之间的依赖距离不能用时间步长衡量3x3 卷积核根本卷积不出匝道对主路的汇入压力。图卷积神经网络GCN正是把卷积从规则网格推广到任意图结构的那一层它用邻接关系代替空间邻域让每个节点的特征聚合来自相邻路段的流量、速度和占有率信息。这篇文章要讲的就是从流量数据到邻接矩阵、再到谱域或空间域卷积的实现路径适合已经会用 LSTM 或 Transformer 做时序预测、但面对路网空间结构感到别扭的工程师。读完你至少能说清楚为什么拉普拉斯矩阵是图卷积的入口如何把一份路口流量表组织成 PyTorch 能跑的数据集以及批次训练时邻接矩阵应该放在哪一侧。2. 从路段拓扑到图卷积交通流量预测先得把图建对2.1 为什么 CNN 处理不了路网GCN 真正读入的是什么交通流量预测的输入通常是每个检测器或路段在一段时间内的历史流量。如果只按时间维度看这是典型的多变量时间序列每一列是一个传感器每一行是一个时刻。但传感器之间存在空间相关性上游发生拥堵10 分钟后下游流量必然下降快速路与平行地面道路之间存在分流关系。传统的 CNN 要求输入是规整网格你可以把传感器强行排列成矩阵但那样排列出来的邻接关系是人为的和真实物理道路毫无关系。LSTM 这类时序模型更麻烦它把每个传感器看作独立特征隐状态共享权重却无法显式表达“哪条路和哪条路直接相连”。GCN 的做法很直接把路网抽象成一张图节点表示路段或路口边表示连通关系。每个节点有一个特征向量例如最近 12 个时间步的流量。图卷积的操作本质上是逐层传播和聚合信息每个节点的新特征由自身特征与邻居特征加权求和得到。这个“邻居”是由邻接矩阵定义的因此模型天然具备空间归纳偏置。注意图卷积不替代时序模型它替换的是空间编码层。常见架构是时间维用 1D 卷积或 GRU空间维用 GCN两者串联或并行后再输出预测。2.2 邻接矩阵的三种构建方式以及拉普拉斯矩阵为何要归一化构建邻接矩阵是交通流量预测里最容易出错的一步。很多开源实现直接用地理距离计算相似度比如两个检测器相距小于 500 米就认为有边但这种方式会产生孤立节点也会把高架桥上下层本不相通的两个点连在一起。更稳妥的做法是结合拓扑关系如果两个检测器在同一个路段上直接连通或者通过路口相邻就在邻接矩阵中置 1。还有一种常见做法是使用交通流相关性计算各时间序列之间的 Pearson 相关系数超过阈值的视为连边。实际项目中通常将前两种混合物理连接优先再用相关性补充缺失边。拿到邻接矩阵 A 之后要计算度矩阵 D 和拉普拉斯矩阵。GCN 里最常用的是对称归一化形式L_sym D^(-1/2) * (A I) * D^(-1/2)加单位阵 I 是为了保留自环否则节点在卷积后只能聚合邻居信息丢失自身特征。为什么要归一化因为如果不做处理度数大的节点比如连接 5 条路的大型互通立交聚合后的特征值会远大于普通路段的末端节点导致梯度在传播时产生数值波动。归一化后每个节点聚合的特征向量都在一个可控量纲范围内。2.3 谱域卷积与切比雪夫近似的核心公式图卷积有两大流派谱域Spectral和空间域Spatial。对于交通流量预测最常见的理论起点是谱域图卷积。它的思想是将图上信号 x 做图傅里叶变换到频域利用拉普拉斯矩阵的特征向量作为基在频域上乘以可学习的滤波器再通过逆变换回到节点域。理论上直接计算需要拉普拉斯矩阵的特征分解复杂度 O(N^3)N 是节点数城市路网上千个节点时计算量不可接受。于是在实际工程中几乎不会直接做特征分解而是用 K 阶切比雪夫多项式近似滤波器x * G sum_{k0}^{K-1} θ_k T_k(Λ_tilde) x其中 T_k 是切比雪夫多项式Λ_tilde 是拉普拉斯矩阵的特征值缩放结果θ 是待学习参数。K 阶近似意味着每个节点最多能聚合 K 跳邻居的信息这直接控制了感知范围。当 K1 时退化为 GCN 的经典形式也常被称为 1 阶近似。对交通流量预测来说K 通常设置在 2 或 3因为交通拥堵的传播往往只影响两三跳之内的路段再远的相关性已经在时序维度上被滞后效应消化掉了。注意切比雪夫近似里不需要显式计算特征向量只需要计算拉普拉斯矩阵的幂乘和多项式组合这就能用稀疏矩阵乘法高效实现。这是它能在真实路网上跑起来的关键。3. 用 PyTorch 和 PyG 实现 GCN 交通流量预测的最小可训练模型3.1 数据构造把流量表格变成带时序滑窗的图样本先准备数据格式。假设你手头有一张二维数组X形状为[num_timesteps, num_nodes]每一行是一个时间步各节点的流量每一列是一个节点的时间序列。预测任务可以定义为给定过去input_steps个时间步的流量预测未来output_steps个时间步的流量。这里不能直接把二维数据扔进图卷积因为图卷积的输入形状要求是[batch, nodes, features]其中 features 在这里就是时间步。我们需要用滑动窗口切分样本。常见做法是把每个样本变成一个三维张量[batch_size, input_steps, num_nodes]然后再转置成[batch_size, num_nodes, input_steps]作为空间卷积层的输入。我写过一个预处理函数是这样的import numpy as np import torch def create_sequences(data, input_steps12, output_steps3): # data: [num_timesteps, num_nodes] num_timesteps, num_nodes data.shape inputs, targets [], [] for i in range(num_timesteps - input_steps - output_steps 1): x data[i:iinput_steps, :] # [input_steps, num_nodes] y data[iinput_steps:iinput_stepsoutput_steps, :] inputs.append(x.T) # 转成 [num_nodes, input_steps] targets.append(y.T) # [num_nodes, output_steps] return np.array(inputs), np.array(targets) X, Y create_sequences(raw_flow_data, 12, 3)为什么转置因为 GCN 的节点维度必须在中间PyTorch 中的线性层和一维卷积都期望特征维度在最后。转置后张量形状为[样本数, num_nodes, input_steps]每个节点的特征就是它过去 12 个时间步的历史流量序列。3.2 定义两层图卷积网络结构在 PyTorch 里最简单的 GCN 层不需要引入 PyTorch Geometric 也能实现。对于 1 阶近似的 GCN其传播规则是H_new ReLU(A_hat H_old W)其中 A_hat 是归一化邻接矩阵W 是可学习权重。下面是定义好的网络类使用稀疏矩阵乘法提高效率import torch import torch.nn as nn import torch.nn.functional as F from scipy.sparse import coo_matrix class GCNLayer(nn.Module): def __init__(self, in_features, out_features): super().__init__() self.W nn.Parameter(torch.FloatTensor(in_features, out_features)) nn.init.xavier_uniform_(self.W) def forward(self, x, adj): # x: [batch, num_nodes, in_features] # adj: [num_nodes, num_nodes] 归一化邻接矩阵 support torch.matmul(x, self.W) # [batch, num_nodes, out_features] # adj 与节点维度做矩阵乘法 out torch.matmul(adj.unsqueeze(0), support) # 广播到 batch return out class TrafficGCN(nn.Module): def __init__(self, num_nodes, input_steps, hidden_dim32, output_steps3): super().__init__() self.gcn1 GCNLayer(input_steps, hidden_dim) self.gcn2 GCNLayer(hidden_dim, hidden_dim) self.fc nn.Linear(hidden_dim, output_steps) def forward(self, x, adj): h F.relu(self.gcn1(x, adj)) h self.gcn2(h, adj) h F.relu(h) out self.fc(h) # [batch, num_nodes, output_steps] return out逻辑说明第一层 GCN 把每个节点的 12 维历史特征映射到 32 维隐藏空间同时聚合一跳邻居的信息。第二层 GCN 再聚合一次相当于每个节点能看到两跳范围内的交通状态。最后的线性层把隐藏状态映射到需要预测的 3 个时间步。如果你觉得两层邻域不够可以把GCNLayer调整为切比雪夫近似多层堆叠会带来过平滑问题后面我们会讨论。这里有个关键细节adj必须是归一化后的矩阵而且要在训练前计算好。注意邻接矩阵不参与梯度更新所以无需放到模型的Parameter里直接作为 forward 的额外参数传入就可以。3.3 训练循环与损失函数选择以及批次维度怎么对齐流量预测的常用损失函数是平均绝对误差MAE或均方误差MSE。如果按节点做回归直接计算预测值和真实值之间的差异就行。但要注意不同路段的流量量级差异很大有的快速路流量是 8000 辆/小时支路只有几百MSE 会被大数值路段主导导致小流量路段学不到信息。我一般会先对流量做 z-score 标准化或者归一化到 0-1然后在标准化后的空间计算损失。训练循环def train_model(model, train_X, train_Y, adj, epochs200, lr0.001): optimizer torch.optim.Adam(model.parameters(), lrlr) loss_fn nn.MSELoss() train_X torch.FloatTensor(train_X) train_Y torch.FloatTensor(train_Y) adj_tensor torch.FloatTensor(adj) for epoch in range(epochs): model.train() optimizer.zero_grad() pred model(train_X, adj_tensor) # [N, nodes, out_steps] loss loss_fn(pred, train_Y) loss.backward() optimizer.step() if epoch % 20 0: print(fEpoch {epoch}, loss {loss.item():.4f})参数说明lr0.001是 Adam 优化器下比较安全的起点如果 loss 震荡可以降到0.0005。batch_size这里没有用因为路网节点数通常几百到几千整个数据集一次前向没有太大内存压力。但如果样本数很大还是需要用DataLoader分批。分批时要注意邻接矩阵必须在所有批次间共享所以它不能在DataLoader里自动 batching而是在每个 batch 前向时手动传入。有一个很容易踩的坑adj.unsqueeze(0)是将邻接矩阵变成[1, N, N]以便与[batch, N, hidden]做批量矩阵乘法。如果某个时刻模型报维度不匹配检查是否是这里少了unsqueeze。另外如果你的邻接矩阵用 SciPy 稀疏矩阵保存记得转成稠密或稀疏 torch 张量否则torch.matmul无法直接处理。4. 交通流量预测的图卷积调参邻接矩阵、邻域阶数、时序窗口怎么定4.1 邻接矩阵的阈值设定与不同构建策略对比很多工程师手里只有经纬度没有路网拓扑这时候只能用距离阈值构建图。常见公式是A_ij exp(-dist(i,j)^2 / sigma^2) if dist(i,j) thresholdsigma 是带宽参数控制权重随距离衰减的速度。threshold 一般在 1 到 3 公里之间具体要看城市路网的平均检测点间隔。这里有个悖论阈值太大图会变成稠密连通每个节点的邻居数太多聚合后的特征趋同模型会退化成各个节点无差别阈值太小图会碎成多个不连通分量图卷积的传播区域受限远端路况信息传不到。我常用的办法是先统计所有检测器对的最短路径距离画出分布。如果中位距离在 400 米左右阈值可以取 800 米到 1.2 公里。建图后检查每个节点的度分布孤岛节点需要做特殊处理。数据驱动方法里还有一种基于历史流量相关性的建图方式计算每个路段流量时间序列与其他路段的 Pearson 相关系数保留相关系数大于 0.8 的边但这种方式很容易产生全连通图因为城市交通存在日夜节奏几乎所有的路段都在早高峰同步上升。所以物理拓扑优先相关性作为补充是最稳健的策略。4.2 K 阶邻居与图卷积层数的配合模式切比雪夫近似的 K 值直接决定了每个图卷积层感受野的大小。K1 时是 GCN 一阶近似每个节点只能看到自己。K2 时能看到两跳覆盖周围 2 到 3 个路段的交通信息。对大多数路网两到三跳已经包含主要传播路径继续增加 K 会导致过平滑。过平滑的表现是随着层数或阶数增加所有节点特征趋于相似模型的预测结果变成几乎相同的常数路段间的差异被磨平。在实际项目中我通常用两层 GCN每层 K2。如果增大 K 到 3模型参数量没有变化但计算量增加训练收敛变慢。如果你想捕获更远处的交通溢出效应不如在输入特征中增加上游路段的滞后流量让时序模型去学习延迟而不是一味扩大 K。比较稳妥的做法是设置一个 K 的候选集合例如 [1,2,3]用验证集上的 MAE 做早停选择。4.3 输入时间窗口长度与预测步长的比例图卷积只捕捉空间关系时间窗口还是要靠模型的输入特征维度来定义。窗口选多长取决于两个因素交通流的周期性和数据采样间隔。如果你的数据是 5 分钟粒度预测未来 15 分钟即 3 步输入窗口用 12 步1 小时比较合适。窗口太短缺乏早高峰的上升趋势信息窗口太长模型参数量增加而且历史久远的信息可能产生干扰。下表是常见参数配置的参考基于 5 分钟粒度数据预测目标输入步数图卷积层数K 阶隐藏向量维度未来 1 步5 分钟6-121-21-216-32未来 3 步15 分钟12-242232-64未来 6 步30 分钟24-482-32-364-128需要注意预测步长越长输入窗口也要相应拉长但不能等比例延长。如果输入窗口超过 2 小时会引入跨周期的重复模式模型会把昨天早高峰信息与当前状态混在一起这时候需要用外部特征或周期特征来补充而不是单纯堆长窗口。4.4 训练时如何验证空间泛化能力交通流量预测最常见的评价指标是 MAE 和 RMSE。但除了整体误差还要关注每个节点的误差分布。我会按照节点流量大小分成 3 组分别计算 MAE。如果你发现大流量节点误差极小、小流量路段误差巨大说明损失函数被大流量主导需要调整。另一个陷阱是时间划分的随机性交通数据有强烈的时间趋势随机打乱样本会让模型看到未来信息必须在时间上顺序划分训练集和验证集例如前 7 天训练第 8 天做验证。5. 自适应图卷积与多步预测输出让真实路网跑得更稳的技巧5.1 避免静态邻接矩阵带来的误差积累前文实现的 GCN 使用的是静态邻接矩阵拓扑在整段时间内不变。但真实交通中的空间依赖会变化早晚高峰时段通勤走廊上的依赖强度明显高于平峰事故发生时上游来车方向的关联权重需要临时增强。这种动态性静态图表达不了。常见做法有两种一种是让图卷积层同时学习一个自适应邻接矩阵例如通过节点嵌入相乘得到初始邻接矩阵再与原始物理邻接矩阵加权求和emb1 nn.Parameter(torch.randn(num_nodes, 10)) emb2 nn.Parameter(torch.randn(num_nodes, 10)) adaptive_adj torch.softmax(torch.relu(torch.matmul(emb1, emb2.T)), dim-1) final_adj alpha * normalized_adj (1 - alpha) * adaptive_adjalpha是平衡系数可以设置为可学习参数或经验值 0.5。注意softmax在节点维度上的作用是让每个节点的邻居权重和为 1这有助于稳定训练。这种自适应图不会破坏路网的真实拓扑只是额外补充了数据中隐藏的空间依赖例如短距离绕行关系、高架与地面道路的直接相互作用等。另一种做法是使用注意力机制计算每条边的动态权重即图注意力网络GAT。GAT 会对每一对相邻节点计算注意力系数这比自适应邻接矩阵更灵活但计算量也更高。如果数据量不大GAT 容易过拟合通常 GCN 加上动态输入特征就足够了。5.2 多步预测的两种输出策略预测未来 30 分钟或 1 小时需要输出多个时间步。两种常用策略直接多步预测一次性输出所有预测步和递归多步预测把上一步预测当作下一步输入。前者训练简单但输出之间独立缺乏时序连贯性后者能模拟真实传播过程但误差会逐步放大。实际应用中我倾向于建立序列到序列模型编码器用 GCN 对历史窗口的空间特征进行编码解码器使用 LSTM 或 GRU 逐步输出未来每个时间步的流量。其中每一步解码时同样使用 GCN 更新空间特征。这种架构既保留了图卷积的空间感知又利用循环网络处理时序依赖。如果项目迭代时间紧张可以用直接多步输出加上一个时间平滑正则项强制相邻预测步之间的变化不要过于陡峭。具体来说在损失函数中增加一项预测值的一阶差分平方和smooth_loss torch.mean((pred[:, :, 1:] - pred[:, :, :-1]) ** 2) total_loss mse_loss lambda_smooth * smooth_losslambda_smooth建议设置在 0.1 到 0.5 之间。过大会让预测曲线过于平缓跟不上流量突变过小则预测步之间容易出现跳变。5.3 用预测区间代替单点预测验证模型稳定性最后分享一个实用的验证技巧不要只关心 MAE还要看你预测值在小流量时段是否出现负值。如果数据归一化后没有被正确反标准化或者 ReLU 没有用在输出层很小的回归误差会导致负流量这在应用层无法解释。更合理的做法是输出层先经过 sigmoid 或 softplus 激活确保输出为非负。softplus 的公式是 log(1exp(x))训练初期梯度较小但能有效避免负值。对于关键路段的事件预警我通常会给模型增加一个蒙特卡洛 dropout在推理时打开 dropout进行 20 到 50 次前向计算预测结果的均值与标准差。标准差作为不确定性估计当某条路段预测流量的置信区间异常宽时说明该节点的历史模式不稳定此时建议结合上下游状态进一步判断。这个技巧不需要改模型结构只在推理前把model.train()打开并在循环里收集输出是一种零成本的不确定性探索方式。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进