通俗《深度学习》-LSTM长短期记忆网络原理及实现
🔥 热身:你觉得理解序列数据,为什么需要一种"特殊"的神经网络?
想想看:我们人看一句话的时候,是逐个字读的,而且前面的字会影响我们对后面字的理解。比如:
"我今天吃了__,味道不错。"
你觉得空白处更可能是 "苹果" 还是 "跑步"?为什么?
没错!🍎 你答对了——而且你刚刚其实已经自己发现了 LSTM 要解决的核心问题!
你想过没有——为什么不是"跑步"?因为:
- "吃" 这个词告诉我们,后面大概率跟的是食物
- "味道不错" 进一步确认了是吃的东西
也就是说:一个词的含义,依赖于它前面的上下文。
🤔 那问题来了:普通的前馈神经网络能处理这个吗?
让我画个简单的对比:
普通神经网络(前馈):
输入 → [一个固定窗口] → 输出
就像你拍照——一张照片只包含那一瞬间的信息,前面发生了什么它不知道。
但序列数据(文字、股价、语音) 的特点是:
- 今天的股价 ← 受昨天、前天……的影响
- 这句话末尾的词 ← 受前面十几个词的影响
🧠 所以,人们先发明了 RNN(循环神经网络)
RNN 的想法很巧妙——给它加一个"记忆盒":

这样每一时刻,它都把上一时刻的信息传下来。
⚠️ 但 RNN 有个大问题……
来,我用一个简单的例子考考你,假设 RNN 在逐字阅读这句话:
"我出生在法国……(中间隔了 50 个字)……我会说流利的_____"
你觉得 RNN 读到最后一个空时,还能记得前面说的 "法国" 吗?还是会忘掉?
答案是会!
👏 这就是 RNN 著名的 "长期依赖问题"——信息传着传着就衰减了,就像你在一个嘈杂的聚会上,有人从远处喊话,传到你耳边时已经听不清了。
🎯 这时候 LSTM 闪亮登场!
LSTM(长短期记忆网络)就是 RNN 的 "升级版",它专门解决"记不住长距离信息"的问题。
怎么做到的呢?我用一个非常直观的类比来解释:
🚪 想象你是看门人,管理一个"记忆仓库"
LSTM 的核心是一个细胞状态(Cell State)——你可以把它想象成一条传送带,信息沿着这条带子一直往前传,几乎不衰减。
而 LSTM 有三个门来控制信息:

让我用整理书桌的例子帮你理解:
| 门 | 类比 | 做什么 |
|---|---|---|
| 遗忘门 | 决定扔掉桌上的哪些旧草稿纸 | 删除不再需要的历史信息 |
| 输入门 | 决定把哪些新笔记贴到墙上 | 把重要的新信息存入细胞状态 |
| 输出门 | 决定今天跟别人说什么 | 基于当前的记忆,输出有用的信息 |
💡 直观例子:还是刚才那句话
"我出生在法国……(中间 50 个字)……我会说流利的____"
普通 RNN 的做法:一个字一个字传记忆 → 传到第 50 个字时,"法国"的信息已经衰减到快要没了 → ❌ 猜不出来
LSTM 的做法:
- 遗忘门:中间的那些描述性文字(比如"我小时候喜欢踢足球")→ 🗑️ 扔掉大部分
- 输入门:发现新信息里没有比"法国"更重要的地点 → 不覆盖
- 传送带上"法国"这条信息一直保留着 → ✅ 到了空白处,能想起填"法语"或"法国话"
二者区别在于:
| RNN | LSTM | |
|---|---|---|
| 记忆方式 | 📦 一个小盒子,东西多了就塞不下 | 🚚 一条传送带,可以长途运输 |
| 长距离记忆 | ❌ 传着传着就衰减了 | ✅ 几乎不衰减 |
| 控制机制 | 没有"门",被迫全部记或全部忘 | 🚪 三个门,主动选择记什么、忘什么 |
所以更精确地说,LSTM 的绝招是——可以选择性地记住重要信息,忘记不重要的信息。
🔧 现在是时候看看实践层面的东西了
选择时间序列预测(天气预报、股价预测)——这是 LSTM 最经典的应用场景之一。
🧱 进入第二层:LSTM 的内部到底发生了什么?
刚才用"三扇门"的类比给了你一个整体画面。现在我们来稍微深入一点点,看看数学上每扇门具体是怎么工作的。
别怕,我会用最直观的方式来解释,不讲复杂的公式推导。
🚪 遗忘门:决定"要不要忘记"
假设我们现在正在做股价预测:
输入序列:昨天的股价、前天的股价、大前天的股价……
当前时刻:t
遗忘门做的事情是——看一眼"当前输入"和"上一时刻的输出",然后给细胞状态里的每条信息打一个 0~1 之间的分数:
- 1 = "完全保留这条信息" ✅
- 0 = "完全删除这条信息" ❌
现实例子:假设公司突然换了 CEO,那么旧的"CEO 偏好"信息对预测股价就没有用了 → 遗忘门会给它打接近 0,把它从传送带上删除。
📝 输入门:决定"要记住什么"
输入门做两件事:
- 挑重点:看看当前的新数据(比如今天的股价、成交量)里,哪些是值得记住的
- 写入传送带:把这些重要的新信息存到细胞状态里
现实例子:今天发布了超预期的财报 → 输入门会说:"这条很重要,记到传送带上!"📈
🎤 输出门:决定"要输出什么"
最后,基于当前更新后的细胞状态,输出门决定:
- "好的,基于我记住的所有信息(过去的 + 新的),现在我要输出什么预测结果?"
现实例子:你综合了"历史走势 + 财报利好 + 行业趋势",然后输出预测——"明天股价大概率上涨 2%"
一张图总结:

🤔 检查理解
现在我想确认你是否真的理解了三扇门的分工,来做个简单的小练习:
场景:你用 LSTM 预测某只股票的价格。
- 第 1 天:股价 100 元
- 第 2 天:股价 102 元 📈
- 第 3 天:发布消息说"公司财务造假" 💥
问题:当 LSTM 处理到第 3 天的数据时,关于第 1、2 天的股价信息,遗忘门会怎么做?输入门又会怎么做?
A.遗忘门保留旧信息,输入门记造假消息(旧数据还有用+新数据很重要)
B.遗忘门删除旧信息,输入门记造假消息(旧数据没用了+新数据很重要)
C.遗忘门保留旧信息,输入门不记造假消息(旧数据有用+造假不重要)
#正确答案是B
- 遗忘门 🗑️:财务造假这种颠覆性消息出来后,旧的股价走势(100→102)已经没参考价值了 → 删除旧信息
- 输入门 📝:造假消息极其重要 → 写入传送带
你已经开始像 LSTM 一样思考了!👏
🎯 接下来:从原理到实操
现在你对 LSTM 的原理已经有了清晰的直觉理解。下一步,我们来看看如何在实际项目中使用 LSTM,具体步骤:
- LSTM 的数据准备:时间序列数据怎么喂给模型?(滑动窗口、归一化等)
- 用 PyTorch / TensorFlow 搭建 LSTM:看一段真实代码
- 调参实战:隐藏层大小、层数、学习率等
第一步:数据准备 🧹
在把数据喂给 LSTM 之前,有两个关键步骤必须做。我先考考你一个直觉题:
情景设定
假设你有某只股票 连续 30 天 的收盘价:
[100, 102, 101, 103, 105, 104, 106, 108, 107, 109, ...]
你想用 过去 3 天的价格 来预测 第 4 天的价格。
🤔 问题:你觉得应该怎么把上面的数据切分成"输入→输出"对?
比如说——第一个样本应该是什么?
X=[100,102,101] → y=103
这就是 滑动窗口(Sliding Window)的核心思想:
样本 1: X = [100, 102, 101] → y = 103
样本 2: X = [102, 101, 103] → y = 105
样本 3: X = [101, 103, 105] → y = 104...
窗口(3天)像一列火车一样,一格一格往前滑 🚂
那窗口大小怎么选?
这是第一个需要你判断的超参数——
你觉得对于股价预测,窗口大小(过去看几天)设得越大越好,还是适中就好?为什么?
适中就好,太远的数据可能反而有噪音,原因很符合直觉:
- 窗口太小(比如 1 天):看不到趋势,像"盲人摸象"🐘
- 窗口太大(比如 100 天):太久远的数据和当前关系不大,反而引入噪音,而且会让训练数据变少
- 适中(实践中常用 10~60 天不等):能捕捉到近期趋势又不会太嘈杂
第二步:归一化(Normalization)
另一个非常重要的步骤。先问一个问题:
场景:假设你要预测两只股票——
- 股票 A:价格在 100~200 元 之间
- 股票 B:价格在 0.5~2 元 之间
如果你直接把原始价格喂给 LSTM,会有什么问题?
就会发现数值大的特征会主导学习,模型会过度关注数字大的特征,忽略小的
所以我们会把数据缩放到一个统一的范围,最常用的是 MinMax 归一化:
标准化后的值 = (原始值 - 最小值) / (最大值 - 最小值)
把所有价格都映射到 0~1 之间,这样模型就能公平地学习每个特征的贡献。
还有一个重要细节:🚫 防止数据泄露!
在做归一化时,只能用训练集的最大/最小值,而不能用整个数据集(包括未来数据)的统计量——否则就相当于"提前偷看了未来",模型在测试时会表现虚高。
📦 数据准备的完整流程总结

