尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

交通流量时空预测实战:从XGBoost到STGCN的完整竞赛复盘

交通流量时空预测实战:从XGBoost到STGCN的完整竞赛复盘 简介时间序列预测是数据挖掘中的经典问题而交通流量预测在时间维度之外引入了复杂的空间依赖性形成了更具挑战的时空预测任务。不同路口、路段之间的流量会相互影响单点历史数据无法支撑精准预测。此时特征工程成为释放数据价值的关键通过周期性编码、滑动窗口统计和基于路网拓扑的邻接矩阵将原始数据转化为模型可理解的结构化信息。从XGBoost基线到LSTM再到时空图卷积网络STGCN模型演进的过程本质上是将时空关联逐步显式化的过程。本文基于山东省数据应用创新创业大赛的真实赛题完整记录了数据预处理、特征构建、模型训练、损失函数设计和多模型融合的工程实践并复盘了数据泄漏、显存优化等常见坑点为时空序列预测相关项目提供了可复用的技术路线。 开赛前我其实没太把这个题目当回事心想不就是交通流量预测嘛套个LSTM往上一跑分数差不多得了。直到真正开始动手做山东省数据应用创新创业大赛这个“城市交通流量时空预测”赛题时才发现自己天真了——光是理解“时空”这两个字背后到底意味着什么数据结构、什么模型输入、什么特征粒度就够喝一壶的了。这篇文章我打算把从拿到赛题到最终提交的全过程完整复盘一遍包括源代码组织方式、数据处理细节、模型选型与实践踩坑希望能给准备参加类似数据挖掘竞赛、或者正在做交通流量预测相关项目的朋友一些可以直接抄作业的参考。如果你正准备入门时空序列预测或者想在竞赛里快速搭建一套能跑通的baseline再逐步优化拿名次那这篇文章应该对你有帮助。我会尽量把每个环节的“为什么这么做”也讲清楚而不是简单贴一段代码就完事。毕竟竞赛里真正拉开差距的往往不是模型有多花哨而是数据理解和特征工程的细腻程度。1. 赛题理解与整体方案设计1.1 赛题解读预测的本质到底是什么先把这个赛题的核心目标说清楚。交通流量时空预测通俗讲就是给你一堆历史交通数据——比如某条道路、某个路口、某个区域在过去若干个时间片内的车流量、平均车速、拥堵指数——让你预测未来若干个时间片内这些位置的交通状态。但“时空”这两个字是题眼也是难点。时间维度的信息好理解周期性、趋势性、突发性空间维度则是这个任务和普通时间序列预测最大的区别——不同路口之间会互相影响一条路堵了相邻路口的流量在十几分钟后大概率也会变化。这种空间依赖性如果完全忽略只用单点历史值做预测精度一定会碰到天花板。我当时拿到赛题后做的第一件事不是急着写模型而是把评测指标、数据字段、时间跨度、空间范围全部拉通看了一遍。这个赛题的数据主要包括三个部分流量记录每个时间片内通过某监测点的车辆数、平均速度记录、以及路网结构信息。数据里还带了大量的缺失值和明显的异常尖峰后面处理起来相当费工夫。这里要特别强调一点一定要把评测指标看透。这个赛题用的是多个指标加权——MAE、RMSE、MAPE综合计算。这意味着你不能只盯着RMSE去优化那样会过度惩罚大误差导致模型宁愿把预测值全部拉平也不愿意冒险预测峰值。但如果MAPE权重高你又得特别注意小流量时段的预测稳定性因为流量接近0时MAPE会爆炸。指标权重的不同直接决定了你在损失函数设计、数据预处理方式上的选择方向。1.2 技术选型与方案架构从Python生态到模型路线技术栈上我选了纯Python方案这一点没啥悬念。Python在数据竞赛里的统治地位主要靠三个库支撑pandas做数据清洗和特征工程、numpy做矩阵运算、PyTorch做深度学习模型。整个方案架构大致分四层数据层、特征层、模型层、融合层。数据层负责把原始的多张表流量表、速度表、路网表整合成一个统一的、带空间索引和时间索引的数据立方体特征层负责构造周期特征、滑动窗口统计特征、空间邻接特征模型层作为核心我先后尝试了XGBoost、LSTM、STGCN时空图卷积网络三条路线最后提交版本是多种模型的加权融合融合层负责把多个模型的预测结果做最优加权组合在竞赛里这往往是提分最快的一步。方案选型上我想多说几句。很多新手一上来就冲着STGCN这种高大上的图神经网络去结果数据预处理没做干净模型效果反而不如一个调好参的XGBoost。我的建议永远是“从简到繁、基线先行”先用传统机器学习模型把数据流程跑通拿到一个稳健的baseline分数再逐步引入深度学习模型。这样做的好处是当深度学习模型效果不如预期时你能确认是模型结构的问题而不是数据管道的问题。另外在源代码组织上我建议从一开始就按照模块化管理。我的项目结构大概是这样的project/ ├── config/ # 配置文件存放路径、参数 ├── data/ # 原始数据与中间数据 ├── features/ # 特征工程代码 ├── models/ # 模型定义与训练脚本 ├── ensemble/ # 融合与提交代码 ├── utils/ # 公共工具函数 └── main.py # 主流程入口这个结构一开始看着费时间但到了比赛后期模型迭代到五六个版本、特征扩展到上百个的时候这种清晰的模块划分能帮你省下大量查找和debug的时间。2. 数据预处理与时空特征工程2.1 交通数据预处理缺失值、异常值、多源对齐拿到原始数据后我只用了十分钟就发现了两个严重问题缺失率接近15%的字段比比皆是而且部分监测点在凌晨时段会出现流量突变为0但速度字段却显示正常行驶的矛盾记录。这种数据脏度在真实业务场景里太常见了比赛数据其实已经算客气但处理不好会让后面所有的时空特征全部失真。缺失值处理上我没有直接整套填充而是分了三种策略。对短暂缺失连续缺失不超过3个时间片用线性插值这能很好地保持交通流的日内变化趋势对长时间缺失超过3个时间片用历史同期均值加随机扰动填充避免引入太多平滑导致的方差塌缩对完全没有任何历史记录的监测点直接用全局同类型路口的均值兜底。顺序很重要先处理异常值再做缺失值填充否则异常尖峰会被插值算法扩散到邻近时间片。异常值处理我用了两个判据绝对阈值法和差分阈值法。绝对阈值就是设定一个物理上限比如城市道路单车道15分钟通过量不会超过300辆车超过直接视为异常差分阈值则是看相邻时间片的流量变化幅度如果某时间片流量从前一刻的200骤降到20再检查这个时间片的速度字段是否与流量匹配不匹配就判定为异常。这里踩过的一个坑是把节假日数据当异常直接剔除。后来发现虽然节假日流量模式和工作日差异很大但它在测试集中是真实存在的分布不能粗暴剔除最好是单独打一个节假日标签让模型自己学。多源数据对齐也是一道坎。流量表是以15分钟为粒度、按监测点记录速度表是5分钟粒度路网表是空间拓扑结构。要做时空预测必须把多源数据统一到相同的时空粒度。我的做法是把5分钟速度数据聚合到15分钟取平均值路网数据则构建成一个邻接矩阵保存下来作为后续图模型的输入。所有的时间戳统一转成时间索引并做排序校验防止出现时间倒序这种低级错误。2.2 时间特征构建周期、趋势与节假日效应时间特征是整个预测模型里最直观也最有效的特征组。交通流量和时间的对应关系非常强早高峰7点到9点、晚高峰17点到19点的流量会显著高于其他时段工作日和周末的模式完全不同寒暑假期间学校周边路段的流量会明显下降。这些规律如果你能通过特征工程显式地喂给模型就可以帮模型减轻很多学习负担。我做的时间特征分成三类。第一类是基础周期特征小时0-23、星期几0-6、是否周末、是否节假日、一年中的第几天。这类特征直接作为类别特征送入模型在XGBoost里可以用enable_categoricalTrue处理在神经网络里做embedding。第二类是周期性编码特征。直接用小时这个整数特征其实有点浪费信息因为23点和0点之间虽然只差1个小时但流量模式可能相差很大。我用正弦余弦变换把周期特征映射到连续的圆形空间import numpy as np def cyclical_encode(df, col, period): df[col _sin] np.sin(2 * np.pi * df[col] / period) df[col _cos] np.cos(2 * np.pi * df[col] / period) return df小时用period24星期用period7这样模型就能学到“23点和0点是相邻的”这个语义。第三类是滞后特征与滑动窗口统计特征。这是时间序列预测的拿手好戏。对每个监测点取当前时刻前1、2、3、4、6、12个时间片对应15分钟、30分钟、45分钟、1小时、1.5小时、3小时的流量值作为滞后特征再取过去N个时间片内的均值、标准差、最大值、最小值、极差作为窗口统计特征。我用了两组窗口短窗口过去4个时间片即1小时捕捉近期趋势长窗口过去24个时间片即6小时捕捉中期状态。滞后特征有个隐患多步预测时需要递归使用预测值误差会累积。后面我会讲到在深度学习模型里怎么用teacher forcing来缓解这个问题。2.3 空间特征构建邻接矩阵与空间图结构构建空间特征之前我先想清楚了一个问题监测点之间的空间关系到底按照什么规则来定义“相邻”最开始我想直接用地理距离两点之间直线距离小于某个阈值就算邻居。后来发现这不太对劲——两个监测点可能直线距离很近但中间隔着一条河或一堵墙车辆根本无法直接通行它们的流量相关性其实是弱的。更合理的方式是利用路网拓扑结构来构建邻接矩阵。如果路网表里有道路的连接关系我可以构建一个有向图节点是监测点边是道路通行关系。邻接矩阵的定义有两种常见做法一种是二值矩阵直接表示是否相邻另一种是加权矩阵用道路长度或旅行时间的倒数作为权重表示影响强度。我用的是高斯核加权矩阵公式如下import numpy as np def compute_adjacency_matrix(distance_matrix, sigma0.1): adjacency np.exp(-distance_matrix ** 2 / sigma ** 2) np.fill_diagonal(adjacency, 0) # 自身到自身的权重设为0 return adjacency距离矩阵是基于路网的通行距离计算出来的。如果赛题没有直接提供监测点之间的路网距离也可以用经纬度坐标算球面距离近似但效果会差一些。这个邻接矩阵在后续STGCN模型里就是图卷积操作的“图”本身它的质量直接决定了空间依赖关系能不能被模型有效捕捉。我当时在调试时发现邻接矩阵的sigma参数对结果非常敏感sigma太小会导致只有极近的节点有联系图消息传递不畅sigma太大会导致所有节点都互相影响空间信息被稀释。后来我通过网格搜索确定了一个相对合理的值并且对邻接矩阵做了归一化处理让每行的和为1避免量纲不一致。3. 从基线到进阶预测模型构建与实现3.1 基线模型用XGBoost把数据流程跑通在这个赛题中我第一个真正训练并提交的模型是XGBoost而不是什么深度学习模型。原因很简单XGBoost对特征工程友好、训练速度快、调参相对容易是验证数据流程和特征有效性的最佳工具。我把每个时间片、每个监测点的数据展开成一行特征列包含上面提到的周期特征、滞后特征、滑动窗口特征、以及目标值对应的历史同期值比如要预测今天9点的流量就把昨天9点、上周同星期9点的流量作为特征。这是一个标准的监督学习构造方式目标变量是未来1个时间片的流量值。要预测多个时间片就为每个预测步长单独训练一个模型或者把步长也作为特征喂进去用单模型多输出。XGBoost的代码比较简洁核心超参我列出了当时调参后的结果参数取值说明n_estimators1000树的数量配合早停learning_rate0.05学习率太小训练慢太大会过拟合max_depth6树深度防止过拟合subsample0.8行采样比例colsample_bytree0.8列采样比例reg_lambda1.0L2正则化系数用XGBoost拿到一个初始分数后我把特征重要性打印出来看了一眼发现排名靠前的全是滞后特征和周期特征空间特征几乎没有进入前30。这其实说明了一个问题对单点预测来说自己的历史数据和周期模式已经包含了大部分信息空间信息的增益在树模型里比较难体现出来。这也是我后来决定引入图神经网络的原因——空间依赖可能需要通过深度模型的层级抽象才能被更高效地利用。3.2 LSTM模型时间依赖建模的进阶方案XGBoost验证了数据流程没问题之后我开始搭建LSTM模型。LSTM相比于树模型最大的优势是能显式地对序列依赖建模不需要手动构造大量滞后特征——模型自己会学习到“过去几个时间片的状态如何影响未来”。数据处理上LSTM的输入格式和XGBoost完全不同。我把数据组织成三维张量[样本数, 时间步长, 特征维度]。时间步长我设为24也就是用过去6小时15分钟粒度的观测来预测未来。每个时间步的特征包含该时刻的流量值、速度值、以及周期编码特征。模型结构上我用了双层LSTM加全连接输出层import torch import torch.nn as nn class TrafficLSTM(nn.Module): def __init__(self, input_size, hidden_size64, num_layers2, output_size1): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropout0.2 ) self.fc nn.Sequential( nn.Linear(hidden_size, 32), nn.ReLU(), nn.Linear(32, output_size) ) def forward(self, x): out, _ self.lstm(x) return self.fc(out[:, -1, :])hidden_size设64num_layers设2dropout设0.2。训练时我用了Adam优化器初始学习率0.001配合ReduceLROnPlateau学习率调度器在验证集loss连续3个epoch不下降时把学习率乘以0.5。batch_size设128训练了50个epoch用了早停策略防止过拟合。LSTM相比XGBoost的提升大约在3%-5%不算特别大但已经验证了深度模型的潜力。更重要的是LSTM为后面引入空间结构打好了基础——纯LSTM只建模了时间维空间维是被忽略的。下一步我打算把空间结构也纳入模型。3.3 时空图卷积网络STGCN把空间依赖建模做到位STGCN是个很经典的时空预测模型结构。它的核心思路是在时间维用卷积或LSTM捕捉时序依赖在空间维用图卷积捕捉路网结构上的空间依赖。我第一次看到STGCN的示意图时觉得挺复杂但真正理解了图卷积的操作之后发现它本质上就是一个“在图上做信息聚合”的操作——每个节点的更新等于把自身特征和邻居节点的特征按权重加权求和后过一层非线性变换。我用的STGCN包含两个时空卷积模块和一个输出层每个时空卷积模块由两个时间卷积单元夹着一个空间图卷积单元组成。时间卷积用一维卷积实现卷积核大小为3padding为1保持序列长度不变空间卷积用上面算好的邻接矩阵做图卷积公式如下import torch import torch.nn as nn class GraphConv(nn.Module): def __init__(self, in_features, out_features, adjacency): super().__init__() self.adjacency adjacency # 归一化后的邻接矩阵 self.weight nn.Parameter(torch.randn(in_features, out_features)) self.bias nn.Parameter(torch.zeros(out_features)) def forward(self, x): # x: [batch, nodes, features] support torch.matmul(self.adjacency, x) out torch.matmul(support, self.weight) self.bias return out实际训练STGCN时我对数据做了标准化处理把每个监测点的流量值做z-score标准化这是因为图卷积对输入的尺度比较敏感。训练配置和LSTM类似但STGCN的收敛速度更快——大概20个epoch就能达到LSTM 50个epoch的效果。在验证集上的表现STGCN比LSTM又提升了2%-3%。这说明在包含明显空间依赖的交通流量任务中显式建模空间结构确实有价值不是因为模型更复杂所以更好而是因为模型结构和数据的底层规律更匹配。STGCN训练中我还要注意一个细节训练集和验证集不能随机切分。时间序列数据一旦随机打乱就会出现数据泄漏——模型在训练时已经看到了未来信息验证分数虚高但测试集一跑就打回原形。我采用的是按时间顺序切分前80%的时间段做训练后20%做验证。这也符合比赛的评估设定评委拿到的测试数据一定在未来。4. 训练策略、损失函数设计与模型融合4.1 损失函数设计从单一指标到组合优化损失函数的选择直接决定了模型优化的方向。这个赛题的官方评测是MAE、RMSE、MAPE三种指标的加权组合如果只在训练时用单一的MSE或MAE损失模型优化的方向和评测指标就会存在偏差。我试过三种损失函数。纯MSE损失收敛快但会过度惩罚大误差导致模型变得保守在流量激增时段预测值明显偏小纯MAE损失对异常值更鲁棒但训练时梯度幅度稳定收敛速度稍慢而且MAE对“大部分时间点都预测得很准、偶尔一个点大偏”这种情况不够敏感最终我用了MSE和MAE的加权组合import torch.nn.functional as F def combined_loss(pred, target, alpha0.7): mse_loss F.mse_loss(pred, target) mae_loss F.l1_loss(pred, target) return alpha * mse_loss (1 - alpha) * mae_lossalpha设0.7让MSE主导优化方向同时加入MAE的梯度来平衡异常值的影响。这个调整虽然朴素但让验证集上的加权评测分数提升了约1.5%。竞赛期间有人常问代码里只改一行loss就能提分是不是说明之前选的模型不行我觉得不是模型不行而是loss和评测指标本来就该对齐很多人一开始就没做这个对齐而已。4.2 超参数调优贝叶斯搜索与人工经验的结合深度学习模型涉及的超参数不少学习率、batch_size、hidden_size、层数、dropout、时间步长等。一个一个手动试会非常耗时我用了Optuna这个贝叶斯优化框架来做超参数搜索目标函数定义为验证集上的评测指标分数。import optuna def objective(trial): hidden_size trial.suggest_categorical(hidden_size, [32, 64, 128]) num_layers trial.suggest_int(num_layers, 1, 3) dropout trial.suggest_float(dropout, 0.1, 0.5) learning_rate trial.suggest_loguniform(learning_rate, 1e-4, 1e-2) # ... 构建模型、训练、返回验证集分数每次搜索会跑30组超参每组训练20个epoch做初步筛选最后挑Top3配置再训练到完整epoch数。这个方法帮我找到了一套比手动调优高3%的超参组合。不过我也要提醒一下超参搜索不能盲目扩大范围。比如时间步长这个参数虽然Optuna也能搜但我更倾向于根据业务知识直接定。交通流量预测中过去6小时到12小时的信息价值最高超过24小时就基本被周期特征覆盖了没必要把时间步长搜到96去增加训练成本。这种“数据知识限定搜索空间、搜索算法在空间内找最优”的思路才是最高效的。4.3 模型融合多个模型的误差互补效应模型融合是我在这个赛题里提分最猛的一步在单模型最优的基础上又提升了近4%。原理很简单如果两个模型的误差模式不完全相同它们的加权平均往往比任何一个单独模型都准。这就像你同时问两个擅长不同科目的朋友一道综合题两个人的加权意见通常比一个人靠谱。我融合了三个模型XGBoost、LSTM、STGCN。用简单的线性加权平均final_pred 0.25 * xgb_pred 0.35 * lstm_pred 0.40 * stgcn_pred权重不是拍脑袋定的而是用遗传算法做了一次小规模寻优在验证集上让评测指标最小化。搜索出来的结果恰好是STGCN权重最高这符合我对模型能力的判断——STGCN同时捕捉了时空信息本身精度最好融合时自然应该占大头。融合还能更进一步。如果不用线性加权而是再训练一个第二层模型stacking比如用XGBoost作为第二层分类器/回归器把三个模型的预测值当作特征输进去理论上能捕捉到更复杂的模型关系。我尝试过stacking但在这个赛题上并没有比线性加权好多少反而多了一重过拟合风险。如果你的时间充裕、训练数据量大stacking值得一试如果时间紧张加权平均已经能拿到大部分收益。5. 常见问题与排查技巧实录5.1 数据泄漏时间序列预测最容易犯的隐形错误这个赛题我犯过的最严重的错误是在第一版特征工程时直接把未来时间片的统计量当作特征加进去了。具体来说我在构造滑动窗口特征是窗口边界写错导致当前时间片的特征包含了未来若干个时间片的信息。模型在验证集上分数异常高当时还挺高兴结果提交后测试集分数暴跌一查才发现是数据泄漏。排查数据泄漏的标志性信号是训练和验证分数远高于测试分数而且差距异常大。如果你发现模型验证集上RMSE是20测试集上却飙到50先检查特征里有没有包含未来的信息。在时间序列预测中一个保险的做法是构造特征时只用严格早于当前时间片的数据并且单独写一个单元测试来断言这一点。5.2 显存不足与训练速度优化STGCN的图卷积操作涉及邻接矩阵和节点特征的矩阵乘法当监测点数量达到数百个、batch_size稍大就会爆显存。我的解决方案有三个降低batch_size到32用混合精度训练PyTorch里一句torch.cuda.amp.autocast()就能实现大概能省40%显存把邻接矩阵转成稀疏张量存储省掉大量零元素占用的空间。这条路走下来原有的模型配置在同一块显卡上从OOM变成了最高占用70%。如果你连显卡都没有也可以用Google Colab免费版跑但要注意训练时间限制和数据集的上传下载问题。数据量不大时在Colab上完成整个实验周期是可行的。5.3 预测结果全是均值模型为什么学不到突发流量我在第一版LSTM的预测结果里发现一个现象模型预测的流量曲线几乎就是历史平均值的平滑版本早高峰的峰值比实际低了将近30%。复盘下来原因有两个。训练数据不均衡绝大多数时间片的流量处于中等偏低水平高峰时段的样本量天生就少模型为了降低整体loss倾向于把预测偏向均值附近。时间步长太短我只用了过去6小时的数据但交通流量的日周期性意味着昨天同一时段的数据对今天预测很重要这个信息被截断了。解决办法是把长周期的历史同期值作为额外特征拼到每个时间步的输入里相当于给了模型一个“今天是上周同期水平附近”的先验在loss里对高峰时段样本稍微加大权重让模型更重视这部分少见但关键的样本。5.4 代码工程化的几条心得竞赛到了后期模型版本多了之后管理源代码和实验结果变得特别重要。我后来养成了几个习惯虽然麻烦但确实救命每个实验记录一份config文件里面保存所有超参和数据路径确保实验可复现所有模型权重按“日期_模型名_验证分数.pth”的格式命名方便快速找回最优权重每一版特征工程代码都加上注释说明特征含义和构造时间防止过几天自己都看不懂。这些做法在赛后复现和写总结文档时省了太多时间。个人经验收尾这个比赛做下来比拿名次更有价值的其实是完整走通了“数据→特征→模型→融合→复盘”的全流程而且把时空预测这个方向的常见坑基本踩了个遍。如果让我重新做一次我会在基线模型上花更少时间把精力提前放到空间特征挖掘和模型融合上。另外刚开始做的时候总觉得模型越复杂越厉害折腾了好几天STGCN跑出来的效果反而被一个调好参的XGBoost碾压后来才想明白模型是放大器输入数据的信息量和质量才是被放大的那个信号本身。最后再分享一个小技巧每次跑完实验把这个版本的评测指标和预测可视化结果存个档后面做模型对比或者写文档总结时翻这些记录比翻聊天记录高效太多。本文还有配套的精品资源点击获取
返回列表