ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于深度学习LSTM的蔬菜价格预测实战:从数据清洗到模型部署

基于深度学习LSTM的蔬菜价格预测实战:从数据清洗到模型部署 简介这是一份基于深度学习LSTM实现蔬菜价格预测的Python毕业设计项目资源面向计算机相关专业正在准备毕设的学生以及需要项目实战练习、课程设计或期末大作业的学习者。项目经导师指导并获评审98分具备较强的参考价值。资源共181个文件包含142个CSV格式的蔬菜价格历史数据集、25个Python源码文件、10个pyc缓存文件以及3个docx说明文档和1个md说明文件压缩包整体仅1.86MB便于下载与部署。通过源码可完整了解LSTM时间序列预测流程包括数据预处理、模型构建、训练与评估等关键环节配套说明文档对项目结构、运行方式和实现思路做了梳理数据集覆盖本地菜心、西红柿等多种常见蔬菜便于直接运行验证。目前已有271人学习下载适合用于毕设参考、课设作业或作为入门深度学习的实战案例。1. LSTM蔬菜价格预测项目先看清这个毕设到底要解决什么问题一份“基于深度学习LSTM实现蔬菜价格预测”的毕设源码核心并不是让读者学会调包跑通一个demo而是讲清楚一件反直觉的事蔬菜价格这种看起来随机、受天气和节假日冲击很大的序列其实隐藏着明显的周周期与季节趋势LSTM恰恰是少数能在不手工构造特征的情况下抓住这种长期依赖的神经网络结构。整份材料由python源码、项目说明和数据集三块组成典型使用场景是拿到一段时间内某批发市场的蔬菜日度价格数据用过去的若干天价格预测未来一天或几天的均价最终输出可视化曲线与误差指标。适合正在做深度学习相关课程设计、毕业论文或者想入门时间序列预测但不想一上来就啃Transformer的读者。等你跑通这份代码再回头去看ARIMA、XGBoost的对比实验会更容易理解“序列记忆”这四个字的分量。2. 数据侧硬功夫从蔬菜批发数据到LSTM能吃的滑窗样本2.1 先看懂蔬菜价格的“脾气”为什么这个序列适合LSTM蔬菜价格不是平稳序列。以叶菜类为例夏季暴雨导致供应短缺价格会在两三天内翻倍随后又快速回落临近春节茄果类价格进入年度高点节后断崖下跌。这些特征叠加在一起形成了“短期剧烈波动 中期周周期 年度季节趋势”的多尺度结构。传统ARIMA类方法要求序列经过差分后近似平稳对这类多尺度叠加数据的建模成本很高而LSTM的门控机制可以自主决定在哪个时间尺度上保留信息这是它在价格预测任务里被反复选用的根本原因。另一个容易被忽略的点是蔬菜价格数据天然支持监督式学习。我们不需要额外标注只要把“过去N天的价格序列”作为输入、“未来第M天的价格”作为标签就能构造出大量训练样本。这比图像、文本任务的数据获取门槛低得多也是这份毕设适合作为深度学习入门项目的原因之一。但低门槛不代表不用处理原始数据里普遍存在品种名不统一、单位混用、节假日缺失等问题这些都会直接影响模型上限。2.2 数据清洗品种对齐、缺失值处理与归一化边界拿到csv之后第一件事不是写模型而是把数据整理成“date, price”两列的整齐结构。蔬菜批发数据常见的脏点有三个同一个品种在不同时间段叫不同名字“西红柿”和“番茄”并存计量单位在“公斤”和“市斤”之间切换以及部分日期因为休市而整行缺失。处理原则是先品种后单位最后再补缺失值。import pandas as pd import numpy as np df pd.read_csv(vegetable_price.csv) df[date] pd.to_datetime(df[date]) # 品种名统一维护一个小字典做映射 name_map {西红柿: 番茄, 西红杮: 番茄, 大白菜: 白菜} df[variety] df[variety].replace(name_map) # 单位统一假设原始单位是公斤统一转成市斤 multiplier_map {公斤: 2.0, 市斤: 1.0} df[price] df.apply(lambda row: row[price] * multiplier_map.get(row[unit], 1.0), axis1) # 按时间排序并以天为频率重采样 df df.sort_values(date) daily df.groupby([variety, pd.Grouper(keydate, freqD)])[price].mean().reset_index() # 缺失日期用线性插值填充注意只插值不做外推 pivot daily.pivot(indexdate, columnsvariety, valuesprice) pivot pivot.interpolate(methodlinear, limit_directionforward)这段代码做了三件事字符串级别的品种名对齐数值级别的单位换算以及时间维度的重采样和插值。线性插值对短期缺失比如连续停业两天是安全的但如果某个品种连续缺了一周以上直接用插值会制造一段虚假的平缓价格段应该把这段数据标成缺失并从训练集剔除。归一化也在这里做但要注意边界只能用训练集的统计量来归一化不能先对整个数据集算min/max再切分否则验证集的信息会通过归一化算子泄露到训练过程中这是时间序列项目里最容易犯的隐蔽错误。2.3 构建滑窗样本look_back、预测步长与Dataset封装LSTM吃进去的不是单个时间点而是一段连续窗口。窗口长度look_back是毕设里必须写在论文里的关键参数取值太小比如3天模型看不到周周期取值太大比如60天训练样本数量骤减且引入了过多无关噪声。对蔬菜日度数据7天和14天是两个务实的起点前者刚好覆盖一周价格节律后者覆盖双周对比。滑窗构造的正确姿势是“预测未来、不偷看现在”。假设要预测t1天的价格输入窗口应该是[t-look_back, t-1]而不是[t-look_back1, t]这个错位决定了模型学到的是“基于历史推未来”还是“基于今天预测今天”效果迥异。import torch from torch.utils.data import Dataset class PriceDataset(Dataset): def __init__(self, values, look_back7, horizon1): self.x, self.y [], [] for i in range(len(values) - look_back - horizon 1): self.x.append(values[i:i look_back]) self.y.append(values[i look_back horizon - 1]) self.x torch.tensor(np.array(self.x), dtypetorch.float32) self.y torch.tensor(np.array(self.y), dtypetorch.float32) def __len__(self): return len(self.x) def __getitem__(self, idx): return self.x[idx], self.y[idx]这里的关键设计是循环边界range(len(values) - look_back - horizon 1)。假设数据有1000天look_back7horizon1最后一个样本的输入是第993天到第999天标签是第1000天循环恰好走到1000-7-11993不越界。horizon1表示预测次日如果要预测未来第3天只需把horizon改成3输入窗口仍然是过去7天标签跳到窗口结束后的第3天。这种“单步多跳”的预测方式比“多步递归预测”更稳定适合作为毕设主结果。3. 用PyTorch搭LSTM预测模型模型定义、训练循环与三处关键参数3.1 为什么选PyTorch而不是Keras写毕设源码做毕设选框架首要标准不是“哪个简单”而是“哪个能让你在答辩时把原理讲清楚”。PyTorch的优势在于网络结构就是Python代码本身前向传播的过程一眼可见调试时可以随时在中间层打印张量形状和值域Keras的封装更友好但Sequential堆出来的模型在解释“LSTM如何处理时间步”这类问题时容易让作者自己也含糊。另外题目里列出的是python源码PyTorch的代码风格与学术论文中的公式对应更直接导师审阅源码时观感更好。我一般建议用PyTorch 2.x配合CPU训练本数据集即可。蔬菜价格数据量通常在几千条级别一个单层LSTM在CPU上训练几十个epoch只需要几分钟没必要引入CUDA依赖——这反而会提高对方复现的门槛。深度学习项目不是GPU越高级越好数据量决定模型容量数据少的情况下用大模型只会过拟合。3.2 模型定义nn.LSTM的参数到底怎么设PyTorch里nn.LSTM的构造参数有4个最常用input_size、hidden_size、num_layers、batch_first。对价格预测任务输入特征是价格本身所以input_size1如果后续把“降雨量、节假日标记”也拼进来这个值就变成特征维度。hidden_size是LSTM隐状态的宽度相当于网络的记忆容量我建议从32开始试效果不够再加到64超过128对毕设数据量往往没有收益。import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size1, hidden_size32, num_layers1): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue ) self.regressor nn.Linear(hidden_size, 1) def forward(self, x): # x: [batch_size, seq_len, input_size] out, (h_n, c_n) self.lstm(x) # 取最后一个时间步的隐状态作为整段序列的表示 last_hidden h_n[-1] return self.regressor(last_hidden)batch_firstTrue意味着输入张量形状是(batch, seq, feature)这更符合人的直觉如果不设这个参数默认形状是(seq, batch, feature)训练时错误会以维度不匹配的形式暴露排查起来费劲。h_n[-1]取的是最后一层LSTM的隐状态当num_layers1时它就是唯一一层如果层数大于1h_n的形状是(num_layers, batch, hidden_size)取[-1]才是顶层输出。regressor把隐状态从hidden_size压到1维输出的是预测价格。注意这里没有在LSTM层后加激活函数因为回归任务的输出层只需要线性变换。还有一个常被问到的点要不要用双向LSTM(Bidirectional)? 在价格预测里答案是不要。双向的意思是一边从左往右读、一边从右往左读后者在预测时刻使用到了未来信息属于泄露。刷分可以答辩时被问到就是硬伤。3.3 训练循环优化器、学习率与梯度裁剪训练过程比模型定义更容易翻车。价格序列的数值范围虽然被归一化过但LSTM在时间维度的梯度传播容易累积偶尔会出现某个batch的loss突然变成nan这正是梯度爆炸的典型信号。import torch.optim as optim model LSTMPredictor(input_size1, hidden_size32) optimizer optim.Adam(model.parameters(), lr0.001) criterion nn.MSELoss() for epoch in range(60): model.train() epoch_loss 0.0 for x_batch, y_batch in train_loader: optimizer.zero_grad() pred model(x_batch).squeeze(-1) loss criterion(pred, y_batch) loss.backward() # 关键梯度裁剪防止梯度爆炸产生nan torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() epoch_loss loss.item() * x_batch.size(0) if (epoch 1) % 10 0: avg_loss epoch_loss / len(train_loader.dataset) print(fepoch {epoch1:3d}, loss: {avg_loss:.6f})两个参数值得展开。lr0.001是Adam的默认学习率对大多数时间序列项目不需要改动如果loss在0.01附近震荡下不去可以改成0.0005而不是把初始学习率调成0.01。max_norm1.0是梯度裁剪阈值意思是把梯度的L2范数压到不超过1裁剪后梯度的方向不变、只压缩大小不会破坏优化方向。这两个参数配合基本能杜绝训练过程中的数值发散。为什么损失用MSELoss而不是L1LossMSE对大幅误差的惩罚是平方级的这会让模型更努力拟合价格暴涨暴跌的大偏差样本数值上也更平滑。但MSE在“量纲解释”上不如MAE直观所以后面评估指标里不能只看训练loss要单独算MAE和MAPE。3.4 预测与反归一化把0到1的数值还原成“元/市斤”模型输出的是归一化空间里的数值必须反归一化才能跟真实价格对比。很多源码包在这里挖了个坑反归一化时直接用了全量数据的scaler而不是训练集scaler导致画出来的曲线看起来拟合得很好实际是测试集信息混入了中间环节。# 假设 scaler 已在训练集上拟合 def inverse_scale(scaler, values): values np.array(values).reshape(-1, 1) return scaler.inverse_transform(values).ravel() model.eval() predictions, truths [], [] with torch.no_grad(): for x_batch, y_batch in test_loader: pred model(x_batch).squeeze(-1) predictions.append(pred.numpy()) truths.append(y_batch.numpy()) pred_all inverse_scale(scaler, np.concatenate(predictions)) true_all inverse_scale(scaler, np.concatenate(truths))这段代码的要点在于torch.no_grad()包裹推理过程关闭梯度追踪以节省内存并防止误操作改变计算图。反归一化必须用之前保存的训练集scaler而不是重新fit一个。判断代码有没有偷看未来就看测试阶段是否出现任何基于测试数据计算出来的统计量只要出现了指标再漂亮也没有可信度。4. 训练评估与项目说明组织让这套代码从“能跑”变成“能答辩”4.1 先后次序时间序列数据为什么不能随机打乱切分图像分类任务把数据集随机切分成训练/验证/测试是惯例但时间序列项目照搬这个习惯会出大问题。价格序列天然有时间顺序随机洗牌会把未来一段数据混进训练集模型相当于提前看到了答案。正确的切分方式是按时间切前70%训练中间15%验证最后15%测试。验证集用于早停和学习率调整测试集只在所有调参结束后跑一次绝不能用测试集反复试参数。def temporal_split(values, train_ratio0.7, val_ratio0.15): n len(values) train_end int(n * train_ratio) val_end int(n * (train_ratio val_ratio)) return values[:train_end], values[train_end:val_end], values[val_end:]切分完之后有个细节构造滑窗数据集时不能让训练集滑窗的末尾伸进验证集区间。常见做法是把三个子序列分别传入PriceDataset各自独立构造滑窗而不是在完整序列上切好窗口再对窗口做划分后者会造成样本重叠导致的间接泄露。训练过程中配合早停验证集loss连续多个epoch不下降就终止训练并恢复最佳参数。PyTorch里没有内置早停但实现非常简单每轮结束记录验证loss如果比历史最优更小就保存一份模型副本超过patience轮没有改善就break并把副本加载回模型。4.2 评估指标的选择与预测图的三个细节回归任务最基础的指标是MAE和RMSE价格预测场景还应该加一个MAPE因为它能回答“预测误差占真实价格的比例是多少”。蔬菜均价如果围绕每市斤3元波动MAE等于0.15元意味着平均误差约5%这个数字在论文里比绝对值更有说服力。def evaluate_metrics(y_true, y_pred): mae np.mean(np.abs(y_true - y_pred)) rmse np.sqrt(np.mean((y_true - y_pred) ** 2)) # 避免除零真实价格太接近0的样本不计入MAPE mask np.abs(y_true) 1e-6 mape np.mean(np.abs((y_true[mask] - y_pred[mask]) / y_true[mask])) * 100 return {MAE: round(mae, 4), RMSE: round(rmse, 4), MAPE%: round(mape, 2)} print(evaluate_metrics(true_all, pred_all))画预测对比图的时候有三个容易忽略的细节先把预测值和真实值按时间戳对齐否则曲线错位会被误判为预测效果好测试集曲线不要从第0天开始画前面若干点没有对应的历史窗口坐标系保留两位小数即可价格小数点后第三位没有现实意义。毕设报告里放一张训练集整体拟合图和一张测试集局部放大图就足够了前者展示模型学习能力后者展示泛化能力。4.3 项目说明文档的组织README、复现清单与实验结果表题目里的“项目说明”是高分毕设和普通代码包的分水岭。一份合格的说明文档不需要长篇大论但必须让一个完全陌生的人按步骤在半小时内复现结果。常见到令人惋惜的情况是源码能跑通但README只写了“运行main.py”既没有Python版本要求也没有依赖清单数据集的列名含义更没有解释。章节内容要求对应文件环境依赖Python版本、pip install命令列表requirements.txt数据说明字段含义、时间范围、品种数、缺失值处理方式data/README.md运行步骤预处理、训练、评估三个命令的先后顺序README.md实验结果MAE/RMSE/MAPE数值、训练曲线、测试集对比图results/答辩要点为什么用LSTM、look_back取值的实验对比论文文档整个源码包的组织建议是data/、src/、results/三个顶层目录。src里按preprocess.py、dataset.py、model.py、train.py、evaluate.py拆分每个文件只做一件事。答辩的时候老师往往会先看目录结构再看代码工程感是从这种细节里透出来的。5. 避坑蔬菜价格预测项目里最常见的5个翻车现场5.1 现象loss收敛到很低但预测曲线整体偏了一截原因归一化统计量泄露。常见做法是在全量数据上计算MinMaxScaler的min和max再把所有数据一次性变换然后切分训练集和测试集。这样测试集的数值分布信息已经参与训练模型在测试阶段占了大便宜。由于价格序列各段分布略有差异模型学到的映射关系带上了“全剧透”的偏移量看上误差不大其实是系统性偏差。解决先把数据按时间顺序切出训练、验证、测试三段然后在训练集上fit归一化器再分别transform三段数据。训练集和测试集各用各的变换结果但变换参数只能来自训练集。代码上只需要多写一行scaler.fit(train_values)。5.2 现象预测曲线比真实曲线滞后一天整体右移原因滑窗标签构造时使用了“当前时间点”而不是“未来时间点”。如果输入窗口是[t-look_back, t]标签是[t]模型实际学到的是“用今天的价格猜今天”但价格序列当日波动很大的时候模型最简单粗暴的策略就是直接输出窗口最后一个值表现为强滞后。这种模型的测试集MAE看起来不错本质上却是做了一次平移复制。解决检查PriceDataset里的边界取值确认输入窗口结束点是i look_back标签取点是i look_back horizon - 1中间至少隔一个未来时间步。把预测目标从“当天价”改成“次日价”或“未来第3天价”后滞后现象会明显缓解。如果滞后仍然存在把horizon从1加大到2或3重新评测。5.3 现象训练集和验证集指标很好测试集指标突然恶化原因验证集参与了调参模型的超参数hidden_size、lr、look_back是在验证集上选出来的相当于验证集也有一点过拟合更常见的是测试集的时间段恰好包含春节或暴雨等极端行情而模型没见过这类模式。蔬菜价格的分布漂移比静态数据集更显著跨时间段的泛化天然有难度。解决先确认测试集切分确实在时间上晚于验证集排除随机切分导致的时间颠倒。再检查测试集区间是否包含极端事件如果包含把“节假日标记”作为额外特征加入训练或者在论文里明确说明这是模型的已知局限。不要为了让测试集指标好看而去调整已训练模型那就是在漏斗里作弊。5.4 现象MAPE数值异常大甚至超过100%原因平均绝对百分比误差的分母是真实价格当某个测试样本的真实价格接近0时即使绝对误差只有0.01元百分比也会被放大成天文数字。蔬菜里像“白萝卜”“冬瓜”这类大路菜批发价在特定季节可以跌到每市斤几分钱正好踩中这个雷区。解决计算MAPE之前加一个过滤掩码把真实价格绝对值小于阈值的样本剔除阈值可以取0.05元或0.1元另一个办法是改用对称MAPE即分母改为真实值和预测值的平均值。在论文里要明确写出MAPE计算时剔除了哪类样本否则评审老师一句“你这个MAPE怎么算的”就能让答辩现场冷场。5.5 现象loss在前几个epoch内完全不动或者一步跳到nan原因学习率过大加上梯度爆炸是LSTM训练最常见的两个并发问题。Adam虽然自适应调整学习率但不等于不需要梯度裁剪另外如果输入数据没有归一化价格原始数值比如几千元的量级会让LSTM内部的sigmoid和tanh饱和梯度直接消失。解决先确认输入已经归一化到约[0,1]区间再看学习率是否大于0.01最后在loss.backward()之后加clip_grad_norm_。按这个顺序排查90%的情况都能解决。如果loss还是不动把模型简化成单层LSTMhidden_size降到16先确认整个pipeline能跑通再逐步加复杂度。6. 进阶验证滚动预测与多步预测的务实做法6.1 用滚动预测检验“明天均价”的真实可用性教科书式地切分训练/测试集只能证明模型在同分布区间的拟合能力但真正的预测场景是“用截止到昨天的数据预测今天”然后今天过完又要用截止到今天的更长序列重新推。我把这种验证方法叫滚动预测测试集上第一个点用历史窗口正常预测拿到真实值后把它拼进历史窗口滑动到下一个时间点继续预测。model.eval() history test_values[:look_back].tolist() rolling_preds [] for i in range(len(test_values) - look_back): x torch.tensor(history[-look_back:], dtypetorch.float32).view(1, look_back, 1) with torch.no_grad(): pred model(x).item() rolling_preds.append(pred) history.append(test_values[i look_back])这段代码里history是被预测区间的真实历史每推进一步就把刚得到的真实值追加进去模拟的是现实中的每日滚动更新。对比看普通测试集预测的误差是“一次性快照”滚动预测的误差才是“持续运行系统”的真实水平。如果滚动预测的误差明显大于一次性预测说明模型对近端历史过于敏感正则化和数据增强还有优化空间。6.2 多步预测的两条路线递归还是直接多输出毕设的预测目标如果扩展到“未来7天价格曲线”路线就分叉了。递归预测是把单步模型用自己的输出反复喂给自己实现简单但误差会逐日累积第7天的误差可能是第1天的两三倍。直接多输出是修改模型输出层把nn.Linear(hidden_size, 1)改成nn.Linear(hidden_size, 7)让网络一次输出未来7天的价格训练时标签也对应7天。两条路线我都在项目里试过结论是数据量小于5000条时直接多输出的训练不稳定因为同一段输入要同时拟合7个不同时间尺度的标签容易顾此失彼递归预测虽然误差累积但每个单步都在分布内稳定性更好。折中的做法是先用直接多输出训练出7个点再对后3个点做一次递归修正这个两阶段方法在答辩时讲出来很有区分度。6.3 把模型导出为ONNX从毕设走向可部署最后一步建议把训练好的模型导出成ONNX格式在报告里写上“模型可在标准推理引擎中运行”这句话会让项目说明的完整度上一档。ONNX的意义在于让推理脱离PyTorch运行时是展示工程意识的低成本操作。dummy_input torch.randn(1, look_back, 1) torch.onnx.export( model, dummy_input, lstm_price.onnx, input_names[history], output_names[forecast], dynamic_axes{history: {0: batch_size}, forecast: {0: batch_size}} )导出时唯一注意点是dynamic_axes它允许推理阶段batch_size是可变的预测单条样本时不用凑batch维度。验证导出的模型输出与PyTorch原模型是否一致只需要用onnxruntime加载再跑一遍相同的输入对比两次输出差异是否在1e-5量级以下。做这个项目时我踩得最深的坑来自“预测滞后”那一类问题当时花了两天调hidden_size和look_back误差纹丝不动后来才发现是滑窗标签边界写错了。做时间序列项目先验证数据管道再信任模型这是我养成至今的习惯希望帮到你。本文还有配套的精品资源点击获取
返回列表