拓冰建站拓冰建站
首页 / 资讯中心 / 正文

LSTM蔬菜价格预测实战:数据清洗、模型训练与区间输出

简介基于深度学习LSTM的蔬菜价格预测毕业设计资源包由个人完成并获导师认可评审分98分。内容涵盖完整Python源码、项目说明与真实蔬菜价格数据集适合计算机相关专业准备毕设的学生也可用于课程设计或期末大作业。整个压缩包共181个文件其中142个csv文件为本地菜心、西红柿、青皮冬瓜等品种的历史价格数据25个py文件包含LSTM模型构建、训练与预测脚本另有docx项目说明文档与md技术笔记整体大小仅1.86MB结构清晰便于查阅。项目说明文档详细介绍了数据预处理、模型调参、预测评估等环节能帮助快速复现并理解整个实验流程。目前已有271人学习下载。通过源码可直接运行LSTM时间序列预测借助文档掌握从数据清洗到结果分析的关键思路完整数据集也支持自行扩展实验是一份实战性较强的高分毕设参考资源。1. 蔬菜价格预测这个题目LSTM 是最不容易跑偏的起点真正做过一次蔬菜价格预测你会发现它比风电负荷好做又比股票难稳定。价格曲线有厚尾、有跳空但一旦把滑窗展开明天价格与前 14 天走势之间的条件依赖其实相当稳。LSTM 之所以适合这个题目不是因为它的记忆机制天生适合价格序列而是因为它在小样本非线性回归上比统计模型自由度更够、又比 Transformer 更容易调稳。六个环节依次是数据规整、滑窗构造、PyTorch 模型、时序验证、失效排查和区间预测既能用来完成一个高分毕设也能直接落地到采购预测这类小规模场景。先把数据集按时间整明白后面所有结论才站得住。2. 把数据装进 LSTM 之前先处理四类脏活在跑任何深度学习模型前先花一天时间确认你手里的数据是否适合做时间序列预测。蔬菜价格数据通常是市场统计的日度批发价字段少、行数少、噪声大常见错误是拿到数据后直接滑窗开训结果验证集和训练集混在一起模型分数虚高。2.1 先确认价格数据的粒度和可用字段一份能用的价格数据集一般包含日期、品种、档口或批发市场、价格单位、成交量。做 LSTM 价格预测最少需要两列日期和日度均价。如果还带成交量可以作为辅助特征输入如果只有价格一列也完全可以跑。这里有一个经常被忽略的点日期必须是完整的自然日而不是“交易日”。蔬菜批发市场在节假日往往休市休市那几天没有数据简单把缺失行删掉会导致时间间隔不一致LSTM 会以为相邻样本之间始终隔 24 小时。正确做法是先按完整日历生成日期序列再对缺失价格做前向填充或插值。2.2 用 log1p 压缩价格的动态范围蔬菜批发价有明显的季节性和事件性波动极端情况下一天波动超过 30%。直接把原始价格喂给 LSTM 有两个问题一是大值样本在 loss 里占主导模型会把注意力集中在价格高位段二是数据分布右偏预测值很容易出现负价格这在业务上不可接受。我一般先对价格做对数变换import numpy as np import pandas as pd df pd.read_csv(vegetable_price.csv, parse_dates[date]) df df.sort_values(date).reset_index(dropTrue) # 价格对数压缩训练和预测都在对数空间进行 df[log_price] np.log1p(df[price]) print(df.head()) print(df[log_price].describe())逻辑说明log1p即计算log(1 x)好处是价格为零时也有定义且还原价格时直接用expm1即可。对数是时间序列建模里常用的方差稳定变换能让模型在不同价格水平下学到相近的相对涨幅而不是被高价段绑架。参数上的注意点预测结束后一定要在同一个空间做反变换不要在原始价格空间评估误差否则高价位样本的绝对误差会掩盖低价位样本的相对误差。2.3 异常值裁剪和缺失值补齐规则要能写进文档对爬取或下载的价格数据常见脏数据有两类一是录入错误产生的离谱高价或负数二是连续多日无成交记录的空白段。处理规则要固定下来方便在项目说明文档里复现。数据问题处理方式参考参数负价格或低于 0.01 元视为脏数据用前一天价格替换阈值为 0.01高于全序列 3 倍中位数视为离群点裁剪到 3 倍中位数用中位数而非均值更抗噪单日缺失线性插值仅限单日缺失连续缺失超过 3 天需要单独标记连续多日缺失前向填充并额外添加一个缺勤标记位缺勤标记作为特征列输入注意不要把缺失列直接删除。连续缺失往往发生在春节或重大天气事件前后这些时段的价格走势本身有信息量删掉会让模型错过价格反弹的样本。2.4 滑窗构造让原始序列变成监督学习样本LSTM 不能直接吃一整条时间序列它吃的是“过去 N 天预测未来 M 天”的样本对。这一步叫滑窗法也是把价格序列转换成深度学习训练集的必经步骤。def make_windows(data, window_size14, horizon1): X, y [], [] for i in range(len(data) - window_size - horizon 1): X.append(data[i: i window_size]) y.append(data[i window_size: i window_size horizon]) return np.array(X), np.array(y) X, y make_windows(df[log_price].to_numpy(), window_size14, horizon1) print(X.shape, y.shape) # 输出示例(N, 14) (N, 1)逻辑说明每个样本X[i]是连续的window_size个对数价格y[i]是紧随其后的目标价格。窗口长度为 14 天意味着模型输入两周的价格走势适合捕捉周度周期性。horizon1表示只预测明天。这一步里最容易出错的参数是window_size。窗口太短模型看不到周期窗口太长训练样本数减少且模型容易学到滞后。蔬菜价格的实证经验是 7 到 21 天之间表现比较稳定不要一上来就用 60 天。3. 用 Python 和 PyTorch 搭建最小可复现的 LSTM 模型模型部分从环境到训练循环都需要可复现。PyTorch 是当前做这类时序任务最顺手的选择动态图在调试时可以看到中间张量形状比静态图更适合频繁改结构。3.1 依赖环境和固定随机种子环境只需要四个基础包torch、numpy、pandas、matplotlib。不需要额外的时序库滑窗已经用 NumPy 实现了。import torch import torch.nn as nn import numpy as np import pandas as pd import matplotlib.pyplot as plt # 固定随机种子保证同一个数据集跑两遍结果一致 def set_seed(seed42): np.random.seed(seed) torch.manual_seed(seed) if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed) set_seed(42) device torch.device(cuda if torch.cuda.is_available() else cpu)逻辑说明固定随机种子在毕设和工程里都是第一步。LSTM 的权重初始化带有随机性不固定种子两次训练结果差异可能超过 10%导致源码和项目说明中的演示结果无法复现。device变量负责把模型和数据送到 GPU 或 CPU。3.2 定义 LSTM 模型输入输出形状要对得上我通常用两层 LSTM 加一层回归头。单层 LSTM 拟合能力有限三层在小数据集上容易过拟合。两层是数据量只有几千条时的稳妥选择。class PriceLSTM(nn.Module): def __init__(self, input_size1, hidden_size64, num_layers2, dropout0.2, output_size1): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout ) self.regressor nn.Sequential( nn.Linear(hidden_size, 32), nn.ReLU(), nn.Linear(32, output_size) ) def forward(self, x): # x 形状: (batch, seq_len, input_size) out, _ self.lstm(x) # 取最后一个时间步的隐状态 last_hidden out[:, -1, :] return self.regressor(last_hidden)逻辑说明batch_firstTrue让输入张量形状变成(batch, seq_len, input_size)和 NumPy 滑窗生成的数组形状直接对应省去转置操作。out[:, -1, :]取的是序列最后一个时间步的隐状态它聚合了整段输入的信息。回归头用一层线性加 ReLU把隐状态映射到价格空间。参数对应关系是新手最容易卡住的地方滑窗生成的X形状是(样本数, 14)要先把它 reshape 成(样本数, 14, 1)再输入模型input_size1表示每一步只有一个价格特征。如果后面要加入成交量、节假日标记等特征input_size要同步修改。3.3 数据加载和训练循环把 NumPy 数组封装成 PyTorch 的 DataLoader加上梯度裁剪。LSTM 在长序列训练中很容易出现梯度爆炸不裁剪的话 loss 会在一两个 epoch 内跳到天文数字。from torch.utils.data import TensorDataset, DataLoader # X 形状从 (N, 14) 转为 (N, 14, 1) X X.reshape(-1, 14, 1) y y.reshape(-1, 1) # 按 8:2 切分训练集和验证集这里先用随机切分第 4 章会纠正 split int(len(X) * 0.8) train_dataset TensorDataset( torch.tensor(X[:split], dtypetorch.float32), torch.tensor(y[:split], dtypetorch.float32) ) val_dataset TensorDataset( torch.tensor(X[split:], dtypetorch.float32), torch.tensor(y[split:], dtypetorch.float32) ) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse) def train_model(model, train_loader, val_loader, epochs60, lr1e-3): optimizer torch.optim.Adam(model.parameters(), lrlr) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size20, gamma0.5) criterion nn.MSELoss() for epoch in range(epochs): model.train() train_loss 0.0 for x_batch, y_batch in train_loader: x_batch, y_batch x_batch.to(device), y_batch.to(device) optimizer.zero_grad() pred model(x_batch) loss criterion(pred, y_batch) loss.backward() # 梯度裁剪防止 LSTM 梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() train_loss loss.item() * x_batch.size(0) model.eval() val_loss 0.0 with torch.no_grad(): for x_batch, y_batch in val_loader: x_batch, y_batch x_batch.to(device), y_batch.to(device) pred model(x_batch) val_loss nn.functional.mse_loss(pred, y_batch).item() * x_batch.size(0) if (epoch 1) % 10 0: print(fEpoch {epoch1:3d} | Train Loss {train_loss/len(train_dataset):.6f} | Val Loss {val_loss/len(val_dataset):.6f}) scheduler.step() model PriceLSTM(input_size1, hidden_size64, num_layers2).to(device) train_model(model, train_loader, val_loader)逻辑说明学习率调度器每 20 个 epoch 把学习率减半让训练前期快速逼近、后期精细收敛。损失函数用 MSE这是回归任务默认选择。clip_grad_norm_把梯度向量的整体范数限制在 1.0 以内超出部分等比缩放。训练阶段的三个观察点一是训练损失在前 10 个 epoch 应该明显下降如果不动检查学习率是否太小二是验证损失在 20 到 40 个 epoch 后趋于平稳如果继续大幅波动说明 batch_size 太小或学习率太大三是如果训练损失持续下降而验证损失反弹发生过拟合优先减小 hidden_size 或增加 dropout。4. 时序验证和超参数选择不要随机切分数据集很多人把数据集用 sklearn 的train_test_split随机切成训练集和验证集价格预测项目这样写基本是错的。价格是时间序列未来不能参与训练这是时序预测的底线。4.1 严格按时间顺序划分训练集、验证集和测试集正确划分方式是按日期把数据集切成三段例如 70% 做训练、15% 做验证、15% 做测试。验证集用于调参测试集只在最终评估时碰一次。# 假设 df 已经按日期升序排列 train_end int(len(df) * 0.7) val_end int(len(df) * 0.85) train_df df.iloc[:train_end] val_df df.iloc[train_end:val_end] test_df df.iloc[val_end:]这里要特别强调滑窗必须在划分之后做不在划分之前做。如果先对全量数据滑窗再切分训练集里可能混有来自未来时间窗口的信息属于特征泄漏。验证集上看到的 R² 再高也不能相信。4.2 小数据量的滚动验证比 K 折更合适标准 K 折交叉验证会随机打乱样本顺序对时间序列不适用。推荐的做法是用TimeSeriesSplit做滚动式验证每一折的训练集始终在验证集之前。from sklearn.model_selection import TimeSeriesSplit tscv TimeSeriesSplit(n_splits5) for fold, (train_idx, val_idx) in enumerate(tscv.split(X_total)): print(fFold {fold 1}: 训练 {train_idx.min()}~{train_idx.max()}, 验证 {val_idx.min()}~{val_idx.max()})逻辑说明TimeSeriesSplit保证时序顺序第一折训练集最短之后每一折在历史基础上扩充训练数据。这种做法能模拟“用截至某天的数据预测之后”的真实场景。注意滚动验证只改变划分方式不改变模型结构。每折的验证损失如果差异很大说明模型对时间段敏感需要检查是否存在概念漂移例如市场统计口径在中间某个日期发生了变化。4.3 参数扫描建议和实际使用的推荐值做价格预测的超参搜索不需要搜很大的范围。以下是一个可以直接抄的参数表超参数建议范围推荐值调整方向window_size7-21 天14数据的季节周期越长窗口越大hidden_size32-12864数据量小于 1000 条时用 32 更稳num_layers1-32超过 3 层在小样本上必过拟合dropout0.0-0.40.2验证 loss 大于训练 loss 时增加learning_rate1e-4 - 1e-21e-3训练震荡时调小收敛太慢时调大batch_size16-6432数据量少时用 16 减小噪声epochs40-10060加早停后根据 patience 决定参数之间的交互比单个参数更重要。hidden_size 从 64 涨到 128需要同时把 dropout 从 0.2 提到 0.3否则验证集表现大概率变差。window_size 和 batch_size 也存在联动窗口变长后样本总数变少batch_size 也要相应调小否则每轮迭代的梯度噪声偏大。实操里我一般先固定 learning_rate1e-3、hidden_size64、window_size14 跑通基线再单独调 window_size确定后再轮到 hidden_size。同时搜索会引入组合爆炸而且难定位是谁拖垮了指标。5. 预测失效时的四个排查方向先看损失再看残差模型训练完预测结果往往不会立刻漂亮。大概率出现的现象是验证集 loss 不高但画出来的预测曲线总比真值慢半拍或者极端价格完全预测不到。这几个问题各有解法。5.1 验证 loss 低但预测曲线滞后问题多半在窗口LSTM 很容易学到“把上一天价格平移过来”这样 loss 看起开很低但预测毫无意义。判断方法把预测序列和真实序列画在一张图里如果预测曲线整体向右偏移一个时间步说明模型把上一时刻的值当成了最优解。缓解手段是引入多步预测。不要用前 14 天直接预测第 1 天而是让模型预测未来 3 天或 7 天取第 1 天的值作为当日预测。多步预测强制模型学习更长期的趋势而不是走捷径复制前一天。5.2 出现负价格或者预测值贴着下界走对数变换之后理论上不会出现负价格但如果代码里只对训练集的log_price做了变换、没有对预测结果做expm1反变换最终还原出来的价格会整体偏差。反变换代码pred_original np.expm1(pred_log.cpu().numpy().flatten()) y_original np.expm1(y_log.cpu().numpy().flatten())另外MSE 损失天然倾向预测均值对极端价格不敏感。如果业务上更关注价格上涨和下跌的区间把损失函数换成分位数损失会更实用。def quantile_loss(y_true, y_pred, q0.5): err y_true - y_pred return torch.mean(torch.maximum(q * err, (q - 1) * err))q0.5就是 MAEq0.9训练出的模型会对价格高位更敏感。实际部署时可以同时训练三个分位模型分别输出 10%、50%、90% 分位的预测值得到一个价格区间。5.3 用残差自相关检查模型是否学到了结构信息训练结束后把验证集上的残差y_true - y_pred保存下来按时间顺序画出残差图并计算自相关系数。如果残差在 lag1 或 lag7 处仍有明显相关性说明确定性成分没有完全被模型捕获下一步应该加大 window_size 或引入天气、节假日等外部特征。更简单的做法是做一个基准对比用前面 7 天价格的加权移动平均作为预测值计算它的验证集误差再和 LSTM 对比。如果差距在 5% 以内说明这个数据集用 LSTM 的增益有限不值得继续加层数应把精力转向特征工程。5.4 数据集整体漂移时的处理边界如果模型在验证集表现好、在测试集突然恶化先检查测试期是否跨了价格结构突变点。蔬菜市场的统计口径变化、极端天气持续影响都会让历史分布失效。此时再调模型参数没有意义正确做法是缩短训练窗口只保留最近两年的数据参与训练。6. 落地到日常预测区间输出比单点值有用得多模型能从源码跑到结果只是第一步能把它变成每天可用的预测工具才算真正完成。最后这部分讲三个可以直接落地的做法。6.1 用三个分位模型输出价格区间单点预测在实际采购场景里几乎无法直接使用业务方更需要的是“明天可能落在什么范围”。固定随机种子后分别用q0.1、q0.5、q0.9的分位损失训练三个模型三个模型的输入输出完全相同只替换训练循环里的损失函数。预测时取三个模型的输出画出一个带状区间比单个预测值更有决策参考价值。6.2 每周重训练一次而不是实时增量训练价格预测场景数据天天都在更新但不建议每次来一条新数据就做一次增量训练。蔬菜价格数据量小全量重训练成本很低每周一跑一次全量训练足够。重训练时把上一周的数据并入训练集保持滚动验证的划分逻辑不变。6.3 交付时至少做三个验证行为最后建议在项目说明文档里额外写清楚三个验证行为查看最近 30 个真实价格和预测曲线的对齐情况、计算模型相比移动平均基准的误差提升幅度、保留测试集的反变换价格在同一单位下对比。能把这三件事写清楚比在源码里堆更多模型结构更能说明问题。最终要拿高分的版本预测趋势方向准确率单列出来在图表上按周标注平均绝对误差输出质量会直观很多。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门