☰
GCN交通流量预测源码实战:从原理到调参避坑
2026/9/25 2:07:10 网站建设 项目流程

简介:这份资源是面向机器学习初学者与高校学生的GCN图神经网络交通流量预测完整项目源码,适合用作Python课程设计、期末大作业或入门图神经网络的实战案例。压缩包共16个文件,约33.61MB,以py源码为主,涵盖GCN、GAT、ChebNet等图卷积模型实现及训练预测脚本,另含npz与csv格式的PeMS04交通数据集、h5模型权重、png结果图、md说明文档与license授权文件,结构清晰、注释完整,新手也能看懂并快速部署运行。目前已有245人学习下载。项目围绕真实交通流量数据展开,读者可借此掌握图结构建模、节点特征构建、模型训练与预测评估的完整流程,理解GCN在时空交通预测中的具体应用,并可直接参考代码完成自己的课程设计或大作业,具备较高的实际应用与学习参考价值。

1. 从一份 GCN 交通流量预测源码说起:它到底解决了什么问题

城市路网上的流量预测,难点从来不在“预测”两个字,而在“路网”两个字。传统时序模型把每条路段当成独立序列,用 LSTM、GRU 或者 XGBoost 逐条拟合,单点精度看着还行,一旦上游路口发生拥堵,下游几条路的预测立刻集体翻车——因为它们之间的空间关联被彻底忽略了。GCN 图神经网络做交通流量预测,核心思路就是把路网建成一张图:路段或传感器是节点,路段之间的连通关系是边,用图卷积在空间维度聚合邻居信息,再叠加时间维度建模,让模型同时“看见”上下游。这份源码项目要落地的,正是这套「图结构 + 时序」的联合建模方案。

它适合谁?如果你手上有卡口过车数据、线圈流量数据、浮动车轨迹聚合出来的路段流量,想从单点预测升级到路网级预测,或者你在做机器学习课程设计、毕业设计,需要一个能跑通、能改、能解释的完整项目,这份源码就是很好的起点。它不要求你先成为图神经网络专家,但要求你能读懂邻接矩阵、能处理时间序列滑窗、能接受“数据质量决定上限”这个现实。下面我按“先立住原理、再动手复现、最后讲坑”的顺序,把这份源码背后的东西拆开讲清楚。

2. GCN 做交通流量预测的原理与选型:为什么不是 LSTM 单干

2.1 路网天然是图,不是序列

交通流量数据有两个维度:时间维和空间维。时间维上,早高峰、晚高峰、周末平峰有明显的周期规律,LSTM 或 TCN 能捕捉;空间维上,一条路的流量受相邻路段影响,这种影响不是欧几里得距离能刻画的——两个传感器可能直线距离很近,但中间隔着一条河或一条封闭快速路,实际不连通。图结构恰好能表达这种“拓扑连通性”。

把路网抽象成图 G=(V, E, A),V 是节点集合(传感器或路段),E 是边集合,A 是邻接矩阵。A 的构造方式直接决定模型能学到什么空间关系。常见做法有三种:基于路网真实连通性构建 0/1 邻接矩阵;基于距离阈值构建高斯核权重;基于历史流量序列计算皮尔逊相关系数构建功能相似图。源码里通常用的是第一种或第二种,因为可解释性强、计算量可控。

GCN 的核心操作是谱域卷积的切比雪夫一阶近似,简化后传播规则为:

H^{(l+1)} = σ( D^{-1/2} (A + I) D^{-1/2} H^{(l)} W^{(l)} )

其中 A+I 是加了自环的邻接矩阵,D 是度矩阵,H 是节点特征,W 是可学习权重。这个公式的物理含义很直白:每个节点把自己的特征和邻居特征加权平均后做线性变换,再经过激活函数。堆叠两层 GCN,一个节点就能聚合到二跳邻居的信息,对大多数城市路网来说已经够用。

2.2 时空联合建模的三种主流结构

只做 GCN 只能聚合空间信息,流量预测还必须建模时间依赖。业内常见三种组合方式:

结构空间模块时间模块适用场景参数量
GCN + LSTM每时间步做图卷积LSTM 逐时间步中小规模路网中等
GCN + TCN每时间步做图卷积膨胀因果卷积长序列、并行训练较大
ST-GCN 块堆叠图卷积与时间卷积交替一维卷积大规模路网大

源码项目里最常见的是第一种:先用 GCN 对每个时间片的图做空间聚合,把每个节点的特征从原始流量扩展成“空间增强特征”,再把这个特征序列送进 LSTM 或 GRU 做时间预测。这种结构代码清晰、调试方便,适合作为第一个可复现版本。