如果没问题,我们就进入下一步——
🖥️ 第二步:实战代码
用 PyTorch 搭建一个 LSTM 来做股价预测!
我会用到的真实数据集——这里我准备用一段模拟的股价数据(这样你不需要下载任何外部文件,直接跑就能看到效果),但代码结构和真实项目完全一致。
第一步,生成模拟数据
# 设置随机种子为42,确保结果可复现
np.random.seed(42)
# 定义数据的时间长度为500天
days = 500
# 生成从100到150的线性趋势序列,模拟股价长期上涨
trend = np.linspace(100, 150, days)
# 生成服从正态分布的随机噪声,标准差为5,模拟市场波动
noise = np.random.randn(days) * 5
# 将趋势和噪声相加,得到最终的模拟股价
price = trend + noise

用模拟数据不需要下载任何外部文件,可以立即跑通代码,看到效果。之后换成真实 CSV 数据只需要改这一块。
第二步,数据预处理
# 将一维数组转换为二维数组,形状为(500, 1),适应scaler要求
prices = price.reshape(-1, 1)
# 归一化到 [0, 1]
# 创建MinMaxScaler实例,指定缩放范围为0到1
scaler = MinMaxScaler(feature_range=(0, 1))
# 拟合数据并进行归一化转换,将股价缩放到[0,1]区间
prices_scaled = scaler.fit_transform(prices)
这就是我们前面讲的归一化,把价格压到 0~1 之间。
接下来创建LSTM所需的时间序列数据
参数:
data (numpy.ndarray): 输入的时间序列数据,形状为(样本数, 特征数)
window_size (int): 滑动窗口大小,即使用多少个时间步长来预测下一个值
返回:
X (numpy.ndarray): 输入特征数组,形状为(样本数, window_size)
y (numpy.ndarray): 目标值数组,形状为(样本数,)
用法示例:
>>> data = np.array([[0.1], [0.2], [0.3], [0.4], [0.5]])
>>> X, y = create_sequences(data, window_size=2)
>>> X
array([[0.1, 0.2],
[0.2, 0.3],
[0.3, 0.4]])
>>> y
array([0.3, 0.4, 0.5])
原理说明:
对于长度为N的序列,滑动窗口大小为W时:
- 第1个样本:X = [data[0], ..., data[W-1]], y = data[W]
- 第2个样本:X = [data[1], ..., data[W]], y = data[W+1]
- 最终生成(N-W)个训练样本
# 定义滑动窗口大小为10,即用过去10天的数据预测下一天
WINDOW = 10
def create_sequences(data, window_size):
# 初始化输入特征列表X和目标值列表y
X, y = [], []
# 遍历数据,从第0个到第(len(data)-window_size-1)个
for i in range(len(data) - window_size):
# 取从i到i+window_size的切片作为输入特征,取第0列
X.append(data[i:i + window_size, 0])
# 取第i+window_size个数据作为目标值(下一天的股价)
y.append(data[i + window_size, 0])
# 将列表转换为NumPy数组并返回
return np.array(X), np.array(y)
这就是滑动窗口函数——用前 10 天预测第 11 天。
# 使用滑动窗口创建时间序列数据
X, y = create_sequences(prices_scaled, WINDOW)
# 按时间顺序切分 (80% 训练, 20% 测试)
# 计算切分点,取前80%作为训练集,后20%作为测试集
split = int(len(X) * 0.8)
# 切分输入特征数据
X_train, X_test = X[:split], X[split:]
# 切分目标值数据
y_train, y_test = y[:split], y[split:]
注意是按时间顺序切分,不能随机打乱! 因为时间序列的未来不能用来预测过去。
训练集: (392, 10, 1) 测试集: (98, 10, 1)
# Reshape: (样本数, 时间步长, 特征数)
# 将训练集形状调整为(样本数, 10, 1),-1表示自动推断样本数
X_train = X_train.reshape(-1, WINDOW, 1)
# 将测试集形状调整为(样本数, 10, 1)
X_test = X_test.reshape(-1, WINDOW, 1)
最终形状为 (样本数, 10, 1) ——这正是 LSTM 要求的输入格式。
还有最后关键的一步,转为 PyTorch 张量
# 将训练集输入特征转换为PyTorch的float32张量
X_train_t = torch.tensor(X_train, dtype=torch.float32)
# 将训练集目标值转换为张量,并调整为二维(样本数, 1)
y_train_t = torch.tensor(y_train, dtype=torch.float32).view(-1, 1)
# 将测试集输入特征转换为PyTorch的float32张量
X_test_t = torch.tensor(X_test, dtype=torch.float32)
# 将测试集目标值转换为张量,并调整为二维(样本数, 1)
y_test_t = torch.tensor(y_test, dtype=torch.float32).view(-1, 1)
值得关注的是
- X_train 已经是三维形状 (样本数, 时间步长, 特征数) ,直接转换即可
- y_train 是一维形状 (样本数,) ,需要额外用 .view(-1, 1) 转换为二维 (样本数, 1) ,才能与模型输出匹配计算损失,损失函数 MSELoss 要求输入和目标的形状必须完全一致 ,否则会报错。
第三步,定义LSTM模型
class LSTMPredictor(nn.Module):
def __init__(self, input_size=1, hidden_size=32, num_layers=1):
# 调用父类nn.Module的初始化方法
super().__init__()
# LSTM 层
self.lstm = nn.LSTM(
# 输入特征维度,这里为1(只有股价)
input_size=input_size, # 特征数 (这里=1, 只有价格)
# 隐藏层维度,控制记忆容量
hidden_size=hidden_size, # 记忆容量 (越大学得越细, 也越易过拟合)
# LSTM堆叠层数,多层可学习更复杂模式
num_layers=num_layers, # 层数 (多层可以学更复杂的模式)
# 设置batch_first=True,输入形状为(batch, seq_len, features)
batch_first=True
)
# 创建全连接层,将LSTM输出从hidden_size维映射到1维
self.fc = nn.Linear(hidden_size, 1)
def forward(self, x):
# 将输入通过LSTM层,返回输出和隐藏状态(h_n, c_n)
out, (h_n, c_n) = self.lstm(x)
# out 形状: (batch, seq_len, hidden_size)
# 取LSTM最后一个时间步的输出,形状为(batch, hidden_size)
last_out = out[:, -1, :] # (batch, hidden_size)
# 通过全连接层将输出映射为单个预测值,形状为(batch, 1)
prediction = self.fc(last_out) # (batch, 1)
# 返回预测结果
return prediction
每个参数在现实中意味着什么?
| 参数 | 解释 | 类比 |
|---|---|---|
input_size=1 |
每个时间步输入1个数字(价格) | 每天只看"收盘价"这一个指标 |
hidden_size=32 |
LSTM 内部的记忆容量 | 你有32个"记事本"来跟踪不同模式 |
num_layers=1 |
LSTM 层叠的层数 | 一层分析 → 直接输出,不经过二次提炼 |
self.fc |
全连接输出层 | 把32个笔记综合成1个结论 |
第四步,训练模型
# 定义损失函数为均方误差(MSE),适用于回归任务
criterion = nn.MSELoss()
# 创建Adam优化器,学习率为0.01
optimizer = optim.Adam(model.parameters(), lr=0.01)
# 定义训练轮数为100轮
EPOCHS = 100
# 初始化列表,用于记录每轮的训练损失
train_losses = []
# 遍历每一轮训练
for epoch in range(EPOCHS):
# 将模型设置为训练模式(启用Dropout和BatchNorm)
model.train()
# 前向传播:将训练数据输入模型,得到预测输出
outputs = model(X_train_t)
# 计算预测输出与真实值之间的损失
loss = criterion(outputs, y_train_t)
# 清空梯度,防止梯度累积
optimizer.zero_grad()
# 反向传播:计算各参数的梯度
loss.backward()
# 更新模型参数
optimizer.step()
# 记录当前轮次的损失值
train_losses.append(loss.item())
# 每20轮打印一次训练信息
if (epoch + 1) % 20 == 0:
# 打印当前轮次和损失值
print(f"Epoch {epoch + 1:3d}/{EPOCHS} Loss: {loss.item():.6f}")
最后一步,进行预测和可视化的代码这里就不展示了,所有代码就放到最后啦,供大家参考。
以下是实验的结果图,最后测试集均方误差RMSE: 5.73 元

