简介:这是一份面向计算机专业毕业设计及课程设计场景的深度学习实战资源,聚焦基于LSTM的蔬菜价格预测任务。项目包含Python源码、项目说明文档与真实蔬菜价格数据集,能够覆盖数据预处理、模型训练、评估与预测的完整流程,适合正在完成毕设、期末大作业或希望入门时序预测的开发者参考。资源包共181个文件,其中142个csv文件用于存放各品种蔬菜历史价格数据,涵盖菜心、西红柿、青皮冬瓜等常见品种,25个py文件为模型相关代码,另有pyc、docx和md文件分别对应运行缓存、项目说明与使用指引,整体仅1.86MB,轻量易部署。目前已有271人学习下载,具备较高参考热度。通过该项目可掌握LSTM在时间序列预测中的实际调用方法,理解数据集的划分与特征构建思路,并借助完整的说明文档快速复用代码,有效提高毕业设计的完成效率。
1. 蔬菜价格预测这条路,为什么值得用 LSTM 走一遍
生鲜蔬菜的价格波动,是典型的非线性时序问题:受天气、节假日、供应链和批发市场情绪共同影响,今天的一块钱差价,明天可能变成三毛还是三块的区别。传统 ARIMA 和指数平滑方法在平稳序列上尚可一战,但一遇到季节性和突发扰动,残差就大到没法看。深度学习里的 LSTM(长短期记忆网络)天生就是为序列建模设计的,它通过门控机制记住“该记的”,忘掉“该忘的”,在果蔬这类短期强相关、长期有趋势的数据上,比普通 RNN 稳得多。
这个标题封装的其实是一套完整最小工程闭环:Python 数据处理、LSTM 模型搭建、训练与评估、以及带项目说明和数据集的可交付源码。对要做毕业设计或入门时序预测的人来说,它的价值不只是“能跑通”,而是能看清从原始 CSV 到预测曲线的每一步是为什么。我下面按自己会做这类项目的顺序,把数据怎么切、网络怎么搭、参数怎么调、坑在哪,完整过一遍。每段都有能直接复制的代码,不是概念演示。
2. 蔬菜价格数据集的处理:从 CSV 到 LSTM 能吃的样本
2.1 数据字段与采样频率的判断
拿到“蔬菜价格数据集”,第一步不是建模,而是先搞清楚它长什么样。常见蔬菜价格数据集的字段大致如下:
| 字段 | 示例 | 说明 |
|---|---|---|
| 日期 | 2023-01-03 | 交易日,注意是否跳过周末 |
| 品种 | 黄瓜、西红柿、菠菜 | 每个品种一个序列,不能混在一起训练 |
| 批发均价 | 3.42 | 单位通常是元/公斤 |
| 最高价/最低价 | 4.10 / 2.80 | 可做辅助特征 |
| 交易量 | 8560 | 批发市场成交量,对价格有滞后影响 |
我一般会先做一次数据体检:看日期连续性、缺值比例、异常值(比如单价为 0 或突然翻倍)。不要在这一步省时间,后面模型不收敛、Loss 变成 NaN,八成要回到这里找原因。
import pandas as pd df = pd.read_csv("vegetable_price.csv", parse_dates=["date"]) df = df.sort_values(["product", "date"]).reset_index(drop=True) # 按品种检查缺失日期和价格异常 for name, grp in df.groupby("product"): grp = grp.set_index("date") full_idx = pd.date_range(grp.index.min(), grp.index.max(), freq="D") missing_dates = full_idx.difference(grp.index) if len(missing_dates) > 0: print(f"{name}: missing {len(missing_dates)} days, e.g. {missing_dates[:3].tolist()}") print(f"{name}: price range = {grp['price'].min():.2f} ~ {grp['price'].max():.2f}")这段代码做的事很直接:按品种分组、排序、检查每一天是否有记录。print 输出的缺失日期列表,能帮你判断是周末休市还是单纯采集中断。如果是休市,就不要人为补 0,否则会把一个“无交易”的假信号灌进模型;如果是采集缺失,用前向填充就够,因为蔬菜价格是连续缓变信号,前一天的成交价本身就是最好的缺失估计。这里也顺带说明:LSTM 建模用的是样本的时序依赖,不是让模型看一整年所有天数的原始序列,所以样本构造方式是下一步的核心。
2.2 滑窗切分:构造监督学习样本的核心方式
LSTM 本身接收的是形如(batch, timesteps, features)的三维张量。原始的一维价格序列必须切成长度为lookback的输入窗口和对应的预测目标,这个“滑窗”操作是所有时序深度学习模型共同的前置步骤。窗口太短学不到周期,太长则引入噪声、增加训练成本,蔬菜数据一般取 7~15 天是合理区间。
import numpy as np def create_sequences(data, lookback=10, horizon=1): X, y = [], [] for i in range(len(data) - lookback - horizon + 1): X.append(data[i : i + lookback]) y.append(data[i + lookback : i + lookback + horizon]) return np.array(X), np.array(y) price = df[df["product"] == "黄瓜"]["price"].values.astype("float32") X, y = create_sequences(price, lookback=10, horizon=1) print(f"X shape: {X.shape}, y shape: {y.shape}")horizon=1表示用前 10 天预测后 1 天,这是最简单也最稳定的设定。如果你做毕设想加亮点,可以把horizon改成 3 或 7,变成多步预测,但多步预测的误差会累积,模型评估起来也复杂得多,建议作为扩展项而不是默认方案。
2.3 归一化方法选择与反归一化时机
LSTM 使用 tanh 和 sigmoid 作为激活函数,输入数值范围如果过大或过小,梯度会很快饱和,训练几乎不动。对价格这类有界正值数据,MinMaxScaler 是最稳妥的选择,把数据缩放到 [0,1] 区间;如果你要预测的是价格变动率(可能为负),就要用 StandardScaler,否则负值会被 MinMaxScaler 截断成 0。
下面这段代码里藏了一个很多人会犯的错,我特意标出来了:
from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler(feature_range=(0, 1)) # 注意:只用训练集拟合 scaler,测试集只做 transform,防止未来信息泄漏 train_len = int(len(price) * 0.8) scaled_price = scaler.fit_transform(price.reshape(-1, 1)).flatten() train_data = scaled_price[:train_len] test_data = scaled_price[train_len:] X_train, y_train = create_sequences(train_data, lookback=10, horizon=1) X_test, y_test = create_sequences(test_data, lookback=10, horizon=1) # 维度调整为 (samples, timesteps, features) X_train = X_train.reshape(X_train.shape[0], X_train.shape[1], 1) X_test = X_test.reshape(X_test.shape[0], X_test.shape[1], 1)要特别强调:scaler.fit_transform只能作用于训练集。曾在实际项目中看到有人对整个序列先归一化再切分,测试集的统计信息已经参与到了归一化参数的拟合中,导致验证集 Loss 显著偏低,但真实上线后模型表现落差很大。对毕设答辩来说,这一点如果被评委问到,你说不清楚会很减分。测试集上的预测结果必须用同一个 scaler 反向变换回原价格单位,才能计算 RMSE 和 MAPE。
3. 基于深度学习 LSTM 的价格预测模型构建
3.1 网络结构设计:LSTM 层数、隐藏单元数与 Dropout 的平衡
搭建 LSTM 模型时,最常见的问题是“层数越深越好”的直觉误导。蔬菜价格序列的复杂度远不及自然语言,单层 LSTM 通常已经能捕捉到主要的价格周期;强行堆到三层以上,不仅训练时间变长,还很容易在训练集上过度拟合,测试集误差反而增大,过拟合在时间序列预测里比分类任务更难察觉。
我的常用配置是:输入层后接一层含 64 或 128 个单元的 LSTM,接 Dropout(0.2),再全连接输出。如果序列长度较长(超过 30 天)或数据量大,才考虑堆两层,并在两层之间加 Dropout。
import torch import torch.nn as nn class PriceLSTM(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=1, 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 if num_layers > 1 else 0, ) self.fc = nn.Linear(hidden_size, 1) def forward(self, x): out, _ = self.lstm(x) # out: (batch, seq_len, hidden) out = out[:, -1, :] # 取最后一个时间步 return self.fc(out)batch_first=True让输入张量维度是(batch, timesteps, features),和numpy里创建样本的维度保持一致,减少维度错乱。out[:, -1, :]取最后一个时间步的隐藏状态做预测,这是序列预测的标准做法——用整个窗口的信息压缩到最后一步输出价格。如果你取全部时间步再平均,会稀释近期信息的权重,效果通常更差。
3.2 损失函数与优化器:MAE 比 MSE 对异常价格更鲁棒
价格预测任务里,MSE(均方误差)是默认选择,因为它处处可导、优化稳定。但蔬菜价格偶尔会出现极端值,比如台风天菠菜一夜翻倍,这种离群样本在 MSE 下会产生巨大的梯度,模型会为了拟合这一天而牺牲正常时段的预测精度。MAE(平均绝对误差)对离群值更鲁棒,但导数是常数,收敛到最优点附近时不够精细。实践里的常见做法是 Smooth-L1 Loss(Huber Loss)。
criterion = nn.SmoothL1Loss(beta=0.5) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, mode="min", factor=0.5, patience=5 )SmoothL1Loss的beta=0.5意味着误差绝对值小于 0.5 时走 MSE,大于时走 MAE,正好兼顾精度和鲁棒性。ReduceLROnPlateau每 5 个 epoch 验证 Loss 不下降就降低学习率,比固定学习率跑到底更实用。Adam 的初始学习率 1e-3 对 LSTM 是安全起点,如果你发现 Loss 在前几个 batch 里震荡上升,优先把 lr 降到 3e-4,而不是换优化器。
3.3 训练循环中的关键细节:验证集切分与早停
训练 LSTM 时,验证集的切分方式与普通机器学习不同。随机打乱数据会破坏时间顺序,正确做法是按时间顺序截取最后 10%~20% 作为验证集。每一次迭代后,用验证集计算 Loss,连续多个 epoch 不下降就早停。
def train_model(model, X_train, y_train, X_val, y_val, epochs=100, patience=10): train_losses, val_losses = [], [] best_val_loss = float("inf") bad_epochs = 0 for epoch in range(epochs): model.train() epoch_loss = 0 for i in range(0, len(X_train), batch_size): batch_X = torch.tensor(X_train[i : i + batch_size], dtype=torch.float32) batch_y = torch.tensor(y_train[i : i + batch_size], dtype=torch.float32) optimizer.zero_grad() pred = model(batch_X) loss = criterion(pred, batch_y) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() epoch_loss += loss.item() val_loss = evaluate(model, X_val, y_val) train_losses.append(epoch_loss / (len(X_train) // batch_size)) val_losses.append(val_loss) if val_loss < best_val_loss: best_val_loss = val_loss bad_epochs = 0 torch.save(model.state_dict(), "best_model.pt") else: bad_epochs += 1 if bad_epochs >= patience: print(f"Early stop at epoch {epoch}") break scheduler.step(val_loss) return train_losses, val_losses这段代码里有两个不能省的细节:梯度裁剪和保存最优权重。clip_grad_norm_把梯度范数限制在 1.0 以内,LSTM 在长序列上很容易梯度爆炸,裁一下训练会稳非常多;torch.save只在验证 Loss 改善时保存,避免最后一轮已经过拟合时把模型权重覆盖掉。早停的patience设为 10 在百轮训练内是均衡值,数据量小可以调小到 5,防止验证集上抖动导致提前退出。
4. 模型评估与训练中那些不在报错里的坑
4.1 逆归一化后的评估指标:RMSE 和 MAPE 各看什么
训练过程中的 Loss 数值是在归一化区间内的,无法直观反映“预测差了几毛钱”,必须把预测结果反变换回来再算指标。RMSE(均方根误差)对大误差更敏感,适合衡量风险;MAPE(平均绝对百分比误差)则直观反映“平均偏差百分之几”,但价格接近 0 时计算结果会爆炸,蔬菜价格一般不会出现这种情况,所以两者可以同时看。
model.eval() with torch.no_grad(): pred_scaled = model(torch.tensor(X_test, dtype=torch.float32)).numpy() pred = scaler.inverse_transform(pred_scaled) y_test_actual = scaler.inverse_transform(y_test) rmse = np.sqrt(np.mean((pred - y_test_actual) ** 2)) mape = np.mean(np.abs(pred - y_test_actual) / y_test_actual) * 100 print(f"RMSE: {rmse:.4f} 元/公斤, MAPE: {mape:.2f}%")RMSE 在 0.3 元/公斤左右对蔬菜批发价算不错;MAPE 能压到 8% 以内说明模型学到了核心规律。评估时我习惯把整个测试期的预测曲线和真实曲线画在一张图上,肉眼看相位是否对齐——有时候指标不差,但预测曲线比真实曲线滞后一两天,说明模型学到的是“把昨天的价格复制过来”,这时候要看特征工程是不是缺失了滞后变量或天气信息。
4.2 LSTM 训练中的两个隐性坑:数据泄漏与随机种子
数据泄漏不止发生在归一化那一步,滑窗本身也可能泄漏。比如你预测未来一天,但滑窗中包含了几小时前刚发生但实际在预测时点之后才产生的数据——这在标准日频数据里不常见,但如果你的数据集粒度是小时级,就要格外小心。毕设项目一般按天采样,这个问题相对小,但答辩时能主动提出来,说明你真理解了时序建模的本质。
随机种子不固定会导致每次训练结果不一样,这在论文和毕设里是致命的——老师跑一次你的代码,结果和你报告里的对不上,印象分会打折扣。
def set_seed(seed=42): np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic = Truecudnn.deterministic = True会牺牲少量训练速度换取可复现性。注意即使设了随机种子,在 GPU 上跑多线程 DataLoader 仍然可能引入不确定性,我的经验是把num_workers设为 0 或者固定worker_init_fn,在毕设场景下 CPU 训练完全够用,不需要折腾这个。
4.3 多步预测的策略:递归预测 vs 直接预测
如果你的项目说明里要求预测未来 3 天或 7 天,那么不能用单步模型连续预测——误差会按步数累积,预测 7 天后的曲线基本变成一条近似水平的线,这个现象在评委演示时非常尴尬。常见做法有两种:
- 递归预测:预测出第 1 天,把它拼到输入序列末尾,再预测第 2 天。实现简单,但误差逐日叠加。
- 直接预测:训练阶段就构造“用前 10 天预测未来第 3 天”的样本,每个预测步单独训练一个模型。精度更高,训练成本翻倍。
def recursive_predict(model, last_seq, steps=7, scaler=None): model.eval() current = torch.tensor(last_seq, dtype=torch.float32).unsqueeze(0) preds = [] with torch.no_grad(): for _ in range(steps): p = model(current) preds.append(p.item()) p_scaled = p.unsqueeze(0) current = torch.cat([current[:, 1:, :], p_scaled], dim=1) return scaler.inverse_transform(np.array(preds).reshape(-1, 1))递归预测时,输入的窗口被逐步挪动,因此需要反复构造新的输入序列,这也是毕设项目里展示“你真正理解时间序列预测”的一个好切入点。直接预测法下样本构造需要单独写一个create_multi_output_sequences函数,目标值改为未来第 k 天的价格,逻辑不复杂,但要在项目说明里写清楚,否则别人看代码会困惑。
5. 用真实场景校验:这个模型到底能做什么
蔬菜价格预测模型放在真实业务里,核心不是“预测明天多少钱”,而是帮助判断采购量、库存周转和定价策略。比如,当 LSTM 预测未来三天黄瓜价格持续上涨时,批发商可以提前囤货或调整订单量;反之则降低库存水位。但要注意,模型预测的是趋势和正常波动,突发性事件如极端天气、市场管控等,LSTM 无法提前预知,它的输出只能作为决策参考的一环,而非唯一依据。
一个具体可落地的做法是把训练好的模型封装成一个简单的 Python 类,对外暴露predict_next_days(product, days)接口,业务方只需要传入品种名称和预测天数,就能得到明天的预测价格。这不仅让代码更整洁,也方便嵌入式部署或在 Web 服务里调用。这类“模型 + 服务化”的结构,在毕业设计展示和实际业务切入里都是加分项。真正有用的模型不是躺在 Notebook 里的那几行训练代码,而是别人能调、结果能解释、边界能说清。
预测结果的稳定性,比单次预测的精度更重要。我的经验是:每次训练完保留模型权重文件,并记录当时的验证集指标和特征参数。下次数据更新时重新训练,对比新旧模型的误差变化,如果新数据下 MAPE 明显上升,优先检查是否出现了新的价格模式(如新品种上线、消费习惯变化),而不是急着调网络结构。
本文还有配套的精品资源,点击获取