蔬菜价格预测毕设实战:LSTM时序建模从数据清洗到调参全攻略
简介基于长短期记忆网络LSTM的蔬菜价格预测毕业设计项目涵盖完整源码、项目说明与数据集面向计算机相关专业正在准备毕业设计或课程设计的学生也适合需要实战练习的初学者。项目经导师指导并通过评审获得九十八分好评整体方案具有较高参考价值。资源共一百八十一个文件包含一百四十二个蔬菜价格历史数据文件、二十五个Python源码脚本、十个编译文件、三个项目说明文档以及一个Markdown阅读说明压缩包仅一点八六MB十分轻量。数据覆盖菜心、西红柿、青皮冬瓜等多种常见蔬菜便于开展多序列对比分析源码模块涵盖数据读取、特征处理、模型搭建、训练评估与可视化代码注释清晰适合按步骤学习。目前已有二百七十一人学习/下载关注度较高。通过该资源可系统掌握长短期记忆网络在时间序列预测中的完整流程项目说明文档还提供了设计思路、调参细节和实验结果分析有助于快速复现并迁移至其他价格预测任务是毕业设计、课程设计及项目实战的实用参考资料。1. 蔬菜价格预测为什么非 LSTM 不可一个“序列问题”被当成静态回归的翻车现场我见过太多把蔬菜价格预测做成静态回归的毕设把日期、节假日、天气塞进随机森林测试集 R2 高达 0.9一画真实曲线才发现模型学的只是“把昨天的价格复制到今天”。蔬菜价格本质上是一个时间序列前一周的批发价、节假日的脉冲需求、季节性的供应变化信息全部藏在时间顺序里。LSTM 通过遗忘门和输入门让模型自己决定记住哪几天的价格形态这正是这个方向上更靠谱的选型。标题里这份源码通常包含 data、models、train.py、predict.py 和项目说明数据集看起来是个完整的高分毕设模板。这篇笔记不逐行复述源码而是按数据清洗、LSTM 结构、训练调参、避坑和项目交付的顺序带你把这个项目真正跑通并讲明白。2. 从业务到序列蔬菜价格数据采集、清洗与滑窗构造2.1 蔬菜价格数据的三个隐藏结构周期性、趋势性、节假日脉冲做蔬菜价格预测我建议你先别急着打开模型代码而是先把 csv 文件拖进 Excel 或 pandas 里看两遍。批发市场的每日报价数据通常包含日期、品种、最低价、最高价、均价偶尔还有交易量。题目里说的数据集无非就是这几种字段的 csv每天一条或每个品种每天一行。拿到手后你要做三件事确认时间粒度、确认价格单位、确认有没有缺失。数据里的三个隐藏结构决定了 LSTM 建模的走向。第一个是周期性。蔬菜价格有明显的周内周期商超周末集中采购会把周五周六的批发价抬高季节周期更明显夏季叶菜供应量大价格低冬天气温低运输成本高价格贵。第二个是趋势性。近两年化肥、燃油和人工成本上涨多数蔬菜品种的价格中枢是缓慢上移的模型要能把这个趋势和短期波动分开。第三个是节假日脉冲。春节前一周韭菜、芹菜、黄瓜的价格可能翻倍节后三天又跌回去。这类脉冲在样本里出现次数极少模型容易当成噪声忽略掉。常见做法是额外构造“距最近法定节假日天数”这个特征让模型有机会捕捉脉冲。我见过很多人在这一步省略直接把原始价格做归一化就开练。结果模型在平常日子的预测很准一到节假日附近就严重偏颇。你可以在数据预处理里加一个特征工程函数用 pandas 的dt.dayofweek生成星期几再加一列节假日倒计时这样 LSTM 输入特征从 1 维变成 3 维预测效果会有肉眼可见的提升。新手常犯的错误是过早开始建模其实这块时间最值得花。2.2 把 csv 变成模型能吃的时间窗滑窗参数与归一化先做异常值清洗。批发市场的均价偶尔会混入错误录入值比如某个品种突然显示 0.5 元/斤而前后两天都是 3.2 元/斤。我常用一个简单的滑动中位数过滤器超过阈值就替换。代价是可能把真实的短期跳价抹掉所以阈值要按品种调。import numpy as np def reject_price_outlier(series, ratio0.25, window5): 通过前后窗口的中位数识别并替换价格毛刺 clean series.copy() for i in range(window, len(series) - window): window_values np.r_[series[i-window:i], series[i1:iwindow1]] med np.median(window_values) if med 0 and abs(series[i] - med) / med ratio: clean[i] med return clean这里的ratio0.25适合价格波动大的叶菜像土豆这种波幅小的品种建议改成0.15。window5表示用前后各 5 天的中位数作为参照。注意春节、中秋前的暴涨是真实信号不是脏数据这个过滤器只处理那些“前一天还在 3 元第二天变 0.5 元”的录入错误。接着做滑窗。LSTM 不直接吃单天的价格它吃“一段连续的价格序列”。核心参数是seq_len也就是用过去多少天预测下一天。我一般先设 7代表一周然后依次试验 10、14、30。如果你的数据是按天记录顺序绝对不能乱。import pandas as pd from sklearn.preprocessing import MinMaxScaler def load_and_prepare(csv_path, date_coldate, price_colprice): df pd.read_csv(csv_path, parse_dates[date_col]) df df.sort_values(date_col).reset_index(dropTrue) series df[price_col].to_numpy(dtypenp.float32) scaler MinMaxScaler(feature_range(0, 1)) scaled scaler.fit_transform(series.reshape(-1, 1)).flatten() return df, scaled, scaler def make_sequences(scaled, seq_len7): X, y [], [] for i in range(len(scaled) - seq_len): X.append(scaled[i:iseq_len]) y.append(scaled[iseq_len]) # 预测下一天 return np.array(X, dtypenp.float32), np.array(y, dtypenp.float32)这段代码把按天排列的价格转换成“过去 7 天、预测第 8 天”的样本。MinMaxScaler把价格压到 0 到 1 之间避免数值范围过大导致梯度异常。注意这里为了演示简单fit_transform用了全量数据但你在正式写项目时应该把scaler.fit放在训练集上否则验证集和测试集的信息会泄漏到训练阶段这一点在第 4 章会重点展开。seq_len是唯一的输入结构超参数。太小模型只看得到一两条价格抓不住周周期太大比如 30 天模型参数需求变大而蔬菜价格受突发因素影响大过长的历史反而变成噪声。做对比实验时保持其它参数不变只改seq_len看验证集 MAPE 变化比盲目增加 LSTM 层数有效得多。2.3 数据集划分的讲究不洗牌按时序切普通机器学习项目会把数据随机切分成训练集、验证集、测试集但时间序列绝对不能洗牌。洗牌等于告诉模型“未来数据可以出现在训练里”验证集精度会好看到不真实一到实际向前预测就崩。正确做法是按时间顺序切通常用前 70% 做训练中间 15% 做验证最后 15% 做测试。也可以把训练和验证都放在前 80%最后 20% 模拟“未来”。比如你有 800 天的价格数据seq_len7经过滑窗后样本大约 793 个。先对原始价格做切分再对每个区间分别做滑窗和归一化不要先滑窗再切否则会造成跨区间的样本重叠。代码可以写成from sklearn.preprocessing import MinMaxScaler def split_and_scale(series, train_ratio0.7, val_ratio0.15): n len(series) train_end int(n * train_ratio) val_end int(n * (train_ratio val_ratio)) train, val, test series[:train_end], series[train_end:val_end], series[val_end:] scaler MinMaxScaler(feature_range(0, 1)) scaler.fit(train.reshape(-1, 1)) # 只用训练集 fit train_scaled scaler.transform(train.reshape(-1, 1)).flatten() val_scaled scaler.transform(val.reshape(-1, 1)).flatten() test_scaled scaler.transform(test.reshape(-1, 1)).flatten() return train_scaled, val_scaled, test_scaled, scaler关键点在于scaler.fit(train)而不是对整个series做 fit。train_ratio和val_ratio是控制数据块大小的参数数据量越少验证集和测试集占比要越低避免验证集只有几十天、统计噪声太大。做完这一步数据已经可以送进模型了。把这个处理流程固化成一个data_preprocess.py以后换数据集只需要改文件名和列名这是项目里最值得复用的部分。3. 用 PyTorch 搭建 LSTM 价格预测模型结构选型与关键参数3.1 为什么用 LSTM 而不是 RNN/GRU门控机制与长序列记忆蔬菜价格预测是典型的单变量时间序列预测输入是过去 N 天的价格输出是未来一天的价格。RNN 虽然能处理序列但反向传播时梯度容易消失或爆炸10 天前的价格信息传不到今天。LSTM 在隐藏层里加了遗忘门、输入门、输出门能够决定哪些历史信息要保留、哪些要丢弃。对价格数据来说昨天价格很重要一周前也很重要模型需要自己学出这个门控权重。GRU 是 LSTM 的简化版参数少、训练更快在数据量只有几百条时我有时会首选 GRU。但很多毕设框架里默认给 LSTM所以这篇笔记以 LSTM 为主。如果你跑下来发现训练数据少到过拟合把模型里的 LSTM 层换成 GRU 也是一行代码的事。PyTorch 里就是nn.LSTM换成nn.GRU输出结构完全一样这种做法在数据集不足的场景下值得一试。3.2 最小可运行的 LSTM 模型代码输入形状与层数决策下面是基于 PyTorch 的 LSTM 模型定义适合单品种价格预测。输入形状是(batch_size, seq_len, num_features)num_features为 1 时就是单变量价格。这里的hidden_size表示 LSTM 内部隐藏状态维度num_layers是堆叠层数。import torch import torch.nn as nn class PriceLSTM(nn.Module): def __init__(self, seq_len7, hidden_size64, num_layers2, output_size1, dropout0.2): super().__init__() self.lstm nn.LSTM( input_size1, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout if num_layers 1 else 0 ) self.regressor nn.Sequential( nn.Linear(hidden_size, 32), nn.ReLU(), nn.Linear(32, output_size) ) def forward(self, x): # x: (batch, seq_len, 1) out, _ self.lstm(x) # 取最后一个时间步的输出 last out[:, -1, :] # (batch, hidden_size) return self.regressor(last)input_size1表示每个时间步输入一个价格数值。batch_firstTrue让输入维度按(batch, seq_len, features)组织这是 PyTorch LSTM 的常见用法别搞错。out[:, -1, :]取出序列最后一个时间步的隐藏状态再通过全连接层映射到预测价格。这里的regressor可以理解成一个价格解码器把记忆压缩成的向量重新翻译成价格。hidden_size和num_layers直接决定模型容量对于几百条样本hidden_size设 32 到 64 就够了num_layers设 2 层比较稳超过 3 层在数据量小的时候只会更差。dropout在多层 LSTM 的层间生效防止过拟合。如果你拿到的是 Keras 写的源码结构同样如此model.add(LSTM(hidden_size, input_shape(seq_len, 1)))然后接Dense(1)。PyTorch 版的优势是训练循环完全可控调参观察梯度更方便。3.3 训练循环里的三个必调参数学习率、序列长度、batch_size训练循环可能是最容易写错的部分。下面是一个标准的训练代码包含 loss、优化器、梯度裁剪和进度打印。import torch.optim as optim def train_model(model, train_loader, val_loader, epochs50, lr0.001): criterion nn.MSELoss() optimizer optim.Adam(model.parameters(), lrlr) scheduler optim.lr_scheduler.ReduceLROnPlateau(optimizer, modemin, factor0.5, patience5) for epoch in range(epochs): model.train() total_loss 0 for xb, yb in train_loader: optimizer.zero_grad() pred model(xb) # xb: (batch, seq_len, 1) loss criterion(pred.squeeze(), yb) loss.backward() # 梯度裁剪防止 LSTM 梯度爆炸 nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() total_loss loss.item() * xb.size(0) model.eval() val_loss 0 with torch.no_grad(): for xb, yb in val_loader: pred model(xb) val_loss criterion(pred.squeeze(), yb).item() * xb.size(0) val_loss / len(val_loader.dataset) scheduler.step(val_loss) if epoch % 10 0: print(fepoch {epoch} | train_loss {total_loss / len(train_loader.dataset):.6f} | val_loss {val_loss:.6f})学习率是最先要调的参数。LSTM 对学习率很敏感0.001是稳妥起点如果 loss 震荡不降降到0.0005或0.0003。batch_size、学习率、seq_len三者不是独立的batch_size越大梯度估计越稳但价格数据样本量小batch_size设 16 到 32 就够设太大一步就把模型带到沟里去。序列长度seq_len前面说了7 到 14 比较合理超过 30 在单变量价格上通常不划算。ReduceLROnPlateau是个好用的学习率调节器它会在验证集 loss 不再下降时自动把学习率减半省去手动干预。训练完之后预测时一定要把结果从 0 到 1 的归一化空间还原成人能看懂的价格predicted_price scaler.inverse_transform(pred.detach().numpy().reshape(-1, 1))如果不做这一步画出来的预测曲线纵轴全是 0 到 1 的小数答辩老师一眼看出问题。4. 评估与调参的避坑指南预测平移、过拟合与数据泄漏这一章我从实际跑数据集翻车最多的地方挑 4 个每一条都能在你自己的训练日志里找到对应现象。避坑不是让你少写代码而是让你在模型不听话时知道先查哪里。4.1 现象预测曲线比真实曲线“慢半拍”在测试集上画出预测与真实价格最常见的现象是预测曲线比真实曲线右移一天看起来像是把昨天的价格搬到了今天。原因是模型学到的是“用当前价格近似下一天价格”。用 MSE 作为 loss只要价格变化平缓复制上一天的值得到的 loss 比预测波动更小模型就偷懒了。解决方法是引入差分特征将目标从“预测绝对价格”改成“预测价格变化量”。也就是 y 不再是price[t1]而是price[t1] - price[t]然后预测值等于当前价格加变化量。另一个办法是在评估指标里加入方向准确率只有上涨下跌方向对了才算准单纯 MSE 低没有意义。你可以在测试集上计算一下方向准确率如果只有 50% 左右说明模型并没有真正学到价格变化规律只是在“抄作业”。4.2 现象loss 降了但预测结果全是均值训练集的 loss 在下降验证集预测几乎是一条直线接近价格的均值。原因是样本里上涨和下跌的样本各一半MSE 的局部最优就是预测均值。加上数据集太小模型容量大也会出现先拟合均值再慢慢拟合波动的现象。解决尽量增加数据量或者把损失函数换成 Huber Loss它结合了 MSE 和 MAE对离群点更鲁棒也能缓解均值回归。还可以对价格取对数再训练把指数增长趋势变成线性。另外把 epoch 设大一点比如 100 到 200配合早停很多模型在原 loss 下看似收敛实际还能继续拟合波动。不要一开始就认定模型不行先把训练轮数拉长看走势。4.3 现象验证集精度高真实环境一跑就翻车原因是数据泄漏。最常见的泄漏是MinMaxScaler.fit用了全量数据包含验证集和测试集相当于让模型提前看到了未来数据的最大最小值。另一种泄漏是滑窗造成样本重叠训练集最后一个样本和验证集第一个样本只差一天验证集不再是干净的未来数据。解决严格按第 2 章的方式先切分再 fit 训练集再 transform 其它集合。检查方法很简单打印模型在训练集和测试集上的 MAPE如果测试集远差于训练集先查数据泄漏再怀疑模型结构。正确的验证方式是滚动预测每次只用当前窗口之前的真实值做未来预测模拟线上场景而不是一次性把所有样本喂进去。这一步是毕设答辩时最容易被打穿的地方一定要自己先查一遍。4.4 现象训练到一半 loss 变成 NaNloss 突然变成 NaN或者出现 inf最常见原因是梯度爆炸。LSTM 在长序列上梯度范数可以指数增大学习率过大、序列过长、数据中存在极端异常值时都会触发。解决思路在optimizer.step之前加梯度裁剪nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)把学习率调低到0.0003检查原始数据是否有极大离群点或归一化后是否有超出[0,1]的值。还有一个容易被忽略的点如果模型输出层没有做激活限制预测值可能超出训练数据范围反归一化时出现极端数。可以在输出层后加torch.clamp限制到合理区间。另外batch 里如果混入了 NaN 的价格值也会让 loss 直接崩掉所以数据加载后要调用np.isnan(series).any()检查一遍。5. 从毕设到可交付命令行入口、三张图与项目说明书的组织5.1 把训练、预测、评估拆成三个入口用 argparse 而不是改代码一份需要反复跑的 LSTM 预测源码最怕每次调参都去改代码文件。我一般会把项目拆成train.py、predict.py、evaluate.py共用data_loader.py和model.py。train.py只负责训练把模型保存为 checkpointpredict.py加载模型对最新一段价格做未来预测evaluate.py在测试集上计算指标。用 argparse 统一传参这样你只需要在命令行里改参数不用动源码。# train.py 简化版 import argparse from data_loader import load_data, make_sequences, split_and_scale from model import PriceLSTM parser argparse.ArgumentParser() parser.add_argument(--data, defaultdata/vegetable_price.csv) parser.add_argument(--seq_len, typeint, default7) parser.add_argument(--epochs, typeint, default100) parser.add_argument(--batch_size, typeint, default32) parser.add_argument(--lr, typefloat, default0.001) parser.add_argument(--save, defaultmodels/lstm_checkpoint.pt) args parser.parse_args()运行命令比如python train.py --data data/price.csv --seq_len 14 --lr 0.0005每次实验都有迹可循。这些参数要记录到项目说明文档里否则一周后你自己都不知道当时跑出最优 loss 用的是什么参数。源码的可维护性往往从命令行参数开始这一点能看出你是否具备工程化意识。5.2 用 Matplotlib 输出三张图损失曲线、预测对比、残差分布毕设答辩最看重可视化。评估阶段至少要输出三张图训练与验证损失曲线、测试集预测价格对比、残差分布直方图。损失曲线能说明模型训练是否收敛预测对比图展示跟踪效果残差分布展示误差是随机还是有系统偏差。一张标准图可以这样画import matplotlib.pyplot as plt def plot_prediction(true, pred, titlePrediction vs True): plt.figure(figsize(10, 4)) plt.plot(true, labelTrue) plt.plot(pred, labelPred) plt.title(title) plt.xlabel(day) plt.ylabel(price (yuan/kg)) plt.legend() plt.grid(alpha0.3) plt.savefig(results/prediction.png, dpi150)注意画图时把预测结果通过scaler.inverse_transform还原成原始价格不然纵轴是 0 到 1 的缩略值答辩老师一眼看出问题。三张图放到results目录项目说明里附上每张图的解读比长篇大论更有效。残差分布尤其重要如果残差叠加在趋势上说明模型漏掉了某个时间特征如果残差随机散布在零附近说明模型已经学到了能学到的部分。5.3 项目说明文档里必须写清的前置条件与数据字典标题里带“项目说明”的压缩包通常文档是老师打分的重点。项目说明至少要包括四块环境依赖python 版本、torch 版本、pandas/numpy/matplotlib、数据字典每个字段叫什么、单位是什么、时间跨度、运行顺序先 data_preprocess 再 train 再 evaluate、还有一次完整的实验结果日志。别小看环境依赖很多同学代码里用了新版本的 PyTorch API换台机器跑不了。我会在requirements.txt里固定版本比如torch2.0.1、pandas1.5.3这样别人拿到源码后只需要pip install -r requirements.txt就能复现。数据字典尤其重要如果数据集是“日期、品种、均价”三列必须在文档里写清均价单位是“元/公斤”还是“元/斤”不然别人用你的模型输出做业务价格差了一倍根本不落地。目录结构通常如下data/ vegetable_price.csv models/ lstm_checkpoint.pt results/ loss_curve.png prediction.png residual.png scripts/ data_preprocess.py train.py evaluate.py predict.py requirements.txt README.md这样的组织方式让老师能清清楚楚看到训练、评估、结果三个过程也方便你自己后面继续调。6. 更进一步把单步预测改成多步滚动预测并给模型一个“后悔药”上面的流程做出来的是“预测下一天”的模型。但真实备货场景里你往往需要知道未来 3 天、7 天的价格走势。最简单的方式是滚动预测把t1的预测值当成已知价格拼到窗口末尾再预测t2、t3。误差会随步数累积所以滚动步数不要超过 3 天。另一种结构是 seq2seq编码器读过去序列解码器逐点输出未来但数据量小的时候非常容易过拟合效果反而不如滚动预测。我习惯给模型加一个“后悔药”训练时用 Teacher forcing也就是每一步都喂真实值但测试时模型只能吃自己的预测值这中间的分布不一致会导致误差漂移。一个简单技巧是训练时随机把真实值或预测值作为下一步输入类似 scheduled sampling让模型提前适应误差环境。实现起来也很直接解码循环里用random.random()决定用真实值还是预测值概率从 1 逐渐降到 0.5。验证多步预测时看单一 MAPE 不够还要看方向准确率和不同步长的误差曲线。我现在的习惯是每跑一组实验就存一份模型和参数 json并且把测试集的预测残差画出来不然调参到第三天最优模型已经在内存里消失了。无论你的毕设是追求高分还是真正想投入生产把单步变多步这一步补上项目的完整度和说服力都会明显上一档。数据量不大的时候加特征比换模型更稳参数记录比调参技巧更重要。希望这些踩坑记录能帮你少走几段弯路也希望能让你在这个方向上做出一个真正经得起追问的 LSTM 价格预测项目。本文还有配套的精品资源点击获取