☰
基于LSTM的蔬菜价格预测:从数据预处理到模型训练全解析
2026/10/9 3:22:48 网站建设 项目流程

简介:这是一份面向计算机相关专业毕业设计及课程设计的深度学习实战项目,以长短时记忆网络(LSTM)为核心,实现蔬菜价格的时间序列预测。项目经导师指导并获评审高分,适合需要完整项目参考的本科生及想动手练习LSTM的学习者。压缩包共181个文件,包含25个Python源码文件、142个蔬菜历史价格CSV数据集、3个docx项目说明文档及1个Markdown说明,整体大小仅1.86MB,结构清晰便于快速定位和复用。目前已有271人学习。资源提供了从数据预处理、模型搭建到训练评估的完整代码,并附有项目设计思路、参数配置和结果分析,可帮助读者理解LSTM在价格预测中的应用细节,也可直接作为毕设底座进行扩展与二次开发。

1. 基于深度学习LSTM的蔬菜价格预测:这份高分毕设资源到底能做什么

如果你正在为毕设选题发愁,或者想拿一个真实场景练手深度学习,这份基于深度学习LSTM实现蔬菜价格预测的python源码+项目说明+数据集资源包,是一个能直接跑通的完整起点。它拿十个蔬菜品种的每日价格当输入,用LSTM神经网络去拟合价格走势,最终给出未来几天的预测结果。资源里的十个CSV文件都是批发市场真实常用的品种:本地菜心、云南小瓜、小塘白菜、本地芹菜、矮脚白菜、青皮冬瓜、西红柿、红尖椒。整个项目按完整毕设流程组织,数据处理、滑动窗口、模型训练、预测评估、项目说明一应俱全,是个人经导师指导并认可通过、评审分98分的高分毕业设计。适合正在做毕设的计算机专业学生,也适合想快速上手时序预测的从业者。这篇笔记按我从下载到跑通、再把它改成自己实验的全过程来讲,重点放在数据怎么喂给LSTM、参数怎么设、以及哪些地方最容易翻车。

2. LSTM为何适合蔬菜价格:时间序列选型与数据集盘点

2.1 菜价序列的三个特征:周期性、波动性、缺失性

蔬菜价格不是平稳序列,这一点和股票、电力负荷还不太一样。它有几个非常明显的特征:第一,周期性很强,叶菜类受季节和天气影响,夏季本地菜心便宜,冬季寒潮一来价格直接跳涨,一周之内就能走出一个完整的波峰波谷。第二,波动性大,个别品种在节假日前后价格能翻倍,数据里会出现明显的尖峰。第三,数据质量参差不齐,批发市场的记录经常有缺失、重复日期、甚至乱码编码。这三个特征决定了选型方向:传统ARIMA要求序列平稳,遇到这种带趋势、带季节性、还有突变的价格数据,需要先做差分和季节性分解,折腾一圈效果还不一定好;MLP这类全连接网络虽然也能拟合曲线,但它把每个时间点当成独立的特征,完全没有时间顺序的概念,预测结果经常出现错位。

LSTM的优势正好落在这些痛点上。它靠遗忘门、输入门、输出门控制信息的保留与丢弃,可以记住几天前甚至几周前的价格模式,比如上个月的台风导致菜价上涨,这种跨天的依赖关系在LSTM里能持续传递。同时它对缺失值和非平稳数据相对宽容,只要把价格归一化到0到1之间,就能直接训练,不需要像ARIMA那样做一堆平稳性检验。一句话总结这个项目的选型逻辑:蔬菜价格是典型的中短期时序预测,样本量不大、周期性强、有突变,LSTM在精度和工程复杂度之间取了一个很舒服的平衡点,这也是它成为毕设选题的原因。

2.2 数据集盘点:十个CSV文件里的字段与数据量

