LSTM+SAE+CNN组合模型实现高速公路交通流预测 简介高速公路交通流预测是一类典型的时空序列建模问题这套资源给出了融合LSTM、SAE与CNN三种深度学习模型的Python解决方案面向智能交通领域的研究生、算法工程师以及正在学习时序深度学习的开发者。资源共20个文件包含7个Python源码文件覆盖数据预处理、SAE特征提取、LSTM与CNN建模及模型对比6个训练好的h5模型权重和4个json配置可直接加载测试2张png为模型结构对比示意图另附PeMS高速公路数据集。整体压缩包31.48MB已有2441人学习下载。读者既能从数据整理到模型训练与评价的完整流程中理解交通流预测的工程实现也能利用预训练模型快速验证效果并借鉴CNN提取空间特征、LSTM捕捉时序依赖的融合设计迁移到其他路段或相关时序预测项目。1. 高速公路交通流预测为什么是 LSTMSAECNN 而不是单一模型凌晨四点高速断面检测器每五分钟吐一条记录流量、速度、占有率。要预测下一个小时这段路的拥堵趋势ARIMA 在线性场景还凑合一旦碰上早高峰突变、事故清障、节假日潮汐误差直接放飞。常见的解法是深度学习组合LSTM 抓长时间依赖CNN 提取局部突变模式SAE 在输入端做无监督特征提取把三个原始字段洗成更紧凑的表示。这套 LSTMSAECNN 结构在高速公路交通流预测TrafficFlowPrediction的论文和开源项目里很常见适合做毕设、横向课题也想真上线基础预测服务的人。下面按一条完整落地路径走数据集清洗、SAE 预训练、双分支模型搭法、参数调优、数据泄漏排查。2. 数据集与预处理从检测器原始数据到可训练的滑窗样本2.1 公开数据集怎么选先看字段再谈建模拿到的数据决定后面一切工作的复杂度。公开数据集里PeMS 这种带独立检测器编号的长时序数据最省事字段一般有 timestamp、station_id、flow、occupancy、speed 和采样间隔。选数据集时先确认三件事采样间隔是多少5 分钟还是 15 分钟、字段是否完整只有流量不够、站点之间有没有上下游关系。前两个决定滑窗参数怎么设第三个决定你做单站点还是多站点预测。自己接高速收费流水或雷达检测器数据也行但要额外处理时区、节假日、免费放行等噪声。三个字段为什么要凑齐流量是预测目标速度反映通行状态占有率反映拥堵程度。深度学习模型可以直接把三者作为输入特征让模型自己学组合关系比手工构造 V/C 比这类特征省事得多。字段含义常见脏数据timestamp检测时间时区错乱、采样间隔不齐station_id检测器编号编号漂移、重复flow车流量负值、跳变speed平均速度0 值、超过 160occupancy占有率超过 100%2.2 缺失值与异常值清洗这一步翻车最多我见过最典型的翻车模型 Loss 迟迟不降画预测曲线才发现某站点连续三小时速度是 0、流量却正常——这是检测器故障不是真实堵车。清洗的通用顺序是按站点分组 → 时间排序 → 异常值置空 → 插值填补。缺失率低于 5% 用线性插值加前向填充缺失率超过 20% 的站点建议直接剔除强行插值只会造出假的拥堵模式。import pandas as pd import numpy as np def clean_station(df): # 先按时间排序检测器数据经常乱序 df df.sort_values(time).reset_index(dropTrue) # 异常值先置为 NaN避免插值时把脏值当正常值 df.loc[df[speed] 160, speed] np.nan df.loc[df[speed] 0, speed] np.nan df.loc[df[flow] 0, flow] np.nan df.loc[df[occupancy] 100, occupancy] np.nan # 线性插值填补中间缺失ffill/bfill 只兜头尾边界 df[flow] df[flow].interpolate(methodlinear).ffill().bfill() df[speed] df[speed].interpolate(methodlinear).ffill().bfill() df[occupancy] df[occupancy].interpolate(methodlinear).ffill().bfill() return df逻辑说明先 sort 是因为检测器数据经常不按时间写入异常值置空而不是直接删行是为了保住时间轴连续interpolate 只处理中间段头尾必须靠 ffill/bfill 补否则边界缺失会整段报废。参数 methodlinear 对 5 分钟粒度的交通流数据够用更高阶的 spline 容易造出负流量。2.3 多站点组织与邻接矩阵为空间特征铺路只看一个断面是不够的上游拥堵会在 10~20 分钟后传导到下游。常见做法是把相邻 N 个检测器按空间顺序排成 (history_steps, n_stations) 的张量这样 CNN 可以在空间维度上卷积学上下游传播。不打算上图神经网络也可以先做一件事把 station_id 映射成有序序号后面无论是画热力图还是做误差分析都用得上。stations sorted(df[station_id].unique()) station_to_idx {s: i for i, s in enumerate(stations)} df[station_rank] df[station_id].map(station_to_idx)说明station_rank 就是空间维度的索引。真实高速路段里空间相关性并不严格等于桩号距离主路和匝道、上下游方向都要重新考虑以后要升级到图卷积再基于这个序号构建邻接矩阵用站间距离或旅行时间做权重顺序不能乱。2.4 滑窗构造样本history_steps 和 pred_steps 怎么定5 分钟粒度下预测未来 15/30/60 分钟分别对应 pred_steps3/6/12。history_steps 一般取 121 小时到 242 小时太短抓不到早高峰的上升段太长则增加模型负担。滑窗步长 step1 会生成海量样本训练时间长样本充足时可设 step2 抽样效果几乎不变。def make_sequences(X, history_steps12, pred_steps6, target_col0): Xs, ys [], [] for i in range(len(X) - history_steps - pred_steps 1): Xs.append(X[i : i history_steps]) # 注意预测窗口从 ihistory_steps 开始不能从 i 开始 ys.append(X[i history_steps : i history_steps pred_steps, target_col]) return np.array(Xs), np.array(ys)逻辑说明y 的窗口是历史的下一步模型输入 t-history1 到 t输出 t1 到 tpred_steps。如果把当前时刻的流量也放进 y模型会直接把 t 时刻的输入抄成输出训练指标好看但测试完全没有泛化能力。预处理还有一个关键顺序先清洗插值再按站点分组归一化最后滑窗。归一化建议用 MinMaxScaler 压到 [0,1]fit 只能用在训练集上验证集和测试集直接 transform——这个顺序错了就是数据泄漏后面第 5 章单独展开。归一化之后的滑窗数据就可以进 SAE 做无监督特征提取了。3. SAE 堆叠自编码器先用无监督重构把特征洗干净3.1 为什么在 LSTM 前面加一层 SAE交通流数据有三个特点检测器抖动带来噪声、流量和占有率高度相关、状态分布极度不均深夜低谷远多于高峰。直接把原始三字段喂给 LSTM模型要花大量参数量去拟合噪声和冗余。SAE 做的事情很简单先无监督学一个压缩-重建通路只保留能还原出原数据的低维表示再用这个表示去喂下游预测模型。和 PCA 对比PCA 是线性降维交通流的流量-速度-占有率关系有明显非线性SAE 用多层非线性变换能把「高峰期流量大、速度低、占有率高」这类组合模式压缩得更彻底。多数对比实验里SAE 预训练后接 LSTM 的收敛速度和最终误差都优于直接 LSTM 和 PCALSTM。代价是多了一段预训练时间通常 GPU 上几分钟到十几分钟能接受。方案特征表示问题原始特征直接 LSTM无降维噪声敏感、收敛慢PCA LSTM线性降维丢非线性关系SAE LSTM无监督非线性表示多一段预训练3.2 结构设计拉平输入和两层编码器SAE 的输入不是单个时刻而是一整段滑窗。把 (history_steps, n_stations, n_features) 拉平成 (history_steps * n_stations * n_features, )让编码器直接学习时空窗口内的状态分布。也有人对每个时间步单独编码但那样会丢掉时间上下文后面再接 LSTM 时上下文的信息已经损失了。结构上我习惯两层编码128 → 64瓶颈 64 维再对称解码。激活函数中间层 ReLU输出层 sigmoid因为输入已经归一化到 [0,1]。两层编码器的理由交通流数据量一般在几万到几十万条两层足够表达常见交通状态再加层数会开始记住噪声重构损失虽然更低但下游预测反而变差。import tensorflow as tf from tensorflow.keras import layers, Model def build_autoencoder(input_dim, encoding_dims[128, 64]): inputs layers.Input(shape(input_dim,)) x inputs for dim in encoding_dims: x layers.Dense(dim, activationrelu)(x) encoded x # 解码层顺序反转输出层用 sigmoid 对应 [0,1] 输入 x encoded for dim in reversed(encoding_dims): x layers.Dense(dim, activationrelu)(x) x layers.Dense(input_dim, activationsigmoid)(x) autoencoder Model(inputs, x) encoder Model(inputs, encoded) return autoencoder, encoder说明autoencoder 用于预训练encoder 用于提取特征两个模型共享权重调用 predict 时只走 encoder 的前半部分。3.3 训练自编码器损失、优化器和早停重构损失用 MSE优化器 Adam初始学习率 0.001。几十轮就够因为自编码器学的是低层统计规律。要盯着验证集重构误差做早停不然随着训练进行模型会把检测器噪声也逐步重构出来特征表示越来越不鲁棒。autoencoder.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-3), lossmse ) history autoencoder.fit( X_train_flat, X_train_flat, validation_data(X_val_flat, X_val_flat), epochs80, batch_size256, callbacks[tf.keras.callbacks.EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue )] ) # 供下游模型使用的特征 features_train encoder.predict(X_train_flat) features_val encoder.predict(X_val_flat)逻辑说明fit 的输入和输出都是 X_train_flat这是重构任务自监督的定义。EarlyStopping 监控 val_losspatience 设为 10表示连续 10 轮不降就停。restore_best_weights 会把权重回滚到最优轮次防止验证损失已经上升但权重停在过拟合点。batch_size 这里要特别说设得太大如 1024会让重构误差快速收敛到均值附近低谷和高峰的样本被平均掉设太小又容易震荡。256 对交通流数据是比较稳的起点。3.4 SAE 和原始特征拼接一个容易被忽略的细节编码后的 64 维特征要不要和原始数据拼接再进预测模型我的经验是拼接而不是替代。SAE 在压缩过程中丢掉了一些高频细节比如事故瞬间的速度骤降这些细节对短时预测恰恰重要。把原始滑窗和 SAE 特征拼接让 LSTM/CNN 自己决定该信哪份。如果实验发现拼接没有提升再退回只用编码特征——这是典型的「跑一版才知道」的玄学但在多个公开数据集上拼接确实更稳。4. LSTM 与 CNN 双分支融合时序依赖和局部模式怎么分而治之4.1 双分支还是串联从交通流数据的特点选一说到深度学习 CNN 和 LSTM很多人第一反应是串联先 Conv1D 提取局部特征再进 LSTM 处理时序。两种结构都能跑但高速公路场景我更推荐双分支。原因在于交通流同时存在两种不同尺度的模式早高峰的周期性是长时依赖匝道汇入、事故清障这类事件是短时尖峰。LSTM 擅长长时依赖但输入维度高时收敛慢CNN 擅长用局部卷积核抓短时尖峰但池化会压缩时间分辨率。双分支让两者各管一段最后融合不会被中间层互相拖累。串联的问题在于 CNN 输出的时间步已经是被压缩过的LSTM 看到的是粗粒度的序列对 5 分钟粒度下的尖峰不够敏感。如果前面已经接了 SAE串联和双分支的差距会小一些因为 SAE 已经把特征洗过一遍但双分支仍然是最稳的选择。4.2 CNN 分支一维卷积的参数怎么设输入形状选 (history_steps, n_features)做多站点预测时把第二维换成 n_stations让卷积核在空间维度上滑动。代码里用两个 Conv1D 堆叠第一层 kernel_size3 在 5 分钟粒度下对应 15 分钟的局部窗口第二层卷积核实际感受野会扩大到 5~7 个时刻对「上游出事故下游 30 分钟后流量变化」这类传导关系有感知。from tensorflow.keras import layers, Model cnn_input layers.Input(shape(history_steps, n_features), namecnn_input) cnn layers.Conv1D(filters64, kernel_size3, paddingsame, activationrelu)(cnn_input) cnn layers.Conv1D(filters64, kernel_size3, paddingsame, activationrelu)(cnn) cnn layers.MaxPooling1D(pool_size2)(cnn) cnn layers.Flatten()(cnn) cnn layers.Dense(32, activationrelu, namecnn_dense)(cnn)参数说明filters64 是卷积核数量第一层足够太多参数容易过拟合。kernel_size3 是当前最常用的起点如果检测器噪声大可以试 kernel_size5相当于用更宽的窗口换平滑。paddingsame 保证输出长度不变避免边缘信息被丢弃。pool_size2 会把时间维度减半如果 history_steps 只有 6 或 12建议直接去掉池化层否则信息损失太大。4.3 LSTM 分支单元数和 dropout 怎么搭配LSTM 分支把整个历史序列逐时刻读入建模早高峰的重复模式。输入形状同样 (history_steps, n_features)如果你想喂 SAE 特征就先把 SAE 编码向量广播或拼接进去输入维度相应变大。层数选两层第一层 64 单元保持完整序列第二层 32 单元输出最终状态。单层容易欠拟合三层在中小数据集上过拟合明显。lstm_input layers.Input(shape(history_steps, n_features), namelstm_input) lstm layers.LSTM(64, return_sequencesTrue, namelstm_1)(lstm_input) lstm layers.Dropout(0.2)(lstm) lstm layers.LSTM(32, namelstm_2)(lstm)说明return_sequencesTrue 让第一层输出所有时刻的隐向量第二层只输出最后一个时刻的状态Dropout 只加在第一层输出后第二层输出就要进融合层了不再加因为那点信息量已经不大。4.4 融合层与输出从头搭建并训练双分支输出的维度一个是 32一个是 32在融合层拼接后经过一层 32 维 Dense 学非线性组合最后输出 pred_steps 个预测值。输出层不用激活函数流量是回归任务发现预测值有负值时再换 softplus。concat layers.Concatenate(namefusion)([cnn, lstm]) x layers.Dense(32, activationrelu)(concat) x layers.Dropout(0.1)(x) output layers.Dense(pred_steps, nameoutput)(x) model Model(inputs[cnn_input, lstm_input], outputsoutput) model.compile(optimizeradam, lossmse, metrics[mae]) model.summary()逻辑说明inputs 列表顺序和训练数据一致数据侧要准备两个形状完全相同的数组 [X_cnn, X_lstm]。如果 CNN 和 LSTM 窗口不同只需要把两个 Input 的 shape 改成不同长度数据侧切出两个不同滑窗即可。4.5 训练数据形状最容易卡住的环节新手最容易在这里报错。model.fit 的 X 必须是一个长度为 2 的列表两个元素形状都是 (样本数, history_steps, n_features)y 的形状是 (样本数, pred_steps)。忘了在滑窗函数里保留 n_features 维度或者少写一个 Input都会直接 shape mismatch。一个很省事的检查方法是拿一个 batch 先 model.predict 一次看输出维度是不是 (batch, pred_steps)再跑 fit。如果想把 SAE 接成一个端到端模型可以直接用函数式 API 把 encoder 的输出接到 lstm_input 上。但要注意端到端训练时 SAE 权重会被下游监督信号微调这不算错只是会失去「SAE 特征纯净」的意义。我一般保持两步走SAE 先预训练固定权重提取特征存成 numpy 数组再喂给 LSTMCNN 模型。好处是省显存调参时不用反复重新预训练。5. 训练策略与避坑指南参数怎么调、哪里容易翻车5.1 先跑通再调参一组稳定的基线数值我每次拿到新数据集都先固定一套参数跑通流程不急着调。Adam 初始学习率 0.001loss 用 MSEbatch_size 128epochs 200EarlyStopping 的 patience 设 10同时加 ReduceLROnPlateau 让学习率在平台期衰减一半。整套流程跑通后看验证集 RMSE再进入调参阶段。callbacks [ tf.keras.callbacks.EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue ), tf.keras.callbacks.ReduceLROnPlateau( monitorval_loss, factor0.5, patience5, min_lr1e-5 ) ] model.fit( [X_cnn_train, X_lstm_train], y_train, validation_data([X_cnn_val, X_lstm_val], y_val), batch_size128, epochs200, callbackscallbacks )说明EarlyStopping 的 patience 必须大于 ReduceLROnPlateau 的 patience否则学习率刚降一轮模型还没机会在更小的步长上搜索就被早停掐断。min_lr 设 1e-5 保证还有微调余地。5.2 滑窗参数先扫再用history_steps 我固定预测步长后只扫 6、12、24 三档。注意 history_steps 越大SAE 输入维度越大每次都要重新预训练一次很浪费时间如果在没有 GPU 的机器上做建议只用 12 和 24 两个候选。pred_steps 按业务需求来做 30 分钟预测比 60 分钟稳得多误差随预测步长线性增长是正常现象。5.3 评估指标怎么选别被 MAPE 骗了交通流预测论文里常见 RMSE、MAE、MAPE。我的习惯是 RMSE 看高峰大误差MAE 看整体误差MAPE 作为参考但会做阈值处理。原因很简单MAPE 在流量接近 0 时会爆炸一个深夜的绝对误差 5 辆车就能让整体 MAPE 上升十几个点。指标看什么坑RMSE高峰大误差被极端值带高MAE整体平均误差对量纲敏感MAPE百分比误差流量接近 0 时爆表5.4 避坑 1归一化泄漏现象验证集 loss 异常低但画预测曲线整体偏小像是被什么东西拽住了。原因在切分数据集之前就对全量数据做了 MinMaxScaler验证集的最大值、最小值已经参加过训练。解决必须先切分再 fit 训练集transform 全部。代码里 fit 只出现在 train 上。from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler() X_flat_train_scaled scaler.fit_transform(X_flat_train) X_flat_val_scaled scaler.transform(X_flat_val)说明如果做多站点不同站点的流量量级差异很大主线 vs 匝道建议按站点分组各做各的归一化。直接对全量做 MinMax 会把量级差异混进特征模型要额外花功夫去学站点身份。5.5 避坑 2滑窗数据泄漏现象训练和验证指标都极好MAPE 甚至 3% 以下上线之后完全不是那么回事。原因最常见的是没有按时间顺序划分而是直接随机切分同一天的未来样本出现在训练集验证集和它高度重叠或者滑窗函数写错把 t 时刻的流量放进了 y模型等于提前看到了未来。解决切分按时间顺序划分构造滑窗前先排序滑窗函数里确保输入是截止到 t 的历史输出从 t1 开始。注意时间序列验证必须按时间顺序划分随机 K 折在这里是错的。同一天的样本会被拆进训练和验证两个集合指标自然漂亮但实际情景是预测未来。5.6 避坑 3深夜流量带来 MAPE 爆表现象RMSE 正常MAPE 却高达几百。原因夜间流量接近 0真实值 5 预测值 10 的绝对误差很小除法却给了巨大百分比。解决计算 MAPE 时过滤掉真实流量小于某个阈值比如 10 辆/小时的样本或者改用对零值更平滑的 SMAPE。报告结果时把白天和夜间分开列会更诚实。5.7 避坑 4预测负值和异常尖峰现象预测流量出现 -5、-20 这种明显不可能的值。原因输出层是线性输出MSE 对负值没有约束而模型在低谷时段外推过头。解决输出层换 softplus 激活softplus 输出非负且平滑或者预测完之后在代码里手动 clip 到 [0,1] 再还原。用 softplus 的坑是初始输出在 0.69 附近收敛速度比线性输出稍慢耐心跑就行。6. 更进一步多步预测与模型验证以及这套组合的边界6.1 直接预测还是递归预测前面输出 pred_steps 个数属于直接预测训练简单梯度一步到位。递归预测则是每次只预测下一个时刻再把预测值当作输入继续滚下去适合预测 2 小时以上。交通流场景下 1 小时内误差累积不明显我一般用直接预测如果要往后 3~4 小时递归预测更稳代价是训练时要处理 teacher forcing 和误差累积问题。可以把两者都跑出来对比你会看到误差在哪个步长开始加速上升这本身就是业务上重要的信息。6.2 验证方法先跑基线再谈模型评估时至少要和历史平均上周同时刻流量取均值、ARIMA、单一 LSTM 对比。固定随机种子用验证集选模型测试集只跑一次。结果放进表格不要只看最好的 RMSE方差的稳定性同样重要。模型RMSEMAEMAPE历史平均ARIMA单一 LSTMSAELSTMCNN实测经验里SAELSTMCNN 相对单一 LSTM 的提升通常在 5%~15%如果提升低于 5%先回头检查数据清洗和归一化顺序很多时候不是模型不够强而是特征输入没对齐。6.3 我的习惯训练前我会把窗口、SAE 预训练轮数、是否拼接、随机种子全部写进一个脚本或 json。有次我为了改一个参数顺手把滑窗步长从 1 调成 2验证集指标好了点测试集却变差了回头看才发现分割边界算错了——这就是黑匣子阶段最容易翻车的地方。要养成一个顺序先清洗插值再按站点归一化再滑窗最后按时间划分这个顺序我吃了不少亏才固定下来希望帮到你。本文还有配套的精品资源点击获取