小刘檀木的大模型日记 · Day16 · RNN、LSTM、GRU-帮普通人把AI学进简历系列

Day16|RNN、LSTM、GRU:大模型时代,为什么还要懂这三个老朋友

封面:系列统一封面 大模型.png

前言:别急着把 RNN 扔进历史垃圾桶

先抛结论:如果你只会说 Transformer 很强,却说不清 RNN、LSTM、GRU 解决过什么问题,你对序列建模的理解大概率是空心的。

这话有点扎心,但很真实。

现在一聊 AI,大家张口闭口都是 attention、MoE、Agent、长上下文。RNN 像个从老项目里翻出来的类库,名字熟,没人想碰。

可我一路摸爬滚打下来,越来越觉得:老模型不是废铁。它们更像技术史里的路标,告诉你一个问题是怎么被一层一层拆开的。

今天这篇,我们不背公式,也不搞论文复读。就围绕一个问题讲清楚:

当输入是一条时间线,模型到底怎么记住过去?

时间序列逐步流过隐藏状态的示意图


PART 01:RNN 的直觉——每一步都带着上一秒的影子

先从最朴素的 RNN 讲起。

普通神经网络看输入,像看一张照片:这一刻给什么,它就算什么。可很多数据不是照片,是一段录像。

比如:

  • - 用户今天、昨天、前天的点击序列
  • - 一句话里第 1 个字、第 2 个字、第 3 个字
  • - 传感器每秒上传一次的温度曲线

这些数据的关键不只在“当前值”,还在前后顺序

RNN 的想法很简单:每一步都吃两个东西。

一个是当前输入 x_t,一个是上一时刻留下来的隐藏状态 h_{t-1}。然后它产出新的隐藏状态 h_t,继续传给下一步。

你可以把 h_t 理解成一句话:我读到现在为止,脑子里留下的摘要。

RNN 循环展开结构图

这就是 RNN 最迷人的地方:同一套参数,沿着时间一步一步复用。序列多长,它就展开多长。

上一个最小可运行代码,感受一下维度:

import torch
import torch.nn as nn

batch_size = 2
seq_len = 5
input_size = 3
hidden_size = 4

x = torch.randn(batch_size, seq_len, input_size)

rnn = nn.RNN(
    input_size=input_size,
    hidden_size=hidden_size,
    batch_first=True,
)

output, h_n = rnn(x)

print(output.shape)  # torch.Size([2, 5, 4])
print(h_n.shape)     # torch.Size([1, 2, 4])

output 是每个时间步的隐藏状态,h_n 是最后一步的隐藏状态。

如果你做一个很短的序列任务,比如“最近 5 次点击预测下一次点击”,RNN 这个思路挺自然。

但问题也在这里。

RNN 把所有历史都压进一个隐藏状态里,就像让你把一整本书压缩成一张便利贴。前几页的重要信息,传着传着就淡了。

这就是经典的长期依赖问题

更底层一点,是训练时的梯度消失 / 梯度爆炸。序列一长,梯度要穿过很多步反向传播,乘来乘去,要么小到几乎没信号,要么大到训练发疯。

所以 RNN 的短板不是“它完全不会记忆”,而是:

它能记住近处的影子,却很难稳稳抓住远处的因果。


PART 02:LSTM 的三扇门——让模型学会忘记和保留

LSTM 出场,就是为了解决 RNN 的这个记忆焦虑。

它做了一件很聪明的事:不再把所有记忆都塞进隐藏状态,而是额外开了一条“长期记忆高速路”,叫 cell state

你可以把普通 RNN 想成一个人边走边背书,所有内容都靠脑子硬扛。

LSTM 则像带了一个笔记本:有些信息写进长期笔记,有些信息当场丢掉,有些信息只拿出来影响当前输出。

它靠三扇门完成这件事:

  • 遗忘门 forget gate:过去的信息,哪些该删掉?
  • 输入门 input gate:当前的新信息,哪些该写进去?
  • 输出门 output gate:长期记忆里,哪些该拿出来影响当前判断?

LSTM 三扇门与长期记忆高速路示意图

这三个门听起来玄,其实工程味很重。

