从 RNN 到 LSTM 再到 BiLSTM:一文搞懂 NLP 核心序列模型原理
NLP-AHU-166
引言
在自然语言处理(NLP)的世界里,我们处理的几乎都是序列数据—— 一句话是由词按顺序组成的,一段文本是由句子串联而成的,就连语音信号也是按时间顺序排列的数值序列。普通的神经网络(比如全连接网络)有个致命问题:它完全无视数据的顺序关系,把输入当成独立的个体去处理,根本没法理解 “词 A 在词 B 前面” 这种序列语义。
而RNN、LSTM、BiLSTM就是为解决序列数据处理而生的三大核心模型,也是 NLP 入门的必学基础。接下来我会从 “为什么要设计它们”“具体怎么设计”“数学公式怎么推导” 三个维度,一步步拆解这三个模型的原理,尽量用初学者能看懂的通俗语言讲清楚。
一、RNN:循环神经网络,序列处理的 “初代尝试”
1. 设计动机
普通神经网络的输入输出都是固定长度的,比如输入一张 28×28 的图片,输出就是 10 个分类概率。但序列数据的长度是不固定的 —— 一句话可能有 5 个词,也可能有 20 个词,这就需要模型能 “记住” 之前的信息,同时适配不同长度的输入。
RNN 的核心设计思路就是:让隐藏层保留 “记忆” ,把上一个时刻的隐藏状态传递到下一个时刻,这样模型就能结合 “当前输入” 和 “历史记忆” 处理序列数据。简单说,RNN 就像一个 “边走边记笔记” 的学习者,遇到新内容会结合之前的笔记理解,而不是从零开始。
2. 核心结构与数学表达
RNN 的核心是循环连接,它的网络结构可以拆解为三个部分:输入层、隐藏层、输出层。
- 输入层:处理序列中的每个元素(比如一句话里的每个词,会先转换成词向量xt)。
- 隐藏层:这是 RNN 的核心,负责存储 “历史记忆”,并更新记忆状态。
- 输出层:根据当前时刻的隐藏状态,输出当前的预测结果(比如词性标注、下一个词的概率)。
关键公式(隐藏层更新)
假设序列中第t个时刻的输入为xt,第t−1个时刻的隐藏状态为ht−1(也就是历史记忆),第t个时刻的隐藏状态ht(更新后的记忆)计算公式为:
其中各参数的含义:
- Wxh:输入层到隐藏层的权重矩阵,负责将当前输入xt映射到隐藏层维度
- Whh:上一时刻隐藏层到当前时刻隐藏层的权重矩阵,负责传递历史记忆;
- bh:隐藏层的偏置项,增加模型的拟合能力;
- tanh:激活函数,将隐藏层的输出映射到[−1,1]区间,同时引入非线性,让模型能学习复杂的序列规律。
输出层的公式也很简单,以分类任务为例(比如情感分析):
- Who:隐藏层到输出层的权重矩阵;
- bo:输出层的偏置项;
- softmax:将输出值转换为概率分布,让每个类别的概率之和为 1。
3. 致命缺点:梯度消失 / 爆炸(长期依赖问题)
RNN 看似解决了序列处理的问题,但实际用起来却有个大漏洞 ——长期依赖问题。
简单说,当序列长度很长时(比如一句话有 100 个词),RNN 在反向传播计算梯度时,梯度会随着时间步的增加呈指数级衰减(梯度消失)或爆炸(梯度爆炸)。这会导致模型 “记不住” 很久之前的信息,比如一句话 “我童年住在北京,后来去了上海,现在还记得____的胡同”,模型很难准确填出 “北京”,因为它把前面的记忆 “丢了”。
这就是为什么需要 LSTM 来优化 RNN—— 专门解决长期依赖问题。
二、LSTM:长短期记忆网络,解决长期依赖的 “升级版 RNN”
1. 设计动机
LSTM 的全称是 Long Short-Term Memory,它在 RNN 的基础上做了核心改进:引入 “门控机制” 和 “细胞状态” ,精准控制信息的 “遗忘”“输入” 和 “输出”,让模型能选择性地保留重要信息,丢弃无用信息,从而解决长期依赖问题。
如果说 RNN 是一个 “记满所有笔记的笔记本”,那 LSTM 就是一个 “智能笔记本”—— 会主动扔掉没用的笔记,只保留关键内容,还能随时补充新笔记,最后只输出需要的部分。
2. 核心结构:细胞状态 + 三大门控
LSTM 的核心是细胞状态(Cell State) ,可以理解为模型的 “长期记忆通道”,信息在这个通道里流动时,只会经过少量的线性变换,避免梯度消失。
而控制信息流动的关键是三大门控(门控本质上是一个带 sigmoid 激活的全连接层,输出 0~1 之间的数值,0 表示 “完全丢弃信息”,1 表示 “完全保留信息”),分别是:
- 遗忘门(Forget Gate) :决定丢弃细胞状态中哪些旧信息;
- 输入门(Input Gate) :决定将哪些新信息存入细胞状态;
- 输出门(Output Gate) :决定从细胞状态中输出哪些信息作为当前时刻的隐藏状态。
3. 详细数学公式
接下来逐个拆解 LSTM 的公式,结合通俗解释会更好理解。
第一步:计算遗忘门
遗忘门的作用是 “筛选历史记忆”,它会结合当前输入xt和上一时刻隐藏状态ht−1,输出一个遗忘概率向量ft(每个元素都在 0~1 之间)。
- σ:sigmoid 激活函数,输出 0~1 的概率;
- Wxf、Whf:分别是输入、上一时刻隐藏状态到遗忘门的权重;
- bf:遗忘门的偏置。
第二步:计算输入门
输入门分两步:先决定 “哪些新信息要存入”,再生成 “新的候选信息”。
① 生成输入门概率it:
② 生成候选新信息Ct(用 tanh 激活输出 - 11 之间的数值):
第三步:更新细胞状态(重点)
用 “遗忘的旧信息”+“新增的新信息”,更新细胞状态Ct:
- 这里是逐元素相乘:ft⋅Ct−1 表示按遗忘概率丢弃旧记忆,it⋅C~t 表示按输入概率添加新记忆,两者相加就是更新后的长期记忆。
第四步:计算输出门
先决定 “输出哪些记忆”,再结合细胞状态生成当前时刻的隐藏状态ht:
① 生成输出门概率
② 生成当前隐藏状态ht:
- ot⋅tanh(Ct) 表示按输出概率筛选细胞状态中的信息,只输出需要的部分作为当前时刻的隐藏状态,传递给下一个时刻。
4. 为什么 LSTM 能解决长期依赖?
LSTM 的细胞状态Ct是线性流动的,只有少量的逐元素相乘和相加操作,反向传播时梯度不会轻易消失 / 爆炸。而三大门控让模型能精准控制信息的流动,比如对重要的历史信息(如句子中的主语),门控会输出接近 1 的概率,保留该信息;对无关的信息(如语气词),输出接近 0 的概率,丢弃该信息。
这样一来,即使序列长度很长,LSTM 也能牢牢记住关键信息,完美解决了 RNN 的长期依赖问题。
三、BiLSTM:双向长短期记忆网络,兼顾上下文的 “升级版 LSTM”
1. 设计动机
LSTM 虽然解决了长期依赖,但它是单向的—— 只能从左到右处理序列,只能利用 “前文信息”,看不到 “后文信息”。
但在很多 NLP 任务中,上下文信息对理解语义至关重要。比如一句话 “苹果公司发布了新款手机,它的屏幕很大”,要理解 “它” 指的是 “新款手机”,不仅需要看前面的 “苹果公司发布了新款手机”,还需要看后面的 “屏幕很大”。
BiLSTM 的设计思路就是:拼接两个单向 LSTM,一个从左到右处理序列(前向 LSTM),一个从右到左处理序列(后向 LSTM),这样每个时刻的输出都能结合 “前文 + 后文” 的信息,让语义理解更全面。
2. 核心结构:前向 + 后向 LSTM 拼接
BiLSTM 的结构可以拆解为两部分:
- 前向 LSTM:按t=1,2,...,T的顺序处理序列,得到前向隐藏状态ht→;
- 后向 LSTM:按t=T,T−1,...,1的逆序处理序列,得到后向隐藏状态ht←;
- 拼接层:将每个时刻的ht→和ht←拼接起来,得到 BiLSTM 的最终输出ht。
3. 数学表达
前向 LSTM 的隐藏状态计算
和普通 LSTM 的计算逻辑一致,只是顺序是从左到右:
后向 LSTM 的隐藏状态计算
后向 LSTM 的计算逻辑和前向一致,只是顺序是从右到左,上一时刻的隐藏状态是ht+1←:
BiLSTM 的最终输出
将前向和后向的隐藏状态拼接,得到每个时刻的最终输出:ht=[ht→;ht←]这里的;表示向量拼接,如果前向隐藏状态的维度是d,后向也是d,那拼接后的维度就是2d。
4. 适用场景
BiLSTM 特别适合需要 “上下文联动” 的 NLP 任务,比如:
- 词性标注:每个词的词性不仅和前面的词有关,也和后面的词有关;
- 命名实体识别:识别一句话中的 “人名”“地名”“机构名”,需要结合上下文判断;
- 文本分类:尤其是短文本,上下文信息对语义判断影响很大。
四、RNN、LSTM、BiLSTM 核心对比总结
RNN 是最基础的循环模型,结构简单,依靠循环连接处理序列,但存在严重的长期依赖问题,只能用于短序列和基础演示,不适合实际复杂任务。
LSTM 在 RNN 基础上加入细胞状态和三大门控,专门解决梯度消失和长期依赖问题,能够处理长序列,是后续很多序列模型的基础。
BiLSTM 则是在 LSTM 基础上实现双向结构,同时利用前文和后文信息,语义理解更全面,在命名实体识别、词性标注、文本分类等 NLP 任务中表现更好,是实际工程中更常用的结构。
最后:代码实现(PyTorch 版)
为了让大家更直观地理解 BiLSTM 的实际应用,我写了一个简易的基于 PyTorch 的 BiLSTM 文本分类示例(以情感分析为例),代码里加了详细注释,大家能直接看懂。
python
运行
import torch
import torch.nn as nn
import torch.optim as optim
from torchtext.vocab import build_vocab_from_iterator
from torchtext.data.utils import get_tokenizer
# 1. 定义超参数
VOCAB_SIZE = 10000 # 词汇表大小
EMBED_DIM = 128 # 词嵌入维度
HIDDEN_DIM = 256 # LSTM隐藏层维度
NUM_CLASSES = 2 # 情感分析二分类(正面/负面)
BATCH_SIZE = 32
EPOCHS = 5
# 2. 定义分词器和词汇表
tokenizer = get_tokenizer("basic_english") # 英文基础分词器
# 这里可以替换成你的数据集,示例用简单的文本
texts = [
"I love this movie, it's amazing!",
"This film is terrible, I want to leave.",
"The acting is great, highly recommend it.",
"The plot is boring, waste of time."
]
labels = [1, 0, 1, 0] # 1=正面,0=负面
# 构建词汇表
def yield_tokens():
for text in texts:
yield tokenizer(text)
vocab = build_vocab_from_iterator(yield_tokens(), max_tokens=VOCAB_SIZE, specials=["<unk>"])
vocab.set_default_index(vocab["<unk>"]) # 未知词映射为<unk>
# 3. 定义BiLSTM模型
class BiLSTMClassifier(nn.Module):
def __init__(self, vocab_size, embed_dim, hidden_dim, num_classes):
super(BiLSTMClassifier, self).__init__()
# 词嵌入层
self.embedding = nn.Embedding(vocab_size, embed_dim)
# BiLSTM层:bidirectional=True表示双向,batch_first=True表示输入格式为[batch, seq_len]
self.bilstm = nn.LSTM(
input_size=embed_dim,
hidden_size=hidden_dim,
num_layers=1,
bidirectional=True,
batch_first=True
)
# 分类层:因为双向,所以输入维度是hidden_dim*2
self.fc = nn.Linear(hidden_dim * 2, num_classes)
def forward(self, x):
# x: [batch_size, seq_len]
embed = self.embedding(x) # [batch_size, seq_len, embed_dim]
# bilstm输出:output[batch_size, seq_len, hidden_dim*2](每个时刻的输出),_是隐藏状态和细胞状态
output, _ = self.bilstm(embed)