☰
STGCN时空图卷积网络实战:路网流量预测与代码复现
2026/10/14 9:51:05 网站建设 项目流程

简介:面向交通流预测与时空图卷积网络学习者,这份资源提供了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 分钟
图卷积阶数 K3
时间卷积核宽度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 时,分子上差个位数就能让百分比翻倍。交通工程里通常更关注早晚高峰的表现,你把清晨和傍晚的误差控制住,整体评分自然会被拉上去。

从那以后我每次换数据集,都强制自己先花一小时检查数据时间对齐和邻接矩阵连通性,再起训练,省下的排错时间远大于检查成本。如果你想用最短时间跑通一套完整的时空图卷积方案,这个压缩包是值得逐行读一遍的。希望帮到你。

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

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

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

立即咨询