简介:面向交通流预测与时空图卷积网络学习者,这份资源提供了IJCAI 2018发表的STGCN模型源码包,可用于城市交通流量预测、智能交通系统等场景。代码主体为Python编写,结合图卷积网络与时间序列分析,适合需要处理非欧几里得空间数据的算法研究者与开发者。压缩包共19个文件,包括11个Python脚本,覆盖模型定义、训练、测试及数据预处理等完整流程;6张PNG结果图直观展示预测值与实际值的对比;另附README说明文档与数据集压缩包,整体约7.05MB。已有2310人学习浏览。借助该资源,读者可学习STGCN的图卷积与时间卷积组合方式、交通流数据建模流程,以及模型评估与结果可视化方法,对深入研究智能交通与深度学习应用具有参考价值。
1. 路网流量预测的刚需与 STGCN 的定位:图卷积不是炫技,是绕不开
城市路网的监测点分布天然是不规则图结构,几百个传感器散布在高速路、匝道和主路上,彼此间距从几百米到几公里不等。传统 CNN 在规则网格上滑窗,RNN 只能抓住时间维度,两者都很难把“相邻站点之间的空间依赖”直接编码进预测过程。这套来自 IJCAI-18 的 STGCN 代码,把路网建模成一张图,用时空图卷积网络同时处理拓扑关系和时间序列,直接预测未来 15、30、45 分钟的路段流量。它解决的是智能交通里最实际的调度问题:调度、诱导、信号控制都需要提前几分钟知道下一个断面会堵成什么样。适合有一定 Python 和深度学习基础、想把第一个时空预测模型跑通的研究者或算法工程师,也适合刚接触非欧几里得数据建模的人当图卷积的入门副本。
2. STGCN 内部拆解:时间卷积与图卷积是怎样协作的
2.1 从网格到图:路网数据不能直接套 CNN 的原因
路网数据最麻烦的一点是传感器坐标没有规律,有的断面密集,有的断面稀疏。CNN 的卷积核要求输入是规则的网格像素,如果把传感器按经纬度强行投影成二维栅格,空白的栅格会占据大量无效计算,邻接关系也失真。RNN 类模型(LSTM、GRU)擅长捕捉时间依赖,但它们默认各个序列之间没有空间交互,这对路网来说是不可接受的——某一段路拥堵,影响会沿上下游和相邻匝道传播。
常见做法是把路网定义成一张图G = (V, E, A),V是监测点,E表示连边,A是邻接矩阵。STGCN 中邻接矩阵不是简单填 0/1,而用监测点之间的实际距离生成:
import numpy as np def build_adjacency_matrix(coord, sigma2=0.1, threshold=0.1): # coord: 每个监测点的经纬度数组,shape=(N, 2) num_nodes = coord.shape[0] dist_mat = np.zeros((num_nodes, num_nodes)) for i in range(num_nodes): for j in range(num_nodes): dist_mat[i, j] = np.linalg.norm(coord[i] - coord[j]) adj = np.exp(-dist_mat ** 2 / sigma2) adj[adj < threshold] = 0.0 # 距离太远就断开 np.fill_diagonal(adj, 0.0) # 去掉自环 return adj逻辑说明:先算监测点两两欧氏距离,再用高斯核把距离换算成 0 到 1 之间的权重;距离越近权重越大,低于阈值的边直接置零。这样构造出的邻接矩阵既保留了真实路网的局部性,又让图卷积可以按边的权重聚合邻居信息。两个参数值得注意:sigma2控制权重的衰减速度,threshold控制图的稀疏程度。阈值太小会让全网节点互连,计算量和噪声同时上升;阈值太大则图被切成孤立块,空间信息传不出去。我一般先用 0.1 起步,再看预测误差调。
2.2 空间图卷积与时间门控卷积的配合方式
STGCN 的核心结构是一个时空卷积块(ST-Block),块内顺序是:时间卷积 → 空间图卷积 → 时间卷积。两个时间卷积夹一个空间卷积,本质上是在做“先压缩时间维度特征,再在空间上传导,再压缩一次时间维度”的信息流。
空间图卷积用的是切比雪夫多项式近似,K 阶切比雪夫展开能让每个节点聚合 K 跳范围内的邻居信息。K=1 就是经典 GCN 的一阶近似,STGCN 中一般取 K=3,让空间感受野覆盖更远,同时避免 K 过大导致过平滑——所有节点特征趋同,预测变成一条水平线。节点特征在空间卷积层内的传播过程可以写成:
import tensorflow as tf def cheb_graph_conv(x, adj_scaled, theta, k): # x: 输入特征 [batch, num_nodes, input_dim] # adj_scaled: 缩放到 [-1, 1] 的拉普拉斯矩阵 # theta: 可学习参数 [k, input_dim, output_dim] outputs = [] x0 = x outputs.append(tf.matmul(x0, theta[0])) if k > 1: x1 = tf.matmul(adj_scaled, x0) outputs.append(tf.matmul(x1, theta[1])) for i in range(2, k): x2 = 2.0 * tf.matmul(adj_scaled, x1) - x0 outputs.append(tf.matmul(x2, theta[i])) x0, x1 = x1, x2 return tf.add_n(outputs)逻辑说明:切比雪夫多项式的递推关系是T_k = 2x T_{k-1} - T_{k-2},代码里迭代地生成高阶邻居聚合结果,每一阶都配一组可学习参数theta。这里adj_scaled是归一化后的拉普拉斯矩阵,一般用2L / lambda_max - I把它压到 [-1, 1],保证递推数值稳定。参数k就是切比雪夫阶数,它直接控制空间感受野,调参时优先动它。
时间维度上,STGCN 用的是门控时间卷积:一维卷积先提取时间窗口内的局部特征,再用一个并行分支做 Sigmoid 门控,决定保留多少信息。这种设计比普通卷积多了一个“门”,对交通流这种含早晚高峰突变的信号更友好,能保留陡升陡降的细节,而不会把峰值直接抹平。
2.3 输入输出形状与预测窗口
整套模型的输入是一个五维张量。假设取 50 个监测点,输入过去 12 个时间片(5 分钟一个片,即过去 1 小时),每个点上有流量、速度、占用率 3 个特征,那么输入形状是[batch, 12, 50, 3]。时间卷积按 2D 卷积处理,把 12 和 3 两个维度合并后卷积,空间图卷积只作用在节点维度上。
| 项目 | 设置 |
|---|---|
| 历史观测窗口 | 12 个时间片,对应 1 小时 |
| 预测窗口 | 未来 3 / 6 / 9 步,对应 15 / 30 / 45 分钟 |
| 图卷积阶数 K | 3 |
| 时间卷积核宽度 | 3 |
| 时空块数量 | 2 |
| 输出层 | 全连接,输出未来多步流量 |
这段比较关键:它预测的不是下一个 5 分钟,而是一段连续窗口,而且三个预测步长分别训练三个模型。这样设计的好处是训练目标清晰,代价是训练时间翻三倍。你要是只需要 15 分钟预测,可以只跑 H=3 的模型,不用全部复现。
3. 把压缩包跑起来:数据准备、环境与首次训练
3.1 压缩包结构:关键文件与职责
这套代码的目录组织得很清楚,拿到手先别急着跑,花五分钟把文件对应关系理清,后面排错会快很多。
| 文件 / 目录 | 职责 |
|---|---|
main.py | 训练与测试入口,定义命令行参数 |
data_loader/data_utils.py | 数据加载、归一化、生成训练/验证/测试集 |
models/layers.py | 时间卷积层、图卷积层、时空块的具体实现 |
models/base_model.py | 模型基类,管理训练步骤与损失计算 |
models/trainer.py | 训练过程控制、梯度下降、日志输出 |
models/tester.py | 测试集评估,输出指标与预测结果 |
utils/math_graph.py | 构造邻接矩阵和拉普拉斯矩阵 |
utils/math_utils.py | 数据标准化、反标准化、指标计算 |
PeMS-M.zip | 原始监测数据压缩包 |
figures/*.png | 训练损失曲线与预测结果对比图 |
先用unzip PeMS-M.zip解压数据,确认目录下有解压后的数据文件。代码里的数据路径一般默认指向./data/PeMS-M/,如果你把压缩包解压到了别的位置,需要同步修改main.py里的--data参数,否则data_utils.py会直接报FileNotFoundError。
3.2 TF 1.x 环境下的依赖安装与兼容写法
这套代码最早基于 TensorFlow 1.x 编写,用的是tf.Session、tf.placeholder这些老 API。TensorFlow 2.x 默认是 Eager 模式,直接跑会在模型构建阶段就报AttributeError: module 'tensorflow' has no attribute 'placeholder'。最省事的方案是装 TF 1.15,这是 1.x 里最后一个版本,兼顾稳定性:
conda create -n stgcn python=3.6 conda activate stgcn pip install tensorflow==1.15.0 numpy pandas scipy逻辑说明:Python 3.6 是 TF 1.15 最稳的组合,numpy 版本建议不要超过 1.19,太新的 numpy 会在某些 TF 算子初始化时报TypeError之类的怪错。装完后可以快速验证 GPU 是否可用:
python -c "import tensorflow as tf; print(tf.__version__); print(tf.test.is_gpu_available())"如果输出1.15.0和True,环境就绪。没有 GPU 也别急着放弃,PeMS-M 数据规模不大,CPU 上跑 50 个 epoch 只是慢一点,不会跑不动。
3.3 训练启动与参数选择
启动训练的命令通常长这样:
python main.py --model stgcn \ --data ./data/PeMS-M/PeMS-M.npz \ --train 1 \ --test 1 \ --batch_size 50 \ --epochs 50 \ --learning_rate 0.001逻辑说明:--model指定模型类型;--data指向预处理后的数据文件;--train 1表示执行训练,--test 1表示训练结束后在测试集上评估;--batch_size是每次送入模型的样本数;--epochs是训练轮数;--learning_rate是初始学习率。首次跑建议把epochs降到 10,先确认数据流和损失函数正常,再加大轮数。
几个参数的实际使用建议:batch_size不是越大越好。STGCN 的图卷积要对整张邻接矩阵做矩阵乘法,batch 增大后显存占用几乎是线性增长。50 这个值在我这边的 GPU 上是安全的,显存偏小的机器可以先降到 32。学习率 0.001 配合 Adam 是常见默认值,如果训练日志里 loss 前几轮就剧烈震荡,先把学习率降到 0.0005 再试。
训练过程中trainer.py会按迭代输出 loss,每个 epoch 结束后还会计算验证集误差。正常的 loss 曲线应该是前几个 epoch 快速下降,之后进入平台期。如果你看到 loss 直接变成NaN,或者验证集误差比训练集高一两个数量级,不要继续等,先停下来对照下一章的排查清单。
4. 结果评估:从 log 与结果图判断模型是否学到时空特征
4.1 三个评估指标:MAE / MAPE / RMSE
这套代码的评测部分用三个指标衡量预测质量,压缩包里的结果文件也是围绕它们计算的。
| 指标 | 公式含义 | 实用价值 |
|---|---|---|
| MAE | 绝对误差的平均值 | 单位与流量一致,最直观,交通工程报告里常写这个 |
| MAPE | 绝对百分比误差 | 不同路段水平可比,但流量接近 0 时会被放大 |
| RMSE | 均方根误差 | 对大误差敏感,能反映“极端拥堵时段的预测是否崩掉” |
只看某一个指标容易误判。MAE 低但 RMSE 高,说明大部分时段预测不错,但早晚高峰的峰值段可能有大幅偏差;MAPE 高但 MAE 低,往往是深夜低流量时段的小误差被百分比放大了。所以 STGCN 原论文和这代码都同时报告三个指标。
训练结束后,日志会打印测试集上的最终数值。你也可以写一段小脚本把每个 epoch 的训练与验证误差提取出来,画成曲线,确认模型是否收敛、有没有过拟合:
import re import matplotlib.pyplot as plt with open("train.log", "r") as f: logs = f.readlines() epochs, train_loss, val_loss = [], [], [] for line in logs: m = re.search(r"Epoch (\d+).*train_loss=([\d.]+).*val_loss=([\d.]+)", line) if m: epochs.append(int(m.group(1))) train_loss.append(float(m.group(2))) val_loss.append(float(m.group(3))) plt.plot(epochs, train_loss, label="train") plt.plot(epochs, val_loss, label="val") plt.legend() plt.savefig("loss_curve.png")逻辑说明:正则表达式按日志格式提取 epoch 序号、训练误差和验证误差。如果train_loss一路下降但val_loss在某个 epoch 之后掉头上升,就是典型的过拟合信号,可以提前终止训练或调低模型容量。
4.2 复现结果图:预测曲线对真实流量的拟合程度
压缩包figures目录里的结果图分两类:一类是PeMS_Train_Time_L.png和PeMS_Train_Time.png,展示的是模型训练阶段的损失变化与单轮训练耗时,主要用来确认训练过程健康;另一类是PeMS_R39.png和PeMS_R141.png,这是两个代表性监测点(编号 R39、R141)的真实流量与预测流量对比图。
看这类对比图时有三个观察点:第一,预测曲线是否在早晚高峰时段与真实曲线同步上升和回落,交通流的序列相关性很强,如果相位错位,说明时间卷积没有学到延迟关系;第二,平峰时段预测值是否贴近真实值,这里最能暴露模型偏差;第三,突变段的误差方向是什么,是一个方向持续低估还是上下震荡,这决定了你要调时间卷积核宽度还是图卷积阶数。
我一般会同时打开四张图横向看,如果 R39 拟合得很好但 R141 偏差明显,问题多半不在模型结构,而在 R141 所在位置的空间邻接关系——比如它附近传感器稀少,图卷积能聚合到的邻居信息太弱。这时候需要回到邻接矩阵的阈值设置上,而不是盲目调学习率。
5. 避坑:复现 STGCN 时最常遇到的五个翻车点
5.1 现象:TF 2.x 环境直接跑,报AttributeError: module 'tensorflow' has no attribute 'placeholder'
原因:这套代码写在 TF 1.x 时代,main.py和base_model.py里大量使用tf.placeholder、tf.Session、tf.Graph等老接口,TF 2.x 把这些 API 挪到了tf.compat.v1下,默认命名空间里找不到。
解决:最省事的是创建 Python 3.6 + TF 1.15 的独立环境,见 3.2 节。如果项目强制用 TF 2.x,可以在代码文件开头统一加:
import tensorflow.compat.v1 as tf tf.disable_v2_behavior()这样会让 TF 2.x 以 1.x 兼容模式运行,大多数老代码都能跑通。但注意:这只是兜底方案,训练速度会比原生 TF 1.x 略慢。
5.2 现象:运行main.py时提示FileNotFoundError,找不到.npz或.pkl数据文件
原因:压缩包里的PeMS-M.zip还没解压,或者解压后放到了和data_utils.py预期不同的目录。代码内部通常把数据路径拼成./data/PeMS-M/xxx.npz,目录层级差一级都读不到。
解决:先解压,再看main.py里--data参数的默认路径,用绝对路径最稳妥:
unzip PeMS-M.zip -d ./data/ python main.py --data ./data/PeMS-M/PeMS-M.npz提示:如果数据文件下载后是.npz格式,不要手动截断或改名,numpy 对文件头有严格校验,改了后缀容易在读入时报EOFError。
5.3 现象:训练第几个 epoch 后 loss 突然变成NaN
原因:最常见的是学习率过大导致梯度爆炸,其次是数据归一化不规范——流量数据如果不做标准化直接喂进模型,数值跨度大,图卷积里的矩阵乘会把梯度放大几个量级。STGCN 对输入特征(流量、速度、占用率)单位差异很大,必须用训练集的均值方差做 z-score 归一化,而不是简单缩放到 0-1。
解决:检查data_utils.py里是否用了训练集统计量做标准化;确认--learning_rate从 0.001 起,如果还炸就降到 0.0005。这类问题不需要动模型结构,数据预处理修复后通常一轮就能恢复。
5.4 现象:显存占用缓慢涨满,训练中途 OOM
原因:图卷积层会对邻接矩阵做稠密矩阵乘法,监测点数量多、邻接矩阵不够稀疏时,中间的[batch, N, N]中间张量会撑爆显存。另外batch_size设置过大,梯度累积过程也会放大显存峰值。
解决:先看邻接矩阵的稀疏率——np.sum(adj > 0) / adj.size,低于 0.3 就要检查阈值是否设得太小。然后调小batch_size,从 50 降到 32 或 16。不要动模型结构,这个模型设计时就没有依赖超大 batch 才能收敛的特性。
5.5 现象:训练损失下降很慢,预测曲线整体滞后真实值半个周期
原因:时间卷积核宽度太小,感受野覆盖不了交通流的强周期特征。5 分钟一个时间片,核宽度为 3 只能看到前后 15 分钟的变化,而早晚高峰的上升趋势持续时间远超这个量级。
解决:在layers.py里把时间卷积核宽度从 3 加大到 5 或 7,同时确认输入历史窗口是否还是 12 个时间片——核宽度加大后,输入窗口最好也同步扩到 16 或 24,否则边界补齐会引入无效值。每次只动一个参数比较稳,先扩核宽看验证集误差变化,再决定要不要扩窗口。
6. 进阶:换成自己的路网数据与可复用的调参习惯
把这套代码用到自己的数据上,不是直接改个路径就完事,关键在于把原始监测数据整理成三样东西:时间序列矩阵、空间坐标数组、预测目标向量。常见做法是先按 5 分钟聚合原始流量数据,得到二维矩阵flows[n_nodes, n_timestamps],同时准备每个节点的坐标文件,然后复用 2.1 节的函数生成邻接矩阵。数据准备好后,按 7:1.5:1.5 分成训练、验证、测试段,用训练集统计量归一化,再调用data_utils.py里已有的数据加载逻辑。
调参顺序上,我习惯先固定学习率 0.001,按这个顺序逐个调:先调图卷积阶数 K,观察空间依赖是否够远;再调时间卷积核宽度,观察时间感受野;最后调邻接矩阵的阈值和sigma2,这两参数对结果影响最隐蔽,值得做几组对比。每次只改一个参数,保存一组测试集指标,用表格对比,而不是凭感觉来回动。
验证环节不要只看总体指标,要把测试结果按时段拆开看:早高峰、午平峰、晚高峰、夜间分别算 MAE。很多时候总体指标很好看,但夜间低流量时段的 MAPE 高得离谱——流量接近 0 时,分子上差个位数就能让百分比翻倍。交通工程里通常更关注早晚高峰的表现,你把清晨和傍晚的误差控制住,整体评分自然会被拉上去。
从那以后我每次换数据集,都强制自己先花一小时检查数据时间对齐和邻接矩阵连通性,再起训练,省下的排错时间远大于检查成本。如果你想用最短时间跑通一套完整的时空图卷积方案,这个压缩包是值得逐行读一遍的。希望帮到你。
本文还有配套的精品资源,点击获取