Pytorch源码理解: RNNbase LSTM

 0.Pytorch中LSTM 的使用方法

LSTM使用方法:

lstm=nn.LSTM(input_size, hidden_size,num_layers)
output,(hn,cn)=lstm(x,(h0,c0))

各参数的shape:

x: (seq_len, batch, input_size)

h0: (num_layers×num_directions, batch, hidden_size)

c0: (num_layers×num_directions, batch, hidden_size)

output: (seq_len, batch, num_directions×hidden_size)

hn: (num_layers×num_directions, batch, hidden_size)

cn: (num_layers×num_directions, batch, hidden_size)

 

1.Packed_Sequence问题

根据pack之后的结果,是按照竖向方法存储,这使得我对packsequence输入的顺序正确性产生了理解问题,经过查询官方文档和内部实现后发现,这样的操作是使得并行化更为方便,同时也要更深刻的理解, RNN是按照time_step的方法来输入序列的,这样的排序使得处于同一个time_step的在内存空间上相互比邻,比如在第一个step,rnn知道每个句子的第一个step是[ I,I,THIS,NO,YES],第二个step是[love,love,is,way],而如果是按照序列存储[I,love,Mom,',s,cooking]则一个输入里每一个time_step都有,而非是竖存储方式的每一个step都是一列,同时每一个step都是一列,这使得GPU的并行化更加容易。

2.内部结构

祭出这一张出名的LSTM图吧,说什么都不如这一张图说的清楚,基于这个图进行解释。

LSTM

要注意几点:
这是时间步上的展开,并不是3个LSTM单元,而是一个,输出的隐状态向量会直接循环输入进输入进,所以这三个是一个展开,而不是3个LSTM

3.源码

好下面开始解读LSTM的源码:

可以见到LSTM继承了RNNBase这个父类。

先看它的初始化,也挺长的:

可以看见在RNNBase里还传入了一个mode参数,这个mode在图底部判断了使用的模型名字,参数赋值就不说了,直接进入条件语句里看,首先判断了dropout是不是一个正常值,同时在我们只有一个LSTM的时候并不让我们dropout,根据解释是,dropout是在所有recurrent layer之后的,也就是只有当我们的隐层状态作为第二个LSTM的输入的时候才允许使用dropout,我个人这样理解,既然第一层是明确的处理时序信息,那我们就不应该在训练时使用dropout,因为这样会降低时序的处理能力,因为drop掉一部分权重,会让我们在第一层就损失时序信息,而当我们输出隐层之后,由于隐层是不明确的时序信息,可以在训练时dropout。随后判断我们使用的是什么模型,当我们输入LSTM的时候,门的维度是隐向量维度的四倍,因为我们在 LSTM里,我们一共有4个gate,分别是input_gate,控制了我有多少要输入进去,而是forget_gate决定了LSTM里cell有多少要遗忘,然后是output_gate决定了我有多少要传入下一个状态,还有就是cell_gate决定有多少要来更新这个cell,这四个gate控制了整个LSTM的读写过程,而每个gate的维度要和隐状态要匹配。

我们先进入每一层,然后判断该层是否是双向LSTM,如果是的话,input的size将会乘以2,同时明确在第一层时以input_size输入。

在这里,我们把所有层要训练的参数都添加进pytorch的Parameter中,当torch的parameter被在一个nn.Module下的神经网络启用时,它将自定把用Parameter包装的向量作为神经网络中需要训练的参数,次数我们有了每层的权值和偏置,然后包装成一个元组作为layer_params,即layer_params包含了该层的所有可训练参数,如果有反响的话还会添加suffix后缀,随后将所有可训练参数都通过_all_weights.append添加进去。下面有个flatten函数,是用于cuda的,不作解读,理解cpu版本,cuda的工作原理一样,只不过可以更简单的并行化。

在这个函数中,我们对parameters进行初始化。

这个函数检查了要前向传输的自变量,当batch_size不为空时,这以为着我们的sequence已经是pack之后的了,当这个sequence是pad之后的我们输入的维度便期待是2维,这正好和pack后的sequence对准,如果此处理解不了的话可以先看一下pack_sequence的函数功能。

当输入维度不是期待维度时提出错误,然后在确定pack之后得到mini_batch的size,否则则以input的size来作为mini_batch。然后判断是否是双向LST,如果是,则期待的隐状态维度要变化,然后check隐状态的size,不对则报错。

在这里定义了前向的传播,也是最终的一部分,首先还是判断sequence是否是pack之后的,如果是pack了的,则获得data和batch大小。同时默认hx为空,此处是如果我们没有默认的隐向量输入时,hx会启用,并初始化为0,随后通过查询_rnn_impls来知道我们要启用的前向函数。

当模型是lstm时,则启动_VF.lstm,进行前向传播,而_VF.lstm是c++代码,在pytorch上找到了相关源码,贴c++代码。

可见,在c++里的这个_lstm_impl还是没有告诉我们具体的计算过程,让我们再看看这个函数做了什么,首先,它将每个层的隐状态向量和细胞状态分层化给每一个layer,注意我们看到result是以_rnn_imp的调用出现的,再往下看,得到了result之后,我们又把每一层的隐状态向量和细胞状态组合起来,做成一个tuple返回,所以在这里我们发现,所有的隐藏状态并不是放在一个大矩阵里不断更新的,而是不断的切分成一层一层的,计算之后,再合在一起返回,既可以理解为分层后可以更快的并行同时将隐藏向量分层计算也比较符合LSTM的特性。

然后我们在追到result所调用的_rnn_impl了,这个应该已经是所有RNN网络都要调用的一个模块了,体现了LSTM只不过是RNN的一种形式。然而我们看到,这里面还是没有告诉我们是怎么计算的。在这里,首先是保存了一堆参数,然后直接判断是否是,如果是,则构双向层,然后调用apply_layer_stack得到双层结果,如果不是也调用这个也会返回结果,那么我们继续找apply_layer_stack这个函数做了什么。

在这里,我们看到还是没有告诉我们是怎么计算的,但是我们先看看,首先检查了的层数是否一致,然后复制给一些参数,随后我们看到每一个层的layer_output都是调用layer得到的,layer的参数是上一层layer的input,隐状态还有权重,然后我们看到,这里有一个RNN的递归实现,每一个的最后隐状态都会push__back进layer中,实现隐状态进入下一时刻的LSTM,而下一层的layer_input则是等于上一层的layer_output的输出,同时如果有dropout则会调用dropout,又由于在const里定义了Layer&layer,在c++中相当于给Layer取了一个别名layer,所以我们去看Layer.

但是经过一番查询后,还是没有找到定义计算的过程,浏览了一整遍的代码,在传入的cell_parameters中,有LSTM,而在构建这个结构体时就已经进行了计算。

直到这里,终于发现了计算过程和整体的定义,在这里有一个cuda版本的我们不作阅读,首先我们就看见了,它将集合起来的gate分成了4个gate,这和python中的操作不谋而合。然后就是根据门来更新细胞的状态和隐向量,随后组成一个tuple返回,再细查这个cell_params,它包括了所有的RNN带有cell的结构,LSTM和GRU这两个带有cell的RNN

回到result这条语句上,这里的rnn_impl就是输入了LSTMCell的结构体进行了运算。

 

源自《Pytorch-RNNBASE-LSTM python+c源码理解

 

参考:
【1】Understanding LSTM Networks​colah.github.io

【2】pytorch对可变长度序列的处理 - 深度学习1 - 博客园​www.cnblogs.com

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值