☰
交通拥堵预测毕设实战:从数据检查到LSTM模型完整流程
2026/10/2 2:44:21 网站建设 项目流程

简介:这份资源是面向计算机、人工智能、通信工程等专业学生与教师的交通拥堵预测毕设项目包,围绕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(method="linear", limit=6) # 找出仍然缺失的位置(长缺失) missing_mask = train["flow"].isnull() print("长缺失段数量:", missing_mask.sum()) # 异常值:流量为负或超过物理上限(比如单车道 5 分钟超过 200 辆) train.loc[train["flow"] < 0, "flow"] = np.nan train["flow"] = train["flow"].interpolate(method="linear", limit=6)

interpolate的limit=6表示最多连续补 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_len=12, output_len=3): 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_len=12, output_len=3) print("X shape:", X_train.shape) # (样本数, 12) print("y shape:", y_train.shape) # (样本数, 3)

input_len和output_len是最需要调的参数。input_len 太短,模型看不到完整的周期模式;太长,计算量增加且容易过拟合。经验值:如果采样间隔是 5 分钟,input_len 取 12(1 小时)到 288(24 小时)之间。output_len 取决于你的预测需求,毕设里通常预测未来 15 分钟到 1 小时,对应 3 到 12 个点。

注意窗口切分时不要打乱顺序。时间序列的样本之间有重叠,随机 shuffle 会导致信息泄露。训练时用train_test_split的shuffle=False,或者手动按时间切分。

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(window=12).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 搭一个两层 LSTM:

import torch import torch.nn as nn class TrafficLSTM(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2, output_size=3): super().__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=0.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_size=1, hidden_size=64, num_layers=2, output_size=3) criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

hidden_size=64是起点,数据量大可以加到 128,数据少就降到 32。num_layers=2比单层能表达更复杂的模式,但超过 3 层容易过拟合。dropout=0.2是正则化,防止过拟合。batch_first=True让输入维度是 (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_size=64, shuffle=True) 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(f"Epoch {epoch+1}, Loss: {total_loss/len(loader):.4f}")

batch_size=64是常用值,显存不够就降到 32 或 16。shuffle=True在训练时打乱样本顺序,但注意这里的打乱是在窗口已经切好之后,不会造成时间泄露。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(f"MAE: {mae:.2f}, RMSE: {rmse:.2f}") # 按预测步长分别看误差 for step in range(3): step_mae = mean_absolute_error(true[:, step], pred[:, step]) print(f"第{step+1}步 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="真实值", alpha=0.8) plt.plot(pred[:200, 0], label="预测值", alpha=0.8) plt.legend() plt.title("交通流量预测对比") plt.xlabel("时间步") plt.ylabel("流量") plt.savefig("comparison.png", dpi=150, bbox_inches="tight")

alpha=0.8让两条线重叠时还能看清。dpi=150保证打印清晰。bbox_inches="tight"去掉多余白边。图 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_len12(1小时)数据周期性强就加大到 288
output_len3(15分钟)不超过 input_len 的 1/4
hidden_size64数据量小降到 32
num_layers2不超过 3
dropout0.2过拟合时加到 0.3-0.5
learning_rate0.001loss 震荡时降到 0.0001
batch_size64显存不够降到 16

这张表是我自己跑过十几组参数后总结的,不一定最优,但能让你少走弯路。真正调参时,先固定其他参数,只动一个,看验证集 loss 的变化方向。

6.4 一个容易忽略的细节:随机种子

深度学习结果有随机性,同一个脚本跑两次 MAE 可能差 2-3。毕设里如果老师让你复现,结果对不上就尴尬了。在脚本开头固定随机种子:

import random import torch import numpy as np def set_seed(seed=42): 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,追加模式。毕设后期要对比十几组实验,没有日志根本记不住哪组是哪组。这个习惯帮我省了至少两次重跑的时间。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询