声明:资源来源于b站黑马程序员课程,本人只是对资源进行整理和学习,希望能够帮助到大家
一. RNN模型
1.1 什么是RNN模型
RNN (Recurrent Neural Nerwork),中文称作循环神经网络,它一般以序列数据为输入,通过网络内部的结构设计有效捕获序列之间的关系特征,一般也是以序列形式输出。
- 一般的单层神经网络结构:

- RNN单层网络结构:

- 以时间步对RNN进行展开后的单层网络结构:

RNN的循环机制使模型隐层上一时间步产生的结果,能够作为当下时间步输入的一部分(当下时间步的输入除了正常的输入外还包括上一步的隐层输出)对当下时间步的输出产生影响.
1.2 RNN模型的作用
因为循环神经网络(RNN)的结构特性,让它很擅长利用序列数据里前后元素之间的关联关系,所以对于自然界中具有连续性的输入序列(比如人类的语言、语音等),它能进行很出色的处理,因此被广泛应用在NLP领域的各项任务中,像文本分类、情感分析、意图识别、机器翻译等。
下面这张图里的序列:"What""time""is""it""?",要理解这个句子的含义,我们的大脑会自然地结合时间顺序和前后词的关联------先看到"What",再结合"time"推测可能问的是"什么时间",接着通过"is""it"完善成"现在是什么时间?"。而RNN(循环神经网络)就像给模型装上了"记忆链条":它让模型在每个时间步(处理每个单词时),都能把上一时间步的隐层输出(相当于"记忆")和当前时间步的输入(当前单词)结合起来,从而捕捉序列里的先后依赖关系。比如在理解这个句子时,RNN会记住前面的"What time",再结合后续的"is it",最终明白是在问时间------这就是RNN的核心作用:利用序列的时间/顺序关联,让模型能基于"历史信息"(之前的隐层输出)理解"当前输入",从而处理像语言、语音这类具有连续性的序列数据。

1.3 RNN模型的分类
对于RNN的分类,主要从两个角度对RNN模型进行分类,第一个角度是输入和输出的结构,第二个角度是RNN内部的构造。
1.按照输入和输出的结构进行分类:
- N vs N - RNN
- N vs 1 - RNN
- 1 vs N - RNN
- N vs M - RNN
2.按照RNN的内部构造进行分类:
- 传统RNN
- LSTM
- Bi-LSTM
- GRU
- Bi-GRU
1.3.1 N vs N - RNN
"N vs N - RNN"是最典型的入门级结构。它的核心特点是输入序列和输出序列长度完全相等 ------就像图里展示的:下方的 x1,x2,x3,x4是输入的序列(比如一句话的4个单词),中间的 h1,h2,h3,h4是模型在每个时间步传递的"隐层状态"(相当于模型的"记忆"),上方的 y1,y2,y3,y4则是输出的等长序列。这种结构虽然因为"输入输出等长"的限制,适用场景相对狭窄,但却非常适合生成等长度的任务(比如写一首和原句字数相同的合辙诗句)。接下来我们就通过这个经典的"N vs N - RNN"结构示意图,拆解这个最基础的RNN是如何用"循环"的方式处理序列的。

要注意,此时的y1作为输出,h1作为隐藏层输出,y1是h1经过一个全连接层(liner层)得到的,所以这种情况并不相等。
1.3.2 N vs 1 - RNN
在处理**"输入是序列,输出是单个值"** 的任务(比如文本分类:输入一段话,输出"体育""科技"等类别)时,普通的"N vs N - RNN"(输入输出等长)就不够用了。这时候需要用到**"N vs 1 - RNN"** 结构------它的核心思路是:让RNN先对整个输入序列 (比如一段话的多个单词)做"循环处理",最后只取最后一个时间步的隐层输出 h4,再通过线性变换(+softmax/sigmoid)得到单个输出 Y。就像图里展示的:下方的 x1,x2,x3,x4是输入的序列(比如4个单词组成的句子),中间的 h1,h2,h3,h4是模型逐时间步传递的"记忆",最后用 h4经过 Softmax(Vh4+c)得到单个输出 Y(比如分类结果)。这种结构非常适合文本分类、情感分析等**"序列输入→单值输出"**的任务,因为它能先"记住"整个序列的信息,再浓缩成一个结论。接下来我们就通过这个"N vs 1 - RNN"的示意图,看看它是如何把"长序列的信息"转化为"单个输出"的。