下载解压之后,先看数据集这一层。压缩包里一共放了十个CSV文件,对应十个蔬菜品种。先注意一个细节:文件列表里矮脚白菜.csv出现了多次,我在解压后第一件事就是对比这几个文件的sha256哈希值。如果是完全一样的重复文件,那说明打包时手滑了;如果是不同批次的数据,文件名相同会互相覆盖,这属于压缩包整理不规范,跑之前必须清理掉。无论如何,你要记住一个原则:拿到的文件先做一次物理核对,别急着打开跑模型。

md5sum *.csv | sort | awk '{print $1}' | uniq -d

这条命令会找出所有哈希值重复的CSV文件。如果有重复,删掉多余的一份,只保留数据行数最多的那个。我一般还会用wc -l看一眼每个文件的行数,正常来说同一时期采集的蔬菜价格数据行数应该接近,某个文件如果只有别人一半的体量,大概率是采集周期没对齐。

这类农产品价格CSV的常见列结构就是两列:日期和价格,个别文件会带产地或批发市场名称。先用Python打开文件头确认具体列名,然后统一清洗。数据集本身规模不大,单品种一般是几百行到两千行左右的日度数据,这个量级对LSTM来说刚好够用,跑一遍训练也就几十秒,不需要昂贵的显卡,CPU也能扛住。需要注意的是,不同蔬菜的启动脚本和读取路径都是独立写的,跑单品种很顺手,但要做对比实验就得自己写一个统一的读取入口,这一点后面专门讲。

3. 把CSV变成LSTM能吃的输入:归一化与滑动窗口实操

3.1 先归一化再切分,顺序真的不能乱

LSTM对输入尺度很敏感,价格从两块到十几块不等,直接丢进网络会让损失函数被大数值主导,训练很难收敛。所以第一步是归一化,把价格压缩到0到1之间。但这里有个毕设里特别爱犯的错:有人在整条序列上做fit_transform,然后再切训练集和测试集,这等于让模型在训练阶段偷看了未来数据的最大值和最小值,测试集的归一化范围被污染了。正确的做法是先切分,再只用训练集的数据去fit,然后拿同一个scaler去transform训练集和测试集。

import pandas as pd import numpy as np from sklearn.preprocessing import MinMaxScaler df = pd.read_csv("本地菜心.csv", encoding="gbk") print(df.columns.tolist()) print(df.head()) df.iloc[:, 1] = pd.to_numeric(df.iloc[:, 1], errors="coerce") df = df.dropna() data = df.iloc[:, 1].values.reshape(-1, 1) train_size = int(len(data) * 0.8) raw_train, raw_test = data[:train_size], data[train_size:] scaler = MinMaxScaler(feature_range=(0, 1)) scaler.fit(raw_train) train_scaled = scaler.transform(raw_train) test_scaled = scaler.transform(raw_test)

这段代码有三个关键点。第一,encoding="gbk",批发市场导出的CSV十有八九是GBK编码,直接用utf-8读会报解码错误,这是第一个坑。第二,pd.to_numeric配合errors="coerce",把文件里的空字符串、逗号千分位、异常符号统一转成NaN,然后dropna删掉,避免某些行是字符串类型导致后续计算报错。第三,也是最重要的,scaler.fit只放在raw_train上,测试集的归一化不参与最大值最小值的计算。我一般还会顺手print(scaler.data_min_, scaler.data_max_),确认一下训练集的范围没有明显异常值,如果有价格突变到20块这种记录,先人工判断要不要去掉。

3.2 滑动窗口构造:用过去七天去预测明天

LSTM不接收一个单独的价格数值,它接收的是一个时间片段。滑动窗口的含义就是:用连续7天的价格作为输入特征,去预测第8天的价格。这个window参数是整个项目里最关键的超参数,窗口太长会把太久远的信息混进来,窗口太短又学不到周期性,我一般从7起步,对应一周的周期。

