交通拥堵预测毕设实战:从数据检查到LSTM模型完整流程
发布时间:2026/10/2 2:44:24来源:尧图网络
简介这份资源是面向计算机、人工智能、通信工程等专业学生与教师的交通拥堵预测毕设项目包围绕GCM Corridor真实路网数据展开解决基于历史交通流预测未来30分钟道路拥堵状态的问题。项目已测试可运行适合课程设计、毕业设计、作业提交或项目初期立项演示也便于在现有代码上二次修改扩展功能。压缩包共19个文件约32KB包含6个py脚本、6个zip子包、5个txt说明与2个csv数据文件覆盖数据预处理、传感器筛选、模型训练与测试等环节并附训练集与测试集。数据含855个传感器每5分钟一条记录拥堵状态分为通畅、轻微、中度、重度四级输出格式为传感器ID对应连续6个状态值。目前已有1190人学习下载读者可据此掌握完整赛题方案、数据挖掘与机器学习建模流程、实验报告撰写思路及评估方法。1. 从一份 zip 说起交通拥堵预测毕设到底在做什么城市道路的流量数据本质上是一条条带时间戳的计数序列。早高峰 7:30 到 9:00 之间某个路口每 5 分钟通过多少辆车这些数字连起来就是一条曲线。交通拥堵预测要干的事就是拿历史曲线去推未来某个时间段的曲线。听起来像时间序列预测但真正做起来麻烦在于流量数据同时受三个东西影响日周期每天早晚高峰位置差不多、周周期工作日和周末完全不同、以及随机扰动下雨、事故、节假日。一个能跑通的车辆流量预测系统核心就是把这三层拆开建模。这份毕设标题里给了说明文档、训练集和测试集意味着它不是让你从零爬数据而是给你一份已经切分好的时序数据让你把预测模型搭起来、跑出指标、写成论文。适合谁适合正在做交通方向毕设、需要一套完整可复现流程的本科生也适合想入门时序预测但不知道拿什么数据练手的 Python 学习者。Python 在这里的角色是工具链pandas 处理时间索引sklearn 或 PyTorch 搭模型matplotlib 出图。整套东西不依赖 GPU 也能跑普通笔记本足够。我见过太多人拿到这类数据后直接上 LSTM结果 MAE 比移动平均还差。问题不在模型在于时间特征没构造对。下面从数据检查开始一步步把这条链路走通。2. 数据到手先别急着建模训练集与测试集的检查与时间对齐2.1 先搞清楚你的数据长什么样拿到训练集和测试集两个文件第一件事不是 import torch而是用 pandas 把结构看清楚。交通流量数据常见的格式有两种宽表每列是一个路段/检测器每行是一个时间点和长表每行是一条记录包含时间、路段 ID、流量值。两种格式的处理方式完全不同先确认再动手。import pandas as pd import numpy as np # 读取训练集先看前5行和基本信息 train pd.read_csv(train.csv) print(train.head()) print(train.shape) print(train.dtypes) print(train.isnull().sum()) # 如果第一列是时间字符串转成 datetime 并设为索引 train[timestamp] pd.to_datetime(train[timestamp]) train train.set_index(timestamp).sort_index() # 检查时间间隔是否均匀 diff train.index.to_series().diff().dropna() print(diff.value_counts().head())这段代码做了四件事看数据形状、看字段类型、统计缺失值、检查时间间隔。pd.to_datetime把字符串转成时间对象set_index让时间成为索引后续做滑动窗口才方便。diff().value_counts()是判断采样频率的关键——如果绝大多数间隔是 5 分钟说明数据是均匀采样的如果出现大量不规则间隔说明中间有缺失时间段需要先补全再建模。参数上唯一需要注意的是pd.to_datetime的format参数。如果时间格式是2023/1/1 7:30这种非标准格式不指定 format 可能会解析失败或解析成错误日期。常见做法是先pd.to_datetime(train[timestamp], format%Y/%m/%d %H:%M)试一次报错了再去掉 format 让它自动推断。2.2 训练集和测试集的时间边界必须连续这是最容易翻车的地方。很多人拿到两个文件各自独立做归一化然后训练集归一化的均值和测试集归一化的均值不一样模型在测试集上直接崩掉。正确做法是用训练集的统计量去归一化测试集。# 假设流量列叫 flow train_flow train[flow].values.reshape(-1, 1) test_flow test[flow].values.reshape(-1, 1) from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler(feature_range(0, 1)) train_scaled scaler.fit_transform(train_flow) # 只在训练集上 fit test_scaled scaler.transform(test_flow) # 测试集只 transform print(训练集时间范围:, train.index.min(), 到, train.index.max()) print(测试集时间范围:, test.index.min(), 到, test.index.max())fit_transform和transform的区别是血泪经验前者会计算均值和方差并应用后者只用已有的均值和方差做变换。如果测试集也 fit等于把未来信息泄露给了模型论文里的指标会虚高答辩时被问到就说不清楚。另外要确认训练集和测试集在时间上是首尾相接的。如果训练集到 2023-06-30 结束测试集从 2023-07-01 开始这是标准切分。如果中间跳了几天滑动窗口在边界处会产生错误的样本需要手动对齐或丢弃边界样本。2.3 缺失值和异常值的处理策略交通流量数据的缺失通常有两种原因设备故障和真实无车。设备故障的缺失应该补真实无车的 0 不应该动。区分方法看缺失段的长度连续缺失超过 1 小时的大概率是设备问题零星单个缺失的可能是传输丢包。# 线性插值补短缺失长缺失标记出来 train[flow] train[flow].interpolate(methodlinear, limit6) # 找出仍然缺失的位置长缺失 missing_mask train[flow].isnull() print(长缺失段数量:, missing_mask.sum()) # 异常值流量为负或超过物理上限比如单车道 5 分钟超过 200 辆 train.loc[train[flow] 0, flow] np.nan train[flow] train[flow].interpolate(methodlinear, limit6)interpolate的limit6表示最多连续补 6 个点对应 5 分钟采样就是 30 分钟。超过这个长度的缺失不补留给模型自己处理或者直接丢弃该时间段。异常值处理不要用 3σ 一刀切交通流量本身波动就大3σ 会误杀高峰值。用物理上限判断更稳城市道路单车道 5 分钟通过 200 辆已经是饱和状态超过这个数基本是计数错误。3. 特征工程把时间戳变成模型能吃的数字3.1 时间特征小时、星期、是否高峰原始数据只有一列流量值模型没法直接理解“早上 8 点”和“下午 3 点”的区别。需要把时间索引拆成多个特征列。def build_time_features(df): df df.copy() df[hour] df.index.hour df[minute] df.index.minute df[weekday] df.index.weekday # 0周一, 6周日 df[is_weekend] (df[weekday] 5).astype(int) # 高峰标记早高峰 7-9晚高峰 17-19 df[is_peak] ((df[hour].between(7, 9)) | (df[hour].between(17, 19))).astype(int) # 周期性编码让 23 点和 0 点在数值上接近 df[hour_sin] np.sin(2 * np.pi * df[hour] / 24) df[hour_cos] np.cos(2 * np.pi * df[hour] / 24) df[weekday_sin] np.sin(2 * np.pi * df[weekday] / 7) df[weekday_cos] np.cos(2 * np.pi * df[weekday] / 7) return df train build_time_features(train) test build_time_features(test)这里的关键是 sin/cos 编码。如果直接把 hour 当数值特征模型会认为 23 点和 0 点相差 23但实际上它们只差 1 小时。sin/cos 把小时映射到单位圆上23 点和 0 点的欧氏距离就变小了。is_peak是人工先验告诉模型这两个时间段要特别关注。is_weekend同理工作日和周末的流量模式差异很大显式标记比让模型自己学更高效。参数上高峰时段的定义可以根据你的城市调整。有些城市早高峰从 7:30 开始有些从 8:00 开始。如果数据里能看到明显的双峰结构用数据驱动的方式确定高峰区间更准对每个小时求平均流量取均值加一个标准差的那些小时作为高峰。3.2 滑动窗口用过去 N 个点预测未来 M 个点时间序列预测的核心操作是切窗口。假设用过去 12 个点1 小时预测未来 3 个点15 分钟窗口切法如下def make_windows(data, input_len12, output_len3): X, y [], [] for i in range(len(data) - input_len - output_len 1): X.append(data[i : i input_len]) y.append(data[i input_len : i input_len output_len]) return np.array(X), np.array(y) # 只用流量列做单变量预测 values train[flow].values X_train, y_train make_windows(values, input_len12, output_len3) print(X shape:, X_train.shape) # (样本数, 12) print(y shape:, y_train.shape) # (样本数, 3)input_len和output_len是最需要调的参数。input_len 太短模型看不到完整的周期模式太长计算量增加且容易过拟合。经验值如果采样间隔是 5 分钟input_len 取 121 小时到 28824 小时之间。output_len 取决于你的预测需求毕设里通常预测未来 15 分钟到 1 小时对应 3 到 12 个点。注意窗口切分时不要打乱顺序。时间序列的样本之间有重叠随机 shuffle 会导致信息泄露。训练时用train_test_split的shuffleFalse或者手动按时间切分。3.3 多变量扩展加入相邻路段和天气如果数据里有多个路段的流量可以把相邻路段的流量作为额外特征。交通流有空间相关性上游路段的流量变化会传导到下游。# 假设有 road_A 和 road_B 两列 # 构造 road_B 的滞后特征作为 road_A 的输入 train[road_B_lag1] train[road_B].shift(1) train[road_B_lag2] train[road_B].shift(2) train train.dropna() # 去掉因为 shift 产生的空行shift(1)表示取上一时刻的值。滞后阶数的选择看两条路段的距离相邻路口滞后 1-2 个采样间隔隔几个路口可能要滞后 3-6 个。这个特征在单变量数据里做不了但如果你的训练集包含多个路段值得加上。天气数据同理如果有降雨标记加一个is_rain列雨天流量通常会下降 10%-20%。4. 模型选型与训练从移动平均到 LSTM 的完整对比4.1 基线模型先跑通再优化任何预测任务都应该先建立一个基线。交通流量预测最简单的基线是“昨天同一时刻的值”和“过去 1 小时的平均值”。# 基线1昨天同一时刻 train[yesterday_same] train[flow].shift(288) # 288 24h * 12 # 基线2过去12个点的移动平均 train[ma_12] train[flow].rolling(window12).mean() # 计算基线在测试集上的 MAE from sklearn.metrics import mean_absolute_error valid train.dropna() mae_yesterday mean_absolute_error(valid[flow], valid[yesterday_same]) mae_ma mean_absolute_error(valid[flow], valid[ma_12]) print(f昨天同一时刻 MAE: {mae_yesterday:.2f}) print(f移动平均 MAE: {mae_ma:.2f})这两个数字是你的“及格线”。后面不管上什么模型MAE 必须明显低于这两个值才有意义。我见过有人 LSTM 跑出来 MAE 比移动平均还高然后强行解释“模型还在学习”这是自欺欺人。基线跑完心里有数了再上复杂模型。4.2 LSTM 模型搭建与关键参数LSTM 是交通流量预测里最常用的深度学习模型能捕捉长距离依赖。用 PyTorch 搭一个两层 LSTMimport torch import torch.nn as nn class TrafficLSTM(nn.Module): def __init__(self, input_size1, hidden_size64, num_layers2, output_size3): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropout0.2 ) self.fc nn.Linear(hidden_size, output_size) def forward(self, x): # x shape: (batch, seq_len, input_size) out, _ self.lstm(x) out out[:, -1, :] # 取最后一个时间步的隐藏状态 out self.fc(out) return out model TrafficLSTM(input_size1, hidden_size64, num_layers2, output_size3) criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr0.001)hidden_size64是起点数据量大可以加到 128数据少就降到 32。num_layers2比单层能表达更复杂的模式但超过 3 层容易过拟合。dropout0.2是正则化防止过拟合。batch_firstTrue让输入维度是 (batch, seq, feature)符合直觉。训练循环里要注意输入 X 的 shape 需要是 (batch, input_len, 1)所以要把之前的二维数组升一维。X_train_t torch.FloatTensor(X_train).unsqueeze(-1) # (N, 12, 1) y_train_t torch.FloatTensor(y_train) dataset torch.utils.data.TensorDataset(X_train_t, y_train_t) loader torch.utils.data.DataLoader(dataset, batch_size64, shuffleTrue) for epoch in range(50): model.train() total_loss 0 for xb, yb in loader: optimizer.zero_grad() pred model(xb) loss criterion(pred, yb) loss.backward() optimizer.step() total_loss loss.item() if (epoch 1) % 10 0: print(fEpoch {epoch1}, Loss: {total_loss/len(loader):.4f})batch_size64是常用值显存不够就降到 32 或 16。shuffleTrue在训练时打乱样本顺序但注意这里的打乱是在窗口已经切好之后不会造成时间泄露。50 个 epoch 是保守估计实际看 loss 曲线如果 20 个 epoch 后 loss 不再下降就可以停。4.3 训练完怎么验证反归一化和指标计算模型输出的是归一化后的值必须反归一化才能和真实流量对比。model.eval() with torch.no_grad(): X_test_t torch.FloatTensor(X_test).unsqueeze(-1) pred_scaled model(X_test_t).numpy() # 反归一化 pred scaler.inverse_transform(pred_scaled) true scaler.inverse_transform(y_test) # 计算指标 mae mean_absolute_error(true, pred) rmse np.sqrt(((true - pred) ** 2).mean()) print(fMAE: {mae:.2f}, RMSE: {rmse:.2f}) # 按预测步长分别看误差 for step in range(3): step_mae mean_absolute_error(true[:, step], pred[:, step]) print(f第{step1}步 MAE: {step_mae:.2f})按步长分别看误差很重要。通常第 1 步预测最准第 3 步误差会增大。如果第 3 步误差突然爆炸说明 output_len 设太大了模型没有能力预测那么远。这时候要么减小 output_len要么增加 input_len 给模型更多上下文。反归一化时注意 scaler 是在训练集上 fit 的测试集的 transform 用的是训练集的参数所以 inverse_transform 也要用同一个 scaler。如果训练集和测试集分布差异大反归一化后的值可能会有偏移这是正常的说明模型泛化能力有限。5. 避坑与排查交通流量预测里最容易翻车的 5 个地方5.1 现象测试集 MAE 远大于训练集 MAE原因最常见的是归一化泄露。测试集用了自己的均值和方差做归一化导致训练和测试不在同一个尺度上。另一个可能是训练集和测试集的时间分布不同比如训练集全是工作日测试集包含周末。解决确认 scaler 只在训练集上 fit。检查训练集和测试集的星期分布如果差异大在划分数据时就按时间分层采样保证两边都有工作日和周末。5.2 现象模型预测的曲线比真实曲线平滑很多原因MSE 损失函数对大误差惩罚重模型倾向于预测均值来降低整体损失。交通流量的高峰值被“平均”掉了。解决换损失函数。用 MAE 或者 Huber Loss 替代 MSE对异常值的惩罚更温和。或者在 MSE 基础上给高峰时段样本加权让模型更关注高峰。# 高峰时段样本加权 weights torch.FloatTensor([2.0 if is_peak else 1.0 for is_peak in train[is_peak].values[:len(X_train_t)]]) loss (weights.unsqueeze(-1) * (pred - yb) ** 2).mean()5.3 现象训练 loss 一直不下降原因学习率太大导致震荡或者输入数据没有归一化。LSTM 对输入尺度敏感如果流量值在 0-200 之间不归一化直接喂进去梯度会爆炸。解决先检查数据是否归一化到 [0,1] 或 [-1,1]。然后降低学习率从 0.001 降到 0.0001 试。如果还不降检查窗口切分是否正确X 和 y 是否对齐。5.4 现象预测结果整体偏移一个固定值原因反归一化时用错了 scaler或者训练集和测试集的流量基线不同。比如训练集平均流量 50测试集平均流量 80模型学到的映射在测试集上整体偏低。解决检查 scaler 的 data_min_ 和 data_max_确认反归一化后的范围合理。如果确实是分布偏移考虑用差分而不是原始值建模预测流量的一阶差分再累加回去。5.5 现象多步预测时后面几步完全不准原因直接多步预测一次性输出所有步比滚动预测难。模型在第 1 步的小误差会累积到后面。解决改用滚动预测每次只预测 1 步然后把预测值作为输入预测下一步。或者用 Seq2Seq 结构让解码器逐步生成。毕设里如果 output_len 不超过 6直接多步通常够用超过 6 建议上 Seq2Seq。6. 把系统跑成可交付的毕设文档、图表与复现脚本6.1 说明文档里必须有的三张图毕设答辩时老师不会逐行看代码但一定会看图表。三张图必须有真实值 vs 预测值的对比曲线、误差随预测步长变化的柱状图、不同模型的指标对比表。import matplotlib.pyplot as plt # 图1对比曲线取测试集前200个点 plt.figure(figsize(12, 4)) plt.plot(true[:200, 0], label真实值, alpha0.8) plt.plot(pred[:200, 0], label预测值, alpha0.8) plt.legend() plt.title(交通流量预测对比) plt.xlabel(时间步) plt.ylabel(流量) plt.savefig(comparison.png, dpi150, bbox_inchestight)alpha0.8让两条线重叠时还能看清。dpi150保证打印清晰。bbox_inchestight去掉多余白边。图 2 用plt.bar画每一步的 MAE图 3 用表格直接写在文档里。6.2 复现脚本的组织方式把整个流程拆成四个脚本按顺序执行01_check_data.py做数据检查和清洗02_build_features.py做特征工程和窗口切分03_train.py训练模型并保存04_evaluate.py加载模型出图和指标。每个脚本开头写清楚输入文件和输出文件中间不要有交互式操作。# 一键复现 python 01_check_data.py python 02_build_features.py python 03_train.py python 04_evaluate.py模型保存用torch.save(model.state_dict(), lstm.pth)加载时先实例化模型再load_state_dict。scaler 也要保存用joblib.dump(scaler, scaler.pkl)否则评估时没法反归一化。6.3 参数速查表参数常用值调整方向input_len121小时数据周期性强就加大到 288output_len315分钟不超过 input_len 的 1/4hidden_size64数据量小降到 32num_layers2不超过 3dropout0.2过拟合时加到 0.3-0.5learning_rate0.001loss 震荡时降到 0.0001batch_size64显存不够降到 16这张表是我自己跑过十几组参数后总结的不一定最优但能让你少走弯路。真正调参时先固定其他参数只动一个看验证集 loss 的变化方向。6.4 一个容易忽略的细节随机种子深度学习结果有随机性同一个脚本跑两次 MAE 可能差 2-3。毕设里如果老师让你复现结果对不上就尴尬了。在脚本开头固定随机种子import random import torch import numpy as np def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True set_seed(42)cudnn.deterministic True会让 GPU 计算变慢但结果可复现。如果不用 GPU这行可以去掉。种子设 42 是习惯设别的数也行关键是整个项目统一。最后说个我自己的习惯每次跑完实验把参数、指标、时间戳写进一个experiment_log.csv追加模式。毕设后期要对比十几组实验没有日志根本记不住哪组是哪组。这个习惯帮我省了至少两次重跑的时间。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网