ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

图卷积网络交通流预测:T-GCN变体源码实战与避坑指南

图卷积网络交通流预测:T-GCN变体源码实战与避坑指南 简介面向城市交通流预测研究的开发者与学生这份资源提供完整的图卷积网络预测项目涵盖时间图卷积网络T-GCN、注意力增强A3T-GCN、属性增强AST-GCN、知识驱动KST-GCN等多种模型源码并集成HA、ARIMA、SVR、GCN、GRU等基线对比方法可系统完成从数据处理、模型训练到效果评估的完整实验流程。压缩包共164个文件约43MB主要包含42个Python脚本、42个CSV交通数据集、18张结果图示、模型权重及Markdown文档目录划分清晰便于按模块查阅与复用。代码已在测试环境验证通过适合计算机、人工智能、自动化等专业用于课程设计、毕业设计或科研入门目前已有71人学习下载下载后还可咨询远程教学。对于希望快速搭建交通流预测模型的读者可直接基于源码改造或参考文档理解图时空网络的实验细节。1. 图卷积网络做城市交通流预测这包Python源码为什么值得跑一遍城市交通流预测这词一出来好多人条件反射想到 LSTM、Transformer但这包源码的主角是图卷积网络GCN和它的时间变体——T-GCN、A3T-GCN、AST-GCN、KST-GCN。先把结论撂这儿路网是天然图结构丢了“节点连通关系”只按序列建模等于把高架和地面混在一起预测误差基本都会更大而 GCN 恰好能把空间依赖直接编码进卷积里这套源码解决的正是“时间序列 空间拓扑”叠加的预测问题。这份下载包不只有 T-GCN 主模型还把 A3T-GCN、AST-GCN、KST-GCN 四个变体全塞进同一个工程附带五组基线历史平均 HA、ARIMA、SVR、GCN、GRU以及深圳和洛杉矶两组真实车速数据。适合准备交通预测方向毕设、课设或者想快速出对比实验的同学不用从零搭基线和数据处理切到哪个模型就是一行配置的事。我拆完源码后的整体印象是它的工程结构把“空间”和“时间”拆得干净每个模型入口基本是独立文件非常适合当脚手架改造成自己的实验。2. 模型选型与时间图卷积网络原理为什么不是纯GCN也不是纯GRU2.1 T-GCN的两种依赖空间关联与时间演化绑在同一循环里交通速度矩阵在任意时刻可以看成一张图节点是路口或路段边是连通关系节点特征是当前时刻的速度。空间依赖就是相邻路段的拥堵会互相传染时间依赖则是早高峰的堵车模式会在过去几个时间步里逐步形成。T-GCN 的做法是把这两种依赖绑成一个时间循环——每个时间步都用图卷积去更新门控循环单元GRU的隐状态而不是先跑完一组 GCN 再单独扔给 GRU。我在复现时会把前向逻辑写成下面这张简化流程class TGCNCell(nn.Module): 一个 T-GCN 单元输入 x_t 和上一时刻隐状态 h_{t-1} 拼起来 先过图卷积提取空间特征再走 GRU 门控更新。 shape 说明 x_t: (batch, num_nodes, input_dim) h: (batch, num_nodes, hidden_dim) adj: (num_nodes, num_nodes) def __init__(self, num_nodes, input_dim, hidden_dim): super().__init__() self.gcn GraphConvolution(input_dim hidden_dim, hidden_dim) self.gru GRUCell(hidden_dim, hidden_dim) def forward(self, x_t, adj, h): combined torch.cat([x_t, h], dim-1) # 拼历史信息 gcn_out self.gcn(combined, adj) # 图卷积捕捉空间依赖 h_new self.gru(gcn_out, h) # 门控更新捕捉时间依赖 return h_new这里最关键的一行是torch.cat([x_t, h], dim-1)。常见误用是只把 x_t 送进图卷积隐状态纯粹走 GRU那样空间信息只作用在输入层没有参与循环的迭代而 T-GCN 的做法是让隐状态本身也经过图卷积空间结构的影响就会被持续“推”进后续时间步。adj邻接矩阵必须做归一化否则图卷积在节点度差异大的路网上会放大某些高架节点的数值。常见做法是D^{-0.5} A D^{-0.5}的对称归一化源码里一般会用稀疏矩阵封装跑大图时显存也更可控。参数上注意input_dim hidden_dim这个拼接维度改 hidden_dim 时两头要同步这也是后面踩坑高频区。2.2 三个变体的差异注意力、属性增强、知识驱动这包源码里真正的财富不是单独一个 T-GCN而是 A3T-GCN、AST-GCN、KST-GCN 三条变体的对照。A3T-GCNATT-TGCN是在时间维度上加了注意力机制T-GCN 每个时间步输出的隐状态都有一份特征但并非所有历史步对预测未来同等重要A3T 会用注意力给不同时间步分配权重相当于把“时间重要性”变成了可学习参数。AST-GCN属性增强的改动在空间侧它把节点自身的属性比如道路等级、车道数、是否为快速路作为增强矩阵拼进图卷积的输入再与邻接矩阵信息融合。这意味着即使两条路在拓扑上不相邻只要属性模式相似卷积过程中也能产生间接交互。KST-GCN知识驱动的时空图卷积网络则是把外部知识显式注入邻接矩阵比如sz_assist_kg.csv里存的节点知识图谱特征。它不是简单拼接而是把知识矩阵作为加权项与原始邻接矩阵相加让模型在卷积时额外“看到”语义层面的关联。实际操作里这个加权系数对收敛影响很大我会在第 5 章专门说。2.3 基线模型的定位HA、ARIMA、SVR、GCN、GRU 对照组怎么用实验论文里不能只有自己模型的曲线必须有公平基线。HA 是历史平均把过去同一时段的交通值取平均当预测逻辑最简单适合当底线参考ARIMA 是经典统计时序模型对平稳性要求高交通流这种强非平稳数据它往往吃得慢但对短时预测还是有一战之力SVR 拿特征工程后的向量做回归非线性拟合能力尚可却不显式利用空间拓扑。GCN 是只做空间卷积不做循环网络GRU 是只做循环不做图卷积。这两个很有价值它们分别验证“只靠时间”和“只靠空间”到底能到多少精度T-GCN 的增益也从差值里来。跑实验时每个基线都要保存预测结果 CSV方便后面画综合对比表。一般我会把 HA 的误差当作“合理性下限”来掂量如果某个变体跑出来比 HA 还差先别急调模型回查数据预处理和邻接矩阵。3. 数据格式与预处理把 sz_speed.csv 和 los_speed.csv 变成图信号3.1 CSV 的真实结构行是时刻列是节点编号下载包里最容易被忽略的文件就是sz_speed.csv和los_speed.csv。这两个数据集的形态是典型的“时间 × 节点”矩阵每一行代表一个采样时刻每一列代表一个路段/检测器编号单元格里是当前时刻该路段的平均速度。打开后你会看到类似下面这样的二维结构import pandas as pd # sz_speed.csv 行列结构检查 df pd.read_csv(sz_speed.csv, index_col0) print(df.shape) # 例如 (1500, 228)1500 个时刻、228 个节点 print(df.iloc[:3, :5]) # 前 3 行 5 列的速度值采样间隔需要单独确认常见数据集是 5 分钟或 15 分钟。这个间隔决定了后面时间窗口的物理含义如果间隔是 5 分钟n_his12代表用过去 1 小时预测未来 5 分钟如果是 15 分钟同样 12 步就是 3 小时。很多同学直接把开源代码的n_his12套到自己的 15 分钟数据上预测的结果其实是用了过于久远的信息我发现这个问题的时候觉得挺坑的。sz_speed.csv和los_speed.csv分别代表深圳和洛杉矶两个城市的道路速度数据节点数量不同、拥堵模式也不同。深圳的早晚高峰很锐利洛杉矶则全天多峰两个数据集的存在意义就是做跨城市泛化验证——同一个模型参数在两套数据上都跑得动才算有说服力。3.2 归一化与时间窗口样本构造分钟的话别选错窗口图卷积网络对输入数值尺度很敏感速度值差异过大时梯度更新不稳所以预处理第一步是归一化。第二步是把连续时间序列切成有监督样本用过去n_his个时间步预测未来n_pred个时间步。这一步的代码常见写法如下import numpy as np def normalize(data): # data shape: (T, N) 时间 × 节点对每一列单独做 min-max 归一化 min_val data.min(axis0, keepdimsTrue) max_val data.max(axis0, keepdimsTrue) norm_data (data - min_val) / (max_val - min_val 1e-8) return norm_data, min_val, max_val def make_samples(data, n_his12, n_pred1, step1): # data shape: (T, N)返回 X: (样本数, n_his, N) 和 Y: (样本数, n_pred, N) X, Y [], [] for i in range(0, len(data) - n_his - n_pred 1, step): X.append(data[i : i n_his]) Y.append(data[i n_his : i n_his n_pred]) return np.stack(X), np.stack(Y)1e-8的加项是用来防除零的。真实数据里部分检测器在凌晨会出现整列全零的情况尤其某些低等级路段如果某个节点在所有时刻的最大值都是 0分母直接爆炸。step1表示滑窗每次移动 1 步样本会高度重叠这会让训练数据量很大但信息冗余资源充裕时把step设成 3 或 5 能显著加速训练且精度损失很小。归一化时要注意保存min_val和max_val这东西就是模型的“后悔药”。测试集预测完必须用同一组 min/max 反归一化回真实速度才能和原始速度算 RMSE如果测试时重新算 min/max等于把测试集信息泄漏进训练过程指标会虚高后面第 5 章展开说。3.3 邻接矩阵构建与sz_assist_kg.csv的知识注入入口图卷积网络没有邻接矩阵就转不起来。源码包里的邻接矩阵通常不是直接给一个现成文件而是要根据路网拓扑自己构造。常见做法是用节点间距离的高斯核两个节点距离越近邻接权值越大若距离超过阈值则置 0。有些数据集也会直接提供连接关系表此时直接把表转成 0/1 矩阵即可。def compute_adjacency(dist_matrix, sigma1.0, threshold0.5): # dist_matrix: (N, N) 节点距离矩阵 # 高斯核加权距离越近权值越大超过阈值则视为不连通 adj np.exp(-dist_matrix ** 2 / sigma ** 2) adj[dist_matrix threshold] 0.0 np.fill_diagonal(adj, 1.0) # 加自环避免梯度在孤立节点上消失 return adj / adj.sum(axis1, keepdimsTrue) # 行归一化sz_assist_kg.csv是 KST-GCN 特有的知识输入内容一般是节点属性和语义关系构成的向量。在模型里它不作为输入特征进入网络而是用于构造知识矩阵去修正邻接矩阵。工程上需要保证它的节点顺序和sz_speed.csv的列顺序一致否则知识矩阵的语义关系会错位——这是 KST-GCN 跑出来不如 T-GCN 的最常见原因不会报错但指标诡异。我自己把邻接矩阵从初始构建到最终确认的完整检查路径固定为查看速度矩阵的列数 → 构造距离矩阵 → 看邻接矩阵的稀疏度 → 检查每一行是否有全零行 → 保存为.npy或稀疏张量。这套流程走完模型训练前的数据链路才算闭合。4. 训练与评估链路从 batch_loss 到 batch_rmse 再到 checkpoint 的完整流程4.1 训练主循环masked RMSE、梯度回传与 batch 指标记录源码训练过程中会产生batch_loss.csv和batch_rmse.csv两个文件前者记录每个 batch 的损失后者记录每个 batch 的均方根误差。这两个文件看起来像监控实际上是判断训练是否正常的第一手资料。核心训练循环大致如下def train_one_epoch(model, train_loader, optimizer, adj, device): model.train() total_loss, total_rmse [], [] for x_batch, y_batch in train_loader: x_batch, y_batch x_batch.to(device), y_batch.to(device) optimizer.zero_grad() out model(x_batch, adj) # (batch, n_pred, num_nodes) loss masked_rmse_loss(out, y_batch) # 只对有效时间步算 RMSE loss.backward() optimizer.step() total_loss.append(loss.item()) total_rmse.append(loss.item() ** 0.5) return np.mean(total_loss), np.mean(total_rmse)masked_rmse_loss的作用是对预测目标做掩码只计算真实数据中非零部分的误差。这个设计针对的是城市速度数据里大量缺失值或无效检测器——如果直接把所有位置都算进 loss空值位置会被模型当作“预测为 0”来学习这对结果影响非常大最终曲线会整体下偏。每次迭代把loss.item()追加进列表跑完整个 epoch 后求均值再写入 CSV。这样得到的batch_loss.csv行数等于 epoch 数画曲线时横轴就是训练轮次。RMSE 是 loss 开根号但在归一化尺度下这个数字没有物理含义只能说它相对下降了多少真的物理 RMSE 要等反归一化之后才算第 6 章讲。4.2 checkpoint 保存逻辑与评估模式切换checkpoint目录保存的是每个 epoch 结束时的模型权重和优化器状态。保存时机和选择标准很讲究不能只看最后一个 epoch 的权重。常见做法是每个 epoch 结束后在验证集上算一次指标如果优于历史最佳才覆盖保存def save_checkpoint(state, is_best, filenamecheckpoint.pt): torch.save(state, filename) if is_best: torch.save(state, checkpoint_best.pt) # 保存验证集最优权重 # 恢复训练时用 torch.load 加 map_location 处理 CPU/GPU 迁移 checkpoint torch.load(checkpoint_best.pt, map_locationdevice) model.load_state_dict(checkpoint[model_state])这个包里 checkpoint 的用途不只是中断恢复更重要的是换数据集评测。你在sz_speed.csv上训练完想直接拿los_speed.csv测试必须把归一化参数随 checkpoint 一起存下来。很多新手只存了模型权重测试集反归一化时不知道用什么 min/max只能重新算或偷偷用测试集统计值属实在制造误差。评估模式的切换也容易翻车。模型在model.train()和model.eval()下行为不同尤其图卷积层里若带了 dropouteval 模式下必须关掉否则每次前向传播的随机性会让预测结果抖动。model.eval() with torch.no_grad(): pred model(x_test, adj) # 推理阶段固定随机性推理阶段把torch.no_grad()包在外面一是不让梯度图累积内存二是速度能快接近一倍这在多次评估时很关键。4.3 不同模型之间切换一个配置项对应一个网络这套源码最有价值的工程点是把四个模型入口统一了。切换时基本就是改一个标志位不用改训练循环。如果你在这套代码里找原型会发现TGCN、A3TGCN、ASTGCN、KSTGCN的 forward 接口是一致的都以(x, adj)为输入以(batch, n_pred, num_nodes)为输出这是典型的研究代码规范。# 模型选择配置示例 MODEL A3TGCN # 可选: TGCN / A3TGCN / ASTGCN / KSTGCN / GCN / GRU if MODEL TGCN: net TGCN(num_nodesN, input_dim1, hidden_dim64) elif MODEL A3TGCN: net A3TGCN(num_nodesN, input_dim1, hidden_dim64, n_his12) # 各模型输出统一为 (batch, n_pred, num_nodes)改成其他模型时只影响net这一行训练循环不需要动。这也意味着你要对比四个模型只需循环跑四次每次输出 batch_loss 和 batch_rmse。注意n_his这个参数A3T-GCN 的时间注意力机制依赖历史步数在切数据集的时候就必须固定改n_his后样本的滑窗宽度要同步更新两处不一致就报维度错误。5. 复现避坑记数据泄漏、归一化、知识矩阵的五个真实踩坑5.1 NaN 出现在第一个 epoch邻接矩阵里有全零行现象是训练日志里第一个 batch 的 loss 就显示nan后续永远拉不回来。原因是邻接矩阵中存在全零行某个节点没有任何邻居也没有自环图卷积在该节点上对邻域求和时结果恒为 0经过梯度回传变成 NaN 传播到全图。解决方法是构造邻接矩阵时强制加自环np.fill_diagonal(adj, 1.0)并且做行归一化。我在跑sz_speed.csv时遇到过几个孤立检测器修正后 loss 立刻正常。这个操作看似小却是我见过最频发的入门级问题。5.2 验证集指标虚高测试时刻和训练时刻重叠现象是验证 RMSE 很低但把预测值画出来和真实曲线在序列首尾有莫名的“接缝式”吻合。原因是切分数据时用了随机打乱的样本划分比如train_test_split(shuffleTrue)同一时间窗的前一半进了训练、后一半进了测试信息发生了跨集泄漏。解决方法是严格按时间顺序切分前 70% 时间片作训练、中间 10% 作验证、最后 20% 作测试。任何网格搜索、模型选型都只允许碰验证集测试集只在最终评估时用一次。5.3 KST-GCN 跑出来不如 T-GCN知识矩阵没有归一化现象是 KST-GCN 作为增强模型反而在深圳数据上比普通 T-GCN 低将近 2 个点。原因是sz_assist_kg.csv生成的辅助矩阵数值范围从 0 到 100 不等直接和邻接矩阵相加后原本 0–1 的邻接关系被知识项淹没图卷积变成了知识网络卷积。解决方法是把知识矩阵也做 min-max 归一化或对称归一化保证其数值范围与邻接矩阵一致并设置一个可学习的加权系数让网络自己决定知识信息含量。从那以后我每次加载知识型数据集都会先单独打印知识矩阵的值分布再进模型。5.4 反归一化后出现负速度测试集重新计算了 min/max现象是预测速度反归一化后出现负值比如 -3.2 km/h而原始数据最小也是 0。原因是推理阶段对测试数据重新调用了normalize()因为测试集范围比训练集小min/max 变了反变换后坐标基准偏移。解决方法是训练时保存训练集的min_val, max_val归一化测试数据和处理预测结果时都使用同一组参数。预测结果经过pred * (max - min) min恢复真实数值而不是重新按测试集范围放缩。5.5 batch_loss 下降但 batch_rmse 发散两个指标统计口径不一致现象是 loss 曲线稳定下探RMSE 曲线上下剧烈抖动甚至上升。原因是脚本里 loss 用的是归一化尺度的 MSERMSE 却在另一段代码里对原始速度值计算两个统计口径不一致导致趋势对不上。解决方法是明确统一如果训练阶段记录 RMSE就基于loss.item() ** 0.5在归一化尺度下记录如果需要物理意义则在 epoch 结束后用验证集整体反归一化再算一次 RMSE。两种口径分开记录、不要混在同一张图里。6. 收敛验证的几个技巧反归一化 RMSE、影子数据与跨数据集迁移6.1 反归一化之后再算 RMSE这是最容易被骗的指标训练过程里不断下降的 loss 只是归一化尺度的中间指标。模型真正的好坏要把预测结果映射回原始速度空间再算误差。我会把评估代码固定成三段式预测 → 反归一化 → 按节点分组统计 RMSE、MAE、MAPE。每一步都有对应的检查点哪一步断了都能立刻发现问题。pred_norm model(x_test, adj) # (batch, n_pred, num_nodes) pred_real pred_norm * (max_val - min_val) min_val rmse_per_node np.sqrt(np.mean((pred_real - y_real) ** 2, axis(0, 1))) print(节点 20 的 RMSE:, rmse_per_node[20], km/h)按节点统计比全局一个数有用得多能看出是高架桥节点预测差还是地面路口预测差从而判断邻接矩阵是否需要修。model.eval() 和 torch.no_grad() 是必须的。6.2 用“影子数据”验证训练回路别等三个小时才确认能跑拿整份深圳数据跑训练要几小时。我在正式跑之前一定会先造一份影子数据取前 500 个时刻、20 个节点跑 2 个 epoch确认 loss 在掉、checkpoint 能保存、batch_rmse 能写文件。验证通过后再跑全量成本相差极大。用这个思路还能顺便验证模型切换配置是否生效——把所有候选模型都用影子数据跑一遍哪条分支报错改起来不用等。对于数据集里节点数多到几百的情况这个技巧几乎能救回一天的调试时间。6.3 把 checkpoint 从一个城市迁移到另一个城市深圳训练好的模型能不能直接拿到洛杉矶数据上测能但有个前提输入节点数必须一致或者做节点筛选。sz_speed.csv的节点数和los_speed.csv不同时我会先做节点对齐保留两个数据集中映射关系明确的路段子集。这样模型输入层权重可以直接复用邻接矩阵则用目标城市自己的拓扑重新计算。迁移测试的目标不是“直接精度高”而是看模型是否具备跨城市的表征能力——如果迁移后绝对误差只比本地训练的模型高出 15% 以内说明模型学到的东西具备普适性这在论文里是一个很有说服力的实验。我的习惯是每次跑新数据集都会强制走一遍完整清单检查邻接矩阵稀疏性、验证归一化参数不泄漏、用影子数据跑两个 epoch、确认吞吐正常再放全量训练。这套流程在多次复现其他开源项目时救过我不下三次。希望帮到你。本文还有配套的精品资源点击获取
返回列表