import numpy as np def make_sequences(seq, window=7, horizon=1): xs, ys = [], [] for i in range(len(seq) - window - horizon + 1): xs.append(seq[i:i + window].reshape(-1)) ys.append(seq[i + window + horizon - 1, 0]) return np.array(xs, dtype=np.float32), np.array(ys, dtype=np.float32) X_train, y_train = make_sequences(train_scaled, window=7, horizon=1) X_test, y_test = make_sequences(test_scaled, window=7, horizon=1) print(X_train.shape, y_train.shape) print(X_test.shape, y_test.shape)

make_sequences的逻辑不复杂:从第0个位置开始,连续取window个数据作为x,再往后跳horizon步取一个值作为y,然后窗口整体右移一位。reshape(-1)把每个窗口展平成7个数值的向量,方便后续转成LSTM需要的(batch, seq_len, input_size)三维结构。现在window=7,表示用过去一周的本地菜心价格预测明天;如果你想预测后天,把horizon=2即可,但相应地训练样本会减少,因为每个序列最后要多留出一天。这一步做完以后,训练集和测试集的形状会打印出来,正常情况X_train.shape应该是(样本数, 7),如果变成(7, 样本数),检查一下是不是reshape方向弄反了。

这里还有个细节值得注意:make_sequences是拿归一化后的序列整体构造的,训练集和测试集各自切完窗口后,样本量会比原始数据少window + horizon - 1个,这在验证集上会表现为预测曲线比真实曲线“短一截”,属于正常现象,不用紧张。整个数据准备阶段做到这一步,模型能接收的输入格式就算齐了。

4. 搭建与训练LSTM:模型结构、训练参数与评估代码

4.1 nn.LSTM加一层全连接:结构不用太花哨

毕设里的LSTM模型不需要堆复杂的结构。按我的习惯,一个nn.LSTM层加一个全连接输出层就够了:LSTM负责提取时间维度的依赖特征,全连接层把LSTM最后一个时间步的隐藏状态压成预测价格。下面这份代码可以直接放进model.py里。

import torch.nn as nn class PriceLSTM(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2, dropout=0.2): super().__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout ) self.fc = nn.Linear(hidden_size, 1) def forward(self, x): out, _ = self.lstm(x) return self.fc(out[:, -1, :])

几个关键参数逐个说。batch_first=True,让输入张量的维度变成(批次, 时间步, 特征数),这符合大多数人的思维习惯,不设的话LSTM默认是(时间步, 批次, 特征数),数据准备阶段就要多一次维度交换。out[:, -1, :]取最后一个时间步的隐藏状态,因为我们的任务是单步预测,只用最终时刻的信息就够了;如果你预测的是未来一周,可以改成out[:, -7:, :]再接全连接,这个进阶玩法放到最后一章。hidden_size=64和num_layers=2是经验值:蔬菜数据集小,隐藏单元超过128基本就过拟合了,层数超过3层在小样本上不但不提升精度,反而会让训练时间翻几倍。dropout=0.2只在多层LSTM层间生效,单层时会给出警告,这是正常的。

4.2 训练循环:损失函数、优化器与整体流程

模型结构定下来以后,训练循环比模型结构更容易出错。先看一份最小可跑的代码。

import torch import torch.optim as optim X_train_t = torch.tensor(X_train, dtype=torch.float32).unsqueeze(-1) y_train_t = torch.tensor(y_train, dtype=torch.float32).view(-1, 1) X_test_t = torch.tensor(X_test, dtype=torch.float32).unsqueeze(-1) model = PriceLSTM() criterion = nn.MSELoss() optimizer = optim.Adam(model.parameters(), lr=0.001) model.train() for epoch in range(60): optimizer.zero_grad() pred = model(X_train_t) loss = criterion(pred, y_train_t) loss.backward() optimizer.step() if (epoch + 1) % 10 == 0: print(f"epoch {epoch + 1:3d} loss {loss.item():.5f}")

