
简介这是一套基于PyTorch的LSTM时间序列预测完整源码定位是让小白也能快速跑通整个预测流程解决单变量/多变量输入、单步/多步预测等常见时序建模需求。程序从Excel或CSV读取数据更换数据集无需改动核心代码内置MAE、MSE、R2、MAPE等评估指标并按标准框架划分训练集、验证集与测试集便于观察模型在不同阶段的表现。资源共27个文件约9.22MB以py源码、pyc编译文件、xlsx示例数据、png效果图及pdf/docx使用说明手册为主另附带训练好的模型权重可快速对照示例结果验证。配套注释和使用手册覆盖数据替换、模型训练、预测与评估全流程做课程设计、毕业设计或复现实验对比都很顺手。已有266人学习下载适合刚接触时序预测并希望直接套用模板的初学者。1. LSTM时间序列预测的“无脑”源码这个包到底帮你省了哪些事当你手里只有一份 Excel——销量、传感器温度、设备振动幅度、水库水位——却要在几天内交付一个能跑的基于 LSTM 的时间序列预测程序时真正让人头大的往往不是模型数学而是数据怎么喂进去、训练完怎么保存权重、预测完怎么算误差。这份基于 PyTorch 的 LSTM 预测源码包刻意走“少折腾、直接出结果”的路线单变量/多变量输入自由切换单步/多步预测自动适配数据直接从 Excel/CSV 读取训练集、验证集、测试集标准三切分训练结束一口气给你 MAE、MSE、R2、MAPE 四个指标。我拆过它全部代码结论是它配得上“无脑”两个字但里面还是藏了四个隐蔽坑。这篇文章从文件结构到模型参数、从数据替换到指标计算把这包源码一次讲透。2. 数据从 Excel 读到 LSTM 输入格式要求、归一化与三集划分2.1 数据读取Excel/CSV 的打开方式与格式红线这个源码包的数据入口是 Excel 或 CSV不是网上现成的数据集。你需要把数据整理成至少两列一列是时间或顺序索引一列是目标值跑多变量就再加若干特征列目标列放最后一列。先看最小读取代码import pandas as pd import numpy as np # data.xlsx 是默认数据文件sheet_name0 表示第一个工作表 df pd.read_excel(data.xlsx, sheet_name0) print(df.columns.tolist()) # 确认列名 print(df.head()) # 确认前几行 print(df.dtypes) # 确认每列数据类型这段代码里有两个容易被忽略的点。第一列名不要用中文和空格虽然源码里很多位置靠列索引取值但换数据时列名干净能大幅减少排错成本。第二dtypes 输出很关键Excel 里看着是数字的列经常混入少量文本变成 object 类型后续 sklearn 的 MinMaxScaler 会直接报错。正常的 dtype 应该是 float64 或 int64看到 object 就说明数据表里藏了非数字内容。CSV 的读取稍微不同df pd.read_csv(data.csv, encodingutf-8-sig)编码参数我建议固定写 utf-8-sig。Windows 上用 Excel 另存的 CSV 默认是 ANSI/GBK 编码不带这个参数中文列名或者文字型特征读进来直接乱码。utf-8-sig 的好处是同时兼容带 BOM 和不带 BOM 的 UTF-8 文件Linux 和 Windows 之间来回拷文件都不出问题。读取之后第一件事是清理数据。时间序列数据的清理按顺序做先 dropna() 或者 fillna(methodffill)把缺值处理掉再用肉眼扫一遍有没有明显超出量级的异常值。时间序列里如果混入一个 1000 倍的峰值归一化时整个数据的尺度都会被拉歪模型的预测基本作废。源码包里数据是干净的但换成你自己的 Excel 时这一关必须自己过。2.2 归一化为什么选 MinMaxScaler 而不是 StandardScalerLSTM 内部激活函数默认是 tanh输出被压在 [-1,1]原始数据如果是几万、几十万的量级梯度在反向传播时很容易爆炸。源码包默认用 MinMaxScaler 把数据压到 [0,1]。from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler(feature_range(0, 1)) data scaler.fit_transform(df.iloc[:, 1:].values.astype(np.float32))这里有两个细节。第一个是 .values.astype(np.float32)PyTorch 默认的 FloatTensor 是 32 位浮点如果喂 float64 的 numpy 数组在构造 Dataset 时大概率报 dtype 不匹配。第二个是 df.iloc[:, 1:]如果 DataFrame 里有时间列或 id 列必须先切掉时间戳在数学上没有时序预测意义混进去反而干扰特征计算。注意fit_transform 用在整份数据上还是先 fit 训练集再 transform 测试集最严格的写法是先对训练集 fit验证集和测试集用同一个 scaler 做 transform避免测试集信息提前泄漏到归一化参数里。数据量够大且分布平稳时直接对全量 fit 工程上也常见但严格项目建议按前者来。这里解释一下为什么不用 StandardScaler。StandardScaler 把数据变成均值 0 方差 1适合线性模型和部分深度网络但 LSTM 场景里 MinMaxScaler 更常见原因是它严格限制输出范围让 tanh 激活始终处于有效工作区间。还有一个实际考虑MinMaxScaler 反归一化时直接乘回去就还原真实数值不来回折腾对新手更友好。2.3 滑动窗口构造与三集划分的逻辑这个源码包比很多开源项目讲究的地方在于分了三份而不是两份。默认比例 70% 训练、20% 验证、10% 测试。代码逻辑等价于train_size int(len(data) * 0.7) val_size int(len(data) * 0.2) train_data data[:train_size] val_data data[train_size:train_size val_size] test_data data[train_size val_size:]时间序列切分有两个必须强调的点。第一不能随机 shuffle。数据一批接一批打乱之后模型就看到未来信息训练指标再好看也是假象。第二三份数据之间允许有滑动窗口的重叠这不叫数据泄漏而是时间序列样本构造的正常形态因为窗口之间本来就共享了部分历史。滑动窗口的构造是时间序列预测的核心步骤源码里对应这段逻辑seq_len 7 # 用过去7个时间步预测下一个 x_samples, y_samples [], [] for i in range(len(data) - seq_len): x_samples.append(data[i:i seq_len, :]) y_samples.append(data[i seq_len, -1])x_samples 的每个元素形状是 (seq_len, feature_num)y_samples 是单个标量。这一个标量就是“单输出”的含义。seq_len 参数决定模型看多长的历史取值建议参考数据周期按天记录的销量数据一个周期是 7 天seq_len 取 7 或 14 起步分钟级交易数据先画自相关图找周期再定没有固定公式。2.4 单变量/多变量切换feature 数量怎么影响 input_size源码支持单变量和多变量自由切换落到代码层面就是 input_size 一个参数。单变量时 input_size1多变量时 input_size特征列总数。在读取阶段的表现差异是# 单变量只保留目标列 df pd.read_excel(data.xlsx, sheet_name0) single df[[value]].values # 多变量保留所有特征列 multi df.iloc[:, 1:].values单变量适合数据维度有限、只关心目标本身趋势的场景多变量适合有外部驱动的场景比如预测销量时把天气、促销、节假日一起喂进去。但多变量不是免费的特征列越多模型需要的数据越多特征之间如果存在强相关性LSTM 可能学出冗余表示。建议先从单变量跑通再用多变量对比涨幅不要一上来就堆特征。这一段也顺带解释了为什么数据读取后要立刻检查列的顺序和目标列位置——源码默认目标列在最后一列如果自己的数据把目标列放在中间记得用 pandas 的 reindex 或按列名切片挪到末尾。所有数据处理做完之后数据端的三件事——读取、归一化、切分——就算齐了下一章进源码包内部。3. 源码包内部结构main.py、models.py、utils.py 各管哪一段3.1 文件地图谁是入口、谁是框架、谁是工具人解压 zip 后真正要动的文件一共五个。先看职责表文件角色使用场景main.py程序入口训练和预测流程编排每次调参都改它models.pyLSTM 网络结构定义改模型架构时才动utils.py数据读取、归一化、指标计算工具换数据格式时动data.xlsx原始数据文件换成你自己的数据model_LSTMMain_weights预训练权重跳过训练直接预测时用main.py 只是调度者不是模型定义的地方。第一次看源码的人容易到处找网络结构翻几圈才发现 LSTM 定义在 models.py 里。utils.py 把数据读取、窗口构造、归一化、指标计算全部拆出来这个分层思路是对的改数据处理逻辑时不用碰模型代码。3.2 模型定义LSTM 层加全连接层的单输出结构models.py 的结构是时间序列预测最标准的形态LSTM 层提取时序特征再接全连接层输出预测值。核心代码import torch import torch.nn as nn class LSTMModel(nn.Module): def __init__(self, input_size, hidden_size, num_layers, output_size): super(LSTMModel, 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, (h_n, c_n) self.lstm(x) # 取最后一个时间步的隐层输出做全连接 out self.fc(out[:, -1, :]) return out参数逐个说。input_size 是特征数量单变量是 1多变量是特征列总数。hidden_size 是 LSTM 隐藏单元数决定模型记忆容量小数据 64 起步数据量大的可以设 128 或 256过大了反而容易过拟合。num_layers 是堆叠层数2 表示两层 LSTM 堆叠能学到更复杂的时序依赖但训练更慢。batch_firstTrue 省了一个 permute 步骤默认 PyTorch LSTM 期望输入形状是 (seq_len, batch, feature)batch_firstTrue 后输入变成 (batch, seq_len, feature)和我们直觉构造的样本形状完全一致。如果你在别处看到 LSTM 代码 forward 里写 x.permute(1,0,2)那就是没用 batch_first 的写法两件事是等价的。forward 里 out[:, -1, :] 是“单输出”的核心操作。不管输入是 7 个时间步还是 14 个时间步模型只看最后一个时间步的隐层状态然后由全连接层压成 1 个预测值。要改成多步预测一条路是把 fc 输出维度从 1 改成 n_steps一口气输出未来 n 步另一条路是保留单输出但迭代预测每次预测一个值再拼回去喂给模型。前者多一个超参后者会累积误差后面进阶章细讲。3.3 权重文件与跳过训练直接预测包内放了训练好的权重主要用于省掉训练时间。加载权重的标准写法model LSTMModel(input_sizefeature_num, hidden_size128, num_layers2, output_size1) checkpoint torch.load(model_LSTMMain_weights.pth, map_locationcpu) model.load_state_dict(checkpoint) model.eval()这里有几个向坑很多的细节。第一map_locationcpu 必须写否则在没有 GPU 的机器上加载别人 GPU 训练的权重会直接报错 Attempting to deserialize object on a CUDA device。第二load_state_dict 之前必须保证模型的 hidden_size、num_layers 与训练时一致否则键名对不上运行报 Missing key 或 Unexpected key。第三加载完成后 model.eval() 是必选项模型里如果带 Dropout 或 BatchNorm训练模式和推理模式的执行路径完全不同忘记切模式会让预测结果带随机性。3.4 main.py 的整体流程从数据到指标的编排main.py 的流程可以归纳为六个顺序步骤读数据、构建窗口、切三集、定义模型、开始训练、评估输出。骨架如下def main(): data load_data(data.xlsx) # 从 utils 读取 x, y build_sequences(data, seq_len) # 构造窗口 train_loader, val_loader, test_loader split_dataset(x, y) # 三集 model LSTMModel(input_size, hidden_size, num_layers, 1) train(model, train_loader, val_loader, epochs) metrics evaluate(model, test_loader) print(metrics)每一行都对应一个实际函数。这种编排方式的好处是把研究流程和工程细节剥离开模型定义不动调节点时只需要改 main.py 顶部的几个参数。这也解释了为什么权重文件名叫 model_LSTMMain_weights——它是对应 main.py 里创建的那个模型实例保存的。参数集中在 main.py 顶部是这份源码特别方便改的地方。打开 main.py 大概率看到一堆赋值语句比如 epochs、batch_size、learning_rate、seq_len、hidden_size。batch_size 在时间序列场景里常用 32 或 64epochs 在 50 到 200 之间学习率 0.001 是 Adam 的推荐值。这些参数改完整个流程重跑一遍就行不用动其它文件。这也是“无脑”二字的来源。4. 训练与评估损失函数、优化器和 MAE/MSE/R2/MAPE 的实现4.1 训练循环Adam、MSE 损失与权重保存main.py 里的训练部分是标准的 PyTorch 循环没有多余封装。关键代码import torch.optim as optim model LSTMModel(input_sizefeature_num, hidden_sizehidden_size, num_layersnum_layers, output_size1) criterion nn.MSELoss() optimizer optim.Adam(model.parameters(), lr0.001) epochs 100 for epoch in range(epochs): model.train() for x_batch, y_batch in train_loader: optimizer.zero_grad() y_pred model(x_batch) loss criterion(y_pred, y_batch.view(-1, 1)) loss.backward() optimizer.step()损失函数选 MSE 而不是 MAE 的原因很实际MSE 对预测值和真实值的偏差做平方梯度大小正比于误差训练收敛更快MAE 梯度恒定误差很小时更新步长不缩小不容易收敛。代价是 MSE 对离群点敏感数据里有一个极端值loss 就大得离谱。时间序列数据异常值多我会先对数据做截断或平滑再训练。optimizer.zero_grad() 必须写在每个 batch 最前面。PyTorch 默认行为是梯度累积不清空上一轮梯度的话 loss 不会收敛模型参数在错误方向上乱跑。y_batch.view(-1, 1) 是为了把形状统一成 (batch, 1)和模型输出的 (batch, 1) 对齐MSE 计算才不会有维度错误或隐式广播。训练完成之后权重保存在流程末尾torch.save(model.state_dict(), model_LSTMMain_weights.pth)只存 state_dict不存整个模型对象。这样做的优点是文件体积小、不绑定模型类的定义路径换电脑加载也不受影响缺点是你必须保证加载时的模型定义和保存时的完全一致键名、参数一个都不能差。自己训练完立刻加载问题不大拿别人给的权重就必须知道当时的 hidden_size 和 num_layers。4.2 四个评估指标公式、代码和业务含义评估部分封装在 utils.py 里最终在 main.py 的输出段打印。核心计算展开如下import numpy as np mae np.mean(np.abs(y_pred - y_test)) # 平均绝对误差 mse np.mean((y_pred - y_test) ** 2) # 均方误差 rmse np.sqrt(mse) # 均方根误差 mape np.mean(np.abs((y_test - y_pred) / y_test)) * 100 # 百分比误差 ss_res np.sum((y_test - y_pred) ** 2) ss_tot np.sum((y_test - np.mean(y_test)) ** 2) r2 1 - ss_res / ss_tot # R2 决定系数这四个指标各有各的“脾气”。MAE 最直观单位跟原始数据一致“平均错多少个单位”适合业务汇报。MSE 把误差平方放大对极大误差极其敏感数据里有几个异常点MSE 会大得离谱——这不是模型坏了是平方运算的数学特性。RMSE 把 MSE 拉回原单位但它放大极端误差的特性还在。MAPE 是百分比误差适合做跨数据集横向对比。它有一个前提条件真实值不能接近 0否则除法分母趋近 0MAPE 直接冲天。如果自己的数据里存在 0 值这个指标打印出来会是 inf不代表模型失败只是指标本身不适合这份数据改用 MAE 做评价即可。R2 的含义和另外三个完全不同。它衡量的是模型解释了目标波动的比例正常范围 0 到 1越接近 1 拟合越好。R2 为负说明你的模型比“直接用真实值均值当预测值”还差这种情况在时间序列里多半意味着数据切分泄漏、特征和目标方向弄反、或者模型结构有问题。看到负 R2 时不要调参先回去检查数据。4.3 反归一化预测结束最容易漏的一步训练时数据被压到 [0,1]模型输出的预测值也在 [0,1]。要把预测值还原成原始量级就必须做反归一化y_pred_original scaler.inverse_transform(y_pred) y_test_original scaler.inverse_transform(y_test)inverse_transform 的输入形状必须和 fit 时的特征数量一致。如果 fit 时传的是整个 data多列现在只传一列预测值会报 shape mismatch。源码里对这个问题的处理方式是单独定义目标列的 scalerscaler_y MinMaxScaler() y_scaled scaler_y.fit_transform(df[[target_col]].values)这样反归一化只用一列没有任何维度问题。如果直接调用整个 data 的 scaler就必须构造一个和训练时相同列数的空矩阵把预测值塞进目标列位置再 inverse_transform处理起来麻烦得多。4.4 指标怎么结合业务场景选四个指标不是用来凑数的。业务上我一般这么选给老板汇报用 MAE因为带原始单位说“平均偏差 120 件”比“R20.87”更容易听懂。调参对比模型时看 RMSE它对大误差更敏感能帮你在多个候选模型里挑出极端情况都拟合得好的那个。跨区域、跨品类对比时用 MAPE因为百分比消除了不同数据量级的影响。R2 则用来判断模型是否整体失效。四个指标一起看才有完整画面MAE 低但 R2 也低说明模型对大趋势拟合不好MSE 高但 MAE 低暗示数据里有少数极端点。5. 实战复现与避坑环境准备、命令顺序和五个高频坑5.1 环境准备依赖安装和 PyTorch 选择上手这个源码包依赖很少核心就五个包pip install torch numpy pandas scikit-learn openpyxlopenpyxl 是 pandas 读 .xlsx 的底层依赖不装的话 pd.read_excel 会直接 ImportError。如果数据只有 CSV可以去掉 openpyxl但保留也不碍事体积很小。PyTorch 的安装要分情况纯 CPU 机器直接 pip install torch 就能装有 NVIDIA 显卡想用 GPU去 PyTorch 官网按 CUDA 版本选安装命令不要自己猜 wheel 名。这份源码的规模用 CPU 完全跑得动差的只是训练时间不用为它大动干戈配 GPU 环境。装好后第一件事是验证 PyTorch 是不是真的能导入python -c import torch; print(torch.__version__)这一行能输出版本号环境基本就绪。经常有人跳过这一步直接跑 main.py在 import torch 那行翻车然后花半天排查是哪个包缺了其实环境从头就没装好。5.2 跑通主流程六步操作整体操作顺序如下。第一步把 data.xlsx 换成你自己的 Excel。列名换成英文目标列放最后一列清理掉空值和明显异常点。第二步打开 main.py把数据路径、sheet 名改成实际值同时确认 seq_len、hidden_size 这些参数。第三步决定单变量还是多变量单变量在读取段只保留目标列多变量保留全部特征列。第四步运行 python main.py。第五步观察训练日志loss 应该持续下降如果看到 loss 变成 NaN 就停住回到避坑清单。第六步训练结束后看测试集上的 MAE、MSE、R2、MAPE 输出同时确认反归一化后的预测曲线有没有明显错位。这六步做完按说已经能用上这份源码了。但时间序列预测的坑密集在数据环节而不是模型环节下面五条是最常见的报错和翻车记录。5.3 避坑5 个高频踩坑记录坑一训练 loss 输出 NaN现象训练头几轮 loss 正常几十轮后突然变成 NaN后续全部 NaN。 原因最常见的是数据里存在 NaN 或者 infMinMaxScaler 会把 NaN 原样保留LSTM 前向传播算出的梯度直接无效。次常见的是学习率太大Adam 一次更新就跨过最优点数值发散。 解决读取 DataFrame 后立刻执行 df df.dropna() 或者 df df.fillna(methodffill)同时把学习率从 0.001 降到 0.0005。绝大多数情况这两步做完 NaN 就没了。如果要兜底在训练循环里加一个 loss 检查if torch.isnan(loss): print(NaN at epoch, epoch); break方便快速定位。坑二加载权重时报 Missing key(s) in state_dict现象torch.load 成功load_state_dict 却报缺失键或者键名对不上。 原因模型定义参数和保存权重时的参数不一致。别人用 hidden_size128 训练保存你用默认 hidden_size64 定义模型键名自然对不上。 解决权重文件名是 model_LSTMMain_weights对应 main.py 里的默认模型。加载前按原结构重新定义模型如果你改过 models.py旧权重大概率失效老老实实重新训练。实在不知道原结构把 hidden_size、num_layers 两个参数跑一遍组合加载成功为止。坑三预测阶段维度报错 RuntimeError: mat1 and mat2 shapes cannot be multiplied现象eval 阶段输入模型的数据形状不对全连接层矩阵乘直接串台。 原因模型的 LSTM 期望三维输入 (batch, seq_len, feature)实际输入变成了二维或者 batch_size 和 seq_len 交叉错位。最常见的是构造序列样本时把 [seq_len, feature] 当成整体传入少了 batch 维。 解决在进模型前加一行 print(x.shape) 确认维度。少一维用 x x.unsqueeze(0)多一维用 x.squeeze(0)。建议在 main.py 关键位置加 assert x.dim() 3训练和预测都套上报错时一秒定位。坑四预测曲线是平移了一个周期的“滞后曲线”现象预测值和真实值曲线形状几乎一样但整体右移了一个周期R2 不低画图一看就露馅。 原因时间序列模型最经典的偷懒结果。在没有强趋势的序列上模型找到的最优策略是“用最近的一个值预测当前值”因此输出天然滞后一个周期。这不是代码故障是数据信息和建模方式的局限。 解决增大 seq_len 让模型看到更长的历史或者对原始数据做一阶差分把预测目标从“绝对值”改成“增量”训练结束后再反差分还原。差分是消除滞后性的常用手段但如果业务指标周期性很强、滞后可以接受也不一定非要改。坑五eval() 没调用预测结果忽好忽坏现象加载权重后预测多次结果每次都不一样。 原因模型里有 Dropout 或 BatchNorm 层时训练模式和推理模式的执行逻辑不同。Dropout 在 train 模式下随机丢弃神经元BatchNorm 在 train 模式下更新运行统计量两者都会让预测带随机性。 解决预测前强制调用 model.eval()。这份源码标准结构 LSTMLinear 没有 Dropout 影响不大但后续加了 Dropout 或 BatchNorm这个坑必定踩到。同样地要重新训练就切回 model.train()两种模式要成对使用。五个坑排完之后这份源码的复现就稳了。6. 进阶改造从单步改成多步预测的验证方法单输出和单步预测是这份源码的默认行为但很多实际场景需要的是一次给出未来若干步的预测——比如未来 7 天的销量、未来 24 小时的电价。要把这份源码从单步改成多步有三个环节必须处理。第一模型输出维度。单输出模型的最后一层全连接输出是 1多步预测最简单的改法是把 output_size 从 1 改成 n_steps让模型一口气输出未来 n 步。相应的损失函数还是 MSE但 y_batch 的形状得跟着变成 (batch, n_steps)不能再 view 成 (batch, 1)。第二数据集构造逻辑。原来窗口取 data[i:iseq_len] 作为输入y 取 data[iseq_len] 单值改成多步后 y 要取 data[iseq_len:iseq_lenn_steps] 连续 n 个目标值。这一步直接导致训练样本数减少数据量不够时多步预测的效果会很惨。第三评估方式。多步预测的指标一般按步分别计算MAE 按第 1 步、第 2 步、第 n 步单独打印这样你能看清误差在哪一步开始明显变大。我的习惯是改造前先加载官方权重跑一遍单步预测记录指标作为 baseline然后从 n_steps2 开始逐步加大步数观察误差随步数的衰减曲线。多步预测最常见的现象是“第一步指标好、远期指标崩”这不是模型坏了而是预测误差在迭代过程中被一步步传导放大越远的步数据含量越低。那年我把多步改成 5 步发现第 7 步的 MAPE 比第 1 步涨了三倍排查了半天模型代码没有任何问题最后意识到是输入序列的周期长度和预测步数不匹配——用 7 天周期去预测 5 天后的值本身就不太稳。从那以后我每次改动步数都会强制先记录 single-step baseline再逐级加大 n_steps对比每步的指标衰减曲线再决定用哪个步数配置上线。希望这一套拆解思路帮到你。本文还有配套的精品资源点击获取