简介:这是一份面向计算机专业本科生的交通流量预测实战项目资源,适用于毕业设计、期末大作业及机器学习课程实践,聚焦Python环境下基于时序数据建模的智能交通分析场景。资源包含267个文件,主体为7个核心Python脚本(含完整注释)、7个CSV格式交通与气象原始数据集(如passenger_flow.csv、weather_utf8.csv)、8个预训练模型文件(.pth)、212张可视化结果图(.png)以及TensorBoard日志文件(.tfevents),整体压缩包仅21.96MB,轻量易部署。已有182人下载学习,项目采用端到端流程设计:从数据清洗、特征工程、LSTM/XGBoost等多模型对比,到Web界面展示预测结果,功能完整、界面美观、操作简洁。读者可直接运行复现高分毕设效果,获取带详细注释的代码逻辑、可复用的数据处理模板、模型调参记录及典型排错说明,特别适合零基础入门机器学习落地应用。
1. 这不是又一个“用 sklearn.fit() 预测明天车流”的玩具项目:它跑在真实交叉口数据上,带 TensorBoard 可视化、天气融合特征、多模型对比实验记录,毕业答辩前一周还能改出 A+ 成绩
你手头那份《基于LSTM的交通流预测》PPT里,训练损失曲线是不是平得像高速公路?验证集 MAE 卡在 120 辆/小时死活下不去?别急着删代码——这个资源包里,5 个events.out.tfevents.*文件就是你缺的「黑匣子日志」:它们不是占空间的垃圾,而是实打实跑过 3 轮超参搜索、4 种模型(Linear Regression / XGBoost / LSTM / GRU / TCN)的完整训练轨迹,TensorBoard 一开就能看到 loss 如何在第 87 epoch 突然下探、learning rate 在哪个 step 被 warmup 触发。passenger_flow.csv是某市主城区 32 个地磁线圈连续 90 天每 15 分钟的断面流量,weather_utf8.csv不是简单拼接温度湿度,而是把原始weather_raw.csv经过pandas.to_datetime()对齐时间戳、用sklearn.preprocessing.StandardScaler归一化后生成的;tmp.csv更关键——它是作者调试时临时保存的特征工程中间态,含 17 个衍生列:hour_sin,hour_cos,is_holiday,lag_1,lag_3,rolling_mean_7,weather_encoded……这些不是教科书里的抽象概念,是答辩老师问“你为什么选 lag=3 而不是 lag=5”时,你能立刻打开tmp.csv指着corr(passenger_flow, lag_3)和corr(passenger_flow, lag_5)的散点图给出的答案。适合谁?机械设计制造及其自动化专业想跨考智能交通方向的同学、计算机专业没接触过时序数据的新手、以及被导师催着“加点深度学习模块”却卡在数据对齐环节的毕设党——它不教你什么是 RNN,但让你在model.py第 42 行把nn.LSTM换成nn.GRU后,5 分钟内看到验证指标变化。
2. 数据准备与特征工程:从 raw CSV 到可喂入模型的 numpy array,绕不开的 4 步清洗 + 3 类特征构造
2.1 原始数据结构解析:为什么weather_raw.csv必须先转weather_utf8.csv
weather_raw.csv是典型的「科研现场数据」:编码为 GBK,时间列格式混乱(2023/05/01 08:00和2023-05-01 08:00:00混存),缺失值用-999填充而非NaN。直接pd.read_csv('weather_raw.csv')会触发UnicodeDecodeError,且pd.to_datetime()对混合格式报ParserError。正确做法是:
import pandas as pd # 步骤1:强制指定编码并跳过错误字节 weather_raw = pd.read_csv('weather_raw.csv', encoding='gbk', on_bad_lines='skip') # 步骤2:统一时间列格式(假设时间列名为 'time') weather_raw['time'] = pd.to_datetime(weather_raw['time'], errors='coerce') # 步骤3:将 -999 替换为 NaN 并插值(用前向填充+线性插值组合) weather_raw = weather_raw.replace(-999, pd.NA) weather_raw = weather_raw.interpolate(method='linear').fillna(method='ffill').fillna(method='bfill') # 步骤4:保存为 UTF-8 格式供后续使用 weather_raw.to_csv('weather_utf8.csv', index=False, encoding='utf-8')提示:
on_bad_lines='skip'是关键,避免因某行乱码导致整表读取失败;errors='coerce'让to_datetime()把无法解析的时间转为NaT,后续再用dropna(subset=['time'])清洗,比硬编码正则匹配更鲁棒。
2.2 时间对齐:让passenger_flow.csv和weather_utf8.csv在同一时间轴上跳舞
交通流数据是 15 分钟粒度,天气数据是小时粒度。不能简单merge,必须做「向上采样」:把天气数据按小时切片,然后用resample('15T')插值到 15 分钟。但注意——resample默认用mean(),而气温在 1 小时内变化平缓可用均值,但降雨量(rainfall_mm)是瞬时事件,用均值会稀释信号。解决方案是分列处理:
import pandas as pd # 读取已清洗的天气数据 weather = pd.read_csv('weather_utf8.csv', parse_dates=['time']) weather.set_index('time', inplace=True) # 对连续型变量(温度、湿度)用线性插值 cont_cols = ['temperature', 'humidity', 'pressure'] weather_cont = weather[cont_cols].resample('15T').interpolate(method='linear') # 对离散型/事件型变量(天气状况、降雨)用前向填充 cat_cols = ['weather_condition', 'rainfall_mm'] weather_cat = weather[cat_cols].resample('15T').fillna(method='ffill') # 合并并重置索引 weather_15min = pd.concat([weather_cont, weather_cat], axis=1).reset_index() # 读取客流数据(假设其 time 列也是 datetime) flow = pd.read_csv('passenger_flow.csv', parse_dates=['time']) # 双向 merge:以 flow 为主表,weather_15min 为辅表,用 time 列左连接 merged = pd.merge(flow, weather_15min, on='time', how='left')逻辑说明:resample('15T')将原数据重采样为 15 分钟频率,interpolate(method='linear')对连续变量做线性插值,fillna(method='ffill')对离散变量保持最近有效值。pd.merge(..., how='left')确保所有客流记录都被保留,即使某时刻无天气数据(此时对应列为 NaN,后续特征工程中会处理)。
2.3 特征构造实战:从tmp.csv逆向工程出 17 个有效特征的生成逻辑
tmp.csv是作者调试时导出的中间态,包含hour_sin,hour_cos,is_holiday,lag_1,lag_3,rolling_mean_7,weather_encoded等列。我们反推其生成过程(以passenger_flow.csv为基础):
import numpy as np import pandas as pd from sklearn.preprocessing import LabelEncoder # 假设 merged_df 已完成时间对齐 df = merged.copy() # 1. 时间周期性编码:避免模型认为 23 点和 0 点差距巨大 df['hour'] = df['time'].dt.hour df['hour_sin'] = np.sin(2 * np.pi * df['hour'] / 24) df['hour_cos'] = np.cos(2 * np.pi * df['hour'] / 24) # 2. 节假日标记(需外部节假日表,此处简化为工作日/周末) df['is_weekend'] = (df['time'].dt.dayofweek >= 5).astype(int) # 周六日为1 # 3. 滞后特征:捕捉时间依赖性(lag_1 = 前15分钟流量,lag_3 = 前45分钟) df['lag_1'] = df['passenger_count'].shift(1) df['lag_3'] = df['passenger_count'].shift(3) # 4. 滚动统计:7个时段(即105分钟)的均值,平滑短期波动 df['rolling_mean_7'] = df['passenger_count'].rolling(window=7).mean() # 5. 天气编码:对字符串型天气状况做标签编码(非 one-hot,因类别少且有序) le = LabelEncoder() df['weather_encoded'] = le.fit_transform(df['weather_condition'].fillna('Unknown')) # 6. 最终特征列(剔除原始时间、目标列、中间计算列) feature_cols = ['hour_sin', 'hour_cos', 'is_weekend', 'lag_1', 'lag_3', 'rolling_mean_7', 'weather_encoded', 'temperature', 'humidity'] X = df[feature_cols].dropna() # 删除含 NaN 的行(滞后特征和滚动均值会引入 NaN) y = df.loc[X.index, 'passenger_count'] # 对齐目标变量参数说明:shift(1)生成前 1 个时间步的值,rolling(window=7).mean()计算当前行及前 6 行的均值;dropna()是必须步骤,否则模型训练会报错;LabelEncoder适用于天气类别数 < 10 的场景,若类别过多(如全国城市天气),应改用TargetEncoder或CatBoostEncoder。
2.4 数据集划分:为什么不用train_test_split,而用TimeSeriesSplit
交通流是强时间序列,随机打乱会泄露未来信息。TimeSeriesSplit保证训练集时间早于测试集:
from sklearn.model_selection import TimeSeriesSplit import numpy as np # 按时间顺序排列(确保索引为时间或有 time 列) df_sorted = df.sort_values('time').reset_index(drop=True) X_sorted = df_sorted[feature_cols].dropna() y_sorted = df_sorted.loc[X_sorted.index, 'passenger_count'] # 使用 TimeSeriesSplit 进行 5 折时序划分 tscv = TimeSeriesSplit(n_splits=5) for train_idx, test_idx in tscv.split(X_sorted): X_train, X_test = X_sorted.iloc[train_idx], X_sorted.iloc[test_idx] y_train, y_test = y_sorted.iloc[train_idx], y_sorted.iloc[test_idx] # 在此处训练模型并评估 break # 仅演示第一折逻辑说明:TimeSeriesSplit生成的每一折,训练集都是测试集的「历史片段」,符合实际预测场景(用过去预测未来)。n_splits=5意味着将数据分为 5 段,依次用前 1、2、3、4 段训练,第 2、3、4、5 段测试,避免单次划分的偶然性。
3. 模型训练与 TensorBoard 日志解析:从events.out.tfevents.*文件读懂训练过程
3.1 TensorBoard 日志文件结构:5 个.tfevents文件对应什么实验
events.out.tfevents.1651977811.LAPTOP-7NJ3ONEL.21948.0这类文件名包含关键信息:
1651977811:Unix 时间戳(2022-05-07 14:43:31),即实验启动时间;LAPTOP-7NJ3ONEL:主机名,说明作者在个人笔记本运行;21948:进程 PID,同一时间可能有多个实验并行;.0:日志文件序号(主日志)。
这 5 个文件对应 5 组实验:
| 文件名时间戳 | 模型类型 | 关键超参 | 目的 |
|---|---|---|---|
| 1651748904 | Linear Regression | — | 基线模型,验证数据质量 |
| 1651749195 | XGBoost | n_estimators=200,max_depth=6 | 传统机器学习性能上限 |
| 1651977471 | LSTM | hidden_size=64,num_layers=2 | 深度学习基线 |
| 1651977811 | GRU | hidden_size=64,num_layers=2 | 验证 GRU 是否比 LSTM 更适合短时序 |
| 1651977889 | TCN | num_channels=[32,32,32],kernel_size=3 | 测试卷积时序网络 |
注意:
.tfevents文件不可直接阅读,必须用 TensorBoard 启动服务查看。它们记录了每个 epoch 的train_loss,val_loss,lr,mae,rmse等标量,以及模型图(Graph)、权重直方图(Histograms)。
3.2 启动 TensorBoard 查看训练轨迹:3 行命令定位性能拐点
# 步骤1:安装 tensorboard(若未安装) pip install tensorboard # 步骤2:进入项目根目录(含所有 events.out.tfevents.* 文件) cd /path/to/your/project # 步骤3:启动 TensorBoard,监听当前目录下的所有 events 文件 tensorboard --logdir=. --bind_all执行后终端输出类似TensorBoard 2.12.0 at http://LAPTOP-7NJ3ONEL:6006/ (Press CTRL+C to quit)。在浏览器打开http://localhost:6006,即可看到:
- SCALARS标签页:选择
val_loss曲线,观察是否在第 80–100 epoch 出现明显下降(如1651977471文件对应的 LSTM 实验); - GRAPHS标签页:点击模型名称,查看网络结构(确认 LSTM 层是否包含
dropout=0.2); - HISTOGRAMS标签页:查看
gru.weight_ih_l0的分布,若出现大量接近 0 的权重,说明梯度消失风险高。
逻辑说明:--logdir=.表示 TensorBoard 扫描当前目录下所有子目录及文件中的 events 文件;--bind_all允许局域网内其他设备访问(如用手机查看),生产环境慎用。
3.3 模型训练脚本核心:train.py中的 4 个关键配置段
train.py是训练入口,其核心配置决定模型成败:
# config.py(被 train.py 导入) class Config: # 1. 数据路径(必须与你解压后的实际路径一致) FLOW_DATA_PATH = "passenger_flow.csv" WEATHER_DATA_PATH = "weather_utf8.csv" # 2. 特征列定义(必须与 2.3 节生成的 feature_cols 完全一致) FEATURE_COLS = ['hour_sin', 'hour_cos', 'is_weekend', 'lag_1', 'lag_3', 'rolling_mean_7', 'weather_encoded', 'temperature', 'humidity'] # 3. 模型参数(修改此处即可切换模型) MODEL_TYPE = "GRU" # 可选:"Linear", "XGBoost", "LSTM", "GRU", "TCN" SEQ_LEN = 7 # 输入序列长度(7个15分钟 = 105分钟) PRED_LEN = 1 # 预测长度(1个15分钟) # 4. 训练超参(直接影响收敛速度和过拟合) BATCH_SIZE = 32 EPOCHS = 150 LEARNING_RATE = 0.001 EARLY_STOPPING_PATIENCE = 20 # 连续20轮 val_loss 不下降则停止参数说明:SEQ_LEN=7意味着模型每次输入过去 7 个时间步(105 分钟)的数据,预测下一个时间步;EARLY_STOPPING_PATIENCE=20是防止过拟合的关键,若验证损失在 20 轮内未改善,自动终止训练并保存最佳模型。
3.4 多模型对比结果:从tmp.csv和日志中提取的 MAE/RMSE 表
作者在tmp.csv的末尾添加了实验总结行,结合 TensorBoard 中各实验的val_mae最小值,整理出以下对比(单位:辆/小时):
| 模型 | 训练时间(分钟) | val_MAE(最小值) | val_RMSE(最小值) | 过拟合迹象(val_loss 上升轮次) |
|---|---|---|---|---|
| Linear Regression | 0.2 | 142.3 | 189.7 | 无(始终平稳) |
| XGBoost | 1.8 | 98.6 | 132.1 | 第 120 轮后 val_loss 缓慢上升 |
| LSTM | 22.4 | 85.2 | 114.8 | 第 95 轮后 val_loss 波动加剧 |
| GRU | 19.7 | 79.4 | 108.3 | 第 110 轮后趋于平稳 |
| TCN | 15.3 | 82.7 | 111.5 | 第 85 轮后轻微上升 |
结论:GRU 在精度(MAE/RMSE 最低)和稳定性(过拟合最晚出现)上综合最优,且训练速度比 LSTM 快 12%。这也是作者在答辩 PPT 中重点展示的模型。
4. 避坑:5 条血泪经验,每一条都来自events.out.tfevents日志里的报错快照
4.1 现象:ValueError: Expected input batch_size (32) to match target batch_size (31)
原因:数据集长度不能被BATCH_SIZE=32整除,DataLoader默认丢弃最后一组不足 32 的样本,但目标张量未同步裁剪。
解决:在dataset.py的__len__方法中,显式截断数据长度:
def __len__(self): return (len(self.data) - self.seq_len - self.pred_len) // self.batch_size * self.batch_size或更简单——在train.py中设置drop_last=True(PyTorch DataLoader 参数)。
4.2 现象:TensorBoard 中val_loss曲线剧烈震荡,振幅 > 50
原因:weather_utf8.csv中存在未被interpolate()修复的长段NaN(如连续 3 小时无气象站数据),导致rolling_mean_7生成大量NaN,模型输入含NaN引发梯度爆炸。
解决:在特征工程后增加NaN检查与硬填充:
X = X.fillna(X.mean()) # 对数值型特征用均值填充 X = X.fillna(0) # 对编码类特征(如 weather_encoded)用 0 填充 assert not X.isnull().values.any(), "X contains NaN after fill"4.3 现象:GRU模型val_loss持续下降但val_mae停滞在 110+
原因:MAE损失函数未被选用,train.py中误用nn.MSELoss()作为优化目标,而val_mae是额外计算的指标——模型在优化 RMSE,但你关心的是 MAE。
解决:修改train.py中的损失函数:
# 错误写法(默认 MSE) criterion = nn.MSELoss() # 正确写法(显式使用 MAE) criterion = nn.L1Loss() # 即 MAE4.4 现象:XGBoost训练时报ValueError: Input contains NaN, infinity or a value too large for dtype('float32')
原因:lag_1和lag_3特征在序列开头产生NaN,XGBoost不容忍NaN,而sklearn的SimpleImputer默认用mean填充,但lag_1的NaN应填充为 0(表示无前序数据)。
解决:对滞后特征单独处理:
# 在特征工程后 df['lag_1'] = df['lag_1'].fillna(0) df['lag_3'] = df['lag_3'].fillna(0) # 再对其他特征用均值填充 df[feature_cols] = df[feature_cols].fillna(df[feature_cols].mean())4.5 现象:tensorboard --logdir=.启动后页面空白,Network 显示 404
原因:TensorBoard 版本与 TensorFlow 不兼容(如 TF 2.12 + TB 2.15),或.tfevents文件被 Windows 资源管理器误删(显示为隐藏文件)。
解决:
- 降级 TensorBoard:
pip install tensorboard==2.12.0; - 在命令行用
dir /a(Windows)或ls -la(Linux/macOS)确认.tfevents文件真实存在; - 若仍失败,手动指定日志目录:
tensorboard --logdir=./logs/lstm_exp1(需先将.tfevents文件移入./logs/lstm_exp1/子目录)。
5. 模型部署与实时预测:用predict.py加载.pth模型,3 分钟实现「输入今天 08:00–09:45 数据,输出 10:00 流量」
5.1 模型保存与加载机制:.pth文件里到底存了什么
作者在train.py结尾调用torch.save()保存了两个关键对象:
# train.py 末尾 torch.save({ 'epoch': epoch, 'model_state_dict': model.state_dict(), # 模型权重(必需) 'optimizer_state_dict': optimizer.state_dict(), # 优化器状态(可选,用于继续训练) 'val_mae': best_val_mae, }, 'best_model_gru.pth')best_model_gru.pth是一个 Python 字典,model.state_dict()是核心——它包含所有可学习参数(如gru.weight_ih_l0,fc.weight)。部署时只需加载权重,无需重新定义模型结构。
5.2predict.py实战:从零构建预测流水线
predict.py是独立预测脚本,不依赖训练环境:
import torch import pandas as pd import numpy as np from model import GRUModel # 从 model.py 导入模型定义 # 1. 加载训练好的模型权重 model = GRUModel(input_size=9, hidden_size=64, num_layers=2, output_size=1) model.load_state_dict(torch.load('best_model_gru.pth')['model_state_dict']) model.eval() # 切换到推理模式(关闭 dropout/batchnorm) # 2. 构造输入数据(模拟实时场景:已有今天 08:00–09:45 的 8 个15分钟数据) # 假设你有一份新数据 new_data.csv,含 time, passenger_count, temperature, humidity... new_df = pd.read_csv('new_data.csv', parse_dates=['time']) # 执行与训练时完全相同的特征工程(复用 2.3 节代码) # ...(此处省略特征工程代码,必须与 train.py 一致)... # 3. 提取最后 SEQ_LEN=7 个时间步的特征(即 08:45–09:45 的 7 条记录) last_seq = X_scaled[-7:].values # X_scaled 是标准化后的特征矩阵 input_tensor = torch.tensor(last_seq, dtype=torch.float32).unsqueeze(0) # shape: [1, 7, 9] # 4. 模型推理 with torch.no_grad(): pred = model(input_tensor) # shape: [1, 1] predicted_flow = pred.item() print(f"预测 10:00 时段客流量:{int(predicted_flow)} 辆")逻辑说明:unsqueeze(0)增加 batch 维度(模型要求输入 shape 为[batch, seq_len, features]);torch.no_grad()禁用梯度计算,加速推理;pred.item()提取标量值。
5.3 输入数据标准化:为什么predict.py必须用训练时的scaler
训练时对特征做了StandardScaler(均值为 0,方差为 1),预测时必须用同一个 scaler,否则输入分布偏移导致预测失效。作者在train.py中保存了 scaler:
# train.py 中 from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_scaled = scaler.fit_transform(X) # ... 训练模型 ... # 保存 scaler import joblib joblib.dump(scaler, 'scaler.pkl')predict.py中必须加载它:
# predict.py 中 import joblib scaler = joblib.load('scaler.pkl') # 对 new_df 特征应用相同 scaler X_new_scaled = scaler.transform(X_new) # X_new 是新数据的特征矩阵提示:
joblib比pickle更适合保存 sklearn 对象,体积小且兼容性好。
5.4 预测结果可信度评估:3 个自检动作避免「玄学输出」
模型输出一个数字,但你需要判断它是否可信:
- 检查输入范围:
predicted_flow应在历史数据 5%–95% 分位数内(如历史客流 50–500 辆/15min,则预测值 600 就可疑); - 对比基线模型:用
LinearRegression对同一输入预测,若 GRU 结果比线性模型差 20% 以上,说明特征或模型异常; - 残差分析:计算
|predicted - actual|(若有真实值),若连续 3 次残差 > 150,触发告警并回退到 XGBoost 模型。
我一般会在predict.py末尾加一段校验:
# 假设 historical_flow 是历史客流数组 q5, q95 = np.percentile(historical_flow, [5, 95]) if not (q5 <= predicted_flow <= q95): print(f"警告:预测值 {predicted_flow} 超出历史合理范围 [{q5:.0f}, {q95:.0f}],建议人工复核") # 此处可插入告警逻辑(如发邮件、写日志)从那以后我每次部署新模型,都强制走一遍predict.py的输入校验 + 基线对比 + 残差阈值检查,哪怕多花 2 秒——毕竟答辩现场大屏上跳出一个 2000 辆/15min 的预测值,比代码报错更致命。希望帮到你。
本文还有配套的精品资源,点击获取