unsqueeze(-1)把X_train从(样本数, 7)扩展成(样本数, 7, 1),表示每个时间步只有一个特征维度,也就是价格本身。view(-1, 1)把y_train从一维向量变成二维列向量,保持和预测输出(样本数, 1)的形状一致。损失函数用MSELoss,它放大预测误差大的样本,适合价格这类连续值回归;如果发现个别离群点把损失拉得过高,换成SmoothL1Loss会更稳。优化器直接用Adam,学习率从0.001起步不折腾,先训练60轮看损失曲线。每次zero_grad务必放在backward之前,这是PyTorch最经典的坑,梯度不清零会累加到下一轮。

数据量小的时候可以直接整批训练,不需要DataLoader。但如果想给毕设答辩加亮点,我一般会在数据准备阶段就分批:batch_size=32,训练代码里加一个for x_batch, y_batch in loader的循环,配合shuffle=False,既能刷几千条的数据集,也能顺便讲清楚批次迭代的机制。评估阶段把model.eval()打开,用torch.no_grad()包住预测过程,这一步不是可有可无,它关闭了梯度计算,内存占用明显下降,预测速度也快得多。

评估指标上,毕设答辩常问的是RMSE和MAE。RMSE对偏离大的点敏感,能反映出模型是否在大涨大跌时崩掉;MAE更直观,用原始价格单位表示平均误差多少元。还可以加一个MAPE,用百分比表示误差占比,便于比较不同蔬菜品种之间的预测精度。

指标计算方式含义适用场景
RMSE均方根误差大偏差被放大看极端天气下的崩溃情况
MAE平均绝对误差误差的平均大小日常预测误差水平
MAPE平均绝对百分比误差相对误差占比跨品种横向对比

预测完别忘了反归一化:把预测值和真实值都乘回原来的尺度,再去算指标。直接在0到1的尺度上算RMSE,答辩老师一眼就知道你没理解归一化。

5. 避坑与排查:LSTM价格预测里最容易翻车的五个地方

5.1 训练时shuffle=True:时序泄漏的经典翻车现场

现象:训练集损失一路降到0.001以下,曲线贴合得近乎完美,但放到测试集上一画图,预测结果整体滞后一天,涨跌拐点全对不上。

原因:有人觉得DataLoader默认不开随机打乱,训练效果不好,于是手动加了shuffle=True。这一打乱,模型看到的样本不再是时间顺序,相邻样本之间没有先后关系,等于把未来信息混进了历史信息,模型学到了“从整体分布猜数值”,而不是“根据走势推价格”。LSTM的时间依赖被整体破坏了。

解决:训练时强制shuffle=False。如果担心模型在固定顺序下过拟合,可以用dropout和早停来控制,不要去动样本顺序。更稳妥的做法是先把测试集切出来放到一边,保证测试集里的任何样本都不会出现在训练过程中。

5.2 归一化的scaler跨文件复用不一致

现象:单品种训练效果很好,一换品种就出现预测值几乎恒定在0.5附近,怎么调参都没用。

原因:这是个隐蔽的复现问题。代码里有个固定写法,比如某个文件里直接写了scaler = MinMaxScaler()然后在整条数据上fit,另一个文件里又偷懒把上一个品种训练好的scaler直接拿来transform新数据。不同蔬菜的价格区间差异很大,本地菜心可能只有两三块钱,红尖椒能到十几块,缩放范围完全对不上。

解决:每个品种单独新建scaler,且严格遵循先切分再fit训练集的流程。如果做了多品种对比实验,维护一个字典,按品种名保存各自的scaler,预测阶段用同名scaler反归一化,不要图省事复用全局变量。

5.3 隐藏层和层数堆太高,小数据集直接过拟合

现象:训练到第20轮时损失还在下降,测试集准确率反而越来越差,重跑一次结果波动很大。

原因:价格数据量小,二三百条到上千条的规模撑不起大网络。hidden_size=256、num_layers=4这种配置在小数据上是灾难,参数数量远超样本量,LSTM会“背”下训练集的所有波动包括噪声,测试集稍微有点变化就崩。还有一个容易忽略的点:多层LSTM的dropout参数如果设为0,层间没有正则化,过拟合会来得更快。

