简介:面向智能交通、深度学习与图神经网络方向的研究者和开发者,这份 PDF 系统阐述基于图卷积神经网络(GCN)的交通流量预测方法。内容涵盖 GCN 基本原理、谱图方法建模城市道路网络、提取拓扑结构与时空特征,并介绍车流量与车速预测的对比实验。实验表明该方法优于传统统计模型及部分深度学习方法,适合作为课程设计、毕业设计或论文研读的参考材料。资源为单个 PDF 文件,大小仅 1.18MB,携带方便。文件包含论文原文、公式推导、实验图表和参考文献,可完整看到从图结构建模、邻居信息聚合到结果验证的技术路线;对于希望快速了解 GCN 在非欧几里得路网数据上如何应用的读者,是一份高密度的入门和复现参考资料。目前已有 319 人学习下载。
1. 图卷积凭什么比普通卷积更适合交通流量预测:一张路网图就是最好的答案
城市路网本质上是图结构——交叉路口是节点,道路是连边,几百个传感器散落在路网上记录着每个位置的流量、速度和密度。图卷积神经网络(GCN)把卷积运算从规则的二维网格搬到不规则的图上,让模型真正理解“邻近路口的拥堵会沿着道路传播”这个基本事实。比起传统的 CNN、LSTM 一类方法,GCN 天然支持任意拓扑结构,对新增路口和缺失传感器也更宽容,正是交通流量预测这类时空任务的主流解法。
这篇文章把整个落地路径拆开讲:从路网拓扑怎么变成图数据开始,到 GCN 模型在 PyTorch 里的最少实现,再到训练参数怎么调、数据坑在哪,最后是验证模型效果的正确姿势。无论你是刚接触图神经网络的研究生,还是想把它跑在真实路网数据上的工程师,照着这套思路都能在本地把基线模型跑通,再逐步加自己的改进。
2. 路网数据怎么变成图:邻接矩阵、特征矩阵和滑动窗口
2.1 邻接矩阵:用 0 和 1 表达“哪条路和哪条路连着”
图神经网络和普通神经网络最本质的区别,就在于输入里多了一张“连接关系表”——邻接矩阵 A。A 的大小是 N×N,N 是传感器节点的数量。A 的第 i 行第 j 列表示节点 i 和节点 j 是否直接相连:1 代表相连,0 代表不相连。交通流量预测里,最常用的建图方式是基于路网距离:两个传感器之间的距离小于阈值(常见做法是取所有距离的均值或 80% 分位数)就认为它们相连。
import numpy as np from scipy.spatial.distance import cdist # 假设 sensors 是一个 N×2 的数组,每行是传感器的经纬度坐标 # 步骤1:计算所有传感器两两之间的距离矩阵 dist_matrix = cdist(sensors, sensors, metric='euclidean') # 步骤2:按距离阈值建邻接矩阵,阈值取距离的均值 threshold = np.mean(dist_matrix) adj_mx = (dist_matrix <= threshold).astype(int) # 步骤3:自己到自己的连接不算,对角线置0 np.fill_diagonal(adj_mx, 0) # 步骤4:删除没有任何邻居的孤立节点,否则 GCN 的消息传递会失效 valid_idx = np.where(adj_mx.sum(axis=1) > 0)[0] adj_mx = adj_mx[np.ix_(valid_idx, valid_idx)]这段代码的关键在步骤 2 的阈值。阈值太小,图会碎成很多互不相连的小块,信息传不出去;阈值太大,图变成全连接,GCN 退化成全局平均池化,空间局部性就没了。我一般先把距离矩阵的直方图画出来,看分布集中在哪,再取 75% 到 85% 分位数,而不是无脑用均值。步骤 4 是血泪经验——有些传感器设备装了但数据一直为空,或位置记录有问题导致距离异常大,不删掉它们,模型在训练时会对这些孤立节点产生 NaN 梯度。
2.2 特征矩阵:把流量、速度和占有率装进一个三维张量
建好图结构之后,要准备每个节点在每个时刻的特征。交通流量预测常见的特征组合是流量(veh/h)、平均速度(km/h)和占有率(%)。这三个量一起给,比单用流量效果好得多,因为流量稀疏时段波动很大,速度反而稳定,模型能从多模态信号里学到互补的信息。
特征矩阵的常见组织方式是 shape = (样本数, 节点数, 特征维度, 时间步长)。PyTorch 里更习惯用 (B, N, F, T) 或 (B, T, N, F) 两种排法,我推荐后者,因为和 LSTM、GRU 这类时序模型的输入格式更贴近,后续接时序模块时不需要频繁做 permute。
# 假设 raw_data 的 shape 是 (N, T_total, F),N 是节点数,T_total 是总时间步,F 是特征数(这里为3) # 先做归一化,再切滑窗 mean = raw_data.mean(axis=(0, 1), keepdims=True) std = raw_data.std(axis=(0, 1), keepdims=True) + 1e-6 data_norm = (raw_data - mean) / std def create_samples(data, input_steps=12, pred_steps=3): N, T_total, F = data.shape X, Y = [], [] for t in range(T_total - input_steps - pred_steps): x = data[:, t:t + input_steps, :] # 过去12个时间步 y = data[:, t + input_steps:t + input_steps + pred_steps, 0] # 预测未来3步的流量 X.append(x.transpose(1, 0, 2)) # 转成 (input_steps, N, F) Y.append(y.transpose(1, 0)) # 转成 (pred_steps, N) return np.array(X), np.array(Y)这里的 X 转成了 (T, N, F) 而不是 (N, T, F),原因是后面要接 GRU——PyTorch 的 GRU 要求输入是 (seq_len, batch, input_size),把时间步长放在第一维可以直接喂给 GRU 处理时间依赖,GCN 再在节点维度上做空间聚合。pred_steps 取 3 对应预测未来 15 分钟(假设时间间隔为 5 分钟),这是交通流量预测最常见的多步预测配置。如果要预测 30 分钟或 1 小时,直接把 pred_steps 调大,但误差会明显上涨,这是交通流本身的不可预测性决定的。
2.3 归一化的两个层级:全局归一化还是按节点归一化
很多人在归一化这里翻车。交通流量数据有很强的周期性:同一路口的工作日早高峰车流可能是凌晨的 20 倍,不同路口之间的绝对数值差异也很大——主干道流量普遍在 5000+,支路可能就 500。如果用全局均值做归一化,主干路和支路会被压缩到完全不同的数值区间,GCN 做消息传递时,支路的特征会被主干路的信息淹没。
我一般按节点做归一化,也就是对每个传感器单独计算均值和标准差,而不是所有节点共用一个。这样每个节点的流量都落在差不多的数值范围内,模型才能平等对待所有路口的信息。
# 按节点归一化 (N, T, F) node_mean = raw_data.mean(axis=1, keepdims=True) # shape (N, 1, F) node_std = raw_data.std(axis=1, keepdims=True) + 1e-6 data_norm = (raw_data - node_mean) / node_std按节点归一化的代价是:预测出来的值要乘回节点自己的标准差再加均值才能得到实际流量——反归一化时千万别用错节点,否则预测曲线和真实曲线对不上,画出来怎么看怎么别扭。这是个只在验证阶段才暴露出来的坑,后面避坑那一章专门写。
3. 用 PyTorch 实现一个 GCN+GRU 的交通流量预测模型
3.1 GCN 层:消息传递就是邻居特征的加权求和
GCN 层的核心思想非常简单:把每个节点自己的特征和邻居节点的特征加权求和,再经过一次线性变换,得到新的节点表示。公式可以简化为 H' = σ(D̂⁻¹ᐟ² Â D̂⁻¹ᐟ² H W),其中 Â = A + I 是加了自环的邻接矩阵(让节点聚合时包含自己),D̂ 是 Â 的度矩阵(每个节点的邻居数量),左右乘 D̂⁻¹ᐟ² 相当于对邻居数量多的节点做归一化,防止图里节点的度分布不均衡导致数值不稳定。
这段公式看起来抽象,落实到代码里其实很直接。关键是提前算好归一化后的邻接矩阵,不要每次训练都重新计算。
import torch import torch.nn as nn class GCNLayer(nn.Module): def __init__(self, in_features, out_features): super().__init__() self.linear = nn.Linear(in_features, out_features) def forward(self, x, adj_norm): # x: (B, N, F_in) # adj_norm: (N, N),对称归一化后的邻接矩阵 support = self.linear(x) # (B, N, F_out) out = torch.einsum('bnf,nm->bmf', support, adj_norm) return outtorch.einsum('bnf,nm->bmf', support, adj_norm)的意思就是:把 support 里每个节点的特征,按 adj_norm 的权重加权到它的邻居节点上。这一步是 GCN 的全部核心。很多实现会把这一层包得特别花哨,搞一堆 Dropout、残差连接、BN,但模型的泛化能力其实主要来自数据质量、邻接矩阵的正确性和合理的训练方式,层结构堆花哨帮助有限。
3.2 对称归一化:必须提前计算好,且只有一种正确写法
GCN 的邻接矩阵归一化有两种常见写法:随机游走归一化和对称归一化。交通流量预测里,我推荐对称归一化,原因是它能保持邻接矩阵的对称性——流量传播在路网里应该是双向对称的(除非是单行道),对称归一化可以让模型不对节点顺序产生偏差。
def symmetric_normalize(adj): """对称归一化: D^-0.5 * A * D^-0.5 """ adj = adj + np.eye(adj.shape[0]) # 加自环 d = np.sum(adj, axis=1) # 度向量 d_inv_sqrt = np.power(d, -0.5) d_inv_sqrt[np.isinf(d_inv_sqrt)] = 0.0 # 保护孤立节点 d_mat = np.diag(d_inv_sqrt) adj_norm = d_mat @ adj @ d_mat return torch.FloatTensor(adj_norm)一个极其隐蔽的坑是:度矩阵 D 必须在加自环之后计算,而不是先算原图的度再加 1。数学上讲,如果在加自环之前算度,那么自环贡献的权重会被错误归一化,导致模型对自己特征的重视程度偏大,对邻居特征的传播权重偏低。线上跑出来的效果就是:模型总是倾向于保持上一时刻的趋势,对突变反应迟缓,看着像“惯性太大”。这个问题不对比实验基本发现不了,很少有人会怀疑是归一化写错了。
3.3 完整模型结构:两层 GCN 捕捉空间依赖,GRU 处理时间序列
空间和时间是交织在一起的。GCN 负责捕捉同一时间步上节点之间的空间相关性——比如东四环堵车,西四环 20 分钟后也会受影响。GRU 负责捕捉单节点的时间演化——比如一个路口早高峰的流量曲线是平滑变化的。把两者串起来是 STGCN、ASTGCN 这一类时空同步网络的通用做法。
import torch.nn.functional as F class GCN_GRU(nn.Module): def __init__(self, n_nodes, in_features, hidden_dim, out_steps): super().__init__() self.gcn1 = GCNLayer(in_features, hidden_dim) self.gcn2 = GCNLayer(hidden_dim, hidden_dim) self.gru = nn.GRU(hidden_dim, hidden_dim, batch_first=True) self.regressor = nn.Linear(hidden_dim, 1) def forward(self, x, adj_norm): # x: (B, T_in, N, F_in) B, T, N, F = x.shape x = x.reshape(B * T, N, F) h = F.relu(self.gcn1(x, adj_norm)) h = F.relu(self.gcn2(h, adj_norm)) h = h.reshape(B, T, N, -1) # 把节点特征展平成时间序列特征,喂给GRU h = h.permute(0, 2, 1, 3).reshape(B * N, T, -1) out, _ = self.gru(h) # (B*N, T, hidden) # 取最后一个时间步做多步回归 out = self.regressor(out[:, -1, :]) # (B*N, 1) out = out.reshape(B, N, 1) out = out.repeat(1, 1, 1) # single-step output return out这个模型的 forward 流程可以拆成四段:第一段把 (B, T, N, F) 压成 (B*T, N, F),这样 GCN 可以一次性处理所有时间步的图卷积,效率更高;第二段经过两层 GCN 实现空间传播;第三段把节点维度放到序列维度上,让 GRU 对每个节点的特征沿时间轴建模;第四段取 GRU 最后一个时间步的输出,接一层线性层回归流量值。注意这里为了演示清晰,模型只输出未来一个时间步,实际做多步预测时,常见的做法是加一个 seq2seq 架构,或者直接让输出维度等于 pred_steps。
3.4 训练一个最小可用模型:损失函数、优化器和完整的训练循环
主流的交通流量预测基线一般用 MAE 或 MSE 作为损失函数。MSE 收敛快但对峰值流量惩罚过重,导致模型学得保守;MAE 对异常值更鲁棒但梯度方向恒定,收敛慢。我习惯用 MAE + 一个非常小的 MSE 权重做混合损失,既让模型关注绝对误差,又保留一点梯度变化,效果比单用任何一个稳定。
model = GCN_GRU(n_nodes=N, in_features=3, hidden_dim=32, out_steps=3) optimizer = torch.optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-5) loss_fn = lambda pred, y: torch.abs(pred - y).mean() + 0.1 * ((pred - y) ** 2).mean() for epoch in range(50): model.train() total_loss = 0 for x_batch, y_batch in train_loader: # x_batch: (B, T_in, N, F), y_batch: (B, N) optimizer.zero_grad() pred = model(x_batch, adj_norm) loss = loss_fn(pred.squeeze(-1), y_batch) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0) optimizer.step() total_loss += loss.item() if (epoch + 1) % 10 == 0: print(f"Epoch {epoch+1}/{50}, Loss: {total_loss / len(train_loader):.4f}")这里面最容易被忽略的是clip_grad_norm_。GCN 的消息传递会让梯度在邻居节点之间反复传播,深层模型或多层 GCN 堆叠时,梯度很容易在节点间累积而爆炸。clip 的半径设 5.0 是我常用的值,数据尺度不同可以调,但每次都记得加。训练轮次设为 50 并不够模型完全收敛,交通数据往往要 150-300 轮才稳定,我先跑 50 轮看 loss 是否快速下降,快速下降说明代码没问题,再放开到全量训练。
4. 训练与调参:数据划分、学习率和训练轮次怎么定
4.1 时间上的数据划分:不能随机打乱,只能按时间切
交通流量数据是典型的时间序列,划分训练集和测试集时,最忌讳随机 shuffle。随机打乱会引入未来信息泄漏——训练集里的某一天凌晨数据可能紧挨着测试集的某天早高峰数据,模型其实已经“见过”了测试集的趋势,这在真实部署时完全不可能发生。
正确做法是按时间顺序切分,常见的比例是 7:1.5:1.5——前 70% 做训练,接下来 15% 做验证,最后 15% 做测试。验证集的作用是选模型、调超参,测试集只在全部定稿后跑一次,否则你就是在拿测试集调参,得到的指标虚高。
train_ratio, val_ratio = 0.7, 0.15 train_end = int(len(samples) * train_ratio) val_end = int(len(samples) * (train_ratio + val_ratio)) train_X, train_Y = samples[:train_end] val_X, val_Y = samples[train_end:val_end] test_X, test_Y = samples[val_end:]一个更严谨的做法是每次预测不能跨越“天”的边界。比如用过去 60 分钟预测未来 15 分钟,样本的切分如果允许前一天 23:50 的数据预测后一天 00:05 的流量,模型会学到“每天最后一刻的流量可以预测第二天开头流量”的伪规律——这在实际系统里是边界点,数据量极少但影响不小。我在做数据预处理时,直接按天为单位切分,对每天内部生成滑动窗口样本,然后按天划分 train/val/test,彻底杜绝跨天样本。
4.2 学习率:0.001 起步,观察 loss 停滞再下降
图卷积网络的训练动态和普通 CNN 不太一样。CNN 的学习率可以从 0.01 起步,GCN 因为消息传递本身就有平滑效应,梯度更新幅度容易过大,一上来就用大学习率经常直接跑到 NaN。我所有 GCN 相关的任务,都从 0.001 起步。
怎么判断学习率合不合适?跑 20 轮,看 loss 的下降曲线。如果 loss 在头 5 轮骤降然后开始震荡,说明学习率偏大,降到 0.0005;如果 loss 前 20 轮几乎不降,说明学习率太小,提到 0.003。这里没有标准答案,数据量、节点数、特征维度都会影响最优学习率。最靠谱的还是跑一个对数网格找最优值:0.0003、0.001、0.003 各跑 30 轮,选验证集 loss 最低的那个。
学习率衰减策略我用 CosineAnnealingLR,比 StepLR 省心,不需要指定衰减步长。总轮次设 200,最小学习率设 1e-5,让模型在后期能慢慢收敛到更平缓的局部最优解。
4.3 Batch Size:图结构决定了 batch 不能太小
交通流量预测的样本格式是 (B, T, N, F),其中 N 是节点数,一个 batch 里所有样本共享同一张邻接矩阵和图结构。Batch Size 的影响比普通 CV 任务更大——因为每个 batch 都要做一次全图的邻接矩阵乘法,batch 太小则 GPU 利用率低,batch 太大则显存占用高且梯度更新频率太低。
我实测下来 16 到 64 之间是比较合理的区间。城市级数据集 500 个节点左右,T=12,F=3,batch 32,显存占用大约 2-4 GB,一般训练卡都能跑。如果节点数到几千,batch 降到 8 甚至 4,配合梯度累积来模拟更大的 batch。
# 梯度累积模拟大 batch accumulation_steps = 4 for i, (x_batch, y_batch) in enumerate(train_loader): pred = model(x_batch, adj_norm) loss = loss_fn(pred, y_batch) / accumulation_steps loss.backward() if (i + 1) % accumulation_steps == 0: optimizer.step() optimizer.zero_grad()注意梯度累积时要先把 loss 除以累积步数,否则梯度会被放大 accumulation_steps 倍,效果相当于把学习率放大了一个系数,模型会不小心训飞。这个细节我在早期踩过坑,损失函数曲线前 100 轮看着正常,之后突然暴涨,排除了很久才发现是梯度累积写错了。
4.4 早停:用验证集 loss 决定何时停止,别拍脑袋定轮次
固定训练 200 轮在数据量大的时候不会有问题,但小数据集上,GCN 后期很容易在训练集上过拟合,验证集 loss 反而开始上涨。早停是最便宜、最有效的正则化手段。
best_val_loss = float('inf') patience = 15 wait = 0 for epoch in range(200): train_loss = train_one_epoch() val_loss = evaluate(val_loader) if val_loss < best_val_loss: best_val_loss = val_loss torch.save(model.state_dict(), 'best_model.pth') wait = 0 else: wait += 1 if wait >= patience: print(f"Early stop at epoch {epoch}") breakpatience 设 15 是比较稳的值。交通数据一天之内有早晚高峰,验证集上的 loss 天然有日内波动,patience 太小容易在早上刚过高峰、loss 暂时偏高时误判为过拟合而提前停止。记得每次验证集 loss 降低都要保存模型权重,因为最后拿去测试的应该是历史最优的权重,而不是最后一个 epoch 的权重——最后一个 epoch 的模型往往已经过拟合不少了。
5. 交通流量预测里最容易翻车的 5 个坑
5.1 数据形状不统一:模型在训练时静默出错,推理时彻底崩
现象:训练时 loss 偶尔出现 NaN,或者训练能跑但预测结果全部是同一个常数,画出来是一条横线。
原因:数据形状 (B, T, N, F) 和 (B, N, T, F) 混淆,permute 维度写错后,流量特征张量可能把时间维和节点维交错在一起。GCN 的 einsum 对维度不匹配会直接报错,但有些代码因为用了 reshape 而不是 permute,张量在内存里被重新解释,形状合法但含义全错,模型把不同节点的数据混着训练,学不到空间结构的任何信息。
解决:在数据进入模型前加一条形状断言,每个 batch 都校验。assert x.shape == (B, T, N, F),不满足就立刻报错。另外,永远用permute而不是reshape来交换维度——reshape 是按内存顺序重新解释数据的,维度交换必须用 permute。
5.2 归一化泄漏:验证集、测试集的数据不能参与均值方差计算
现象:训练时 loss 降到 0.01,验证集 loss 也非常低,但换到另一批新数据上预测,误差突然翻了 3 倍。
原因:数据归一化时把整个数据集的均值、方差一并算出来再切分,验证集和测试集的分布信息已经“泄漏”进训练过程。测试集被压到了模型熟悉的区间,看起来效果很好,换个真实场景就露馅。
解决:必须先划分 train/val/test,再单独在训练集上计算均值和方差,用训练集的均值和方差去转换所有数据。
# 正确做法:在训练集上计算统计量 train_mean = train_data.mean(axis=(0, 1), keepdims=True) train_std = train_data.std(axis=(0, 1), keepdims=True) + 1e-6 train_norm = (train_data - train_mean) / train_std val_norm = (val_data - train_mean) / train_std test_norm = (test_data - train_mean) / train_std反归一化时同样用训练集的统计量,不要用验证集计算出来的 mean/std。
5.3 邻接矩阵的节点索引和数据特征的行号对不上
现象:模型 loss 一直下降,但预测值和真实值有恒定的偏差,或者说模型做空间聚合后某些节点完全不学。
原因:原本数据集里的传感器 ID 是有业务意义的编号(比如检测站的编号 3051),你把它转成 0 到 N-1 的整数索引时,邻接矩阵的行号和数据特征矩阵的行号分别用了两套排序逻辑。比如特征矩阵按传感器 ID 升序排列,而距离矩阵计算时按读取顺序排列,一旦不是同一个顺序,整个图的边就连错了,模型在解释一个“错位”的拓扑关系。
解决:建立一一映射表,数据读进来后用同一个排序规则统一所有维度。我把传感器 ID 排过序后直接作为特征矩阵的行索引,邻接矩阵也按这个顺序构建,然后印输出前 10 行的对应关系做人工抽查,确保 ID 一一对应。
5.4 预测多个时间步时,把真实值当成了输入
现象:多步预测的前 1-2 步很准,第 3 步开始偏差迅速放大,几个时间步后预测值变成一条平线。
原因:做 3 步预测时,如果代码在第二步把上一步的预测结果和真实值拼接后传给模型,这就是标签泄漏。在训练时可以拿到真实值,看起来 loss 很低,部署时根本没有未来数据,第 2 步开始只能喂模型自己的预测结果,误差逐级累计,整个推理链路就崩溃了。
解决:训练时所有时间步的输入都来自同一条时间窗口内的观测数据,预测目标完全独立于输入。如果要处理长期预测,考虑用 seq2seq 结构,在推理阶段把前一步的模型输出作为下一步的输入,但训练阶段要加 schedule sampling(按概率把真实输出替换成模型输出),避免训练和推理不一致。
5.5 图里存在孤立节点或断开的子图:信息根本传不过去
现象:大部分节点预测效果不错,但总有几个节点模型完全学不到任何空间信息,要么一直是平均值要么直接输出常数。
原因:这些节点在邻接矩阵里的度为 0——它们没有邻居或者邻居太少,GCN 的消息传递对它们没有输入,节点无法聚合任何邻居信息,只剩自己的历史信号,甚至在前向传播中直接产生 NaN。
解决:建图之后先检查邻接矩阵的连通分量。把规模过小的连通分量删掉,或者人工补充就近连接(把距离最近的节点设为邻居)来保证图连通。这一步是数据工程的范畴,不是模型的错,但这里不做后面浪费的都是训练时间。
6. 验证模型没白做:早晚高峰、单点曲线和误差指标的核对方法
预测模型的验证不能只看一个宏观平均误差。交通流量预测一天里有几个明显不同的模式:凌晨低流量平稳、早高峰陡增、午间波动、晚高峰缓降。一个模型完全可能白天预测得很好、早高峰全面拉胯,平均误差看起来还不算离谱。
# 按小时统计误差,找出模型最弱的时段 import pandas as pd results = pd.DataFrame({'pred': preds.flatten(), 'true': y_test.flatten(), 'hour': hours.flatten()}) hourly_mae = results.groupby('hour').apply( lambda df: abs(df['pred'] - df['true']).mean(), include_groups=False ) print(hourly_mae.sort_values(ascending=False).head(5))正常情况早高峰(7:00-9:00)的 MAE 会是凌晨的 3-5 倍,因为流量本身大且变化剧烈。如果早高峰的误差异乎寻常地低,反而要怀疑数据泄漏或归一化写错了。如果固定某个小时的误差在结果里异常突刺——比如 8:00 远高于 7:30 和 8:30——大概率那个时段有数据缺失,插补逻辑没处理干净。
画单点曲线是每个做时序预测的人都绕不过去的步骤。随机抽 5 个节点,把测试集的某个连续 3 天画出来,预测曲线和真实曲线叠在一起。最典型的失败模式是预测曲线比真实曲线“延后”一个周期——峰值对不上、拐点明显滞后。
这种现象叫惯性预测:模型学到的规律是“现在什么值,下一时刻大概率还是什么值”,因为在 MAE 损失下这是风险最低的选择。解决不了的时候,可以对损失函数加一阶差分项,让模型对变化方向更敏感:
# 在损失函数里加入预测差分的惩罚,抑制惰性预测 pred_trend = pred[:, 1:] - pred[:, :-1] true_trend = y[:, 1:] - y[:, :-1] trend_loss = torch.abs(pred_trend - true_trend).mean() total_loss = loss_fn(pred, y) + 0.3 * trend_loss模型层面的确认做完后,再看误差指标。MAE 的数值要结合流量量纲理解——如果路网峰值流量 6000 veh/h,MAE 300 就是 5% 的误差,相当不错;如果平均流量本身就低,MAE 300 就是不可用。我习惯同时报 MAE、RMSE 和 MAPE 三个指标,RMSE 放大峰值误差,如果 RMSE 远大于 MAE,说明模型在高峰期或者异常事故事件上错得很离谱,要继续查。MAPE 在流量接近 0 的时段会爆炸,计算时可以把流量低于阈值的样本剔除,否则数值难看还不能说明真实问题。
最后提醒一句:别忘了做 basline 对比。拿历史均值预测、上一时刻值平移(persistence model)和线性回归各跑一遍,如果 GCN 连 persistence 模型都赢不了,问题大概率出在数据本身,而不是 GCN 的实现。我见过不止一个团队在复杂的图模型上调了一个月参,最后发现数据缺失率超过 40%,填充出来的数据本身就没有预测价值。
我现在的习惯是:换模型之前先换数据、换特征、换图结构,这三样改动带来的收益通常远大于模型架构本身的改动。希望帮到你。
本文还有配套的精品资源,点击获取