做业务系统久了你就知道,信息管理最难的不是“多记”,而是该记的记住,该忘的忘掉

比如做用户行为预测,用户三个月前买过一次键盘,这个信息可能还有用;但他 10 秒前误点了一个广告,可能就不该被长期记住。

LSTM 让模型自己学这个取舍。

同样用 PyTorch,换一行模型就能跑:

import torch
import torch.nn as nn

x = torch.randn(2, 5, 3)

lstm = nn.LSTM(
    input_size=3,
    hidden_size=4,
    batch_first=True,
)

output, (h_n, c_n) = lstm(x)

print(output.shape)  # torch.Size([2, 5, 4])
print(h_n.shape)     # torch.Size([1, 2, 4])
print(c_n.shape)     # torch.Size([1, 2, 4])

注意这里多了一个 c_n,它就是 LSTM 的 cell state。

这也是 LSTM 和普通 RNN 最关键的差别:隐藏状态负责当前表达,细胞状态负责长期记忆。

当然,LSTM 也不是免费午餐。门多了,参数也多,训练更慢,调参更烦。

但在 Transformer 统治世界之前,LSTM 长期是文本、语音、时间序列里的主力选手,不是因为大家怀旧,而是因为它确实把“长一点的记忆”这件事做得更稳。


PART 03:GRU 的取舍——少一扇门,快一点,够用很多场景

讲完 LSTM,再看 GRU 就很顺了。

GRU 可以理解成 LSTM 的轻量版。它觉得:三扇门是不是有点复杂?长期状态和隐藏状态是不是可以合并?

于是 GRU 只保留两扇门:

  • 更新门 update gate:过去的信息保留多少,新信息写入多少?
  • 重置门 reset gate:计算当前候选状态时,要不要忽略一部分过去?

它没有单独的 cell state,结构更简单,参数更少,训练通常也更快。

所以在很多工程场景里,GRU 是一个很务实的选择。

不是最豪华,但够用、快、成本低。

import torch
import torch.nn as nn

x = torch.randn(2, 5, 3)

gru = nn.GRU(
    input_size=3,
    hidden_size=4,
    batch_first=True,
)

output, h_n = gru(x)

print(output.shape)  # torch.Size([2, 5, 4])
print(h_n.shape)     # torch.Size([1, 2, 4])

你看,GRU 的返回又回到了 output, h_n,没有额外的 c_n

那项目里到底怎么选?

我给一个不玄学的版本:

  • - 序列很短、逻辑简单:RNN 可以作为 baseline
  • - 有较长依赖、数据量还可以:优先试 LSTM
  • - 资源敏感、延迟敏感、小数据:GRU 常常更划算
  • - 长文本理解、多轮复杂推理、大规模预训练:交给 Transformer

RNN LSTM GRU 与 Transformer 的工程选型树

这也是很多人容易误解的一点。

Transformer 不是因为“会记忆”才强,而是因为它用 attention 让序列里的任意位置可以直接互相看见。RNN 家族是一步一步传话,Transformer 更像把全班同学同时拉进一个会议室。

传话有传话的成本优势,开会有开会的全局视野。

真正成熟的工程判断,不是“新的一定替代旧的”,而是知道每个工具的代价。


结尾:旧模型不是废铁,是地基

RNN、LSTM、GRU 这三个名字,今天看起来确实不够时髦。

但你把它们串起来,会看到一条非常清晰的技术演进线:

RNN 解决“序列怎么读”,LSTM 解决“长期怎么记”,GRU 解决“能不能更轻一点”。

这条线走通了,再回头看 Transformer 的 self-attention,你会更明白它为什么像一次范式跃迁。

大模型时代,旧模型不是废铁,而是地基。地基越稳,楼才敢盖得越高。

互动时间

你第一次接触 RNN / LSTM / GRU 是在 NLP、时间序列,还是推荐系统里?评论区聊聊,我想看看大家踩过哪些“序列建模”的坑。


下一篇预告:Day17 准备讲 Self-Attention 和 Transformer——为什么它能绕开 RNN 的长距离传话问题,又为什么会成为大模型的骨架。关注「小刘檀木」,别错过。

— END —

小刘檀木 · 帮普通人把 AI 学进简历

Logo

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

更多推荐