第三步:调参实战 🔧
调参的本质是什么?我打个比方——
LSTM 就像一辆赛车 🏎️
- 参数 = 这辆车的各种旋钮
- 调好了 → 赛道(你的数据)上跑得快
- 调不好 → 要么跑偏(欠拟合),要么过弯太猛翻车(过拟合)
下面我们逐一讲解最关键的几个"旋钮"。
🎛️ 旋钮 1:hidden_size(隐层大小)—— 记忆容量
这是我们模型里最重要的旋钮。一起来看一下代码中的这一行,
self.lstm = nn.LSTM(input_size=1, hidden_size=32, num_layers=1)
hidden_size=32 意味着 LSTM 内部有 32 个记忆神经元。
| hidden_size | 效果 | 问题 |
|---|---|---|
| 太小(如 4~8) | 训练快,但记不住复杂模式 | ❌ 欠拟合 — 预测效果差 |
| 适中(如 32~64) | 能捕捉到趋势和周期模式 | ✅ 通常最佳 |
| 太大(如 256+) | 训练慢,容易"背"下噪音 | ❌ 过拟合 — 训练集很准,测试集很差 |
经验法则:对于简单时间序列(如单变量股价),32~64 就够了。如果你的数据非常复杂(如多变量 + 长序列),可以试试 128。
🎛️ 旋钮 2:num_layers(LSTM 层数)—— 层级深度
单层 vs 多层 的区别就像:
| 层数 | 类比 | 效果 |
|---|---|---|
| 1 层 | 一个人直接分析 | ✅ 够用,训练快 |
| 2~3 层 | 一个人先粗筛 → 另一个人深入分析 | 🎯 捕获更抽象的模式 |
| 4 层以上 | 层层转述,信息失真 | ⚠️ 训练极慢,容易过拟合 |
对于这个规模的数据,1 层完全够用。多层 LSTM 通常在以下场景才需要:
- 数据非常多(10 万+ 样本)
- 特征很复杂(比如同时用价格、成交量、新闻情绪等)
- 任务是语音识别、机器翻译等高难度序列任务
🎛️ 旋钮 3:learning_rate(学习率)—— 步长
optimizer = optim.Adam(model.parameters(), lr=0.01)
学习率决定了模型每次调整参数时"迈多大的步子"。
用一个下山类比 🏔️
想象你在黑夜里下山,想走到山谷(找到最小损失):
| 学习率 | 场景 | 效果 |
|---|---|---|
| 太大(0.1~1.0) | 🏃 大步流星往下冲 | ❌ 可能跨过山谷,跳来跳去不收敛 |
| 适中(0.001~0.01) | 🚶 正常步伐 | ✅ 稳定走到山谷 |
| 太小(0.00001) | 🐢 小碎步 | ❌ 半天走不到山谷,训练太久 |
你的测试结果(RMSE=5.73)用的是 lr=0.01,这是 Adam 优化器的一个很好的起点。
🎛️ 旋钮 4:WINDOW(窗口大小)—— 回头看多远
WINDOW = 10 # 用过去10天预测下一天
这实际上是一个领域知识 + 实验决定的值:
- 股价:一般认为近 20~60 个交易日(1~3 个月)包含足够信息
- 天气:季节性强的数据(如气温)可能需要 365 天窗口
- 传感器数据:通常短窗口(5~20 步)就够了
🧪 调参的"黄金工作流"
你不需要盲目试——而是有策略地调参:

