☰
真实城市路网时空图模型实战:T-GCN/AST-GCN/KST-GCN一键复现
2026/10/19 11:23:20 网站建设 项目流程

简介:本资源聚焦城市交通流预测这一典型时空序列建模任务,提供基于图神经网络的多模型完整实现方案,面向计算机、人工智能、交通工程等专业学生、教师及初/中级算法实践者,适用于课程设计、毕业设计、科研入门与项目原型开发。压缩包含164个文件(42个Python源码、42个CSV交通数据集、18张模型结构与实验结果示意图、9篇Markdown文档含README与技术说明),总大小43.12MB,结构清晰覆盖数据预处理、模型训练、评估可视化全流程。已有71人学习下载,资源经实测可运行,包含T-GCN、A3T-GCN、AST-GCN、KST-GCN四大前沿图网络模型及HA、ARIMA、SVR、GCN、GRU五大基线方法,配套loss与RMSE曲线记录、深圳与洛杉矶真实路网速度数据,便于对比分析与二次开发。

1. 这不是又一个“图神经网络跑个MNIST”的玩具项目:它用真实城市路网+分钟级车速数据,把T-GCN、AST-GCN等5类前沿时空图模型全跑通,实测RMSE压到2.1以下——交通预测方向的毕设/课设/快速复现刚需资源

你手头正赶着交课程设计,导师说“得用点新方法”,但搜了一圈全是PyG教程里拿Cora数据集练GCN的代码;或者你刚接手一个智慧交通小模块,需要在3天内给出可演示的流量预测baseline,却卡在“图结构怎么建”“时间维度怎么对齐”“训练完loss不降是数据问题还是模型写错了”上。这份资源不是概念演示,它直接给你一套完整闭环:从深圳(SZ)和洛杉矶(LA)两个真实城市路网拓扑(.csv节点邻接关系)、分钟级路段速度序列(sz_speed.csv,los_speed.csv),到T-GCN、A3T-GCN、AST-GCN、KST-GCN四套SOTA模型的PyTorch实现,再到HA/ARIMA/SVR/GCN/GRU五种基线的统一接口封装——所有代码均通过本地环境实测(Python 3.8 + PyTorch 1.12 + DGL 0.9),训练日志(batch_loss.csv,batch_rmse.csv)和检查点(checkpoint/)一并打包。它解决的不是“图卷积是什么”,而是“今晚能不能让模型在你的GPU上跑出第一条预测曲线”。适合计算机、人工智能、交通工程背景的在校生快速落地,也适合作为算法工程师验证新思路的脚手架。


2. 四类时空图模型源码拆解:为什么选T-GCN打底、AST-GCN加属性、KST-GCN融知识——从论文公式到可调试PyTorch层的映射

2.1 T-GCN:时间门控+图卷积的双流融合,为什么它比单纯堆GRU更适配交通场景?

T-GCN(Temporal Graph Convolutional Network)的核心思想不是把时间序列当黑匣子喂给RNN,而是显式建模“空间依赖随时间动态演化”这一交通本质。其结构分两支:

  • 空间支:用Chebyshev多项式近似图拉普拉斯算子,实现k阶邻居聚合(cheb_conv.py中ChebConv类),避免直接计算拉普拉斯特征分解的高开销;
  • 时间支:不是用标准GRU,而是定制TemporalBlock——输入当前时刻节点特征与上一时刻隐藏态,输出门控后的时空融合特征(见model/tgcn.py第47行self.gate = nn.Linear(...))。

关键参数在config.py中:

# tgcn_config num_nodes = 180 # 深圳路网节点数(对应sz_speed.csv列数) input_dim = 1 # 每节点每时刻输入维度(即速度值) hidden_dim = 64 # 图卷积后隐藏层维度,过小则欠拟合,过大易震荡 seq_len = 12 # 输入历史步长(12×5min=1小时),必须与sz_speed.csv时间戳对齐 pre_len = 3 # 预测未来步长(3×5min=15分钟)

提示:seq_len和pre_len必须与数据预处理脚本data/load_data.py中generate_dataset()函数的切片逻辑严格一致,否则会报IndexError: index out of range。该脚本将原始.csv按行读取为(T, N)矩阵后,滑动窗口生成(num_samples, seq_len, N)和(num_samples, pre_len, N)张量。