公式中的V代表liner层,一个矩阵,c代表偏置量,softmax代表归一化。
1.3.3 1 vs N - RNN
在需要**"从单个输入生成连续序列"** 的任务中(比如图片生成描述文字、单个和弦生成音乐旋律),我们会用到**"1 vs N - RNN"** 结构。这种设计让模型在生成序列的每一个时间步 ,都能使用相同的初始输入 (比如图片特征、起始音符),结合上一步的隐层状态生成当前输出。如图所示,单个输入 X贯穿整个生成过程,依次输出 y1,y2,y3,y4等序列元素,从而在生成连续内容时保持与原始输入的一致性。下面我们通过这张"1 vs N - RNN"结构图,具体理解其运作机制。

1.3.4 N vs M - RNN
当我们需要**"序列输入→变长序列输出"** (比如机器翻译:输入英文句子,输出中文句子;语音识别:输入音频序列,输出文字序列)时,就要用到**"N vs M - RNN"** (也叫seq2seq架构 )。它的核心是编码器+解码器的两部分RNN结构:
-
编码器(左侧):把输入序列 (如英文句子的单词序列 x1,x2,x3,x4)逐步编码,最终输出一个隐含变量 c(相当于"压缩后的输入信息")。其实属于N vs 1 -RNN结构。
-
解码器(右侧):以 c为"初始记忆",逐步生成输出序列(如中文句子的字/词序列 y1,y2,y3)。其实属于1 vs N -RNN结构。
这种结构最大的优势是不限制输入输出长度(输入可以是任意长度,输出也可以是任意长度),非常适合翻译、摘要生成等任务。下面我们就通过这张seq2seq的结构示意图,拆解它是如何用"编码-解码"的方式处理变长序列的。

1.4 传统RNN模型
1.4.1 RNN结构分析

- 根据结构分析得出内部计算公式:

- 激活函数tanh的作用:用于帮组调节流经网络的值,tanh函数将值压缩在-1和1之间。

要理解RNN的核心计算逻辑,我们可以聚焦其内部结构的时间步运算流程:
RNN每个时间步 t的输入包含两部分:
-
上一时间步的隐层输出 h(t−1)(承载"历史记忆");
-
当前时间步的输入数据 x(t)(承载"当前信息")。
这两部分输入会先被拼接 成一个新张量 [x(t), h(t−1)],随后输入到一个全连接层 (线性层)中。该层使用 tanh作为激活函数,最终输出当前时间步的隐层状态 h(t)。

这个 h(t)会同时承担两个角色:
-
作为下一个时间步的输入(传递给 t+1时的 h(t)位置,延续"记忆");
-
与下一个时间步的输入 x(t+1)一起,进入下一个时间步的计算流程,循环往复。
简言之,RNN通过"拼接历史隐层+当前输入→全连接+tanh→输出新隐层(兼作下一时间步输入)"的逻辑,实现对序列信息的"循环记忆"与传递。
1.4.2 RNN代码实现
在RNN的源代码中可以看到:

虽然用的公式和上面讲述的公式看着有所不同,但其实源代码中只是将上面的公式进行了拆分实现,不要搞混淆即可:

接下来展示RNN模型的代码实现,重点牢记RNN的参数意义:
python
# -*- coding:utf-8 -*-
import torch
import torch.nn as nn
def dm1_rnn_base():
# 1.实例化模型
# RNN的参数说明:
# 第一个参数input_size:输入的词嵌入维度
# 第二个参数hidden_size:RNN单元输出的隐藏层张量的维度
# 第三个参数num_layers:有几层RNN单元(有几个隐藏层)
input_size = 5
hidden_size = 6
num_layers = 1
model = nn.RNN(input_size, hidden_size, num_layers)
# 2. 获取x0输入
# x0的参数说明
# 第一个参数sequence_len:每个样本的长度(单词的个数)(因为RNN模型batch_first=False, seq_len放在第一位置)
# 第二个参数batch_size:一个批次送入几个样本
# 第三个参数input_size:输入的词嵌入维度
sequence_len = 1
batch_size = 3
x0 = torch.randn(sequence_len, batch_size, input_size)
# 3.获取h0输入
# h0的参数说明
# 第一个参数num_layers:有几层RNN单元(有几个隐藏层)
# 第二个参数batch_size:一个批次送入几个样本
# 第三个参数hidden_size:RNN单元输出的隐藏层张量的维度
h0 = torch.randn(num_layers, batch_size, hidden_size)
# 4.将输入送给RNN模型得到下一时间步的输出结果
output, hn = model(x0, h0)
print(f'output--》{output}')
print('*'*80)
print(f'hn--》{hn}')
对于上面代码中提到的x0输入的参数中,有一点需要注意,在源码中是这样说明的:

也就是当RNN模型中的参数batch_first默认是False,则此时x0的参数顺序为(sequence_len, batch_size, input_size),,如果手动修改为Ture,则x0的参数顺序变( batch_size,sequence_len input_size),这个必须注意,下面介绍这两者的区别。
- 在参数batch_first默认为False的情况下,调整x0的参数如下(2个样本,每个样本两个单词数):

输出结果如下:


可以观察到:batch_first默认False时,hn的输出刚好和output的最后一个输出相同,刚好是对齐的!
2.在参数batch_first默认为True的情况下,x0的参数和上面一样:

输出结果为:


在这种情况下可以观察到,hn的输出和output的最后一个输出不一致,也就是没有对齐,而是交叉对齐的,这样就很不好观察,所以将batch_first默认为False的原因就是想让hn的输出与output的最后一个输出 对齐,便于观察!!
1.4.3 传统RNN的优缺点
1 传统RNN的优势
由于内部结构简单,对计算资源要求低,相比之后我们要学习的RNN变体:LSTM和GRU模型参数总量少了很多,在短序列任务上性能和效果都表现优异。
2 传统RNN的缺点
传统RNN在解决长序列之间的关联时,通过实践, 证明经典RNN表现很差,原因是在进行反向传播的时候,过长的序列导致梯度的计算异常,发生梯度消失或爆炸。
3 梯度消失或爆炸介绍
根据反向传播算法和链式法则,梯度的计算可以简化为以下公式

其中sigmoid的导数值域是固定的,在[0, 0.25]之间,而一旦公式中的w也小于1,那么通过这样的公式连乘后,最终的梯度就会变得非常非常小,这种现象称作梯度消失.反之,如果我们人为的增大w的值,使其大于1,那么连乘就可能造成梯度过大,称作梯度爆炸。
- **梯度消失或爆炸的危害:**如果在训练过程中发生了梯度消失,权重无法被更新,最终导致训练失败;梯度爆炸所带来的梯度过大,大幅度更新网络参数,在极端情况下,结果会溢出(NaN值)。
二. LSTM模型
2.1 LSTM介绍
LSTM(Long Short:Term Memory)也称为长短时记忆结构,它是传统RNN的变体,与经典RNN相比能够有效捕获长序列之间的语义关联,缓解梯度消失或爆炸现象,同时LSTM的结构更加复杂,它的核心结构可以分成四个部分去解析:
- 遗忘门
- 输入门
- 输出门
- 细胞状态
2.2 LSTM的内部结构图
2.2.1 LSTM结构分析

上图中各结构图说明:

- 遗忘门部分的结构图

- 遗忘门计算公式

公式逐项解释:
- ft ------ 遗忘门的输出(在时刻 t)
这是一个向量,其维度与细胞状态 Ct相同。
每个元素的值在 [0,1]之间,表示该时刻"遗忘"细胞状态中对应信息的程度:
接近 1 → 保留信息
接近 0 → 遗忘信息
- σ ------ Sigmoid 激活函数
公式:σ(x)=1+e−x1
作用:将任意实数映射到 (0, 1) 区间,用于生成"门控信号"(gate signal)。
在遗忘门中,它决定"哪些历史信息要被保留"。
- Wf ------ 遗忘门的权重矩阵
一个可训练的参数矩阵。
它的输入维度是:[ht−1,xt]的拼接向量(即前一时刻隐藏状态 + 当前时刻输入)。
输出维度是:与 ft相同(即细胞状态维度)。
- **[ht−1,xt]** ------ 输入拼接向量
ht−1:上一时刻的隐藏状态(hidden state),包含之前所有时间步的压缩信息。
xt:当前时刻的输入(input)。
两者按列拼接(concatenation),形成一个更长的向量。
例如:若 ht−1∈Rn,xt∈Rm,则拼接后向量维度为 n+m。
- bf ------ 遗忘门的偏置项(bias)
- 一个可训练的参数向量,用于调整门的激活阈值。
- 遗忘门结构分析
与传统RNN的内部结构计算非常相似,首先将当前时间步输入x(t)与上一个时间步的隐含状态h(t-1)进行拼接,得到[x(t),h(t-1)],然后通过一个全连接层做变换,最后通过sigmoid函数进行激活得到f(t),我们可以将f(t)看作是门值,好比一扇门开合的大小程度,门值都将作用在通过该扇门的张量,遗忘门门值将作用的上一层的细胞状态上,代表遗忘过去的多少信息,又因为遗忘门门值是由x(t),h(t-1)计算得来的,因此整个公式意味着根据当前时间步输入和上一个时间步隐含状态h(t-1)来决定遗忘多上上一层的细胞状态所携带的过往信息。
- 输入门部分的结构图

