简介面向时间序列预测课程设计与期末大作业场景这份基于LSTM模型的Python实现压缩包提供了可复现的完整方案。代码围绕股票收盘价预测展开包含核心训练脚本、源数据、Markdown分析报告和模型结构讲解图既能支撑作业演示也有助于理解LSTM与RNN的区别及计算过程。资源共30个文件涵盖.py源码、.md文档、.xlsx/.csv数据文件、.png结构图以及开发环境配置等整体仅1.83MB轻量而清晰。目前已有1792人学习浏览适合作为期末项目参考或快速上手的实践模板。使用者可将源数据替换为自有序列根据分析报告调整模型参数借助可视化图片梳理解题思路在较短时间内完成高质量的时间序列预测任务。1. 时间序列预测LSTM模型python代码实现这一套zip里装的其实是一整条工程链路时间序列预测LSTM模型python代码实现这种zip包在论文复现、课程设计和毕业设计圈子里几乎是标配装备95分以上标注的意思是评分口径不只盯模型精度还包含代码结构、注释完整度、可视化和报告可复现性也就是说你拿到的是一个能直接交付的工程而不只是一段训练脚本。这套代码解决的是用深度学习做单变量、多变量时序预测的标准流程——从CSV原始数据出发经过滑窗切分、归一化、LSTM训练、反归一化最终输出预测曲线和评价指标中间不需要人工干预。适合刚接触LSTM的论文复现者、做毕设需要现成基线的学生以及想把预测能力迁移到销量、流量、能源负荷等场景的Python工程师。但拿到zip只是第一步真正耗时的是让代码在你自己的环境和数据上跑通并且让每个数字都禁得起追问。2. 跑通LSTM预测前的环境与数据准备版本兼容、数据形态、滑窗与归一化的顺序2.1 版本选择Python、TensorFlow与NumPy的兼容矩阵LSTM预测这个方向的生态和CV不太一样TensorFlow版本带来的坑是最常见的翻车点。先说结论如果你用TensorFlow 2.10及以上Python建议选3.9到3.11TensorFlow 2.15搭配Python 3.11是常见的稳定组合。Keras现在作为TensorFlow的官方高层API存在写LSTM时直接from tensorflow.keras导入即可不需要单独安装Keras。NumPy版本最容易出问题TensorFlow编译时对NumPy接口有兼容窗口所以一般依赖统一交给pip处理你自己额外装的高版本NumPy反而会把它顶坏。无论你用VSCode还是PyCharm配置python环境的思路都一样先建虚拟环境再pip安装依赖。VSCode下要手动选解释器PyCharm则去Project Interpreter页面切换新手在这里卡住的概率比写模型代码还高。下面这段是我每次新环境都会先跑的版本检查脚本python --version pip show tensorflow numpy pandas scikit-learn matplotlib输出里重点看tensorflow和numpy的版本号。如果numpy被装成2.1.x而tensorflow是2.10很多导入阶段会直接报 np.float was removed 之类的错误这个问题在NumPy 1.24之后就非常常见也是从别人环境复制代码过来直接跑炸的十大原因之一。参数说明python --version用于确认解释器版本pip show用于列出关键包的版本信息返回Not found说明该包没装。装包时我习惯用pip install tensorflow pandas numpy scikit-learn matplotlib一次性装齐避免一个个装导致版本互相打架。如果你不打算用TensorFlowPyTorch的LSTM同样能做但从代码量和熟练度看Keras的Sequential写法对课程作业友好太多——十几行就能拼出一个能跑的结构PyTorch还需要自己写forward循环和初始化hidden state对只是想快速拿到一个95分作业的人来说有些不划算。2.2 数据集形态CSV里常见的三种列结构与滑窗的必要性时间序列预测的数据集没有统一格式但九成以上的场景是CSV。列结构通常三种第一种是两列date, value典型单变量第二种是date加多特征列比如温度、湿度、销量一起典型多变量第三种是干脆没有日期列只有一排数值常见于竞赛给的脱敏数据。拿到数据第一件事不是建模而是先判断它属于哪一种这决定了后续滑窗写法和特征处理方向。不管zip里的原始数据长什么样先把它读进DataFrame看一眼这比直接跑训练脚本能省出一下午。下面这个模板兼容以上三种情况import pandas as pd df pd.read_csv(data.csv, encodingutf-8) print(df.head(), df.dtypes, df.isnull().sum()) # 如果有日期列做一次排序并转成datetime索引 if date in df.columns: df[date] pd.to_datetime(df[date]) df df.sort_values(date).reset_index(dropTrue)逻辑说明head查看前五行了解列结构dtypes看每列类型isnull().sum()统计缺失量这三步能解决90%的代码看起来没问题但模型训不动的疑云——因为问题常常就出在数据里有NaN或者日期乱序。排序那行尤其关键时间序列训练的前提是序列顺序真实反映时间顺序乱序数据喂给LSTM等于让模型学一个错误的时间逻辑。参数说明encodingutf-8只在遇到中文乱码时改成gbk或gb18030to_datetime能解析大多数常见日期格式遇到 2024/01/05 08:30:00 这类带时间戳的字符串也能自动处理。滑窗是什么一句话把连续W个时间步作为输入第W1步作为输出像推拉窗一样一格一格往后滑。LSTM学的不是整条序列而是最近W步到下一步的映射关系这个W就是后面调参时最核心的超参数。2.3 滑窗构造与归一化先按时间切分再fit缩放器绝大多数第一次跑通相关代码的人用的都是同一个错误姿势对整个数据集做归一化然后再切训练测试集。这在数学上是数据泄漏因为测试集的统计信息提前进入了训练流程表现出来的结果是验证集指标虚高一换真实数据就垮。正确顺序是先按时间顺序切分只在训练集上fit缩放器再用同一个缩放器transform测试集。从df里把要预测的目标列取成numpy数组后续所有操作都基于这个数组from sklearn.preprocessing import MinMaxScaler import numpy as np data df[value].values.astype(float) # 先切分不shuffle保持时间顺序 split_idx int(len(data) * 0.8) train_raw, test_raw data[:split_idx], data[split_idx:] # 只在训练集上fit scaler MinMaxScaler(feature_range(0, 1)) train_scaled scaler.fit_transform(train_raw.reshape(-1, 1)) test_scaled scaler.transform(test_raw.reshape(-1, 1))逻辑说明reshape(-1, 1)把一维序列变成N行1列的二维数组这是sklearn接口的硬性要求。fit_transform在训练集上求min和max并完成缩放transform在测试集上复用训练集的参数保证测试阶段看不到未来信息。这是整个时间序列预测代码里最不能省的一步也是答辩时最常被追问的考点。参数说明0.8的切分比例是常见默认值数据量少于500条时我一般用0.9让训练集尽量厚一些feature_range(0, 1)对应LSTM内部tanh和sigmoid的激活范围如果输出层是Dense线性激活归一化范围其实没有严格规定但0到1最容易让模型稳定收敛。注意如果你的目标列不叫value把这一行改成实际的列名。滑窗函数单独拎出来写因为它直接决定X的形状def create_sequences(scaled_data, window_size): X, y [], [] for i in range(len(scaled_data) - window_size): X.append(scaled_data[i:i window_size, 0]) y.append(scaled_data[i window_size, 0]) return np.array(X), np.array(y) window_size 10 X_train, y_train create_sequences(train_scaled, window_size) X_test, y_test create_sequences(test_scaled, window_size) print(X_train.shape, y_train.shape) # 例如 (NTrain-10, 10), (NTrain-10,)逻辑说明循环从0走到len(window)之前每次取连续window_size个点作为输入紧跟其后的一个点作为标签这正是用过去10步预测下一步的语义。此时X_train是二维形状(N, window_size)LSTM真正需要的是三维(N, window_size, features)下一步马上处理。参数说明window_size取10只是起步值。对日销数据取7能捕捉周周期对小时级数据取24或48更合理。窗口太小模型看不到周期窗口太大训练样本数量变少这个平衡要靠实验来定。提示为什么必须归一化LSTM内部有tanh和sigmoid输入范围过大或过小都会让梯度更新失效归一化到0~1后每个特征的量纲被拉齐训练才稳定。反归一化是在预测完成之后用scaler.inverse_transform把结果还原成真实数值别把还在0~1尺度的预测值直接拿去和原始数据对比那样图上永远是错位的会误以为模型完全没学到东西。3. 核心代码拆解LSTM模型从滑窗到训练的最小可运行集3.1 形状转换把二维窗口变成LSTM要的三维张量数据准备的最后一步是把上一步的二维(N, window_size)变成三维(N, window_size, n_features)。很多人在这里卡住报错五花八门但本质都是形状对不上。LSTM的输入形状约定是(batch, timesteps, features)batch是一次喂多少条窗口样本timesteps是窗口长度features是每条样本的特征通道数。n_features 1 # 单变量预测时只有value一列 X_train X_train.reshape((X_train.shape[0], X_train.shape[1], n_features)) X_test X_test.reshape((X_test.shape[0], X_test.shape[1], n_features)) print(训练集形状:, X_train.shape) # (N, 10, 1) print(标签形状:, y_train.shape) # (N,)逻辑说明reshape把每个窗口从一维行向量变成二维(time, features)样式的通道数据。X_train.shape[0]是窗口数量X_train.shape[1]是每个窗口内的时间步数最后的1表示每步只有一个特征值。多变量场景把n_features改成实际列数同时构造滑窗时要按行堆叠多列数据进窗口而不是只取一列。参数说明单变量时n_features1如果是多变量三维表滑窗函数的append行要改成X.append(scaled_data[i:iwindow_size, :])去掉那个写死的第0列索引这一步漏改会造成多变量训练时特征维度丢失。如果是从网上下载的代码包有时会直接给你一个已经切好的npz或npy文件那就跳过CSV处理但我还是建议无论原始格式是什么都先打印一遍shape再进模型形态错误越早暴露越省时间。3.2 模型搭建LSTM层数、神经元数与Dropout的搭配原则模型结构的选择在时间序列预测里经验性很强没有绝对最优但有一个稳定起点两层LSTM加一层Dense回归头这是这个方向最常见的落地结构。第一层LSTM把时间信息继续向下传递需要return_sequencesTrue让每个时间步的输出都保留给下一层第二层只输出最后一个时间步的信息默认return_sequencesFalse。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.optimizers import Adam model Sequential([ LSTM(64, return_sequencesTrue, input_shape(window_size, n_features)), Dropout(0.2), LSTM(32, return_sequencesFalse), Dropout(0.2), Dense(1) ]) model.compile(optimizerAdam(learning_rate0.001), lossmse, metrics[mae]) print(model.summary())逻辑说明第一层LSTM的64是记忆单元个数窗口里每一步都会产生一个64维的输出向量Dropout(0.2)随机把20%的神经元输出置零防止过拟合第二层LSTM把时间信息压缩成最后的32维状态向量Dense(1)输出最终预测值。lossmse是回归任务的标准选择真实值和预测值差的平方被平均值越小越好。参数说明units从32到128都有数据量大或序列复杂时用64起步再往上加learning_rate0.001是Adam的常见默认值跑崩了改0.0001比改层数见效更快metrics[mae]只是作为训练过程的观察指标最终评分还是看第4章用sklearn算的那组指标。注意input_shape只出现在第一层后面的层会自动推断形状。如果你复制网上两层LSTM代码时第一层漏写return_sequencesTrue运行时会抛 Input 0 of layer lstm_2 is incompatible 一类的错误翻译过来就是上一层给它喂的形状不对先检查这里。3.3 训练闭环Epochs、BatchSize与EarlyStopping的止损机制训练写得好不好直接决定能不能拿到那个95分。最常见的翻车姿势是固定epochs100硬跑要么欠拟合要么过拟合。我一般会给EarlyStopping加一个ModelCheckpoint让它在验证集指标不再变好时提前停并保存最优权重而不是保存最后一步的权重。from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint early_stop EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue ) checkpoint ModelCheckpoint( best_model.keras, monitorval_loss, save_best_onlyTrue ) history model.fit( X_train, y_train, validation_split0.15, epochs100, batch_size32, callbacks[early_stop, checkpoint], verbose1 )逻辑说明validation_split0.15表示在滑窗后的训练样本里再切15%作为验证集用来观察模型在没有见过的训练数据上的表现。注意这里的验证样本在时间上仍然位于训练集末尾没有引入未来信息不构成泄漏。fit返回的history里记录了每个epoch的loss和val_loss这是后面画训练曲线的数据来源。patience10表示val_loss连续10轮没有下降就提前停止restore_best_weightsTrue保证停的时候恢复历史最优权重而不是停在那个可能已经过拟合的权重上。参数说明epochs设100只是个上限实际通常二三十轮就稳定batch_size32是时间与精度的平衡点数据量小时用16数据量大用64甚至128verbose1在训练时每轮打印一次进度课程答辩时这个输出能直观展示训练过程。训练结束后做一次预测闭环包括反归一化和形状对齐pred_scaled model.predict(X_test) pred scaler.inverse_transform(pred_scaled.reshape(-1, 1)) y_test_original scaler.inverse_transform(y_test.reshape(-1, 1))逻辑说明predict的输出shape是(N, 1)反归一化前先reshape(-1, 1)因为scaler在fit时接受的输入是二维列向量。y_test同理转换把原始量纲的真实值拿出来才能和pred画在同一张图上对比。4. 评估与可视化95分答卷的指标口径与两张必出图4.1 指标选择MAE、RMSE与R²的计算口径评分人看的不只是曲线好不好看而是指标能不能复现。时间序列预测最常用的三个指标是MAE、RMSE、R²。其中R²在课程作业里几乎是必算项因为它能直观说明预测对真实值方差的解释程度超过0.9会被认为是很理想的结果。下面是标准计算实现from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score import numpy as np mae mean_absolute_error(y_test_original, pred) rmse np.sqrt(mean_squared_error(y_test_original, pred)) r2 r2_score(y_test_original, pred) print(fMAE: {mae:.4f}) print(fRMSE: {rmse:.4f}) print(fR²: {r2:.4f})逻辑说明MAE是误差绝对值的平均直观反映平均偏离多少RMSE对误差取了平方再开方离群点会被放大同样的两个模型谁的RMSE小说明谁的大误差少R²的取值范围理论上可以从负无穷到1越接近1越好如果出现负数说明模型连用均值填充的水平都打不过一定哪里装错了。参数说明这三个指标必须在反归一化之后计算也就是用真实物理量纲去比。如果在归一化尺度上算指标数字会非常小看着好看但没有实际意义。另外如果测试集本身方差很小任何模型都会得到偏低的R²这时额外报告MAE来佐证是更严谨的做法。提示数据里如果有明显离群点RMSE会被拉得比MAE大很多两个指标一起报比只报一个更能说明模型的真实鲁棒性。4.2 预测效果图真实值与预测值对比的两张核心图课程作业评分里图几乎占半壁江山。最少要画两张一张是测试集时间跨度上的真实曲线与预测曲线叠加图另一张是训练过程的loss和val_loss下降曲线前者证明预测贴合度后者证明训练收敛性缺一张都会被评语里扣分。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False plt.figure(figsize(12, 5)) plt.plot(y_test_original, label真实值, linewidth1.2) plt.plot(pred, label预测值, linewidth1.2, linestyle--) plt.legend() plt.title(测试集真实值与LSTM预测值对比) plt.xlabel(时间步) plt.ylabel(数值) plt.tight_layout() plt.savefig(prediction_curve.png, dpi200) plt.show()逻辑说明真实值用实线预测值用点划线用线型区分保证黑白打印时也能看出差异。标题、图例和坐标轴标签是隐藏的加分项很多代码包只输出一条裸线答辩老师看到带完整标注的版本会明显更有好感。参数说明dpi200保证保存的图插入论文时不糊SimHei用于Windows中文字体显示如果你用的是Mac或Linux改成PingFang或Noto Sans CJK否则中文会变方框。loss曲线图用history里的数据plt.figure(figsize(12, 4)) plt.plot(history.history[loss], labeltrain loss) plt.plot(history.history[val_loss], labelval loss) plt.legend() plt.title(训练损失与验证损失) plt.xlabel(epoch) plt.ylabel(loss) plt.tight_layout() plt.savefig(loss_curve.png, dpi200) plt.show()逻辑说明train loss一路下降、val loss同步下降且两者没有明显拉开是健康训练的典型图像。如果train loss降得很低而val loss中途反弹说明过拟合这个拐点比任何指标都更早暴露问题也是调参时最主要的观察对象。参数说明history.history里只有fit时配置了validation_split或validation_data才存在val_loss这个key否则画图时直接KeyError这是常被忽略的前提。4.3 结果导出预测结果写成CSV的规范格式最后把预测结果落盘课程作业里要求提交预测结果表的场景很常见。给出一个直接可用的导出方式result_df pd.DataFrame({ 真实值: y_test_original.flatten(), 预测值: pred.flatten(), 绝对误差: np.abs(y_test_original.flatten() - pred.flatten()) }) result_df.to_csv(prediction_result.csv, indexFalse, encodingutf-8-sig) print(result_df.head())逻辑说明flatten()把二维列向量拍成一维和前面reshape(-1, 1)是配套操作绝对误差列方便你在后处理时筛出预测得差的样本定位到特殊事件期或突变点。head()打印前五行用于抽查结果是否合理。参数说明encodingutf-8-sig专门解决Excel打开CSV中文乱码的问题用WPS或Excel打开文件确认一遍不乱码是交作业前的基本礼仪。5. 避坑与常见问题排查从跑通到跑好的5个踩坑记录5.1 归一化后预测值变成一条平直线现象是反归一化之后的预测值几乎是一个常数画在图上是一条水平线真实值的起伏完全没被跟上。原因通常有三个一是学习率太大模型很快收缩到输出均值附近二是window_size太小模型看不到周期只能学到一个平均值三是数据本身有强烈趋势且没有做差分平稳化。解决方法是先看训练loss曲线如果loss在第一个epoch就掉到0.01以下并且不再变化直接把learning_rate调到0.0001重训如果loss正常但预测仍是直线把window_size改成数据的周期长度日数据带周周期就给7带年周期可以先降采样到月或缩短滑窗还不行就做一阶差分把原始值变成今天减昨天的差值让模型预测变化量而非绝对值预测结果再累加还原。5.2 训练集R²接近1测试集R²却是负数这是典型的过拟合标本。现象是训练阶段指标非常漂亮一换到测试集全面崩盘。原因在于LSTM参数量大而时间序列样本通常只有几百条模型很快把训练集背下来了。解决不复杂第一把LSTM的units从64降到32Dropout从0.2提至0.3第二确认数据划分是严格按时间顺序而不是随机切分随机切分会把未来的点混进训练集造成虚假的高分第三用重叠滑窗替代非重叠切分步长取1就是连续重叠等于在有限数据里变出更多训练样本这是时间序列数据增强里最朴素也最有效的手段之一。5.3 先fit全量数据再切分指标虚高却不自知这个坑在2.3节提示过但这里要讲排查方法。症状是测试集指标很好但换一个数据集就大幅退化且训练集和测试集分布稍有变化就崩。原因是归一化器看到了整个数据集的min和max未来信息提前泄漏模型在已经知道全局尺度的条件下训练指标自然虚高。解决方法是在fit_transform前后分别打印scaler.data_min_和scaler.data_max_与训练集真实极值对比如果发现等于全量数据的极值说明切分顺序写错了。把代码改成先按时间切分再在训练集子集上fit测试集只transform也就是2.3节给出的顺序。5.4 LSTM输入形状报错维度对不上现象是运行时报 Input 0 of layer lstm is incompatible with the layer: expected ndim3, found ndim2 一类错误。原因是忘了把二维(N, window_size) reshape成三维或者滑窗函数的输出本身就不是预期的窗口结构。解决方法是在训练前打印X_train.shape确认是(N, window_size, n_features)不是就按3.1节的reshape补一层。另外多变量场景里n_features必须等于滑窗每个片段里的列数很多人把样本数误填到第三维形状检查时会报 cannot reshape array of size ...跟着报错信息回查切片逻辑就好。5.5 训练时loss出现NaN或inf这是最让人头大的情况。现象是训练到某个epoch后loss突然变成nan或者一开始就是nan。原因通常是三件事凑在一起数据里有NaN、学习率过大、梯度爆炸。解决顺序是先用df.isnull().sum()检查原始数据有没有缺失再检查缩放后的数组里是否存在inf最后把learning_rate降到0.0001重新训练。如果数据本身有极端的离群值MinMaxScaler会把正常数据压到极小区间导致梯度变化剧烈这时换成RobustScaler或StandardScaler往往更快见效。6. 进阶技巧让LSTM时间序列预测达到95分级别的四个细节6.1 时间戳特征工程让模型更早发现周期性如果数据带日期列把星期几、月份、小时拆出来作为额外输入特征很多周期性规律就不需要LSTM自己硬学习。比如销量数据的周末效应模型看到日期特征后能更快收敛。具体做法是在滑窗前把日期列转换成数值特征和原始值一起缩放后进入窗口n_features相应改成特征总数。6.2 多步预测直接输出与递归的取舍如果题目要求预测未来多步而不是一步优先用直接多步输出把最后一层的Dense(1)改成Dense(horizon)标签y从单值改成未来horizon步的序列。递归预测先预测第1步再把它喂回去预测第2步的误差会随时间步叠加越往后越漂移课程作业场景里直接多步的稳定性明显更好。6.3 调参顺序一次只动一个超参数我固定下来的习惯是先固定其他参数只扫window_size记录一组指标画出趋势找到梯度最优的窗口再调隐藏层神经元数最后才动学习率。一次只动一个超参数改三个以上出了问题根本没法定责也说不清哪个改动带来了收益。6.4 先跑naive基线再谈模型优劣把预测值等于上一个时刻的真实值作为最简单的基线用同一套指标计算它的MAE和RMSE。如果LSTM跑出来的结果连这个基线都打不过说明模型根本没有学到时序规律调参都是白调。一个简单的基线计算只需要几行# naive基线用测试集里上一个时刻的真实值作为当前预测值 naive_pred y_test_original[:-1] naive_true y_test_original[1:] print(Naive MAE:, mean_absolute_error(naive_true, naive_pred))这也是我近年在预测类项目里养成的习惯任何LSTM模型报告里都先放naive基线的成绩模型能赢过它才有资格谈调参。这个思路对新手格外友好至少能帮你避开在盲目调参里耗掉整个下午。拿到一份时间序列预测LSTM模型python代码实现后先别急着改模型把环境、切分顺序、基线和三张图跑齐你就已经从能跑通跨到能讲清楚了那个95分自然也就站得住。希望帮到你。本文还有配套的精品资源点击获取