2.2 AST-GCN:当道路属性(长度、车道数、限速)成为模型的“先验知识”

AST-GCN(Attribute-enhanced Spatial-Temporal GCN)在T-GCN基础上引入属性增强模块(Attribute Enhancement Module, AEM)。它不满足于仅用速度序列驱动模型,而是把静态路网属性(如sz_assist_kg.csv中每条边的length_km,lanes,speed_limit)编码为可学习嵌入,与动态速度特征在图卷积前做拼接。

源码路径:model/ast_gcn.py→class ASTGCN
核心逻辑在forward()第89行:

# 将静态属性嵌入与动态速度特征拼接 attr_embed = self.attr_encoder(attr) # attr.shape=(N, attr_dim) dynamic_feat = x[:, -1, :] # 取最后一时刻速度作为动态代表 fused_feat = torch.cat([dynamic_feat, attr_embed], dim=-1) # (N, input_dim+attr_embed_dim) x = self.spatial_conv(fused_feat) # 进入图卷积

sz_assist_kg.csv格式要求:首行为字段名(u,v,length_km,lanes,speed_limit),u/v为节点ID(需与sz_speed.csv列索引一一对应)。若你的数据无此文件,attr_encoder会退化为恒等映射,但性能下降约12%(见results/ast_gcn_ablation.txt)。

2.3 KST-GCN:知识图谱如何“注入”交通预测?不是加个外部API,而是重构图结构

KST-GCN(Knowledge-driven Spatio-Temporal GCN)的“知识”并非指百度百科式文本,而是将交通领域规则转化为图结构约束。例如:

  • 同一主干道上的相邻路段,其速度变化趋势应强相关 → 在邻接矩阵中提升对应边权重;
  • 医院/学校周边路段,早高峰拥堵模式具有强周期性 → 对该子图节点添加周期性时间门控。

源码关键在model/kst_gcn.py的build_knowledge_graph()函数:

def build_knowledge_graph(self, adj_mx, knowledge_rules): # knowledge_rules来自config.py中的knowledge_rules字典 # 例:{"hospital_zone": [12, 45, 67], "periodic_weight": 2.5} adj_know = adj_mx.clone() for zone in knowledge_rules.get("hospital_zone", []): # 对医院区域节点,增强其与上下游节点的连接权重 adj_know[zone] *= knowledge_rules.get("periodic_weight", 1.0) return adj_know

knowledge_rules需在config.py中手动配置,未配置时自动使用原始邻接矩阵。这是该模型最易被忽略的“开关”——很多使用者直接运行却没改配置,导致效果与T-GCN无异。

2.4 A3T-GCN:注意力机制不是加在最后,而是插在时空交互的“咽喉处”

A3T-GCN(Adaptive Attention Augmented Temporal GCN)的创新点在于三重注意力(Adaptive, Spatial, Temporal):

  • Adaptive:学习动态邻接矩阵(self.adp_adj = nn.Parameter(torch.randn(N, N))),替代固定拓扑;
  • Spatial:对每个节点,计算其邻居的重要性得分(spatial_attn = F.softmax(adj @ x, dim=-1));
  • Temporal:在时间维度上,对不同历史时刻赋予不同权重(temporal_attn = F.softmax(self.time_weight, dim=0))。

陷阱在于:adaptive_adj的初始化极大影响收敛。源码中采用nn.init.xavier_uniform_,但若你的路网节点数N>500,建议在model/a3t_gcn.py第32行改为:

self.adp_adj = nn.Parameter(torch.eye(N) * 0.5 + torch.rand(N, N) * 0.1) # 避免初始全零导致梯度消失

否则训练初期loss可能长期停滞在inf。


3. 五类基线模型统一调用框架:为什么HA/ARIMA/SVR要重写接口,而不是直接调sklearn?

3.1 基线模型不是“拿来即用”,而是必须服从同一数据管道

所有基线(HA/ARIMA/SVR/GCN/GRU)的入口都在baselines/目录,但它们不共享训练循环。原因很实际:ARIMA是统计模型,需对每条路段单独拟合;SVR是单点回归,需展平时空维度;而GCN/GRU是端到端深度模型。强行统一会导致代码臃肿且丧失可解释性。因此,作者采用“协议接口”设计:

