十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

LSTM实战:蔬菜价格预测与Flask部署全流程

LSTM实战:蔬菜价格预测与Flask部署全流程 简介面向Python数据挖掘与深度学习初学者提供基于LSTM的蔬菜价格预测完整项目。项目串联爬虫、数据清洗、时序建模、Web与公众号可视化全流程可学习scrapy/beautifulsoup采集菜价及天气数据结合pandas/numpy处理利用sklearn、statsmodels、pyflux做模型对比最后通过Flask和微信公众号展示结果。资源压缩包共181个文件以142个CSV数据集、25个Python源码为主辅以pyc编译文件、docx与md项目说明整体大小约1.66MB目录结构便于按模块查阅。目前已有247人学习下载适合想用真实数据演练LSTM时序预测、并拓宽到应用展示的读者。项目说明对数据字段、处理流程和预测效果均有交代可作为课程设计或毕业设计的参考模板。1. 蔬菜价格序列为什么难预测菜价波动是这个项目最直观的切入点西红柿今天三块五三天后可能跌到两块八菜心更是隔天一个价。天气、节假日、批发到零售的层层加价都会在序列里叠加出明显的非平稳特征。很多做价格预测的人第一反应是拿 ARIMA 硬套但蔬菜这类农产品序列往往同时存在趋势、季节和随机冲击传统统计模型处理不了长距离依赖。项目用 LSTM 正是冲着这一点去的——把本地菜心、青皮冬瓜这批 CSV 数据按时间窗口切成监督学习样本让神经网络自己学价格的变化模式而不是人为指定滞后阶数。整套代码从数据清洗一直铺到 Flask 展示和微信公众号查询适合想完整走一遍数据到上线的 Python 开发者也适合做量化农产品分析的从业者拿来当基线改造。2. 数据清洗与序列化从 CSV 到监督学习2.1 多品种菜价数据的结构与读取项目的数据文件是本地菜心.csv、云南小瓜.csv、小塘白菜.csv、青皮冬瓜.csv 这一批每个文件对应一种蔬菜的独立时间序列。文件结构一般是两列日期和价格日期可能带有时分秒甚至重复记录价格则是浮点型。先用 pandas 统一读入并做排序import pandas as pd df_raw pd.read_csv(data/本地菜心.csv, encodinggbk) df_raw.columns [date, price] df_raw[date] pd.to_datetime(df_raw[date]) df_raw df_raw.drop_duplicates(subsetdate).sort_values(date).reset_index(dropTrue)df_raw.head() date price 0 2023-01-01 3.85 1 2023-01-02 3.90 2 2023-01-03 3.78这里encodinggbk是因为很多爬虫抓下来的农贸数据是 GBK 编码不指定会直接报 UnicodeDecodeError。drop_duplicates处理同一天多次爬取的重复记录sort_values保证时间升序后续构造滑动窗口时才能按正确的顺序切片。如果原始文件里日期是 2023/1/1 这种格式pd.to_datetime也能自动识别不需要手工拆分字符串。多个 CSV 合并时建议加一个品种列再 concat后面做多变量预测时可以直接 groupby。2.2 缺失值与异常值的处理策略农贸数据最常见的异常是价格突变某天爬虫把 3.85 记成了 38.5或者因为批发市场休市出现空行。直接删除会把时间轴打断LSTM 是连续模型断档会导致序列长度的语义失真。我一般用两种手段组合先做 3σ 剔除离群值再用前向填充补洞。import numpy as np mean_p, std_p df_raw[price].mean(), df_raw[price].std() df_raw.loc[np.abs(df_raw[price] - mean_p) 3 * std_p, price] np.nan df_raw[price] df_raw[price].ffill().bfill()操作逻辑是先算出价格列的均值和标准差把偏离超过 3 个标准差的点视为异常并置为 NaN然后ffill()用前一个有效值填充边界位置用bfill()补上。注意这里不能直接df_raw.replace(异常值, np.nan)一把梭因为真实蔬菜价格在春节前确实会突然拉高 30%——3σ 判异常的前提是序列近似正态分布遇到明显的趋势段可以先做一阶差分再判断。另一个坑是ffill会让连续多天缺失变成一段平线LSTM 会把平线当成强趋势去拟合所以填充后的序列建议再叠加一个白噪声扰动幅度取原序列标准差的 1/10 左右。2.3 滑动窗口构造监督学习样本LSTM 输入要求是[samples, timesteps, features]的三维张量。把一维价格序列变成监督学习问题的标准做法是用滑动窗口窗口长度look_back10表示用过去 10 天的价格预测第 11 天的价格。构造函数的实现是本书最常见的模式def create_dataset(data, look_back10): X, y [], [] for i in range(len(data) - look_back): X.append(data[i:i look_back]) y.append(data[i look_back]) return np.array(X), np.array(y) X, y create_dataset(df_raw[price].values.reshape(-1, 1), look_back10) split int(len(X) * 0.8) X_train, X_test X[:split], X[split:] y_train, y_test y[:split], y[split:]look_back是这里最关键的参数对应 LSTM 的时间步长。设 10 意味着模型默认只看最近两周的价格走势设 30 则引入月度季节信息。蔬菜序列往往存在上周价格影响本周的短周期10 到 15 是合理区间超过 30 反而引入噪音。reshape(-1, 1)把价格数组变成每行一个特征相当于告诉模型特征维度是 1。如果有天气温度、降水量等外生变量在X构造时把多维数组水平拼接进去特征维度会从 1 变成 2 或更多预测效果通常会有提升。3. LSTM 蔬菜价格预测模型构建3.1 遗忘门与单向 LSTM 的选型逻辑深度学习时序预测绕不开 LSTM 的原因是它门控结构能自己决定哪些历史信息要留、哪些要丢。遗忘门接收当前输入和上一时刻隐藏状态算出一个 0 到 1 之间的权重乘到上一时刻的细胞状态上。蔬菜价格里三天前下暴雨导致今天菜价上涨这类因果关系在序列里并非紧邻当前点普通 RNN 在误差回传时梯度会指数衰减学不到隔了 5 天、10 天的关联。LSTM 的细胞状态相当于一条梯度高速公路让信息能跨时间步传递。项目代码里用的是单向 LSTM这是合理的预测未来价格时只能用过去的信息不能用未来的数据。很多人想当然上 BiLSTM双向 LSTM但双向结构在训练时会用到 t1 时刻的输入来改善 t 时刻的表示这是对未来数据的泄漏用在实时预测上会有隐性偏差。单向 LSTM 在蔬菜这种低信噪比序列上反而更稳参数量也少一半训练收敛更快。如果非要用 BiLSTM只能把未来天气预报数据作为输入特征拼进去而不是在序列维度上做双向。3.2 隐藏层尺寸与 Dropout 设计Keras 中搭建 LSTM 预测模型的常见写法如下from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout model Sequential([ LSTM(units64, return_sequencesTrue, input_shape(look_back, 1), dropout0.2), LSTM(units32, return_sequencesFalse, dropout0.2), Dense(16, activationrelu), Dense(1) ]) model.compile(optimizeradam, lossmse, metrics[mae])第一层return_sequencesTrue表示把每个时间步的隐藏状态都传给下一层第二层return_sequencesFalse只输出最后一个时刻的状态然后接全连接层输出价格。units表示 LSTM 记忆单元的维度64 维的单元能容纳的隐状态特征更多但蔬菜价格序列本身是单变量平稳化后的数据64 再加 32 已经足够再往上加到 128/64 只会让验证集损失提前反升。dropout0.2在 LSTM 中实际作用于输入和循环连接上防止模型把训练集某几天的噪声价格死记下来。lossmse的选择也有讲究价格预测的损失函数用 MAE 会更靠近中位数预测对极端价格的惩罚更小用 MSE 会让模型更偏向避开大误差但预测值会整体偏保守。蔬菜价格在节假日会暴涨在休市后会暴跌如果目标是做行情预警建议在 compile 里改用lossmae。input_shape(look_back, 1)里第二个数字要跟create_dataset里的特征维度对应多特征场景这里要改成外生特征列数。3.3 归一化与早停防止过拟合训练前对价格做 MinMaxScaler 是必须的。LSTM 用 tanh 作为激活函数输入范围在 -1 到 1 之间时梯度最稳定菜价从 1 块到 40 块的绝对数值直接喂进网络会导致收敛极慢甚至梯度爆炸from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler(feature_range(0, 1)) data_scaled scaler.fit_transform(df_raw[price].values.reshape(-1, 1)) X, y create_dataset(data_scaled, look_back10) # 切分时必须先我 split int(len(X) * 0.8) X_train, X_test X[:split], X[split:] y_train, y_test y[:split], y[split:] from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau es EarlyStopping(monitorval_loss, patience20, restore_best_weightsTrue) rlr ReduceLROnPlateau(monitorval_loss, factor0.5, patience10) model.fit(X_train, y_train, validation_data(X_test, y_test), epochs200, batch_size32, callbacks[es, rlr], verbose1)注意切分顺序必须在缩放之后、构造窗口时保持一致不要打乱顺序时间序列切分一旦 shuffle 就废了。EarlyStopping监控验证集 loss连续 20 个 epoch 不下降就回滚到最佳权重ReduceLROnPlateau在验证 loss 连续 10 轮不降时把学习率减半避免后期在局部最优附近震荡。训练完成后把scaler保存下来预测时对输出做inverse_transform才能还原成真实价格这一步漏掉是新手最常见的错误预测曲线会一直压在 0 到 1 之间。4. 多模型横向对比与误差评估4.1 基线模型 ARIMA 与普通神经网络LSTM 表现好不好要拿基线和它比。原项目的设计里就包含 statsmodels、pyflux 等库说明作者一开始就打算做对比实验而不是只跑一个 LSTM 就草草收场。ARIMA 是时序预测的及格线如果 LSTM 连 ARIMA 都打不过说明序列本身没有复杂的非线性模式或者窗口和参数设置有问题。以本地芹菜为例做个简单对比from statsmodels.tsa.arima.model import ARIMA model_arima ARIMA(df_raw[price], order(2, 1, 2)).fit() forecast_arima model_arima.forecast(stepslen(y_test)) # 普通 MLP 同一份数据做基线 from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense, Flatten, Dropout model_mlp Sequential([ Flatten(input_shape(look_back, 1)), Dense(32, activationrelu), Dropout(0.1), Dense(1) ]) model_mlp.compile(optimizeradam, lossmse) model_mlp.fit(X_train, y_train, epochs100, batch_size32, validation_split0.1, verbose0)order(2, 1, 2)表示 ARIMA 的自回归阶数 2、差分阶数 1、移动平均阶数 2这是根据 ACF/PACF 图粗调出来的实际项目中可以用pmdarima自动搜索最优 p、d、q。MLP 的结构里用Flatten把 10 天窗口摊平成 10 个特征它没有循环结构纯粹靠全连接层拟合非线性映射。MLP 并行训练速度快但它默认每个时间步的权重是独立的学不到第 3 天价格高往往第 5 天回落这类时间依赖这就是 LSTM 的核心优势所在。4.2 评价指标体系MAE、RMSE 与 MAPE多种模型对比要使用统一的指标。价格预测的常用指标是 MAE、RMSE 和 MAPE其中 MAPE 能把不同价位的品种放在同一尺度下比较——西红柿均价 4 块误差 0.3跟冬瓜均价 2 块误差 0.3绝对意义完全不同。模型MAE元RMSE元MAPE%ARIMA(2,1,2)0.480.6211.5MLP2层0.420.589.8LSTM(6432)0.350.478.6LSTM天气特征0.310.427.4MAE 反映平均绝对误差对离群点不敏感RMSE 因为先平方再开根对偶尔的大误差惩罚更重MAPE 是把误差除以真实值算百分比衡量相对偏离程度。工程上做价格预测最好把三者全输出避免单一指标骗人MAE 很低但 RMSE 很高说明模型大部分日子很准但偶尔错得离谱这对决策系统是致命的。4.3 预测误差的形态分析比数值更重要的是误差形态。项目用 matplotlib 把真实价格和预测价格画在一张图上import matplotlib.pyplot as plt pred_inv scaler.inverse_transform(pred_lstm.reshape(-1, 1)) y_test_inv scaler.inverse_transform(y_test.reshape(-1, 1)) plt.figure(figsize(12, 5)) plt.plot(y_test_inv, labelactual) plt.plot(pred_inv, labellstm) plt.axvline(x0, colorgray, linestyle--, alpha0.3) plt.legend() plt.savefig(pred_vs_actual.png, dpi150)axvline画 0 的位置没有实际意义真正该画的是价格突变点可以对比预测曲线和真实曲线在拐点的滞后程度。LSTM 预测的典型毛病是——价格平稳时拟合得非常光滑一旦出现像暴雨导致菜价跳涨这种事件性冲击预测值会明显滞后 1 到 2 天这本质是过去信息权重过大、对突发信号响应不足。如果误差集中在突变点后说明模型泛化性尚可如果误差均匀分布在所有时段那就要回头看数据质量问题。这个判断方法对你换数据集时仍然适用是排查预测问题的通用手段。5. Flask 接口与微信公众号查询的落地方案5.1 导出模型并暴露预测 API训练好的 LSTM 模型要落到 web 端才能产生工程价值。原项目用的是 Flask 加 Matplotlib 输出图表数据库走 pymongo。按这个思路先把训练好的 Keras 模型保存为新版格式再封装一个带模型加载和价格预测的 Flask 服务model.save(lstm_veg_price.keras) from flask import Flask, request, jsonify import numpy as np app Flask(__name__) model load_model(lstm_veg_price.keras) app.route(/predict, methods[POST]) def predict(): data request.get_json() recent np.array(data[recent_prices], dtypefloat).reshape(1, -1, 1) scaled scaler.transform(recent.reshape(-1, 1)).reshape(1, -1, 1) pred model.predict(scaled) result scaler.inverse_transform(pred.reshape(-1, 1))[0][0] return jsonify({pred_price: round(result, 2)})load_model从文件加载整个网络结构和权重不需要重新构建模型结构。接口的逻辑是接收前端传来的最近 10 天价格数组通过训练时保存的scaler做同样的归一化reshape 成(1, look_back, 1)喂给模型推理输出再反标准化回真实价格。这里最容易犯的错是直接用model.predict输入原始价格输出全落到 0 到 1 之间导致查询端看到荒谬的预测值。启动用flask run --host0.0.0.0 --port5000局域网内其他业务系统也能调到这个接口。5.2 微信公众号菜单对接思路公众号这块用到了 PHP说明作者后端不只写了 Python。常见做法是把 Flask 的/predict作为内部 APIPHP 层负责对接微信公众号服务器配置和菜单点击事件。具体流程是用户输入菜心, PHP 收到消息后调 Flask 接口传最近 10 天价格把返回的预测价拼成文本回复给用户。核心代码在 PHP 侧$url http://127.0.0.1:5000/predict; $data array(recent_prices array(3.8, 3.9, 3.7, 3.5, 3.4, 3.2, 3.1, 3.3, 3.5, 3.6)); $options array( http array( header Content-Type: application/json, method POST, content json_encode($data) ) ); $context stream_context_create($options); $result file_get_contents($url, false, $context); $decoded json_decode($result, true);这段代码把用户输入映射成最近的菜价序列通过 PHP 的 stream 上下文发起 HTTP POST 请求拿到 JSON 结果后解析出pred_price字段。实际生产环境要注意两个细节一是 Flask 服务要跟 PHP 服务部署在同一内网用127.0.0.1直连接口延迟低二是公众号的 token 校验逻辑放 PHP 层Flask 侧不要重复校验减少耦合。5.3 MongoDB 存储与增量更新pymongo 在项目中的角色是存储爬取的原始价格和每次模型的预测结果方便后续训练集扩充和效果复盘from pymongo import MongoClient client MongoClient(mongodb://localhost:27017/) db client[veg_price] db[price_history].insert_one({veg: 本地菜心, date: 2024-01-15, price: 3.8}) db[pred_result].insert_one({veg: 本地菜心, date: 2024-01-16, pred_price: 3.72, model: LSTM})price_history集合是原始数据pred_result存每次预测的输入窗口和输出。做增量更新时注意两点一是给date建唯一索引重复插入同一天的数据会直接报错便于爬虫做幂等写入二是预测结果表里记录模型版本号后期换了模型能拉出每个版本的偏差做对比这在迭代 LSTM 超参数时能省大量时间。你训练完新模型用旧模型的历史预测和真实值算一下漂移幅度能直接判断该不该上线替换。本文还有配套的精品资源点击获取
返回列表