NLP基础之传统RNN模型
- 互联网
- 2025-09-19 00:48:01

RNN及其变体 RNN简述
RNN(Recurrent Neural Network),中文称作循环神经网络,是一种专门用于处理序列数据的神经网络架构。一般以序列数据为输入,通过网络内部的结构设计有效捕捉序列之间的关系特征,一般也是以序列形式进行输出。它的特点是能够捕捉序列数据中的时间依赖关系,广泛应用于自然语言处理(NLP)、时间序列预测、语音识别等领域。
序列数据 (Sequential Data):指的是数据项之间存在顺序关系的数据,例如: 文本:句子中的单词顺序语音:音频信号的时间序列股票价格:随时间变化的股价视频:连续的帧序列 循环连接 (Recurrent Connection):RNN的核心在于其循环连接,它允许信息在网络内部循环流动。这使得网络可以保留过去的信息,并将其用于处理当前的输入。隐藏状态 (Hidden State):RNN在每个时间步都会维护一个隐藏状态,这个隐藏状态包含了过去的信息。它可以被认为是网络的"记忆"。时间步 (Time Step):指的是序列数据中的每个元素,例如文本中的一个单词、语音中的一个音频帧。 基本工作原理**输入:**RNN接收一个序列数据作为输入,每次输入序列中的一个元素 (时间步)。
**隐藏状态更新:**对于每个时间步,RNN根据当前的输入和上一个时间步的隐藏状态计算出新的隐藏状态。这个计算通常使用激活函数(如tanh或ReLU)。
隐藏状态更新公式h t = σ ( W h h t − 1 + W x x t + b ) h_t=σ(W_hh_{t−1}+W_xx_t+b) ht=σ(Whht−1+Wxxt+b)
Wh,Wx |:权重矩阵
ht |:t时刻的隐藏状态
**输出:**RNN根据当前的隐藏状态计算出当前的输出。
**循环:**上述过程会在序列的每个时间步重复进行,直到序列结束。
📓 理解RNN的关键:按时间步对RNN进行展开
RNN模型分类 概览 类型输入结构输出结构应用场景N vs N等长序列输入等长序列输出词性标注、逐点预测N vs 1序列输入单个时间步输出文本分类、情感分析1 vs N单个时间步输入序列输出文本/音乐生成、图像描述生成N vs M不等长序列输入不等长序列输出机器翻译、文本摘要 1.按输出和输出的结构分类N vs N:输入和输出序列等长. 适用范围较小,可以用于生成等长度的合辙诗句
1 vs N:输入是一个时间步[ 输入(一个词/值/向量) ], 输出是一个序列数据, 常用于文本/音乐生成/图片描述生成
N vs 1:输入是一个序列数据,一个时间步[ 输入(一个词/值/向量) ], 常用于情感/文本分类任务
N V M模式: 输入输出都是序列数据, 但是不等长,常用于机器翻译/语音识别/文本摘要
这是一种不限输入输出长度的RNN结构,它由编码器和解码器两部分组成,两者的内部结构都是某类RNN,它也被称为seq2seq架构。输入数据首先通过编码器, 最终输出一个隐含变量c, 之后最常用的做法是使用这个隐含变量c作用在解码器进行解码的每一步上,以保证输入信息被有效利用。
seq2seq架构最早被提出应用于机器翻译,因为其输入输出不受限制,如今也是应用最广的RNN模型结构。在机器翻译、阅读理解、文本摘要等众多领域都进行了非常多的应用实践。
2.按RNN内部构造分类概览
类型传统RNNLSTMBi-LSTMGRUBi-GRU 1.传统RNN模型 概况[内部结构示意图]
[解读] :在 t 时刻时(对应图片的中间),存在两部分输入:ht-1 和 Xt ,代表上一时间步的隐藏层输出以及此时间步的输入。
在进入RNN结构体后:
1.它们会"融合"到一起,形成新的张量[xt, ht−1]。
2.这个新的张量[xt, ht−1] 将通过一个全连接层(线性层),该层使用tanh作为激活函数,最终得到该时间步的输出ht,它将作为下一个时间步的输入和xt+1一起进入结构体。
即进行了以下运算:
h t = t a n h ( W t [ X t , h t − 1 ] + b t ) h_t = tanh(W_t[X_t,h_{t-1}] + b_t) ht=tanh(Wt[Xt,ht−1]+bt)
对这一过程在pytorch有另一种解读:
pytorch对这两部分输入:ht-1 和 Xt 分配了各自的权重和偏置项,并进行了以下运算(两次线性运算 + tanh激活) h t = tanh ( x t W i h T + b i h + h t − 1 W h h T + b h h ) h_t = \tanh(x_t W_{ih}^T + b_{ih} + h_{t-1}W_{hh}^T + b_{hh}) ht=tanh(xtWihT+bih+ht−1WhhT+bhh)
重复前两步直到循环结束将所有时间步的隐藏层输出{hi , i = 1,2,…}合并为output,将output的最后的张量(output[-1],即 hn)作为全连接层的输入,进行预测计算损失,反向传播,参数更新 pytorch构建RNN模型[API] : torch.nn.RNN
# torch.nn.RNN 的 __init__参数说明 __init__(input_size, hidden_size, num_layers=1, nonlinearity='tanh', bias=True, batch_first=False, dropout=0.0, bidirectional=False, device=None, dtype=None)input_size:输入特征的数量,即每个时间步输入的维度。[词向量维度]
hidden_size:隐藏状态的特征数量,即每个时间步隐藏状态的维度。
num_layers:循环层的数量。例如,设置 num_layers=2 表示堆叠两个 RNN 层。默认值为 1。
nonlinearity:使用的激活函数,可以是 'tanh' 或 'relu'。默认值为 'tanh'。
bias:如果为 False,则该层不使用偏置权重 b_ih 和 b_hh。默认值为 True。
batch_first:如果为 True,则输入和输出张量的形状为 (batch, seq, feature),否则为 (seq, batch, feature)。默认值为 False。
dropout:如果非零,则在每个 RNN 层(除了最后一层)的输出后引入一个 Dropout 层,丢弃概率为 dropout。默认值为 0。
bidirectional:如果为 True,则使用双向 RNN。默认值为 False。
device:指定模型所在的设备。
dtype:指定模型参数的数据类型。
句子长度为1的基础RNN模型代码
def dm01(): """ 构建句子长度为 1 的基础 RNN模型 :return:None """ # 0- 手动设定随机种子方便后续复现 torch.manual_seed(12) # 1- 构建 RNN模型 # input_size: 词向量维度 # hidden_size: 隐藏层维度 # num_layers: 隐藏层层数 rnn = nn.RNN(input_size=3, hidden_size=4, num_layers=1) # 2- 定义输出向量 # size: (seq_len,batch_size,input_dim) # seq_len:句子长度 # batch_size: 批次的样本数 # input_dim=input_size: 词向量维度 input = torch.randn(1, 2, 3) # 3- 定义初始隐藏状态张量 h0 # size:(num_layer * 网络方向数,batch_size,hidden_size) # num_layer: 隐藏层层数 # 网络方向数: 为 1 或 2 -> 1:代表是单向RNN 2:代表是双向RNN # batch_size: 样本批次数 # hidden_size: 隐藏层维度 h0 = torch.zeros(1, 2, 4) ## 注意!! h0也可以不显式定义出来,默认就是全0初始化. ## 使用rnn模型时可以不传入h0,就是使用的隐式设置hidden层 ### 示例:output, hn = rnn(input) # 4- 使用RNN模型 output, hn = rnn(input, h0) # 5- 查看模型输出 print('output->', output, output.shape) print('hn->', hn, hn.shape) # 结论: 若句子只有1个词,output输出结果等于hn输出结果
output-> tensor( [[[-0.2506, -0.3442, -0.2307, 0.1333], [-0.1602, -0.1754, -0.5785, -0.3782]]] , grad_fn=<StackBackward0>) torch.Size([1, 2, 4]) hn-> tensor( [[[-0.2506, -0.3442, -0.2307, 0.1333], [-0.1602, -0.1754, -0.5785, -0.3782]]] , grad_fn=<StackBackward0>) torch.Size([1, 2, 4])结论: 若句子只有1个词,output输出结果等于hn
句子长度大于 1 的RNN模型 def dm02(): """ 构建句子长度大于 1 的基础 RNN模型 :return:None """ # 0- 手动设定随机种子方便后续复现 torch.manual_seed(12) # 1- 构建 RNN模型 # input_size: 词向量维度 # hidden_size: 隐藏层维度 # num_layers: 隐藏层层数 rnn = nn.RNN(input_size=4, hidden_size=3, num_layers=1) # 2- 定义输出向量 # size: (seq_len,batch_size,input_dim) # seq_len:句子长度 # batch_size: 批次的样本数 # input_dim=input_size: 词向量维度 input = torch.randn(3, 2, 4) # 3- 定义初始隐藏状态张量 h0 # size:(num_layer * 网络方向数,batch_size,hidden_size) # num_layer: 隐藏层层数 # 网络方向数: 为 1 或 2 -> 1:代表是单向RNN 2:代表是双向RNN # batch_size: 样本批次数 # hidden_size: 隐藏层维度 h0 = torch.zeros(1, 2, 3) ## 注意!! h0也可以不显式定义出来,默认就是全0初始化. ## 使用rnn模型时可以不传入h0,就是使用的隐式设置hidden层 ### 示例:output, hn = rnn(input) # 4- 使用RNN模型 output, hn = rnn(input, h0) # 5- 查看模型输出 print('output->', output, output.shape) print('hn->', hn, hn.shape) # 结论: 若句子由多个词组成, output的最后一组词向量(每个句子的最后一个词组成)等于h0输出结果
output-> tensor( [[[ 0.0096, -0.4331, 0.0036], [-0.2697, -0.4088, -0.1225]], [[-0.4351, -0.8297, -0.5539], [-0.2669, -0.6056, 0.5748]], [[-0.3306, 0.0999, -0.7674], [-0.0076, -0.7076, 0.6473]]] ,grad_fn=<StackBackward0>) torch.Size([3, 2, 3]) hn-> tensor( [[[-0.3306, 0.0999, -0.7674], [-0.0076, -0.7076, 0.6473]]] , grad_fn=<StackBackward0>) torch.Size([1, 2, 3])结论: 若句子由多个词组成, output的最后一组词向量(每个句子的最后一个词组成)等于h0
模拟 RNN模型循环机制 def dm03(): """ 模拟 RNN循环机制 :return:None """ # 0- 手动设定随机种子方便后续复现 torch.manual_seed(12) # 1- 构建 RNN模型 # input_size: 词向量维度 # hidden_size: 隐藏层维度 # num_layers: 隐藏层层数 rnn = nn.RNN(input_size=4, hidden_size=3, num_layers=1) # 2- 定义输出向量 # size: (seq_len,batch_size,input_dim) # seq_len:句子长度 # batch_size: 批次的样本数 # input_dim=input_size: 词向量维度 input = torch.randn(3, 1, 4) # print('input->', input) # 3- 定义初始隐藏状态张量 h0 # size:(num_layer * 网络方向数,batch_size,hidden_size) # num_layer: 隐藏层层数 # 网络方向数: 为 1 或 2 -> 1:代表是单向RNN 2:代表是双向RNN # batch_size: 样本批次数 # hidden_size: 隐藏层维度 h0 = torch.zeros(1, 1, 3) ## 注意!! h0也可以不显式定义出来,默认就是全0初始化. ## 使用rnn模型时可以不传入h0,就是使用的隐式设置hidden层 ### 示例:output, hn = rnn(input) # 4- 一个词一个词的 给模型喂数据 for i in range(input.shape[0]): tmp_x = input[0].unsqueeze(dim=0) # print('tmp_x->', tmp_x) output, hn = rnn(tmp_x, h0) # 5- 查看模型输出 print(f'output_{i + 1}->', output, output.shape) print(f'hn_{i + 1}->', hn, hn.shape) print()输出结果:
output_1-> tensor([[[ 0.1012, -0.1932, 0.0585]]], grad_fn=<StackBackward0>) torch.Size([1, 1, 3]) hn_1-> tensor([[[ 0.1012, -0.1932, 0.0585]]], grad_fn=<StackBackward0>) torch.Size([1, 1, 3]) output_2-> tensor([[[ 0.1012, -0.1932, 0.0585]]], grad_fn=<StackBackward0>) torch.Size([1, 1, 3]) hn_2-> tensor([[[ 0.1012, -0.1932, 0.0585]]], grad_fn=<StackBackward0>) torch.Size([1, 1, 3]) output_3-> tensor([[[ 0.1012, -0.1932, 0.0585]]], grad_fn=<StackBackward0>) torch.Size([1, 1, 3]) hn_3-> tensor([[[ 0.1012, -0.1932, 0.0585]]], grad_fn=<StackBackward0>) torch.Size([1, 1, 3]) 隐藏层大于1的RNN模型 def dm04(): """ 构建隐藏层层数大于1的 RNN模型 :return:None """ # 0- 手动设定随机种子方便后续复现 torch.manual_seed(12) # 1- 构建 RNN模型 # input_size: 词向量维度 # hidden_size: 隐藏层维度 # num_layers: 隐藏层层数 rnn = nn.RNN(input_size=3, hidden_size=3, num_layers=2) # 2- 定义输出向量 # size: (seq_len,batch_size,input_dim) # seq_len:句子长度 # batch_size: 批次的样本数 # input_dim=input_size: 词向量维度 input = torch.randn(1, 2, 3) # 3- 定义初始隐藏状态张量 h0 # size:(num_layer * 网络方向数,batch_size,hidden_size) # num_layer: 隐藏层层数 # 网络方向数: 为 1 或 2 -> 1:代表是单向RNN 2:代表是双向RNN # batch_size: 样本批次数 # hidden_size: 隐藏层维度 h0 = torch.zeros(2, 2, 3) ## 注意!! h0也可以不显式定义出来,默认就是全0初始化. ## 使用rnn模型时可以不传入h0,就是使用的隐式设置hidden层 ### 示例:output, hn = rnn(input) # 4- 使用RNN模型 output, hn = rnn(input, h0) # 5- 查看模型输出 print('output->', output, output.shape) print('hn->', hn, hn.shape) print('rnn模型->', rnn) # 结论:若只有1个隐藏层,output输出结果等于hn # 结论:如果有2个隐藏层,hn有2个,output输出结果等于最后1个隐藏层的hn输出结果:
output-> tensor( [[[-0.3889, -0.1856, -0.5709], [-0.3924, -0.2318, -0.4879]]] , grad_fn=<StackBackward0>) torch.Size([1, 2, 3]) hn-> tensor( [[[-0.6897, -0.7110, 0.7299], [-0.8599, -0.6114, 0.4448]], [[-0.3889, -0.1856, -0.5709], [-0.3924, -0.2318, -0.4879]]] , grad_fn=<StackBackward0>) torch.Size([2, 2, 3]) rnn模型-> RNN(3, 3, num_layers=2)结论:若只有1个隐藏层,output输出结果等于hn 结论:如果有2个隐藏层,hn有2个,output输出结果等于最后1个隐藏层的hn
关于output的维数,给出如下步骤方便理解 隐藏状态的维度信息 h i h_{i} hi:(num_layers * 网络方向数,batch_size,hidden_size) ,i=1,2…n
rnn层前向传播时,按照以下步骤执行: 1.每一个时间步会产生一个正向隐藏状态: h i f h^{f}_{i} hif,一个反向隐藏状态** h i b h^{b}_{i} hib**(如果是双向网络)
如果是此时rnn是双向网络则会执行2.0:
2.0 根据dim= -1 将这两个张量拼接(concat)起来
即** h i h_i hi = h i f , h i b {h^{f}_{i},h^{b}_{i}} hif,hib =>(num_layers,batch_size,hidden_size * 网络方向数)**
2.output会按照dim=0维度连接 h 0 h_0 h0/ h 1 h_1 h1…/ h n h_n hn 所有向量(concat操作,并不会增加维度)
因此output也是一个三维张量,最后output的维数 => (seq_len,batch_size,hidden_size * 网络方向数)
传统RNN模型的优点与不足
优点
结构简单,易于理解和实现: 传统RNN的结构非常直观,只有一个隐藏层和循环连接,因此容易理解其工作原理。由于结构简单,其代码实现也相对容易。 适用于处理序列数据: 传统RNN的核心优势在于它能够处理具有顺序关系的数据,例如文本、语音、时间序列等。通过循环连接,RNN可以将过去的信息传递到当前时刻,从而学习序列数据中的依赖关系。 能够处理可变长度的序列: 传统RNN可以处理长度不固定的序列,而无需提前固定输入数据的长度。这使得它在处理自然语言、语音等具有可变长度数据的任务中非常灵活。 参数共享: 传统RNN在每个时间步都共享相同的参数(权重矩阵和偏置项),从而减少了模型参数的数量。参数共享还允许RNN学习序列数据中跨时间步的通用模式。 计算效率较高 (相对于更复杂的RNN变体): 由于结构简单,传统 NN的计算复杂度相对较低,训练速度相对较快。在一些计算资源有限的场景下,传统RNN是一个可行的选择。缺点
梯度消失问题: 这是传统RNN最主要的缺点。在处理长序列时,梯度在反向传播过程中会逐渐衰减,导致网络无法学习到长距离依赖关系。梯度消失使得模型难以捕捉输入序列中较早的信息,从而限制了其在长序列任务上的性能。 梯度爆炸问题: 与梯度消失相反,在某些情况下,梯度在反向传播过程中可能会呈指数级增长,导致训练不稳定甚至发散。梯度爆炸也限制了传统RNN在某些任务中的应用。 难以捕捉长期依赖: 由于梯度消失问题,传统RNN很难学习长距离的依赖关系,即难以记住输入序列中较早的信息,并将其用于影响后面的输出。这意味着在处理文本、语音等长序列时,传统RNN很难理解上下文的含义。 训练不稳定: 由于梯度消失和梯度爆炸问题,传统RNN的训练过程可能会不稳定,需要小心调整超参数。 无法并行计算: 由于RNN的循环结构,每个时间步的计算都依赖于前一个时间步的隐藏状态,因此无法进行并行计算,训练速度受到限制。NLP基础之传统RNN模型由讯客互联互联网栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“NLP基础之传统RNN模型”
下一篇
快检查达梦库怎么了