简介LCTFP是一个针对高速公路短时交通流预测的深度学习项目采用1D CNN与LSTM组合结构分别提取历史车流数据的空间与时间特征解决传统模型难以同时刻画两类特征的不足适合智能交通领域研究者、时间序列建模初学者以及相关课题开发者使用。资源共21个文件、压缩包31MB包含7个Python脚本、6个H5模型权重、4个JSON配置、2张网络结构图等脚本覆盖原始数据读取、缺失值处理、时间归一化、滑动窗口构造、模型训练与评估等多个环节权重与配置文件可直接加载用于复现实验图片能辅助理解网络结构。随包提供PeMS真实高速公路交通流数据及完整处理流程支持通过hyperas自动搜索超参数并内置SAE、LSTM、CNN等基准模型用于横向对比。已有5334人学习适合需要快速运行完整交通流预测流程并做扩展实验的进阶开发者。整体代码模块化程度较高便于替换数据集或调整网络参数也可迁移至其他交通流预测场景。1. 先说结论LCTFP 不是黑匣子它就是把 CNN 和 LSTM 串起来的高速公路交通流量预测模型LCTFP 这个缩写第一次看到确实容易懵但拆开看它做的事情并不玄学用 CNN 提取流量序列里的局部特征再用 LSTM 记住跨时段的依赖最后用 Python 输出未来半小时到一小时的高速公路交通流量预测结果。我在做断面流量短时预测时最看重的就是它能不能用一份普通收费站 5 分钟粒度数据直接跑通。它适合谁适合手里已经有流量、速度和占有率数据想做短时预测落地的人。比如匝道汇入预警、施工窗口安排、服务区饱和度预判都可以拿这个结构当基线。它不挑显卡普通 CPU 也能训练真正的难点不在模型有多复杂而在数据切分、滑动窗口构造和评估指标怎么设。下面按我平时从数据到上线的顺序讲。2. 数据准备才是 LCTFP 的胜负手从断面流量到滑动窗口样本用 LCTFP 之前先把交通流量预测问题定义清楚。高速公路交通流量预测的目标通常是“未来 N 个时间段的流量”不是一个点。如果数据是 5 分钟聚合一次未来 30 分钟就是 6 个点模型要一次输出 6 个值。把任务定义成多步回归后面评估才不会因为只看单点误差被带偏。2.1 先定特征、时间粒度和预测窗口我一般会保留下面四个字段。flow 是预测目标speed 和 occupancy 是辅助特征time 用来排序和构造时间窗口。如果收费流水能折算出车型比例也可以加进去但第一条原则是特征必须和预测目标来自同一断面不能把下游收费站的数据挪过来。否则模型学到的只是“下游流量传到上游”的假关系换个断面就失效。字段含义建议处理time5 分钟粒度断面采样时间升序排列按时间去重flow单位时间通过车辆数目标值缺失用线性插值speed断面平均车速缺失按邻域中位数补occupancy车道占有率与 flow 一起处理缺失时间粒度选 5 分钟比较折中。1 分钟噪声太大15 分钟会抹平早晚高峰的尖峰。预测窗口我常用 lookback12、horizon6也就是“看过去 1 小时预测未来半小时”。这个组合对收费站排队预警足够也不会让 LSTM 背太长的记忆负担。2.2 清洗两步走排序、去重、按时间插值拿到 CSV 后不要急着训练先做两件事把时间轴理干净把缺失值补得尽量不像假数据。import pandas as pd import numpy as np df pd.read_csv(flow_5min.csv, parse_dates[time]) # 1) 时间有序且唯一这是后续滑窗的前提 df df.sort_values(time).drop_duplicates(subsettime) # 2) 重采样保证每个 5 分钟都有记录 df df.set_index(time).resample(5min).mean().reset_index() # 3) 流量用线性插值不要用 0 填充 df[flow] df[flow].interpolate(methodlinear) df[speed] df[speed].interpolate(methodlinear) df[occupancy] df[occupancy].interpolate(methodlinear) print(df.head()) print(df.isna().sum())这段代码先排序去重再用 resample 补出缺失时间戳。为什么不用 fillna(0) 填流量高速夜间断面流量本身可能接近 0用 0 填充会把设备故障和真实低峰混在一起模型会误以为 0 是常态。线性插值是保守做法但如果一段断网超过两三个小时插值会插出一条平滑的假曲线我建议直接把这段标记出来不参与训练。提示Python 环境里装好 pandas、numpy、scikit-learn、tensorflow 这套常用库上面代码就能直接跑。关键不是库的版本号而是插值之后再看一次 isna()确保没有残留的 NaN。如果数据是按车道给的流量我建议先合并成断面总流量再训练而不是把每个车道当作独立序列。车辆换道会让单车道流量看起来像噪声LCTFP 很难从里面学到稳定规律。2.3 按时间切分数据集再写滑动窗口函数很多人习惯用 train_test_split 做随机切分但时间序列绝对不行。车流有明显的早晚高峰、工作日和周末差异随机切分等于把未来信息混进训练集验证集指标看着漂亮上线以后立刻露馅。我习惯按时间比例切前 70% 训练中间 15% 验证最后 15% 测试验证集紧跟在训练集后面。from sklearn.preprocessing import MinMaxScaler def split_by_time(df, train_ratio0.7, val_ratio0.15): n len(df) train_end int(n * train_ratio) val_end int(n * (train_ratio val_ratio)) return df.iloc[:train_end].copy(), df.iloc[train_end:val_end].copy(), df.iloc[val_end:].copy() feature_cols [flow, speed, occupancy] target_col flow train_df, val_df, test_df split_by_time(df) # 只对训练集拟合 scaler防止数据泄漏 feat_scaler MinMaxScaler() targ_scaler MinMaxScaler() train_feat feat_scaler.fit_transform(train_df[feature_cols]) train_targ targ_scaler.fit_transform(train_df[[target_col]]) val_feat feat_scaler.transform(val_df[feature_cols]) val_targ targ_scaler.transform(val_df[[target_col]]) test_feat feat_scaler.transform(test_df[feature_cols]) test_targ targ_scaler.transform(test_df[[target_col]])这里两个 scaler 分开 fit 是有意为之特征矩阵是三列一起缩放目标值单独用一个 scaler后续做反归一化才不会被特征维度干扰。测试集在训练阶段只能碰 transform不能碰 fit_transform这个原则后面避坑部分会再提。接着构造滑动窗口def make_windows(features, target, lookback12, horizon6): X, y [], [] for i in range(len(features) - lookback - horizon 1): X.append(features[i:i lookback]) y.append(target[i lookback:i lookback horizon]) return np.array(X), np.array(y) X_train, y_train make_windows(train_feat, train_targ, lookback12, horizon6) X_val, y_val make_windows(val_feat, val_targ, lookback12, horizon6) X_test, y_test make_windows(test_feat, test_targ, lookback12, horizon6) print(X_train.shape, y_train.shape)X_train 的形状是样本数, 12, 312 是 lookback3 是特征数。y_train 的形状是样本数, 6代表未来 6 个 5 分钟流量。这里最容易错的地方是窗口滑动的终点循环上限必须减掉 lookback horizon否则最后一个窗口会滑到序列末尾之外。这一步形状不对后面模型结构再复杂也跑不出可解释的结果。3. 模型搭建用 Keras 写一个 Conv1D 提特征、LSTM 做记忆的 LCTFP模型结构本身不神秘难的是理解“为什么 CNN 在前、LSTM 在后”。我见过不少同事上来就堆两层 LSTM结果训练很慢还过拟合也见过有人用 Conv2D 处理一维序列把维度搞得很别扭。LCTFP 这个方向更适合用一维卷积。3.1 为什么 Conv1D 在前、LSTM 在后流量序列里最值得关注的模式是局部突变。连续三个 5 分钟流量突然下降可能是事故或者匝道管制连续 20 分钟缓慢上升可能是进入高峰的前兆。这种相邻时间步之间的相关关系用 Conv1D 提取最直接卷积核只在时间轴上滑动参数共享让模型不用为每一时刻单独学一套权重。LSTM 的职责是接住更长的依赖。同样是早上 7 点昨天的 7 点和今天的 7 点相关性很强但中间隔了 144 个 5 分钟时间步Conv1D 的局部感受野覆盖不到这么远。LSTM 用门控状态把这种跨时段的记忆保留下来和 CNN 提取的局部特征互补。为什么不把两层并联并联结构适合一个分支处理原始序列、另一个分支处理外部特征的场景。如果只有流量、速度、占有率三列数据串联结构更简单也更容易排查问题。先让 CNN 把序列缩短并抽象再让 LSTM 处理压缩后的特征训练稳定性更好。3.2 张量形状一路变化定义模型之前先算一下每一层输出是什么形状。以 lookback12、特征数3 为例层输出形状作用Input(None, 12, 3)原始窗口Conv1D, filters64, kernel3(None, 12, 64)提取局部模式MaxPooling1D, pool_size2(None, 6, 64)时间维减半Conv1D, filters128, kernel3(None, 6, 128)更高层抽象MaxPooling1D, pool_size2(None, 3, 128)保留关键时间点LSTM, units64(None, 64)记忆跨时段依赖Dense, units6(None, 6)输出未来 6 步None 是批次维度实际训练时等于 batch_size。注意两个 MaxPooling 之后时间步从 12 变成了 3这个 3 就是 LSTM 的输入时间步长。只要时间步大于 1LSTM 就能工作如果池化层再多加一层时间步变成 1LSTM 就退化成全连接了这是很多人忽略的细节。3.3 完整 build 函数参数、激活、Dropout 的一次性选择下面是我常用的一个最小可跑版本函数名直接叫 build_lctfp。import tensorflow as tf from tensorflow.keras.layers import Input, Conv1D, MaxPooling1D, LSTM, Dropout, Dense from tensorflow.keras.models import Model def build_lctfp(lookback12, n_features3, horizon6): inputs Input(shape(lookback, n_features)) # 第一层卷积提取相邻 3 个 5 分钟内的局部趋势 x Conv1D(filters64, kernel_size3, paddingsame, activationrelu)(inputs) x MaxPooling1D(pool_size2)(x) # 第二层卷积在 6 个时间步上做更高层抽象 x Conv1D(filters128, kernel_size3, paddingsame, activationrelu)(x) x MaxPooling1D(pool_size2)(x) # LSTM 接收 (batch, 3, 128)记住跨时段依赖 x LSTM(units64, return_sequencesFalse)(x) x Dropout(0.2)(x) # 输出前加一层全连接做非线性组合 x Dense(units32, activationrelu)(x) outputs Dense(unitshorizon)(x) model Model(inputs, outputs, nameLCTFP) return model model build_lctfp() model.compile( optimizertf.keras.optimizers.Adam(learning_rate0.001), lossmse, metrics[tf.keras.metrics.RootMeanSquaredError()] ) model.summary()几个参数我说一下自己的默认选择。Conv1D 的 kernel_size 在时间序列里一般取 3 或 5取 3 意味着每次看相邻三个 5 分钟取 5 就能覆盖 25 分钟但要小心后面池化后时间步不够。filters 先给 64 和 128是希望模型在小数据上不至于立刻过拟合如果数据量很大再翻倍也不迟。Dropout 放在 LSTM 之后而不是之前是因为 LSTM 的输出维度较高这里做随机丢弃对抑制过拟合最有效。损失函数用 mse 而不是 mae是因为 mse 对大偏差更敏感流量预测里最怕的就是高峰时刻突然差一大截。如果后续发现结果太平滑可以切换到 mae 再对比一轮。这段代码在 CPU 上也能跑epoch 不是越大越好后面配合早停看验证集再说。4. 训练与评估预测曲线先回答“早晚高峰准不准”再看总误差很多深度学习教程只到模型训练就停了但对交通流量预测来说评估才是真正决定能不能上线的部分。训练集 loss 降到很低没有意义我要的是测试集上早晚高峰时段误差可控。4.1 按时间顺序给模型喂数据回调与超参训练 LCTFP 时我会用三个回调ModelCheckpoint 保存最优权重EarlyStopping 防止训练过度ReduceLROnPlateau 在验证 loss 停滞时自动降低学习率。from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping, ReduceLROnPlateau checkpoint ModelCheckpoint(lctfp_best.h5, monitorval_loss, save_best_onlyTrue) early_stop EarlyStopping(monitorval_loss, patience15, restore_best_weightsTrue) lr_scheduler ReduceLROnPlateau(monitorval_loss, factor0.5, patience5, min_lr1e-5) history model.fit( X_train, y_train, validation_data(X_val, y_val), epochs100, batch_size128, callbacks[checkpoint, early_stop, lr_scheduler], shuffleTrue, verbose1 )batch_size 我一般取 128。LSTM 对批量大小比纯 CNN 敏感批量太大梯度更新方向太平滑会把早晚高峰的突变抹掉批量太小训练又慢又容易震荡。128 在多数路测数据上是个稳妥起点。shuffleTrue 只在每个 epoch 内打乱样本顺序没有打乱单个样本内部的时间顺序所以可以开。它能让训练更稳避免模型总按同一个时间顺序被喂数据。如果数据量很小也可以用 shuffleFalse但容易过拟合我不推荐。4.2 反归一化以后算指标不要直接拿 0-1 的数值汇报模型的输出是归一化后的流量不能直接拿来汇报。要先用 target_scaler 做 inverse_transform把预测值和真实值还原成车辆数再算 RMSE 和 MAPE。y_pred model.predict(X_test, verbose0) # 反归一化回真实车辆数 y_pred_inv targ_scaler.inverse_transform(y_pred) y_test_inv targ_scaler.inverse_transform(y_test) rmse np.sqrt(np.mean((y_pred_inv - y_test_inv) ** 2)) mae np.mean(np.abs(y_pred_inv - y_test_inv)) # 防止流量为 0 时除零给分母加一个保护 mape np.mean(np.abs((y_test_inv - y_pred_inv) / np.maximum(y_test_inv, 1))) * 100 print(RMSE:, rmse) print(MAE:, mae) print(MAPE:, mape)这里要注意targ_scaler 是在训练集的目标值上 fit 的不能拿测试集重新 fit。否则还原出来的预测值只是“看起来合理”其实包含了未来 min/max 信息评估结果会偏乐观。np.maximum(y_test_inv, 1)是给除零兜底更严格的做法是只在 flow 大于某个阈值的时段算 MAPE低流量时段不参与。4.3 按小时和峰值分桶看误差整体 RMSE 只能告诉你大方向对不对不能回答“早高峰能不能用”。我的习惯是把测试集按时间拆成几个桶早高峰 7:00-9:00、晚高峰 17:00-19:00、平峰、夜间分别计算 MAE。时段关注点建议指标7:00-9:00通勤流量突变MAE、峰值前后 30 分钟误差17:00-19:00返程拥堵MAE分车道对比平峰模型稳定性RMSE夜间低流量偏差直接看绝对误差别看 MAPE分桶评估做起来不复杂把测试集的 time 列和 y_test_inv 拼成一个 DataFrame然后用 pandas 的 between_time 筛出对应时段再算误差。关键是注意预测的 6 个时间点如何归属时段我一般取预测窗口的第一个时间点作为标签比如预测 8:00-8:30就归到早高峰桶。如果混着归误差表会变得很难读。5. 避坑清单LCTFP 实测里最容易翻车的 5 个原点这一章是我最想让你先看的部分。模型跑不出来通常不是网络结构的问题而是下面这些看似不起眼的位置出了问题。5.1 时间错位预测曲线比真实晚一个时段现象把预测值和真实值画在同一张图上预测曲线明显比真实曲线向右平移了一格。原因滑动窗口构造时把输出索引写错。常见写法是 y.append(target[i lookback - 1])这里只取了窗口结束后第一个点但那时刚好是“当前时刻”而模型要预测的是未来。正确做法是 y.append(target[i lookback: i lookback horizon])。解决打印前三条样本的索引确认 y 的第一个值确实晚于 X 的最后一个时间步。我调试时会给每个窗口加上时间戳列对比“窗口最后时间”和“预测目标开始时间”是否相差一个时段。差一个点不是小事它会让所有评估指标都看着不错但上线后每次都慢一拍。5.2 归一化泄漏训练集用了未来的 min/max现象训练集和验证集 loss 都很好到了测试现场却差一大截甚至模型输出的曲线被压在一个很窄的范围里。原因数据还没切分就直接调用 MinMaxScaler 对整个 DataFrame 做了 fit_transform。scaler 在计算 min 和 max 时已经看到了未来数据相当于把测试集的数值范围提前告诉了模型。解决严格按时间切分先切训练、验证、测试再在训练集上 fit scaler验证集和测试集只允许 transform。如果特征和目标分开两个 scaler也要保证两个 scaler 都只见过训练集。这个错误在实验阶段不会报错所以特别隐蔽。5.3 时间步被 pool 没了看到 Negative dimension size 时别急着调层现象模型 summary 之前还能跑加上池化层后直接报类似 Negative dimension size caused by subtracting 2 from 1 的错。原因lookback 太小或者 MaxPooling1D 层数太多把时间维压缩成了 0 或负数。比如 lookback4池化一次变 2池化两次就出问题。解决先算好每一层 shape再搭模型。我习惯把 lookback 设成 12 或 24保证经过两层池化后时间步至少还有 3。如果确实想用很小的 lookback就用 paddingsame并且少放池化层。打印中间层 shape 的成本很低比训练报错了再猜快多了。5.4 缺失值救过头把流量缺失补成 0现象验证集整体 loss 不高但周末早上的预测明显偏低尤其节假日前后误差很大。原因数据清洗阶段用了 fillna(0) 或者直接把缺失行删掉后没做插值。周末凌晨流量低缺失如果被 0 填进去模型学到“低流量时段输出 0”白天也会把偏小值当真。解决流量缺失用相邻时间的线性插值或者按同一个星期几、同一个时间段的中位数填充。更稳妥的做法是额外生成一列 is_missing 标记把缺失时刻告诉模型这样模型能区分“设备没回数据”和“路上确实没车”。如果缺失超过两小时建议把这天标记成异常样本不进训练集。5.5 夜间低流量让 MAPE 失真现象MAPE 一算 60%看起来模型没法用但把早高峰单独拿出来看误差又不大。原因夜间流量绝对值小比如真实值是 5 辆/5 分钟预测值是 8 辆绝对误差只有 3MAPE 却到了 60%。低流量时段对 MAPE 的贡献被放大会掩盖真正需要关注的早晚高峰问题。解决评估时分桶看或者只在流量超过 30 辆/5 分钟的时间段计算 MAPE。对外汇报时先给 MAE再给高峰时段的 MAPE不要拿一个整体数字下结论。模型是给运营用的运营关心的永远是高峰时段准不准而不是凌晨有多么精确。6. 上线前的一个习惯用滚动预测代替整段预测少被数据漂移坑模型在测试集上表现不错之后我建议立刻写一个滚动预测函数。测试集的评价是“一次性预测未来 6 步”但线上真实场景是每 5 分钟来一条新数据模型要反复读取最新观测值再重新预测未来 30 分钟。def rolling_predict(model, latest_df, lookback12, horizon6): # 只取最近 lookback 个时间点的真实观测值 recent latest_df[feature_cols].tail(lookback).copy() recent_scaled feat_scaler.transform(recent) X recent_scaled.values.reshape(1, lookback, len(feature_cols)) y_pred_norm model.predict(X, verbose0) return targ_scaler.inverse_transform(y_pred_norm).ravel()滚动预测的关键是不能把模型上一步的预测值当成输入继续传下去否则误差会累积。每次新数据到达把窗口往前挪一格丢掉最旧的一条再让模型重新算。这样预测结果永远基于真实观测误差不会滚动放大。我在部署这类模型时还会保留最近 2 周真实流量做盲测每两天用最新数据微调一次观察 MAE 是否漂移。另一个值得做的习惯是把时间特征加进 LCTFP 的输入。最简单的是加一列 is_weekend再复杂一点可以加小时的正弦编码。虽然标题里 CNN 和 LSTM 是主角但高速公路流量的周模式很强没有时间特征时模型只能硬记。这个技巧看起来不起眼却往往比调整 LSTM units 更有效。我现在的做法是所有新模型先跑一版滚动预测再和旧模型在早高峰和节假日两个场景上并行对比两周确认误差没有再切换。这条路我走过不少弯路最深的感受是模型结构只占三成数据切分和滚动评估占七成。希望帮到你。本文还有配套的精品资源点击获取