
简介这份PDF面向智能交通、深度学习与数据建模方向的学习者与研究人员聚焦短时交通流预测中时空特征难以充分利用的问题给出一种CNN与BiLSTM组合的C-BiLSTM预测模型方案。资源包共1个文件为1.3MB的PDF文档内容涵盖模型结构设计、一维CNN空间特征提取、双向LSTM时间周期特征捕获及全连接层输出预测的完整思路并附有美国交通研究数据实验室实测数据的验证结果与对比分析。文中还系统梳理了卷积神经网络、双向长短时记忆网络、机器学习、数据建模等知识点说明C-BiLSTM相比双向LSTM与单向LSTM分别提升1.6%和6.6%预测准确性。目前已有366人学习适合希望理解时空组合建模、复现交通流预测实验或撰写相关论文的读者参考。1. 短时交通流预测CNN 与 BiLSTM 组合模型到底在解决什么问题城市快速路上每 2 分钟上传一次的车流数据看起来只是一个个数字但真正做过短时交通流预测的人都知道这里面同时藏着两种完全不同的规律。一种是局部突变前方路口排队回溢、匝道汇入、事故清障都会让流量在几个时间片内出现尖峰或断崖这种模式靠相邻几个采样点就能捕捉。另一种是长程依赖早高峰的爬坡、晚高峰的回落、周五下午的提前放量往往要回看几十分钟甚至更久才能判断走向。单靠卷积神经网络CNN擅长抓局部突变却记不住长程节奏单靠双向长短时记忆网络BiLSTM能双向建模时序依赖却对局部尖峰不够敏感。把两者串起来用 CNN 先做局部特征提取、再用 BiLSTM 做前后向时序建模就是这套组合模型的核心思路。它适合手上有路段级或线圈级时序数据、想在不引入复杂图结构的前提下把预测误差再压一档的从业者也是短时交通流预测里性价比很高的一条落地路线。2. 组合模型的结构选型为什么是 CNN 接 BiLSTM 而不是反过来2.1 两种网络的互补关系与串联顺序短时交通流序列本质是一维时间序列所以这里说的 CNN 通常是一维卷积神经网络卷积核沿时间轴滑动提取的是「相邻若干时间片内的局部变化模式」。一个长度为 3 的卷积核扫过去能识别出「连续三个时间片持续上升」这类局部形态多个卷积核并行就能同时捕捉上升、下降、平台、尖峰等不同局部模式。这一步的输出仍然是时间序列只是每个位置的特征被重新编码了。BiLSTM 的价值在于它有两个方向的 LSTM前向 LSTM 从序列开头读到结尾后向 LSTM 从结尾读回开头两者输出拼接后每个时间步都同时拥有「过去的信息」和「未来的信息」。对预测任务来说用未来信息看起来像作弊但在训练阶段输入窗口内的未来观测是已知的用双向结构去编码这个窗口内的上下文能让模型对窗口内模式的表征更充分预测的是窗口之后的值并不违反因果。串联顺序上常见做法是 CNN 在前、BiLSTM 在后。原因是 CNN 先把原始序列里噪声和局部形态处理掉输出一组更干净、维度更规整的特征序列BiLSTM 再在这组特征上做时序建模负担更轻。反过来先 BiLSTM 再 CNN等于让 LSTM 直接吃原始噪声再让卷积去扫已经被循环结构打散的特征实践中收敛更慢、效果也不稳定。我一般会坚持 CNN 前置。2.2 输入窗口、预测步长与数据切分在动手写模型之前先把三个数定下来输入窗口长度、预测步长、采样间隔。假设原始数据是 2 分钟一个采样点你想用过去 30 分钟预测未来 10 分钟那么输入窗口就是 15 个点预测步长是 5 个点。这三个数直接决定后面所有张量的形状必须先定。数据切分上时序数据绝对不能随机打乱。常见做法是按时间顺序切前 70% 训练、中间 15% 验证、最后 15% 测试。如果随机切分测试集里的点会「穿越」到训练集附近评估结果会虚高这是时序预测里最经典的翻车点之一。下面这段代码演示如何把一维流量序列切成监督学习样本并做标准化。标准化参数只能用训练集统计量验证和测试集复用否则同样是数据泄漏。import numpy as np def make_windows(series, in_len, out_len): 把一维序列切成 (输入窗口, 预测目标) 样本对 X, y [], [] total len(series) - in_len - out_len 1 for i in range(total): X.append(series[i : i in_len]) y.append(series[i in_len : i in_len out_len]) return np.array(X), np.array(y) # series 为原始流量序列形状 (T,) in_len, out_len 15, 5 # 过去 15 个点预测未来 5 个点 X, y make_windows(series, in_len, out_len) # 按时间顺序切分禁止 shuffle n len(X) n_train, n_val int(n * 0.7), int(n * 0.15) X_train, y_train X[:n_train], y[:n_train] X_val, y_val X[n_train:n_train n_val], y[n_train:n_train n_val] X_test, y_test X[n_train n_val:], y[n_train n_val:] # 标准化只用训练集统计量 mu, sigma X_train.mean(), X_train.std() X_train (X_train - mu) / sigma X_val (X_val - mu) / sigma X_test (X_test - mu) / sigma这段代码里in_len和out_len就是前面说的输入窗口和预测步长改这两个数就能切换预测任务。make_windows用滑动窗口生成样本total保证不会越界。标准化那三行是关键mu和sigma只从X_train算验证和测试集直接套用这样评估才可信。如果偷懒对全量数据算均值和方差测试集信息就漏进训练了。2.3 组合模型的 PyTorch 实现结构定下来之后模型本身并不复杂。一维卷积层负责局部特征池化层压缩长度BiLSTM 做双向时序编码最后取最后一个时间步的输出接全连接层映射到预测步长。下面是一个可以直接跑的最小实现。import torch import torch.nn as nn class CNNBiLSTM(nn.Module): def __init__(self, in_len, out_len, conv_channels32, kernel_size3, hidden64): super().__init__() # 一维卷积输入通道 1输出 conv_channels self.conv nn.Conv1d(1, conv_channels, kernel_size, paddingkernel_size // 2) self.relu nn.ReLU() self.pool nn.MaxPool1d(2) # 长度减半 # BiLSTM输入维度 conv_channels双向 self.lstm nn.LSTM(conv_channels, hidden, batch_firstTrue, bidirectionalTrue) self.fc nn.Linear(hidden * 2, out_len) # 双向拼接后维度翻倍 def forward(self, x): # x: (B, in_len) - (B, 1, in_len) x x.unsqueeze(1) x self.relu(self.conv(x)) # (B, C, in_len) x self.pool(x) # (B, C, in_len/2) x x.permute(0, 2, 1) # (B, L, C) 适配 LSTM out, _ self.lstm(x) # (B, L, 2*hidden) last out[:, -1, :] # 取最后时间步 return self.fc(last) # (B, out_len)conv_channels控制卷积提取多少种局部模式太小欠拟合、太大容易过拟合路段数据一般 16 到 64 之间试。kernel_size是卷积核长度取 3 表示看相邻三个时间片想捕捉更长的局部形态可以调到 5。hidden是 LSTM 隐藏单元数双向所以最后全连接输入是hidden * 2。paddingkernel_size // 2保证卷积后长度不变池化再减半这样 LSTM 的输入长度是in_len // 2改in_len时不用手动调。注意permute那一步卷积输出是(B, C, L)LSTM 要的是(B, L, C)顺序错了会直接报维度错误这是新手最常见的翻车点。3. 训练配置与调参让组合模型真正收敛的几个关键设置3.1 损失函数、优化器与学习率短时交通流预测是回归任务损失函数首选 MSE 或 Huber。MSE 对异常值敏感如果数据里有事故导致的极端尖峰Huber 更稳它对大误差线性增长而不是平方增长能避免模型被少数异常点带偏。我一般先用 MSE 跑通发现验证损失被个别尖峰拉高时再换 Huber。优化器用 Adam学习率从 1e-3 起步。组合模型里卷积层和 LSTM 层对学习率的敏感度不同如果训练几个 epoch 后损失不降先降到 1e-4 试。批量大小取 32 或 64太小梯度噪声大太大泛化变差。训练轮数不要拍脑袋定用早停验证损失连续若干轮不下降就停并保留验证损失最低那轮的权重。import torch from torch.utils.data import TensorDataset, DataLoader device torch.device(cuda if torch.cuda.is_available() else cpu) model CNNBiLSTM(in_len15, out_len5).to(device) optimizer torch.optim.Adam(model.parameters(), lr1e-3) criterion torch.nn.HuberLoss() train_ds TensorDataset(torch.tensor(X_train, dtypetorch.float32), torch.tensor(y_train, dtypetorch.float32)) train_loader DataLoader(train_ds, batch_size64, shuffleTrue) best_val, patience, wait float(inf), 10, 0 for epoch in range(200): model.train() for xb, yb in train_loader: xb, yb xb.to(device), yb.to(device) optimizer.zero_grad() loss criterion(model(xb), yb) loss.backward() optimizer.step() # 验证 model.eval() with torch.no_grad(): xv torch.tensor(X_val, dtypetorch.float32).to(device) yv torch.tensor(y_val, dtypetorch.float32).to(device) val_loss criterion(model(xv), yv).item() if val_loss best_val: best_val, wait val_loss, 0 torch.save(model.state_dict(), best.pt) else: wait 1 if wait patience: breakpatience10是早停耐心值验证损失连续 10 轮不降就停。shuffleTrue只打乱训练集内部样本顺序不影响前面按时间切分的边界这是允许的。保存best.pt而不是最后一轮权重是因为最后一轮往往已经过拟合。注意验证时切到model.eval()并关掉梯度否则 BatchNorm 和 Dropout 行为不一致验证损失会失真。3.2 评价指标别只看 MSEMSE 和 RMSE 反映绝对误差但不同路段流量量级差别很大跨路段比较时更该看 MAPE 或 MAE 占均值的比例。短时交通流预测里常用三个指标MAE、RMSE、MAPE。MAE 直观RMSE 对大误差更敏感MAPE 是百分比误差便于横向比较。但 MAPE 在流量接近 0 时会爆炸夜间低流量时段要小心必要时对分母加一个小常数或改用 SMAPE。评估时一定要把预测值反标准化回原始量纲再算指标否则算出来的是标准化空间的误差没有业务意义。反标准化用训练集的mu和sigmapred_real pred * sigma mu。这一步经常被漏掉导致指标看起来很小但实际预测完全不能用。3.3 训练不收敛时的排查顺序组合模型训练不动按这个顺序查先看数据确认标准化做了、没有 NaN、输入窗口和标签没有错位再看维度卷积输出到 LSTM 的permute是否正确全连接输入维度是否等于hidden * 2然后看学习率1e-3 不降就试 1e-4最后看梯度如果出现 NaN多半是学习率过大或数据里有极端值加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0)通常能救回来。这个顺序能覆盖九成以上的不收敛问题比盲目改结构高效得多。4. 避坑与常见问题组合模型落地时最容易踩的五个坑4.1 数据泄漏随机切分让指标虚高现象测试集 MAE 低得离谱上线后预测完全不准。原因用train_test_split随机切分了时序数据测试集的相邻时间点出现在训练集里模型等于见过答案。解决严格按时间顺序切分训练集在前、测试集在后中间留验证集任何标准化、归一化参数都只用训练集统计。4.2 维度错位卷积输出没 permute 就喂给 LSTM现象运行时报维度不匹配或者不报错但损失一直不降。原因nn.Conv1d输出形状是(B, C, L)nn.LSTM默认要(B, L, C)不转换直接喂LSTM 会把通道维当时间步语义完全错乱。解决卷积池化后加x x.permute(0, 2, 1)并在第一次跑通时打印每层输出形状确认。4.3 双向结构用错场景在线预测时拿不到未来现象离线评估很好实时预测时效果骤降。原因BiLSTM 的后向分支需要窗口内的未来观测离线时整个窗口都在手上实时滚动预测时窗口末尾之后的数据还没产生。解决明确预测模式如果是滚动在线预测输入窗口必须是当前时刻之前的完整历史窗口内的双向编码仍然成立但绝不能把预测目标之后的数据喂进窗口。评估时也要用同样的滚动方式不能用一次性整段预测的结果冒充在线效果。4.4 过拟合训练损失降、验证损失升现象训练集误差持续下降验证集误差先降后升。原因模型容量相对数据量偏大或者训练轮数过多。解决先加 Dropout 和权重衰减再把conv_channels和hidden调小最后靠早停保留验证最优权重。数据量实在少时可以考虑对训练序列做窗口重叠采样来扩增样本。4.5 异常值未处理个别尖峰带偏整个模型现象大部分时段预测正常事故或节假日时段误差极大且拉高整体指标。原因MSE 对极端值敏感少数尖峰会主导梯度。解决训练前对流量序列做异常检测对确认的异常点做标记或平滑损失函数换成 Huber评估时把异常时段单独统计不要混在整体指标里掩盖问题。5. 进阶技巧用残差连接和滚动验证把组合模型再压一档基础版跑通之后想再提升我一般从两个方向下手。第一个是结构上的残差连接。CNN 提取的局部特征经过池化会损失一部分细节如果直接把池化后的特征喂给 BiLSTM细粒度信息就丢了。可以在卷积输出和池化输出之间加一条跳跃连接或者让 BiLSTM 的输出和卷积特征的全局池化结果拼接后再进全连接这样模型既有局部细节又有全局时序上下文。改动不大但在流量突变频繁的路段上MAE 通常能再降几个百分点。第二个是验证方式。前面用的是单次时间切分评估结果受切分点位置影响较大。更稳的做法是滚动验证把测试段再切成若干连续子段模型在每个子段上滚动预测最后汇总指标。这样能看出模型在不同时段的稳定性而不是被某一段好数据蒙蔽。滚动验证的代码骨架如下。def rolling_eval(model, series, in_len, out_len, mu, sigma, step1): 在序列上滚动预测返回真实值与预测值 model.eval() preds, trues [], [] for i in range(0, len(series) - in_len - out_len 1, step): window (series[i : i in_len] - mu) / sigma x torch.tensor(window, dtypetorch.float32).unsqueeze(0).to(device) with torch.no_grad(): p model(x).cpu().numpy().flatten() preds.append(p * sigma mu) # 反标准化 trues.append(series[i in_len : i in_len out_len]) return np.array(trues), np.array(preds)step控制滚动步长取 1 是最细粒度但最慢取out_len则每次预测不重叠、速度快。反标准化那行p * sigma mu必须做否则算出来的指标没有意义。拿到trues和preds后按子段分别算 MAE 和 MAPE就能看出模型在高峰、平峰、夜间各自的表现。参数上还有两个值得试的方向一是把输入窗口从 15 拉长到 30看长程依赖是否带来提升但要注意 BiLSTM 序列变长后训练变慢二是把单变量输入扩展成多变量比如同时输入流量、占有率、平均速度卷积的输入通道从 1 改成 3模型能利用更多信息但要注意各变量量纲不同必须分别标准化。我自己踩过最深的一个坑是早期图省事对全量数据做了标准化离线指标漂亮得不像话换到新路段直接崩盘。后来养成习惯任何进入模型的统计量都只从训练集算验证和测试一律复用。这个习惯比任何调参技巧都值钱。希望帮到你。本文还有配套的精品资源点击获取