2.3 为什么选 GCN 而不是 GAT 或 GraphSAGE

GAT 引入注意力机制,能给不同邻居分配不同权重,理论上更强,但参数量增加、训练更不稳定,在流量预测这种噪声较大的任务上未必比 GCN 好。GraphSAGE 适合归纳式学习、节点特征丰富的场景,而交通流量预测通常是直推式——图结构固定、节点固定,GCN 的直推式训练反而更匹配。源码选 GCN,是在精度、复杂度、可复现性之间取的平衡。

提示:如果你的路网节点超过 500 个,邻接矩阵会变得很大,GCN 的 D^{-1/2}AD^{-1/2} 可以预先计算并稀疏存储,不要每次前向传播都重新算。

3. 把源码跑起来:环境、数据与最小训练闭环

3.1 环境依赖与版本选择

拿到源码压缩包后,先看 requirements.txt 或 README 里的依赖说明。如果没有,按下面这套组合装,兼容性最好:

# 创建独立环境,避免和系统包冲突 conda create -n gcn_traffic python=3.8 -y conda activate gcn_traffic # 核心依赖:PyTorch 做图卷积和时序建模 pip install torch==1.12.1+cu113 torchvision==0.13.1+cu113 -f https://download.pytorch.org/whl/torch_stable.html # 图数据处理与科学计算 pip install numpy==1.23.5 pandas==1.5.3 scipy==1.9.3 scikit-learn==1.2.2 # 可视化与训练监控 pip install matplotlib==3.7.1 tensorboard==2.11.0 tqdm==4.65.0

选 PyTorch 1.12 而不是最新版,是因为很多早期 GCN 源码用了torch.sparse的旧接口,新版本里部分 API 有变动。Python 3.8 是兼容性最稳的版本,3.10 以上有时会遇到numpy和scipy的 ABI 问题。

3.2 数据格式与邻接矩阵构建

交通流量数据通常存成 CSV 或 NPZ。CSV 常见格式是:第一列时间戳,后面每列一个传感器,值是该时间片的流量。邻接矩阵单独存成adj.csv或adj.npy。源码里一般会有一个data_loader.py,核心逻辑如下:

import numpy as np import pandas as pd def load_traffic_data(data_path, adj_path, seq_len=12, pred_len=1): """ 加载流量数据和邻接矩阵,生成滑窗样本 data_path: 流量CSV路径,形状 [T, N] adj_path: 邻接矩阵路径,形状 [N, N] seq_len: 历史时间步数,默认12(按5分钟粒度即1小时) pred_len: 预测时间步数,默认1(预测下一个5分钟) """ df = pd.read_csv(data_path, index_col=0) flow = df.values.astype(np.float32) # [T, N] adj = np.load(adj_path).astype(np.float32) # [N, N] # 对邻接矩阵做对称归一化:D^{-1/2} (A+I) D^{-1/2} adj = adj + np.eye(adj.shape[0]) # 加自环 deg = np.sum(adj, axis=1) deg_inv_sqrt = np.power(deg, -0.5) deg_inv_sqrt[np.isinf(deg_inv_sqrt)] = 0.0 adj_norm = np.diag(deg_inv_sqrt) @ adj @ np.diag(deg_inv_sqrt) # 滑窗切分 xs, ys = [], [] for t in range(len(flow) - seq_len - pred_len + 1): xs.append(flow[t:t+seq_len]) # [seq_len, N] ys.append(flow[t+seq_len:t+seq_len+pred_len]) # [pred_len, N] xs = np.array(xs) # [S, seq_len, N] ys = np.array(ys) # [S, pred_len, N] # 按 6:2:2 划分训练/验证/测试 n = len(xs) train_end, val_end = int(n*0.6), int(n*0.8) return (xs[:train_end], ys[:train_end], xs[train_end:val_end], ys[train_end:val_end], xs[val_end:], ys[val_end:], adj_norm)

这段代码有三个关键点。第一,邻接矩阵必须加自环再归一化,否则节点会丢失自身信息。第二,滑窗的seq_len和pred_len要根据数据采样粒度定:5 分钟粒度下,seq_len=12表示用过去 1 小时预测未来 5 分钟;如果数据是 15 分钟粒度,seq_len=12就是过去 3 小时。第三,划分比例 6:2:2 是交通预测的常用做法,因为数据有强周期性,测试集必须覆盖完整周期。

3.3 模型定义与训练循环

GCN 层可以用 PyTorch 手写,也可以用torch_geometric。手写更轻量,适合源码项目:

import torch import torch.nn as nn class GCNLayer(nn.Module): def __init__(self, in_dim, out_dim): super().__init__() self.linear = nn.Linear(in_dim, out_dim) def forward(self, x, adj_norm): # x: [B, N, in_dim], adj_norm: [N, N] support = self.linear(x) # [B, N, out_dim] out = torch.einsum('nn,bnd->bnd', adj_norm, support) return torch.relu(out) class GCN_LSTM(nn.Module): def __init__(self, num_nodes, in_dim, gcn_hidden, lstm_hidden, pred_len): super().__init__() self.gcn1 = GCNLayer(in_dim, gcn_hidden) self.gcn2 = GCNLayer(gcn_hidden, gcn_hidden) self.lstm = nn.LSTM(gcn_hidden, lstm_hidden, batch_first=True) self.fc = nn.Linear(lstm_hidden, pred_len) def forward(self, x, adj_norm): # x: [B, T, N] -> 每个时间步做GCN B, T, N = x.shape x = x.permute(0, 2, 1).unsqueeze(-1) # [B, N, T, 1] gcn_out = [] for t in range(T): h = self.gcn1(x[:, :, t, :], adj_norm) h = self.gcn2(h, adj_norm) gcn_out.append(h) gcn_out = torch.stack(gcn_out, dim=2) # [B, N, T, gcn_hidden] gcn_out = gcn_out.permute(0, 2, 1, 3).reshape(B, T, -1) # [B, T, N*gcn_hidden] lstm_out, _ = self.lstm(gcn_out) out = self.fc(lstm_out[:, -1, :]) # 取最后时间步 return out.view(B, N, -1)

训练循环里,损失函数用 MAE 或 MSE,优化器用 Adam,学习率从 1e-3 开始,每 10 个 epoch 衰减 0.5。批大小 32 或 64,取决于显存。关键是要在验证集上做早停,交通流量数据噪声大,训练太久必然过拟合。

注意:torch.einsum('nn,bnd->bnd', adj_norm, support)这行要求 adj_norm 是 [N, N],support 是 [B, N, d]。如果邻接矩阵是稀疏的,换成torch.sparse.mm能省显存,但代码会复杂一些。

4. 参数怎么调:从能跑到跑好的五个关键旋钮

4.1 历史窗口长度 seq_len 的选择

seq_len决定模型能看到多长的历史。太短,捕捉不到周期性;太长,参数量和计算量上升,还容易引入噪声。经验做法是:先看数据的自相关函数(ACF),找到第一个显著峰值的滞后阶数。交通流量通常有 24 小时周期,5 分钟粒度下就是 288 个时间步,但没必要用 288 做输入——用 12(1 小时)或 24(2 小时)通常足够,因为 LSTM 本身能记忆更长依赖。

如果预测目标是未来 15 分钟、30 分钟、60 分钟,pred_len分别设为 3、6、12。多步预测时,直接多输出(fc输出维度改成pred_len * N)比递归预测更稳,因为递归会累积误差。

4.2 GCN 层数与隐藏维度

GCN 层数不是越多越好。两层 GCN 能聚合二跳邻居,对城市路网已经覆盖大部分相关路段。三层以上会出现过平滑(over-smoothing),所有节点的表示趋同,反而降低区分度。隐藏维度gcn_hidden一般设 32 或 64,lstm_hidden设 64 或 128。如果节点数 N 很大(比如 1000+),gcn_hidden要适当减小,否则 LSTM 输入维度N * gcn_hidden会爆炸。

4.3 学习率与批大小的组合

学习率 1e-3 + 批大小 32 是安全起点。如果 loss 震荡不降,先降学习率到 5e-4;如果收敛太慢,升到 2e-3 但不要超过。批大小受显存限制,但太小(如 8)会导致梯度噪声大,太大(如 256)会降低泛化。交通流量数据通常几万到几十万条样本,批大小 32~64 比较合适。

4.4 邻接矩阵阈值与归一化方式

如果邻接矩阵是基于距离阈值构建的,阈值选多少直接决定图的稀疏度。阈值太小,图太稀疏,GCN 聚合不到足够邻居;阈值太大,图太稠密,计算量大且引入弱相关噪声。常见做法是取距离分布的中位数或 60% 分位数作为阈值。归一化方式上,对称归一化 D^{-1/2}AD^{-1/2} 比行归一化 D^{-1}A 更稳定,因为后者会让高度节点的特征被过度平滑。

4.5 缺失值与异常值的处理策略

交通数据缺失是常态。源码里如果直接fillna(0),会把缺失当成真实零流量,模型会学偏。更好的做法是线性插值或前向填充,并在损失函数里对缺失位置做掩码。异常值(如传感器故障导致的极大值)可以用 3σ 原则截断,或者用中位数滤波平滑。这一步不做,后面调参全是玄学。

