简介面向计算机专业毕业设计场景这份基于深度学习LSTM的蔬菜价格预测项目提供完整Python源码、项目说明和配套数据集。项目围绕蔬菜价格时间序列预测任务覆盖数据预处理、特征工程、LSTM模型构建、训练与评估等环节适合作为毕业设计、课程设计或期末大作业也适合希望掌握时序预测实战技巧的学习者。代码结构清晰说明文档配合数据可快速理解建模思路。压缩包共181个文件约1.86MB其中142个CSV文件为多种蔬菜历史价格数据25个Python源码用于模型训练、预测与可视化3个docx为项目说明报告还有少量pyc编译文件与markdown文档便于按索引逐步复现实验。该项目经导师指导并认可评审分98分已有271人学习浏览。对于正在准备毕设的学生这份资料既提供可直接运行的项目蓝本也展示了从数据清洗到结果分析的完整链路具有较强参考与二次开发价值。1. 基于深度学习的蔬菜价格预测一份能在答辩现场讲清楚的LSTM毕设蔬菜价格预测是典型的时间序列回归问题用深度学习里的LSTM神经网络来做既能体现代码建模能力又有真实业务场景托底所以一直是计算机专业毕设和课程设计的高频选题。我手上这份基于深度学习LSTM实现蔬菜价格预测的完整项目包含python源码、项目说明和8个蔬菜品种的CSV数据集本地菜心、云南小瓜、小塘白菜、本地芹菜、矮脚白菜、青皮冬瓜、西红柿、红尖椒每个文件都是独立的价格序列。它的适用人群很明确正在写毕设或课程设计的学生以及想快速跑通一个LSTM价格预测全流程、理解时间序列建模细节的学习者。接下来我会从数据预处理、模型搭建、训练评估、避坑排查到最终的论文呈现把这个项目的每个环节拆开讲。2. 数据预处理把8个蔬菜CSV整理成LSTM能吃的时间序列样本2.1 先看原始数据CSV列结构、编码和文件名三个坑打开压缩包的数据目录能看到8个CSV文件分别对应本地菜心、云南小瓜、小塘白菜、本地芹菜、矮脚白菜、青皮冬瓜、西红柿、红尖椒。文件名直接是蔬菜名称这种命名方式在毕设数据集里很常见写代码时反而方便可以用一个列表把所有文件名读进来批量处理不用为每个品种单独写一段加载逻辑。每个文件对应一个品种的日度价格记录行数大致在几百到一千多之间波动。用pandas读入数据以后第一件事是看shape、dtypes和head()别急着画图或建模import pandas as pd df pd.read_csv(data/本地菜心.csv, encodingutf-8-sig) print(样本条数:, df.shape[0], 列数:, df.shape[1]) print(字段类型:\n, df.dtypes) print(前5行:\n, df.head(5))编码用utf-8-sig的原因是带中文列名的CSV在Windows下导出时经常带BOM头直接用utf-8读会把第一列列名读成“\ufeff日期”后面所有对列名的引用都会报KeyError。如果读进来列名正常说明源文件本身没有BOM头改成utf-8也无所谓。第二个值得做的事是打印df.columns确认列名因为我见过有的价格CSV列名是“日期/价格”有的是“date/price”还有的多出一列“涨跌幅”。这个项目的8个CSV结构基本一致所以可以写一套清洗流程直接跑8个品种这是毕设项目里比较省时间的处理方式。把8个文件统一读入并做一个总的概览是个容易在白板演示时加分的小操作import os import pandas as pd file_list [本地菜心.csv, 云南小瓜.csv, 小塘白菜.csv, 本地芹菜.csv, 矮脚白菜.csv, 青皮冬瓜.csv, 西红柿.csv, 红尖椒.csv] for fname in file_list: path os.path.join(data, fname) df pd.read_csv(path, encodingutf-8-sig) print(fname, -, df.shape, 列名:, list(df.columns))这段代码会把8个文件的形状和列名一次性打印出来用在数据章节的配图说明里非常直观。如果某个文件和其他的列名不一致在这一步就能暴露出来不用等到训练阶段才在各种报错里翻车。2.2 清洗顺序缺失值、重复值、异常值一次处理完时间序列数据最怕的不是个别缺失而是缺失之后产生错位。今天的价格记录丢了没有处理的话后续所有价格在时间轴上就偏移了一天模型训练时会把偏移后的序列当成正常序列学预测结果越练越错。清洗阶段我一般按固定顺序走完日期转datetime类型、按日期排序、查缺失值、查重复行、查异常价格。df[日期] pd.to_datetime(df[日期]) df df.sort_values(日期).reset_index(dropTrue) print(日期范围:, df[日期].min(), 到, df[日期].max()) print(缺失值:\n, df.isnull().sum()) print(重复行:, df.duplicated().sum()) print(价格统计:\n, df[价格].describe())把日期列统一成datetime类型之后排序才有意义因为价格预测的滑窗完全依赖样本顺序如果CSV里日期是乱序的后面切出来的每一个X和y对都是错位的模型学到的东西全是噪声。describe()能快速看到价格的最小值、最大值、标准差和分位数如果出现价格为0或者负数基本可以直接判为异常值后续处理时直接过滤掉。缺失值的处理方式这里不建议直接用dropna删除中间行因为删除会把连续的时间序列切断本来今天和昨天是有连续性的删掉中间一天模型看到的走势就多了一个断裂。除非缺失率极低低于1%否则更稳妥的做法是用前向填充df[价格] df[价格].ffill() df df.dropna().reset_index(dropTrue) df df[df[价格] 0] print(清洗后样本数:, len(df))ffill()的逻辑是拿缺失时刻前最近的一个有效值来补适用于价格这种变化相对平滑的序列缺一两天时误差可控。后面的dropna解决的是文件开头就连续缺失的情况最后一行的过滤条件把0价和负价一起排除掉。做完这三步每个品种都得到一条干净、连续、有序的价格序列可以放心进入滑窗和归一化阶段。2.3 滑窗采样从一维价格序列构造出X和yLSTM的输入是三维的形状是(样本数量, 时间步长, 特征个数)。对单变量价格预测最常见的做法是用过去N天价格预测第N1天的价格这个N就是滑窗大小也叫lookback或时间步长。它是一个直接决定模型视野的参数设得太小比如3天模型看不到任何周期性规律预测结果跟瞎猜差不多设得太大比如60天训练样本数量会显著下降而且数据里未必有足够长的历史支撑这个窗口。这个数据集单条序列几百到一千多条用14天作为默认窗口是比较稳的——恰好是两周的日度价格取的是一个相对完整的短期波动周期。import numpy as np def create_sequences(data, lookback14, predict_days1): X, y [], [] for i in range(len(data) - lookback - predict_days 1): X.append(data[i : i lookback]) y.append(data[i lookback : i lookback predict_days]) return np.array(X), np.array(y) X, y create_sequences(values, lookback14) print(X的形状:, X.shape, - y的形状:, y.shape)这个函数的逻辑是把一个长度为L的一维数组切成长度为lookback的连续片段作为X每个片段后面predict_days个值作为y。比如L等于500、lookback等于14、predict_days等于1最终能切出大约486个样本。注意这个数字是L减去14再减去1再加1不要把它当成L除以14否则会以为样本数少了很多。在数据量只有几千条的情况下用for循环构造序列完全够用不用追求复杂的窗口类优化。predict_days控制的是预测步长这里有两种主流的做法一是让模型只预测明天一天单步预测二是让模型一次输出未来7天。后者只需要把输出层Dense(1)改成Dense(7)其余结构不用动但训练难度会大一些因为要求模型同时做好7个不同时间尺度的拟合。这个项目默认按单步预测跑滚动预测的扩展方式放到最后一章展开。2.4 归一化MinMaxScaler的fit和transform时机别搞混价格序列的数值范围直接影响LSTM训练的稳定性。西红柿两三块钱一斤红尖椒十几块钱一斤直接把原始数值喂给模型大数值特征会主导梯度更新小数值特征几乎学不到东西。所以训练之前必须做归一化常用的是MinMaxScaler把价格映射到[0,1]区间再进模型。这里最容易翻车的坑是把全量数据直接fit_transform之后再切训练集和测试集。从工程严谨性的角度应该先按时间顺序切分再对训练集做fit然后用拟合好的scaler对测试集做transform。原因是MinMaxScaler会记录序列的min和max测试集一旦参与fit它的统计信息就等于提前暴露给了训练阶段最终评估出来的指标会虚高答辩时老师深问一句就会被问住。from sklearn.preprocessing import MinMaxScaler train_size int(len(scaled) * 0.8) train scaled[:train_size] test scaled[train_size:] scaler MinMaxScaler(feature_range(0, 1)) train_scaled scaler.fit_transform(train.reshape(-1, 1)).flatten() test_scaled scaler.transform(test.reshape(-1, 1)).flatten()代码的核心是test_scaled用了transform而不是fit_transform也就是说测试集的归一化完全复用训练集学到的min和max保证训练和测试处于同一个度量体系。这样预测完成之后反归一化也只需要一个scaler就能还原真实价格。我第一次做这个项目的时候在图省事全量数据上fit_transform测试集MAE低得很好看但换一个品种数据结果就崩了后来定位到就是归一化泄漏。3. 构建LSTM预测模型网络结构、超参数与训练配置3.1 为什么是LSTM门控机制如何建模价格走势蔬菜价格序列有几个特点带有明显的趋势性、季节性还有突发因素带来的随机波动。普通全连接网络在处理这类序列时有个根本短板——输入特征是相互独立的模型没有机制感知先后顺序。普通RNN虽然能处理顺序但梯度在反向传播中会随步数增长而消失或爆炸长距离依赖学不到。LSTM在其中加入了门控机制三个门遗忘门、输入门、输出门分别决定历史信息丢多少、新信息写多少、当前信息输出多少这让它能够保留重要的长期依赖同时忽略噪声。放在价格预测场景里LSTM能记住“半个月前价格处于低位”这样的长周期状态同时过滤掉昨天偶然出现的一次异常报价。这种能力是价格序列建模中最需要的。而且Keras里实现LSTM非常简单几行代码就能搭好不需要自己手写反向传播这也是它成为毕设常用模型的原因之一。模型定义之前先把输入输出形状理清楚输入是(样本数, 14, 1)14就是之前切好的滑窗大小1代表单特征只有价格输出是(样本数, 1)代表第15天的预测价格。3.2 两层LSTM加Dropout模型结构怎么搭对这个数据量不算大的单变量序列预测我推荐一个不太容易出问题的结构两层LSTM加两层Dropout最后接一个全连接输出。这个结构在特征提取和防止过拟合之间比较平衡。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout model Sequential([ LSTM(units64, return_sequencesTrue, input_shape(lookback, 1)), Dropout(0.2), LSTM(units32, return_sequencesFalse), Dropout(0.2), Dense(units1) ]) model.summary()第一层LSTM的units64代表64个记忆单元return_sequencesTrue保证它输出三维序列给第二层LSTM第二层LSTM的units32return_sequencesFalse只保留最后一个时间步的输出把它压缩成二维。Dropout(0.2)的意思是每次训练迭代随机丢弃20%的神经元输出目的是让网络不依赖某几个特定神经元从而减轻过拟合。最后一层Dense(1)把32维特征压缩成1个预测价格。units的取值没有绝对标准我一般看训练集规模来定。这个项目每个品种滑窗之后样本量大概几百个units取32到64比较稳。如果数据量大、价格波动剧烈可以尝试128但过拟合风险会同步上升。Dropout取0.2到0.3足够太高了会造成训练不充分loss压不下去。3.3 编译与训练优化器、损失函数和早停的配合回归问题的默认损失函数是均方误差MSE优化器选Adam这是LSTM价格预测场景里最稳妥的组合。Adam自带自适应学习率对初始学习率不那么敏感对新手很友好。如果换成SGD需要手动调学习率和动量训练过程不稳定是大概率事件。from tensorflow.keras.callbacks import EarlyStopping model.compile(optimizeradam, lossmse, metrics[mae]) early_stop EarlyStopping( monitorval_loss, patience15, restore_best_weightsTrue ) history model.fit( X_train, y_train, validation_data(X_val, y_val), epochs100, batch_size32, callbacks[early_stop], verbose1 )参数的含义分别是epochs100是最大训练轮数实际不会真的跑满100轮因为EarlyStopping会在val_loss连续15轮不再下降时自动中断训练并恢复效果最好的那轮权重batch_size32表示每32个样本做一次梯度更新这个值在几百到上千的样本量下比较合适。batch_size设太大会让每个epoch的梯度更新次数太少设太小比如4训练曲线会抖动得非常厉害。需要注意训练集和验证集的切分时机。我通常在归一化之后、滑窗之前按时间顺序切好训练集和验证集这能避免滑窗产生的重叠窗口混进不同集合造成数据泄漏。验证集和测试集是两个概念验证集用于早停和观察过拟合测试集是最后才用的。毕设里可以把最后20%数据当测试集从训练集里再切出最后10%当验证集这样分工清晰。3.4 多维输入扩展加入更多特征时要注意什么蔬菜价格预测这个选题如果只预测单变量模型结构会显得稍微单一。一个很容易扩展的方向是加入更多特征比如同一天的天气温度、节假日标记、前一天的成交量、甚至不同蔬菜之间的价格联动。一旦输入变成多维X的形状就从(样本数, 14, 1)变成(样本数, 14, 特征数)模型代码只要改一处model Sequential([ LSTM(units64, return_sequencesTrue, input_shape(lookback, n_features)), Dropout(0.2), LSTM(units32, return_sequencesFalse), Dropout(0.2), Dense(units1) ])这里n_features就是特征数量价格只是其中一列。注意在构建滑窗时create_sequences函数的输入就得从一维数组换成二维数组data[i : ilookback]切片的结果形状是(lookback, n_features)y则始终只取价格那一列。动手扩展之前先明确一点多一个特征就多一列需要清洗和归一化的数据尤其是温度、天气这类数值量级和价格差异很大的列必须分别归一化不能共用同一个scaler。4. 训练评估与可视化从损失曲线到预测对比4.1 训练过程的判断loss曲线告诉你的三件事模型训练完第一件事不是去看预测图而是看loss曲线。一个健康的训练过程train_loss和val_loss应该是同步下降的并且两者之间的差距始终不大。如果train_loss降得很快而val_loss不降反升说明模型开始过拟合此时优先调整Dropout或减小units。如果两个loss都很高且下降缓慢问题大概率不出在模型结构而在前面的数据预处理环节比如归一化没做、滑窗数据有错位或者特征里有大量异常值。画loss曲线用matplotlib几行就够import matplotlib.pyplot as plt plt.figure(figsize(10, 4)) plt.plot(history.history[loss], labeltrain_loss) plt.plot(history.history[val_loss], labelval_loss) plt.xlabel(epoch) plt.ylabel(loss) plt.legend() plt.title(训练与验证损失曲线) plt.show()如果loss曲线剧烈抖动先检查batch_size和学习率如果val_loss长期比train_loss高一倍以上先检查模型容量和Dropout。按这个顺序排查往往比盲目改模型结构高效得多。这张图也是论文里必放的图之一所以画图时记得把横纵坐标标签和legend都加上别拿一张没标注的图塞进论文。4.2 反归一化与评价指标MAE、RMSE、MAPE怎么算模型预测出来的结果还是归一化值要算真实价格误差必须先反归一化。这个步骤非常容易出错尤其是反归一化时用错了scaler。整个项目只应该出现一个scaler就是我们第2章在训练集上fit出来的那个train_pred model.predict(X_train) test_pred model.predict(X_test) train_pred_price scaler.inverse_transform(train_pred) test_pred_price scaler.inverse_transform(test_pred) train_y_price scaler.inverse_transform(y_train) test_y_price scaler.inverse_transform(y_test)k在这里train和test都是同一个scaler反归一化之后的价格单位和原始数据完全一致。如果中途不小心用了另一个scaler反出来的价格量纲就会对不上画图时会发现曲线整体偏移。评估指标我建议MAE、RMSE、MAPE三个都算评价不同品种时结合起来看才全面from sklearn.metrics import mean_absolute_error, mean_squared_error train_mae mean_absolute_error(train_y_price, train_pred_price) test_mae mean_absolute_error(test_y_price, test_pred_price) test_rmse mean_squared_error(test_y_price, test_pred_price, squaredFalse) def mape(y_true, y_pred): return np.mean(np.abs((y_true - y_pred) / y_true)) * 100 test_mape mape(test_y_price.flatten(), test_pred_price.flatten()) print(f测试集 MAE: {test_mae:.3f} 元, RMSE: {test_rmse:.3f} 元, MAPE: {test_mape:.2f}%)MAE代表平均绝对误差单位是元比如西红柿测试集MAE是0.35意味着平均每次预测值和真实值差三毛五。RMSE因为先平方再开方会对较大误差施加更多惩罚如果RMSE明显高于MAE说明部分样本的预测误差特别大。MAPE用百分比消除了价格基数的影响是横向比较不同品种最合适的指标。4.3 8个品种的预测效果比较怎么横向比才公平跑完8个品种之后测试集MAE和MAPE的差异还是比较明显的大致规律可以归纳为下表。注意这里的数值区间是为了说明相对关系不代表每个数据集固定如此品种价格量级波动特征MAPE整体表现备注青皮冬瓜偏低平缓相对低序列平稳容易学西红柿中等较平稳中等偏低规律性较好本地菜心偏低剧烈相对高季节性突变多红尖椒偏高剧烈看基数绝对误差大但占比较低直接用MAE对比不同品种其实不公平因为价格基数不同。红尖椒均价十几块本地菜心可能只有几块同样0.5元的误差在菜心上占比远远高于红尖椒。所以论文里横向比较时用MAPE纵向分析单品种时用MAE和RMSE这样每个指标都用在了合适的位置。8个品种的预测结果放在一张总表里答辩时老师一眼能看到你做了充分的实验验证。4.4 训练完的模型怎么复用保存与加载毕设项目里模型训练完通常要在测试阶段反复加载使用不可能每次预测都重新训练一遍。保存模型用一行代码加载也是model.save(lstm_price_model.h5) from tensorflow.keras.models import load_model loaded_model load_model(lstm_price_model.h5)保存时建议把模型结构和训练好的权重一起存进h5文件后面加载出来直接对测试集做predict就行不要再经过compile。如果还需要复核训练日志可以用model.save_weights只存权重配合代码里的模型结构定义使用。我一般每个品种保存一个带品种名的模型文件比如lstm_本地菜心.h5这样后期做滚动预测或者写API接口时能快速加载对应模型。5. 避坑指南LSTM价格预测里最常见的五个坑这一章的内容来自我自己跑毕设和帮别人调代码时遇到的高频问题每条都按“现象→原因→解决”来写希望能帮你绕开这些坑。5.1 现象loss不下降训练曲线成了一条直线原因大概率出在数据没有归一化或者归一化范围不对。价格序列如果是几十上百的大数值LSTM的梯度计算会很不稳定Adam的自适应学习率虽然能缓解一部分问题但面对分布极端的输入照样会卡住。还有一种情况是数据里有极端大值比如某天价格突然冲到20块正常MinMax归一化之后这个值附近的梯度会异常大让整个训练过程震荡。解决方法是先检查训练数据的数值范围确认在[0,1]或者[-1,1]区间再把数据分布画出来看有没有离谱的尖峰。如果归一化没问题就把学习率从Adam默认值调低比如显式设置learning_rate0.001或0.0005通常能解决loss不降的老大难问题。5.2 现象预测曲线和真实曲线形状很像但整体滞后一天这个是很经典的时间序列预测滞后效应原因通常是目标值对齐错了。用过去14天预测明天X的最后一个时间步应该对应t-1当天的数据y对应t1当天。如果代码里不小心把y取成了X窗口最后一天的值模型学到的其实是“今天的价格约等于昨天的价格”预测结果还好但本质上是抄作业没有预测能力。判断方法很简单把y_true和y_pred的前20个点打印出来对比。如果预测值总是等于真实值的前一天价格那就是对齐错了。回头仔细检查create_sequences里的索引逻辑确认ilookback对应的位置比X最后一个时间步至少大1。5.3 现象测试集效果反而好于训练集测试集表现比训练集好看上去是好事其实是不正常的信号十有八九是信息泄漏。最常见的泄漏来源有两个一是在全量数据上做MinMaxScaler的fit测试集的min和max提前参与了训练阶段的统计二是切分数据集时没有按时间顺序随机打乱了样本让模型在训练时直接看到未来数据。解决方法是严格按时间顺序切分先归一化只在训练集上fit再做滑窗最后切训练集、验证集、测试集。这个顺序不能乱任何一步做反了都会引入不同程度的泄漏。泄漏最可怕的地方在于它在测试集上指标很好看会让你误以为模型已经做得很好了等换到全新数据才彻底暴露。5.4 现象预测结果趋近于均值曲线基本是平的如果预测曲线接近一条直线数值在均值附近小幅震荡说明模型没有学到价格的真实变化规律退而求其次选择了“预测均值”这种最保守的输出。原因主要有两个滑窗太小模型看不到足够的历史模式模型容量不足LSTM层数太少或者units太小非线性拟合能力有限。解决的方法是先把lookback从3-5天提高到14天或30天再看模型的units是不是太小比如只有8或16这个数量级确实偏小适当提高到32或64。另外检查训练轮数如果EarlyStopping在val_loss还在持续下降时过早截断也会出现学不到位的情况这时候可以把patience调大到20再试。5.5 现象不同品种合并训练整体效果被拉垮有些学生会试图把8个品种的数据合并成一个数据集来训练统一模型理由是数据量更大。这个想法听起来合理实际做起来结果往往不理想。因为不同蔬菜的价格基数和波动形态差异太大西红柿两三块钱红尖椒十几块钱它们的时间序列分布完全不同共享同一个归一化scaler和一个模型会让模型在多个价格区间之间来回摇摆最终每个品种都学不好。解决方法是每个品种单独训练一个模型或者按价格量级分组后再建模。这个项目的数据规模完全支持每个品种单独训练训练耗时也不长。单独建模的另一个好处是8个品种的8套训练结果本身就构成了丰富的实验对比论文的实验章节会充实很多。6. 做得更可信滚动预测与答辩材料呈现6.1 滚动预测连续预测未来7天的一种简单实现单步预测能证明模型“知道”明天价格的大致区间但答辩时老师多半会追问一句“你能不能预测未来一周的价格”这时候滚动预测就很实用。用前14天预测第15天然后把预测值当作新的输入接到序列末尾继续预测第16天如此循环就能得到未来7天的预测序列。def rolling_forecast(model, initial_sequence, predict_days7): seq initial_sequence.copy() predictions [] for _ in range(predict_days): pred model.predict(seq.reshape(1, lookback, 1)) predictions.append(pred[0, 0]) seq np.append(seq[1:], pred[0, 0]) return np.array(predictions)这段代码的逻辑是每次用当前长度为lookback的窗口做一次预测得到预测值后把窗口最前面一天丢掉把新预测值接到末尾构成一个新窗口继续预测下一天。要提醒的是滚动预测会把误差逐步累积第一天的小偏差经过7次迭代会被放大所以滚动预测的误差通常比单步预测大这是正常的不需要去改模型。论文里如果能同时展示单步预测和7天滚动预测的结果对比会让工作显得完整很多。6.2 论文里放什么两张图、一张表和一张超参数表关于实验成果的呈现,我会建议固定放四样东西训练与验证loss曲线图、测试集真实值与预测值对比图、三个指标MAE、RMSE、MAPE的品种对比表、以及一张关键超参数表。对比图里真实价格用深色线预测价格用浅色线按时间顺序画在同一张图上一眼能看出贴合程度。超参数表单独列出滑窗大小、LSTM层数、units、Dropout比例、学习率、epochs、batch_size放在实验设置一节。这些小细节能显著提升答辩质感和可信度也是以后做任何时间序列预测项目可以复用的习惯。从那以后我每次跑价格预测类的项目都会先写超参数表再开始调参模型效果不好时对着表逐项排查而不是无头苍蝇一样乱试。这样的过程和文档也能给后续接手的人留下清晰的复现路径希望帮到你。本文还有配套的精品资源点击获取