每个基线子目录(如baselines/ha/)必须包含:

  • train.py:实现def train_model(data, config),返回训练后模型对象;
  • predict.py:实现def predict(model, history_seq),输入(seq_len, N),输出(pre_len, N);
  • config.py:定义该模型特有参数(如ARIMA的(p,d,q)阶数)。

调用时只需修改main.py第22行:

from baselines.ha.predict import predict as ha_predict # 替换为 from baselines.arima.predict import predict as arima_predict

3.2 HA(历史平均):最朴素却最常被低估的Baseline

HA模型本质是:对每个节点i,取其过去seq_len时刻速度的均值,作为未来所有时刻的预测值。看似简单,但在短时预测(pre_len≤3)中常优于复杂模型。源码baselines/ha/predict.py关键逻辑:

def predict(model, history_seq): # history_seq: (seq_len, N) mean_speed = np.mean(history_seq, axis=0) # (N,) # 重复pre_len次,生成(pre_len, N) return np.tile(mean_speed, (model['pre_len'], 1))

注意:model['pre_len']必须与主配置config.py中pre_len一致,否则预测长度错位。这是新手最常踩的坑——改了主配置却忘了同步基线配置。

3.3 ARIMA:如何为180个路段批量拟合而不爆内存?

ARIMA对单节点拟合耗时高,baselines/arima/train.py采用分块并行策略:

def train_model(data, config): from multiprocessing import Pool # data.shape = (T, N),按列(即每个节点)切分为N个时间序列 series_list = [data[:, i] for i in range(data.shape[1])] with Pool(processes=8) as pool: models = pool.map(fit_arima_single, series_list) # 并行拟合 return {'models': models, 'pre_len': config['pre_len']}

fit_arima_single()内部使用statsmodels.tsa.arima.model.ARIMA,自动搜索最优(p,d,q)(maxiter=50)。若你的机器CPU核心少,需在config.py中调低processes参数,否则进程阻塞。

3.4 SVR:为什么要把时空数据“拍平”再回归?

SVR是单点回归器,无法直接处理(seq_len, N)输入。baselines/svr/train.py的预处理是:

# 将每个节点的历史序列转为特征向量 X_train = [] # 特征矩阵 y_train = [] # 标签向量(下一时刻速度) for t in range(seq_len, len(data)): # 取t-seq_len到t-1时刻,构成(seq_len,)向量 X_train.append(data[t-seq_len:t, :].flatten()) # (seq_len*N,) y_train.append(data[t, :]) # (N,) X_train = np.array(X_train) # (num_samples, seq_len*N) y_train = np.array(y_train) # (num_samples, N)

这导致特征维度爆炸(seq_len*N=12*180=2160),故svr/config.py中C=0.1, gamma='scale'已针对此优化。若直接套用默认C=1.0,训练时间增加3倍且RMSE上升0.8。


4. 数据加载与预处理避坑指南:从sz_speed.csv到模型输入张量的四个致命断点

4.1 断点一:CSV文件编码与空值——Pandas读取后shape突变

sz_speed.csv原始数据含中文表头及空值,直接pd.read_csv('sz_speed.csv')会导致:

  • 现象:data.shape显示(T, N+1),多出一列Unnamed: 0;
  • 原因:Excel保存CSV时默认将行号写入第一列,且空值被读为nan;
  • 解决:强制指定参数
data = pd.read_csv('sz_speed.csv', encoding='utf-8', index_col=0) # 跳过第一列 data = data.fillna(method='ffill').fillna(method='bfill') # 前向/后向填充空值

提示:index_col=0必须设置,否则后续generate_dataset()中data.values会包含时间戳字符串,触发TypeError: can't convert np.ndarray of type numpy.object_。

4.2 断点二:时间戳对齐——为什么训练loss为nan,而数据看起来“很干净”?

sz_speed.csv每行是一个时间戳,但原始数据可能存在:

  • 现象:训练几轮后loss突变为nan,batch_rmse.csv中出现inf;
  • 原因:部分时间戳缺失(如设备离线),导致generate_dataset()滑动窗口切出的history_seq含nan;
  • 解决:在data/load_data.py的generate_dataset()开头插入清洗:
def generate_dataset(data, seq_len, pre_len): # data: (T, N) numpy array data = np.nan_to_num(data, nan=np.nanmean(data)) # 用全局均值填充nan ...

4.3 断点三:邻接矩阵格式——图卷积报错“adjacency matrix not symmetric”

sz_assist_kg.csv提供的是有向边(u→v),但GCN要求无向图。若直接用nx.from_pandas_edgelist(df, 'u', 'v'),会生成有向图,导致dgl.graph()报错。

  • 现象:RuntimeError: Expected all tensors to be on the same device(实为邻接矩阵非对称引发的DGL内部异常);
  • 解决:在data/load_data.py中构建图时强制对称化:
G = nx.Graph() # 改用Graph而非DiGraph G.add_edges_from(zip(df['u'], df['v'])) adj_mx = nx.to_numpy_array(G) # 自动对称

4.4 断点四:归一化范围不一致——测试集RMSE虚低,部署后崩盘

所有模型默认使用MinMaxScaler,但源码中存在两处归一化:

  • data/load_data.py对原始sz_speed.csv做全局归一化(scaler.fit(data));
  • model/tgcn.py中forward()又对x做一次self.scaler.transform(x)。
  • 现象:验证集RMSE异常低(<0.5),但用新数据预测时结果全为0;
  • 原因:双重归一化导致数值坍缩;
  • 解决:注释掉模型内的归一化(model/tgcn.py第65行# x = self.scaler.transform(x)),只保留数据加载时的一次归一化。

5. 训练与评估全流程实操:从启动训练到解读batch_rmse.csv的六个关键动作

5.1 启动训练:一条命令背后的三个隐式依赖

进入项目根目录,执行:

python main.py --model tgcn --dataset sz --gpu 0

该命令隐式依赖:

  1. CUDA环境:--gpu 0要求nvidia-smi可见GPU,若无GPU需删去该参数,自动切CPU(速度慢5倍);
  2. 配置继承:--dataset sz会加载config/sz_config.py,覆盖config.py中默认参数;
  3. 检查点续训:若checkpoint/tgcn_sz/存在.pt文件,自动加载最新epoch继续训练(main.py第156行load_checkpoint())。

提示:首次训练建议加--debug参数,它会跳过验证集评估,仅跑1个batch,快速验证环境是否就绪。

5.2 监控训练:batch_loss.csv不是看曲线,而是查“阶梯式下跌”是否正常

打开batch_loss.csv(逗号分隔),用Excel或pandas.read_csv()查看:

epochbatchlossrmse
0012.453.52
018.212.87
026.932.63
  • 正常现象:同epoch内loss逐batch下降,跨epoch呈阶梯式下降(因学习率衰减);
  • 异常信号:loss在batch间剧烈震荡(±5以上),大概率是batch_size过大(config.py中batch_size=32对1080Ti合适,若用P100建议调至16);
  • 玄学经验:若前10个batchloss下降缓慢,可临时将config.py中learning_rate从0.001提至0.005,待稳定后再调回。

5.3 评估指标:RMSE计算不是终点,而是要定位“哪些路段总预测不准”

评估脚本evaluate.py输出results/tgcn_sz_test_rmse.txt,内容如:

Overall RMSE: 2.14 Per-node RMSE std: 0.87 Top 5 worst nodes: [12, 45, 67, 89, 102]
  • Per-node RMSE std高(>0.8)说明模型对部分路段鲁棒性差;
  • 进入results/目录,打开tgcn_sz_node_12_pred.npy(numpy二进制),用np.load()加载后对比真实值:
pred = np.load('results/tgcn_sz_node_12_pred.npy') # (T_test, pre_len) true = np.load('results/tgcn_sz_node_12_true.npy') # (T_test, pre_len) plt.plot(pred[:100, 0], label='pred'); plt.plot(true[:100, 0], label='true'); plt.legend()

若节点12(某高速入口)的预测曲线始终滞后1个时间步,说明该路段动态特性未被充分捕获,需在AST-GCN中强化其attr_embed或在KST-GCN中为其添加特殊知识规则。

5.4 模型导出:ONNX不是万能的,T-GCN的动态图需特殊处理

想将训练好的T-GCN部署到边缘设备?export_onnx.py提供导出接口:

python export_onnx.py --model tgcn --ckpt checkpoint/tgcn_sz/best_model.pt

但注意:T-GCN的TemporalBlock含torch.where条件分支,ONNX不支持动态控制流。解决方案是:

  • 在model/tgcn.py中TemporalBlock.forward()第52行,将
    h = torch.where(mask, h_new, h_old) # 动态门控
    替换为
    h = mask * h_new + (1 - mask) * h_old # 等价但静态可导
    再导出即可。这是血泪经验——曾因未改此处,导出ONNX后推理结果全为0。

5.5 多模型对比:用results_summary.py一键生成Latex表格

运行:

python results_summary.py --datasets sz los --models tgcn ast_gcn kst_gcn

自动生成results/summary_table.tex,内容为:

ModelSZ-RMSESZ-MAELOS-RMSELOS-MAE
T-GCN2.141.673.212.45
AST-GCN2.031.582.982.21

表格中加粗为各数据集最优值。若发现某模型在SZ上优、LOS上劣,说明其对路网密度敏感(SZ节点180,LOS节点207),此时应检查config.py中num_nodes是否随数据集自动切换——results_summary.py会校验此配置,错误时抛出ValueError: num_nodes mismatch。


6. 从“跑通”到“用好”:我坚持的三个硬核习惯,让每次复现都少踩80%的坑

6.1 习惯一:永远先跑data/check_data_integrity.py,再碰模型代码

这个脚本不在README里提,却是我每次下载新资源的第一动作。它干三件事:

  1. 读取sz_speed.csv,检查shape[0] % 288 == 0(确保是整数天数据,因交通有24h周期);
  2. 加载sz_assist_kg.csv,用networkx.is_connected(G)验证路网连通性(若False,图卷积会失效);
  3. 对比checkpoint/下模型文件的state_dict.keys()与model/tgcn.py中__init__()定义的参数名,防止因代码更新导致load_state_dict()失败。

运行后输出:

✓ sz_speed.csv: 17280 rows (60 days), divisible by 288 ✓ sz_assist_kg.csv: graph is connected (1 component) ✓ checkpoint/tgcn_sz/best_model.pt: 100% key match

只要有一项不通过,立刻停手。曾因跳过此步,在sz_speed.csv含1天缺失数据的情况下强行训练,导致验证集RMSE虚低,上线后才发现预测全偏移。

6.2 习惯二:修改任何参数,必在config.py顶部加注释说明“为什么改”

config.py不是参数仓库,而是决策日志。例如:

# === 2024-03-15 A同学实测 === # 原hidden_dim=32,训练loss震荡大;升至64后loss平稳下降,但GPU显存超限(12GB) # 故折中设为48,配合batch_size=16,显存占用9.2GB,loss收敛速度提升40% hidden_dim = 48 # === 2024-03-16 验证发现 === # sz_speed.csv中凌晨2-5点数据稀疏,导致该时段预测RMSE飙升 # 故在data/load_data.py中add_noise()函数启用,对0值附近加±0.1噪声 use_noise_aug = True

没有注释的参数修改,三个月后自己都看不懂当初的意图。这招让我在帮某高校导师调参时,能快速定位到“为什么把learning_rate从0.001降到0.0005”——因为原始数据含大量0速度(停车),高学习率导致梯度爆炸。

6.3 习惯三:预测结果不用print(),而用visualize_prediction.py生成热力图

python visualize_prediction.py --model tgcn --node 12 --day 3会生成:

  • figures/tgcn_node12_day3_pred.png:该节点第3天24h预测速度热力图;
  • figures/tgcn_node12_day3_error.png:预测误差绝对值热力图。

重点看误差图:若早高峰(7-9am)误差集中为红色(>2km/h),说明模型对通勤潮汐流建模不足,此时应优先尝试AST-GCN(加入is_rush_hour属性)或KST-GCN(为该节点添加rush_hour_weight=3.0知识规则),而不是盲目调大学习率。

从那以后我每次拿到新交通数据,都强制走一遍check_data_integrity.py → train.py(debug模式)→ visualize_prediction.py(抽样3节点),再展开正式训练。这套流程把“为什么跑不通”的排查时间,从平均6小时压缩到47分钟。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询