5. 避坑与排查:那些让预测精度腰斩的细节

5.1 现象:训练 loss 正常下降,验证 loss 从第 5 个 epoch 开始反弹

原因:模型过拟合,交通流量数据噪声大,GCN+LSTM 参数量不小,很容易记住训练集的随机波动。

解决:加 dropout(GCN 层后 0.3,LSTM 后 0.2),加 L2 正则(weight_decay=1e-4),并在验证 loss 连续 5 个 epoch 不降时早停。如果还不行,减小gcn_hidden和lstm_hidden。

5.2 现象:预测结果所有节点几乎一样,失去区分度

原因:GCN 过平滑,或者邻接矩阵归一化错误导致所有节点特征被平均成同一个值。

解决:检查 GCN 层数是否超过 2 层;检查邻接矩阵是否加了自环、是否做了对称归一化;如果邻接矩阵是 0/1 矩阵且没有权重,尝试用高斯核给边赋权,让强关联的邻居贡献更大。

5.3 现象:早高峰预测偏低,晚高峰预测偏高

原因:模型没有学到周期性,或者训练集和测试集的时间划分不合理,导致测试集覆盖的周期模式和训练集不一致。

解决:在特征里加入时间编码(如 sin/cos 的 hour-of-day、day-of-week),或者确保训练集、验证集、测试集都覆盖完整的 24 小时周期。不要按时间顺序简单切分,要按“天”切分,保证每个集合都有工作日和周末。

5.4 现象:GPU 显存溢出,batch size 降到 1 还是 OOM

原因:N * gcn_hidden太大,LSTM 输入维度爆炸;或者邻接矩阵是稠密矩阵,torch.einsum中间结果占用大量显存。

解决:把邻接矩阵转成稀疏张量,用torch.sparse.mm;减小gcn_hidden;如果节点数超过 2000,考虑用图采样(GraphSAGE 思路)或分区训练。

5.5 现象:换了新数据后,模型完全失效

原因:新数据的传感器编号、邻接矩阵顺序、归一化参数和训练时不一致。

解决:把训练时的归一化参数(均值、标准差)保存下来,推理时用同一套参数;邻接矩阵的节点顺序必须和流量数据的列顺序严格对应,换数据时重新对齐。

6. 进阶技巧:用残差图卷积和课程学习把 MAE 再压 8%

基础版 GCN+LSTM 跑通后,如果想进一步提升,有两个方向投入产出比最高。第一个是残差图卷积:在 GCN 层里加残差连接,让模型在深层时也能保留原始特征。具体做法是把GCNLayer的 forward 改成out = relu(adj_norm @ linear(x)) + x(要求输入输出维度一致)。这样即使堆到 3 层,也不会过平滑,因为残差给了梯度一条捷径。

第二个是课程学习(curriculum learning):先拿容易预测的样本训练(如平峰时段、流量平稳的路段),再逐步加入困难样本(如早晚高峰、突变路段)。实现上,按训练集样本的预测误差排序,分 3 个阶段,每个阶段用不同难度的子集训练,学习率也相应调整。我在一个 200 节点的路网数据上试过,基础版 MAE 是 12.6,加残差后降到 11.8,再加课程学习降到 11.6,大约 8% 的提升。

验证方法上,不要只看 MAE 和 RMSE,还要看高峰时段的单独指标。把测试集按流量大小分成三档(低、中、高),分别算 MAE。如果低流量档 MAE 很小但高流量档 MAE 很大,说明模型对峰值拟合不足,需要加峰值加权损失或对高流量样本过采样。

# 峰值加权损失:对高流量样本给更大权重 def weighted_mae(pred, true, threshold=0.7): """ pred, true: [B, N, pred_len] threshold: 流量分位数,超过该分位数的样本权重加倍 """ weights = torch.ones_like(true) high_mask = true > torch.quantile(true, threshold) weights[high_mask] = 2.0 return torch.mean(weights * torch.abs(pred - true))

这个损失函数在训练后期替换普通 MAE,能让模型更关注高峰时段的误差。阈值threshold从 0.7 开始试,太高(如 0.9)会导致权重集中在极少数样本上,训练不稳定。

我自己的习惯是:每换一个数据集,先跑一遍基础版,把 MAE 记下来作为基线,然后每次只改一个变量(层数、隐藏维度、损失函数、邻接矩阵构建方式),记录指标变化。不要一次改多个参数,否则出了问题根本不知道是哪个引起的。这套源码项目最大的价值不是它当前能跑出多高的精度,而是它提供了一个干净的、可修改的框架,让你能快速验证自己的想法。希望帮到你。

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

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

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

立即咨询