🤔 最后考考你
假如你的模型出现了以下情况,你觉得应该调大还是调小哪个参数?
情况:训练集损失降到了很低(0.0001),但测试集损失很高(0.02),这说明模型 ____?
过拟合
解决过拟合的常用招数(按推荐顺序):
- 🎛️ 减小
hidden_size— 降低模型容量,让它没法背下噪音 - ⏳ 减小
epochs— 在它"背答案"之前就停止训练(早停法) - 📊 增大训练数据量 — 更多数据 = 更难死记硬背
- 🛡️ 加 Dropout 层 — 随机"关掉"一些神经元,强迫模型学得更鲁棒
LSTM 股票价格预测完整代码
# ============================================
# 环境要求: pip install torch numpy matplotlib scikit-learn
# ============================================
# 导入NumPy库,用于数值计算和数组操作
import numpy as np
# 导入Matplotlib绘图库,用于数据可视化
import matplotlib.pyplot as plt
# 导入PyTorch深度学习框架
import torch
# 导入PyTorch神经网络模块,包含各种网络层
import torch.nn as nn
# 导入PyTorch优化器模块,用于模型参数优化
import torch.optim as optim
# 从scikit-learn导入MinMaxScaler,用于数据归一化
from sklearn.preprocessing import MinMaxScaler
# 设置字体为SimHei(黑体),解决中文显示问题
plt.rcParams['font.sans-serif'] = ['SimHei']
# 解决负号显示为方块的问题
plt.rcParams['axes.unicode_minus'] = False
# ------------------ 1. 生成模拟数据 ------------------
# 设置随机种子为42,确保结果可复现
np.random.seed(42)
# 定义数据的时间长度为500天
days = 500
# 生成从100到150的线性趋势序列,模拟股价长期上涨
trend = np.linspace(100, 150, days) # 长期趋势
# 生成服从正态分布的随机噪声,标准差为5,模拟市场波动
noise = np.random.randn(days) * 5 # 随机噪音
# 将趋势和噪声相加,得到最终的模拟股价
price = trend + noise # 合成价格
# ------------------ 2. 数据预处理 ------------------
# 将一维数组转换为二维数组,形状为(500, 1),适应scaler要求
prices = price.reshape(-1, 1)
# 归一化到 [0, 1]
# 创建MinMaxScaler实例,指定缩放范围为0到1
scaler = MinMaxScaler(feature_range=(0, 1))
# 拟合数据并进行归一化转换,将股价缩放到[0,1]区间
prices_scaled = scaler.fit_transform(prices)
# 滑动窗口: 用过去 WINDOW 天预测下一天
# 定义滑动窗口大小为10,即用过去10天的数据预测下一天
WINDOW = 10
def create_sequences(data, window_size):
# 初始化输入特征列表X和目标值列表y
X, y = [], []
# 遍历数据,从第0个到第(len(data)-window_size-1)个
for i in range(len(data) - window_size):
# 取从i到i+window_size的切片作为输入特征,取第0列
X.append(data[i:i + window_size, 0])
# 取第i+window_size个数据作为目标值(下一天的股价)
y.append(data[i + window_size, 0])
# 将列表转换为NumPy数组并返回
return np.array(X), np.array(y)
# 使用滑动窗口创建时间序列数据
X, y = create_sequences(prices_scaled, WINDOW)
# 按时间顺序切分 (80% 训练, 20% 测试)
# 计算切分点,取前80%作为训练集,后20%作为测试集
split = int(len(X) * 0.8)
# 切分输入特征数据
X_train, X_test = X[:split], X[split:]
# 切分目标值数据
y_train, y_test = y[:split], y[split:]
# Reshape: (样本数, 时间步长, 特征数)
# 将训练集形状调整为(样本数, 10, 1),-1表示自动推断样本数
X_train = X_train.reshape(-1, WINDOW, 1)
# 将测试集形状调整为(样本数, 10, 1)
X_test = X_test.reshape(-1, WINDOW, 1)
# 转为 PyTorch 张量
# 将训练集输入特征转换为PyTorch的float32张量
X_train_t = torch.tensor(X_train, dtype=torch.float32)
# 将训练集目标值转换为张量,并调整为二维(样本数, 1)
y_train_t = torch.tensor(y_train, dtype=torch.float32).view(-1, 1)
# 将测试集输入特征转换为PyTorch的float32张量
X_test_t = torch.tensor(X_test, dtype=torch.float32)
# 将测试集目标值转换为张量,并调整为二维(样本数, 1)
y_test_t = torch.tensor(y_test, dtype=torch.float32).view(-1, 1)
# ------------------ 3. 定义 LSTM 模型 ------------------
class LSTMPredictor(nn.Module):
def __init__(self, input_size=1, hidden_size=32, num_layers=1):
# 调用父类nn.Module的初始化方法
super().__init__()
# LSTM 层
self.lstm = nn.LSTM(
# 输入特征维度,这里为1(只有股价)
input_size=input_size, # 特征数 (这里=1, 只有价格)
# 隐藏层维度,控制记忆容量
hidden_size=hidden_size, # 记忆容量 (越大学得越细, 也越易过拟合)
# LSTM堆叠层数,多层可学习更复杂模式
num_layers=num_layers, # 层数 (多层可以学更复杂的模式)
# 设置batch_first=True,输入形状为(batch, seq_len, features)
batch_first=True # 输入形状: (batch, seq_len, features)
)
# 全连接输出层: 32 -> 1
# 创建全连接层,将LSTM输出从hidden_size维映射到1维
self.fc = nn.Linear(hidden_size, 1)
def forward(self, x):
"""
前向传播函数
参数:
x (torch.Tensor): 输入张量,形状为(batch_size, seq_len, input_size)
返回:
torch.Tensor: 预测值张量,形状为(batch_size, 1)
"""
# 将输入通过LSTM层,返回输出和隐藏状态(h_n, c_n)
out, (h_n, c_n) = self.lstm(x)
# out 形状: (batch, seq_len, hidden_size)
# 取LSTM最后一个时间步的输出,形状为(batch, hidden_size)
last_out = out[:, -1, :] # (batch, hidden_size)
# 通过全连接层将输出映射为单个预测值,形状为(batch, 1)
prediction = self.fc(last_out) # (batch, 1)
# 返回预测结果
return prediction
# 实例化LSTM模型,输入特征1维,隐藏层32维,1层LSTM
model = LSTMPredictor(input_size=1, hidden_size=32, num_layers=1)
# 打印模型结构信息
print(model)
# 计算模型总参数量
total_params = sum(p.numel() for p in model.parameters())
# 打印模型总参数量
print(f"参数量: {total_params}")
# ------------------ 4. 训练模型 ------------------
# 定义损失函数为均方误差(MSE),适用于回归任务
criterion = nn.MSELoss()
# 创建Adam优化器,学习率为0.01
optimizer = optim.Adam(model.parameters(), lr=0.01)
# 定义训练轮数为100轮
EPOCHS = 100
# 初始化列表,用于记录每轮的训练损失
train_losses = []
# 遍历每一轮训练
for epoch in range(EPOCHS):
# 将模型设置为训练模式(启用Dropout和BatchNorm)
model.train()
# 前向传播:将训练数据输入模型,得到预测输出
outputs = model(X_train_t)
# 计算预测输出与真实值之间的损失
loss = criterion(outputs, y_train_t)
# 清空梯度,防止梯度累积
optimizer.zero_grad()
# 反向传播:计算各参数的梯度
loss.backward()
# 更新模型参数
optimizer.step()
# 记录当前轮次的损失值
train_losses.append(loss.item())
# 每20轮打印一次训练信息
if (epoch + 1) % 20 == 0:
# 打印当前轮次和损失值
print(f"Epoch {epoch + 1:3d}/{EPOCHS} Loss: {loss.item():.6f}")
# ------------------ 5. 预测 & 可视化 ------------------
# 将模型设置为评估模式(关闭Dropout等)
model.eval()
# 禁用梯度计算,节省内存和加速
with torch.no_grad():
# 对训练集进行预测,并转换为NumPy数组
y_train_pred = model(X_train_t).numpy()
# 对测试集进行预测,并转换为NumPy数组
y_test_pred = model(X_test_t).numpy()
# 反归一化回原始价格
# 将训练集真实值从归一化还原为原始价格
y_train_orig = scaler.inverse_transform(y_train.reshape(-1, 1))
# 将测试集真实值从归一化还原为原始价格
y_test_orig = scaler.inverse_transform(y_test.reshape(-1, 1))
# 将训练集预测值从归一化还原为原始价格
y_train_pred_orig = scaler.inverse_transform(y_train_pred)
# 将测试集预测值从归一化还原为原始价格
y_test_pred_orig = scaler.inverse_transform(y_test_pred)
# 绘图
# 创建一个尺寸为14×5的图形窗口
plt.figure(figsize=(14, 5))
# 创建1行2列的子图,当前为第1个子图
plt.subplot(1, 2, 1)
# 绘制训练损失曲线
plt.plot(train_losses)
# 设置子图标题为"训练损失"
plt.title("训练损失")
# 设置X轴标签为"Epoch"
plt.xlabel("Epoch")
# 设置Y轴标签为"MSE Loss"
plt.ylabel("MSE Loss")
# 显示网格线
plt.grid(True)
# 当前为第2个子图
plt.subplot(1, 2, 2)
# 绘制训练集真实值,蓝色
plt.plot(y_train_orig, label="真实(训练)", color="blue", alpha=0.7)
# 绘制训练集预测值,红色虚线
plt.plot(y_train_pred_orig, label="预测(训练)", color="red", alpha=0.7, linestyle="--")
# 绘制测试集真实值,绿色
plt.plot(range(len(y_train_orig), len(y_train_orig) + len(y_test_orig)),
y_test_orig, label="真实(测试)", color="green", alpha=0.7)
# 绘制测试集预测值,橙色虚线
plt.plot(range(len(y_train_pred_orig), len(y_train_pred_orig) + len(y_test_pred_orig)),
y_test_pred_orig, label="预测(测试)", color="orange", alpha=0.7, linestyle="--")
# 绘制垂直虚线,分隔训练集和测试集
plt.axvline(x=len(y_train_orig), color="black", linestyle=":", alpha=0.5)
# 设置子图标题为"预测 vs 真实"
plt.title("预测 vs 真实")
# 设置X轴标签为"时间步"
plt.xlabel("时间步")
# 设置Y轴标签为"价格"
plt.ylabel("价格")
# 显示图例
plt.legend()
# 显示网格线
plt.grid(True)
# 自动调整子图参数,避免重叠
plt.tight_layout()
# 显示图形
plt.show()
# 评估
# 计算测试集的均方误差(MSE)
test_mse = ((y_test_orig - y_test_pred_orig) ** 2).mean()
# 打印测试集的均方根误差(RMSE),单位为元
print(f"\n测试集 RMSE: {np.sqrt(test_mse):.2f} 元")
更多推荐




所有评论(0)