简介:针对时间序列预测课程设计与期末大作业场景,提供一套基于长短期记忆网络(LSTM)的Python完整实现方案。围绕股票收盘价预测任务,从数据读取、序列预处理、模型构建、训练评估到结果可视化均有清晰代码与注释,结构模块化,可直接运行。压缩包共三十个文件,核心为一个主程序,另含分析报告、源数据、多张神经网络结构示意图以及相关配置文件,整体大小约一点八三兆字节,轻量便捷。目前已有一千七百九十二人学习下载,适合高校学生、课程设计者及入门开发者借鉴,也可作为课程报告与答辩的配套参考资料。资料中还整理了长短期记忆模型讲解与若干架构图,配合分析报告可直观理解长期依赖、门控机制等关键概念,能显著缩短搭建时序预测环境的排错时间。
1. 这份需求到底是什么,以及拿到 95 分以上的关键在于哪里
看到一个带.zip后缀、标注“95分以上”的标题,基本可以判断这不是生产级项目,而是课程设计、期末大作业或毕业设计一类的东西。时间序列预测用 LSTM 来做,是深度学习入门阶段最经典的任务组合。它的难点不在“跑通一个模型”,而在“如何让整个交付物看起来完整、专业、可复现”。评分标准通常分布在几个维度:预测效果(RMSE、MAE 这些指标是否好看)、代码结构是否清晰、有没有可视化对比图、有没有对模型原理的解释、以及实验是否设置了合理的对比(比如 LSTM 和 ARIMA、和简单全连接网络的差距)。这篇文章就按这个思路,讲清楚从数据准备到模型构建、再到训练评估和最终打包交付的完整路径,目标是让这份代码拿到 90 分以上的评价——而不仅仅是模型能跑。
常见做法是使用 PyTorch 实现一个标准 LSTM 回归模型,配合滑动窗口构造样本,用 MinMaxScaler 做归一化,然后输出训练损失曲线、验证集真实值与预测值的对比图。这一整套下来大概 300 行以内就能完成,但每一步都有值得优化的细节,后面会逐个展开。
2. LSTM 时间序列预测的基本原理与数据构造方法
2.1 为什么选 LSTM 而不是 RNN 或普通全连接网络
时间序列预测的核心问题是:如何根据过去一段时间的观测值,预测未来一个或多个时间步的值。传统 ARIMA 模型对线性关系处理得不错,但遇到复杂的非线性模式就力不从心。普通 RNN 能处理序列,但在长序列上存在梯度消失问题,导致模型很难学到相隔较远的依赖关系。LSTM 通过引入门控机制(输入门、遗忘门、输出门)和细胞状态(cell state),让梯度可以在时间维度上更顺畅地传播,因此在处理中等长度的序列依赖时明显优于 RNN。
和全连接网络相比,LSTM 的优势在于它显式建模了时间顺序。全连接网络把每个时间步的特征拼在一起输入,本质上丢失了先后关系;而 LSTM 每个时间步的输入都会和上一个时间步的隐藏状态结合,天然适合序列建模。对于单变量时间序列预测,输入维度通常是 1,隐藏层大小设 32 或 64,网络规模不需要很大就足够拟合常见的作业数据集。
2.2 滑动窗口构造样本的核心代码
不管用什么框架,LSTM 时间序列预测的第一步都是把原始序列转换成(样本数, 时间步长, 特征维度)的三维张量。这里的关键参数是look_back,即用过去多少个时间步来预测下一个时间点。这个值直接决定了模型的“视野”:太小,模型学不到趋势;太大,训练样本数减少,训练时间增加,还可能引入噪声。
import numpy as np def create_sequences(data, look_back=10): X, y = [], [] for i in range(len(data) - look_back): X.append(data[i:i + look_back]) y.append(data[i + look_back]) return np.array(X), np.array(y) # 假设 raw_data 是一维 numpy 数组,已经按时间顺序排列 raw_data = np.sin(np.linspace(0, 20, 500)) + np.random.normal(0, 0.1, 500) X, y = create_sequences(raw_data, look_back=10) print(X.shape, y.shape) # (490, 10, 1) / (490,)这段代码值得说清楚几个点。create_sequences循环里data[i:i + look_back]取出前look_back个点作为输入特征,data[i + look_back]作为监督学习的标签。注意循环终止条件是len(data) - look_back,这样保证标签不越界。输出形状中(490, 10)是样本数和时间步数,但还没到 LSTM 的输入要求。LSTM 期望的输入是三维的,所以后面需要加上特征维度:X = X.reshape((X.shape[0], X.shape[1], 1))。如果你的数据集是多变量(比如同时有温度、湿度、风速),最后一维的数值就不是 1,而是特征数量。
2.3 数据归一化:必须只拟合训练集
归一化是这类任务里最容易犯错误的地方,也是评分老师喜欢看的一个细节。常见做法是用 MinMaxScaler 把所有数据按同一套 min/max 压缩到 [0,1] 区间。严谨的做法是:先切分训练集和验证集,然后只在训练集上调用fit_transform,在验证集上只调用transform。原因很好理解:如果先对全量数据做归一化,验证集的信息就已经泄露到了训练过程中,测试指标会偏乐观,这在作业答辩时很容易被问住。
from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler(feature_range=(0, 1)) # 假设原始数据是一维序列,先转成列向量再归一化 train_size = int(len(raw_data) * 0.8) train_data = raw_data[:train_size].reshape(-1, 1) val_data = raw_data[train_size:].reshape(-1, 1) # 关键:只用训练集的统计量 scaled_train = scaler.fit_transform(train_data) scaled_val = scaler.transform(val_data) # 再把归一化后的数据切成监督学习样本 X_train, y_train = create_sequences(scaled_train.flatten(), look_back=10) X_val, y_val = create_sequences(scaled_val.flatten(), look_back=10)feature_range=(0,1)是常用的归一化范围,把数据压缩到 0 和 1 之间,配合默认的 tanh 激活函数正好合适。fit_transform和transform的区别是:前者计算训练集的最小值和最大值并做转换,后者直接用已计算好的参数做转换。预测完成后,反向使用scaler.inverse_transform把预测值还原到原始量纲,画图时才能和真实数据放在同一坐标系下对比。
3. PyTorch 中 LSTM 模型的构建与训练配置
3.1 定义 LSTM 网络结构
PyTorch 的torch.nn.LSTM封装了完整的 LSTM 单元运算,不需要手动实现门控逻辑。网络结构上,常见做法是:LSTM 层 -> 全连接层 -> 输出。LSTM 层负责提取时间维度上的特征,全连接层负责把最后一个时间步的隐藏状态映射到预测值。注意这里只取最后一步的隐藏状态,因为我们的任务是“看到过去 10 个点,预测下一个点”。
import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2, output_size=1): super().__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True ) self.regressor = nn.Sequential( nn.Linear(hidden_size, 32), nn.ReLU(), nn.Linear(32, output_size) ) def forward(self, x): # x shape: (batch_size, seq_len, input_size) lstm_out, (h_n, c_n) = self.lstm(x) # 取最后一个时间步的输出,形状为 (batch_size, hidden_size) last_hidden = lstm_out[:, -1, :] return self.regressor(last_hidden) model = LSTMPredictor(input_size=1, hidden_size=64, num_layers=2) # 验证前向传播 dummy_input = torch.randn(32, 10, 1) # 32个样本,每个10个时间步,1个特征 output = model(dummy_input) print(output.shape) # torch.Size([32, 1])几个参数需要解释。batch_first=True让输入张量的维度顺序是(batch, seq_len, input_size),这对习惯用 batch 维度写代码的人来说更直观;如果不设置,默认的维度顺序是(seq_len, batch, input_size),很容易搞混。num_layers=2表示堆叠两层 LSTM,第二层会把第一层的输出作为输入,深层的表达能力更强,但训练时间也成倍增加。对于作业级别的数据(几百到几千个样本),一层或两层已经足够,不要堆太深,否则容易过拟合。
lstm_out[:, -1, :]是取序列中最后一个时间步的隐藏输出。lstm_out的形状是(batch_size, seq_len, hidden_size),[:, -1, :]把 seq_len 维度上最后一个位置取出来。这里有一个容易踩的坑:如果要预测未来多个时间点,就不能只取最后一步的输出,而需要考虑用解码器逐步预测,或者直接让 LSTM 输出整个预测序列。
喜欢用h_n做输出的做法也很常见:h_n是最后一个时间步的隐藏状态,形状是(num_layers, batch_size, hidden_size),取最后一层就是h_n[-1],效果和lstm_out[:, -1, :]基本等价,但后者对多层 LSTM 更友好,因为lstm_out已经包含了最后一层所有时间步的输出。
3.2 训练循环、损失函数与优化器选择
回归问题一般用均方误差nn.MSELoss()作为损失函数,优化器用 Adam,学习率从0.001起步。训练循环的写法上有几个细节值得注意:每个 batch 前要调用optimizer.zero_grad()清空梯度,否则 PyTorch 会默认累加梯度;模型要调用.train()方法,这会启用 Dropout 和 BatchNorm 的训练行为;反向传播后要调用optimizer.step()更新参数。这三个步骤顺序不能乱。
import torch.optim as optim from torch.utils.data import TensorDataset, DataLoader # 转换为 PyTorch 张量并创建数据加载器 X_train_t = torch.FloatTensor(X_train).unsqueeze(-1) # (样本数, 时间步, 特征维度) y_train_t = torch.FloatTensor(y_train) X_val_t = torch.FloatTensor(X_val).unsqueeze(-1) y_val_t = torch.FloatTensor(y_val) train_dataset = TensorDataset(X_train_t, y_train_t) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True) criterion = nn.MSELoss() optimizer = optim.Adam(model.parameters(), lr=0.001) num_epochs = 100 train_losses = [] for epoch in range(num_epochs): model.train() epoch_loss = 0.0 for batch_X, batch_y in train_loader: optimizer.zero_grad() predictions = model(batch_X) loss = criterion(predictions.squeeze(), batch_y) loss.backward() optimizer.step() epoch_loss += loss.item() * batch_X.size(0) avg_loss = epoch_loss / len(train_dataset) train_losses.append(avg_loss) if (epoch + 1) % 10 == 0: print(f"Epoch [{epoch + 1}/{num_epochs}], Loss: {avg_loss:.6f}")squeeze()的作用是去掉形状为 1 的维度,否则predictions的形状是(batch_size, 1)而batch_y是(batch_size,),两者相减会触发广播机制,虽然不一定报错,但会让代码逻辑变得混乱。batch_size=32是一个相对稳妥的选择:太小会导致训练震荡,太大在 CPU 上一次前向传播耗时就很长。如果你的电脑没有独立显卡,用 CPU 跑完整训练可能要几分钟到十几分钟,这是正常的,不需要担心。
3.3 验证与早停机制
训练过程中需要时刻关注模型在验证集上的表现。一个常见的坏习惯是只关注训练 loss 的下降,却不看验证 loss。在作业评分场景下,验证集的指标才是真正的评分依据。可以每一轮或每几轮计算一次验证集 loss,如果连续 N 轮没有下降,就提前停止训练,保存最优模型。这叫做 early stopping,是一个值得写进报告里的技术点。
best_val_loss = float("inf") patience = 15 counter = 0 for epoch in range(num_epochs): # ... 训练代码如上 ... # 验证阶段 model.eval() val_loss = 0.0 with torch.no_grad(): predictions = model(X_val_t) val_loss = criterion(predictions.squeeze(), y_val_t).item() if val_loss < best_val_loss: best_val_loss = val_loss torch.save(model.state_dict(), "best_model.pth") counter = 0 else: counter += 1 if counter >= patience: print(f"Early stopping at epoch {epoch + 1}") breakmodel.eval()和with torch.no_grad()是这里两个容易被忽略但极其重要的细节。.eval()会关闭 Dropout 和 BatchNorm 的运行统计更新,保证验证时的前向传播是确定性的;torch.no_grad()则让 PyTorch 不构建计算图,大幅减少内存占用并加快计算速度。推理完成后如果要继续训练,必须记得切回model.train(),否则模型会一直在验证模式下训练,效果大打折扣。torch.save(model.state_dict(), ...)只保存模型参数,不保存整个模型结构,加载时需要用同样的类结构初始化后再load_state_dict。
4. 模型评估、预测效果可视化与分数提升手段
4.1 回归任务的三类核心评估指标
作业评分中,除了看预测曲线和真实曲线的贴合程度,数值化指标也是重点扣分项。常用的三个指标是:均方根误差(RMSE)、平均绝对误差(MAE)、平均绝对百分比误差(MAPE)。RMSE 对大误差更敏感,能放大那些偏离较远的预测点;MAE 反映平均绝对偏差,单位更直观;MAPE 以百分比形式呈现,便于理解误差相对真实值的比例。三个指标结合使用,可以从不同角度评判模型性能。
from sklearn.metrics import mean_squared_error, mean_absolute_error def mape(y_true, y_pred): y_true, y_pred = np.array(y_true), np.array(y_pred) # 避免除零:将真实值为 0 的点从计算中移除 mask = y_true != 0 return np.mean(np.abs((y_true[mask] - y_pred[mask]) / y_true[mask])) * 100 # 反归一化后才能计算有意义的指标 y_val_np = y_val_t.numpy() pred_np = model(X_val_t).squeeze().detach().numpy() # 还原到原始数据尺度 y_val_original = scaler.inverse_transform(y_val_np.reshape(-1, 1)) pred_original = scaler.inverse_transform(pred_np.reshape(-1, 1)) rmse = np.sqrt(mean_squared_error(y_val_original, pred_original)) mae = mean_absolute_error(y_val_original, pred_original) mape_value = mape(y_val_original, pred_original) print(f"RMSE: {rmse:.4f}") print(f"MAE: {mae:.4f}") print(f"MAPE: {mape_value:.2f}%")MAPE 函数里用了掩码操作,排除真实值为 0 的样本,因为真实值为 0 时除法会得到无穷大。inverse_transform把预测值和真实值从归一化后的区间还原到原始数据的量纲,这样计算出来的 RMSE 和 MAE 才有实际含义。建议把这三个指标连同训练时间、参数量一起写进实验报告,评分老师会比较看重这种规范化呈现。
4.2 真实值与预测值对比图:让分数上一个台阶的核心手段
很多学生提交的作业里只有训练 loss 曲线,这是非常可惜的,因为真实值与预测值的对比图是评分者最直观感受模型效果的地方。用 matplotlib 画图时,需要把训练集和验证集的数据放在同一张图中展示,并用不同颜色区分。这样能清楚地看到模型在训练区间内拟合得好不好,在验证区间内的预测是否接得上真实的趋势。
import matplotlib.pyplot as plt plt.figure(figsize=(14, 5)) plt.plot(np.arange(len(y_val_original)), y_val_original, label="true_value", color="blue", linewidth=1.5) plt.plot(np.arange(len(pred_original)), pred_original, label="predicted_value", color="red", linestyle="--", linewidth=1.5) plt.title("LSTM Time Series Prediction on Validation Set") plt.xlabel("Time Step") plt.ylabel("Value") plt.legend() plt.grid(True, alpha=0.3) plt.tight_layout() plt.savefig("prediction_curve.png", dpi=150) plt.show()np.arange(len(y_val_original))生成验证集的时间步序号,作为 x 轴。预测值和真实值用同一组时间索引,方便逐点对比。grid(True, alpha=0.3)让网格线变淡,避免干扰数据曲线的观察;dpi=150让输出图片足够清晰,在论文或报告里直接使用也不会模糊。需要特别强调的是,预测曲线和真实曲线的缺口本身是正常的,因为验证集整体平移了look_back个时间步,前面这些步没有足够的上下文来生成预测,如果不对齐两个序列,图看起来就会错位。
4.3 95 分以上的关键:多步预测与注意力机制
大多数基础作业只要求预测单步,也就是“用过去 10 天预测下一天”。如果你想让方案明显高于平均水平,可以尝试另一个方向:多步预测,即用过去 10 天预测未来 5 天。实现方式分为两种。
第一种是迭代预测法,把第一个预测值作为输入的一部分,滚动预测下几个点。这种实现简单,但误差会累积:第一步的偏差会被后续预测放大。第二种是直接学习法,训练时就把目标设置成未来 5 个点的序列,模型输出变成一个 5 维向量。这个方案虽然训练数据的构造稍复杂,但模型能直接学习到未来多个时间步的模式。
还有一个常被忽视的加分项是加入 Bahdanau 注意力机制。标准的nn.LSTM把所有历史时间步的信息压缩到最后一个隐藏状态里,信息瓶颈不可避免;注意力机制可以让模型在每一步解码时根据需要动态选择不同历史时刻的信息。在作业代码中实现一个轻量级的注意力层并不复杂,只需几十行代码:
class AttentionLSTM(nn.Module): def __init__(self, hidden_size=64, output_size=1): super().__init__() self.lstm = nn.LSTM(input_size=1, hidden_size=hidden_size, batch_first=True) self.attention = nn.Linear(hidden_size, 1) # 计算每个时间步的注意力权重 self.regressor = nn.Linear(hidden_size, output_size) def forward(self, x): lstm_out, _ = self.lstm(x) # lstm_out: (batch, seq_len, hidden) -> 注意力打分 attn_scores = self.attention(lstm_out).squeeze(-1) # (batch, seq_len) attn_weights = torch.softmax(attn_scores, dim=1).unsqueeze(1) # (batch, 1, seq_len) # 加权求和得到上下文向量 context = torch.bmm(attn_weights, lstm_out).squeeze(1) # (batch, hidden) return self.regressor(context)torch.softmax(attn_scores, dim=1)沿着时间步维度做归一化,让所有时间步的注意力权重之和为 1。torch.bmm是批量矩阵乘法,用注意力权重对 LSTM 输出的所有时间步做加权求和。这个结构在原理上和你会在报告里写清楚的“时间步权重分配机制”是一致的,在答辩展示中的表达空间也更大。如果你的数据比较长,比如超过 100 个时间步,注意力机制的效果会更明显。
5. 模型应用的完整验证流程与常见坑
拿 95 分以上的核心在于:让评分者感觉到你不仅会用框架,还理解背后的原理和边界。这里给出一套我觉得比较标准的项目结构参考,做作业时可以直接照搬目录组织方式,解释起来也流畅:
time_series_lstm/ |-- data/ # 存放原始数据 | `-- dataset.csv |-- models/ # 保存训练好的模型权重 | `-- best_model.pth |-- figures/ # 可视化图片输出 | |-- loss_curve.png | `-- prediction_curve.png |-- src/ | |-- data_processing.py # 数据加载、归一化、滑窗 | |-- model.py # LSTM 模型定义 | |-- train.py # 训练和验证逻辑 | `-- predict.py # 加载模型并做预测 |-- requirements.txt `-- README.mdREADME.md里写明如何安装依赖、如何运行、如何调整look_back、hidden_size、num_epochs等参数,这在课程作业评审中是很加分的项目意识。运行顺序通常分三步:
pip install -r requirements.txt python src/train.py python src/predict.pyrequirements.txt里至少要包含这些版本信息:
numpy==1.24.3 pandas==2.0.3 matplotlib==3.7.2 scikit-learn==1.3.0 torch==2.0.1固定版本号可以避免环境和版本不一致造成的行为差异。需要特别注意 PyTorch 从 2.x 开始对某些旧版 NumPy 不兼容,如果你在安装依赖时报np.object相关的错误,多半是 NumPy 版本太高或太低。
最后做一个简单的模型完整性验证:加载best_model.pth,输入一个长度为look_back的序列,确认输出形状正确,并手动计算预测值和真实值的差异是否符合预期。这一步看似多余,但在答辩现场如果模型加载时报 shape mismatch 的错,会直接拉低印象分。可以用下面这段代码做统一检查:
import torch # 从 src.model 导入模型类 from model import LSTMPredictor model = LSTMPredictor(input_size=1, hidden_size=64, output_size=1) model.load_state_dict(torch.load("models/best_model.pth")) model.eval() test_seq = torch.randn(1, 10, 1) # 模拟一个长度为 10 的输入序列 with torch.no_grad(): pred = model(test_seq) print(pred.shape) # 期望输出 torch.Size([1, 1])如果输出是torch.Size([1, 1]),说明模型权重加载成功且前向传播没有问题。到这里,从数据准备到模型构建、训练评估、可视化再到最终交付验证的完整路径已经打通了。按这个流程做出来的时间序列预测 LSTM 项目,在代码结构完整度、实验丰富度和工程规范性上都覆盖了 95 分评分档位的主要要求。最后记得把预测曲线的对比图做得美观一些——在一个评分权重很高的环节里,这张图的呈现质量往往比模型那零点几个百分点的精度提升更直接。
本文还有配套的精品资源,点击获取