
简介基于深度学习的交通流预测实现采用堆叠自编码器、长短时记忆网络与门控循环单元三种典型网络适用于本科、硕士阶段的教研学习以及智能交通领域的时序预测任务。压缩包共17个文件包含4个Python脚本主程序、模型定义、训练与数据预处理等、5个CSV格式的训练测试数据集、3个已训练好的H5模型权重文件分别对应三种网络、4张损失曲线与预测对比的PNG图片以及1份MD说明文档整体大小仅3.2MB目录按数据、模型、图像划分结构清晰便于快速定位。目前已有860人学习下载获得一定关注度。借助完整的代码和预训练权重读者可以直接运行或复现三种模型的交通流预测效果直观对比堆叠自编码器与循环网络在时序建模上的差异理解特征提取和记忆机制的各自特点也可基于已有数据与脚本进行调参、扩展或迁移到其他交通场景作为毕业设计或课程实验的参考实现。1. 交通流预测为什么要用 SAEsLSTMGRU一个反直觉的起点深夜开车回家导航地图上的路况条已经红得发紫而你看到的其实是预测结果不是实时路况——导航在预判下一个5分钟的拥堵趋势这正是交通流预测的核心场景。早几年大家用ARIMA这类统计模型但城市交通流有强周期、强突变、多传感器高耦合统计模型在这些场景下频繁翻车。基于深度学习的方案尤其是SAEsLSTMGRU的组合成了从业者最常用的落地套路SAEs从多路传感器的历史流量数据里压出稳定特征LSTM记住“上周二早高峰也这样”的长时依赖GRU用更少的参数逼近LSTM的效果。标题里这份带Python代码的压缩包目标就是把这套方案跑通把关键参数和坑点讲透。适合正在做交通流量预测、时序预测或者想对比LSTM和GRU落地效果的人。2. 选型逻辑SAEs做特征、LSTM做序列、GRU做轻量替代三层分工的边界2.1 SAEs在交通流预测中的真实角色先说一个容易误解的点SAEs不是预测模型它是特征提取器。交通流数据最常见的形态是一条路段的多个传感器按固定粒度比如5分钟上报流量、速度、占有率。一个研究区域的输入维度轻松到几十甚至上百。如果把几十维的原始序列直接拼给LSTM训练慢是一回事更麻烦的是那些互不相关的噪声维度会干扰时序建模。SAEs要解决的就是这个高维问题。堆叠自编码器的训练方式与普通神经网络不一样。它不依赖标签而是用“输入等于输出”的重建任务逼网络找出数据里的结构。每一层先单独预训练输入层到隐藏层是编码器隐藏层到输出层是解码器训练目标是最小化重建误差。逐层预训练完成后再把整个网络展开做一次微调。最后丢掉解码器只保留编码器把原始的几十维变成十几维乃至几维的压缩表达。这里有一个参数需要认真调encoding_dims堆叠维度序列。我做过一组对比对16路传感器数据SAE从64压到32再压到16效果最好继续压到8就开始丢信息。压缩后的特征再进LSTM或GRU训练时间大约能省30%-40%验证集MAE能降10%-15%。当然这种收益只在特征维度中等偏上时才明显——如果你手上本来就一个序列只做单变量时间序列预测那SAE的意义不大别强行往上叠。还有一个容易忽略的细节SAE在特征提取时逐样本处理不关心时间顺序。它把每个时间步的多路传感器读数当作一条独立样本编码后再用滑动窗口组织成时间序列。也就是说时序关系是在SAE之后由LSTM或GRU来建模的。不要试图把时序也丢给SAE那只会得到一个更差的降维结果。结构上我一般这样组织输入维度等于传感器数量编码层数2-3层每层神经元数递减激活函数用ReLU解码层与编码层对称最后一层用线性激活损失函数取MSE。训练轮次不需要太多重建loss不再下降就停代码在第3章。2.2 LSTM与GRU的取舍什么时候GRU反而更香LSTM的三门结构遗忘门、输入门、输出门让它有了独立的细胞状态长序列记忆能力更强。GRU只有更新门和重置门没有单独的细胞状态参数更少。对交通流预测来说选择主要看数据规模和延迟要求。绝大多数交通流场景是分钟级采样一天的样本量288个一周也不过2016个。这个规模对LSTM来说偏小容易在训练集上过拟合。GRU参数更少数据量不够大的时候反而更容易收敛验证集表现经常与LSTM打平甚至更好。另一个场景是实时路况预测模型要跑在流式管道上延迟按秒算GRU推理速度的优势就很实际。我做过一次对比同样64隐藏单元、两层结构GRU单次推理耗时大约是LSTM的70%在上线频率高的场景这笔账很划算。如果你的数据量大上百个传感器、历史数据覆盖几年且预测任务依赖较长时序LSTM的上限更高。一个务实的判断方法是先跑GRU做baseline验证集loss离业务目标差得远再上LSTM不要一上来把两个模型都仔细调。这里有一个常见坑很多项目用默认参数直接比较LSTM和GRU这不公平。隐藏单元数、层数、dropout、训练轮次都要保持一致最好都用早停对比才有意义。还有一个更隐蔽的坑两层LSTM或GRU堆叠时第一层必须设return_sequencesTrue否则第二层拿不到序列输入初学者经常在这里翻车。2.3 推荐的组合管线与数据流梳理一下整个方案的数据流方便后面照着写代码。原始数据是二维矩阵形状为时间步数, 传感器数。第一步用SAE对每个时间步的多路传感器读数做特征压缩输出仍是二维矩阵形状为时间步数, 编码维度比如从20维压到8维。第二步用滑动窗口把压缩后的特征切成三维样本形状为样本数, 窗口长度, 编码维度。第三步送入LSTM或GRU输出层是未来一个时刻的流量值如果是多步预测就把输出层改成多个神经元。这条管线里最容易被忽略的是第一步和第二步的衔接。SAE训练时完全不考虑时间因素但切窗时每个窗口内的特征顺序必须保持原始时序不能打乱。训练SAE的样本顺序可以打乱切窗阶段必须严格按时间先后进行验证集和测试集也不能按随机抽样划分必须按时间切片。交通流数据的时间相关性是模型能力的核心打乱测试集等于作弊。如果业务要求预测未来1小时也就是12个5分钟步我一般把输出层改为12个神经元一次性输出做多步直接预测避开递归预测的误差累积。具体差别在第5章避坑里细讲。3. 用Python复现交通流预测环境、数据准备与SAEs/LSTM/GRU三段代码3.1 环境配置Python版本、依赖库与GPU注意先说明运行环境。我建议用Python 3.10或3.11配TensorFlow 2.x。如果你机器上已有TensorFlow先确认大版本是2.x1.x的API差异极大很多教程代码在上面跑不通。依赖清单如下pip install tensorflow2.15.0 numpy pandas scikit-learn matplotlibTensorFlow包很大下载慢的话可以换国内镜像源pip install tensorflow2.15.0 -i https://pypi.tuna.tsinghua.edu.cn/simpleGPU不是必需。交通流预测的数据量通常不大CPU也能在十几分钟内跑完一轮对比实验。唯一需要注意的是TensorFlow默认会吞掉所有可用显存如果你还要跑其他任务可以在代码开头限制显存按需增长import tensorflow as tf gpus tf.config.experimental.list_physical_devices(GPU) if gpus: try: for gpu in gpus: tf.config.experimental.set_memory_growth(gpu, True) except RuntimeError as e: print(e)这段代码设置显存按需增长而不是强行限制上限单个小模型运行时无感知但服务器上多人共用时会避免互相抢显存。如果你根本不用GPU这段可以直接忽略。3.2 数据准备模拟多传感器数据与滑动窗口切样本真实的交通流预测常用PEMS、METR-LA这类公开数据集但新手拿到后会被数据格式和数据清洗劝退。先用模拟数据跑通主流程再换真实数据是更平滑的路径。下面的模拟数据生成20个传感器的两周流量每5分钟一个点每个传感器共享同一套早晚高峰节律但各有相位偏移和独立噪声import numpy as np import pandas as pd np.random.seed(42) points_per_day 288 # 24小时 * 12个5分钟 days 14 n points_per_day * days t np.arange(n) n_sensors 20 traffic_all np.zeros((n, n_sensors)) for s in range(n_sensors): phase 2 * np.pi * s / n_sensors pattern (0.5 0.4 * np.sin(2 * np.pi * t / points_per_day phase) 0.2 * np.sin(2 * np.pi * t / (points_per_day * 7))) traffic_all[:, s] np.clip(pattern * 100 np.random.normal(0, 5, n), 5, 120) print(traffic_all.shape) # (4032, 20)这个生成方式有两个关键点第一每个传感器共享主干趋势模拟的是同一路段上下游传感器之间的相关性第二phase制造了传感器间的相位差异模拟车流从上游到下游的传播延迟。真实数据比这个复杂得多但用它跑通代码已经足够。接下来按时间切分训练、验证、测试集再做归一化。注意MinMaxScaler只能fit训练集这一点极其重要泄漏问题在第5章详谈。from sklearn.preprocessing import MinMaxScaler train_size int(len(traffic_all) * 0.7) val_size int(len(traffic_all) * 0.1) train_raw traffic_all[:train_size] val_raw traffic_all[train_size:train_size val_size] test_raw traffic_all[train_size val_size:] 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)这里fit_transform和transform的区别不是语法细节训练集的min-max统计量一旦固定验证集和测试集只是套用同一套缩放参数整套评估流程里就不会混入未来信息。3.3 SAEs特征提取堆叠自编码器的完整代码SAE的输入是每个时间步的20维传感器读数输出是压缩后的特征向量。我用编码维度(16, 8)也就是先压到16再压到8解码层对称重建回20维from tensorflow.keras.layers import Input, Dense from tensorflow.keras.models import Model def build_sae(input_dim, encoding_dims(16, 8)): inputs Input(shape(input_dim,)) x inputs # 编码器逐层降维 for dim in encoding_dims: x Dense(dim, activationrelu)(x) encoded x # 解码器对称重建回原始维度 x encoded for dim in reversed(encoding_dims): x Dense(dim, activationrelu)(x) decoded Dense(input_dim, activationlinear)(x) autoencoder Model(inputs, decoded) autoencoder.compile(optimizeradam, lossmse) encoder Model(inputs, encoded) return autoencoder, encoder代码逻辑很清楚前半段是编码器后半段是解码器返回值有两个——完整的自编码器和只保留编码部分的encoder模型。编码器后面会单独用来做特征提取。这里有个容易忽略的细节解码层也用了relu但最外层的输出用了linear因为流量数据归一化后落在[0,1]区间relu会在0处截断导致重建值偏小。训练和特征提取sae, encoder build_sae(n_sensors, encoding_dims(16, 8)) sae.fit(train_scaled, train_scaled, validation_data(val_scaled, val_scaled), epochs50, batch_size256, verbose0) train_features encoder.predict(train_scaled) val_features encoder.predict(val_scaled) test_features encoder.predict(test_scaled) print(train_features.shape) # (2822, 8)参数说明epochs取50是因为无监督重建任务通常收敛快30-50轮足够batch_size取256交通流样本量大且特征维度不高256在计算速度和收敛稳定性之间比较平衡。真正训练完成后train_features就是压缩后的8维特征丢掉了部分噪声保留了多传感器相关性。3.4 滑动窗口切样本把特征序列变成三维输入压缩后的特征仍然是二维矩阵时间步, 特征维度LSTM需要的是三维输入。窗口长度我取24也就是过去2小时的5分钟粒度数据预测下一个时间点def create_sequences(data, window_size24, horizon1, step1): 把二维特征序列切成三维样本。 data: (n_timesteps, n_features) X: (n_samples, window_size, n_features) y: (n_samples, horizon)预测第0个传感器目标断面 X, y [], [] for i in range(0, len(data) - window_size - horizon 1, step): X.append(data[i:i window_size]) y.append(data[i window_size:i window_size horizon, 0]) return np.array(X), np.array(y) X_train, y_train create_sequences(train_features, window_size24, horizon1) X_val, y_val create_sequences(val_features, window_size24, horizon1) X_test, y_test create_sequences(test_features, window_size24, horizon1) print(X_train.shape, y_train.shape) # (2799, 24, 8), (2799, 1)y取第0列也就是第一个传感器的未来流量。这样设定的原因是业务模型通常只关心关键断面不需要同时预测全部20路传感器。step1表示逐时间步滑动如果你的样本量过大导致训练太慢可以改成step5做下采样但验证集评估时最好保持step1避免漏掉某些时段。3.5 LSTM与GRU模型代码同一套接口跑对比LSTM和GRU的模型结构设计成完全对称隐藏单元64两层堆叠中间夹dropout保证对比公平from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, GRU, Dense, Dropout def build_lstm(input_shape(24, 8), units64, dropout_rate0.2): model Sequential([ LSTM(units, return_sequencesTrue, input_shapeinput_shape), Dropout(dropout_rate), LSTM(units // 2, return_sequencesFalse), Dropout(dropout_rate), Dense(1) ]) model.compile(optimizeradam, lossmse, metrics[mae]) return model def build_gru(input_shape(24, 8), units64, dropout_rate0.2): model Sequential([ GRU(units, return_sequencesTrue, input_shapeinput_shape), Dropout(dropout_rate), GRU(units // 2, return_sequencesFalse), Dropout(dropout_rate), Dense(1) ]) model.compile(optimizeradam, lossmse, metrics[mae]) return model两个函数的结构完全对齐第一层返回序列return_sequencesTrue第二层只返回最后一个时间步的输出再经dropout后接一个Dense输出层。units64作为中间层的容量units // 2 32作为最后一层主要考虑是交通流序列的复杂度还不至于需要两层64个单元。input_shape的24是窗口长度8是SAE压缩后的特征维度和create_sequences的输出形状对应。训练和早停用同一个回调from tensorflow.keras.callbacks import EarlyStopping early_stop EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue) lstm_model build_lstm() history_lstm lstm_model.fit( X_train, y_train, validation_data(X_val, y_val), epochs100, batch_size256, callbacks[early_stop], verbose1 ) gru_model build_gru() history_gru gru_model.fit( X_train, y_train, validation_data(X_val, y_val), epochs100, batch_size256, callbacks[early_stop], verbose1 )restore_best_weightsTrue相当于买了后悔药早停触发时模型参数回滚到验证集loss最小的那个epoch而不是停在最后一个epoch这一点能明显改善最终效果。epochs设100是上限实际大多数情况下20-40轮就会触发早停。4. 交通流预测必调的5个核心参数窗口长度、学习率、隐藏单元、Dropout与早停4.1 窗口长度24步与48步的差别窗口长度决定模型“回头看多远”。5分钟粒度下window_size24代表过去2小时48代表过去4小时。短期预测5分钟到15分钟用24通常足够因为交通流的记忆效应在2小时左右已经衰减得差不多。更长窗口不一定带来更高精度反而增加计算量并且会把更早的无关波动引入模型的注意力范围。我建议把窗口长度作为第一个对比实验固定其他参数分别跑12、24、48看验证集变化。一个典型结果是从12升到24时loss下降明显从24升到48时可能只有微小变化甚至略微回升。这说明2小时已经是该数据集的记忆边界。另外窗口长度必须和SAE的特征维度一起考虑24乘8的特征矩阵已经包含了192个数值再拉长窗口会让模型容量压力变大。4.2 学习率Adam默认值不一定对Adam默认lr0.001在LSTM里经常偏大尤其在交通流数据这种噪声水平较高的场景训练loss会出现低频震荡验证集曲线也跟着抖。我见过不少项目调loss却忽略了lr最后归咎于模型结构不行其实只是学习率不合适。建议从0.001开始跑10轮观察loss曲线如果呈锯齿状震荡降到0.0005如果平稳下降但速度慢可以保持或微升到0.002。更稳重的做法是配合ReduceLROnPlateau回调验证集loss连续5轮不降时自动把学习率减半from tensorflow.keras.callbacks import ReduceLROnPlateau lr_scheduler ReduceLROnPlateau( monitorval_loss, factor0.5, patience5, min_lr1e-5 )这个回调的意义在于你不需要一开始就猜对学习率训练过程会自动找到合适的下降路径。把它和早停同时挂上跑一组完整的对比实验结果会稳定很多。4.3 隐藏单元LSTM与GRU的容量匹配隐藏单元数量决定模型的记忆容量。我在第3章的代码里取了units64、第二层32这是交通流预测比较常用的起点。特征维度只有8输入信息量不大隐藏单元太大反而容易记住训练集里的噪声细节。一个朴素的经验隐藏单元数不要超过训练样本数的1/10否则过拟合风险急剧上升。按第3章的数据样本数约280064到128个隐藏单元都是安全的但256就要小心了。对比LSTM和GRU时还要注意相同units下LSTM的参数数量大约是GRU的1.3到1.4倍。这不是问题但解释结果时要清楚——如果GRU效果更好一部分原因是参数少、在有限数据下更容易泛化而不一定是结构更优。反过来如果LSTM更好也不代表它结构更强可能是容量大恰好拟合了数据里的复杂模式。4.4 Dropout防止路况噪声带来的过拟合交通流数据天生含噪偶发事故、红绿灯相位变化、天气影响都会造成流量突变。模型很容易把这些噪声当成规律记下来Dropout是抵御这种过拟合的最直接手段。0.2到0.3是常用区间我一般从0.2起步如果训练集和验证集的loss差距仍然明显再升到0.3或0.4。需要说明的是Keras里LSTM和GRU层的dropout参数和普通Dense层的dropout行为不完全一样。LSTM的dropout作用于输入和递归连接属于变分dropout训练时生效推理时自动关闭所以模型文件的体积会因为你用了dropout而略有变化。某些实现里dropout和recurrent_dropout是分开的前者拦输入后者拦循环连接。第3章代码里用的Dropout层是普通情况放在LSTM输出之后简单直接效果也可控。4.5 早停用验证损失决定训练终点早停看起来简单但有两个参数经常被调错。第一个是patience我取10意思是验证集loss连续10轮不下降才停。对于交通流数据validation loss的曲线有明显的平台期patience太小比如3-5容易停在平台边缘错过后面小幅的改善。patience太大会浪费训练时间。第二是monitor必须监视图验证集val_loss不要用训练集loss。训练集loss会一直下降用它做早停没有任何意义。另外记得加上restore_best_weightsTrue否则模型停在最后一个epoch正好是验证集loss开始反弹的位置效果会差不少。5. 避坑指南交通流预测里的5个常见坑与排查方法5.1 归一化泄漏用全数据集fit MinMaxScaler会让测试集结果虚高现象测试集MAE低得惊人模型上线后预测表现却远不如实验数据。原因代码里先对全量数据调了scaler.fit()再划分训练测试集scaler的统计量已经看了测试集的分布相当于测试信息在预处理阶段泄漏进了训练流程。这在交通流数据里特别容易发生因为流量序列不同时段的均值变化大比如深夜流量低、早高峰流量高全数据集fit后测试集的min-max尺度被训练集约束预测误差被人为压缩。解决必须严格按时间切分先切分再用训练集的transform统计量去处理验证集和测试集。第3章代码里已经遵循了这个顺序排查自己的代码时重点看两点fit_transform和transform是否用对了对象有没有在划分之前调用fit。5.2 多步预测的误差累积预测越远越漂浮现象递归预测未来12个时间步前3步误差尚可第6步之后预测值几乎变成一条平稳直线。原因递归预测把上一步的预测值当作下一步的输入误差会逐级放大。LSTM和GRU本身对输入的微小扰动非常敏感尤其是在序列边缘一个偏了0.1的输入经过几个时间步的递归就能让输出完全漂移。解决做多步预测时不要用递归改成多输出模型也就是把输出层的Dense(1)换成Dense(horizon)一次性输出未来horizon个时间步。这样每个输出步共享同一个输入窗口误差不会迭代累积训练和推理也更简单。第3章代码里的create_sequences已经支持horizon大于1你只需要把输出层的神经元数改成horizon。5.3 早晚高峰系统性延迟模型总在车流转折后一拍才响应现象把预测值和真实值画在同一张图上发现早高峰来临和退去这两个拐点处预测曲线总是晚了15到20分钟。原因MSE损失本质上在拟合条件均值当训练数据里拐点样本占比小模型倾向于输出一个平滑的中间值而不是冒险预测突变。这不是bug是损失函数偏向保守的必然结果。解决第一个选择是给模型加入时间特征比如当前时刻的小时数、星期几让模型有机会学到周期性规律。具体做法是把hour_of_day归一化后拼到SAE特征后面再一起进LSTM。第二个选择是换损失函数比如用Huber损失或分位数损失对离群点更鲁棒。第三种方案是直接用GRU或LSTM做分位数回归输出低分位和高分位把预测区间交给业务方。5.4 随机种子不一致同一份代码两次运行结果差很大现象LSTM和GRU的对比实验换了一台机器重新跑结论反转了——原本GRU更好现在LSTM更好。原因NumPy和TensorFlow各有自己的随机源只固定其中一个另一个仍然会造成权重初始化、数据shuffle顺序的差异。尤其在使用Dropout和EarlyStopping时随机性会被进一步放大。解决代码最前面集中固定所有随机源import os import random import numpy as np import tensorflow as tf os.environ[PYTHONHASHSEED] 0 random.seed(42) np.random.seed(42) tf.random.set_seed(42)还要注意如果用了GPUTensorFlow的某些算子仍存在非确定性执行路径。要求完全可复现的话需要加一层tf.config.threading.set_inter_op_parallelism_threads(1) tf.config.threading.set_intra_op_parallelism_threads(1)这会牺牲一点训练速度但对对比实验来说可复现比那几分钟更值钱。5.5 验证集划分不当随机抽样等于把未来泄漏给模型现象验证集loss很低但模型在真实场景中的表现明显更差且测试集结果也不稳定。原因很多人习惯用train_test_split随机切分数据这在普通机器学习任务里是默认做法但对时间序列是完全错误的。随机抽样会把时间上相邻的样本同时分到训练集和验证集比如样本i和i1在时间上仅相隔5分钟信息高度重叠验证集就失去意义。解决严格按时间顺序切分前70%训练、中间10%验证、最后20%测试。这种切分方式更接近线上环境模型只见过过去必须预测未来。如果数据存在周期性比如一周完整周期最好让训练集覆盖至少两个完整的星期验证集和测试集也各自落在完整星期上避免因为周一和周日流量差异造成评估偏差。6. 用残差图验证模型是否真的可用上线前的最后一个检查6.1 残差图的画法与判读标准训练完模型后我习惯先画残差图而不是只看测试集MAE。MAE是一个压缩后的标量它掩盖了误差随时间和预测值变化的规律。残差图能直接暴露模型的系统性失误。import matplotlib.pyplot as plt pred gru_model.predict(X_test).ravel() residual y_test.ravel() - pred fig, axes plt.subplots(2, 1, figsize(12, 7)) axes[0].plot(residual[:300], linewidth0.8) axes[0].axhline(y0, colorr, linestyle--) axes[0].set_title(Residual over time (first 300 points)) axes[1].scatter(y_test.ravel()[:300], residual[:300], alpha0.5) axes[1].axhline(y0, colorr, linestyle--) axes[1].set_xlabel(Actual value) axes[1].set_ylabel(Residual) plt.tight_layout() plt.show()判读标准不复杂上面那张图里残差应在0附近随机波动没有连续几段持续为正或持续为负。如果某段时间残差连续为正说明模型系统性地低估流量大概率是早晚高峰这类动态阶段没学透。下面那张散点图残差应该均匀分布在0刻度线两侧如果呈喇叭形——流量越大残差越散——说明模型在高流量区间稳定性不足这对疏导应用来说是有风险信号的。6.2 上线前最后检查表残差图过了最后再走一遍检查流程检查项具体内容特征口径线上输入的特征顺序、命名、缺失值填充方式是否与训练时一致异常值传感器掉线补0是否会被模型当成真实值是否有清洗层推理延迟单次预测耗费的时间是否满足线上服务时延要求模型回滚是否保存了上一版模型文件新模型出问题能否快速切回我吃过一次亏模型在离线测试上表现很好上线后第一次出现传感器断流补零的数据被当成真实流量模型十分钟内预测出拥堵指数翻倍误报警直接打到了路况大屏。从那以后我养成了一个习惯——任何预测模型上线前先看残差再看测试指标并且一定要在代码里留一道异常值清洗逻辑在线服务永远要假设输入数据是脏的。这套流程走完交通流预测方案才算真正落地。希望帮到你。本文还有配套的精品资源点击获取