解决:先把hidden_size定在32到64之间,num_layers不超过2。训练时盯着验证集损失曲线,前20轮验证损失还在下降就继续,一旦连续5轮不降就停。这个逻辑写成ReduceLROnPlateau或者手动保存最佳模型权重都行,比盲目加大模型靠谱得多。

5.4 预测值等于上一个值的延迟效应

现象:测试集上的预测曲线看起来还行,但放大看,预测值总是紧贴着前一日的真实值,涨跌方向经常慢半拍,曲线整体像一个平移了一格的原始序列。

原因:这是LSTM慢时间步预测的通病,尤其在价格序列波动频繁的时候。模型发现把上一日价格当预测基准能获得最低损失,于是学成了“复制前值”。单靠MSELoss很难惩罚这种延迟,因为延迟一格带来的误差并不大。

解决:检查训练好的模型输出的滞后相关性,把预测值和真实值画在同一张图上,观察有没有整体右移。如果存在,可以改用SmoothL1Loss降低离群值干扰,或者把horizon设成2或3,让模型学会跳过短期噪声去拟合趋势。这个问题的根治方法就是最后一章要说的滚动多步预测。

5.5 同名CSV文件覆盖与数据行数不对齐

现象:矮脚白菜.csv在文件列表里出现多次,解压时被覆盖,数据行数和其他文件对不上,训练出来的模型精度特别差。

原因:打包时把不同批次的数据放进了同名文件,或者重复文件只是预览残留。我排查过一次,三个同名CSV里前两个行数一样但价格列完全不同,第三个是空文件,说明从采集到打包整个链路里有环节把文件搞混了。

解决:解压后立刻用md5sum对比重复文件,只保留行数最多、日期范围最完整的一份。数据统一放进data/目录,文件名改成“品种_起止日期.csv”格式。这个命名习惯救过我好多次,项目说明文档里通常写的是原始文件名,你自己整理后要在README里做一张映射表,防止答辩前一周找不到对应文件。

6. 进阶:从单步预测到滚动多步预测与稳定性验证

单步预测只是把明天猜准,实际应用里更关心未来一周的走势,比如批发商想知道下周菜心大概是什么价位区间。这时候需要滚动预测:拿模型输出的第一步预测值,拼到历史序列末尾,再作为输入预测下一天,循环下去。

def rolling_forecast(model, test_seq, scaler, window=7, steps=7): model.eval() inputs = test_seq[-window:].copy() preds = [] with torch.no_grad(): for _ in range(steps): x = torch.tensor(inputs, dtype=torch.float32).view(1, window, 1) y_pred = model(x).item() preds.append(y_pred) inputs = np.append(inputs[1:], y_pred) return scaler.inverse_transform(np.array(preds).reshape(-1, 1))

这段代码的精髓在inputs = np.append(inputs[1:], y_pred),把窗口最前面的旧数据丢掉,把预测出的新价格接到末尾,模拟“用昨天预测今天,再用今天预测明天”的真实过程。滚动预测的误差会随时间累积,这是正常现象,所以我看结果时不只看最后一两天准不准,更关心前三天能不能对上趋势。验证稳定性的方法是多跑几个品种,同一个模型结构、同一组超参数,看每个品种的MAPE是否都在可接受范围内。蔬菜价格预测里,方向准确率比绝对误差更重要,涨跌判对意味着交易时机把握对了,我用一张小表记录三个指标:RMSE看整体误差、MAPE看相对误差、方向准确率看拐点判别能力。比如本地芹菜MAPE能压到8%到10%,方向准确率稳定在65%以上,这个模型在毕设阶段就算能打了。

不是所有品种都适合同一套超参数,西红柿价格波动比其他叶菜大,window=7对它偏短,我会单独跑一次window=10再对比MAPE。从那以后,我每次拿到新的价格序列,都会强制走一遍四步检查:确认文件编码、画一次序列图看缺失和跳空、固定随机种子和窗口参数、先跑单步再跑多步。这套流程看起来笨,但确实帮我避开了不少暗坑,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询