尧图精选

基于LSTM的蔬菜价格预测:附Python源码与数据集实战

🕒 发布时间:2026/11/5 8:39:25 📁 来源:尧图网络
简介面向蔬菜价格预测与Web展示的完整Python实战资源以深度学习LSTM为核心覆盖从数据采集到结果呈现的全流程。项目采用Python为主、PHP辅助集成爬虫、数据处理、模型训练与Web/公众号展示适合希望系统学习时间序列预测及项目落地的初中级开发者。压缩包共181个文件包含142个CSV历史价格数据、25个Python源码、2个Word说明文档、2个Markdown文件及少量PYC缓存整体仅1.66MB轻量便于下载与复用。已有247人浏览学习。资源内含完整的项目说明读者可掌握Scrapy/BeautifulSoup爬取蔬菜价格与天气数据、Pandas清洗、Statsmodels/PyFlux等时间序列对比以及通过Flask将预测结果可视化在网页和微信公众号的方法。代码结构清晰便于二次开发是实践数据科学与Web应用结合的不错参考。 做蔬菜批发或生鲜零售的人都有过这种纠结明天该按什么价收货产地涨了三毛库里还有两吨标价高了走不动低了又保不住毛利。靠经验只能赌个大概用传统ARIMA做预测又会被节假日的价格跳变打得找不着北。这就是「基于深度学习LSTM实现蔬菜价格预测」这类项目存在的理由LSTM能记住几十天前的价格形态再结合当天的特征做非线性判断比线性模型更贴合真实菜价的走势。标题里带上了python源码、项目说明和数据集三件套目标很明确——不是讲原理是让你能跑通一个完整的预测流程。适合两类人一是要做毕设或课程设计的学生二是手里攒了历史菜价、想自己搭预测工具的小团队。2. 把价格预测改写成监督学习窗口构造、特征工程与数据划分2.1 滑动窗口把一串价格变成(x, y)样本对LSTM模型不会理解「今天是几号」它只能从输入矩阵里学习模式。所以预测的第一步是把一条连续的价格曲线切成一组组「过去→未来」的样本对这个过程叫滑动窗口。比如手上有60天的黄瓜批发均价想用过去7天的价格预测第8天那么从第0天到第6天是第一个样本的x第7天是它的y然后窗口往后挪一天第1天到第7天是第二个x第8天是y。这样60天数据能切出53个样本。这个「过去几天→未来一天」的映射关系就是LSTM学习的对象。import numpy as np def create_sequences(data, window_size): xs, ys [], [] for i in range(len(data) - window_size): x data[i:i window_size] y data[i window_size] xs.append(x) ys.append(y) return np.array(xs), np.array(ys)上面的函数接受一维价格序列和窗口大小返回两个numpy数组。xs的每个元素形状是(window_size, 1)代表一段连续价格ys是对应的下一个价格。注意循环边界i最大到len(data)-window_size-1这样x不会超出数组范围y也能取到合法值。实际调用时要再reshape成(batch, window_size, 1)因为PyTorch的LSTM层在batch_firstTrue模式下正好要求这个维度顺序。窗口大小是第一个要拍板的超参数。蔬菜价格有星期周期周一和周末的批发价走势明显不同7天是一个合理的默认值生鲜电商如果关心周环比趋势可以试14天想捕捉月度季节性要加到30天左右。但窗口越长训练样本越少——60条数据用7天窗口能切出53个样本用30天窗口只剩30个样本模型还没学够就已经没有数据了。2.2 特征构造日期、节假日、天气与价格缺口的取舍纯价格序列就能跑出一个基线模型但蔬菜价格有明显规律批发市场周初和周末价格不一样节假日之前几天往往涨价极端天气后一周价格跳涨。这些信息如果只靠价格本身LSTM虽然能猜出部分周期但要额外花费大量隐层容量去「硬记」日历规律。我一般建议往特征里加两类信息。第一类是星期几用7个维度的独热编码或者用sin/cos编码让周一和周日相邻避免模型把「周一」和「周日」当作两个无关类别。第二类是节假日哑变量法定节假日前一天和假期当天分别取值1和-1这个特征是零成本的加进去后节假日价格跳变的预测准确度会明显提升。天气和产地供应数据要不要加我的判断是除非数据集里已经按天和市场维度对齐好了否则不要碰。天气数据要按城市、按市场做空间对齐产地受灾信息还需要人工标注工程量和收益不成正比。很多公开的蔬菜价格数据集里根本没有天气字段强行从外部爬取反而引入时间戳对不齐的问题。先把时间特征做扎实模型跑通了再加外部数据这是最稳妥的路线。2.3 时序数据不能乱切训练集、验证集与测试集的顺序划分这个坑我在自己的项目里踩过也帮别人排查时见过很多次用sklearn的train_test_split随机切分价格数据。表面上看模型在测试集上误差非常小实际没有任何参考价值。价格数据有强自相关性相邻几天的价格高度接近随机切分会让测试集里混进与训练样本相邻的日期模型等于提前见过答案。这不是模型厉害是数据划分作弊。时间序列预测的数据划分必须按时间顺序来。我一般按6:2:2切前60%训练中间20%验证最后20%测试三者之间没有重叠total_len len(prices_scaled) train_size int(total_len * 0.6) val_size int(total_len * 0.2) train_data prices_scaled[:train_size] val_data prices_scaled[train_size:train_size val_size] test_data prices_scaled[train_size val_size:]划分完成后三个数据集各自用create_sequences生成样本对。验证集和测试集的样本里x部分可能包含训练集最后几天的价格这是允许的因为滑动窗口的x本来就是历史数据预测目标是未来真正要避免的是训练样本出现在验证集或测试集里。验证集用来调超参数测试集只在最终评估时用一次用多了模型会间接在测试集上过拟合。3. 用PyTorch搭一个可复现的LSTM预测模型结构、参数与代码3.1 最小模型LSTM层与全连接输出的串联不要把这个模型想复杂。LSTM层做的事是读入一段序列逐步更新内部状态最终把整段序列的信息浓缩在最后一个时间步的隐状态里。我们要做的就是把这个浓缩后的状态接一个全连接层映射成预测价格。很多人想翻PyTorch的LSTM源码去理解门控机制其实对落地来说搞清楚输入输出的形状比理解内部公式更关键。nn.LSTM的输入形状是(batch, seq_len, input_size)输出有两个部分每个时间步的隐状态序列以及最后一个时间步的隐状态和细胞状态。对价格预测来说我们只取隐状态序列的最后一个时间步。import torch import torch.nn as nn class PriceLSTM(nn.Module): def __init__(self, input_size, hidden_size, num_layers, output_size): super(PriceLSTM, self).__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue ) self.fc nn.Linear(hidden_size, output_size) def forward(self, x): out, _ self.lstm(x) last_step out[:, -1, :] pred self.fc(last_step) return predforward里有三个关键操作。self.lstm(x)返回的out形状是(batch, seq_len, hidden_size)代表每个时间步的输出out[:, -1, :]取出最后一个时间步因为我们要预测的是窗口结束后的那一个价格final的隐状态和细胞状态我们不关心直接用下划线丢弃。input_size一般等于特征维度只做单品种价格预测就是1加了星期和节假日特征就是3。hidden_size控制记忆容量常见取值32到128。num_layers超过2层对几百条样本的小数据集基本没有收益反而容易在反传时梯度不稳定。3.2 训练循环里要动手调的三组参数代码结构定了真正决定能不能跑出结果的是三组参数序列长度、隐藏单元数、学习率。序列长度window_size是LSTM回头看多远。蔬菜价格周周期明显7到14是比较合理的起点如果数据量大、想抓月周期可以试30。窗口太长训练样本减少还会把无关噪声引入输入。隐藏单元数hidden_size影响模型容量几百条样本用64个单元足够上千条可以试128设太大就是过拟合的开始。学习率用Adam优化器时从0.001起步这是PyTorch里最常见的设置。loss震荡就降到0.0005loss收敛太慢就提到0.003。batch_size在数据量小时用16数据量大用32太大容易让模型在局部震荡里出不来。下面是我常用的参数起点表供你在第一次跑通时对照参数常见起点调整方向与现象window_size7周周期明显往14调过大时训练样本骤减hidden_size64数据量大可试12832明显欠拟合256以上开始过拟合num_layers1到2超过2层梯度不稳定小数据收益接近零learning_rate0.001loss震荡调小loss几乎不动可以试着调大batch_size16到32样本少用16显存不紧张不优先动它3.3 归一化与反归一化别让预测值停留在0到1之间LSTM内部用tanh和sigmoid激活对输入数值范围敏感。原始菜价从几毛到十几块一斤直接送进网络会让梯度计算不稳定所以要先归一化到0到1。常见做法是MinMaxScaler。这里最关键的规则是scaler只fit在训练集上验证集和测试集只做transform。如果拿全量数据fit归一化时就已经「看到」了未来价格的最大最小值这属于最隐蔽的数据泄漏后面会专门讲。from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler(feature_range(0, 1)) train_scaled scaler.fit_transform(train_data.reshape(-1, 1)) val_scaled scaler.transform(val_data.reshape(-1, 1)) test_scaled scaler.transform(test_data.reshape(-1, 1)) # 预测结束后的反归一化 pred_inverse scaler.inverse_transform(pred_scaled.reshape(-1, 1))最后这行inverse_transform特别容易漏。很多新手把模型输出的0.37当预测价格怎么对都对不上就是因为忘了把归一化后的结果还原成「元/斤」。我的习惯是模型训练好之后单独保存scaler状态预测脚本里重新加载避免每次预测都重新fit一次。4. 从原始菜价数据到完整训练脚本清洗、特征工程与训练串联4.1 先看原始数据长什么样字段、粒度与常见脏数据标题里的数据集打包在zip里一般是一张csv或Excel表。常见字段包括日期、蔬菜名称、市场名称、最低价、最高价、平均价、单位有些还带产地和品类编号。粒度通常是「每天×每个市场×每个品种」一条记录这意味着同一品种在同一天会出现在多个市场里不能直接整表丢进模型。拿到数据第一件事不是写模型是打开看一眼。我一般先打印前10行和后10行检查列名、日期格式、空值数量再统计每个品种有多少条记录。这能避免后期跑训练时突然发现某些品种只有二十几天数据窗口一滑就只剩十几个样本。4.2 数据清洗的实操步骤去重、去零、补缺失菜价数据常见的三个坑同一日期同一品种重复采集个别价格录入成0节假日前后整段缺失。逐条处理import pandas as pd df pd.read_csv(vegetable_price.csv, encodingutf-8) df[日期] pd.to_datetime(df[日期]) # 按品种日期去重保留最后一次采集记录 df df.sort_values([蔬菜名称, 日期]) df df.drop_duplicates(subset[蔬菜名称, 日期], keeplast) # 过滤价格明显异常的行 df df[df[平均价] 0] df df[df[平均价] 100] # 只保留数据量足够长的品种 df df.groupby(蔬菜名称).filter(lambda g: len(g) 60) # 按日期排序并重设索引 df df.sort_values([蔬菜名称, 日期]).reset_index(dropTrue)去重用keeplast是因为重复采集通常后录入的价格更接近真实成交价。过滤平均价大于100是为了排除单位录错比如把公斤当斤的离谱值。按品种过滤掉短序列是因为LSTM至少要喂60天以上的数据才能学到周期低于这个量预测结果基本靠猜。4.3 完整训练脚本数据加载、训练、模型保存与预测输出把前面的步骤串成一个完整脚本一次跑通。下面的代码只预测单个品种你可以把黄瓜换成数据集里的任意蔬菜名。import numpy as np import pandas as pd import torch import torch.nn as nn from sklearn.preprocessing import MinMaxScaler def load_prices(df, name): sub df[df[蔬菜名称] name].sort_values(日期) return sub[平均价].values.astype(float) def create_sequences(data, window_size): xs, ys [], [] for i in range(len(data) - window_size): xs.append(data[i:i window_size]) ys.append(data[i window_size]) return np.array(xs), np.array(ys) df pd.read_csv(vegetable_price.csv, encodingutf-8) prices load_prices(df, 黄瓜) scaler MinMaxScaler(feature_range(0, 1)) prices_scaled scaler.fit_transform(prices.reshape(-1, 1)).flatten() total_len len(prices_scaled) train_size int(total_len * 0.6) val_size int(total_len * 0.2) train_data prices_scaled[:train_size] val_data prices_scaled[train_size:train_size val_size] test_data prices_scaled[train_size val_size:] WINDOW 7 X_train, y_train create_sequences(train_data, WINDOW) X_val, y_val create_sequences(val_data, WINDOW) X_test, y_test create_sequences(test_data, WINDOW) X_train torch.tensor(X_train, dtypetorch.float32).unsqueeze(-1) y_train torch.tensor(y_train, dtypetorch.float32).unsqueeze(-1) X_val torch.tensor(X_val, dtypetorch.float32).unsqueeze(-1) y_val torch.tensor(y_val, dtypetorch.float32).unsqueeze(-1) X_test torch.tensor(X_test, dtypetorch.float32).unsqueeze(-1) y_test torch.tensor(y_test, dtypetorch.float32).unsqueeze(-1)这段代码有两点要说明。torch.tensor转换时明确指定dtypetorch.float32是因为LSTM不支持int64输入原始价格一旦是整数类型就会直接报错。unsqueeze(-1)是把输入从二维变成三维即在最后补一个特征维度让形状从(样本数, 窗口长度)变成(样本数, 窗口长度, 1)这是nn.LSTM要求的最小维度。训练部分用标准的PyTorch循环配合早停逻辑验证集loss连续10个epoch不降就停止防止过拟合model PriceLSTM(input_size1, hidden_size64, num_layers1, output_size1) optimizer torch.optim.Adam(model.parameters(), lr0.001) criterion nn.MSELoss() train_dataset torch.utils.data.TensorDataset(X_train, y_train) train_loader torch.utils.data.DataLoader(train_dataset, batch_size16, shuffleTrue) best_val_loss float(inf) patience 10 wait_count 0 for epoch in range(100): model.train() for x_batch, y_batch in train_loader: optimizer.zero_grad() pred model(x_batch) loss criterion(pred, y_batch) loss.backward() optimizer.step() model.eval() with torch.no_grad(): val_pred model(X_val) val_loss criterion(val_pred, y_val) if val_loss best_val_loss: best_val_loss val_loss torch.save(model.state_dict(), lstm_price_model.pt) wait_count 0 else: wait_count 1 if wait_count patience: break if (epoch 1) % 10 0: print(fepoch {epoch 1}, loss {loss.item():.4f}, val {val_loss.item():.4f})shuffleTrue这个位置容易踩坑。训练集可以shuffle因为每个样本是独立窗口但验证集和测试集不能shuffle它们要按时间顺序评估模型对连续未来日期的预测能力。早停保存的是验证集上最好的那版参数而不是最后一个epoch的参数这个细节能让最终模型鲁棒很多。5. LSTM价格预测避坑指南五条写在注释里的踩坑记录5.1 全量归一化等于偷看未来数据泄漏的典型症状现象训练loss正常下降验证loss一直接近0测试集表现也好得反常。这时候不是模型厉害而是归一化泄漏了。原因MinMaxScaler在全量数据上fit意味着train、val、test三段的数值都被压缩到同一个0到1区间而这个区间的上下界来自整段数据里的最高价和最低价。如果最高价恰好出现在测试集里模型在训练时就已经知道未来会出现这个极值归一化后的价格分布被整体拉平了局部波动变得更容易预测。解决scaler只fit在train_data上val和test只调用transform。代码上改动一行效果天差地别。判断自己有没有踩这个坑最简单的方法是看scaler.data_min_和scaler.data_max_这两个属性如果它们来源于全量数据就说明泄漏了。5.2 验证集随机切分价格突变被平均之后的虚高指标现象验证集和测试集的MAE都小得惊人但把模型接到真实场景里预测第二天价格时准度明显下滑。原因随机切分让验证集里散布着训练集前后几天的数据这几天价格高度相关模型几乎是在做复制粘贴而不是预测。价格突变比如下暴雨前一天暴涨会被随机分配到训练集和验证集两侧模型在训练时已经见过了突变后的价格水平验证时自然「未卜先知」。解决坚持按时间顺序切分换模型、换特征时只在验证集上调参最终评估只碰一次测试集。这条规则没有例外任何用随机切分跑出来的时序指标都应该标注「仅供参考」不能作为投入依据。5.3 序列长度不是越大越好较长窗口带来的过拟合现象window_size从7调到30后训练loss下降更快但验证loss反而升高预测曲线变得毛躁出现很多不合理的尖峰。原因窗口越长样本数越少同时LSTM需要学习的时序依赖越长。对一个只有两三百天数据的品种来说30天窗口把样本数砍掉一大截模型容量没变数据却不够喂了必然过拟合。蔬菜价格的周周期非常强7天窗口已经能覆盖一个完整的价格形态加到30天增加的更多是噪声而不是信息。解决我习惯的做法是先在7、14、21三档上分别跑一个训练看验证loss选最低的那档。如果三档差别不大选最小的窗口因为它训练最快、部署时对数据量要求也最低。窗口参数和隐藏层大小是联动的窗口加大的时候要同步减少hidden_size否则模型参数量翻倍过拟合风险成倍增加。5.4 Loss降了但预测是一条直线学习率与梯度问题现象训练loss在下降但把测试集的预测曲线画出来几乎是一条水平直线价格波动完全没有被预测出来。原因数据归一化后价格序列的方差如果本身就很小尤其是某些常年稳定的蔬菜比如土豆模型很快发现预测均值比预测波动更能降低MSE于是「躺平」输出了一个常数。另一个常见原因是学习率过大loss在最小值附近震荡参数始终没有落进能捕捉波动的局部谷底。解决先看归一化后数据的标准差如果小于0.05说明序列本身太平稳LSTM的收益有限不如直接拿上一日价格当预测值。如果方差正常把学习率从0.001降到0.0003重跑同时给LSTM层加weight_decay逼模型放弃复制均值这种懒惰解。用测试集真实值和预测值画一张散点图如果点都集中在一条45度线附近说明模型只学到了滞后复制没有学到独立预测。5.5 预测值总滞后一天单步预测的固有毛病现象模型跑通了但预测曲线总比真实曲线慢半拍价格涨了才跟着涨跌了才跟着跌像是昨天价格的平移。原因单步预测模型在训练时输入是过去7天真实价格输出是第8天。因为相邻天价格高度相关模型发现把第7天价格稍微调整一下就能把loss压得很低于是学成了一个「平滑器」而不是「预测器」。这是单步监督学习在价格预测里的通病不是模型结构的问题。解决一方面可以改训练目标用价格的差分序列今天减昨天代替原始价格序列作为学习目标让模型学习涨跌而不是复制绝对价格。另一方面可以引入迭代多步预测把模型输出的第8天价格拼回输入窗口再预测第9天强迫模型基于自己生成的数据做推理这能显著减少滞后感。滞后问题不可能完全消除但能通过这两个手段把预测曲线和真实曲线错开的程度控制在合理范围内。6. 把单步预测推进到多步预测迭代预测、滚动验证与模型重训时机6.1 迭代多步预测用模型自己的输出当输入如果业务上要预测未来7天的价格走势单步预测就需要跑7次迭代。每次把预测出的新价格拼接进窗口同时丢掉窗口最早一天的数据窗口长度不变内容逐步更新。def iterative_predict(model, last_window, steps, scaler): model.eval() window last_window.clone() preds [] with torch.no_grad(): for _ in range(steps): input_tensor window.unsqueeze(0).unsqueeze(-1) pred model(input_tensor).squeeze() preds.append(pred.item()) window torch.cat([window[1:], pred.reshape(1)]) return scaler.inverse_transform(np.array(preds).reshape(-1, 1))window[1:]丢掉最早一天pred.reshape(1)接在末尾窗口始终是过去7天。这里有一个关键问题迭代预测的误差会累积第一步预测偏了第二步就用这个偏差值当输入后面越偏越远所以迭代步数越多预测可信度越低。对蔬菜价格这种波动大的数据迭代到第5天以后基本只有方向性参考价值绝对价格不要拿来下采购决策。6.2 滚动验证与模型重训时机误差超过阈值就回炉多步预测做出来后需要一套评估方法判断模型还有没有用。我的做法是在测试集上做滚动验证从测试集起点开始每天用模型预测第二天价格把真实价格加入历史窗口再预测下一天记录每天的MAE。滚动验证模拟的是真实使用场景——每天早上拿最新一天的真实价格更新模型输入。如果滚动MAE连续7天超过历史平均MAE的1.5倍说明市场结构可能变了换了供应商、极端天气影响、政策干预这时候需要重训模型。重训时不要只追加新数据要保留原始训练集和验证集的划分逻辑把新数据滚动进入训练集同时把窗口按同样比例扩展避免数据泄漏。我的习惯是每周五收盘后跑一次滚动验证连续两周达标就不动模型一旦超过阈值当天重训并记录触发原因下次同类场景出现能直接判断该不该干预。这套流程跑下来LSTM不会给你一个「准确到分」的价格但它能帮你把预测误差控制在一个明确的范围里告诉你什么时候该相信模型什么时候模型失效了要换规则。我自己最深的教训是不要追求验证集上loss最低的模型要追求连续预测时最稳的模型。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →