ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

LSTM共享单车使用量预测实战:时序数据预处理与模型调优全指南

LSTM共享单车使用量预测实战:时序数据预处理与模型调优全指南 简介基于长短期记忆网络LSTM的共享单车使用情况预测项目是一份面向高校学生与初学者的完整课程设计/大作业参考。内容覆盖数据预处理、多维度可视化分析、LSTM模型构建与预测结果对比适合人工智能、数据科学、计算机等专业用于课设、毕设或项目初期验证。压缩包共32个文件约8.06MB包含4个Python脚本模型训练、预测、可视化、8个CSV数据文件、13张分析图表、H5模型文件及说明文档目录结构清晰配合README和依赖清单可快速复现。已有141人学习下载。项目附带完整共享单车骑行数据按时和按天统计提供训练/验证/测试集划分与标准化参数并给出温度、湿度、风速、季节、工作日等多因素影响分析图模型损失曲线与预测对比图也一并包含。代码均测试通过适合作为高分作业参考下载后也可私聊询问运行问题支持远程教学。1. 用 LSTM 预测共享单车使用量这个高分大作业到底难在哪如果你的课程设计或毕业设计选了“基于 LSTM 的共享单车使用情况预测”那恭喜你这是个典型到不能再典型的时间序列回归任务。它不像图像分类那样需要调一堆数据增强也不像 NLP 那样要处理词表它最核心的挑战就三个把原始骑行记录整理成模型吃得下的序列、把 LSTM 的超参数调到不欠拟合也不过拟合、以及把预测结果用一张像样的图展示出来让答辩老师点头。这个标题里带的 Python 源码、文档说明和数据其实就是把这三件事打包成一份可以交作业的工程。本文不假设你有现成源码就按你手头有一份 CSV 骑行记录、一台能跑 PyTorch 或 TensorFlow 的电脑来展开从数据构造讲到模型调优最后落到你答辩时最可能被问的细节。适用人群很明确正在做课设、需要快速出成果但不想只调库跑通的本科生以及刚接触时序预测、想弄明白 LSTM 每一步在干什么的研究生。读完你应该能独立完成数据预处理、滑窗构造、模型训练、反归一化和绘图展示并知道哪些地方容易翻车、为什么翻车。2. 先把数据收拾干净从原始订单到能训练 LSTM 的序列2.1 原始数据长什么样时间戳、站点和骑行时长是三个关键字段共享单车数据集通常来自政府开放平台或企业 API字段一般包括开始时间、结束时间、起始站点、结束站点、骑行时长、车辆类型等。但你做 LSTM 预测需要的是“按时间聚合后的使用量序列”不是一条条订单记录。也就是说你要先把数据从明细表压缩成“每半小时 / 每小时 / 每天有多少次骑行开始”这样的统计量。常见做法是用 pandas 的resample按时间粒度聚合。之前我带过一个学生他直接把 50 万条订单丢进模型跑了一晚上没出结果其实他连groupby都没做。记住LSTM 吃的是序列不是明细。先聚合再滑窗才是正确顺序。import pandas as pd # 假设原始数据有两列start_time 和 bike_id其他列暂时不用 df pd.read_csv(bike_trips.csv, parse_dates[start_time]) df.set_index(start_time, inplaceTrue) # 按小时统计骑行开始次数 hourly df[bike_id].resample(1h).count().to_frame(count) # 补上缺失的小时段避免时间轴空洞 hourly hourly.asfreq(1h, fill_value0) hourly.head()这段代码里resample(1h)是核心它把任意时间戳归入所在小时count()统计该小时内的订单数量。asfreq(1h, fill_value0)用来填补那些没有订单的小时——比如凌晨三点可能一条记录都没有但模型需要连续的序列填空 0 比不填更合理否则时间间隔不一致会让 LSTM 误以为中间有时间跳跃。如果你拿到的是半小时粒度数据把1h改成30min即可。注意resample默认左闭右开意味着 09:00 的记录归入 09:00 那个桶这点在答辩时经常被问提前想好。2.2 滑窗构造为什么不能用全部数据直接训练LSTM 的输入是一个三维张量(batch_size, time_step, feature_dim)。time_step是你用过去多少个小时去预测下个小时。例如用过去 24 小时预测下一小时那time_step24feature_dim1只用历史使用量。如果还想加入天气、温度或是否为工作日feature_dim就变成对应列数。构造滑窗时常见的错误是让相邻窗口重叠过多导致数据泄漏。严格来说训练集、验证集、测试集必须按时间先后切分不能用随机打乱。因为时序数据有自相关性随机打乱等于让模型看到未来数据测试集上的效果会虚高。import numpy as np def create_sequences(data, time_step24, pred_step1): X, y [], [] for i in range(len(data) - time_step - pred_step 1): X.append(data[i:itime_step]) y.append(data[itime_step:itime_steppred_step]) return np.array(X), np.array(y) # 假设已经有归一化后的序列 values values hourly[count].values.astype(float32) # 先切分再归一化更严谨但常见做法是先归一化再切分 from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler() values_scaled scaler.fit_transform(values.reshape(-1, 1)).flatten() train_size int(len(values_scaled) * 0.7) val_size int(len(values_scaled) * 0.15) train_data values_scaled[:train_size] val_data values_scaled[train_size:train_sizeval_size] test_data values_scaled[train_sizeval_size:] X_train, y_train create_sequences(train_data, time_step24, pred_step1) X_val, y_val create_sequences(val_data, time_step24, pred_step1) X_test, y_test create_sequences(test_data, time_step24, pred_step1) # 调整维度到 (样本数, time_step, 1) X_train X_train.reshape(-1, time_step, 1) X_val X_val.reshape(-1, time_step, 1) X_test X_test.reshape(-1, time_step, 1)这段代码有个容易踩坑的点构造序列时range(len(data)-time_step-pred_step1)决定了你从原始数据里取多少个窗口。如果你漏掉pred_step的偏移最后几个样本会越界。另外MinMaxScaler应该在构造序列之前对整体数据拟合但这里有个细节——如果在切分前就 fit那么 test 数据的信息已经参与了 scaler 的参数计算严格上算轻微泄漏。对于课设作业通常可接受但如果追求严谨应该在训练集上 fit再用同一个 scaler 转换验证集和测试集。这个差异值得你在文档说明里写一笔能加分。2.3 要不要加天气和日期特征模型的第二根支柱只用历史使用量预测未来本质上是用过去的周期性推断未来。共享单车有明显的早高峰、晚高峰和周末效应所以光靠使用量序列本身也能拟合出大概形状。但如果你想预测某天突如其来的雨天骑行量骤降那纯序列模型做不到。这时候需要外生特征比如温度、湿度、风速、是否节假日。加特征的方式是在拼接滑窗时把每个时间步的气象数据作为附加列拼在历史使用量后面。这样feature_dim从 1 变成 1 气象特征数。模型在每个时间步看到的是“那一小时的使用量 那一小时的气温”。# 假设 weather_df 有温度列索引与 hourly 对齐 merged hourly.join(weather_df[[temp]], howleft) merged[temp] merged[temp].fillna(methodffill) features merged[[count, temp]].values.astype(float32) # 每个特征单独归一化 scaler_count MinMaxScaler() scaler_temp MinMaxScaler() features[:, 0:1] scaler_count.fit_transform(features[:, 0:1]) features[:, 1:2] scaler_temp.fit_transform(features[:, 1:2])这里用fillna(methodffill)处理气象站缺失值是实战中不得已的办法。注意不要让归一化把两列混在一起MinMaxScaler会对每一列独立计算范围但不能在 2D 数组上直接用fit_transform对单列操作容易广播出错。上面的切片写法保持了二维结构比较稳妥。加了外生特征的最大好处是模型能学到“下雨 低温 → 使用量下降”的组合模式但这要求你的气象数据时间粒度和骑行数据一致且没有大段缺失。如果数据里根本没有天气字段那也别硬造直接用纯使用量序列也能交一份合格的作业只是性能天花板低一些。3. 把 LSTM 模型搭起来PyTorch 实现、参数选择和训练流程3.1 为什么选 LSTM 而不是普通 RNN 或 TransformerLSTM 引入门控机制解决了长序列梯度消失问题。共享单车使用量的周期一般是 24 小时和 7 天要用过去 24 小时预测下一小时普通 RNN 在序列长度超过 20 时梯度已经很难传到最前面LSTM 的遗忘门和输入门可以更好地保持长期记忆。对比 Transformer它的计算复杂度更高需要更多数据才能发挥优势课设数据量通常就几千到几万小时LSTM 更合适。而且 LSTM 的参数量小CPU 上几分钟就能训练完调试成本低。PyTorch 里实现 LSTM 非常简洁。nn.LSTM接受input_size、hidden_size、num_layers三个主要参数输出是output, (h_n, c_n)。你要预测的是下一个时刻的具体数值所以取最后一个时间步的隐藏状态output[:, -1, :]接一个全连接层。import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size1, hidden_size32, num_layers2, output_size1): super().__init__() self.lstm nn.LSTM(input_size, hidden_size, num_layers, batch_firstTrue) self.fc nn.Linear(hidden_size, output_size) def forward(self, x): # x shape: (batch, seq_len, input_size) out, _ self.lstm(x) # 取最后一个时间步的输出 last out[:, -1, :] y self.fc(last) return y逻辑说明batch_firstTrue让输入维度变成(batch, seq, feature)这比默认的(seq, batch, feature)更直观不容易搞混维度。out[:, -1, :]表示对于每个样本取序列最后一个位置的隐藏输出。hidden_size是 LSTM 内部记忆容量设 32 到 64 通常够了太大容易过拟合。num_layers2表示堆叠两层 LSTM能增强模型表达能力但也会增加训练时间和过拟合风险。如果数据只有几千个样本建议先从 1 层试起不行再加层。损失函数用nn.MSELoss均方误差因为这是回归任务。3.2 训练循环里那几个容易写错的细节训练 LSTM 最关键的坑是初始化隐状态。PyTorch 的nn.LSTM如果不传h_0, c_0默认初始化为零向量这在大多数情况下没问题但如果你在循环里手动管理状态必须记得每批数据重新初始化否则上一批的隐状态会泄漏到下一批。另一个坑是梯度裁剪。时序模型容易在某个异常峰值上产生大梯度导致 loss 变成 NaN。加上clip_grad_norm_能有效避免这类翻车。import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset model LSTMPredictor(input_size1, hidden_size32, num_layers2, output_size1) loss_fn nn.MSELoss() optimizer optim.Adam(model.parameters(), lr0.001) # 将 numpy 数组转成 PyTorch Tensor train_dataset TensorDataset(torch.from_numpy(X_train), torch.from_numpy(y_train)) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue) epochs 50 for epoch in range(epochs): model.train() total_loss 0.0 for X_batch, y_batch in train_loader: optimizer.zero_grad() output model(X_batch).squeeze(1) loss loss_fn(output, y_batch) loss.backward() # 梯度裁剪防止梯度爆炸 nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() total_loss loss.item() if (epoch 1) % 10 0: print(fEpoch {epoch1:03d}, Loss: {total_loss/len(train_loader):.6f})这段代码里shuffleTrue是个容易争议的点。对时序数据打乱训练样本会破坏时间顺序但 LSTM 的每个样本内部已经包含了时间顺序样本与样本之间本来就是独立的窗口所以打乱不影响模型学到时间依赖反而有利于优化器收敛。但要注意验证集和测试集的 DataLoader 应设置shuffleFalse否则评估结果不稳定。batch_size64是权衡值。共享单车数据量不大64 是稳妥选择。如果显存不足或数据量小可以降到 32如果追求更快训练可以升到 128。lr0.001对 Adam 是常用默认值但如果你发现 loss 下降很慢或震荡剧烈可以试试 0.0005 或 0.003。3.3 验证和测试如何判断模型真的在学习而不是背答案训练过程中的 loss 下降只能说明模型在训练集上拟合了。你要在验证集上监控 loss如果验证 loss 先降后升就是过拟合信号。常见做法是保存验证 loss 最低时的模型参数用这个最佳模型去跑测试集。best_val_loss float(inf) best_model_state None model.eval() with torch.no_grad(): val_pred model(torch.from_numpy(X_val)).squeeze(1).numpy() val_loss loss_fn(torch.from_numpy(val_pred), torch.from_numpy(y_val)).item() # 测试阶段 test_pred model(torch.from_numpy(X_test)).squeeze(1).numpy() # 反归一化 test_pred_inv scaler.inverse_transform(test_pred.reshape(-1, 1)) y_test_inv scaler.inverse_transform(y_test.reshape(-1, 1))反归一化这一步非常关键。模型输出的 0~1 之间的值要还原成真实骑行次数才能算误差和画图。直接拿归一化后的值去算 RMSE 会得到一个小得离谱的数字但答辩老师一眼就能看出你没做 inverse transform。还有个细节验证集 loss 的计算方式要和训练集一致但不要做梯度裁剪也不要用model.train()模式。因为 LSTM 里的 dropout 层在训练和测试时行为不同你用model.eval()是必须的。如果你的模型加了很多 dropout漏了这句会导致每次预测结果不一样。4. 超参数和网络结构怎么调从学习率到 hidden_size 的实用经验4.1 学习率是最先要调的东西我见过太多同学一上来就把lr设成 0.01结果 loss 在 0.5 附近来回震荡不下降也不爆炸硬生生耗了半小时还以为是模型结构错了。先检查学习率如果 loss 在一两个 epoch 内几乎不动把lr降到 0.0005如果 loss 变成 NaN说明太大了降到 0.0001 重跑。Adam 自带自适应学习率但初始值还是有讲究。使用ReduceLROnPlateau是个好习惯当验证 loss 连续几个 epoch 不下降时自动把学习率乘以 0.5。这在时间序列预测里通常比固定学习率多涨几个点的精度。from torch.optim.lr_scheduler import ReduceLROnPlateau scheduler ReduceLROnPlateau(optimizer, modemin, factor0.5, patience5, verboseTrue) # 在每轮验证后调用 scheduler.step(val_loss)modemin表示监控指标越低越好patience5容忍 5 个 epoch 不下降再调整。损失曲线会变得平滑但训练时间增加适合做最终模型不适合快速试错。4.2 hidden_size 和 num_layers 怎么选这不是玄学但有规律。hidden_size控制 LSTM 记忆容量太小欠拟合太大过拟合。一个土办法是先用hidden_size16跑通流程看验证 loss 大概水平然后依次试 32、64、128画一条验证 loss 曲线取最低点对应的值。对于共享单车这种有一定周期性的数据32 或 64 通常足够。num_layers从 1 到 2 提升明显2 到 3 提升不大但训练时间翻倍。千万别用 4 层以上除非你有上万小时的数据。层数越多梯度在反向传播时越容易消失即使 LSTM 有门控也不能完全抵消。4.3 序列长度 time_step 到底该设多少标题里没写序列长度但作业通常默认用过去 24 小时预测下 1 小时。这个选择的价值在于能捕捉日内模式。如果你想预测的是次日的骑行量time_step可以设 1687 天捕捉周周期。但要小心序列越长训练样本越少因为滑窗会截掉开头部分。例如总共 3000 小时数据time_step24 能产生约 2976 个样本time_step168 就只剩 2832 个差距不明显但如果加上验证集构造时又从头部截掉一块有效训练样本可能不够。实际经验是先测试 time_step24 和 time_step48如果验证 loss 差不多选较小的那个因为训练更快。如果你要预测未来 24 小时那pred_step24输出维度变成 24模型最后一层 Linear 的输出要改成 24。这是一个大的结构调整否则 loss 维度对不上会直接报错。4.4 输出维度预测多步从单步到多步的两种做法多步预测有两种常见策略。第一种是直接多步输出让模型一次输出未来 N 个值修改output_sizeN损失函数在多个输出上取平均。第二种是递归预测先用单步模型预测下一个值然后把预测值当作输入再预测下下个值。后者会累积误差预测越远越不准前者训练更容易但 N 太大时模型负担重。共享单车课设一般只预测 1 到 24 小时通常选直接多步输出简单且误差可控。实现直接多步输出时模型改为class LSTMMultiStep(nn.Module): def __init__(self, input_size, hidden_size, num_layers, output_size): super().__init__() self.lstm nn.LSTM(input_size, hidden_size, num_layers, batch_firstTrue) self.fc nn.Linear(hidden_size, output_size) def forward(self, x): out, _ self.lstm(x) last out[:, -1, :] return self.fc(last) # 输出形状 (batch, output_size)然后标签y_train的形状也要从(batch, 1)换成(batch, output_size)。这个改动不大但你在答辩时要能解释为什么直接输出多步比递归方法更常用训练时目标值真实存在模型显式学习未来 N 小时的变化推理时不需要持续迭代推理速度快。5. 避坑与常见问题排查LSTM 预测共享单车最容易翻车的 5 个地方5.1 数据泄漏切分顺序不对导致测试集虚高现象测试集 loss 比训练集还低预测曲线几乎贴着真实值但换一段新数据就完全不准。原因构造序列时使用了全量数据的MinMaxScaler而且没有按时间切分测试集里混入了训练集时间段附近的样本模型“见过”类似序列。解决严格按时间比例切分原始数据再在训练集上fit_transformscaler对验证集和测试集只做transform。顺序是原始序列 → 切分 → 分别归一化 → 构造滑窗。如果发现课程数据文件已经固定了一份叫 test.csv 的独立文件那你直接分别归一化即可。5.2 隐状态遗忘模型在长序列推理时预测值趋近常数现象训练时 loss 正常但测试时预测的后半段几乎是一条水平线尤其长期预测时。原因模型只看到最近 24 小时或 48 小时窗口如果测试序列开头偏离历史模式模型只能依赖最后几个时间步的信息长时间递归预测时误差累积输出被拉向均值。解决检查是否用了递归多步预测。如果是改用直接多步输出。如果一定要递归可以在推理时加入真实观测值做 teacher forcing 的替代方案——即在每一步把真实前一个值喂给模型但这只在在线预测场景可行课设里用直接多步更稳。5.3 数值不稳定loss 出现 NaN现象训练到某个 epoch 后loss 突然变成nan后续无法恢复。原因输入数据里有 NaN 或无穷大或者学习率过高导致权重爆炸。共享单车原始数据里可能有缺失start_time的行聚合后出现空值fillna(0)没做好。解决在聚合后做hourly.isna().sum()检查有缺失就fillna(0)或前向填充。然后把lr降到 0.0001加梯度裁剪clip_grad_norm_(max_norm0.5)再重训。5.4 预测值全在 0 到 1 之间反归一化后曲线被压扁现象测试集 RMSE 很低但画出来预测曲线峰值比真实值矮很多低谷也高一点。原因用了 MinMaxScaler 归一化且训练时 loss 在归一化空间优化模型倾向于输出中间值以降低均方误差因为这样对大峰值和小谷值的惩罚最小。解决换用 StandardScaler 让数据均值为 0、方差为 1模型更容易预测相对波动。或者检查 min-max 缩放时是否有极端离群值把 99% 分位数之外的截断掉。对于共享单车数据偶尔会有体育赛事导致的 10 倍峰值这种离群值直接影响 scaler 参数。5.5 维度不匹配out[:, -1, :]取出来后在反向传播时报错现象nn.LSTM输出维度明明是(batch, seq_len, hidden_size)但直接接nn.Linear时报错说输入维度不对。原因batch_firstFalse时默认输出是(seq_len, batch, hidden_size)你的切片逻辑全错。或者你在forward里对out做了 squeeze 操作把 batch 维度挤掉了。解决在模型定义里显式写batch_firstTrue并且不要对out使用squeeze(0)。保持三维形状只取最后一维时间步。另外打印out.shape是一个又快又直观的排错方法别凭感觉写。6. 把预测结果画成让答辩老师满意的图并做误差分析6.1 三线图训练集、验证集、测试集上的预测对比图的质量直接影响评分。不要只画测试集应该把整个时间轴都画出来用不同颜色区分数据集。由于数据量大可以抽样展示比如只画最近 200 个小时的测试集对比。import matplotlib.pyplot as plt plt.figure(figsize(14, 5)) # 测试集真实值前 200 个点 plt.plot(y_test_inv[:200], labelTrue, color#333) # 测试集预测值前 200 个点 plt.plot(test_pred_inv[:200], labelPred, color#d9534f, alpha0.8) plt.legend() plt.title(Test set: LSTM vs True Bike Usage) plt.xlabel(Hour) plt.ylabel(Trip Count) plt.tight_layout() plt.savefig(prediction_result.png, dpi150)注意线条不要过密200 个点已经足够看出趋势。alpha0.8让预测线半透明以便重叠时容易观察。保存 PNG 时用dpi150放到论文里足够清晰。除了直观的图你还要算三个数值指标MAE、RMSE、MAPE。MAPE 在有零值或接近零值时会爆炸所以共享单车这种低谷可能为零的数据优先用 MAE 和 RMSE 汇报。from sklearn.metrics import mean_absolute_error, mean_squared_error mae mean_absolute_error(y_test_inv, test_pred_inv) rmse np.sqrt(mean_squared_error(y_test_inv, test_pred_inv)) # 对非零值计算 MAPE mask y_test_inv.flatten() 0 mape np.mean(np.abs((y_test_inv[mask] - test_pred_inv[mask]) / y_test_inv[mask])) * 100 print(fMAE: {mae:.2f} trips, RMSE: {rmse:.2f} trips, MAPE: {mape:.2f}%)6.2 误差分布图用直方图判断是否存在系统偏差如果预测值在高峰期系统性偏低说明模型没有充分学习峰值模式。画误差直方图可以看到误差集中在哪个区间。error y_test_inv.flatten() - test_pred_inv.flatten() plt.hist(error, bins50, color#5bc0de, alpha0.7) plt.xlabel(Prediction Error (trips)) plt.ylabel(Frequency) plt.title(Error Distribution on Test Set) plt.savefig(error_histogram.png, dpi150)如果误差直方图明显左偏负值多说明模型普遍预测偏大右偏则偏小。这时可以检查训练集最后的序列是否恰好处于上升期模型学到的惯性让它继续上升。6.3 一个进阶技巧用滑动平均做后处理平滑误报尖峰LSTM 预测有时会在某个点产生突刺尤其真实数据里有特殊事件时。比赛或课设中可以试试对预测结果做 3 小时的滑动平均它会小幅降低 RMSE但也会削弱峰值。我一般只在答辩展示时用它让曲线更平滑作业提交里同时给原始预测和平滑后两条曲线解释清楚各自优缺点让老师看到你的思考深度。项目文档说明里建议把上面这些图和指标放到一张结果页里辅以两段文字第一段描述模型结构和训练配置第二段解释误差来源并提出改进方向比如加入天气特征、使用 Attention LSTM、改时间粒度。这样做完技术含量和完成度都足够拿到高分。毕竟课设不只是跑通代码更重要的是你能围绕着这个 LSTM 预测任务把数据处理、模型选择、评估闭环讲完整。希望这篇笔记里踩过的坑能帮你少走弯路也祝你答辩顺利。本文还有配套的精品资源点击获取
返回列表