🔥 热身:你觉得理解序列数据,为什么需要一种"特殊"的神经网络?

想想看:我们人看一句话的时候,是逐个字读的,而且前面的字会影响我们对后面字的理解。比如:

"我今天吃了__,味道不错。"

你觉得空白处更可能是 "苹果" 还是 "跑步"?为什么?

没错!🍎 你答对了——而且你刚刚其实已经自己发现了 LSTM 要解决的核心问题!

你想过没有——为什么不是"跑步"?因为:

  • "吃" 这个词告诉我们,后面大概率跟的是食物
  • "味道不错" 进一步确认了是吃的东西

也就是说:一个词的含义,依赖于它前面的上下文。


🤔 那问题来了:普通的前馈神经网络能处理这个吗?

让我画个简单的对比:

普通神经网络(前馈):

输入 → [一个固定窗口] → 输出

就像你拍照——一张照片只包含那一瞬间的信息,前面发生了什么它不知道。

序列数据(文字、股价、语音) 的特点是:

  • 今天的股价 ← 受昨天、前天……的影响
  • 这句话末尾的词 ← 受前面十几个词的影响

🧠 所以,人们先发明了 RNN(循环神经网络)

RNN 的想法很巧妙——给它加一个"记忆盒":

这样每一时刻,它都把上一时刻的信息传下来。


⚠️ 但 RNN 有个大问题……

来,我用一个简单的例子考考你,假设 RNN 在逐字阅读这句话: 

"我出生在法国……(中间隔了 50 个字)……我会说流利的_____"

你觉得 RNN 读到最后一个空时,还能记得前面说的 "法国" 吗?还是会忘掉?

答案是会!

👏 这就是 RNN 著名的 "长期依赖问题"——信息传着传着就衰减了,就像你在一个嘈杂的聚会上,有人从远处喊话,传到你耳边时已经听不清了。


🎯 这时候 LSTM 闪亮登场!

LSTM(长短期记忆网络)就是 RNN 的 "升级版",它专门解决"记不住长距离信息"的问题。

怎么做到的呢?我用一个非常直观的类比来解释:

🚪 想象你是看门人,管理一个"记忆仓库"

LSTM 的核心是一个细胞状态(Cell State)——你可以把它想象成一条传送带,信息沿着这条带子一直往前传,几乎不衰减。

而 LSTM 有三个门来控制信息:

让我用整理书桌的例子帮你理解:

类比 做什么
遗忘门  决定扔掉桌上的哪些旧草稿纸 删除不再需要的历史信息
输入门  决定把哪些新笔记贴到墙上 把重要的新信息存入细胞状态
输出门  决定今天跟别人说什么 基于当前的记忆,输出有用的信息

💡 直观例子:还是刚才那句话

"我出生在法国……(中间 50 个字)……我会说流利的____"

普通 RNN 的做法:一个字一个字传记忆 → 传到第 50 个字时,"法国"的信息已经衰减到快要没了 → ❌ 猜不出来

LSTM 的做法:

  1. 遗忘门:中间的那些描述性文字(比如"我小时候喜欢踢足球")→ 🗑️ 扔掉大部分
  2. 输入门:发现新信息里没有比"法国"更重要的地点 → 不覆盖
  3. 传送带上"法国"这条信息一直保留着 → ✅ 到了空白处,能想起填"法语"或"法国话"

二者区别在于:

RNN LSTM
记忆方式 📦 一个小盒子,东西多了就塞不下 🚚 一条传送带,可以长途运输
长距离记忆 ❌ 传着传着就衰减了 ✅ 几乎不衰减
控制机制 没有"门",被迫全部记或全部忘 🚪 三个门,主动选择记什么、忘什么

所以更精确地说,LSTM 的绝招是——可以选择性地记住重要信息,忘记不重要的信息。


🔧 现在是时候看看实践层面的东西了

选择时间序列预测(天气预报、股价预测)——这是 LSTM 最经典的应用场景之一。

🧱 进入第二层:LSTM 的内部到底发生了什么?

刚才用"三扇门"的类比给了你一个整体画面。现在我们来稍微深入一点点,看看数学上每扇门具体是怎么工作的。

别怕,我会用最直观的方式来解释,不讲复杂的公式推导。


🚪 遗忘门:决定"要不要忘记"

假设我们现在正在做股价预测:

输入序列:昨天的股价、前天的股价、大前天的股价……

当前时刻:t

遗忘门做的事情是——看一眼"当前输入"和"上一时刻的输出",然后给细胞状态里的每条信息打一个 0~1 之间的分数

  • 1 = "完全保留这条信息" ✅
  • 0 = "完全删除这条信息" ❌

现实例子:假设公司突然换了 CEO,那么旧的"CEO 偏好"信息对预测股价就没有用了 → 遗忘门会给它打接近 0,把它从传送带上删除。


📝 输入门:决定"要记住什么"

输入门做两件事:

  1. 挑重点:看看当前的新数据(比如今天的股价、成交量)里,哪些是值得记住的
  2. 写入传送带:把这些重要的新信息存到细胞状态里

现实例子:今天发布了超预期的财报 → 输入门会说:"这条很重要,记到传送带上!"📈


🎤 输出门:决定"要输出什么"

最后,基于当前更新后的细胞状态,输出门决定:

  • "好的,基于我记住的所有信息(过去的 + 新的),现在我要输出什么预测结果?"

现实例子:你综合了"历史走势 + 财报利好 + 行业趋势",然后输出预测——"明天股价大概率上涨 2%" 

一张图总结:


🤔 检查理解

现在我想确认你是否真的理解了三扇门的分工,来做个简单的小练习:

场景:你用 LSTM 预测某只股票的价格。

  • 第 1 天:股价 100 元
  • 第 2 天:股价 102 元 📈
  • 第 3 天:发布消息说"公司财务造假" 💥

问题:当 LSTM 处理到第 3 天的数据时,关于第 1、2 天的股价信息,遗忘门会怎么做?输入门又会怎么做?

A.遗忘门保留旧信息,输入门记造假消息(旧数据还有用+新数据很重要)

B.遗忘门删除旧信息,输入门记造假消息(旧数据没用了+新数据很重要)

C.遗忘门保留旧信息,输入门不记造假消息(旧数据有用+造假不重要)

#正确答案是B

  • 遗忘门 🗑️:财务造假这种颠覆性消息出来后,旧的股价走势(100→102)已经没参考价值了 → 删除旧信息
  • 输入门 📝:造假消息极其重要 → 写入传送带

你已经开始像 LSTM 一样思考了!👏


🎯 接下来:从原理到实操

现在你对 LSTM 的原理已经有了清晰的直觉理解。下一步,我们来看看如何在实际项目中使用 LSTM,具体步骤:

  1. LSTM 的数据准备:时间序列数据怎么喂给模型?(滑动窗口、归一化等)
  2. 用 PyTorch / TensorFlow 搭建 LSTM:看一段真实代码
  3. 调参实战:隐藏层大小、层数、学习率等

第一步:数据准备 🧹

在把数据喂给 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),这说明模型 ____?

过拟合

解决过拟合的常用招数(按推荐顺序):

  1. 🎛️ 减小 hidden_size — 降低模型容量,让它没法背下噪音
  2. ⏳ 减小 epochs — 在它"背答案"之前就停止训练(早停法)
  3. 📊 增大训练数据量 — 更多数据 = 更难死记硬背
  4. 🛡️ 加 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} 元")

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