- 输入门计算公式

- 输入门结构分析
我们看到输入门的计算公式有两个, 第一个就是产生输入门门值的公式, 它和遗忘门公式几乎相同, 区别只是在于它们之后要作用的目标上. 这个公式意味着输入信息有多少需要进行过滤. 输入门的第二个公式是与传统RNN的内部结构计算相同. 对于LSTM来讲, 它得到的是当前的细胞状态, 而不是像经典RNN一样得到的是隐含状态。
简单来说,输入门就像一个"信息过滤器 + 新信息生成器":先判断"要不要记",再生成"记什么",最后和遗忘门("要不要忘旧信息")配合,完成细胞状态的更新。
- 细胞状态更新图

- 细胞状态计算公式

细胞状态的更新分为两步:
-
"遗忘旧记忆":ft∗Ct−1------ 用遗忘门 ft控制上一时刻细胞状态 Ct−1中哪些信息需要被保留(哪些被遗忘)。
-
"加入新记忆":it∗C~t------ 用输入门 it控制当前时刻的候选新信息 C~t中哪些需要被加入到细胞状态中。
最终,新的细胞状态 Ct是"遗忘旧记忆后保留的部分"与"加入新记忆后新增的部分"之和,实现了对长期记忆的选择性更新(既保留有用的历史信息,又加入当前的关键新信息)。
- 细胞状态更新分析
细胞更新的结构与计算公式非常容易理解,这里没有全连接层,只是将刚刚得到的遗忘门门值与上---个时间步得到的C(t-1)相乘,再加上输入门门值与当前时间步得到的未更新C(t)相乘的结果.最终得到更新后的C(t)作为下一个时间步输入的一部分.整个细胞状态更新过程就是对遗忘门和输入门的应用
- 输出门结构图

- 输出门计算公式

- 输出门结构分析
输出门部分的公式也是两个, 第一个即是计算输出门的门值, 它和遗忘门, 输入门计算方式相同. 第二个即是使用这个门值产生隐含状态h(t), 他将作用在更新后的细胞状态C(t)上, 并做tanh激活, 最终得到h(t)作为下一时间步输入的一部分. 整个输出门的过程, 就是为了产生隐含状态h(t).
2.2.2 LSTM代码实现
LSTM属于RNN的变体,所以代码的实现逻辑实际上大差不差。
python
# -*- coding:utf-8 -*-
import torch
import torch.nn as nn
def dm1_lstm_base():
# 1.实例化模型
# LSTM的参数说明:
# 第一个参数input_size:输入的词嵌入维度
# 第二个参数hidden_size:RNN单元输出的隐藏层张量的维度
# 第三个参数num_layers:有几层RNN单元(有几个隐藏层)
input_size = 5
hidden_size = 6
num_layers = 2
model = nn.LSTM(input_size, hidden_size, num_layers)
# 2. 获取x0输入
# x0的参数说明
# 第一个参数sequence_len:每个样本的长度(单词的个数)(因为RNN模型batch_first=False, seq_len放在第一位置)
# 第二个参数batch_size:一个批次送入几个样本
# 第三个参数input_size:输入的词嵌入维度
sequence_len = 4
batch_size = 3
x0 = torch.randn(sequence_len, batch_size, input_size)
# 3.获取h0\c0输入
# h0、c0的参数说明
# 第一个参数num_layers:有几层RNN单元(有几个隐藏层)
# 第二个参数batch_size:一个批次送入几个样本
# 第三个参数hidden_size:RNN单元输出的隐藏层张量的维度
h0 = torch.randn(num_layers, batch_size, hidden_size)
c0 = torch.randn(num_layers, batch_size, hidden_size)
# 4.将输入送给RNN模型得到下一时间步的输出结果
output, (hn, cn) = model(x0, (h0, c0))
print(f'output--》{output}')
print('*'*80)
print(f'hn--》{hn}')
print(f'cn--》{cn}')
if __name__ == '__main__':
dm1_lstm_base()
注意:在这段代码中有一个小细节:在将输入送给LSTM模型得到输出的过程中,由于LSTM是RNN的变体,所以输入应该符合RNN的要求,也就是输入两个元素,但是LSTM多了一个细胞状态,实际输入有三个元素,这时采用的方法是将上个时间步的隐藏层输出和细胞状态写成一个元组的形式进行输入:

