第04回 记事的门派——RNN 与 LSTM/GRU
字字相随如走马,一招一式续前缘。
若无心法留余火,转眼长情化断烟。
上回我们让词“认亲”——嵌入把“吃饭”和“用餐”拉到一桌,语义终于不再陌路。
可看官很快又发现新麻烦:
“词有了向量,句子怎么办?‘我 不 爱 你’只多一个‘不’,意思却翻天覆地。词袋不看顺序,岂不误事?”
正是。语言是“按顺序发生的事”。要读懂一句话,模型得学会“记事”。
这便轮到本回主角登台:循环神经网络(RNN),以及它的两位名门弟子:LSTM 与 GRU。
一、RNN 是什么:一盏灯接一盏灯,走到哪算到哪
RNN 的江湖形象,最像“走夜路提灯”。
你读一个词,灯里就多一缕光;再读下一个词,带着上一刻的光继续走。
把这“灯里的光”叫作隐藏状态 。它每一步都更新一次:
这里 是第 个词的向量(上回的嵌入), 是同一套更新规则,步步复用。
RNN 的好处一眼就明白:
- 顺序保住了:先看“我”,再看“不”,再看“爱”,顺序不同,状态就不同
- 计算省:每步只和上一状态打交道,推理时是线性的
可 RNN 也有老毛病:记性会散。句子一长,早先的信息像风里烛火,越吹越弱,这就是常说的“长期依赖难题”(背后牵涉到梯度在长链路上衰减/爆炸的现象,你只需记住直觉:太长就记不牢)。
于是江湖就发明了“带门的记忆匣子”:LSTM 与 GRU。
二、LSTM:三道门,一只匣,专治“记不住”
LSTM 可以理解成:
在 RNN 的“灯”旁边,再配一只更稳的“记忆匣”(常被写作 ),并用几道“门”来决定:
- 哪些旧记忆该忘
- 哪些新信息该收
- 最后把哪部分拿出来用
你不用背公式,把它当作三道门的规矩即可:
- 忘门:旧事有的该放下
- 收门:新事有的要记牢
- 出门:对外说话时,挑合适的记忆上台
所以 LSTM 像老账房:
不是所有流水都往册子里抄,而是“该记的记,该扔的扔”,于是能撑更长的叙事。
2024 年还有人专门把 LSTM 这套门派再扩展,提出 xLSTM,用更强的门控与结构改造,去争取更好的长序列能力与可扩展性。1
三、GRU:两道门,轻装上阵
GRU 是 LSTM 的“轻功版”:
门少一点,结构简一点,训练常更利索。
你可以把它看作:
- 仍然有“要不要更新”的门
- 也有“要不要忘”的门
- 但不再单独拎出一只 记忆匣,整体更紧凑
在很多实际任务里,GRU 与 LSTM 表现接近,选择更多取决于:
- 速度与资源
- 数据规模
- 任务是否确实需要很长的记忆
四、2024–2026 的回潮:RNN 为何又被人惦记
看官可能听说过一句江湖传闻:
“Transformer 之后,RNN 不是退隐了吗?”
其实近两年(尤其 2024 起),对“非注意力序列模型”的兴趣大涨:
一方面是长上下文成本太高;另一方面是大家开始重新打磨“线性复杂度”的路线。
这股风里,有几条支流与你本回所学能对上眼:
- xLSTM:把 LSTM 的门控与记忆结构做现代化扩展1
- “Were RNNs All We Needed?”:从历史与算法相似性出发回看 LSTM/GRU,并提出更“精简”的 RNN 变体,强调训练/推理效率的潜力2
- RWKV:把“可并行训练”和“RNN 式线性推理”揉成一体,被视为“Transformer 时代的 RNN 路线”之一3
你无需在本回就吃透这些新派,只要抓住主线:
RNN 的“状态”是个很有价值的容器。后面我们讲 Mamba/SSM 时,会再次见到“用状态承载历史”的思想。
五、极简代码:用 LSTM 做一句话情感分类(PyTorch 可跑)
下一回要做情感分析实战,本回先把“会记事的模型”练起来。
下面这段代码用一个玩具数据集演示:
把一句话(分词后)输入 LSTM,取最后一步的状态做二分类。
import random
import torch
import torch.nn as nn
import torch.nn.functional as F
def build_vocab(samples):
vocab = {"<pad>": 0, "<unk>": 1}
for text, _ in samples:
for w in text.split():
if w not in vocab:
vocab[w] = len(vocab)
return vocab
def encode(text, vocab, max_len):
ids = [vocab.get(w, vocab["<unk>"]) for w in text.split()]
ids = ids[:max_len]
ids += [vocab["<pad>"]] * (max_len - len(ids))
return ids
class LSTMClassifier(nn.Module):
def __init__(self, vocab_size, dim=32, hidden=64, num_classes=2):
super().__init__()
self.emb = nn.Embedding(vocab_size, dim, padding_idx=0)
self.lstm = nn.LSTM(input_size=dim, hidden_size=hidden, batch_first=True)
self.fc = nn.Linear(hidden, num_classes)
def forward(self, x):
e = self.emb(x)
out, _ = self.lstm(e)
last = out[:, -1, :]
return self.fc(last)
if __name__ == "__main__":
torch.manual_seed(0)
random.seed(0)
samples = [
("这 家 店 真 好吃", 1),
("味道 很 棒 还 会 再 来", 1),
("太 难吃 了 不 会 再 来", 0),
("服务 很 差 失望", 0),
("好吃 便宜 推荐", 1),
("踩 雷 了 真 糟糕", 0),
]
vocab = build_vocab(samples)
max_len = 8
x = torch.tensor([encode(t, vocab, max_len) for t, _ in samples], dtype=torch.long)
y = torch.tensor([label for _, label in samples], dtype=torch.long)
model = LSTMClassifier(vocab_size=len(vocab))
opt = torch.optim.Adam(model.parameters(), lr=0.03)
for step in range(200):
logits = model(x)
loss = F.cross_entropy(logits, y)
opt.zero_grad()
loss.backward()
opt.step()
with torch.no_grad():
pred = model(x).argmax(dim=1)
acc = (pred == y).float().mean().item()
print("acc:", round(acc, 3))
for (t, label), p in zip(samples, pred.tolist()):
print(f"text={t} label={label} pred={p}")
这段“玩具练功”当然不代表真实效果,但它把关键流程走全了:
- 词 → id → embedding
- embedding 序列 → LSTM 逐步更新状态
- 取最后状态 → 分类
下一回我们会把数据、特征与评估讲得更像“实战”,让你知道:
模型不仅要会练,还得会打。
六、小结:本回解了“顺序”,下回要讲“如何评卷”
本回把“句子为什么要按顺序处理”讲清了:
- RNN:一步一步走,状态携带历史
- LSTM:加门加匣,专治记不住
- GRU:轻装版本,常更省更快
同时也埋下一颗伏笔:
2024–2026 许多“后 Transformer 路线”,仍在围绕“状态如何承载更长历史”做文章。
可江湖规矩是:武功要看实战。
下一回我们就拿“情感分析”当擂台——
不只讲模型,还讲数据怎么切、怎么评、怎么避免自欺欺人。
欲知后事如何,且听下回分解。
引用与溯源
Footnotes
-
Beck, M., et al. xLSTM: Extended Long Short-Term Memory arXiv:2405.04517(2024-05,v2 2024-12)https://arxiv.org/abs/2405.04517 ↩ ↩2
-
Were RNNs All We Needed? arXiv:2410.01201(2024-10)https://arxiv.org/abs/2410.01201 ↩
-
Peng, B., et al. RWKV: Reinventing RNNs for the Transformer Era arXiv:2305.13048(2023-05)https://arxiv.org/abs/2305.13048 ↩