ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

深度学习交通流量预测实战:LSTM数据预处理与避坑指南

深度学习交通流量预测实战:LSTM数据预处理与避坑指南 简介面向深度学习入门者的交通流量预测实战源码包覆盖LSTM、GRU及CNN三种主流模型的完整建模流程。代码围绕纽约出租车流量数据NYC-stdn展开将数据预处理、模型训练与测评、性能可视化整合为清晰工程结构适合作为学术实验、课程设计或毕业设计的参考基线。资源共16个文件以8个Python源码为核心包含主程序、配置文件、数据加载器及四类模型实现另附训练/测试npz数据、运行日志txt、三张训练指标曲线图及数据说明docx压缩包约1.18MB结构紧凑便于按需查阅。目前已有91人学习使用。读者可获得一套开箱即用的端到端预测框架在统一代码结构下横向对比三种模型效果快速理解交通流预测的数据处理思路与调参细节降低入门门槛。1. 先看懂深度学习交通流量预测不是刷榜是把一段路的历史折算成未来假设你负责某条城市快速路的拥堵预警手里只有上游线圈检测器回传的流量、车速和占有率老板要的是“未来 15 分钟会不会堵”。这就是深度学习交通流量预测的典型场景用过去一段时间的检测器时序数据预测未来若干时间步的交通状态。这类项目数据源公开、评价指标明确、模型从 LSTM 到时空图网络都有成熟套路尤其适合深度学习新手拿来做第一个实战项目源码练手——它不像图像分类那样吃 GPU也不像 NLP 那样动辄预训练模型一份 CSV、一台 CPU 机器就能跑通全流程。这篇就按我实际做这类项目的顺序把数据、模型、参数和那些查不到文档的坑一次讲完。2. 数据集与预处理把原始检测器数据变成能喂给网络的样本2.1 选数据集PeMS、广州公开数据与自采数据的取舍做交通流量预测第一步不是选模型而是选数据。国内外的公开数据集中最常用的是美国加州 PeMSCaltrans Performance Measurement System它按 5 分钟粒度提供每个检测器断面的流量、平均车速和占有率按天生成 CSV 或文本文件。另一个常见选择是广州的 Highway-Toll-Gantry 公开数据集10 分钟粒度字段类似。两个我都用过给新手的建议是优先选 PeMS 或同样按固定时间间隔输出的数据因为它们已经是准确定时的时序数据少掉很多对齐的麻烦。原始 PeMS 数据的组织方式是“一天一个文件、一个文件包含多个检测器”你要自己按站点 ID 过滤。更麻烦的是里面可能有车道级记录需要先决定你是预测整个断面流量还是单车道。常见做法是聚合到断面级把同一条路的多个车道求和或求平均得到一个检测器的时间序列。这个决策会影响后续所有处理我一般写个脚本先把数据规整成标准的三列表格——时间戳、检测器 ID、流量/速度/占有率。选数据有个容易忽略的原则公开数据集的周期性和你实际业务是否匹配。PeMS 是典型的工作日早高峰、晚高峰双峰形态周末基本平缓。如果你的业务场景是高速公路收费站之间那是自由流为主峰值形态完全不同。所以拿到数据先画几天曲线确认它的时序形态再动手别直接套预处理流程。2.2 清洗与聚合读 CSV、重采样到固定粒度PeMS 的原始记录有些是 30 秒粒度上传有些已经聚合到 5 分钟。如果是 30 秒数据直接拿来训练会有两个问题一是序列太长导致 LSTM 训练慢二是 30 秒粒度里单车道流量噪声极大。常见做法是统一重采样到 5 分钟。import pandas as pd # 读取 PeMS 风格的逐日 CSV时间戳解析成 datetime df pd.read_csv(d03_text_station_5min.csv, parse_dates[timestamp]) # 画出单个检测器的原始流量先看序列形态再做后续 df df[df[station] 400123].sort_values(timestamp) # 按 5 分钟聚合流量求和、车速和占有率取平均 flow df.set_index(timestamp)[flow].resample(5min).sum() speed df.set_index(timestamp)[speed].resample(5min).mean() occ df.set_index(timestamp)[occupancy].resample(5min).mean()这段代码的要点在resample的用法上。sum用于流量因为“15 分钟内通过的车数”是可加的车速、占有率如果用求和就会得到毫无意义的数值必须取平均。另一个关键点是resample默认按左闭右开的时间窗口对齐如果你的数据本身还存在跨天缺口重采样会把缺失时段填成 NaN需要在后面统一做缺失处理。清洗阶段还有个常见操作剔除异常值。车速为 0 但流量很大的记录通常是检测器故障或施工挡道流量超过车道理论通行能力的记录也直接删。我见过有人拿这种脏数据训练出了看起来不错的 loss理由是模型学会了把异常值当作特征去拟合但换一段新数据立刻失效。这类清洗没有固定阈值而是看数据分布画出流量的直方图找到明显离群的那个尾巴砍掉。2.3 构造监督学习样本滑窗与预测步长时序预测的本质是把历史序列转成监督学习的 (X, y) 样本。交通流量预测里X 是过去若干个时间步的多维特征y 是未来若干个时间步的流量。这个转换看似简单却是最容易出错导致“数据泄漏”的一步。import numpy as np def make_samples(series, seq_len12, pred_len3, step1): 滑窗构造样本。 series: 一维流量序列长度 N seq_len: 用过去多少个时间步做输入12 步 1 小时5min 粒度 pred_len: 预测未来多少个时间步3 步 15 分钟 step: 滑动步长常取 1 或 2影响样本总量和时间相关性 X, y [], [] for i in range(0, len(series) - seq_len - pred_len 1, step): X.append(series[i : i seq_len]) y.append(series[i seq_len : i seq_len pred_len]) X np.array(X, dtypenp.float32) y np.array(y, dtypenp.float32) # 形状统一为 (样本数, seq_len) 和 (样本数, pred_len) return X, yseq_len和pred_len是交通流量预测里最值得花时间调的两个参数。5 分钟粒度下seq_len12表示用过去 1 小时seq_len24表示过去 2 小时。短时预测15 分钟内通常 12 步就够因为近因主导拉长到 24 步不一定更好反而把早高峰的尾部噪声带进来。pred_len3是单模型预测 15 分钟如果你要预测 30 分钟可以直接把pred_len设成 6但后面会讲多步长时间预测的误差会明显变大。step参数经常被忽略。它决定相邻两个样本在时间轴上的重合度。step1会让样本高度重叠训练集很大但信息冗余有把验证集“间接学走”的风险step2或step3能显著降低样本量、加快训练且对最终精度影响很小。我的习惯是先用step1跑通确认模型能收敛再调大 step 压缩训练时间。2.4 归一化与切分顺序错一步验证集就是废的数据预处理的最后两件事是归一化和训练/验证/测试集切分这两步的顺序不能乱。业内几乎每天都在发生一个错误先对整个序列做 MinMaxScaler再切分训练集和测试集。这样测试集的信息通过 scale 参数泄漏到了训练过程线上预测时你无法用“未来数据”算最大值必然出现验证集指标好、上线翻车的情况。from sklearn.preprocessing import MinMaxScaler # 按时间顺序切分前 70% 训练中间 15% 验证最后 15% 测试 train_end int(len(flow_norm) * 0.70) val_end int(len(flow_norm) * 0.85) scaler MinMaxScaler() # 先 fit 训练集再 transform 全序列 train_flat flow_norm[:train_end].reshape(-1, 1) scaler.fit(train_flat) train_scaled scaler.transform(train_flat).reshape(-1) val_scaled scaler.transform(flow_norm[train_end:val_end].reshape(-1, 1)).reshape(-1) test_scaled scaler.transform(flow_norm[val_end:].reshape(-1, 1)).reshape(-1)注意fit只发生在训练集切片上验证集和测试集只用transform。这保证模型在训练阶段完全看不到未来时间段的统计信息。切分比例 70/15/15 在时序任务里是一个经验值样本量上万时可以把训练提到 80%。还有一个容易踩的细节两条切分边界处要留若干步的间隔或者干脆在切分后从头构造样本否则验证集的前几个样本可能包含训练集末段的上下文造成轻微泄漏。预处理做完下一步才是模型选型。很多教程一上来就摆模型结构但数据没处理好时模型越复杂越容易让问题隐藏得更深等你发现验证集 MAPE 异常时已经浪费了好几天。3. 模型选型与最小复现先用 LSTM 把基线跑通3.1 为什么基线选 LSTM而不是直接上 Transformer流量预测是典型的多元时序预测输入是连续的、有强周期性的序列。新手最容易犯的选择错误是看到 SOTA 榜单上有 Transformer 就去复现结果训练时间长、数据量不够、调参半年最终精度还不如一个两层 LSTM。我说说实际理由Transformer 的自注意力机制需要大量数据才能学到可靠的时序偏移模式公开流量数据集里一个检测器通常只有几万到几十万条记录远达不到它在大型语料上的数据规模。LSTM 的循环结构天然对“近因主导”的短时预测有效参数量小CPU 上几分钟就能看到收敛趋势。当然LSTM 也有它的短板训练时序较长时梯度消失、无法并行。但作为基线它的定位是给你一个可信的数值参照——如果后来换 TCN 或 Transformer 的模型连 LSTM 都打不过那就不是模型结构的问题而是数据或训练流程的问题。这也是我建议所有做这个方向的人先跑 LSTM 基线的原因。3.2 用 PyTorch 定义 LSTM 流量预测网络主线代码用 PyTorch因为它调试直观、生态成熟。网络结构很简单LSTM 层提取时序特征取最后一个时间步的隐状态接一个全连接层输出pred_len个值。import torch.nn as nn class TrafficLSTM(nn.Module): def __init__(self, input_size3, hidden_size64, num_layers2, pred_len3, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, # 输入形状 (batch, seq_len, input_size) dropoutdropout, ) self.fc nn.Linear(hidden_size, pred_len) def forward(self, x): # x: (batch, seq_len, input_size) out, _ self.lstm(x) # 取最后一个时间步的隐状态 (batch, hidden_size) last_hidden out[:, -1, :] return self.fc(last_hidden)几个参数要先解释。input_size对应每个时间步的特征数这里写了 3表示流量、车速、占有率三通道如果你只预测流量且只用流量序列就改成 1。hidden_size表示 LSTM 内部隐状态的宽度64 适合几千到几万样本的规模数据量大时可以加到 128。num_layers2是层数两层通常足够第三层起训练难度明显增加但精度提升有限。dropout只在多层 LSTM 的层间生效单层时加了也没作用新手容易误以为它和全连接层的 dropout 一样。这里有个实际工程里的经验虽然模型定义只输出未来 3 步的流量但输入特征可以融合速度、占有率甚至天气、节假日。交通流量预测的核心不是模型多复杂而是特征能不能描述“上游拥堵会传导到下游”这类因果关系。所以我在拼接张量时通常把input_size从 1 扩展到 35代价是训练时间增加不多收益往往比换模型明显。3.3 训练循环数据加载、损失函数与 shuffle 的陷阱训练代码里最需要警惕的是DataLoader的shuffle参数。大多数深度学习任务会默认shuffleTrue打乱数据但时序预测打乱后模型会失去对时间连续的感知验证集指标会变得虚高。正确做法是shuffleFalse按时间顺序迭代。import torch from torch.utils.data import TensorDataset, DataLoader X_tensor torch.tensor(X_train, dtypetorch.float32) y_tensor torch.tensor(y_train, dtypetorch.float32) dataset TensorDataset(X_tensor, y_tensor) loader DataLoader(dataset, batch_size64, shuffleFalse) model TrafficLSTM(input_size3, hidden_size64, pred_len3) opt torch.optim.Adam(model.parameters(), lr1e-3) loss_fn nn.MSELoss() for epoch in range(30): model.train() epoch_loss 0.0 for xb, yb in loader: opt.zero_grad() pred model(xb) # (batch, pred_len) loss loss_fn(pred, yb) # yb: (batch, pred_len) loss.backward() opt.step() epoch_loss loss.item() * xb.size(0) print(fepoch {epoch:02d}, loss {epoch_loss / len(X_train):.6f})训练循环有两点值得展开。第一损失函数用 MSE 会让模型把注意力集中在流量峰值时段——峰值时段误差大、梯度贡献大如果你关心的是全天整体表现Huber Lossnn.SmoothL1Loss对离群点更鲁棒。我实际项目中大多用 Huber因为流量数据经常有检测器抖动产生的尖峰这些尖峰不该主导训练。第二batch_size64是经过取舍的设太大256 以上会让模型偏向学习样本平均趋势忽略小尺度波动设太小8 以下则梯度噪声过大、收敛慢。64 对我见过的几个公开数据集都是一个不会出错的值。这段训练代码还有一个隐蔽问题每个 epoch 内数据顺序完全不变模型会“背”下训练集顺序导致最后几个 epoch 在验证集上漂移。常见做法是每个 epoch 后手动把训练序列平移若干个时间步再生成样本或者在加载时用一个随机偏移量切出子段。新手场景下不必做到这一步但要意识到验证集波动可能来自这个原因。3.4 多步预测的三种方式直接式、递归式与 seq2seq当pred_len大于 1 时预测方式直接决定误差形态。递归式预测是把模型上一次的输出当作下一次的输入循环预测多个时间步直接式是训练一个输出多值的模型一步到位seq2seq 是用编码器-解码器结构逐时间步生成。流量预测里三种方式我都试过结论是短时间步3 步以内直接用上面那个多输出的 LSTM 就很好超过 5 步时递归式误差会累积直接式每个时间步单独训练多个模型开销大seq2seq 效果最好但训练复杂度高。我建议新手阶段只做直接式也就是本节模型的多输出方式把未来 15 分钟一次性预测出来。等到你把单步预测的流程完全跑通再考虑用递归式做对比实验看看误差累积到底有多大。那时候你会发现流量预测里误差最明显的形态不是平均偏高而是“预测曲线比真实曲线晚了一个时间步”这个问题我放在第 5 章单独讲因为它坑过几乎所有人。4. 训练、评估与参数调节用 MAE 说话别被 MSE 骗了4.1 流量场景下评估指标怎么选MAE、RMSE、MAPE训练完成后评估指标的选择直接决定你对模型好坏的判断。很多教材默认用 MSE 或 RMSE但在流量预测场景RMSE 对峰值的惩罚过重——一个早高峰尖峰预测偏了 200 辆RMSE 会掩盖你日常时段 50 辆的误差改善。我在实际项目里主要看 MAE平均绝对误差它表示“平均每个 5 分钟时段预测差多少辆车”业务人员也能听懂。RMSE 只作为辅助用来观察模型是不是在峰值时段产生极端误差。MAPE 在流量预测里要特别小心。当真实流量接近 0 的深夜时段误差百分比趋近无穷MAPE 会被这几个点拉爆统计上完全失真。这也是为什么很多论文里 MAPE 数值好看复现时却对不上——他们可能悄悄排除了低流量时段。我的习惯是看 MAE 的同时单独统计早高峰 7:00-9:00 和晚高峰 17:00-19:00 的 MAE因为那才是业务真正关心的拥堵时段。4.2 三个必调参数seq_len、hidden_size、batch_size这三个参数用网格搜索调能获得最优但新手先用经验值跑通更重要。seq_len我在第 2 章说过默认 12现在补充它的调试信号如果验证集 MAE 从第 4 步开始持续恶化很可能是输入窗口太短、模型没有足够上下文反过来如果训练集 MAE 很低但验证集上升说明窗口太长、模型记住了过多历史噪声。hidden_size从 32 试到 128每翻一倍训练时间大约增加 50%但精度提升会快速饱和。我常用的判断标准是在训练集上算一次 loss如果 64 和 128 几乎没有区别就用 64因为小模型泛化更好尤其数据量不大时。batch_size的影响相对稳定64 是流量数据的甜点位如果训练集样本超过 5 万可以试 128 加速。这三个参数之间的交互才是调参真正的难点。seq_len和hidden_size是强相关的窗口越长需要的隐状态容量越大。batch_size调整后学习率也要跟着改批量翻倍时学习率通常也要适度上调否则收敛步数会变多。4.3 早停与学习率的粗调经验新手训练模型最常见的画面是 loss 一直降但验证 MAE 从某个 epoch 开始反弹。这就是过拟合信号最直接的处理是早停记录验证 MAE连续 10 个 epoch 没有下降就停。代码实现不难我在训练循环里用一个全局变量跟踪最优模型参数验证 loss 改善时保存 checkpoint这是给新手的最好“后悔药”。best_mae float(inf) patience 10 bad_epochs 0 for epoch in range(max_epochs): # ... 训练步骤 ... val_mae evaluate(model, val_loader) if val_mae best_mae: best_mae val_mae torch.save(model.state_dict(), best_lstm.pt) bad_epochs 0 else: bad_epochs 1 if bad_epochs patience: print(fearly stop at epoch {epoch}) break学习率的调节比 epoch 数更重要。Adam 默认 1e-3 对 LSTM 起步一般没问题但如果 loss 一开始就振荡不降先降低到 3e-4 再观察如果收敛太慢再试 3e-3。我还习惯配合一个简单的学习率衰减每 10 个 epoch 乘以 0.5让训练后期稳定下来。这里没有玄学就是在验证集上盯住 MAE 的变化确定一个“快到拐点”的位置再把步子放小。4.4 结果可视化只看 loss 曲线无法发现预测滞后训练结束后第一件事不是看指标而是画预测曲线对比。我每次都会把测试集的前 200 个时间步的预测值和真实值画在同一张图上。这个操作能帮你发现一个 loss 完全暴露不了的问题预测是否滞后于真实序列一个时间步。import matplotlib.pyplot as plt model.eval() with torch.no_grad(): pred model(X_test_tensor).numpy() true y_test # 取测试集前 200 个预测步画第二个未来时间步的预测 plt.figure(figsize(12, 4)) plt.plot(true[:, 1], labeltrue, linewidth2) plt.plot(pred[:, 1], labelpred, linewidth1.5) plt.legend() plt.title(Test set: future t2 step prediction) plt.tight_layout() plt.savefig(lstm_pred_vs_true.png, dpi150)画完之后重点看两条曲线在峰谷处的相对位置。如果预测曲线的波峰出现在真实波峰后面一格说明模型学到了“把上一时刻的值搬过来”而不是真正预测未来。这个现象在交通流量里尤其常见因为流量序列有很强的自相关性上一时刻的流量本身就是下一时刻最有效的特征模型会偷懒复制。下一章我会讲怎么系统排查和解决这个问题。5. 避坑流量预测项目里我踩过的四个真实坑5.1 训练 loss 很低预测曲线却整体滞后一步这个坑我几乎在每次教学里都会碰到。现象是训练集 MSE 漂亮地降到零点几验证集 MAE 也在合理范围但把预测曲线画出来发现它比真实曲线晚了一个时间步。原因是流量序列相邻时间步高度相关模型发现“复制最近值”比“真正推测趋势”更容易降低 loss于是退化成跟随器。这本质上也是滑窗构造样本时seq_len末端时刻与预测起点太近给模型提供了偷懒路径。解决方式是三管齐下。第一把输入特征从单一流量扩展到包含速度和占有率模型至少能从特征交叉中学到一点变化的先验。第二对序列做一阶差分后再训练差分序列的平稳性更强模型必须学会预测“变化量”而不是复制水平值预测完再把差分结果累加还原。第三用更长的seq_len让模型看到完整的流量上涨过程减少“抄近道”的可能。我在实际项目中靠差分这一招就解决了 80% 的滞后问题。5.2 缺失值用全列均值填充验证集 MAE 突然崩掉PeMS 这类真实数据一定有缺失时段常见原因包括检测器故障、断点传输、维护停用。新手最先想到的办法是df[flow].fillna(df[flow].mean())这会让白天时段被填成全天均值夜间时段也被填成白天均值直接把流量的昼夜节律抹平。训练时模型学到的是一个被压低峰值的曲线一旦测试集包含真实的夜间接近零值误差立刻爆发。正确的填充要保留时间局部性。我一般用前后时间插值df[flow].interpolate(methodlinear)如果缺失跨度超过 2 个小时干脆把这段数据标记为无效并从训练集中剔除而不是硬填。还有一种进阶做法在输入 x 中额外拼一个是否缺失的 mask 通道模型能主动学习缺失时刻的置信度这在同时处理多个检测器时尤其有效因为一个检测器坏了可以借用相邻检测器的时间特征来补。5.3 随机切分训练测试集模型“偷看”到了未来时序预测最忌讳随机切分。现象是训练集 loss 降低到不可思议的低验证集误差也很低你觉得模型完美了但换一段新数据立刻打回原形。原因是随机切分让训练集和验证集的时间段互相穿插验证集里某天的早高峰数据它的前一个序列窗口其实落在了训练集里模型等于提前知道了答案。这个坑的排查方式是用时间顺序切分重新跑一遍如果 MAE 显著变差说明之前的结果是假的。前面第 2 章我已经给出了按 70/15/15 切分的代码这里再补充一个细节两个集合之间最好留下至少seq_len pred_len步的间隔避免验证集最前面几个样本用到训练集末尾的数据作为上下文。实际操作中我会在切分边界多丢弃 20 步数据成本极小但能彻底杜绝边界泄漏。5.4 验证阶段 loss 变成 NaN训练阶段却正常训练时 loss 正常验证时突然变成 NaN这个情况容易让人误以为是模型问题其实多半出在数据上。常见原因是验证集里存在inf或未归一化的离群值比如某时段检测器误报了 99999 的流量训练集里恰好没有验证集计算 loss 时触发了数值爆炸。另一个原因是对数或除法操作在归一化时遇到 0 值。排查方法是先检查验证集原始数据np.isinf(arr).sum()和np.isnan(arr).sum()发现异常值就删掉或裁剪到合理范围。还有一个坑藏在 MinMaxScaler 里如果训练集的某个特征恰好是常数它的 scale 会变成 0transform时产生除零错误。检查方法是打印scaler.scale_看到 0 就删掉那个特征或改用 StandardScaler。5.5 排查问题的顺序先画图再查数据最后怀疑模型踩完上面四个坑之后我总结出一条排查顺序可以帮你省掉大量无效调参。当模型效果不符预期时第一件事是画预测曲线和真实曲线的对比图判断有没有滞后或相位偏移第二件事是检查训练/验证/测试切分的时间顺序和缺失值处理第三件事才是去调模型结构或学习率。我见过太多人在确认数据没问题之前就换网络结构结果换了三个模型最后发现是切分泄漏。数据的问题会传导给模型而模型不会自己修正坏数据。6. 进阶下探多步预测与时空图模型值得投入吗把单检测器的 LSTM 跑通之后自然会想两个方向预测更长的未来或者利用整个路网的空间关系。先说结论这两个方向都值得投入但投入顺序要注意。多步预测是把pred_len从 3 拉长到 6 或 12误差会随预测步长线性增长通常 30 分钟以后误差大到没有业务价值。在这种情况下递归式预测误差累积太快我一般用“分桶模型”0-15 分钟一个模型15-30 分钟另一个模型各自训练反而比一个模型硬预测 6 步更好。时空图模型是另一个进阶方向它把每个检测器看作图节点、路网连接看作边用图卷积层捕捉相邻路段的拥堵传播。这个方向的效果上限更高一个设计良好的 GCNLSTM 模型能把多步预测 MAE 再压 10%-20%。但代价也明显需要准备路网邻接矩阵确定“相邻”的定义是车流可达还是物理距离还要处理检测器之间的数据同步问题。我的建议是先把单点 LSTM 的基线和特征工程做到位再引入空间信息。我现在做这类项目有个固定习惯无论模型多复杂第一版一定只用单检测器 LSTM 跑通并画出预测曲线确认滞后问题解决后才谈升级。这个习惯帮我在很多“看起来是模型不行、实际是数据没对齐”的项目里省下过几周时间。交通流量预测的入门门槛不高但真正的分水岭往往不在模型而在数据和评估的严谨度上。希望这篇能帮你少踩几个我当年踩过的坑把第一个流量预测模型稳稳跑起来。本文还有配套的精品资源点击获取
返回列表