这也与LSTM源代码中的forward方法相对应:

2.3 Bi-LSTM介绍
Bi-LSTM即双向LSTM,它没有改变LSTM本身任何的内部结构,只是将LSTM应用两次且方向不同,再将两次得到的LSTM结果进行拼接作为最终输出。
整体框架图:

- Bi-LSTM结构分析
我们看到图中对"我爱中国"这句话或者叫这个输入序列, 进行了从左到右和从右到左两次LSTM处理, 将得到的结果张量进行了拼接作为最终输出. 这种结构能够捕捉语言语法中一些特定的前置或后置特征, 增强语义关联,但是模型参数和计算复杂度也随之增加了一倍, 一般需要对语料和计算资源进行评估后决定是否使用该结构.
三. GRU模型
3.1 GRU介绍
GRU(Gated Recurrent Unit)也称门控循环单元结构,它也是传统RNN的变体,同LSTM一样能够有效捕捉长序列之间的语义关联,缓解梯度消失或爆炸现象。同时它的结构和计算要比LSTM更简单,它的核心结构可以分为两个部分去解析:
-
更新门
-
重置门
3.2 GRU结构分析


- 内部结构分析
和之前分析过的LSTM中的门控一样, 首先计算更新门和重置门的门值, 分别是z(t)和r(t). 计算方法就是使用x(t)与h(t-1)拼接进行线性变换, 再经过sigmoid激活. 之后重置门门值作用在了h(t-1)上, 代表控制上一时间步传来的信息有多少可以被利用. 接着就是使用这个重置后的h(t-1)进行基本的RNN计算, 即与x(t)拼接进行线性变化, 经过tanh激活, 得到新的h(t). 最后更新门的门值会作用在新的h(t)上, 而1-门值会作用在h(t-1)上, 随后将两者的结果相加, 得到最终的隐含状态输出h(t), 这个过程意味着更新门有能力保留之前的结果, 当门值趋于1时, 输出就是新的h(t), 而当门值趋于0时, 输出就是上一时间步的h(t-1).
3.3 GRU代码实现
python
# -*- coding:utf-8 -*-
import torch
import torch.nn as nn
def dm1_gru_base():
# 1.实例化模型
# GRU的参数说明:
# 第一个参数input_size:输入的词嵌入维度
# 第二个参数hidden_size:GRU单元输出的隐藏层张量的维度
# 第三个参数num_layers:有几层GRU单元(有几个隐藏层)
input_size = 5
hidden_size = 6
num_layers = 2
model = nn.GRU(input_size, hidden_size, num_layers)
# 2. 获取x0输入
# x0的参数说明
# 第一个参数sequence_len:每个样本的长度(单词的个数)(因为GRU模型batch_first=False, seq_len放在第一位置)
# 第二个参数batch_size:一个批次送入几个样本
# 第三个参数input_size:输入的词嵌入维度
sequence_len = 4
batch_size = 3
x0 = torch.randn(sequence_len, batch_size, input_size)
# 3.获取h0输入
# h0参数说明
# 第一个参数num_layers:有几层GRU单元(有几个隐藏层)
# 第二个参数batch_size:一个批次送入几个样本
# 第三个参数hidden_size:GRU单元输出的隐藏层张量的维度
h0 = torch.randn(num_layers, batch_size, hidden_size)
# 4.将输入送给GRU模型得到下一时间步的输出结果
output, hn = model(x0, h0,)
print(f'output--》{output}')
print('*'*80)
print(f'hn--》{hn}')
if __name__ == '__main__':
dm1_gru_base()
3.4 Bi-GRU
Bi-GRU与Bi-LSTM的逻辑相同,都是不改变其内部结构,而是将模型应用两次且方向不同,再将两次得到的LSTM结果进行拼接作为最终输出。