
极市导读
本文则通过总结最近对pytorch中RNN模型的使用,来从如何编码使用RNN模型的角度,来着力于提升实际动手操作的能力。 >>加入极市CV技术交流群,走在计算机视觉的最前沿
对于 RNN 模型的原理说明,已经是非常熟悉了,网上也有很多详细的讲解文章。本文就不赘述 RNN 模型的数学原理了,而是从实际代码实现与使用的角度来阐述。毕竟,“原理我都懂了,但是就是不会用”,应该是很多同学的通病。理论和实际应用还是有一定的gap的。
本文则通过总结最近对pytorch中RNN模型的使用,来从如何编码使用RNN模型的角度,来着力于提升实际动手操作的能力。
本文主要内容:
-
RNN模型(包括GRU以及LSTM)的使用说明 -
处理变长序列的方法以及一些小技巧 -
pack_padded_sequence 与 pad_packed_sequence -
使用RNN进行文本处理的基本步骤
RNN模型(包括GRU以及LSTM)的使用说明
torch.nn.RNN
RNN原理
基本RNN计算公式如下:
其中 是时刻 的隐状态 (hidden state) , 是 时刻的输入, 是 时刻的状态。
因此,简单来说一句话:RNN就是根据当前时刻的输入和上一时 刻的状态求当前时刻的状态,就可以简化成一个函数: 。
参数说明
-
模型参数
-
input_size:输入向量维度 -
hidden_size:隐层状态维度 -
num_layers:RNN层数 -
nonlinearity:使用哪种非线性激活函数。[tanh, relu],Default:tanh -
bias:bool, 如果为False,则不使用偏置项。Default: True -
batch_first: bool, 如果为True,则输入形状为(batch, seq, feature)。默认为False,因此RNN的默认输入形状为(seq, batch, feature) -
dropout:float, 指定dropout率,Default: 0 -
bidirectional:是否为双向。Default:False -
输入说明
-
input:Tensor,形状(seq_len, batch, input_size)。或者是使用 torch.nn.utils.rnn.pack_padded_sequence()进行pack过的对象。 -
h_0:(num_layers * num_directions, batch, hidden_size)。如果RNN初始状态没有指定,则默认为全零张量。如果bidirectional为True,则num_directions=2,否则为1。 -
输出说明
-
output:(seq_len, batch, num_directions * hidden_size)。输出最后一层每个step的隐层特征。如果输入是 torch.nn.utils.rnn.PackedSequence对象,则输出也是经过packed的对象,需要使用torch.nn.utils.rnn.pack_sequence()给变回Tensor。如果指定batch_first=True,则输出形状为(batch, seq_len, num_directions * hidden_size)。 -
h_n:(num_layers * num_directions, batch, hidden_size)。输出每一层最后一个step的隐层特征。
-
对于RNN模型的输出output,可以使用 output.view(seq_len, batch, num_directions, hidden_size)来分离方向维度,第0维是前向,第1维是反向。
对于RNN模型的隐层状态h_n,可以使用h_n.view(num_layers, num_directions, batch, hidden_size)来分离层数维度和方向维度。 -
对于双向RNN来说,前向传播中,最后一个step是最后时刻的输出,即 output[-1, :, :]。而对于反向传播中,第0个step是最后时刻的状态,即output[0, :, :]。
RNN用例
rnn = nn.RNN(10, 20, 2)
input = torch.randn(5, 3, 10)
h0 = torch.randn(2, 3, 20)
output, hn = rnn(input, h0)
torch.nn.GRU
GRU原理
GRU计算公式如下:
GRU中增加了两个门控装置,分别是reset 和 update 门,分别对应 。 则是经过门控之前的下一时刻的状态,然后将下一时刻的状态 与上一时刻的状态 通过reset门和update门进行加劝分配得到最终的下一时刻的状态 。
公式中 * 表示Hadamard积, 表示sigmoid函数。根据公式,我们可以将三个门控分别看作三个函数:
至于这些函数应该如何实现和设计,就都是使用神经网络自己去根据数据学习拟合的了,在训练的过程中不断调整函数中的参数,从而最终学到合适的函数。这也是神经网络的强大之处,人们只需要指定变量之间的关系,至于他们到底有什么关系,就交给神经网络自己去根据数据拟合了,只要有足够大规模的训练数据即可。
参数说明
GRU模型与RNN在使用上可以说完全一致。基本参数可以参见RNN部分的参数说明。
-
模型参数
-
input_size -
hidden_size -
num_layers -
bias -
batch_first -
dropout -
bidirectional -
输入参数
-
input :(seq_len, batch, input_size) -
h_0 :(num_layers * num_directions, batch, hidden_size) -
输出参数
-
output :(seq_len, batch, num_directions * hidden_size) -
h_n :(num_layers * num_directions, batch, hidden_size)
GRU用例
rnn = nn.RNN(10, 20, 2)
input = torch.randn(5, 3, 10)
h0 = torch.randn(2, 3, 20)
output, hn = rnn(input, h0)
torch.nn.LSTM
LSTM原理
LSTM计算公式如下:
LSTM中增加了三个门控装置, 分别是 input, forget, output 门, 分别对应 , , 。公式中 表示Hadamard积, 表示sigmoid 函数。
根据公式,我们可以将三个门控分别看作三个函数:
也就是说, 三个门控装置都是根据输入 和上一时刻的隐状态 决定的。 是不经过门控时的下一时刻的状态。得到三个门控信号以及不经门控时的下一时刻的状态 后,更新cell状态,也就是上一时刻的cell状态经过forget门来控制遗忘部分内容,下一时刻的状态经过输入门控制输入部分内容,共同得到下一时刻的cell状态。最后cell状态经过output门控制输出部分内容,从而输出 。
参数说明
LSTM模型的基本参数与RNN和GRU相同。稍微有些不同的地方在于模型的输入与输出。LSTM模型除了每个step的隐状态 之外,还有每个step的cell状态 。cell状态与输出的hidden状态上面公式已经解释了,也就是cell的状态并没有直接输出,而是通过一个输出门来控制输出哪些内容。
-
模型参数
-
input_size -
hidden_size -
num_layers -
bias -
batch_first -
dropout -
bidirectional -
输入参数
-
input :(seq_len, batch, input_size) -
h_0 :(num_layers * num_directions, batch, hidden_size),初始状态。若不提供则默认为全零。 -
c_0 :(num_layers * num_directions, batch, hidden_size),初始cell状态。若不提供则默认全零。 -
输出参数
-
output :(seq_len, batch, num_directions * hidden_size),最后一层每个step的输出特征。 -
h_n :(num_layers * num_directions, batch, hidden_size),每一层的最后一个step的输出特征。 -
c_n :(num_layers * num_directions, batch, hidden_size),每一层的最后一个step的cell状态。
LSTM用例
rnn = nn.LSTM(10, 20, 2)
input = torch.randn(5, 3, 10)
h0 = torch.randn(2, 3, 20)
c0 = torch.randn(2, 3, 20)
output, (hn, cn) = rnn(input, (h0, c0))
处理变长序列
在处理文本数据时,通常一个batch中的句子长度都不一样。
例如如下几个句子
[it is a lovely day]
[i love chinese food]
[i love music]
对应词典
{pad:0, it:1, is:2, a:3, lovely:4, day:5, i:6, love:7, chinese:8, food:9, music:10}
而pytorch中Tensor一定是所有向量的维度都相同的。因此在处理变长序列时,需要进行以下步骤:
首先,将所有文本padding成固定长度。
[1, 2, 3, 4, 5]
[6, 7, 8, 9, 0]
[6, 7, 10, 0, 0]
然后将单词转化为one_hot的形式,变为(batch,seq_len,vocabsize)形状的Tensor。
到这里,所有句子都padding成了相同的长度,但是现在还不能直接送到RNN中,因为句子中有大量的0(PAD),这些PAD也送入RNN中的话,也会影响对句子的计算过程。为了排除这些PAD的影响,pytorch提供了两个函数torch.nn.utils.rnn.pack_padded_sequence 和 torch.nn.utils.rnn.pad_packed_sequence。下面介绍一下这两个函数的使用。
pack and pad
torch.nn.utils.rnn.pack_padded_sequence(input, lengths, batch_first=False, enforce_sorted=True)
-
参数 -
input: Tensor, (seq_len, batch, *),*表示可以是任意维度,如果是one-hot表示,则是vocabsize维度,如果是其他embedding表示,则是对应embedding的维度。这里输入的是padding之后得到的Tensor,因此seq_len都是固定长度的。 -
lengths:Tensor, batch中每个句子的真实长度。 -
batch_first:第一维度是否是batch -
enforce_sorted
torch.nn.utils.rnn.pad_packed_sequence(sequence, batch_first=False, padding_value=0.0, total_length=None)
-
参数 -
sequence:batch to pad -
batch_first:if True, the output will be in B x T x * format. -
padding_value:values for padded elements. -
total_length:if not None, the output will be padded to have length total_length
在实际使用中,pack_padded_sequence函数将padding之后的Tensor作为输入,pack_padded_sequence输出一个PackedSequence对象,其中Tensor中padding的部分都去掉了,也就是只保留了序列的真实长度。
然后经过RNN模型或者双向RNN模型,得到输出。
之后再利用pad_packed_sequence函数将RNN的输出结果变回来。
一个使用示例如下:
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence
import numpy as np
# padding函数
x: [[w1, w2,...], [w1, w2, ...], ...]
def padding(x):
maxlen = max([len(l) for l in x])
for sen in x:
if len(sen) < maxlen:
sen.extend([0] * (maxlen-len(sen)))
return x
# 句子单词映射到onehot表示
# V: 单词表, padded_tokens:padding之后的输入
def id2onehot(V, padded_tokens):
onehot = np.eye((len(V)))
embeddings = []
for sen in padded_tokens:
sen_embdding = []
for i,tokenid in enumerate(sen):
sen_embdding.append(onehot[tokenid].tolist())
embeddings.append(sen_embdding)
# print(embeddings)
return torch.FloatTensor(embeddings)
V = {'PAD':0, 'a':1, 'b':2, 'c':3, 'd':4}
sentences = ['abcd', 'd', 'acb']
sen_lens = [len(x) for x in sentences]
tokens = []
for sen in sentences:
token = []
for c in sen:
token.append(V[c])
tokens.append(token)
padded_tokens = padding(tokens)
X = id2onehot(V, padded_tokens)
# print(X)
torch.random.manual_seed(10)
# 定义一个双向lstm网络层
lstm = nn.LSTM(5, 3, num_layers=1, batch_first=True, bidirectional=True)
X = X.float()
# 压紧数据,去掉padding部分
packed = pack_padded_sequence(X, torch.tensor(sen_lens), batch_first=True, enforce_sorted=False)
print(packed)
# 通过lstm进行计算,得到的结果也是压紧的
output, hidden = lstm(packed)
# 解压,恢复成带padding的形式
encoder_outputs, lenghts = pad_packed_sequence(output, batch_first=True)
print(encoder_outputs)
上面的例子中,输入的句子长度是没有经过排序的,输入到pack_padded_sequence函数的输入句子并不是按照长度排序的。网上很多教程都说必须要将输入句子按照长度进行排序,然后输入到pack_padded_sequence中,之后再把顺序变回来。
但是我看pytorch官方手册里,pack_padded_sequence函数实际上有一个参数enforce_sorted的,该参数默认是True。如果该参数为True,则输入的句子应该按照长度顺序排序。而如果是False的话,实际上输入句子不排序也可以。那么什么情况下需要将该参数设为True呢?手册上是这么写的:
For unsorted sequences, use enforce_sorted = False. If enforce_sorted is True, the sequences should be sorted by length in a decreasing order, i.e. input[:,0] should be the longest sequence, and input[:,B-1] the shortest one. enforce_sorted = True is only necessary for ONNX export.
即只有使用ONNX export时,必须将该参数设为True。也就是说,平常使用的时候,不是必须设为True的,我们将该参数设为False,就可以直接输入无序的句子了,不用再手动对其排序,之后再变回来了。这样省事多了。
如果觉得有用,就请分享到朋友圈吧!
公众号后台回复“transformer”获取最新Transformer综述论文下载~
# CV技术社群邀请函 #
备注:姓名-学校/公司-研究方向-城市(如:小极-北大-目标检测-深圳)
即可申请加入极市目标检测/图像分割/工业检测/人脸/医学影像/3D/SLAM/自动驾驶/超分辨率/姿态估计/ReID/GAN/图像增强/OCR/视频理解等技术交流群
每月大咖直播分享、真实项目需求对接、求职内推、算法竞赛、干货资讯汇总、与 10000+来自港科大、北大、清华、中科院、CMU、腾讯、百度等名校名企视觉开发者互动交流~

