简介:基于深度学习LSTM的蔬菜价格预测完整毕业设计项目,包含Python源码、项目说明与配套数据集。项目针对蔬菜价格时间序列数据,利用长短期记忆网络建模预测,适合计算机相关专业正在准备毕设的学生,以及需要通过真实项目完成课程设计或期末大作业的实战学习者。压缩包内共181个文件,约1.86MB;其中142个csv为本地菜心、西红柿、青皮冬瓜等常见蔬菜的价格历史数据,25个py为模型构建、训练与预测的Python源码,10个pyc为编译缓存,3个docx为项目背景与使用说明文档,1个md为简要说明,整体结构清晰。项目在站内已有271人学习,且为导师指导并认可的高分毕业设计,评审分98分。通过该项目可掌握LSTM时序预测的完整流程,包括数据清洗与划分、模型搭建、训练调参及结果评估,并可直接对照源码与文档进行二次开发,适合作为毕业设计或课程实践参考。
1. 蔬菜价格预测,为什么值得用 LSTM 做毕业设计
黄瓜三天前还卖两块五一斤,一场雨后直接翻到五块;菠菜上周还在一块钱档,这周就冲上三块。蔬菜价格天然带着滞后和波动,线性回归和传统 ARIMA 见到这类序列往往直接投降,不是算不准,而是它连“前天的高价会在今天产生什么影响”这种非线性关系都表达不出来。基于深度学习 LSTM 做蔬菜价格预测,正是把这类时间序列问题交给循环网络去学:模型不只看当天的价格,还会在内部记住一段历史,再从历史里找规律。这个标题里的 python 源码、项目说明和数据集,打包的是一个可以从数据处理一路做到预测画图的完整毕设项目。适合正在做机器学习或数据科学方向选题、想避开“烂大街图像分类”的同学,也适合想快速把 LSTM 用于真实业务数据练手的从业者。
2. 读懂这套基于 LSTM 的蔬菜价格预测项目:原理与代码结构
2.1 LSTM 为什么能记住“上周的菜价”,普通神经网络记不住
在做时间序列预测时,最简单的做法是“滑窗”:把过去七天的价格当作七个特征,丢给全连接网络,让它预测第八天。这种做法本身没有错,错在窗口长度固定后,模型永远只能用最近这七天,再早的影响就丢了。比如端午节前十天蒜薹开始涨价,只给七天窗口,模型根本看不到节前备货的信号。
LSTM 属于循环神经网络家族,它允许信息沿着时间步一路向后传递。每个时间步都接收当前输入和上一步传下来的隐藏状态,然后通过三个称为“门”的结构决定三件事:丢掉哪些旧记忆、写入哪些新信息、输出哪些内容。对应到菜价里就是:上周五的暴雨导致菠菜大涨,这个记忆会被保留几天;等到价格回落、市场恢复平稳,模型又会把这条旧记忆逐步“忘掉”。这种机制让 LSTM 在输入只有“当天价格”这种单变量时,也能自己从数据里学出滞后和趋势,不需要手工构造一堆移动平均特征。
这也是它长期被用于股票、销量、气象和价格预测的根本原因。相比 ARIMA 这类统计模型,LSTM 对非线性特征更宽容;相比普通前馈网络,它又能把远近不同时间跨度的历史一起纳入考虑。理解到这一层,打开代码时就不会被num_layers、hidden_size这些参数吓住,它们其实是在调节 LSTM 的记忆容量和时间建模深度。
2.2 拿到源码包先看什么:目录结构与运行流程
这种“源码+项目说明+数据集”的毕设打包,解压后通常会有相对固定的结构。我拿到手的第一步,从来不是直接跑训练,而是先花十分钟理清目录,避免缺失依赖和数据路径写到一半才报错。
| 常见文件/目录 | 典型职责 | 一开始看这里的目的 |
|---|---|---|
data/ | 存放原始数据和清洗后的数据 | 确认数据格式是 csv 还是 xlsx,是否已经按日期排好 |
models/ | 保存训练好的模型权重 | 看有没有提供现成 checkpoint,能不能跳过训练直接预测 |
train.py | 训练入口 | 确认命令行参数、默认值、模型保存位置 |
predict.py | 预测入口 | 确认输入输出是单条样本还是整个文件 |
data_preprocess.py或utils.py | 清洗、归一化、构造滑窗样本 | 看数据预处理和训练是否耦合在一起 |
requirements.txt | Python 依赖列表 | 确认 torch 版本、是否依赖 pandas 和 sklearn |
README.md或项目说明.docx | 项目说明 | 优先读它,里面通常会写明数据集来源和运行步骤 |
读项目说明时重点看三件事:数据集是采集的真实农贸市场价格,还是模拟生成的;价格字段单位是元/斤还是元/公斤;模型是每个蔬菜品种单独建模,还是所有品种合在一个模型里。这三件事直接决定后面所有步骤正确与否。
整个项目的运行链路一般是:读取原始 csv → 清洗缺失值和异常值 → 归一化 → 构造滑窗样本 → 划分训练集和验证集 → 训练 LSTM → 反归一化 → 输出并绘制预测结果。这个链路中任何一环出问题,都会在生产预测阶段暴露出来,而不是在训练阶段,这点做到后面会有更深的体会。
2.3 最小跑通:从构建环境到完成第一次训练
先把项目跑起来,再回头理解细节,这是成本最低的启动方式。假设压缩包已经解压到本地目录,打开终端执行下面的命令。
cd /path/to/解压后的项目目录 # 创建虚拟环境,避免 torch 和其他项目依赖互相污染 python -m venv .venv # Windows 用 .venv\Scripts\activate,Linux/macOS 用下面这行 source .venv/bin/activate # 安装依赖:torch、pandas、numpy、matplotlib、scikit-learn pip install -r requirements.txt如果项目没有提供requirements.txt,装一套最小依赖也够用:torch、pandas、numpy、matplotlib、scikit-learn。安装完成后,看一眼入口脚本名称,多数项目是train.py,也可能叫main.py。训练命令通常长这样:
python train.py --seq_len 7 --epochs 50 --batch_size 32 --lr 0.001这里seq_len=7代表用最近 7 天价格预测下一天,epochs=50是把全部训练数据重复学 50 轮,batch_size=32是每批喂入的样本数量,lr是 Adam 优化器的学习率。第一次跑通,不需要追求精度,只要能看到 loss 逐轮下降,说明环境、数据、模型三者的衔接没有问题。
跑通后打开项目里的数据文件看一眼结构,我习惯用下面这段代码做快速预览:
import pandas as pd # 用 pandas 直接读 csv,打印前五行和字段类型 df = pd.read_csv('data/raw/vegetable_price.csv', parse_dates=['date']) print(df.head()) print(df.info())打印结果里最重要的信息是字段名和日期范围。字段名要和你后面预处理代码里写的列名完全一致,否则KeyError会在训练中途爆出来。日期范围决定了你做训练集和验证集划分时能留多少历史数据,一般少于 500 条记录的蔬菜序列,LSTM 能学到的规律非常有限。
3. 把菜价历史数据变成 LSTM 能学的东西:数据集与特征工程
3.1 一份合格的蔬菜价格数据集长什么样
很多同学从网上下载到的城市菜价数据,字段命名五花八门:“品种”“品名”“avg_price”“prices”等等。不管叫什么,最后落到训练脚本里至少要包含三个字段:日期、品种、价格。下面是一个我见过比较规整的格式。
| date | variety | price | unit |
|---|---|---|---|
| 2023-01-01 | 黄瓜 | 2.80 | 元/斤 |
| 2023-01-02 | 黄瓜 | 2.50 | 元/斤 |
| 2023-01-03 | 黄瓜 | 3.20 | 元/斤 |
拿到原始数据后,第一步不是建模,而是检查数据质量。常见的问题有三个:同一日期出现多条重复记录,需要按均值或最后一次记录去重;某个日期连续缺失多天,需要决定是前向填充还是直接剔除;价格列里混入“暂无报价”“--”之类的非数字字符串,要用pd.to_numeric配合errors='coerce'把它们转成 NaN 再处理。
有一个很容易忽略的细节是价格单位。有的批发市场数据按“元/公斤”记录,有的零售数据按“元/斤”记录。标题里说“蔬菜价格预测”,但价格单位不统一会让模型输出变得不可解释。统一单位是预处理里最优先做的事,不然预测结果即便 loss 很低,实际换算后也可能差一倍。
在处理缺失值这件事上,蔬菜数据比股票数据更难办。股票可以停牌不交易,菜价没有“停盘”一说,缺失日期的价格只能靠相邻日期插值。对连续缺失超过 3 天的区间,我通常直接放弃这一段,而不是强行填充,因为连续多天的价格真空会让 LSTM 学到一段假的平滑过渡。短于 3 天的缺失,用前一天的数值前向填充就足够了。
3.2 归一化与滑窗:顺序做反了,精度直接掉一半
LSTM 默认使用 tanh 作为激活函数,输入数据如果分布在几十元甚至上百元的尺度,梯度很容易饱和,训练初期 loss 会跳动得厉害。因此价格序列几乎都要先做归一化。最常见的做法是 MinMaxScaler,把价格映射到 0 到 1 之间。
顺序上有个新手极容易踩的坑:必须在切分训练集和测试集之后,只对训练集做fit,再用同一个 scaler 去transform测试集。这样做的原因后面第 5 章会展开,这里先保证别把顺序写反。
import pandas as pd from sklearn.preprocessing import MinMaxScaler # 读取并排序,数据按日期升序排列是时间序列建模的前提 df = pd.read_csv('data/processed/cucumber.csv', parse_dates=['date']) df = df.sort_values('date').reset_index(drop=True) # 按 8:2 切分训练集和测试集,注意这里不能用随机切分 train_size = int(len(df) * 0.8) train_price = df['price'].iloc[:train_size] test_price = df['price'].iloc[train_size:] # 只对训练集 fit,测试集数据不能参与计算 min/max scaler = MinMaxScaler(feature_range=(0, 1)) train_scaled = scaler.fit_transform(train_price.values.reshape(-1, 1)) test_scaled = scaler.transform(test_price.values.reshape(-1, 1))这段代码里最值得注意的就是scaler.fit_transform和scaler.transform的配对关系。fit_transform会计算训练集的最小值和最大值,transform只是套用这些参数,不会重新计算测试集的统计量。如果写成先合并全部数据再整体归一化,测试集的信息就提前进入了模型训练过程,验证指标会虚高,部署后会明显缩水。
归一化完成后,接下来要构造滑窗样本。LSTM 的输入形状通常是(batch_size, seq_len, input_size),input_size 在单变量预测时等于 1。下面的函数把一个连续价格数组切成了很多个“窗口+标签”样本。
import numpy as np def make_sequences(data, seq_len=7): xs, ys = [], [] for i in range(len(data) - seq_len): x = data[i:i + seq_len] y = data[i + seq_len] xs.append(x) ys.append(y) return np.array(xs, dtype=np.float32), np.array(ys, dtype=np.float32) # 用 7 天窗口预测第 8 天价格 X_train, y_train = make_sequences(train_scaled, seq_len=7) X_test, y_test = make_sequences(test_scaled, seq_len=7) print(X_train.shape, y_train.shape)这里seq_len=7意味着训练样本第一条是第 1 到第 7 天的价格,标签是第 8 天;第二条是第 2 到第 8 天,标签是第 9 天,依此类推。窗口越大,模型能看到的历史越长,但同时可用的训练样本越少,因为序列长度固定时,前seq_len个位置没有足够的“过去”。对蔬菜价格这类周期大约一周的序列,7 天和 14 天是比较常用的窗口。如果你不知道选多少,先跑 7,再跑 14,对比验证集误差再决定。
3.3 按蔬菜品种建模还是混合建模:两种做法的取舍
数据集里往往有几十个品种,大家第一反应是全部塞进一个模型。这样做的优点是样本量大,缺点是不同品种的价格水平差异太大:大白菜常年两块钱以内,四季豆冬天能冲到十几块。共用一个输出尺度时,LSTM 会把重点集中在价格数值更大的品种上,大白菜的预测误差反而不太会受到惩罚,因为它的绝对值太小。
主流做法有两种。第一种是按品种拆分,单独训练多个 LSTM 模型,每个模型只学一个品种的价格规律。这种做法的优点是完全解耦,缺点是品种一多,训练和调参的工作量成倍上涨。第二种是混合建模,也就是把所有品种放在一起,但给模型额外输入一个品种编号作为特征。特征工程上,可以对品种做 one-hot 编码,也可以训练一个 Embedding 层,让模型自己去学习品种之间的相似性。
# 按品种拆分训练的常见写法 variety_list = df['variety'].unique() for variety in variety_list: sub = df[df['variety'] == variety].sort_values('date') # 每个品种都单独走一遍归一化和滑窗构造 # 然后训练一份独立的 LSTM pass我个人的经验是:如果毕设要求呈现“一整套完整系统”,按品种拆分更容易讲清楚,因为你可以展示模型在不同品种上的差异化表现;如果想突出模型的泛化能力,或者数据集较小、单个品种样本不够,那就走混合建模。混合建模只需要给模型多设一个输入分支,PyTorch 里用nn.Embedding就能实现,这里不展开。新手做毕设,优先选择按品种拆分,逻辑清楚,调试方便,答辩时也能有更多话说。
4. 训练 LSTM 模型:结构、参数与结果解读
4.1 模型结构:几层 LSTM、Dropout 和全连接层怎么接
滑窗构造完成后,接下来就是模型本身。一个能跑通的 LSTM 预测模型,核心结构一般只有三层:LSTM 层、全连接输出层,中间可以夹 Dropout。下面的代码是一个最常见的教科书级结构,也是这套源码里最可能出现的写法。
import torch import torch.nn as nn class PriceLSTM(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2, dropout=0.2): super().__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout ) self.fc = nn.Linear(hidden_size, 1) def forward(self, x): # x 形状: (batch_size, seq_len, input_size) out, _ = self.lstm(x) # 取最后一个时间步的输出,相当于把“看完整个序列后的理解”交给全连接层 out = out[:, -1, :] return self.fc(out)几个参数需要认真理解。input_size=1是因为当前输入只有“价格”这一个特征,如果你后面加入了温度、节假日等特征,就要改成对应的数值。hidden_size=64是 LSTM 隐状态的维度,相当于模型记忆的容量,容量过小会记不住周期规律,容量过大则容易过拟合。num_layers=2表示堆叠两层 LSTM,层数越多模型可以建模更高层的时间抽象,但训练耗时和内存占用也会上升。batch_first=True表示输入形状是“批次在第一位”,如果忘了这个参数,拿出来的数据形状就会和输入维度错位,报错信息会让人绕很久。
Dropout 要注意一点:nn.LSTM的dropout参数只在num_layers > 1时才生效,单层 LSTM 就算设置了dropout=0.2也不会有任何正则化效果。此外,最后一层 LSTM 后面再接一个nn.Dropout(0.2)也很常见,有的代码会把 Dropout 放在全连接层之前,这两种位置都可以,但不要为了调参把两处都设得很大,否则模型可能学不到东西。
4.2 训练循环与评估指标:不要只看 loss 曲线
模型定义好之后,训练循环本身并不复杂。PyTorch 的标准迭代逻辑就是前向传播、算损失、反向传播、更新参数。这里给出最简版本,足够作为源码阅读对照。
from torch.utils.data import TensorDataset, DataLoader X_train_t = torch.from_numpy(X_train) y_train_t = torch.from_numpy(y_train) train_loader = DataLoader( TensorDataset(X_train_t, y_train_t), batch_size=32, shuffle=True ) model = PriceLSTM(input_size=1, hidden_size=64, num_layers=2, dropout=0.2) optimizer = torch.optim.Adam(model.parameters(), lr=0.001) criterion = nn.MSELoss() for epoch in range(50): model.train() epoch_loss = 0.0 for x_batch, y_batch in train_loader: optimizer.zero_grad() pred = model(x_batch) loss = criterion(pred.squeeze(), y_batch) loss.backward() optimizer.step() epoch_loss += loss.item() if (epoch + 1) % 10 == 0: print(f'epoch {epoch + 1}, loss: {epoch_loss / len(train_loader):.6f}')DataLoader里的shuffle=True是大多数分类任务的标准配置,但时间序列任务里它需要被谨慎对待。后面避坑章节会细讲,这里先记住一点:对滑窗样本做洗牌,会把原本按时间排列的样本顺序打乱,但每个样本内部的窗口顺序没有变化,训练本身仍然有效。关键在于训练集和验证集必须按时间先后切分,不能让验证集日期出现在训练集之前。
训练完成后,评估才是重头戏。很多人只看 loss,loss 是归一化尺度上的误差,看着 0.001 很兴奋,反归一化后才发现误差有五六毛钱。更直观的指标是 MAPE,也就是百分比误差。下面这段代码计算的是“平均绝对百分比误差”。
def mape(pred, true): pred = np.array(pred) true = np.array(true) return np.mean(np.abs((true - pred) / true)) * 100MAPE 在菜价预测里比 MSE 有用得多,因为它直接告诉你“平均偏离真实值百分之几”。比如 MAPE 等于 15,意味着预测价格与真实价格平均偏差 15%,2 元一斤的黄瓜平均偏掉三毛钱,这个精度放在实际市场里已经很有说服力。评价模型好坏,不能只看单点误差,还要把验证集的预测结果反归一化后和真实价格画在一起,肉眼看曲线拟合程度。训练集表现好、验证集表现差,是过拟合;验证集和测试集都表现平平,可能是序列长度或者隐藏层容量没设好。
4.3 三个最值得调的参数:序列长度、隐藏层、学习率
LSTM 的可用超参数不少,但对蔬菜价格这种规模不大的数据集,真正对结果影响最显著的只有三个:序列长度、隐藏层大小、学习率。
| 参数 | 常用范围 | 影响 |
|---|---|---|
seq_len | 7、14、30 | 控制模型回看历史长度;太短学不到周期,太长压缩训练样本 |
hidden_size | 32、64、128 | 记忆容量;过大容易过拟合,过小欠拟合 |
lr | 0.0005、0.001、0.003 | 学习率过高导致 loss 震荡,过低收敛极慢 |
seq_len的选择可以结合蔬菜价格的波动周期来看。叶菜类一般一周到十天一个供应周期,根茎类波动更慢,所以 7 和 14 是起步首选。hidden_size不建议直接上 256,蔬菜价格序列样本量通常只有几百到几千条,隐藏单元太多会让模型轻松背下训练集,验证集却错得离谱。lr对 Adam 优化器来说,0.001 是安全的起点。如果 loss 前几个 epoch 不降反升,先看看是不是lr设大了;如果 loss 下降很慢,那可以试试提高到 0.003。
调参没有银弹,我习惯的做法是固定其他参数,只动一个变量,跑完记录验证集 MAPE,再回来看结果。同时开太多参数组合,最后出了问题你根本不知道是哪一项的锅。毕设项目能交差的标准其实不高:训练曲线收敛、验证集 MAPE 在 15% 上下、预测图能跟上真实曲线的整体走势,就已经达到“基于深度学习 LSTM 实现蔬菜价格预测”的预期了。
5. 避坑:蔬菜价格预测中最容易翻车的 5 个细节
5.1 归一化泄漏:测试集信息跑到训练集里
现象:训练集和验证集的 loss 都低得漂亮,预测曲线单独看也正常,但把测试集真实价格放上去,发现预测曲线整体比真实值低一截,或者缩在一个很窄的区间里。
原因:归一化时没有先切分训练集和测试集。如果对整条价格序列直接做fit_transform,测试集的最大值和最小值已经参与了计算,模型在训练时等于提前知道了未来数据的取值范围。预测阶段套用这个 scaler 后,新数据里的极端价格很容易被压出既定区间。
解决:严格遵循“先切分,再拟合”的顺序。训练集上scaler.fit_transform,验证集和测试集只做transform。代码改动很小,但影响非常大。
# 错误:全量数据 fit,会引入未来信息 scaler = MinMaxScaler() all_scaled = scaler.fit_transform(price.reshape(-1, 1)) # 正确:只在训练集上 fit scaler = MinMaxScaler() train_scaled = scaler.fit_transform(train_price.reshape(-1, 1)) test_scaled = scaler.transform(test_price.reshape(-1, 1))5.2 时间顺序被打乱:随机切分让模型“作弊”
现象:代码里用train_test_split(test_size=0.2, random_state=42)切分数据后,验证集 MAPE 表现很好,换成按时间顺序切分再测,误差直接涨了几倍。
原因:train_test_split默认是随机采样。随机切分时,训练集里混入了日期上位于验证集之后的样本。对时间序列来说,这就是未来数据泄漏。LSTM 在训练时已经见过“未来”的价格走势,验证自然漂亮,但部署时未来的数据根本不存在,模型立刻现出原形。
解决:用按时间顺序的索引切分,或者使用TimeSeriesSplit。下面的写法最直观。
# 按时间顺序切分,前 80% 训练,后 20% 测试 train_size = int(len(df) * 0.8) train_series = df['price'].iloc[:train_size] test_series = df['price'].iloc[train_size:]5.3 多步预测的误差越滚越大
现象:单步预测每天误差很小,但让模型连续预测未来 30 天,预测曲线要么慢慢变成一条水平线,要么越走越偏,最后飞到天上。
原因:连续预测时,第一步预测的误差会被当作第二步的输入,误差像滚雪球一样累积。LSTM 在训练时用的是真实历史价格作为输入,而预测阶段输入的是自己上一轮的输出,分布不一致,这在学术上叫“暴露偏差”。
解决:不要用“预测一天→把预测值拼到输入里→再预测一天”的滚动方式做长周期预测,除非你明确知道误差累积的影响可以接受。更稳定的做法是一次性输出未来 N 天价格,也就是把输出维度改成 N,或者在训练时随机用一部分预测结果替代真实值作为输入,让模型学会纠正自己的误差。对毕设来说,最简单的改进是评估时只看未来 1 到 7 天的预测,并明确写出误差累积的边界。
5.4 数据里的尖峰价:模型学会了“晴天霹雳”
现象:真实价格曲线在某几天突然暴涨,比如从 2 元跳到 6 元,但模型的预测曲线在这些位置总是反应迟钝,往往晚两天才跟上去,而且滞后还会污染后面几天的预测。
原因:暴雨、台风、节假日、市场短期缺货都会造成价格尖峰,但这些信息没有体现在输入特征里。模型只能从历史价格里推测,可这种突发性尖峰在训练集里只是偶发事件,模型为了整体 loss 最小,宁可把尖峰当噪声忽略掉。
解决:两个方向。第一,修正训练数据,把明显异常且无法解释的尖峰做削峰处理,比如用前后三天的中位数替代;第二,保留尖峰,但把节假日、天气这类特征作为额外输入加入模型,让模型知道“暴雨后的涨价是合理反应”而不是偶然噪声。如果毕设数据里没有天气字段,最简单的做法是只对尖峰做标记,让模型在后续反向传播时少被这些极端值牵着走。
5.5 跨品种预测时价格尺度不一致
现象:把所有蔬菜品种放在一起训练,loss 收敛得很快,但单独看每个品种的预测图,发现高价品种拟合得好,低价品种简直是条直线。
原因:不同蔬菜的价格范围差异悬殊。大白菜价格常年在一两元,四季豆旺季能到十几元。共享一个归一化尺度时,LSTM 在均方误差的驱动下,把绝大多数容量用来拟合高价品种的波动,低价品种的误差绝对值太小,对 loss 的贡献微不足道,自然学不好。
解决:最省事的方法是按品种拆分模型,一个品种独立归一化、独立训练。正如第 3 章所说,这个方法会带来多套模型和配置,但胜在稳定。如果你有品种编号字段,并且想保留混合建模,记得对每个品种单独做归一化,再把品种 id 作为离散特征输入,通过 Embedding 层把品种差异交给模型自己学习。
6. 再进阶一步:多步预测与“预测值-真实值”对照验证
当你已经把单步预测跑通,验证集 MAPE 也能稳定在 15% 以内,下一步值得做的是把预测结果变成更可信的业务输出。我每次做完训练后的第一件事,不是调参,而是把验证集最后 30 天的真实价格拿来做一次整体对齐,画出两条曲线并排放在一起。这一步能立刻暴露出前面 5 个坑里至少一半的问题。
线画好之后,如果只看单天效果不错,那可以再做一次回测式验证。常见做法是 walk-forward:从数据集中间某个位置开始,每次都只用当前时刻之前的窗口做预测,然后向后移动一步,看历史预测误差稳定不稳。
def walk_forward_validate(model, window, scaler, pred_steps=7): model.eval() future = [] cur_window = window.copy() with torch.no_grad(): for _ in range(pred_steps): x = torch.tensor(cur_window, dtype=torch.float32).unsqueeze(0) pred = model(x).item() future.append(pred) cur_window = np.concatenate( [cur_window[:, 1:, :], np.array([[[pred]]], dtype=np.float32)], axis=1 ) # 反归一化,得到真实价格尺度下的预测值 return scaler.inverse_transform(np.array(future).reshape(-1, 1))这段代码展示了递归多步预测的基本逻辑:预测出一个点,就把它接到窗口末尾,同时丢掉窗口最前面的旧点,继续预测下一天。值得注意的是第 5 章讲过的误差累积问题,跑这个函数时你会直观看到预测曲线在前 1 到 3 步内贴近真实值,越往后越平缓。我习惯把递归结果和直接多步输出各跑一组,两个结果一对比,就能判断模型到底是因为学到规律而预测得好,还是只是因为惯性延展了上一步的走势。
做完这些之后,不管项目是不是到做多步预测的深度,都建议回到损失曲线的震荡幅度上看一眼。如果 loss 曲线上下跳得厉害,多半是lr偏高或者 hidden_size 偏大。我现在的调参习惯是:先不管那些“深度学习玄学”,把一条能跑通的基线段落先稳定下来;然后拿真实价格和预测价格的残差图去看,哪些日期误差最大,再回过头查这些日期是不是节假日、下雨天或市场断供期。能解释清楚的误差,才是对你模型预期的真实反馈。
这个方向无论作为毕设答辩还是入门时序预测实战,都是一个容易讲清楚、也容易验证的项目。真正把它做好,靠的还是对数据质量较真、对参数变化敏感、对预测边界诚实。希望这些整理出来的细节,能帮你在自己的数据集上少踩几个坑,早点跑出第一条能看过眼的预测曲线。
本文还有配套的精品资源,点击获取