ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

LSTM实战:蔬菜价格预测从数据清洗到模型调参

LSTM实战:蔬菜价格预测从数据清洗到模型调参 简介这是一份基于深度学习LSTM的蔬菜价格预测完整项目资源适合学习时间序列预测、爬虫与Web展示的Python开发者。项目实现了从菜价与天气数据爬取、清洗到以LSTM为主并与时间序列、神经网络方法对比的预测流程同时包含Flask Web展示与微信公众号查询模块覆盖数据获取到产品落地的闭环。资源共181个文件核心包括142个CSV蔬菜价格历史数据、25个Python源码爬虫、数据处理、预测与Web脚本以及项目说明文档压缩包仅1.78MB便于快速下载和复用。目前已有251人学习适合需要参考完整业务场景、希望结合爬虫与深度学习的入门及中级开发者。通过阅读代码可掌握Scrapy/BeautifulSoup、Pandas、Statsmodels、Flask在实际项目中的协作方式。1. 蔬菜价格预测为什么选中LSTM一条价格序列里的时间依赖做蔬菜价格预测的人十有八九先试过线性回归或ARIMA然后发现预测曲线在价格剧烈波动时完全失灵。蔬菜价格和股票、气温一样本质上是带明显季节性和随机扰动的时间序列今天的价格和过去几天的价格存在强关联但又不是简单的重复。LSTM长短期记忆网络作为深度学习里专门处理序列数据的网络结构能在训练中自动学到前几天涨价后几天倾向回落这类时间依赖模式而不是靠人工硬编码规则。这个项目把价格预测做成了一条完整链路原始行情数据 → 清洗构造样本 → LSTM训练 → 输出未来价格。适合手里有蔬菜行情记录、想用深度学习做短期价格预判的从业者也适合拿时间序列练手的 Python 开发者。下面按实际落地的顺序把每一步拆开讲。2. 蔬菜价格数据集的处理从原始行情到LSTM能吃的样本2.1 数据采集与字段说明一份价格记录里哪些列能用常见做法是拿到一份带时间戳的蔬菜批发价记录字段大致包括日期、蔬菜品种、市场名称、最低价、最高价、平均价、交易量。不是所有列都有用。LSTM预测价格核心输入是连续日期的同品种价格序列所以日期和均价两列是主料交易量可以作为辅助特征能提升模型对供需扰动的敏感度但前提是数据记录完整。市场名称和品种名称是分组键用于把数据拆成单品种、单市场的独立序列。import pandas as pd df pd.read_csv(vegetable_prices.csv, parse_dates[date]) # 取某市场某品种的数据避免不同市场价差干扰模型 df df[(df[market] 中心批发市场) (df[product] 黄瓜)] df df[[date, avg_price, volume]].sort_values(date) df df.set_index(date)这段代码先按市场和品种筛出同一口径的数据再排序保证时间顺序正确。sort_values(date)这步不能省——源数据常常按品种分组乱序排列跳过它会让时间窗口样本错位训练出来的模型等于看着未来预测过去。2.2 缺失值与异常值的处理价格跳变背后的三种情况蔬菜价格数据很少是干净的。月缺几天记录是常态逢年过节价格翻倍、暴雨后单日暴涨也时有发生。处理策略分三层缺失日期用前后2天均值填充单价为0或负数的行直接删除超过3倍滚动标准差的价格标记为异常人工确认是真实波动还是录入错误。# 缺失日期补全并填充价格 full_idx pd.date_range(df.index.min(), df.index.max(), freqD) df df.reindex(full_idx) df[avg_price] df[avg_price].fillna( df[avg_price].rolling(2, min_periods1).mean() ) # 剔除价格异常值3倍滚动标准差 mean df[avg_price].rolling(7, centerTrue).mean() std df[avg_price].rolling(7, centerTrue).std() df df[(df[avg_price] - mean).abs() 3 * std]reindex把缺失日期补成NaN再用前向滚动均值填充比单纯ffill更平滑。滚动均值窗口取2天是因为蔬菜价格不会连续暴跌邻近两天的平均价足够接近真实值。3倍标准差剔除的逻辑有风险春节前价格暴涨可能超过阈值如果确认是真实行情需要从剔除名单里恢复。2.3 构造时间窗口样本滑动窗口长度与预测步长的权衡LSTM不能吃一整条序列它吃的是固定长度的窗口。常见做法是用过去1015天的价格预测未来13天。窗口太短学不到周期性规律太长则样本量骤减。比如600天的数据窗口15天步长1天预测第16天能构造约585个样本如果预测未来7天和目标日的间隔要拉开模型学习的是15天趋势影响7天后价格的映射。import numpy as np def create_sequences(data, window15, horizon1): X, y [], [] for i in range(len(data) - window - horizon 1): X.append(data[i:iwindow]) y.append(data[iwindowhorizon-1]) return np.array(X), np.array(y) X, y create_sequences(df[avg_price].values, window15, horizon1) print(X.shape, y.shape) # (585, 15) (585,)这里的horizon1表示预测第16天的价格。窗口长度15是蔬菜价格项目的常见起点两周的行情走势足以覆盖短期供需波动周期又不至于吞掉太多样本。如果数据量超过2000天可以试试窗口24、horizon3的组合能捕捉月度周期但训练时间会明显上升。2.4 归一化与反向还原MinMaxScaler的正确用法LSTM内部的激活函数对输入范围敏感价格几百到几千的原始数值直接喂进去会让Sigmoid和tanh饱和梯度几乎消失。归一化到01之间是标准做法。两个关键坑一是fit只能用训练集统计量不能先归一化再切分否则验证集信息泄露到训练过程二是预测完必须还原成真实价格否则输出结果没法做业务判断。from sklearn.preprocessing import MinMaxScaler # 先切分再归一化防止数据泄露 train_size int(len(df) * 0.8) train, test df[avg_price].values[:train_size], df[avg_price].values[train_size:] scaler MinMaxScaler(feature_range(0, 1)) train_scaled scaler.fit_transform(train.reshape(-1, 1)).flatten() # 验证集用训练集的scaler变换不重新fit test_scaled scaler.transform(test.reshape(-1, 1)).flatten()scaler.fit_transform记录训练集的min/max验证集只用transform套用同一套映射。预测完成后用scaler.inverse_transform还原这是新手最容易漏的一步漏掉之后画出的预测曲线和真实价格完全对不上量纲看起来就是一条躺平的直线。3. 搭建LSTM预测模型网络结构与核心代码3.1 LSTM单元的工作方式门控机制与长期记忆LSTM和普通RNN的本质区别在于内部多了三个门输入门决定新信息写入多少遗忘门决定上一步记忆保留多少输出门决定当前状态输出多少。这个机制让网络在反向传播时不容易出现梯度消失能够捕捉跨多天甚至跨季节的价格规律。比如夏季黄瓜价格连续一个月走低后突然反弹普通RNN可能只记得最近两三天的下跌LSTM却能保留更长周期的底部信号。3.2 定义模型结构输入层、LSTM层与全连接输出层搭建LSTM预测模型的常用框架是PyTorch和Keras。以PyTorch为例一个典型的单变量价格预测模型长这样import torch import torch.nn as nn class PriceLSTM(nn.Module): def __init__(self, input_size1, hidden_size64, num_layers2, output_size1): super().__init__() self.lstm nn.LSTM(input_size, hidden_size, num_layers, batch_firstTrue) self.fc nn.Linear(hidden_size, output_size) def forward(self, x): # x: (batch, window, input_size) out, _ self.lstm(x) # out: (batch, window, hidden_size) out out[:, -1, :] # 取最后一个时间步的隐状态 return self.fc(out) # (batch, output_size)batch_firstTrue让输入维度变成(batch, window, features)和通常的直觉一致。取out[:, -1, :]是只保留最后一个时间步的隐状态因为我们要预测的是窗口之后的价格而不是重建整个序列。hidden_size64对单品种价格序列已经够用数据量大时可扩展到128。3.3 前向传播与损失函数选择回归任务的MSE与MAE价格预测是回归问题损失函数常见选择是 MSE 和 MAE。MSE 对大误差惩罚更重收敛快但容易被极端价格带偏MAE 对异常值不敏感训练后期更稳。实践中一般先用 MSE 训到损失不再下降再切到 MAE 精调几个 epoch效果往往比单一损失更好。def train_model(model, train_loader, val_loader, epochs100): optimizer torch.optim.Adam(model.parameters(), lr0.001) criterion nn.MSELoss() for epoch in range(epochs): model.train() train_loss 0 for X_batch, y_batch in train_loader: optimizer.zero_grad() pred model(X_batch) loss criterion(pred, y_batch) loss.backward() optimizer.step() train_loss loss.item() if epoch % 20 0: val_loss evaluate(model, val_loader) print(fEpoch {epoch}: train_loss{train_loss:.4f}, val_loss{val_loss:.4f})Adam优化器是当前LSTM训练的事实标准它自动调整每个参数的学习率比SGD省去手动调momentum的麻烦。学习率0.001是起步值loss曲线震荡时降低到0.0003。批量大小通常取32或64太小会让梯度噪声大太大则训练慢且容易收敛到平坦的局部最优。4. 训练与调参让模型真正收敛的4个关键参数4.1 学习率0.001还是0.0005看损失曲线说话学习率是LSTM训练里最敏感的超参数。设大了损失曲线上下乱跳设小了训练几十个epoch损失纹丝不动。判断方法很简单前5个epoch训练损失如果从初始值快速下降说明学习率合适如果忽高忽低砍一半如果几乎不降翻3倍再试。另外一个实用技巧是带上学习率衰减每30个epoch乘以0.8让训练后期逐步逼近最优解。scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size30, gamma0.8) for epoch in range(epochs): ... scheduler.step()StepLR每30轮把学习率乘0.8前期快速搜索后期精细收敛。注意scheduler.step()的位置必须在每个epoch结束之后调用放在batch循环里会导致学习率更新过频模型无法稳定收敛。4.2 批大小与序列长度的互动关系批大小和滑动窗口长度共同决定了每个batch的数据形状(batch, window, 1)。窗口长、批太大时会显存溢出批太小则LSTM的隐状态在batch内传不稳定。经验区间是窗口1030、批大小3264。一个容易被忽略的点shuffle训练数据时要小心时间序列打乱后预测目标对应的上下文也变了模型学的是窗口模式 → 下一值的映射只要每个样本内部保持时间顺序shuffle是无害的但验证集绝对不能shuffle否则评估指标失真。4.3 隐藏层维度太大过拟合、太小欠拟合隐藏层维度控制模型的记忆容量。蔬菜价格序列的复杂度远低于自然语言64维已能覆盖常见的涨跌模式128维在超长数据两年以上时可以尝试。判断维度是否合适训练损失远低于验证损失说明高维过拟合两边都高说明低维欠拟合。LSTM层数同理2层是性价比最高的配置3层以上在小数据集上几乎必然过拟合。4.4 early stopping与模型保存留好后悔药训练到后期验证损失开始回升说明模型已经在记住训练集的噪声。提前停止是防止过拟合最便宜的办法每轮记录验证损失连续10轮不下降就恢复最佳参数。模型保存要注意不仅存权重还要连scaler的参数一起存否则预测阶段还原价格时对不上。if val_loss best_loss: best_loss val_loss torch.save({ model_state: model.state_dict(), scaler: scaler, window: 15, hidden_size: 64 }, best_model.pt)torch.save里参数名scaler存的是整个MinMaxScaler对象加载后用checkpoint[scaler].transform(...)就能在新数据上做同样的归一化。把这些配置一并存下换数据重新训练或部署预测时不需要手动重设模型自解释性也更好。5. 避坑指南蔬菜价格预测的5个常见翻车现场5.1 现象预测曲线平平的几乎是一条水平线原因归一化之后目标值的取值范围被压缩到01之间模型发现直接输出训练集均值附近的常数也能让损失降到较低水平于是偷懒不走复杂路径。尤其在价格波动小的品种上这个现象特别明显。解决一是把窗口长度拉长给模型更多上下文变化信号二是改用差值预测——不直接预测价格而是预测明天相对今天的涨跌幅模型被迫学习变化量而非偷懒取均值。5.2 现象训练损失下降但验证损失不断上升原因模型容量超过数据承载能力典型的是隐藏层设了128或者层数设了4而训练样本只有几百条另一个可能是归一化时验证集的极小值/极大值被截断导致预测和真实值对不上。解决先砍模型容量——hidden_size降到32层数降到1再确认scaler只拟合了训练集。改完这两处验证损失大概率能跟上训练损失。5.3 现象反归一化后的价格出现负数原因模型在归一化空间输出的值略小于0反归一化把它投射到原始价格区间之外。蔬菜价格理论上是正数出现负说明预测分布偏移。解决输出层前加一个ReLU激活强制预测值非负或者在后处理里把负值截断为0。加激活更优雅但要注意ReLU会让梯度在负区间完全消失训练中一旦陷入负区很难逃离配合初始学习率0.0005更稳。5.4 现象换一批数据效果就崩原因上一批数据训练出的模型学到了那个市场的特殊模式比如中心批发市场的周末规律换了产地直供市场规律完全不同。价格预测模型的迁移能力天然弱因为它本身是个高度拟合特定场景的时序模型。解决按市场品种分别训练独立模型不要试图用一个模型覆盖所有场景数据集不足300天时考虑合并相似品种叶菜类合并、根茎类合并增加样本量再用market作为LSTM的额外输入特征。5.5 现象预测结果整体滞后一天原因这是最隐蔽的一个坑。LSTM学习了今天价格约等于昨天价格微小修正这条捷径在价格平稳期预测曲线紧贴真实值但整体右移一天一旦价格剧烈波动预测完全跟不上。解决检查训练数据的窗口构造——如果y取的是窗口最后一天而非窗口后一天就出现了数据泄露模型等于直接抄答案。修正create_sequences里的目标偏移量确保预测目标是窗口范围之外的那一天。6. 验证模型的最后一关用滚动预测检验模型真实力常规的train/test切分验证只能说明模型在分布相近的时间段表现如何实际使用时更关心的是模型上线后用截至昨天的数据预测明天准确率还稳不稳。这时要用滚动预测rolling forecast来验证def rolling_forecast(model, df_all, scaler, window15, steps7): model.eval() predictions [] # 从最后window条数据开始逐步外推 current_window df_all[avg_price].values[-window:] for _ in range(steps): scaled scaler.transform(current_window[-window:].reshape(-1, 1)) x_tensor torch.tensor(scaled.T.reshape(1, window, 1), dtypetorch.float32) with torch.no_grad(): pred model(x_tensor).item() # 还原价格并把预测值接回窗口 pred_price scaler.inverse_transform([[pred]])[0][0] predictions.append(pred_price) current_window np.append(current_window, pred_price) return predictions这段代码有意思的地方在最后一行每预测一天就把预测值接回窗口尾部当作新一天的输入继续预测。连续7天外推之后误差会随步长累积——第一天可能只偏0.1元第七天可能偏0.8元。这正是真实业务里的情况因为未来几天的实际价格还没发生只能用预测值当输入。滚动预测的误差曲线能告诉你模型的真实可用步长如果第3天的误差已经超过0.5元业务上就该把预测周期限制在2天以内而不是硬上7天。我自己的习惯是每次调参结束先跑一遍滚动预测把逐日误差画出来再决定这个模型能不能上线。那些train/test指标好看但滚动预测发散很快的模型实战里基本就是摆设。数据清洗、窗口构造、归一化、训练调参、滚动验证这条链路走一遍你对这个模型的真实能力心里就有底了。希望帮到你。本文还有配套的精品资源点击获取
返回列表