ARTICLE DETAIL

资讯详情

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

交通流预测实战:SAE特征提取+LSTM/GRU建模与避坑指南

交通流预测实战:SAE特征提取+LSTM/GRU建模与避坑指南 简介这是一份基于深度学习的交通流预测实现资料包主要面向高校及科研机构中从事智能计算、神经网络预测研究的师生适用于课程设计、毕业设计与学术探索。压缩包共23个文件以Python源码py、训练模型权重h5、数据文件csv及可视化图像png为主整体大小约3.32MB目录按模型SAE、LSTM、GRU分组并包含README说明文档便于快速定位与复现。目前已有29人学习下载。内容涵盖完整执行数据、结构化代码框架以及标准化说明文档三种模型的训练与对比过程均可直接运行验证可帮助读者理解深度学习在交通流预测中的建模思路与调参方法是入门时序预测与模型对比实验的实用参考。1. 交通流预测为什么卡在“最后一公里”SAE、LSTM、GRU各自解决什么问题交通流短时预测做了几年的人基本都有同一个体感模型从论文里搬到自己的数据上效果经常对半砍。不是LSTM、GRU这些结构不好用而是数据入口的坑比模型结构更致命。这个任务本质是带强周期性的时间序列回归检测器每5分钟回传一次流量、速度和占有率我们需要用最近一个时间窗预测下一时段的路况。SAE负责把高维、带噪声的输入特征压缩成更紧致的表达LSTM和GRU负责把时间依赖建模出来Python生态则负责把这三者串成能直接跑通的代码。这套组合在深度学习实战项目里已经算固定搭配但真正落地时光是一个归一化范围配错就能让训练白跑一天。这篇文章会按“数据清洗 → 特征提取 → 序列建模 → 避坑 → 验证”的链路展开把每个环节的参数、代码和坑位摆出来适合正在做智能交通、路况预测或时序回归的从业者直接参照搭基线。2. 建模前的数据准备交通流数据清洗、时间窗构造与训练集划分2.1 原始检测器数据长什么样缺失值、异常尖峰与修复策略先明确输入数据。常见的高速公路或城市快速路检测器按5分钟粒度上报字段至少包含流量veh/5min、平均速度km/h、时间占有率%。数据质量比模型结构更影响最终MAPE线圈检测器掉线、漂移是常态我见过一个月的原始数据里缺失率超过15%、单日出现几十个传感器异常脉冲这种数据直接进LSTM结构再强也白搭。处理顺序有讲究先补缺失再剔异常最后对齐时间戳。缺失值用线性插值不要只用前向填充一条道走到黑——流量序列在早晚高峰陡升陡降ffill会让突变点后移把峰谷相位带偏。异常尖峰用滚动中位数加绝对中位差MAD识别流量在相邻窗口内跳变超过3倍MAD基本可以判定为检测器脉冲干扰。import pandas as pd import numpy as np # 读取5分钟粒度检测器数据 df pd.read_csv(traffic_flow.csv, parse_dates[time]) df.set_index(time, inplaceTrue) df.sort_index(inplaceTrue) # 1) 先看缺失率决定要不要补 print(df.isnull().mean()) # 2) 线性插值补齐限定连续缺失不超过6个点 df df.interpolate(methodlinear, limit6, limit_areainside) # 3) 基于滚动中位数的异常修复 win 12 # 12 x 5min 1小时窗口 median df[flow].rolling(windowwin, centerTrue).median() mad (df[flow] - median).abs().rolling(windowwin, centerTrue).median() thresh 3 * 1.4826 * mad bad (df[flow] - median).abs() thresh df.loc[bad, flow] median[bad]第3步里的1.4826是MAD换算到标准差的系数乘3就相当于3σ略激进。如果数据本身噪声大把阈值放宽到4甚至5更稳。rolling窗口取12个点即1小时比较合适窗口再小会把正常的峰谷当成异常抹掉。做完这三步把流量曲线画出来扫一眼应该能明显看到尖刺被压平。2.2 滑窗构造look_back怎么选才不玄学把时间序列预测转成监督学习常见做法是滑窗窗口里放look_back个历史时间步目标是下一个时刻或未来半小时。这里的核心矛盾是窗口太短模型看不到早晚高峰的上升趋势窗口太长模型被冗余历史拖累训练成本也上去。我做交通流时一般把look_back设成12到24个点也就是1到2小时。在此基础上我通常会叠加一个“同时刻前一天”的特征比如要预测早上9点就把昨天9点、前天9点的值作为额外特征拼进输入。这个trick比单纯把窗口加到48更有效等于直接告诉模型这是周期性数据。滑窗代码本身很简单但边界索引必须写对。def create_sequences(data, look_back12, horizon1): 把二维数组转成(样本数, look_back, 特征数)的监督样本。 data: 归一化后的二维数组 look_back: 历史时间步数 horizon: 预测未来第几个点1表示下一时刻 X, y [], [] for i in range(len(data) - look_back - horizon 1): X.append(data[i:i look_back]) y.append(data[i look_back horizon - 1]) return np.array(X), np.array(y) X, y create_sequences(scaled_array, look_back12, horizon1) print(X:, X.shape, y:, y.shape)X的shape是(样本数, 12, 特征数)这个三维张量可以直接喂给PyTorch的LSTM或GRU。horizon1做的是单步预测要预测30分钟后就把horizon改成6。特别提醒y取的是未来第6个点的值不是未来6个点的平均这两者含义不同写代码时别混。特征列如果包含次日周期性特征需要在create_sequences之前拼接到原始数组上再一起滑窗顺序不能乱。2.3 归一化与训练/验证/测试划分一个容易忽视的数据泄漏点归一化在这种任务里不是可选项。流量、速度、占有率量纲不同不归一化直接进LSTM梯度会被大数值特征主导收敛慢且容易震荡。MinMaxScaler把每个特征压缩到[0,1]比StandardScaler更适合配合Sigmoid或ReLU系列激活函数。这里有两个点必须重视一是scaler只能fit训练集二是划分必须按时间顺序不能随机打乱。from sklearn.preprocessing import MinMaxScaler # 按时间顺序划分为训练/验证/测试 n len(df) n_train, n_val int(n * 0.7), int(n * 0.15) train_raw df.iloc[:n_train] val_raw df.iloc[n_train:n_train n_val] test_raw df.iloc[n_train n_val:] # 关键只用训练集fit验证/测试集用同一个scaler做transform scaler MinMaxScaler(feature_range(0, 1)) train_scaled scaler.fit_transform(train_raw) val_scaled scaler.transform(val_raw) test_scaled scaler.transform(test_raw) # 构造滑窗样本 X_train, y_train create_sequences(train_scaled, look_back12, horizon1) X_val, y_val create_sequences(val_scaled, look_back12, horizon1) X_test, y_test create_sequences(test_scaled, look_back12, horizon1)注意create_sequences是在划分后的数据上分别执行而不是在整个序列上做完再切否则验证集的样本会用到前一段数据的时间步造成隐性数据泄漏。feature_range显式写成(0,1)是为了跟后面SAE的Sigmoid解码输出保持一致。如果后续激活函数换成tanh归一化范围要对应改成(-1,1)这是很多人翻车的地方。时序任务也尽量不要用随机K折交叉验证我一般用“滚动验证”训练集前推、验证集后移最后再固定测试集这样评估出来的指标才接近上线后的真实水平。3. 用SAE做特征提取堆叠自编码器的预训练与参数细节3.1 自编码器为什么能用于交通流特征降维自编码器做的事情说白了就是“压缩-重建”encoder把输入D维压成d维decoder再把d维重建回D维损失函数衡量重建误差。交通流数据有天然冗余——流量、速度、占有率高度相关早晚高峰同步变化直接把这些原始特征送进LSTM不是不行但噪声和冗余会让模型去拟合不稳定的细节泛化变差。SAE把多层AE叠起来逐层提取更抽象的特征这在深度学习知识点里属于无监督预训练那一类特别适合交通流这种标注成本高但历史数据量大的场景。实际工程里SAE有两种用法。一是当特征提取器先拿无标注数据训练好encoder之后把LSTM的输入替换成压缩后的低维特征二是当初始化工具用encoder参数初始化LSTM输入层的权重再端到端微调。前一种在数据量小时更稳后一种在数据量足够大时上限更高。我一般先用第一种把基线跑通再对比要不要微调。3.2 SAE网络结构与Python实现从输入层到压缩特征这里给出一个逐时刻压缩的清晰方案。假设原始特征有3列流量、速度、占有率每个时间步的3维特征通过SAE压缩到1维压缩后的序列再送入LSTM。如果用的是多传感器数据把输入维度从3扩展成几十维即可结构逻辑不变。import torch import torch.nn as nn class AutoEncoder(nn.Module): def __init__(self, in_dim, hidden_dim): super().__init__() self.encoder nn.Sequential( nn.Linear(in_dim, hidden_dim), nn.ReLU(inplaceTrue), ) self.decoder nn.Sequential( nn.Linear(hidden_dim, in_dim), nn.Sigmoid(), # 输入在[0,1]时用Sigmoid重建 ) def forward(self, x): return self.decoder(self.encoder(x))输入是3维编码到2维再重建回3维。hidden层的输出就是压缩后的特征。如果要多加一层比如3→8→4→8→3把encoder和decoder写成更长的Sequential就行。decoder最后一层用Sigmoid是因为前面MinMax归一化到[0,1]如果改用StandardScaler这里要去掉激活或换成恒等映射否则输出范围会卡死。训练AE的过程和训练普通回归模型没区别但要注意batch里的数据可以shuffle因为重建任务与时间顺序无关。def train_ae(model, X, epochs50, lr1e-3, batch_size256): criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lrlr) dataset torch.utils.data.TensorDataset(torch.FloatTensor(X)) loader torch.utils.data.DataLoader(dataset, batch_sizebatch_size, shuffleTrue) for epoch in range(epochs): for (batch,) in loader: recon model(batch) loss criterion(recon, batch) optimizer.zero_grad() loss.backward() optimizer.step() if (epoch 1) % 10 0: print(fepoch {epoch1}, recon loss {loss.item():.5f}) return modelshuffleTrue只用于预训练阶段等真正提取特征喂给LSTM时数据必须保持时间顺序因为LSTM要靠序列姿态捕获前后依赖。batch_size256对交通流这种小时级数据量够用样本多时调到512或1024能省不少训练时间。3.3 逐层贪心预训练三个关键参数与微调策略堆叠自编码器的核心动作是逐层训练先训练第一层AE(3→2→3)然后把每个样本经第一个encoder得到的2维输出作为第二个AE的输入接着训练第二层AE(2→1→2)。整个预训练过程不需要标签可以充分利用未标注的历史数据。# 假设 feats 是 (N, 3) 的逐时刻特征 ae1 AutoEncoder(in_dim3, hidden_dim2) train_ae(ae1, feats, epochs50, lr1e-3) with torch.no_grad(): feats_2d ae1.encoder(torch.FloatTensor(feats)).numpy() # 第二层2 - 1 - 2 ae2 AutoEncoder(in_dim2, hidden_dim1) train_ae(ae2, feats_2d, epochs50, lr1e-3) with torch.no_grad(): feats_1d ae2.encoder(torch.FloatTensor(feats_2d)).numpy()三个关键参数要单独调。第一逐层预训练的学习率要比端到端小一档我一般用1e-3起步若loss震荡就降到5e-4千万别用默认的1e-2去训AE重建loss会直接飞掉。第二epoch数不宜过大AE拟合得过狠中间特征会把噪声也编码进去下一步LSTM反而在学噪声50轮左右就停多观察重建loss是不是还在持续下降。第三隐层宽度按特征维度的一半再砍半比较稳3维特征压到1维已经足够再强行加稀疏约束容易把有效信号压没。预训练结束后有两种接入方法分别对应不同的训练策略。class SAEEncoder(nn.Module): 把两层AE的encoder串起来输出压缩特征序列。 def __init__(self, ae1, ae2): super().__init__() self.encoder nn.Sequential( ae1.encoder, ae2.encoder, ) def forward(self, x): return self.encoder(x) # 用法对滑窗样本逐时间步压缩 # X: (B, T, 3) - (B, T, 1) sae_enc SAEEncoder(ae1, ae2) compressed [] for t in range(X.shape[1]): step_in torch.FloatTensor(X[:, t, :]) compressed.append(sae_enc(step_in).numpy()) X_compressed np.stack(compressed, axis1)注意这段代码是逐时间步压缩实际计算时可以先把X reshape成(B×T, 3)一次过encoder再reshape回(B, T, 1)速度快很多。我写这种循环是为了把“压缩发生在每个时间步上”这个逻辑说清楚。接下来X_compressed就直接送进LSTM的nn.LSTM(input_size1, ...)。如果测试下来预训练特征对预测没有帮助就放弃SAE直接让LSTM从原始特征学习后面避坑章会细说这个判断标准。4. LSTM与GRU做时间序列预测门控原理与PyTorch模型代码4.1 LSTM为什么适合交通流时序门控机制与序列依赖循环神经网络的设计初衷就是处理变长序列LSTM在RNN基础上引入了遗忘门、输入门、输出门让梯度能沿着时间步传得更远。交通流的时间依赖跨度很大短则连续5分钟流量强相关长则早晚高峰之间存在跨小时的结构依赖。普通RNN在几十步之后基本记不住早期信息而LSTM能撑到上百步这正是它适合做交通流预测的原因。从实现角度理解LSTM每个时间步维护一个细胞状态c和一个隐状态h。遗忘门决定上一时刻的细胞状态保留多少输入门决定当前信息写入多少输出门决定隐状态输出多少。这三个门组合起来让模型既能记住“昨天同时段开始拥堵”这种周期性信息又能及时丢弃“临时事故已经结束”的过时状态。实际调参时hidden_size就是隐状态的维度这个值直接决定了模型记忆容量。4.2 GRU原理与LSTM的差异轻量替代的选型判断GRU把LSTM的遗忘门和输入门合并成了更新门又加了重置门参数比LSTM少四分之一左右。对交通流这种中等长度序列GRU的训练更快在数据量不足时往往比LSTM更稳。GRU的原理可以简单记成两条更新门控制“保留多少旧状态”重置门控制“忽略多少历史信息”。它的计算路径更短梯度传播更顺畅因此收敛速度通常明显快于LSTM。选型判断有一条比较实用的经验第一次跑新数据集时先用GRU做baseline因为GRU对学习率和初始化不那么敏感能在短时间内告诉你这个数据集的预测难度等GRU基线稳定后再换成LSTM对比如果LSTM的验证集指标没有显著提升就继续用GRU。多步预测或者需要建模复杂跨天依赖时LSTM的两套门控表达力更强赢面更大。对比项LSTMGRU门控数量遗忘门、输入门、输出门更新门、重置门参数规模约4组权重约3组权重收敛速度相对慢相对快小数据量表现容易过拟合更稳复杂长周期依赖更强稍弱这张表是选型时的第一判断依据。通常GRU的hidden_size可以比LSTM小一些因为它的参数效率更高我一般从32开始过拟合再往16降。4.3 LSTM/GRU模型代码与训练流程模型定义本身很简洁重点是几个维度的对应关系。class TrafficLSTM(nn.Module): def __init__(self, input_size3, hidden_size32, num_layers2, output_size1, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout, ) self.fc nn.Linear(hidden_size, output_size) def forward(self, x): out, _ self.lstm(x) # out: (B, T, hidden_size) out self.fc(out[:, -1, :]) # 取序列最后一个时间步 return outbatch_firstTrue意味着输入形状是(B, T, F)PyTorch 2.x里必须显式声明。out[:, -1, :]是取最后一个时间步的隐状态再经过fc输出预测值。如果要做多变量同时预测把output_size改成预测的特征数量即可。GRU版本几乎一模一样只是把nn.LSTM换成nn.GRU。class TrafficGRU(nn.Module): def __init__(self, input_size3, hidden_size32, num_layers2, output_size1, dropout0.2): super().__init__() self.gru nn.GRU( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout, ) self.fc nn.Linear(hidden_size, output_size) def forward(self, x): out, _ self.gru(x) out self.fc(out[:, -1, :]) return out训练循环有一个值得注意的细节梯度裁剪。def train_model(model, X_train, y_train, X_val, y_val, epochs80, lr1e-3, clip1.0): criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lrlr) for epoch in range(epochs): model.train() optimizer.zero_grad() pred model(X_train) loss criterion(pred, y_train) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), clip) optimizer.step() if (epoch 1) % 10 0: model.eval() with torch.no_grad(): val_pred model(X_val) val_loss criterion(val_pred, y_val) print(fepoch {epoch1:3d}, train {loss.item():.5f}, val {val_loss.item():.5f}) return modelclip_grad_norm_设成1.0能防止交通流序列里的突变样本把梯度带崩。lr1e-3是LSTM的常见起点GRU我一般从5e-4开始。epochs80在小数据集上足够如果val_loss在40轮后还在明显下降就把epochs往上加到150同时配合早停。hidden_size和num_layers我会按32/2作为默认组合再用16/1和64/3各跑一组对比不同数据集的敏感度差异很大这个步骤不要省。5. 避坑指南训练不收敛、预测延迟一个相位、过拟合……这五个坑你也会踩5.1 坑一归一化范围与激活函数不匹配loss剧烈震荡不下降现象很直观loss在0.5上下反复跳或者训练了二三十轮完全不动打印出来的train loss和val loss像两条水平线。原因多半是目标值和输出层的值域不匹配。比如MinMax归一化到[0,1]但输出层用了nn.Linear接nn.Tanh()模型输出被压到[-1,1]和真实label的[0,1]范围不一致或者是decoder用了Sigmoid但归一化用的是StandardScaler输出永远到不了负值区间。解决方法是统一口径用MinMax归一化(0,1)预测头就是纯nn.Linear用tanh激活数据就得归一化到(-1,1)。改了任何一边另一边必须跟着动。还有一个隐蔽版本是训练时忘了切换model.train()和model.eval()dropout在验证阶段仍在工作loss自然就是震荡的。先排除这个再改模型结构。5.2 坑二数据泄漏——用全局统计量归一化验证集loss虚低有个典型现象是验证集指标好得离谱MAPE只有3%一到真实环境或者测试集就变成15%。最常见的原因是归一化时对整个数据集做了fit_transform。比如先拼接所有数据再MinMaxScaler.fit归一化时就已经把未来数据的最小最大值带进来了等于验证集提前看到了分布信息。另一个同源问题是划分数据集前先做了随机shuffle时间序列被搅乱模型在验证集上遇到的是“未来预测过去”的作弊样本。解决就一条scaler只能在训练集上fit验证集和测试集只做transform划分严格按时间顺序相关代码我放在2.3节。另外滑窗构造也要在划分之后执行不能先做滑窗再切否则边界样本一样会串数据。验证集loss虚低还有一个隐蔽来源是早停时用了验证集调参、又用验证集汇报正确的做法是划分出第三个测试集只在全部实验结束后碰一次。5.3 坑三GRU收敛快但震荡大learning rate要单独调用同一套超参数跑LSTM和GRU最常见的结果是GRU前20轮降得飞快往后却在最优值附近反复横跳甚至把已经降下来的val_loss又拉回去。原因是GRU参数路径短、更新幅度天然比LSTM大同一个学习率下它的有效步长更激进。我一般会把GRU的学习率设成LSTM的0.5到0.7倍LSTM用1e-3时GRU就用5e-4。更省事的方案是接入学习率调度器让验证集指标停滞时自动降学习率。scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience5, min_lr1e-6 ) # 每个epoch结束后调用 scheduler.step(val_loss)patience5表示验证集连续5轮不下降才降一半学习率min_lr给个下限防止过度退化。加上这个GRU的震荡问题至少能缓解一半剩下的靠梯度裁剪和batch_size调大来兜底。5.4 坑四预测结果整体“延迟一拍”问题不在模型而在滑窗预测曲线画出来是真实曲线的整体右移早晚高峰永远对不上看起来像是预测了个寂寞。这不是网络结构有问题而是模型学到了“把上一时刻的值复制过来”。交通流曲线高度自相关相邻5分钟流量本来就差不大用递归预测方式把上一步输出当作下一步输入时模型会倾向于输出一个近似不变的常量整体曲线被时间轴拖慢。解决的关键在于改变目标构造。如果要做未来30分钟的预测就不要用horizon1单步滚动直接把target改成第6个点让模型被迫去预测更远的未来。输入侧也可以叠加“同时刻前一天”的特征模型有了周期参照就不会只盯着上一时刻做惯性复制。另外评估时不要只算RMSE把曲线画出来观察相位对齐程度延迟问题肉眼一眼就能看出来。5.5 坑五SAE预训练加微调反而掉点什么时候该放弃预训练SAE预训练后接入LSTM一起微调结果MAPE比随机初始化直接训练还差这不是代码写错。原因是预训练阶段没有监督信号AE学到的特征分布和“预测未来流量”这个目标并不对齐。数据量越小这个错位越明显因为模型本来就没有足够样本去修正预训练带来的偏置。我在几万条样本的数据集上做过多次对比预训练微调模式普遍不如直接端到端。判断标准可以看数据量几十万条以下先试“预训练后冻结encoder只训练LSTM”如果val_loss没有明显优势果断放弃预训练路径。几十万条以上预训练才有稳定正收益。做这个对比时必须保证随机种子一致、训练轮数相同否则预训练和端到端的差距会被随机性掩盖。如果决定不用SAE直接把原始特征送入LSTM通常不会比加了错误正则的预训练差。6. 验证与进阶三个指标的算法规避与Attention、多步预测的扩展思路6.1 MAE、RMSE、MAPE三个指标怎么算、怎么看指标不能只看一个。MAE反映平均绝对误差RMSE对大误差更敏感MAPE则以百分比形式直接说明预测精度。计算MAPE时要注意流量接近零的时段分母趋近零会让误差爆炸我一般在分母上加一个平滑项。from sklearn.metrics import mean_absolute_error, mean_squared_error def evaluate_model(model, X_test, y_test, scaler, feature_idx0): model.eval() with torch.no_grad(): pred model(X_test).numpy() pred_inv scaler.inverse_transform(pred) y_inv scaler.inverse_transform(y_test) mae mean_absolute_error(y_inv, pred_inv) rmse np.sqrt(mean_squared_error(y_inv, pred_inv)) mape np.mean(np.abs((y_inv - pred_inv) / (y_inv 1e-3))) * 100 return mae, rmse, mape反归一化时一定要用训练集那个scaler而不是重新fit否则指标全部失真。汇报结果时把三个指标一起列出来只看RMSE会被个别事故日的大误差带偏只看MAPE又容易低估晚高峰的绝对偏差。6.2 往哪个方向扩展Attention、多步预测、多源数据融合模型基线稳定后提升效果最明显的通常不是换更大的LSTM而是改造目标函数和输入结构。多步预测可以换成sequence-to-sequence结构用LSTM编码历史窗口、GRU解码未来多步Attention则让模型在解码每一步时自动回顾历史窗口中最相关的时刻对早晚高峰的相位对齐有直接帮助。把天气、节假日、上下游断面流量拼进特征矩阵属于成本最低的扩展方式很多数据集上比加一层LSTM管用。6.3 一个值得养成的习惯固定随机种子、重复实验、记录实验日志最后说一个你早晚会后悔没早做的事跑模型前固定随机种子。PyTorch的权重初始化和DataLoader的shuffle都带随机性不固定种子的话同一个模型同一次实验跑两遍MAPE能差出1到2个百分点这时候任何调参结论都是不可信的。我现在的做法是固定Python、NumPy、PyTorch三个种子并把种子编号写进实验记录表每次调参只改一个变量。配上TensorBoard或简单的CSV日志把每个配置的loss曲线、指标和耗时都留下来你回头看时才知道哪些改动是真有效哪些只是随机波动。这个习惯帮我少走了很多弯路希望帮到你。本文还有配套的精品资源点击获取
返回列表