多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

LSTM蔬菜价格预测:从爬虫到模型训练完整Python实践

LSTM蔬菜价格预测:从爬虫到模型训练完整Python实践 简介基于深度学习LSTM的蔬菜价格预测项目资源包面向具备Python基础、希望掌握时间序列预测与全栈展示的开发者提供从数据采集、清洗、建模到Web及公众号展示的完整解决方案。包内涵盖181个文件共1.78MB主要包括142个CSV格式的蔬菜价格与天气历史数据、25个Python源码文件以及10个pyc编译文件另附2个docx项目说明和1个md说明文档。源码按“爬取-清洗-预测-展示”四个环节组织涉及Scrapy/BeautifulSoup爬虫、Pandas/Numpy数据处理、LSTM及时间序列模型statsmodels、pyflux预测、Flask Web展示和pymongo数据库存储代码结构清晰便于对照学习。该资源还包含项目整体实现步骤与微信公众号对接思路适合课程设计、毕业设计及实战练习。目前已有251人前来学习和下载对理解LSTM在真实价格预测场景中的完整落地流程很有参考价值。1. 用 LSTM 预测蔬菜价格这份 Python 源码包为什么值得跑一遍蔬菜价格预测是个典型的短周期时间序列问题价格受天气、节假日、运输成本多重因素干扰波动大但又有周规律拿来做深度学习的入门项目比用股票数据顺手得多。这份源码包把完整链路都补齐了scrapy 爬虫采集菜价和天气数据、pandas 清洗、statsmodels 做 ARIMA 基准、LSTM 神经网络做预测、Flask 做 Web 展示最后还接上了微信公众号查询。数据集是 8 个常见蔬菜品种的本地 CSV包括本地菜心、云南小瓜、小塘白菜、本地芹菜、青皮冬瓜、西红柿、红尖椒等拿到手就不需要再去找外部数据。适合两类人一是做课程设计或毕业设计、需要数据采集→训练→展示完整闭环的同学二是想快速上手 lstm 预测、但又不想从零写数据管道的从业者。动手深度学习的第一步往往不是调参而是先把数据链路跑通这份资源正好提供了这样一个可复现的起点。2. 从爬虫到 CSV 数据集菜价采集、清洗与落盘2.1 爬虫采集scrapy 抓价格与天气数据的方案项目的第一步是把菜价和天气数据从目标网站抓下来。这一环决定了后面所有模型的上限数据不干净LSTM 再强也白搭。常见做法是用 scrapy 写一个爬虫按蔬菜品种分组抓取历史价格再用 beautifulsoup 解析 HTML 表格。scrapy 的优势是并发和去重机制成熟菜价这种低频数据用它有点大材小用但胜在结构清晰、重启断点好控制。# spiders/price_spider.py import scrapy class PriceSpider(scrapy.Spider): 按品种抓取蔬菜日报价格数据交给 pipeline 入库 name price_spider def start_requests(self): # settings.py 里维护待抓取品种清单与 data/ 目录下 CSV 文件名对应 for veg in self.settings.get(VEGETABLES, []): url fhttps://market.example.com/price/{veg} yield scrapy.Request(url, callbackself.parse, meta{veg: veg}) def parse(self, response): veg response.meta[veg] # 以常见价格表结构为例实际字段名按目标站点调整 for row in response.css(table.price-table tr): date row.css(td.date::text).get() price row.css(td.price::text).get() if date and price: yield { vegetable: veg, date: date.strip(), price: float(price), }这段代码的核心是用 meta 把当前品种名从 start_requests 传到 parse 回调避免拿到一个 response 却不知道数据属于哪棵菜。实际项目中我会在 settings.py 里维护 VEGETABLES 这个品种清单并给每个请求加 Downloader Middleware 控制请求间隔价格表页面反爬不严但频率太高照样会被限流。价格解析出来是字符串必须用 float() 转成数值否则清洗阶段没法做运算。为什么要爬天气数据菜价的短期波动很大程度由降雨、台风、寒潮这类天气事件驱动把天气作为外生特征喂给模型理论上能提升预测精度。爬虫部分的原始输出建议落到 MongoDB用 pymongo 写入这样原始数据保留一份完整底账清洗出错还能回溯。爬虫跑完后检查两个指标抓取条目数和数据库文档数是否对得上对不上就说明有页面结构变动导致解析漏行这种问题静默发生不查根本不知道。2.2 数据集字段与清洗pandas 处理缺失、异常与重复项目附带的 data/ 目录下是清洗好的 CSV品种包括本地菜心、云南小瓜、小塘白菜、本地芹菜、矮脚白菜、青皮冬瓜、西红柿、红尖椒。每个 CSV 通常包含日期、价格两列核心字段有的还带天气信息列。打开之后第一件事不是建模而是确认数据质量。我习惯先跑一遍描述性统计看每个品种的记录天数、价格 min/max、缺失数量再决定后续处理策略。# preprocess.py import pandas as pd df pd.read_csv(data/本地菜心.csv, encodingutf-8-sig) df[date] pd.to_datetime(df[date]) df df.sort_values(date) # 同一日期出现多条记录时保留最后一条 df df.drop_duplicates(subset[date], keeplast) # 缺失值用线性插值价格骤变用 3σ 判定后做平滑替换 price df[price].astype(float) df[price] ( df[price] .interpolate(methodlinear) .clip(lowerprice.quantile(0.02), upperprice.quantile(0.98)) )这里有两个细节容易翻车。编码必须用 utf-8-sig直接按 utf-8 读在 Windows 上会出现首列乱码缺失值我用线性插值而不是 fillna(methodffill)因为价格序列在相邻日期间连续性较强前后值线性插值比单纯向后填充更接近真实走势。clip 的上下限分别取了 2% 和 98% 分位数用来消化极端尖峰但注意上下界必须基于原始序列计算不能基于插值后的序列否则边界会被自己的填充值污染异常值反而被放大。另外值得注意的一点资源文件列表里矮脚白菜.csv 出现了三次。这看起来像打包时的重复文件实际上对建模有直接影响——如果代码用 glob 遍历目录训练重复文件会导致同一个品种被重复抽样训练集和验证集之间产生泄漏验证指标虚高。拿到资源后第一件事应该是做去重校验确认 data/ 下每个品种只有一个文件再开始清洗和建模。这种隐患不踩到不会意识到等模型上线才发现训练数据里混着重复文件后悔药都没处买。2.3 存储分工pymongo 与 CSV 各自的职责为什么要同时用 MongoDB 和 CSV 两套存储这是分工问题。爬虫抓下来的原始数据是脏的、带重复的、含异常值的适合放 MongoDB 存底账清洗后用于训练的、结构化的数据适合导出成 CSV 直接喂给 pandas。我在实际项目里还会加一个 batch 字段标记清洗批次方便复现任何一次实验结果这是保证实验可复现的最小成本。# db.py from pymongo import MongoClient client MongoClient(mongodb://localhost:27017/) db client[vegetable_price] # 原始数据入库保留完整字段便于回溯 db.raw.insert_one({ vegetable: 西红柿, date: 2024-05-12, price: 4.80, weather: rain, source: market.example.com, }) # 清洗后供训练用的数据也落一份带清洗批次号 db.clean.insert_one({ batch: v20240512, vegetable: 西红柿, date: 2024-05-12, price: 4.80, })pymongo 的 insert_one 是同步写入菜价按天抓一年也就三百多个文档完全不需要批量优化。pipeline 顺序我建议是scrapy 抓取进 raw 集合 → pandas 清洗 → 结果存 CSV 用于训练 → clean 集合留一份带批次号的备份。这样任何一个环节出了偏差都能从原始数据重新推导不至于重新爬一遍。存储内容用途MongoDB raw原始抓取数据含天气和来源字段回溯、排查爬虫问题MongoDB clean清洗后带 batch 标记的数据复现实验、对比清洗策略CSV data/训练用最终数据直接供 pandas 读取建模连接串在本地跑的时候用 localhost 就行不需要用户名密码。如果后面接了公众号服务建议把连接串放到环境变量里而不是硬编码在源码中这是部署卫生问题后面章节还会碰到类似的。3. 先立基准再上 LSTMARIMA 与经典回归的对比框架3.1 重采样与滑动窗口入模前的数据结构化菜价数据按天记录但原始 CSV 里可能存在部分日期缺失直接建模会让时间步不连续。第一步是把日期列设为索引并重采样到日频缺失的日期用插值补齐然后构造滑动窗口样本——用过去 N 天的价格预测未来 M 天的价格。窗口大小是第一个要定的超参数菜价周期一般是七天我把 n_steps 默认设为 7这样每个样本恰好覆盖一个完整的价格波动周期。# build_dataset.py import pandas as pd import numpy as np df pd.read_csv(data/本地菜心.csv, encodingutf-8-sig) df[date] pd.to_datetime(df[date]) df df.set_index(date).sort_index() # 重采样到日频缺失日期用前后值线性填充 daily df[price].resample(D).interpolate(methodlinear) def make_windows(series, n_steps7): 把一维序列切成 (样本数, n_steps, 1) 的监督学习格式 X, y [], [] for i in range(len(series) - n_steps): X.append(series[i:i n_steps].values) y.append(series[i n_steps]) return np.array(X), np.array(y) X, y make_windows(daily, n_steps7)滑动窗口的坑在于切分顺序。resample(D) 之后索引对齐到日历日但周末菜市场休市时价格往往缺失interpolate 会把这些日期也填出数值模型就学到了周末价格是前两天插出来的假象。更严谨的做法是按实际交易日切窗口或者接受缺失并按交易日重采样。我一般先画一条序列图看缺失日期集中在什么位置再决定填充策略而不是上来就无脑插值。窗口长度不是越大越好。n_steps7 来自价格波动周期但如果数据本身只有三个月窗口切得太长会导致样本数骤减训练集从几百条缩到几十条LSTM 很容易过拟合。样本量与窗口长度的关系是有效样本数约等于总天数减 n_steps少于二百条就要考虑缩短窗口或改用小模型。3.2 统计基准statsmodels 的 ARIMA 与 sklearn 回归深度学习模型的结论必须跟统计模型对比才有说服力。项目里用 statsmodels 做 ARIMA 基准这是把LSTM 到底有没有用量化出来的关键一步。ARIMA 的三个参数 (p, d, q) 分别代表自回归阶数、差分阶数和移动平均阶数。菜价序列有日频周期但不是强季节性d 取 1 阶差分后序列基本平稳p 和 q 需要在小范围内搜索。# baseline_arima.py from statsmodels.tsa.arima.model import ARIMA import numpy as np train_series daily[:int(len(daily) * 0.8)] test_series daily[int(len(daily) * 0.8):] # 在 (0,1,0) 到 (3,1,3) 区间内按 AIC 搜索最优阶数 best_order, best_aic None, float(inf) for p in range(4): for q in range(4): try: model ARIMA(train_series, order(p, 1, q)) fitted model.fit() if fitted.aic best_aic: best_aic, best_order fitted.aic, (p, 1, q) except Exception: continue fitted ARIMA(train_series, orderbest_order).fit() forecast fitted.forecast(stepslen(test_series)) mae_arima np.abs(forecast.values - test_series.values).mean() print(fARIMA{best_order} MAE: {mae_arima:.3f} 元)定阶不是玄学但不少人把它搞成了玄学。我没有做全空间 AIC 暴力搜索而是在小范围网格里选最优组合同时用 fitted.plot_diagnostics() 确认残差没有明显的自相关结构。ARIMA 只能吃单变量序列天气特征加不进去这是它的天然上限也是 LSTM 的对比优势所在——后面评估阶段如果 LSTM 连 ARIMA 都赢不了那说明要么数据有问题要么网络结构根本没调对。源码包里还提到了 pyflux这是个做贝叶斯时间序列的库可以跑对比实验但 pyflux 维护频率低、依赖容易冲突环境配置上很容易翻车。我只用它验证简单模型主力基准还是 statsmodels。sklearn 在这一环节配合做线性对照比如把过去 7 天价格拼成特征向量喂 Ridge 回归代码量不大但对比表里能直观看出线性模型与神经网络在处理短期波动上的差距。基准模型的意义在于给 LSTM 设一条及格线神经网络花了几十倍训练时间如果误差没有显著低于 ARIMA这个项目就要重新审视数据或者模型设计而不是假装深度学习一定更好。3.3 LSTM 网络设计层数、神经元数与损失函数选择LSTM 是循环神经网络的一种核心是门控机制它能让信息在长序列中传递而不被梯度消失打断。对于菜价这种长度几百条的序列LSTM 不需要堆太深。我看到过不少翻车案例照搬图像分类的网络加深层数训练时间翻倍精度反而下降。这个项目合理的默认结构是单层 LSTM 加一层全连接输出LSTM 单元数在 32 到 128 之间调。# lstm_model.py from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout model Sequential([ LSTM(64, activationtanh, input_shape(7, 1), return_sequencesFalse), Dropout(0.2), Dense(1) ]) model.compile(optimizeradam, lossmse, metrics[mae])参数选型有三个原则。activation 用 tanh 而不是 relu因为 LSTM 内部的状态更新依赖有界激活函数relu 会让状态值在时间步传递中快速膨胀loss 用 mse 而不是 maemse 的梯度在误差大时更陡收敛更快价格预测的极端值虽然被放大惩罚但菜价没有真正的尖峰噪声mse 是合理默认Dropout 加在 LSTM 输出到 Dense 之间只对输出做正则不影响时序内部的状态记忆这比在 LSTM 层内部加 recurrent dropout 更容易调稳。超参数默认值调整方向LSTM 单元数64样本量少就降到 32防止过拟合n_steps 窗口7周期不明显时试 14 或 3Dropout0.2训练 loss 与验证 loss 剪刀差大时提高到 0.5学习率0.001收敛慢降到 0.0005震荡则减半损失函数mse只需涨跌方向时换成交叉熵或自定义方向 loss损失函数的选择要结合业务场景。如果只是预测价格数值mse 合理如果要预测涨跌方向用于采购决策应该把输出层改成二分类并用交叉熵。这个项目定位是数值预测mse 是默认选择。训练时 Adam 初始学习率设 0.001比任何公式推导都接近工程经验。深度学习环境配置上TensorFlow 2.x 配 Python 3.8 是稳妥组合Keras 接口直接调用即可不需要自己实现反向传播。4. 训练与评估跑通 LSTM 脚本并读懂三条曲线4.1 归一化、时间步切分与训练参数LSTM 对输入尺度很敏感菜价几块钱到几十块钱的范围差异虽然不大但不做归一化梯度在小数值特征上很难收敛。归一化用 MinMaxScaler 把价格映射到 0 到 1 之间。关键约束是 fit 只用在训练段测试段必须复用同一个 scaler 做 transform一旦测试段参与 fit数据分布被污染评估结果会虚高得离谱。# lstm_train.py import numpy as np from sklearn.preprocessing import MinMaxScaler from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense series daily.values.reshape(-1, 1) # 只在训练段 fit测试段共用该 scaler scaler MinMaxScaler(feature_range(0, 1)) train_len int(len(series) * 0.8) scaler.fit(series[:train_len]) scaled scaler.transform(series) def make_windows(data, n_steps7): X, y [], [] for i in range(len(data) - n_steps): X.append(data[i:i n_steps, 0]) y.append(data[i n_steps, 0]) return np.array(X).reshape(-1, n_steps, 1), np.array(y) X, y make_windows(scaled, n_steps7) X_train, y_train X[:train_len - 7], y[:train_len - 7] X_test, y_test X[train_len - 7:], y[train_len - 7:] model Sequential([ LSTM(64, activationtanh, input_shape(7, 1)), Dense(1) ]) model.compile(optimizeradam, lossmse, metrics[mae]) history model.fit( X_train, y_train, epochs60, batch_size16, validation_split0.1, verbose1 )train_len - 7 这个偏移很多人看不懂——前 7 天没有足够的历史窗口做预测最后一个训练样本的预测目标刚好落在测试段边界所以训练样本要往前多退一个窗口长度。epochs 设 60 在几百个样本的小数据集上是合理的配合 ModelCheckpoint 把最优权重存下来batch_size 16 是 128 样本规模下的常见选择太大收敛慢太小梯度噪声大。如果发现训练 loss 在 epoch 40 附近已经平了可以考虑把 epochs 砍到 40省时间也降低过拟合风险。训练迭代里 validation_split0.1 是从训练尾部分出校验集校验 loss 用来观察过拟合。训练完成后画 loss 曲线看训练 loss 和校验 loss 是否出现剪刀差——校验 loss 反弹而训练 loss 继续下降就是典型的过拟合信号。那时优先调 Dropout而不是继续加 epochs很多人在这条路上反向操作结果越练越差。4.2 误差评估MAE、RMSE 与滞后判断训练完不能只看 loss要回到原始价格尺度算误差。用 inverse_transform 把预测值还原成元再算 MAE 和 RMSE。菜价正常波动范围在 2 到 10 元之间MAE 在 0.3 元以内算不错的结果超过 0.8 元基本等于没预测出来。RMSE 比 MAE 大说明存在少数误差很大的日期需要定位是不是节假日前后价格跳变这类样本单独分析比整体误差更有价值。# evaluate.py from sklearn.metrics import mean_absolute_error, mean_squared_error pred model.predict(X_test) pred_price scaler.inverse_transform(pred) true_price scaler.inverse_transform(y_test.reshape(-1, 1)) mae mean_absolute_error(true_price, pred_price) rmse np.sqrt(mean_squared_error(true_price, pred_price)) print(fMAE: {mae:.3f} 元, RMSE: {rmse:.3f} 元) # 用残差检验滞后残差与前一日真实价格相关性 0.5 基本可判定预测滞后 residual true_price.flatten() - pred_price.flatten() lag_corr np.corrcoef(residual[1:], true_price[:-1].flatten())[0, 1] print(f残差滞后相关性: {lag_corr:.3f})lag_corr 这个指标我建议任何做时间序列预测的人都算一下。LSTM 做价格预测最容易出现的假象是预测曲线完美贴合真实曲线但整体平移了一天——模型学到的是今天价格约等于昨天价格这种复读规律。残差与前一天真实价格的相关系数如果显著为正说明预测里没有增量信息。这个判断比单纯看 MAE 可靠得多因为复读机的 MAE 反而很小容易让人误以为模型已经收敛到很好。评估样本的选择也有讲究。按时间顺序切分训练测试段是最简单的方式但如果测试段恰好落在菜价暴涨或暴跌的月份误差会被单月极端行情放大。我一般会额外按季度做分组评估看模型在平稳期和波动期的表现差异这个信息对业务方的决策价值远大于单一整数。4.3 可视化与解读matplotlib 出图与多模型对比matplotlib 在这个项目里承担两件事训练过程出 loss 曲线预测结果出真实与预测对比图。Flask 展示端复用了同一套出图逻辑只是输出到 BytesIO 而不是本地文件。出图脚本最需要注意的是中文显示默认字体遇到本地菜心这种标签会直接变成方块配置要提前写死。# plot_results.py import matplotlib.pyplot as plt import matplotlib matplotlib.rcParams[font.sans-serif] [SimHei] matplotlib.rcParams[axes.unicode_minus] False plt.figure(figsize(10, 4)) plt.plot(range(len(true_price)), true_price, label真实价格) plt.plot(range(len(pred_price)), pred_price, labelLSTM 预测) plt.xlabel(日期) plt.ylabel(价格元) plt.legend() plt.tight_layout() plt.savefig(output/lstm_result.png, dpi120)rcParams 里 axes.unicode_minus 这一行很容易被忽略——负号在默认字体下渲染成横杠坐标轴出现负价格会非常难看。这行配置不写出图就是错的属于查半天才发现是字体问题的那种经典坑。出图之后我会把 LSTM 预测线、ARIMA 预测线、真实价格三条曲线画在一张图上观察 LSTM 的优势到底在哪段区间。通常菜价突变段 LSTM 的响应明显快于 ARIMA平稳段的差距并不大这个结论比LSTM 更优这种笼统表述对业务方更有说服力。保存图片时指定 dpi120 兼顾了网页加载速度和清晰度。如果是 Flask 动态返回用 matplotlib 的 Agg 后端渲染到 BytesIO再通过 send_file 输出逻辑见后面章节。出图规范建议固定下来对比图必须标注训练段和测试段的边界否则看图的人很容易把测试段前几天的整体偏移误判成模型能力差。5. 菜价预测避坑五个高频问题的现象与解法5.1 预测曲线整体滞后一天像准了其实没用现象训练完后预测曲线和真实曲线形状几乎一致但整体向右平移了一天MAE 看着很小领导说不错实际毫无价值。原因样本窗口 n_steps7 时模型学到的是价格序列近似随机游走最佳预测就是前一天的观测值。LSTM 在 mse 损失驱动下自动选择这条偷懒路径因为它是最小化训练误差的捷径不是模型笨是目标函数诱导它这么做。解决不要只看 MAE。先算残差与前一日价格的滞后相关性超过 0.5 基本判定为复读机。然后改多步预测让模型预测未来第 3 天而不是第 1 天迫使它学习非平凡的模式。再就是输入特征里加入天气哑变量或价格周环比破坏输入昨天价格就能近似输出今天价格的捷径。5.2 归一化反变换后预测值异常MinMax 作用域不一致现象inverse_transform 还原出的预测价格出现负值或远超历史极值图形完全失真第一反应是模型坏了。原因训练时对全序列 fit 了 scaler测试段也被纳入归一化范围预测值落入训练外区间时反变换自然溢出。另一个更隐蔽的原因是 y 标签也被用全序列 scaler 反变换但标签序列的分布范围和输入并不完全一致边界被放大。解决严格按训练段 fit全序列 transform预测结果用训练段 scaler 反变换的流程执行。在代码里加断言检查反变换后价格是否落在历史价格 min-max 区间内超了就打印报警。这条断言能拦截九成以上的归一化事故我把它写进所有时间序列项目的公共工具函数里。5.3 CSV 中文列名读出来乱码、价格列是 object 类型现象pd.read_csv 读本地菜心.csv列名显示乱码价格列 dtype 是 object一求均值就报错。原因文件是带 BOM 的 UTF-8Windows 系统下编辑过的文件经常附带 BOMpandas 默认 utf-8 编码不解 BOM 就会把首列读成乱码。价格列变成 object 则是因为个别单元格混入了暂无缺货这类文本pandas 为了保住整列只能降级成字符串类型。解决read_csv 显式指定 encodingutf-8-sig然后对价格列执行 pd.to_numeric(errorscoerce)非数值自动变 NaN再走插值流程。我在清洗脚本里固定这两步任何 CSV 进来先过一遍。顺手用 DataFrame.info() 检查每列 dtype这是进模型前的最后一道保险数据质量不过关什么都别谈。5.4 Flask 页面图表不加载或刷新不更新matplotlib 会话状态在作怪现象局域网访问 Flask 页面第一次能看到图刷新后图表变成空白或仍然显示旧图换个浏览器又正常。原因matplotlib.pyplot 维护全局会话状态多个请求共用一个进程时 figure 被复写BytesIO 写入位置没有重置浏览器又缓存了同名图片三个因素叠加就会表现成时好时坏。解决改用 matplotlib.figure.Figure 面向对象接口不使用 pyplot每个请求新建 Figure用 FigureCanvasAgg 渲染到 BytesIO每次响应前对 BytesIO 执行 seek(0) 重置指针。响应头加 Cache-Control: no-store 防浏览器缓存。这几个坑我逐一踩过最后统一封装成一个 render_chart() 函数页面代码干净很多。5.5 微信公众号菜单查询超时微信服务器只有 5 秒耐心现象用户在公众号菜单点查询菜价转圈很久后返回该公众号提供的服务出现故障服务端日志却显示接口正常执行了。原因微信服务器要求 5 秒内响应。Flask 接口里同步调用 LSTM 预测加 MongoDB 查询首次请求还要加载模型权重整体耗时轻松超过 5 秒。项目说明里列了 Python 和 PHP 两种语言公众号后台如果用 PHP 做签名校验和菜单响应更要留意这个时间窗口。解决把每日预测结果在凌晨统一算好写入数据库公众号接口只做查库返回这个轻操作不碰模型推理。首次请求仍超时的兜底方案是接口先返回 success再用客服消息异步推送结果但客服消息有 48 小时时效限制业务流程上需要权衡。我现在所有公众号数据类接口都强制走预计算加缓存这条路模型计算永远不在用户请求链路上出现。6. 把验证做扎实用滚动回测替代单次划分评估 LSTM单次 train/test 划分评估 LSTM 有个隐患只验证了一组切分点的表现换一组切分点结论可能完全不同。菜价序列不长更可靠的做法是滚动回测——从序列尾部向前滚动每次用截至当前时间的数据训练预测未来 1 天窗口前移累计所有预测误差。这种评估方式更接近真实业务场景每天凌晨重新训练一次预测当天价格预测完再把这天并入训练数据。# rolling_backtest.py import numpy as np def rolling_evaluate(series, model_fn, n_steps7, n_rolls30): 滚动回测每轮只预测未来 1 天窗口逐步前移 errors [] for k in range(1, n_rolls 1): end len(series) - n_rolls k train series[:end] # 训练数据截至当前日 pred model_fn(train) # 返回明日价格预测 true series[end] # 明日真实价格 errors.append(abs(pred - true)) return np.mean(errors), np.median(errors)model_fn 内部封装了归一化、切窗口、重训、预测的完整流程每次迭代用到的训练集都比上一轮多一天。滚动回测的代价是重训多轮但菜价数据量小单轮训练几十秒完全可接受。要再快一点可以用 model_fn 的增量拟合接口只做一步 fit 而不是从头重训精度损失在这个数据规模下几乎看不出来。对比单次划分滚动回测能暴露模型在不同序列位置的表现差异。如果某几个月的误差整体上移基本能对应到价格波动的特殊时段比如节假日前后的跳变这是单次划分看不到的信息。多步预测也建议在滚动框架里做把 horizon 参数设成 3 或 7让模型直接输出未来第 3 天或第 7 天的价格和单步预测比能看出模型长时记忆能力的真实边界。从那以后我拿到任何一个时间序列预测资源第一件事都是先搭滚动回测框架跑完再决定要不要深入调参。单次划分只用来快速调试所有最终结论以滚动回测为准。这套流程下来你对 LSTM 在菜价预测上到底有几斤几两心里会很踏实不用再被单次划分的漂亮指标骗着走。希望帮到你。本文还有配套的精品资源点击获取
返回列表