GCN交通流量预测实战:从路网建图到时空模型源码
简介这份资源是面向机器学习初学者与高校学生的GCN图神经网络交通流量预测完整项目源码可作为Python期末大作业、课程设计或自学练手素材。项目以图卷积网络为核心结合GAT、ChebNet等模型对PeMS04交通数据集进行建模预测帮助读者理解图神经网络在时序交通场景中的落地方式。压缩包共16个文件约33.61MB包含7个Python源码文件、3张结果可视化图片、1个npz数据文件、1个h5模型权重、1个csv数据集以及README说明与LICENSE协议代码附有注释结构清晰便于新手阅读与部署。目前已有245人学习下载。读者可从中获得完整的交通流量预测方案、图神经网络模型实现细节、数据处理与训练脚本、可视化结果及排错思路适合需要快速完成大作业或入门图神经网络应用的读者参考。1. 交通流量预测遇上 GCN为什么路网数据不能直接塞进 LSTM做交通流量预测的人大多踩过同一个坑把某条路的历史流量序列丢进 LSTM单点预测看着还行一旦要预测整片路网误差就崩了。原因不复杂——路网不是一堆独立的序列相邻路口的车流会互相传导上游堵了下游几分钟后才堵这种空间依赖关系单靠时间序列模型根本抓不住。GCN 图神经网络的价值就在这里它把路网建成一张图节点是检测器或路口边是路段连接关系让模型在学时间模式的同时也学空间上的流量传播。这个项目标题指向的就是这套思路的完整落地用 GCN 处理路网的空间结构配合时间模块做交通流量预测并给出可跑的源码。它适合两类人——一类是刚入门图神经网络、想找一个真实场景练手的机器学习学习者另一类是手上有交通数据、想验证 GCN 到底比传统方法强多少的从业者。读完你应该能自己搭出数据、模型、训练、评估这条链路而不是只会调包。2. 把路网变成图GCN 交通流量预测的数据准备与邻接矩阵构建2.1 交通流量数据的三种常见格式与选型交通流量预测的数据来源五花八门但落到建模常见就三种格式。第一种是长表格式每行是「时间戳、检测器ID、流量」最贴近原始采集适合做清洗和聚合。第二种是宽表格式行是时间戳列是检测器ID每个格子是流量值直接喂给模型最方便。第三种是张量格式形状为(时间步, 节点数, 特征数)是 GCN 类模型的标准输入。我一般会先把长表聚合成宽表再滑窗切成张量。这里有个容易忽略的点交通流量数据缺失非常普遍检测器掉线、通信中断都会造成空值。直接填 0 会让模型以为那个时刻真的没车正确做法是用前后时刻的均值或同一时刻相邻检测器的均值来补。下面这段代码演示从长表到张量的完整转换。import pandas as pd import numpy as np # 假设 raw 是长表timestamp, sensor_id, flow raw pd.read_csv(traffic_raw.csv, parse_dates[timestamp]) # 1. 聚合成宽表行时间列检测器 wide raw.pivot_table( indextimestamp, columnssensor_id, valuesflow, aggfuncmean ) # 2. 缺失值处理先按时间前后线性插值再按检测器横向补 wide wide.interpolate(methodlinear, limit_directionboth) wide wide.fillna(wide.mean()) # 3. 滑窗切张量用过去 12 个时刻预测未来 1 个时刻 def make_windows(arr, in_steps12, out_steps1): x, y [], [] for i in range(len(arr) - in_steps - out_steps 1): x.append(arr[i : i in_steps]) y.append(arr[i in_steps : i in_steps out_steps]) return np.array(x), np.array(y) values wide.values.astype(np.float32) # 标准化按列做 z-score避免不同检测器量级差异 mean, std values.mean(axis0), values.std(axis0) 1e-6 values (values - mean) / std X, Y make_windows(values, in_steps12, out_steps1) print(X.shape, Y.shape) # (样本数, 12, 节点数) (样本数, 1, 节点数)这段代码的关键参数有三个。in_steps12表示用过去 12 个时间片如果数据是 5 分钟粒度就是过去 1 小时out_steps1是预测下一步做多步预测就把它调大。标准化必须按列做因为不同路段的流量基数可能差十倍不归一化会让模型偏向大流量节点。limit_directionboth保证首尾的缺失也能被补上否则滑窗会在边界处丢样本。2.2 邻接矩阵怎么建距离阈值法 vs 相关性法GCN 的核心输入除了特征矩阵还有邻接矩阵 A。它决定了节点之间怎么传消息。交通场景里建 A 有两条主流路线。第一条是距离阈值法两个检测器地理距离小于阈值就连边权重常用高斯核exp(-d²/σ²)。第二条是相关性法用历史流量序列算皮尔逊相关系数超过阈值就连边。前者反映物理连通性后者反映流量联动性实际项目里我倾向于两者融合。from scipy.spatial.distance import cdist # coords: (节点数, 2) 的经纬度或平面坐标 def build_adj_distance(coords, sigma1.0, threshold0.5): dist cdist(coords, coords, metriceuclidean) adj np.exp(-dist ** 2 / (sigma ** 2)) adj[adj threshold] 0 # 弱连接直接切断减少噪声 np.fill_diagonal(adj, 1.0) # 自环保留自身信息 return adj # 相关性邻接values 是标准化后的 (时间, 节点) def build_adj_corr(values, threshold0.6): corr np.corrcoef(values.T) corr[corr threshold] 0 np.fill_diagonal(corr, 1.0) return corr # 融合加权平均后重新归一化 adj 0.5 * build_adj_distance(coords) 0.5 * build_adj_corr(values)sigma控制距离衰减速度取值要参考你的坐标单位如果坐标是经纬度sigma 设 0.01 量级才合理设 1.0 会让所有节点都强连接。threshold是稀疏化门槛交通图不宜太稠密一般保留每个节点 5 到 10 个邻居效果最好。融合时权重 0.5/0.5 是起点如果数据里物理距离和流量相关性差异大可以调成 0.7/0.3 试试。建完 A 后一定要做对称归一化D^(-1/2) A D^(-1/2)否则度数大的节点会在聚合时主导输出。3. GCN 交通流量预测模型搭建从图卷积层到时空模块3.1 图卷积层为什么用切比雪夫近似而不是傅里叶变换原始谱图卷积要做特征分解节点上千时计算量爆炸。实际工程里几乎都用切比雪夫多项式近似把卷积核限制在 K 阶邻居内复杂度降到线性。K1 时就是最常见的 GCN 形式每层只聚合直接邻居。交通流量预测里 K 一般取 2 到 3因为流量影响通常传两三个路口就衰减了取太大反而引入远端噪声。import torch import torch.nn as nn class GCNLayer(nn.Module): def __init__(self, in_dim, out_dim): super().__init__() self.linear nn.Linear(in_dim, out_dim) def forward(self, x, adj_norm): # x: (batch, 节点数, 特征) adj_norm: (节点数, 节点数) support self.linear(x) # 邻居聚合adj_norm 已做对称归一化 out torch.einsum(nm,bmf-bnf, adj_norm, support) return torch.relu(out)einsum那行就是图卷积的核心对每个节点把它的邻居特征按归一化权重加权求和。adj_norm必须提前算好并转成 tensor不要在每个 batch 里重算。如果显存吃紧可以把adj_norm转成稀疏张量用torch.sparse.mm替代einsum节点数超过 500 时提速明显。3.2 时空块组合GCN 抓空间GRU 抓时间纯 GCN 只能处理单帧图交通流量是时序问题必须叠时间模块。常见组合有两种GCN GRU或者 GCN 一维卷积。前者适合捕捉长时依赖后者训练更快。我一般先用 GCNGRU 打基线效果不够再换注意力机制。class STGCNBlock(nn.Module): def __init__(self, feat_dim, hidden_dim, adj_norm): super().__init__() self.register_buffer(adj, adj_norm) self.gcn GCNLayer(feat_dim, hidden_dim) self.gru nn.GRU(hidden_dim, hidden_dim, batch_firstTrue) def forward(self, x): # x: (batch, 时间步, 节点数, 特征) b, t, n, f x.shape x x.reshape(b * t, n, f) x self.gcn(x, self.adj) # 空间聚合 x x.reshape(b, t, n, -1) x x.permute(0, 2, 1, 3).reshape(b * n, t, -1) out, _ self.gru(x) # 时间建模 out out[:, -1].reshape(b, n, -1) # 取最后时刻 return outregister_buffer把邻接矩阵注册成模型的一部分这样保存和加载模型时 A 会跟着走不用额外传参。reshape和permute的顺序是这类模型最容易写错的地方GCN 要求输入是(batch, 节点, 特征)GRU 要求(batch, 时间, 特征)中间必须把节点维和时间维正确交换。训练时如果 loss 不下降先检查这两步维度有没有对错。3.3 训练循环与关键超参设置模型搭好后训练循环本身不复杂但几个超参直接决定成败。学习率用 1e-3 起步配合ReduceLROnPlateau在验证 loss 停滞时减半。batch size 交通数据一般取 32 或 64太大泛化变差。损失函数用 MAE 比 MSE 更稳因为交通流量有尖峰MSE 会被异常值带偏。model STGCNBlock(feat_dim1, hidden_dim64, adj_normadj_tensor).cuda() optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience5) criterion nn.L1Loss() for epoch in range(100): model.train() for xb, yb in train_loader: xb, yb xb.cuda(), yb.cuda() pred model(xb) loss criterion(pred, yb.squeeze(1)) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0) optimizer.step() # 验证 model.eval() with torch.no_grad(): val_loss sum(criterion(model(x.cuda()), y.cuda().squeeze(1)) for x, y in val_loader) / len(val_loader) scheduler.step(val_loss)weight_decay1e-4是轻量正则交通数据噪声大不加容易过拟合。clip_grad_norm_阈值 5.0 防止梯度爆炸GCN 叠 GRU 时梯度不稳是常事。验证 loss 用整个验证集平均不要只看最后一个 batch否则 scheduler 会被噪声误导。4. 训练完就完事GCN 交通流量预测的评估与调参避坑4.1 评估指标别只看 RMSE交通流量预测的评估RMSE 和 MAE 是基础但只看这两个会漏掉关键问题。我习惯再加两个指标MAPE 看相对误差尤其关注低流量时段因为低流量时绝对误差小但相对误差可能很大还有峰值误差单独统计早晚高峰时段的预测偏差。很多模型整体 RMSE 好看一到高峰就崩这种模型上线就是灾难。指标计算方式关注场景MAE绝对误差均值整体稳定性RMSE均方误差开根对大误差敏感MAPE相对误差均值低流量时段峰值MAE高峰时段MAE拥堵预测评估时一定要把标准化反变换回去再算指标在标准化空间算出来的数字没有物理意义也没法和别人的结果对比。4.2 避坑GCN 交通流量预测最常见的五个翻车点现象一loss 降到某个值就不动了。原因通常是邻接矩阵没归一化或者归一化时用了D^-1 A而不是对称形式导致度数大的节点输出爆炸。解决检查adj_norm D^-0.5 A D^-0.5并确认对角线是自环。现象二验证集 loss 比训练集低。这看着反直觉实际多半是验证集样本太少或者验证集的时间段恰好流量平稳。解决按时间顺序切分数据集不要随机打乱训练/验证/测试按 7:1:2 切并保证验证集覆盖高峰和低谷。现象三预测结果整体偏小。常见于用了 ReLU 做最后一层激活流量非负但 ReLU 会把小值压成 0。解决输出层不加激活或者用 Softplus。同时检查标准化时 std 是否过小导致数值被放大。现象四换一批数据就完全不能用。邻接矩阵是硬编码的换了检测器布局 A 就对不上。解决把建图逻辑写成独立函数节点顺序和 A 的行列顺序严格绑定保存模型时一并保存节点 ID 映射表。现象五GPU 显存不够。节点数上千时邻接矩阵是稠密的 N×N显存直接爆。解决转稀疏矩阵或者用邻居采样每个节点只采固定数量的邻居参与聚合。5. 让 GCN 预测更准的两个进阶技巧残差连接与多步预测基础版跑通后想再往上提精度我一般先动两个地方。第一个是给 GCN 层加残差连接。交通流量预测里节点自身的当前状态往往比邻居信息更重要残差能让模型保留自身特征避免多层聚合后自身信息被稀释。实现很简单在GCNLayer.forward里加一行out out support前提是输入输出维度一致。第二个是多步预测的改造。前面out_steps1只预测下一步实际调度需要未来 3 到 6 步。直接把out_steps调大让模型一次输出多个时刻比递归预测更稳因为递归会把误差累积放大。改造时把 GRU 的输出接一个线性层映射到out_steps * 节点数再 reshape 成(batch, out_steps, 节点数)。class MultiStepHead(nn.Module): def __init__(self, hidden_dim, out_steps): super().__init__() self.out_steps out_steps self.fc nn.Linear(hidden_dim, out_steps) def forward(self, h): # h: (batch, 节点数, hidden) out self.fc(h) # (batch, 节点数, out_steps) return out.permute(0, 2, 1) # (batch, out_steps, 节点数)out_steps设 3 到 6 比较合理再大精度会明显下降因为远期流量受太多随机因素影响。训练时多步损失可以直接对所有步求平均也可以给近步更高权重我一般用平均简单且不容易调坏。验证多步预测效果时别只看整体 MAE把每一步单独算出来画条曲线如果第 1 步准、第 3 步崩说明模型没学到长期模式得回去加时间模块的深度或者换注意力。我自己做这类项目最大的教训是数据质量和图结构的重要性远大于模型结构邻接矩阵建错再深的网络也救不回来。每次改完建图逻辑先拿几个已知强相关的路段对一下确认 A 里它们确实连上了再开始训练。希望帮到你。本文还有配套的精品资源点击获取