
简介这份PDF文献面向智能交通、深度学习与数据建模方向的研究生、算法工程师及科研人员聚焦短时交通流预测中时空特征难以充分利用的问题提出并验证了CNN与BiLSTM组合的C-BiLSTM深度学习模型。资源包内仅含1个PDF文件大小约1.3MB即论文全文涵盖摘要、引言、模型构建、实验验证与结论等完整章节便于系统研读与引用。文中以美国交通研究数据实验室实测数据为验证基础详细阐述一维CNN提取观测点交通流空间特征、双向LSTM捕获时间周期特征、全连接层输出预测结果的建模流程并给出与双向LSTM、单向LSTM的对比结果预测准确性分别提升1.6%和6.6%。读者可从中获取完整的模型设计思路、时空特征融合方法、实验设置与性能分析适合作为短时交通流预测、智能交通系统研究的参考材料。目前已有365人学习下载。1. 短时交通流预测CNN 与 BiLSTM 组合模型到底在解决什么问题城市快速路上每 2 分钟刷新一次的流量数据看起来只是一个个数字但背后是路口信号配时、匝道控制、出行诱导屏能不能及时响应的关键依据。短时交通流预测要做的就是根据过去若干个时间片的流量推断未来 15 分钟、30 分钟甚至 1 小时的路况走势。难点在于流量序列既有周期性早晚高峰又有随机突变事故、天气、临时管制单一模型很难同时抓住这两类特征。卷积神经网络擅长从局部窗口里提取空间或短时模式双向长短时记忆网络则能从前后两个方向捕捉时序依赖把两者串起来就是这套组合模型的核心思路。它适合做交通工程、智能交通系统、城市计算方向的工程师和研究生也适合已经跑通过 LSTM 但发现预测滞后、峰值抓不准的从业者。下面从数据准备、模型搭建、训练调参到避坑把这条路线完整走一遍。2. 数据准备与特征工程把原始流量表变成模型能吃的张量2.1 短时交通流数据的常见来源与清洗规则常见的数据来源有三类地磁线圈、微波雷达、视频卡口。不管哪种拿到的原始表通常长这样一列时间戳一列车道编号一列流量值可能还有占有率和速度。真正能直接喂给模型的很少因为存在缺失、重复和异常。清洗规则我一般按下面顺序做时间对齐把所有记录统一到固定时间片短时预测常用 2 分钟、5 分钟或 15 分钟。如果原始是 1 分钟就按窗口求和或求平均。缺失标记连续缺失少于 3 个时间片用线性插值超过 3 个用同星期同时间段的均值填充不要用全局均值否则会把高峰削平。异常剔除流量为负、流量超过车道理论通行能力 1.5 倍、连续多个时间片完全不变这三类先标记再处理。归一化按训练集的最大最小值做 Min-Max验证集和测试集用同一个 scaler不能各自归一化。import pandas as pd import numpy as np from sklearn.preprocessing import MinMaxScaler # 读取原始流量表假设列名为 timestamp, lane_id, flow df pd.read_csv(raw_flow.csv, parse_dates[timestamp]) df df.sort_values([lane_id, timestamp]) # 按 5 分钟重采样同一车道同一时间片求和 df df.set_index(timestamp).groupby(lane_id).resample(5T)[flow].sum().reset_index() # 缺失值处理先线性插值再对长缺口用同星期同时段均值补 df[flow] df.groupby(lane_id)[flow].transform(lambda s: s.interpolate(methodlinear, limit3)) df[weekday] df[timestamp].dt.weekday df[slot] df[timestamp].dt.hour * 12 df[timestamp].dt.minute // 5 long_gap_mean df.groupby([lane_id, weekday, slot])[flow].transform(mean) df[flow] df[flow].fillna(long_gap_mean) # 异常值处理超过该车道 99 分位数的值截断 cap df.groupby(lane_id)[flow].quantile(0.99) df[flow] df.apply(lambda r: min(r[flow], cap[r[lane_id]]), axis1) # 归一化注意 scaler 只在训练集上 fit scaler MinMaxScaler() train_mask df[timestamp] 2024-01-01 df.loc[train_mask, flow_scaled] scaler.fit_transform(df.loc[train_mask, [flow]]) df.loc[~train_mask, flow_scaled] scaler.transform(df.loc[~train_mask, [flow]])这段代码的关键参数是limit3和quantile(0.99)。limit3表示连续缺失超过 3 个时间片就不再线性插值避免把长缺口拉成一条直线。quantile(0.99)是截断阈值如果你数据里事故导致的异常特别多可以降到 0.98如果数据本身很干净可以升到 0.995。归一化那一步最容易翻车很多人把全量数据一起 fit结果测试集的信息泄漏到训练里验证损失看起来很低上线就崩。2.2 滑动窗口构造与 CNNBiLSTM 的输入形状CNN 和 BiLSTM 组合时输入通常是一个三维张量样本数 × 时间步长 × 特征数。短时交通流预测里特征数可以是 1只用量流量也可以是 3流量、占有率、速度。时间步长决定模型能看多长的历史常见取 121 小时5 分钟粒度或 242 小时。构造滑动窗口的逻辑是用前 T 个时间片预测第 T1 个时间片。如果你要做多步预测比如预测未来 3 个时间片就把标签改成 T1 到 T3。def make_windows(series, time_steps12, horizon1): X, y [], [] for i in range(len(series) - time_steps - horizon 1): X.append(series[i:itime_steps]) y.append(series[itime_steps:itime_stepshorizon]) return np.array(X), np.array(y) # 按车道分别构造再拼接 X_all, y_all [], [] for lane, group in df.groupby(lane_id): vals group[flow_scaled].values X_lane, y_lane make_windows(vals, time_steps12, horizon1) X_all.append(X_lane) y_all.append(y_lane) X np.concatenate(X_all, axis0) y np.concatenate(y_all, axis0) X X[..., np.newaxis] # 形状变为 (样本数, 12, 1) print(X.shape, y.shape)time_steps12是短时预测里比较稳的起点。如果你数据粒度是 15 分钟12 步就是 3 小时可能太长可以降到 8。horizon1表示单步预测改成 3 就是多步。注意X[..., np.newaxis]这一步CNN 的 Conv1D 层要求输入至少是三维不加这个维度会直接报错。另外按车道分别构造再拼接比把所有车道混在一起滑窗更合理因为不同车道的流量基线差异很大混在一起会让模型学到错误的模式。3. CNNBiLSTM 组合模型搭建从 Conv1D 到双向 LSTM 的层间逻辑3.1 为什么先 CNN 后 BiLSTM而不是反过来组合顺序不是随便定的。CNN 放在前面作用是在时间维度上做局部卷积提取短时突变特征比如流量在几个时间片内快速上升。Conv1D 的卷积核沿着时间轴滑动每个核只关注一个小窗口这正好对应交通流里的局部波动。BiLSTM 放在后面接收 CNN 输出的特征序列从前向和后向两个方向建模长距离依赖比如早高峰的上升趋势和晚高峰的下降趋势。如果反过来先 BiLSTM 再 CNNBiLSTM 输出的序列已经混合了双向信息再做卷积会破坏时序因果性预测未来时容易用到未来信息离线指标好看上线就出问题。所以常见做法是 CNN 做特征提取BiLSTM 做时序建模最后接全连接层输出预测值。3.2 用 Keras 搭一个可复现的 CNN-BiLSTM 模型下面是一个可以直接跑的 Keras 实现。输入形状是(time_steps, features)输出是单步流量值。import tensorflow as tf from tensorflow.keras import layers, models, callbacks def build_cnn_bilstm(time_steps12, features1): inputs layers.Input(shape(time_steps, features)) # CNN 部分两层 Conv1D提取局部时序特征 x layers.Conv1D(filters64, kernel_size3, paddingsame, activationrelu)(inputs) x layers.BatchNormalization()(x) x layers.MaxPooling1D(pool_size2)(x) x layers.Conv1D(filters32, kernel_size3, paddingsame, activationrelu)(x) x layers.BatchNormalization()(x) # BiLSTM 部分双向 LSTM捕捉前后依赖 x layers.Bidirectional(layers.LSTM(64, return_sequencesFalse))(x) x layers.Dropout(0.3)(x) # 全连接输出 x layers.Dense(32, activationrelu)(x) outputs layers.Dense(1)(x) model models.Model(inputs, outputs) model.compile(optimizertf.keras.optimizers.Adam(learning_rate1e-3), lossmse, metrics[mae]) return model model build_cnn_bilstm(time_steps12, features1) model.summary()逐层说明第一层Conv1D(64, 3)表示 64 个卷积核每个核覆盖 3 个时间步paddingsame保证输出长度不变。BatchNormalization放在卷积后、激活前还是激活后Keras 里常见是卷积后直接 BN 再激活但上面写法也能跑差别不大。MaxPooling1D(2)把时间步减半减少后面 LSTM 的计算量。第二层Conv1D(32, 3)进一步压缩通道。Bidirectional(LSTM(64))会输出 128 维向量前向 64 后向 64return_sequencesFalse表示只取最后一个时间步的输出。Dropout(0.3)是防过拟合的如果你数据量超过 10 万条可以降到 0.2如果只有几千条可以升到 0.4。训练时用早停和学习率衰减early_stop callbacks.EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue) reduce_lr callbacks.ReduceLROnPlateau(monitorval_loss, factor0.5, patience5, min_lr1e-6) history model.fit(X_train, y_train, validation_data(X_val, y_val), epochs100, batch_size64, callbacks[early_stop, reduce_lr])patience10表示验证损失 10 个 epoch 不下降就停restore_best_weightsTrue会恢复到最优权重这个参数很关键不然拿到的可能是过拟合后的模型。batch_size64是起点显存够可以加到 128显存不够降到 32。学习率初始 1e-3如果训练损失震荡降到 5e-4。3.3 训练完怎么验证模型真的学到了东西不要只看 loss 曲线。我一般做三件事第一画预测值和真实值的对比图重点看高峰时段。如果模型在高峰时预测值明显偏低说明它没抓住峰值特征可能是 CNN 的卷积核太小或者 BiLSTM 的隐藏单元不够。第二算三个指标MAE、RMSE、MAPE。短时交通流里 MAPE 容易受低流量时段影响因为分母小所以低流量时段多的数据集 MAPE 会虚高这时候看 MAE 更实在。第三做残差分析。把残差按时间片画出来如果残差在某个时间段系统性偏正或偏负说明模型有结构性偏差可能需要加入星期几、是否节假日这类外部特征。from sklearn.metrics import mean_absolute_error, mean_squared_error pred model.predict(X_test) mae mean_absolute_error(y_test, pred) rmse np.sqrt(mean_squared_error(y_test, pred)) print(fMAE: {mae:.4f}, RMSE: {rmse:.4f}) # 反归一化后看真实误差 pred_inv scaler.inverse_transform(pred) y_test_inv scaler.inverse_transform(y_test) mae_inv mean_absolute_error(y_test_inv, pred_inv) print(f反归一化后 MAE: {mae_inv:.2f} 辆/5分钟)反归一化后的 MAE 才是能跟业务方沟通的数字。比如 MAE 是 12 辆/5 分钟意味着平均每个时间片预测偏差 12 辆车如果车道流量基线是 300 辆/5 分钟这个误差大约 4%算可用。如果超过 10%就要回头查数据质量或模型结构。4. 避坑与排查CNN-BiLSTM 短时交通流预测里最容易翻车的 5 个点4.1 现象验证损失比训练损失低很多但测试集一塌糊涂原因数据泄漏。最常见的是归一化时用了全量数据 fit或者滑动窗口构造时训练集和测试集有重叠。另一个可能是验证集和测试集的时间段分布差异太大比如验证集是平峰、测试集是高峰。解决归一化只在训练集 fit验证和测试用同一个 scaler。滑动窗口按时间顺序切分不要随机打乱。如果时间分布差异大用时间序列交叉验证或者按星期几分层采样。4.2 现象模型预测总是滞后一个时间片原因BiLSTM 的双向结构在训练时看到了未来信息但推理时只能看到历史。如果训练和推理的输入构造方式不一致就会出现滞后。另一个原因是损失函数对峰值不敏感MSE 会让模型偏向预测均值。解决检查推理时的输入窗口是否和训练时一致。如果滞后严重把损失函数换成 Huber 或 MAE或者在损失里给高峰时段加权。我一般会加一个峰值权重高峰时间片的损失乘以 1.5平峰乘以 1.0。4.3 现象训练到一半 loss 突然变成 NaN原因学习率太大或者数据里有极端异常值没处理干净。BiLSTM 对梯度爆炸比较敏感尤其是序列较长时。解决加梯度裁剪tf.clip_by_global_norm或者把学习率降到 1e-4。检查数据里有没有超过 1e6 的值有的话先截断。Keras 里可以在 optimizer 里设clipnorm1.0。4.4 现象换一条车道预测效果就崩了原因不同车道的流量基线差异大模型在一条车道上过拟合了。如果所有车道混在一起训练模型可能学到了车道编号的隐式特征换车道就失效。解决按车道分别建模或者把车道编号做 embedding 输入。更稳的做法是每条车道单独训练一个模型虽然模型多但每个模型的数据分布一致效果更可控。如果车道数太多可以按流量等级分组每组一个模型。4.5 现象CNN 层数加多了效果反而下降原因Conv1D 层数太多会过度平滑时间序列把短时突变特征抹掉。短时交通流预测里局部突变恰恰是最重要的信息。解决CNN 部分控制在 2 层以内卷积核大小 3 或 5 就够了。如果效果不够优先加 BiLSTM 的隐藏单元而不是加 CNN 层数。另外MaxPooling 也会丢失时间分辨率如果时间步本来就只有 12池化后只剩 6BiLSTM 能用的信息就太少了这时候可以去掉池化层。5. 进阶技巧用注意力机制和外部特征把预测误差再压一压5.1 在 BiLSTM 后面加一个注意力层BiLSTM 输出的是每个时间步的隐藏状态但预测下一个时间片时不同历史时间步的重要性不一样。比如预测晚高峰 18:00 的流量17:30 的数据比 14:00 的数据更重要。加一个注意力层让模型自己学权重。from tensorflow.keras import backend as K def attention_layer(inputs): # inputs 形状: (batch, time_steps, hidden) score layers.Dense(1, activationtanh)(inputs) # (batch, time_steps, 1) weights layers.Softmax(axis1)(score) # 时间步维度归一化 context layers.Multiply()([inputs, weights]) # 加权 context layers.Lambda(lambda x: K.sum(x, axis1))(context) return context # 在 BiLSTM 后接注意力 x layers.Bidirectional(layers.LSTM(64, return_sequencesTrue))(x) x attention_layer(x) x layers.Dropout(0.3)(x)注意return_sequencesTrue这样 BiLSTM 输出每个时间步的隐藏状态而不是只输出最后一个。注意力层的Dense(1)给每个时间步打一个分数Softmax 在时间步维度归一化然后加权求和。这个改动通常能把 MAE 降 3% 到 8%具体看数据。5.2 加入时间特征和天气特征纯流量序列能用的信息有限。把星期几、是否节假日、时段编码成 one-hot 或 embedding和流量序列拼在一起输入模型能学到更细的模式。天气数据如果有也可以加雨天和晴天的流量分布不一样。# 假设 df 里已有 weekday, is_holiday, hour 列 time_features pd.get_dummies(df[[weekday, is_holiday]], columns[weekday]) # 和流量序列拼接注意时间步要对齐 # 这里只示意实际要按滑动窗口同步构造外部特征不是越多越好。我试过把温度、湿度、风速全加进去结果验证损失反而上升因为噪声特征太多。建议先加星期几和节假日这两个对交通流影响最直接其他特征用消融实验逐个验证。5.3 一个我踩过的坑注意力层不是万能药有一次我在一个只有 8000 条样本的数据集上加注意力层训练损失降得很快但验证损失比不加还高。后来发现是注意力层参数太多小数据集上过拟合了。所以加注意力之前先确认你的数据量够不够。一般来说样本数少于 2 万条优先加 Dropout 和早停而不是加注意力。数据量超过 5 万条注意力层的收益才比较稳定。另外注意力层的 Softmax 在时间步很短时比如 time_steps6区分度不高权重会接近均匀这时候加注意力意义不大。时间步至少 12 以上注意力才有发挥空间。我现在的习惯是先用 CNNBiLSTM 跑一个基线记录 MAE然后加注意力看验证集 MAE 有没有下降再逐个加外部特征每次只加一个确认有收益再保留。不要一次性全加上不然出了问题都不知道是哪个模块导致的。希望帮到你。本文还有配套的精品资源点击获取