简介一份基于PyTorch的LSTM时间序列预测完整代码包面向Python数据科学初学者及有序列预测需求的开发者可应用于股票行情、气温电力等连续变量的趋势建模重点解决历史数据特征提取与未来数值预测问题。压缩包共4个文件包含2个Python脚本和2个CSV数据文件整体仅100KB脚本覆盖LSTM模型搭建、训练循环与结果输出配套数据分析模块用于缺失值查看、标准化和特征梳理两个CSV文件则提供丹麦2023年的历史特征和对应真实标签形成可直接运行的监督学习小项目。目前已有61人学习下载。整套代码清晰呈现时间序列预测的关键链路从数据读取、训练集/测试集划分到多步预测、反归一化对比再到误差评估可帮助初学者避开常见的数据泄漏与维度不匹配问题模型结构简洁稍作调整即可迁移到其他单变量序列场景适合课程设计、入门实战和算法复现。1. LSTM预测代码完整数据先想清楚要预测的是什么第一次把LSTM预测跑通的人十有八九会盯着那张滞后一个时间步的预测图发呆曲线确实贴得很近但看起来就是把昨天的值搬到今天。这不是模型没学好而是数据喂法错了。这份LSTM预测代码完整数据资源就是把“时间序列转监督学习样本、LSTM训练、滚动预测、反归一化”整条链路串起来的Python模板里面还带了一组真实采集的连续序列解压后能直接跑出预测结果。它的价值不在层数多深而是把预处理、窗口切分、归一化、逆变换这些容易忽略的环节全部补齐了。适合正在做销量预测、设备寿命预测、视频流量预测这类连续值预测的从业者也适合看过一堆教程但没跑通完整流程、想找一个能复现样例代码的新手。2. 数据准备从原始序列到监督学习样本2.1 滑动窗口构造样本时间步长不是拍脑袋定的LSTM的输入不是一条一维序列而是一批“过去一段、未来一段”的样本对。这段过去就叫look_back未来长度叫forecast_horizon。很多入门代码把look_back写死成10但资源里的数据是日粒度的销量序列我用7作为起步值因为一周七天本身就是天然周期如果是月度数据12或24更合理完全没有周期先验时先用10到20做基线再拿验证集去比。import numpy as np def create_dataset(series, look_back7, forecast_horizon1): X, y [], [] for i in range(len(series) - look_back - forecast_horizon 1): # 取最近 look_back 个点作为输入 X.append(series[i : i look_back]) # 取后面 forecast_horizon 个点作为目标 y.append(series[i look_back : i look_back forecast_horizon]) return np.array(X), np.array(y) # series 是一维 numpy 数组 X, y create_dataset(series, look_back7, forecast_horizon1) print(X shape:, X.shape) # (样本数, 7) print(y shape:, y.shape) # (样本数, 1)这段函数用纯Python实现逻辑不绕从序列头开始逐个滑出长度为look_back的窗口作为特征紧接着的forecast_horizon个点作为标签。输出X是二维数组后边建模时还需要补一个特征维度。参数上的坑在于forecast_horizon。单步预测设为1模型结构最简单多步预测如果设成7相当于一次输出未来7个值后边Dense层的神经元数量也要跟着改。预测步数越长误差越容易累积所以我的习惯是先设成1确认基线能跑通再改成多步。样本数方面窗口越短样本越多但可能丢失长周期依赖窗口越长样本越少训练起来容易欠拟合这组数据量不大7到14是比较安全的区间。2.2 归一化为什么直接用原始值训练会让loss像过山车LSTM内部用tanh和sigmoid激活输出范围有界。原始销量序列如果是几百到几万不做归一化直接喂进去梯度会来回震荡loss忽高忽低训练后期很难收敛。所以训练前一定做归一化常见做法是MinMaxScaler把序列压到0到1区间。from sklearn.preprocessing import MinMaxScaler # train_raw / val_raw / test_raw 都是按时间顺序切好的一维数组 scaler MinMaxScaler(feature_range(0, 1)) train_scaled scaler.fit_transform(train_raw.reshape(-1, 1)) val_scaled scaler.transform(val_raw.reshape(-1, 1)) test_scaled scaler.transform(test_raw.reshape(-1, 1))注意第一行是fit_transform后两行只是transform。fit是计算这一列数据的最小值和最大值transform是套用这套映射。为什么测试集不能fit因为一旦把测试集的极值也加进归一化参数相当于模型已经“看到”了测试集的数据分布这是典型的数据泄漏会让测试指标虚高。这个细节我在第5章还会专门展开。feature_range选[0,1]还是[-1,1]取决于后续是否做差分。差分数据有正有负用[-1,1]更自然原始非负序列用[0,1]足够。如果序列里有极端异常值最大值会被一个离群点拉得很大正常数据全被压到0附近这时要先做分位数截断或者改用RobustScaler别急着换模型。这种数据层面的问题换任何网络结构都救不回来。2.3 数据集划分按时间切开不要随机洗牌分类任务里shuffle是标准操作但时间序列不能随机打乱。昨天的数据不能拿明天的数据去预测所以划分必须严格按时间顺序切。常见比例是70%训练、15%验证、15%测试数据量特别小时验证集可以缩到10%但最好不要少于一个完整业务周期。n len(series) train_end int(n * 0.70) val_end int(n * 0.85) train_raw series[:train_end] val_raw series[train_end:val_end] test_raw series[val_end:]切分顺序和create_dataset的执行顺序也有讲究。我一般先切分再分别归一化最后构造样本。如果先构造样本再切分样本之间会交叉使用窗口数据验证集里就会混进训练段的信息同样属于数据泄漏。如果你的数据量只有几百条可以直接用Keras的TimeseriesGenerator做批量生成省内存。但这份资源里我保留了手动构造方式因为能看到每个样本长什么样排查问题更快。后面模型报错时也更容易定位是shape问题还是内容问题。3. LSTM模型搭建输入输出形状与层数选择3.1 单步预测还是多步预测先把输出层想清楚LSTM建模前要先决定一件事预测未来1个点还是未来N个点。单步预测的输出层是1个神经元多步预测的输出层是N个神经元。很多刚上手的同事会拿着多步预测的目标去套单步模型结构结果Dense层输出永远只有1个值训练目标形状对不上直接报错。如果业务只需要未来一天的销量预测单步就够。如果要做库存补货常常需要未来7天或14天的预测这时有两种做法一是输出层直接输出7个值模型一次给全二是只输出1个值把预测结果滚动作为新输入逐步递推。我建议先用第一种结构简单、训练稳定等把误差吃透了再试第二种。注意多步预测的输出神经元数量必须和create_dataset里的forecast_horizon保持一致改一个就要同步改另一个。3.2 用Keras搭一个可复现的LSTM预测模型完整的模板我习惯先用Keras Sequential搭一个两层LSTM的基线模型。为什么是两层第一层提取短周期局部特征第二层再在时间维度上做组合比单层多一层非线性拟合能力堆到三层以上在普通数据量下收益很有限反而更容易过拟合。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout model Sequential() model.add(LSTM(64, return_sequencesTrue, input_shape(look_back, 1))) model.add(Dropout(0.2)) model.add(LSTM(32, return_sequencesFalse)) model.add(Dropout(0.2)) model.add(Dense(forecast_horizon)) model.compile(optimizeradam, lossmae, metrics[mse]) model.summary()第一层LSTM的return_sequencesTrue表示输出完整的时间步序列给下一层第二层return_sequencesFalse只保留最后一步的输出再接全连接层。units没有公式64和32是数据量在几千条时的常见起步值数据量更大可以到128数据量只有几百就降到32和16否则必然过拟合。input_shape(look_back, 1)里第一个数是窗口长度第二个数是特征维数。单变量预测时特征维度为1如果后续加入星期几、是否节假日这类外部特征这里的1要变成总特征数输入数据也要相应拼接成三维张量。Dropout放在LSTM层后是为了随机丢掉一部分输出防止网络把这组数据的噪声背下来。3.3 早停与模型保存别把训练曲线当KPILSTM训练最容易出现的假象是训练集loss不断下降但验证集loss先降后升这就是过拟合信号。硬撑着跑完几百个epoch会把模型最后一次权重保存下来反而是最差的状态。所以我会用两个回调EarlyStopping在验证loss连续多轮不降时自动停ModelCheckpoint只保存验证loss最低的权重。from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint early EarlyStopping(monitorval_loss, patience20, restore_best_weightsTrue) ckpt ModelCheckpoint(best_lstm.h5, monitorval_loss, save_best_onlyTrue) history model.fit( X_train, y_train, epochs200, batch_size32, validation_data(X_val, y_val), callbacks[early, ckpt], verbose1 )patience20的意思是连续20个epoch没见val_loss更低就停不是一波动就停。restore_best_weightsTrue保证停的时候把权重回滚到最优状态避免停在一个随机点上。batch_size我一般先设32显存小就用16数据噪声大可以用64压一压。训练日志里val_loss才是判断依据训练loss低不代表模型真能预测。4. 训练与预测从静态预测到滚动预测4.1 损失函数选MAE还是MSE与业务误差承受方式有关回归预测的损失函数最常见的就是MAE和MSE。MSE因为平方项的存在对偏离大的样本惩罚非常重训练时会把权重往几个异常点上靠MAE对所有点的惩罚一样离群值的影响相对小。如果数据里偶尔有促销冲量造成的尖峰用MSE会把注意力全吸到尖峰上普通时段的误差反而被忽略了。所以工业场景里我更常用MAE指标直接对应平均绝对误差给业务解释成本低。# 调整损失函数时只需改 compile 一行 model.compile(optimizeradam, lossmae, metrics[mae, mse])如果你对误差方向也有偏好比如库存安全量更怕短期缺货可以后期定义自定义损失。但新手阶段先别碰这个资源模板里默认用MAE稳定性和可解释性兼顾把基线跑通再说。4.2 学习率与优化器Adam不是默认万能解Keras默认的adam优化器学习率是0.001大部分数据直接跑都能收敛。如果loss震荡很厉害可以把学习率降到0.0005或0.0001。如果已经收敛得很稳想再拔高一点精度可以换成带动量的SGD让它缓慢震荡出更小的loss。这个进阶操作不一定每次都有提升但值得一试。from tensorflow.keras.optimizers import Adam, SGD # 方案1降低学习率让训练更稳 model.compile(optimizerAdam(learning_rate0.0005), lossmae) # 方案2用带动量的SGD做精调 model.compile(optimizerSGD(learning_rate0.01, momentum0.9), lossmae)学习率是最典型的“玄学”参数没有公式能算出最优值。我自己的习惯是先从0.001开始看前10个epoch的val_loss完全不降就降一个数量级后期震荡就再减半。务必记住学习率过大的直接症状就是loss变成NaN后面第5章会讲这个报错。4.3 滚动预测把模型用起来的关键一步模型训练好之后不能在测试集上一次性predict完成就认为任务结束。一次性predict只适合单步预测场景假设是每一步都有真实观测值。但实际业务往往要预测未来若干天中间没有真值必须用滚动预测把模型自己的预测值当新数据喂回去。def forecast_rolling(model, last_window, steps, scaler): results [] current last_window.copy() # shape: (look_back, 1) for _ in range(steps): x_input current.reshape(1, look_back, 1) pred model.predict(x_input, verbose0)[0] # shape: (forecast_horizon,) results.append(pred[0]) # 将新预测值拼到窗口末尾丢掉窗口最前面一个点 next_window np.append(current[1:], pred.reshape(1, 1), axis0) current next_window return scaler.inverse_transform(np.array(results).reshape(-1, 1))这段代码的核心在循环里每一步用当前窗口预测一个点然后把预测点作为历史值拼回窗口同时丢掉最旧的一个点窗口像传送带一样往前滚。这样就能得到未来steps个点的预测序列。最后的inverse_transform把结果还原回原始量纲否则拿到的还是0到1的归一化值没法跟业务对比。滚动预测的误差是累积的。步数越多误差越滚越大。所以做设备寿命预测时通常只滚动预测几步或者干脆用多输出模型一次预测未来N个点。代码里每循环一次就调用model.predictCPU裸跑会慢真上生产时可以把batch_size设成输入大小或者用GPU推理。5. 避坑/常见问题五个高频报错与数据泄漏5.1 预测曲线比真实值滞后一个时间步现象验证集和测试集上的预测曲线贴着真实值但整体向右平移了一位相关系数很高业务上却毫无意义。原因模型学到的是“t1的值约等于t的值”本质是数据切分时引入了泄漏或者窗口太短模型没有足够历史信息做推断。最常见的是测试集的构造方式里混进了训练集末尾的真值导致模型在测试时“抄作业”。解决回到create_dataset确认X和y的切分索引没有重叠把look_back从7调到14或30重新训练再用滚动预测方式对比一次如果滚动预测误差明显大于一步预测说明短期依赖被模型背住了不是真正的预测能力。5.2 验证集loss很漂亮测试集却一塌糊涂现象训练和验证阶段指标都好换到测试集误差突然放大几倍。原因归一化时对全量数据做了fit_transform测试集的极值提前参与了训练样本的scale计算让验证阶段看着正常测试阶段数据分布一偏就崩。这也是我在2.2里反复强调的原因。解决严格按“先切分、再分别fit/transform”的流程走。训练集用fit_transform验证集和测试集只用transform。检查资源里的代码顺序一旦调反立刻修回来。5.3 报错Input 0 of layer lstm is incompatible with the layer现象fit时报维度不匹配信息显示expect 3 dimensions, but got array with shape (n, time_steps)。原因LSTM期望输入是三维张量也就是(样本数, 时间步数, 特征数)。create_dataset返回的X是二维的没有补特征维度。解决训练前加一行reshapeX_train X_train.reshape((-1, look_back, 1)) X_val X_val.reshape((-1, look_back, 1)) X_test X_test.reshape((-1, look_back, 1))如果加入外部特征最后的1要换成总特征数。这个错误本身不致命但很消耗时间尤其当样本是二维数组时新手往往会卡在这里。5.4 loss变成NaN或者直接爆炸现象训练到某个epochLoss变成NaN之后全部是NaN。原因学习率过大导致梯度爆炸或者数据里本身就带NaN/Inf。MinMaxScaler遇到Inf会计算出错归一化结果也会跟着坏掉。解决先用np.isnan(series).any()和np.isinf(series).any()清理数据再调低学习率到0.0001。两项都不行就把MSE换成MAEMAE对极值的惩罚小一些训练稳定性更好。典型情况下这一招在业务数据抖动大的时候特别管用。5.5 换一组数据后预测结果变成水平线现象同一套LSTM模板前一组数据效果满意换到另一组波动更大的数据预测曲线几乎就是一条平线压在均值附近。原因数据本身非平稳趋势和季节波动太剧烈归一化后被压缩到很小的区间模型学到的是“预测均值最安全”。另一种情况是测试集数据跟训练集分布差异过大模型直接退化。解决先对原始序列做一阶差分把趋势去掉再把差分结果归一化训练完成后预测值累加回原值。diff_series np.diff(series, n1) # 用 diff_series 替代 series 训练 # 预测得到 diff_pred 后还原原始值 pred_original np.cumsum(diff_pred) series[0]这也是我在第2章强调“数据没洗干净模型天花板很低”的原因。遇到折腾很久效果都不行的情况先怀疑数据再怀疑特征最后才怀疑模型结构。LSTM结构本身没有那么脆弱脆弱的是喂进去的数据组织形式。6. 验证预测效果把误差逐点拆开看模型能跑起来只是第一步真正能在业务里落地要回答“预测到底准不准”。只看一张真实值和预测值叠在一起的折线图远远不够因为线条叠得近你看不出哪个时间段在拖后腿。我习惯训练完之后做三件事第一把预测结果反归一化到原始量纲第二按周或按月分组计算MAE、RMSE、MAPE第三把误差最大的样本逐条拉出来看确认是数据问题还是模型问题。以MAPE为例不同业务量级的误差可比性比MAE好得多。销量在10和10000的两个品类MAE数值没法直接比MAPE却可以。计算分组误差时注意别让分母变成0销量为0时MAPE会变成无穷大这种情况直接跳过或者加一个平滑项。import numpy as np def group_metrics(y_true, y_pred, groups): mae_per_group {} mape_per_group {} for g in np.unique(groups): mask groups g true_g y_true[mask] pred_g y_pred[mask] mae np.mean(np.abs(true_g - pred_g)) mape np.mean(np.abs((true_g - pred_g) / (true_g 1e-6))) mae_per_group[g] mae mape_per_group[g] mape return mae_per_group, mape_per_group这段代码返回每个分组比如周一、周二或者每小时的MAE与MAPE。如果发现某一个时段误差比其他时段高出一倍说明这个时段的模式没被模型学到下一步就该针对这个时段补特征或者单独建一个小模型而不是继续堆LSTM层数。误差分析到这里才算是闭环。从那以后我每次换数据集都会强制自己走一遍先差分、再归一化、再滚动预测、最后分组误差检查的流程不再因为训练loss下降就急着说模型好用。这套流程也原样放到了资源代码里解压后直接跑就能看到完整输出。希望帮到你。本文还有配套的精品资源点击获取