极市导读
本文则通过总结最近对pytorch中RNN模型的使用,来从如何编码使用RNN模型的角度,来着力于提升实际动手操作的能力。 >>加入极市CV技术交流群,走在计算机视觉的最前沿
对于 RNN 模型的原理说明,已经是非常熟悉了,网上也有很多详细的讲解文章。本文就不赘述 RNN 模型的数学原理了,而是从实际代码实现与使用的角度来阐述。毕竟,“原理我都懂了,但是就是不会用”,应该是很多同学的通病。理论和实际应用还是有一定的gap的。
本文则通过总结最近对pytorch中RNN模型的使用,来从如何编码使用RNN模型的角度,来着力于提升实际动手操作的能力。
本文主要内容:
基本RNN计算公式如下:
其中 是时刻 的隐状态 (hidden state) , 是 时刻的输入, 是 时刻的状态。
因此,简单来说一句话:RNN就是根据当前时刻的输入和上一时 刻的状态求当前时刻的状态,就可以简化成一个函数: 。
模型参数
输入说明
torch.nn.utils.rnn.pack_padded_sequence()
进行pack过的对象。
输出说明
torch.nn.utils.rnn.PackedSequence
对象,则输出也是经过packed的对象,需要使用
torch.nn.utils.rnn.pack_sequence()
给变回Tensor。如果指定
batch_first=True
,则输出形状为(batch, seq_len, num_directions * hidden_size)。
output.view(seq_len, batch, num_directions, hidden_size)
来分离方向维度,第0维是前向,第1维是反向。
h_n.view(num_layers, num_directions, batch, hidden_size)
来分离层数维度和方向维度。
output[-1, :, :]
。而对于反向传播中,第0个step是最后时刻的状态,即
output[0, :, :]
。
rnn = nn.RNN(10, 20, 2)
input = torch.randn(5, 3, 10)
h0 = torch.randn(2, 3, 20)
output, hn = rnn(input, h0)
GRU计算公式如下:
GRU中增加了两个门控装置,分别是reset 和 update 门,分别对应 。 则是经过门控之前的下一时刻的状态,然后将下一时刻的状态 与上一时刻的状态 通过reset门和update门进行加劝分配得到最终的下一时刻的状态 。
公式中 * 表示Hadamard积, 表示sigmoid函数。根据公式,我们可以将三个门控分别看作三个函数:
至于这些函数应该如何实现和设计,就都是使用神经网络自己去根据数据学习拟合的了,在训练的过程中不断调整函数中的参数,从而最终学到合适的函数。这也是神经网络的强大之处,人们只需要指定变量之间的关系,至于他们到底有什么关系,就交给神经网络自己去根据数据拟合了,只要有足够大规模的训练数据即可。
GRU模型与RNN在使用上可以说完全一致。基本参数可以参见RNN部分的参数说明。
模型参数
输入参数
输出参数
rnn = nn.RNN(10, 20, 2)
input = torch.randn(5, 3, 10)
h0 = torch.randn(2, 3, 20)
output, hn = rnn(input, h0)
LSTM计算公式如下:
LSTM中增加了三个门控装置, 分别是 input, forget, output 门, 分别对应 , , 。公式中 表示Hadamard积, 表示sigmoid 函数。
根据公式,我们可以将三个门控分别看作三个函数:
也就是说, 三个门控装置都是根据输入 和上一时刻的隐状态 决定的。 是不经过门控时的下一时刻的状态。得到三个门控信号以及不经门控时的下一时刻的状态 后,更新cell状态,也就是上一时刻的cell状态经过forget门来控制遗忘部分内容,下一时刻的状态经过输入门控制输入部分内容,共同得到下一时刻的cell状态。最后cell状态经过output门控制输出部分内容,从而输出 。
LSTM模型的基本参数与RNN和GRU相同。稍微有些不同的地方在于模型的输入与输出。LSTM模型除了每个step的隐状态 之外,还有每个step的cell状态 。cell状态与输出的hidden状态上面公式已经解释了,也就是cell的状态并没有直接输出,而是通过一个输出门来控制输出哪些内容。
模型参数
输入参数
输出参数
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
。下面介绍一下这两个函数的使用。
torch.nn.utils.rnn.pack_padded_sequence(input, lengths, batch_first=False, enforce_sorted=True)
torch.nn.utils.rnn.pad_packed_sequence(sequence, batch_first=False, padding_value=0.0, total_length=None)
在实际使用中,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、腾讯、百度等名校名企视觉开发者互动交流~