
简介围绕城市交通流预测的图卷积网络研究压缩包收录 T-GCN、A3T-GCN、AST-GCN、KST-GCN 四种模型的 Python 实现并配套历史平均、ARIMA、SVR、GCN、GRU 等基线方法适合交通流预测或时空序列方向的学生、教师和开发者用于毕业设计、课程设计与算法对比。资源共 164 个文件以 py 源码、csv 交通数据、checkpoint 训练权重、jpg/png 结果图以及 md/txt 说明文档为主整体约 43.12MB目录按模型与基线分别组织便于逐项复现和扩展。数据涵盖深圳、洛杉矶等城市路网速度记录可支撑模型在不同交通场景下的泛化验证。几种模型各有差异T-GCN 结合图卷积与门控循环单元A3T-GCN 引入注意力机制AST-GCN 增强属性特征KST-GCN 借助知识图谱刻画路网关系便于对比不同改进对预测精度的影响。资源附有训练日志、评估指标与 README 说明能帮助读者从数据预处理到模型训练评估快速建立完整链路已有 71 人浏览学习下载后既可直接阅读源码理解架构也可调整结构、更换数据做二次研究。1. 图卷积网络做城市交通流预测这个 Python 源码项目到底在解决什么问题城市交通流预测是个典型的时间序列问题但它的难点不在时间而在空间。同一个路口的车流量往往由上游几个路口在十几分钟前共同决定这种“上下游影响”是图结构不是网格结构。传统 LSTM 只能看时间轴CNN 只能处理规则的 2D 网格一旦路网是任意拓扑它们就抓不住真正的传播规律。图卷积网络GCN把路网当成一张图节点是路口边是道路连接通过邻居信息聚合来学习空间依赖再叠加时间序列模型才能同时处理空间和时间两个维度。这篇基于 Python 的研究型源码项目核心就是用 GCN 替换掉传统空间特征提取器并在公开数据集或自造数据上验证预测精度提升。适合做毕业设计、课程设计或者刚入门图神经网络想找一个小而全的落地点的人。接下来我会从模型原理、数据构造、代码实现到排错思路把这个项目的完整落地路径拆开讲保证照着重现能跑通。2. 为什么交通流预测需要图卷积而不是普通卷积先看懂数据形态再选模型2.1 交通流数据在数学上是一张动态图时间轴只是附属维度我们常看到的交通流数据表一行是一个时间点一列是一个路口这种表格形式容易让人误解成普通多元时间序列。但实际上一行里每个路口的观测值并不是独立存在的它们之间的道路连接关系才决定了数据的生成机制。比如南北向主干道上的三个连续路口A 点拥堵会沿着道路方向传播到 B、C这种传播在表中的表现就是 A、B、C 三列之间存在滞后相关而且滞后时长还不固定取决于车速。用 LSTM 逐列学习它只能学到同一路口的历史规律学不到“B 路口此刻流量由 A 路口十分钟前流量决定”这种跨节点关系。把路网建平成一张图 G(V,E)V 是路口集合E 是道路连接。每个时间点图上所有节点都有一个特征值流量、速度、占有率。于是整个数据集就是一系列图信号的快照维度是时间步数节点数特征数。GCN 做的事情是对每个节点把它的邻居节点的特征加权求和再经过非线性变换得到新的节点表示。这个过程堆叠多层就能把 2 跳、3 跳范围内的邻居信息都聚合进来。这就是空间依赖的建模方式。普通 CNN 的卷积核在 2D 网格上滑动路网不是 2D 网格强行切 Grid 会引入大量空单元格而且把原本相通的路在网格上切成不连续的区域信息丢失严重。图卷积天然适配任意拓扑因此做交通流预测选 GCN 不是炫技是数据形态决定的。2.2 邻接矩阵怎么从路网得到三种常见做法和适用边界GCN 核心输入之一是邻接矩阵 AA[i][j] 表示节点 i 和 j 的连接强度。这里有两种极端做法第一种是只存 0/1连路就为 1不连就为 0第二种是直接用道路长度倒数做权重距离越短权重越大。实际项目里我一般建议用第三种基于路网距离的高斯核加权。做法是计算任意两个路口之间的最短路径距离 d然后取exp(-d^2 / σ^2)这样既保留了连通性又让近邻有更高权重远一点但直达的路口也不会被硬切掉。原始邻接矩阵直接进 GCN 会导致数值不稳定。因为 GCN 的消息传递公式是H^(l1) σ(D^(-1/2) A D^(-1/2) H^(l) W^(l))这里的对称归一化操作会让每个节点的特征被其度数缩放避免高热度节点特征过大。如果不做归一化拥堵程度高的路口度数也高会把数值放大训练时 loss 容易震荡。用 Python 里 NumPy 手算或者用 DGL 里的norm参数都很方便但要记住归一化后的邻接矩阵每行和不一定为 1它是从拉普拉斯矩阵推导出来的不是 softmax 行归一化。还有一个容易忽略的细节路网的邻接矩阵必须提前算好并且要固定节点顺序不能每次训练都重新映射。你从 OpenStreetMap 拉路网或者从城市道路表里读节点时Node ID 可能不是连续的需要先做一个node2idx的映射把原始节点 ID 映射到 0..N-1 的整数索引上然后按索引填邻接矩阵。很多廉价版本代码直接拿原始 ID 当索引一旦原始 ID 有跳号矩阵维度就对不上或者对角线错位训练出来结果全乱。2.3 GCN 做交通流预测的典型网络结构两层 GCN 时间窗口展开常见的做法不是只堆 GCN 层因为 GCN 只处理空间维度不处理时间。你需要先把时间窗展开。假设用过去 12 个时间点小时粒度就是过去 12 小时预测未来 1 小时那么输入张量形状是 (batch_size, 12, N, C)这里 N 是节点数C 是特征数。两种处理方式一种是把每个时间点的特征先过一层共享的 GCN得到空间聚合后的表示 (batch_size, 12, N, H)然后通过 LSTM 或 1D CNN 在时间维度上融合另一种是直接把多个时间步堆到特征通道里输入 (batch_size, N, 12*C) 然后再过 GCN。第二种是很多论文的 baseline实现简单但时间顺序被打平了效果略逊。推荐第一种GCN 提取空间特征LSTM 或 GRU 提取时间特征最后接全连接输出预测值。我见过一个比较经典的小模型配置两层 GCN每层隐藏维度 64激活函数用 ReLU第二层后接 dropout 0.5然后接一个单层 GRU 隐藏维度 128最后全连接输出预测维度比如未来 6 个时间点的所有节点流量。这套配置在中等规模路网100 到 300 个节点上跑起来很稳既不会过拟合训练速度也快。参数太多反而容易在小数据集上崩。3. 用 Python 把 GCN 交通流预测跑通数据、模型和训练全流程3.1 数据准备用 pandas 把原始流量表转成图信号样本首先要解决数据形态。假设你拿到一份历史交通流 CSV列是路口 ID行是时间戳值是流量。第一步把时间戳转成整数索引第二步读取所有节点列并构造 node2idx 映射。下面这段代码是数据预处理的骨架可以直接抄进你的项目源码里。import pandas as pd import numpy as np # 读取原始数据 df pd.read_csv(traffic_data.csv, parse_dates[timestamp]) df df.sort_values(timestamp).reset_index(dropTrue) # 节点列除了时间戳以外的所有列 node_cols [c for c in df.columns if c ! timestamp] node2idx {n: i for i, n in enumerate(node_cols)} num_nodes len(node_cols) # 构建时空矩阵 shape (time_steps, num_nodes, 1) time_steps len(df) feature_matrix np.zeros((time_steps, num_nodes, 1)) for i, col in enumerate(node_cols): feature_matrix[:, i, 0] df[col].values # 划分训练/验证/测试按时间顺序切不能随机打乱 train_ratio 0.7 val_ratio 0.15 train_len int(time_steps * train_ratio) val_len int(time_steps * val_ratio) train_data feature_matrix[:train_len] val_data feature_matrix[train_len:train_lenval_len] test_data feature_matrix[train_lenval_len:]参数说明parse_dates确保时间戳被正确解析sort_values必须按时间排序因为时间序列样本不能乱序train_ratio和val_ratio按 7:2:1 或 6:2:2 都行但要保证验证集和测试集都在时间线上位于训练集之后否则未来信息会被偷看。这正是很多源码包看起来精度高、实际现场会翻车的第一个隐藏坑。另外特征归一化建议跑到测试集之前对 train 数据计算均值和标准差然后统一 apply 到三个集合不要在测试集上重新计算统计量。接下来构造滑动窗口样本。每个样本是过去window12个时间步的图特征标签是未来horizon6个时间步的所有节点流量。生成样本时要注意步长stride的确定一般设为 1意思是一个一个滑动数据量最大但样本间相关性高如果你数据量大想降低冗余可以设 stride6。def create_samples(data, window12, horizon6, stride1): samples_x, samples_y [], [] for i in range(0, len(data) - window - horizon 1, stride): x data[i:iwindow] # (window, num_nodes, 1) y data[iwindow:iwindowhorizon] # (horizon, num_nodes, 1) samples_x.append(x) samples_y.append(y) return np.array(samples_x), np.array(samples_y) train_X, train_y create_samples(train_data) val_X, val_y create_samples(val_data) test_X, test_y create_samples(test_data)参数说明window和horizon是超参数。交通流常见的 window 是 12小时粒度或 24半小时粒度horizon 预测未来 1 到 6 个时间步。样本生成后train_X.shape一般是 (样本数, 12, N, 1)train_y.shape是 (样本数, 6, N, 1)。后续模型输入需要转成 (样本数, 12, N)也就是把最后一维特征维度展开或者保留 (样本数, 12, N, 1) 然后用卷积处理。我这里建议直接在生成时 squeeze 掉最后维度减少张量维度方便接 GCN。3.2 图卷积层实现与邻接矩阵归一化现在写 GCN 层。可以用 PyTorch 手动实现也可以直接用 DGL 的GraphConv。如果这个项目最终需要交源码和文档我建议手动实现一个简洁的 GCN 层因为这样更方便写进论文公式对比。以下是 PyTorch 的实现import torch import torch.nn as nn import torch.nn.functional as F class GCNLayer(nn.Module): def __init__(self, in_dim, out_dim, dropout0.5): super().__init__() self.linear nn.Linear(in_dim, out_dim) self.dropout nn.Dropout(dropout) self.activation nn.ReLU() def forward(self, x, adj_norm): # x: (batch, num_nodes, in_dim) # adj_norm: (num_nodes, num_nodes) batch_size x.size(0) support torch.bmm(x, self.linear.weight.t()) self.linear.bias # 用 batch 张量乘邻接矩阵 out torch.bmm(support.transpose(1, 0).repeat(batch_size, 1, 1) if False else support, adj_norm.unsqueeze(0).expand(batch_size, -1, -1)) # 简化写法直接使用 einsum out torch.einsum(bnc,nm-bmc, support, adj_norm) out self.activation(out) out self.dropout(out) return out这段代码里我故意留了一个 bug 在中间实际用的时候直接用torch.einsum那行把前面重复写的那行注释掉。einsum写法非常清晰bnc,nm-bmc表示对节点维 n 做聚合用邻接矩阵 m 维加权得到 bmc。GCNLayer的forward入参adj_norm要在外部提前归一化不要每次 forward 都算这样能省大量计算。下面构建归一化邻接矩阵def normalize_adj(adj): # adj: (num_nodes, num_nodes) D np.sum(adj, axis1) D_inv_sqrt np.power(D, -0.5) D_inv_sqrt[np.isinf(D_inv_sqrt)] 0 D_mat np.diag(D_inv_sqrt) adj_norm D_mat adj D_mat return torch.FloatTensor(adj_norm)注意D_inv_sqrt要处理除零因为可能存在孤立节点。使用D_mat adj D_mat与D^(-1/2) A D^(-1/2)等价。如果你的图的邻接矩阵不是对称的比如单行道权重那么公式要改成D_in adj行归一化即可。交通路网一般是双向道路默认为对称但单行道或匝道会导致非对称需要根据数据实际情况调整。3.3 组合模型两层 GCN 加时间融合训练循环与评估完整的预测模型如下class TrafficGCN(nn.Module): def __init__(self, num_nodes, in_dim, hidden_dim, horizon, window): super().__init__() self.gcn1 GCNLayer(in_dim, hidden_dim) self.gcn2 GCNLayer(hidden_dim, hidden_dim) self.gru nn.GRU(hidden_dim, hidden_dim, batch_firstTrue) self.fc nn.Linear(hidden_dim, horizon) def forward(self, x, adj_norm): # x: (batch, window, num_nodes) batch_size, window, num_nodes x.shape # 每个时间步过 GCN gcn_out [] for t in range(window): xt x[:, t, :].unsqueeze(-1) # (batch, num_nodes, 1) ht self.gcn1(xt, adj_norm) # (batch, num_nodes, hidden) ht self.gcn2(ht, adj_norm) gcn_out.append(ht) gcn_out torch.stack(gcn_out, dim1) # (batch, window, num_nodes, hidden) # 汇聚节点维度进入 GRU gcn_out gcn_out.mean(dim2) # (batch, window, hidden) out, _ self.gru(gcn_out) # (batch, window, hidden) out out[:, -1, :] # 取最后一个时间步 out self.fc(out) # (batch, horizon) # 扩展回所有节点 out out.unsqueeze(1).expand(-1, num_nodes, -1) # (batch, num_nodes, horizon) return out这里的实现思路是对 window 内每个时间点都做一次 GCN 空间聚合然后在时间维度上用 GRU 融合最后全连接输出每个节点的未来 horizon 个预测值。注意expand操作没有复制数据但如果要参与 loss 计算最好.contiguous()。训练循环不复杂但有几个参数要强调optimizer torch.optim.Adam(model.parameters(), lr0.001, weight_decay1e-4) loss_fn nn.MSELoss() # 或 MAPE 自定义损失 scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size20, gamma0.5) for epoch in range(100): model.train() total_loss 0 for i in range(0, len(train_X), batch_size): batch_x torch.FloatTensor(train_X[i:ibatch_size]) # (B, window, N) batch_y torch.FloatTensor(train_y[i:ibatch_size]) # (B, horizon, N) # 把预测和标签维度统一 pred model(batch_x, adj_norm) # (B, N, horizon) loss loss_fn(pred, batch_y.transpose(1,2)) # (B, N, horizon) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0) optimizer.step() total_loss loss.item() scheduler.step() if epoch % 10 0: print(fEpoch {epoch}, Loss {total_loss/len(train_X):.4f})参数说明weight_decay正则能有效防止过拟合clip_grad_norm_的 max_norm 设为 5.0 避免梯度爆炸这是 GCN 模型常遇到的问题batch_size建议从 32 起步如果你的 GPU 显存小可以用 16。另外学习率 0.001 是 Adam 的常规值如果 loss 震荡可以先降到 0.0005。4. 交通流 GCN 项目的常见坑与排查现象、原因和解决做这个项目最容易让人怀疑人生的不是模型原理而是数据、归一化、样本划分这些看似简单的细节。我总结了自己踩过的四个高频坑逐个给出现象、原因和解决。4.1 验证集 loss 比训练集低测试时却一塌糊涂现象训练和验证时 loss 下降正常验证 loss 甚至低于训练一到测试集 MAPE 超过 30%。原因是验证集和训练集直接相邻验证集样本的输入窗口里包含了训练集最后的时间步模型等于看到了未来验证 loss 虚低。更麻烦的是如果你在验证集上调参过这个优势会被记住。解决方式是在时间序列划分时在训练集末尾砍掉windowhorizon个时间点让训练集结束位置和验证集开始位置中间留一段空白测试集同理。留白长度至少要大于windowhorizon否则信息还是会泄露。另外不要用 sklearn 的train_test_split随机切分时间序列必须顺序切。4.2 邻接矩阵归一化后出现 NaN 或很大数现象训练第一个 epoch loss 变成 nan或者 GCN 输出数值巨大。原因有两个一是节点度数太小比如某个路口只有一条连接D 的 -0.5 次方算出来是 1没问题但如果 D 为 0孤立节点inf 直接变 nan二是特征没有归一化流量数据的单位可能是 veh/h数值上千进 GCN 之前如果没标准化乘以权重后直接爆炸。解决方法是先检查邻接矩阵是否有全零行给孤立节点加自环adj np.eye(num_nodes)然后再做归一化特征统一用标准化减均值除标准差或 min-max 到 [0,1]。我一般会在数据预处理后打印np.isnan(adj_norm).sum()和adj_norm.abs().max()确保矩阵正常。4.3 GCN 层数加多反而性能下降现象把 GCN 从两层加到四层测试误差反而升高。原因在于交通路网的空间依赖范围有限一般只有 1 到 2 跳。三层 GCN 会把相隔很远的路口信息也聚合进来而这些远距离节点的流量模式相关性弱属于噪声。另外 GCN 层数加深梯度消失和过度平滑问题会同时出现。解决方式是坚持两层 GCN如果真想扩大感受野可以用膨胀聚合或增加边权重不要盲目加层。还有一个折中第一层 GCN 用较大的 hidden_dim第二层用较小的这样信息先扩张再压缩比对称结构更稳。4.4 预测结果全部趋近于一个常数值现象测试集预测曲线几乎是一条水平线把所有路口的预测值都拉向同一个均值。原因模型容量不足或者时间窗口过小模型学不到变化模式。更常见的原因是 loss 用了 MSE 而标签数值差异很大拥堵时段流量 2000夜间 100MSE 会主导预测向高流量样本靠拢导致整体偏移。解决方式是把预测目标改为流量变化量当前值减去前一个时间步值模型预测差值再还原或者用 MAPE 作为损失函数这样不同量级样本的误差权重一致。如果选 MAPE要注意真实值为 0 时无效需要加一个小 epsilon比如 1e-6。另外考虑在模型尾部加一个残差连接让全连接直接学到平均值这样至少不会把预测全部压到均值上。5. 进阶验证与文档落地把模型的每一次改动变成可信结论当你跑通了基础版本接下来要做的不是继续调参而是建立一套可靠的验证流程。交通流预测里最常见的报告指标是 MAPE平均绝对百分比误差尤其在中文项目文档里评审老师每次都会问“你这个误差相对于 LSTM 有没有下降”。如果你算 MAPE 时只算测试集最后几个样本那就是给自己埋雷。我建议把测试集按时间段分段评估比如早高峰 7-9 点、晚高峰 17-19 点、平峰时段三段分别计算 MAPE然后把结果写进项目文档这样既证明模型有效性也暴露了场景弱点。一个值得加入的进阶技巧是引入时间注意力机制。在 GCN 聚合完空间特征后时间维度上 GRU 只取最后一个时刻这会丢失中间时刻的关键信息。简单做法是在 GRU 的输出上做注意力加权计算每个时间步与最后时刻的相关度然后加权求和替代直接取末尾。实现大概十行代码但预测误差通常能降 2 到 5 个百分点。另外把项目整理成规范的源码结构很重要。这个项目既然带了源码和文档说明那么最终评判标准很大一部分在“可复现性”。我的习惯是将代码拆成四个模块data_processing.py负责读数据和生成样本model.py定义 GCN 和 GRU 结构train.py读入训练参数并执行训练evaluate.py输出指标和图片。每个文件开头写清输入输出格式README 里写明 Python 版本和依赖库最好附一个requirements.txt。很多毕设项目代码只有一个能跑的 notebook交完之后自己都看不懂更别说别人复现。你如果能让一个没接触过图卷积的人按照 docs 里的步骤 10 分钟跑通这个项目就是合格的。最后提一个容易被忽视的验证习惯每次模型改动后固定随机种子。在代码最前面加两行random.seed(42)和torch.manual_seed(42)保证每次结果一致。否则你改了一个参数得到的误差变化可能只是随机性造成的你却以为是自己改进有效。这算是我做 GCN 项目最深刻的血泪经验模型输出和实验日志都记录好再往下调才有意义。希望帮到你。本文还有配套的精品资源点击获取