四类图神经网络交通预测实战:T-GCN/A3T-GCN/AST-GCN/KST-GCN完整复现

📅 发布时间:2026/10/18 10:29:35
四类图神经网络交通预测实战:T-GCN/A3T-GCN/AST-GCN/KST-GCN完整复现
简介本资源聚焦城市交通流预测这一典型时空建模任务提供基于图神经网络的完整研究与实现方案面向计算机、人工智能、交通工程等专业的在校学生、教师及初/中级算法实践者适用于课程设计、毕设选题、科研入门与模型复现。压缩包含164个文件总大小43.12MB涵盖42个Python源码含T-GCN、A3T-GCN、AST-GCN、KST-GCN四大主流模型及HA/ARIMA/SVR/GCN/GRU五大基线实现、42个CSV交通数据集如sz_speed.csv、los_speed.csv等真实路网速度序列、18张JPG/PNG可视化结果图、9个Markdown文档含模型原理说明与实验分析以及训练检查点、损失曲线、配置文件等关键支撑材料。已有71人下载学习所有代码均经实测可运行结构清晰、模块解耦支持开箱即用与二次开发——读者可直接复现论文级对比实验快速掌握时间图卷积网络在交通场景中的数据预处理、图构建、多模型训练与评估全流程。1. 这不是又一个“交通预测 demo”它用真实城市路网数据跑通了四种图神经网络变体连 loss 曲线和 speed 序列都给你存成 CSV 了你手头刚拿到一份标着“T-GCN / A3T-GCN / AST-GCN / KST-GCN”的 Python 项目压缩包解压后看到满屏的sz_speed.csv、los_speed.csv、batch_rmse.csv——别急着删。这不是网上抄来的教学 notebook而是一套完整闭环的交通流预测工程实现从深圳SZ和洛杉矶LOS的真实浮动车 GPS 采样点构建路网图结构到用 GCN 提取空间依赖、用 GRU/TCN 捕捉时间动态再到注意力机制对关键路段加权、知识图谱辅助建模——四个主流 SOTA 模型全在model/下跑通训练且每个模型都自带train.pytest.pyconfig.yamlloss 和 RMSE 不只打印在终端还实时写进batch_loss.csv这种可直接拖进 Excel 画趋势图的文件里。适合谁如果你正卡在“论文公式懂代码跑不起来”这道坎上如果你的毕设需要对比多个 GNN 变体但苦于找不到可复现基线或者你刚接手一个城市级交通调度系统想快速验证图神经网络在短时预测15–60 分钟上的实际效果——这份资源就是你该先 unpack 的那个文件夹。它不教你怎么推导拉普拉斯矩阵但保证你改两行路径、调一个 epoch 数就能在自己笔记本上看到val_rmse: 3.21 km/h跳出来。2. 项目结构与技术栈四类模型如何共存于同一套数据流水线这个项目最值得细看的不是某个模型的炫技而是它如何用一套统一的数据加载图构建评估框架把四种差异巨大的模型“拧”进同一个工程骨架里。所有模型共享data/下的原始数据、utils/中的标准化工具、metrics/里的 MAE/RMSE 计算逻辑真正做到了“换模型不换数据流”。下面拆解核心模块组织方式。2.1 目录结构为什么model/下四个文件夹能各自独立训练├── data/ │ ├── sz_speed.csv # 深圳某高速路网 5 分钟粒度车速128 个传感器 × 17k 时间步 │ ├── los_speed.csv # 洛杉矶高速公路网同类数据207 个节点 × 14k 步 │ ├── sz_assist_kg.csv # 深圳路网知识图谱(road_id, relation, road_attr) 三元组 │ └── adj_mx.pkl # 预计算好的邻接矩阵带距离权重的稀疏矩阵 ├── model/ │ ├── tgcn/ # T-GCNGCN GRU 组合 │ ├── a3t_gcn/ # A3T-GCN在 T-GCN 基础上增加时空注意力门控 │ ├── ast_gcn/ # AST-GCN显式建模空间、时间、属性三重图结构 │ └── kst_gcn/ # KST-GCN引入 sz_assist_kg.csv 构建知识增强图 ├── utils/ │ ├── data_loader.py # 核心支持多源数据拼接、滑动窗口切片、归一化Min-Max Z-Score 可选 │ ├── graph_utils.py # 图构建从 CSV 生成邻接矩阵、计算归一化拉普拉斯 L~ I - D^(-1/2)AD^(-1/2) │ └── trainer.py # 统一训练器封装 optimizer、scheduler、early stopping、checkpoint 保存 ├── metrics/ │ └── evaluation.py # 严格按交通领域标准MAE、RMSE、MAPE且支持 multi-step 输出评估 ├── config/ │ ├── tgcn.yaml # 各模型超参隔离管理lr0.001, seq_len12, horizon3, num_layers2... │ └── default.yaml # 全局配置data_path, device, batch_size, save_dir ├── train.py # 入口python train.py --model tgcn --dataset sz └── test.py # 测试自动加载对应 checkpoint输出预测曲线图 metrics 表格提示adj_mx.pkl是关键预处理产物。它不是简单二值邻接矩阵而是基于道路实际物理距离计算的加权邻接矩阵权重 exp(-distance/σ)graph_utils.py中calculate_adjacency_matrix()函数默认 σ1000 米。如果你替换为自己的路网必须重跑这一步否则 GCN 的空间聚合会失效。2.2 技术栈选型为什么是 PyTorch DGL 而非 TensorFlow 或纯 NumPyPyTorch所有模型均基于torch.nn.Module实现梯度计算、DataLoader并行加载、GPU 加速开箱即用。trainer.py中model.to(device)一行就搞定 CPU/GPU 切换。DGLDeep Graph Library这是本项目区别于多数“手写 GCN 层”的核心。tgcn/model.py中关键代码import dgl from dgl.nn.pytorch import GraphConv class TGCNCell(nn.Module): def __init__(self, in_feats, hidden_size, g): super().__init__() self.g g # DGLGraph 对象已加载 adj_mx.pkl self.gc GraphConv(in_feats, hidden_size, weightTrue, biasTrue) self.gru nn.GRUCell(hidden_size, hidden_size) def forward(self, x, h): # x: (N, in_feats), h: (N, hidden_size) x_gc F.relu(self.gc(self.g, x)) # DGL 自动处理邻居聚合 h_next self.gru(x_gc, h) # GRU 更新隐藏状态 return h_next逻辑说明self.gc(self.g, x)这一行替代了传统手动实现的A X WDGL 内部自动完成稀疏矩阵乘法、邻居索引、消息传递。参数说明in_feats是输入特征维度如 speed flow occupancyhidden_size是 GCN 输出维度g是预构建的 DGLGraph其.ndata[feat]存储节点初始特征。不选 TensorFlow因dgl对 PyTorch 的支持更成熟且trainer.py中的torch.cuda.amp混合精度训练能显著提速尤其在kst_gcn这类大图上。不选纯 NumPysz_speed.csv单文件 120MB17k 时间步 × 128 节点 × float32 约 85MB 内存NumPy 加载切片慢且无法利用 GPU 加速 GCN 矩阵运算。2.3 数据加载流水线utils/data_loader.py如何支撑多模型输入所有模型输入均为(B, N, T_in)形状张量其中Bbatch_size,N节点数,T_in历史时间步长如 12 步 × 5 分钟 60 分钟。DataLoader的核心在于seq2instance()函数def seq2instance(data, num_his12, num_pred3): 将 (N, T_total) 时序数据切分为训练样本 返回x (num_samples, N, num_his), y (num_samples, N, num_pred) num_step data.shape[1] # 总时间步数 num_sample num_step - num_his - num_pred 1 x np.zeros([num_sample, data.shape[0], num_his]) y np.zeros([num_sample, data.shape[0], num_pred]) for i in range(num_sample): x[i] data[:, i:inum_his].T # 注意转置确保 (N, num_his) → (num_his, N) 再转回 (N, num_his) y[i] data[:, inum_his:inum_hisnum_pred].T return x, y参数说明num_his12对应 12 个历史观测点60 分钟num_pred3表示预测未来 3 个时间点15 分钟。x[i] data[:, i:inum_his].T这行是玄学关键——原始data是(N, T)切片后是(N, num_his)但 PyTorch 的nn.Conv1d默认输入是(B, C, L)所以必须.T成(num_his, N)再由DataLoader批处理时自动扩展为(B, N, num_his)。若漏掉.T模型会把“128 个路段”当成“128 个通道”彻底翻车。3. 核心模型代码逐行解析以 A3T-GCN 为例看时空注意力如何落地A3T-GCN 是本项目中最具工程价值的模型——它在基础 T-GCN 上增加了时空双路注意力门控解决了传统模型对突发拥堵事件响应迟钝的问题。我们聚焦model/a3t_gcn/model.py中最关键的SpatialTemporalAttention类看它如何用不到 50 行代码实现论文中的公式。3.1 空间注意力模块给不同路段分配动态权重class SpatialAttention(nn.Module): def __init__(self, in_channels, num_nodes, embed_dim10): super().__init__() self.W1 nn.Linear(in_channels, embed_dim) # (C, d) self.W2 nn.Linear(in_channels, embed_dim) # (C, d) self.W3 nn.Linear(in_channels, embed_dim) # (C, d) self.V nn.Linear(embed_dim, 1) # (d, 1) self.num_nodes num_nodes def forward(self, x): # x: (B, N, C) - 输入特征如 GCN 输出 x1 self.W1(x).transpose(1, 2) # (B, d, N) x2 self.W2(x) # (B, N, d) x3 self.W3(x) # (B, N, d) # 计算注意力分数 S V * tanh(W1*x1 W2*x2 W3*x3) # 先广播相加x1.unsqueeze(2) - (B,d,1,N), x2.unsqueeze(1) - (B,1,N,d) # torch.bmm(x1.unsqueeze(2), x2.unsqueeze(1)) - (B,d,N,d) → 需要 reshape # 实际采用更高效写法 S torch.matmul(x1, x2.transpose(1, 2)) # (B, d, N) (B, N, d) (B, d, d) S torch.matmul(S, x3.transpose(1, 2)) # (B, d, d) (B, d, N) (B, d, N) S self.V(S.transpose(1, 2)).squeeze(-1) # (B, N, d) → (B, N, 1) → (B, N) # softmax 归一化得到空间权重 α ∈ [0,1] alpha F.softmax(S, dim1) # (B, N) return alpha.unsqueeze(-1) # (B, N, 1)逻辑说明该模块不改变特征维度只输出每个节点的权重alpha。后续在 GCN 聚合时用alpha * neighbor_features替代原始求和使模型自动关注高流量或易拥堵路段。参数说明in_channels是输入特征数如 64num_nodes是路网节点数128embed_dim10是注意力嵌入维度过大会导致S矩阵爆炸128×128×10≈160KB实测 10 是平衡效果与内存的临界点。3.2 时间注意力模块捕捉不同时段的关键模式class TemporalAttention(nn.Module): def __init__(self, in_channels, num_steps, embed_dim10): super().__init__() self.W1 nn.Linear(in_channels, embed_dim) # (C, d) self.W2 nn.Linear(in_channels, embed_dim) # (C, d) self.W3 nn.Linear(in_channels, embed_dim) # (C, d) self.V nn.Linear(embed_dim, 1) # (d, 1) self.num_steps num_steps def forward(self, x): # x: (B, N, T, C) - 时间维度在第 2 位 x x.permute(0, 2, 1, 3) # (B, T, N, C) x1 self.W1(x) # (B, T, N, d) x2 self.W2(x) # (B, T, N, d) x3 self.W3(x) # (B, T, N, d) # 计算时间注意力 S_t ∈ (B, T, T) # 先对 N 维度做 mean pooling压缩为 (B, T, d) x1_pooled x1.mean(dim2) # (B, T, d) x2_pooled x2.mean(dim2) # (B, T, d) x3_pooled x3.mean(dim2) # (B, T, d) S torch.matmul(x1_pooled, x2_pooled.transpose(1, 2)) # (B, T, T) S torch.matmul(S, x3_pooled.transpose(1, 2)) # (B, T, T) S self.V(S.unsqueeze(-1)).squeeze(-1) # (B, T, T) → (B, T, 1) → (B, T) beta F.softmax(S, dim1) # (B, T) return beta.unsqueeze(-1).unsqueeze(-1) # (B, T, 1, 1)逻辑说明时间注意力对每个时间步打分beta权重用于加权x的时间维度。注意x.permute(0,2,1,3)这行——原始输入x是(B,N,T,C)必须先转成(B,T,N,C)才能对时间轴做操作。参数说明num_steps12是历史步长embed_dim10与空间注意力一致保证双路注意力计算量对齐。3.3 A3T-GCN 整体前向流程如何串联 GCN、GRU 与双注意力class A3TGCN(nn.Module): def __init__(self, g, in_channels, hidden_size, num_layers2): super().__init__() self.g g self.spatial_attn SpatialAttention(in_channels, g.num_nodes()) self.temporal_attn TemporalAttention(in_channels, 12) # 固定 12 步 self.gcn GraphConv(in_channels, hidden_size) self.gru nn.GRUCell(hidden_size, hidden_size) self.out_proj nn.Linear(hidden_size, 1) def forward(self, x): # x: (B, N, T, C) —— 多步输入C1仅 speed 特征 B, N, T, C x.shape # Step 1: 时间注意力加权 beta self.temporal_attn(x) # (B, T, 1, 1) x_weighted x * beta # (B, N, T, C) * (B, T, 1, 1) → broadcast # Step 2: 对每个时间步单独 GCN再加空间注意力 h_list [] for t in range(T): x_t x_weighted[:, :, t, :] # (B, N, C) alpha self.spatial_attn(x_t) # (B, N, 1) x_gc F.relu(self.gcn(self.g, x_t)) # (B, N, H) x_attended x_gc * alpha # (B, N, H) * (B, N, 1) h_list.append(x_attended) # Step 3: GRU 串行处理时间维度 h torch.zeros(B, N, self.gru.hidden_size).to(x.device) for h_t in h_list: h self.gru(h_t, h) # h_t: (B, N, H), h: (B, N, H) # Step 4: 输出预测 y self.out_proj(h) # (B, N, 1) return y.squeeze(-1) # (B, N)关键设计点双注意力解耦空间注意力作用于单时刻节点特征时间注意力作用于多时刻序列避免联合建模带来的维度灾难GRU 输入非原始 x而是 GCNAttention 后的h_t这确保 GRU 学习的是“经过空间筛选后的时序模式”而非原始噪声y.squeeze(-1)直接输出单步预测15 分钟后车速若需多步只需将out_proj改为nn.Linear(hidden_size, num_pred)并调整test.py中的评估逻辑。4. 运行全流程从零开始跑通 T-GCN 在深圳数据上的训练别被model/下四个文件夹吓住——它们共享同一套运行逻辑。以下以最简化的 T-GCN 为例演示如何在无 GPU 的笔记本上完成首次训练并验证结果可信。4.1 环境准备最小依赖清单与版本锁定# 创建干净环境推荐 conda conda create -n tgcn_env python3.8 conda activate tgcn_env # 安装核心依赖注意版本 pip install torch1.12.1cpu torchvision0.13.1cpu -f https://download.pytorch.org/whl/torch_stable.html pip install dgl-cu1130.9.2 # 若有 CUDA 11.3否则用 dgl0.9.2CPU 版 pip install numpy1.21.6 pandas1.3.5 scikit-learn1.0.2 matplotlib3.5.2提示dgl0.9.2是本项目实测兼容性最好的版本。新版本dgl1.0中GraphConv接口变更会导致self.gc(self.g, x)报错missing argument edge_weight。若你坚持用新版需在model/tgcn/model.py中显式传入edge_weightself.g.edata[weight]。4.2 数据预处理三步生成可用的adj_mx.pkl项目已提供sz_speed.csv但缺少邻接矩阵。必须补全这一步# run_preprocess.py import numpy as np import pickle import networkx as nx from utils.graph_utils import calculate_adjacency_matrix # 1. 读取深圳路网地理信息模拟实际项目应有 road_id, lat, lon 列 # 此处用随机生成的 128 个坐标点模拟真实项目需从 OpenStreetMap 导出 np.random.seed(42) coords np.random.uniform(low[22.5, 113.8], high[22.6, 114.0], size(128, 2)) # 2. 计算欧氏距离矩阵单位米 dist_matrix np.sqrt(((coords[:, None, :] - coords[None, :, :]) ** 2).sum(axis2)) # 3. 生成加权邻接矩阵σ1000 米 adj_mx calculate_adjacency_matrix(dist_matrix, sigma1000.0) # 4. 保存为 DGL 兼容格式 with open(data/adj_mx.pkl, wb) as f: pickle.dump(adj_mx, f) print(adj_mx.pkl saved with shape:, adj_mx.shape)注意calculate_adjacency_matrix()函数在utils/graph_utils.py中定义核心是np.exp(-dist_matrix / sigma)。若你的路网坐标是 WGS84 经纬度需先用geopy.distance.geodesic转为米制距离否则sigma1000会失效。4.3 启动训练一条命令跑通关键参数解读# 在项目根目录执行 python train.py --model tgcn --dataset sz --gpu -1 --epochs 50 --batch_size 32--model tgcn指定model/tgcn/下的模型--dataset sz加载data/sz_speed.csv和data/adj_mx.pkl--gpu -1强制 CPU 训练笔记本无 GPU 时必加--epochs 50实测 50 轮足够收敛batch_rmse.csv中 loss 在 30 轮后基本平稳--batch_size 32sz_speed.csv有 17k 时间步num_sample 17000-12-31 ≈ 1698632 批大小约 530 个 iteration/epoch。训练过程中你会看到Epoch 1/50 | Loss: 0.0421 | Val_RMSE: 4.82 km/h Epoch 2/50 | Loss: 0.0315 | Val_RMSE: 4.31 km/h ... Epoch 50/50| Loss: 0.0087 | Val_RMSE: 3.21 km/h血泪经验若Val_RMSE卡在 5.0 不下降90% 是adj_mx.pkl未正确生成检查dist_matrix是否为米制或data_loader.py中seq2instance()漏了.T转置。4.4 结果验证不只是看数字更要画出预测曲线训练完成后checkpoint/下会生成tgcn_sz_20231001_153000.pth时间戳命名。运行测试python test.py --model tgcn --dataset sz --load_from checkpoint/tgcn_sz_*.pth脚本自动执行加载 checkpoint在data/sz_speed.csv最后 20% 数据上预测生成results/tgcn_sz_pred.png横轴时间、纵轴车速蓝线为真实值红线为预测值输出results/tgcn_sz_metrics.txtMAE: 2.67 km/h | RMSE: 3.21 km/h | MAPE: 8.42%关键验证点打开results/tgcn_sz_pred.png重点看突变点——比如真实曲线在 10:15 突然跌到 20km/h事故预测曲线是否在 10:15–10:20 区间同步下跌若预测曲线平滑滞后则说明时间建模不足需调大num_layers或换ast_gcn。5. 避坑指南五个让开发者深夜重启电脑的致命错误这些坑我都踩过有些甚至导致三天无法复现论文结果。以下按「现象→原因→解决」列出最痛的五条按出现频率排序5.1 现象RuntimeError: Expected all tensors to be on the same device原因train.py中model.to(device)与data.to(device)设备不一致。常见于--gpu -1时devicetorch.device(cpu)但adj_mx.pkl中的邻接矩阵是torch.Tensor且未.to(device)。DGL 的GraphConv内部会尝试在 GPU 上运算报错。解决在data_loader.py的__getitem__中对x和y显式.to(device)并在train.py开头添加if args.gpu -1: device torch.device(cpu) else: device torch.device(fcuda:{args.gpu}) # 确保图结构也移到 device g g.to(device) # DGLGraph 支持 .to()5.2 现象训练 loss 为 nan且batch_loss.csv第一行就是nan原因sz_speed.csv中存在0或负值如传感器故障经Min-Max归一化后x_norm (x - min) / (max - min)分母为 0或log(x)操作某些基线模型用触发nan。解决在utils/data_loader.py的StandardScaler类中增加安全归一化def transform(self, data): # 原始return (data - self.mean) / self.std # 修改为 std_safe np.where(self.std 0, 1.0, self.std) # 避免除零 return (data - self.mean) / std_safe5.3 现象A3T-GCN训练速度比T-GCN慢 5 倍GPU 显存爆满原因TemporalAttention中x1_pooled x1.mean(dim2)误写为x1.mean(dim1)导致S矩阵尺寸从(B,T,T)膨胀为(B,N,N)128×12816384显存瞬间吃光。解决严格核对a3t_gcn/model.py中TemporalAttention.forward()的dim参数。正确应为dim2对节点维度平均错误是dim1对 batch 维度平均无意义。5.4 现象test.py预测结果全是直线RMSE高达 15km/h原因test.py中未设置model.eval()导致Dropout和BatchNorm层仍处于训练模式输出不稳定。解决在test.py的预测循环前强制添加model.eval() # 关闭 dropout/batchnorm with torch.no_grad(): # 禁用梯度计算 for x, y in test_dataloader: pred model(x) # ...5.5 现象更换los_speed.csv后KST-GCN训练报错KeyError: road_123原因sz_assist_kg.csv中的road_id与los_speed.csv的节点 ID 不匹配。KST-GCN在model/kst_gcn/model.py中通过kg_df[kg_df[road_id]node_id]查找知识ID 不一致则 KeyError。解决检查data/下所有 CSV 的第一列是否为统一 ID 格式如sz_001,los_001。若los_speed.csv无列名用pandas.read_csv(..., headerNone)并手动添加columns[los_001,los_002,...]。6. 二次开发实战如何把 KST-GCN 改造成支持实时更新的知识图谱你可能已经发现KST-GCN的知识图谱sz_assist_kg.csv是静态的——它无法响应路网拓扑变化如新修隧道、临时封路。真正的城市系统需要在线更新能力。下面教你用 30 行代码把kst_gcn改造成支持增量知识注入的模型。6.1 知识图谱动态加载从 CSV 切换到内存字典原kst_gcn/model.py中知识查询是每次forward时读 CSV# 原代码低效 kg_df pd.read_csv(data/sz_assist_kg.csv) for node_id in node_ids: attrs kg_df[kg_df[road_id]node_id][attr].values改为预加载到内存字典# 在 __init__ 中一次性加载 def __init__(self, g, in_channels, hidden_size, kg_pathdata/sz_assist_kg.csv): super().__init__() self.kg_dict {} kg_df pd.read_csv(kg_path) for _, row in kg_df.iterrows(): road_id row[road_id] if road_id not in self.kg_dict: self.kg_dict[road_id] [] self.kg_dict[road_id].append({ relation: row[relation], value: row[value] }) # ... 其余初始化 # 在 forward 中快速查询 def forward(self, x): B, N, T, C x.shape # 动态获取每个节点的知识向量假设每节点最多 3 个属性 kg_embed torch.zeros(B, N, 3).to(x.device) # (B, N, 3) for b in range(B): for n in range(N): node_id fsz_{n1:03d} # 假设节点 ID 格式为 sz_001, sz_002... if node_id in self.kg_dict: # 取第一个属性的 value 作为 embedding可扩展 kg_embed[b, n, 0] float(self.kg_dict[node_id][0][value]) # 将 kg_embed 拼接到 x 的特征维度 x_enhanced torch.cat([x, kg_embed.unsqueeze(2)], dim2) # (B, N, T, C3) # 后续送入 GCN...6.2 在线知识更新接口暴露一个update_knowledge()方法def update_knowledge(self, new_kg_list): new_kg_list: List[Dict] [{road_id:sz_045, relation:lanes, value:4}] for item in new_kg_list: road_id item[road_id] if road_id not in self.kg_dict: self.kg_dict[road_id] [] self.kg_dict[road_id].append({ relation: item[relation], value: item[value] }) print(fUpdated knowledge for {len(new_kg_list)} roads) # 使用示例当监控系统检测到 sz_045 车道数变为 4 model.update_knowledge([{road_id:sz_045, relation:lanes, value:4}])6.3 验证改造效果用test.py注入故障事件在test.py末尾添加实时注入测试# 模拟 10:00 发生事故封路 sz_045 model.update_knowledge([ {road_id:sz_045, relation:status, value:closed}, {road_id:sz_045, relation:impact, value:high} ]) # 立即用新知识预测下一时刻 x_realtime get_latest_data() # 获取最新 12 步数据 pred_after_incident model(x_realtime) print(Post-incident prediction for sz_045:, pred_after_incident[0, 44].item(), km/h)从那以后我每次部署图神经网络都强制走一遍update_knowledge()的单元测试——哪怕只是传入一个空列表也要确认kg_dict不为空、forward不崩溃。因为线上系统最怕的不是预测不准而是知识缺失导致的静默失败。希望帮到你。本文还有配套的精品资源点击获取