大名鼎鼎的LSTM详解
本文大部分内容来自于 >>> LSTM详解 .我又从别的地方补充了一些材料,尽可能把它讲的更加完善,尤其是细节方面。尤其是补充了为什么会出现LSTM。这是我的学习笔记,请大神指正。
前言
即使你刚刚入门深度学习,你对传统RNN也不会陌生。我们以NLP常见的序列标注问题为例,h表示一个状态,也就是我们想要的输出,句子被word embedding之后,顺序进入模型,在参数矩阵A的作用下,其中h_0包含the这个词的信息,h_1包含the 和cat 两个词的信息,最后到h_t之后,就包含所有句子的信息。大家很快就能发现问题:句子很长之后就会出现梯度消失的问题,ht包含第一个字符x_0的信息很少很少。在这个基础上LSTM应运而生,它的论文发表在1997年。
![](https://i-blog.csdnimg.cn/blog_migrate/01e6a893b829fa33f2a28cf68d7942b9.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/774e77b0db79322603108bf5d9e6ff8c.png)
在许多讲LSTM的文章中,都会出现下面这个图。 说实话,这个图确实很清晰明了(对于懂的人来说),一些很“显然”的问题就被忽略了,但是对于刚入门的人来说,一些基础的问题却要搞很久才能弄明白,我在原作者讲的很清楚的情况下再补充新手需要的内容。
![](https://i-blog.csdnimg.cn/blog_migrate/06c861577791ad249c45db426e0e9945.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/4a1709f6c119f4d88f6fce44873c1c0b.png)
![](https://i-blog.csdnimg.cn/blog_migrate/2632589fec935a991f19e364eb14dee1.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/37dfda403cbf9ac7c11753684ea54d4e.png)
在这里先说一下这些符号的含义:
Neuial Network layer:一层神经网络,也就是w^T x + b的操作。区别在于使用的激活函数不同,σ表示的是sigmoid函数,他是将数据压缩到[0,1]范围内,如下图所示;tanh表示的是双曲正切激活函数,他把数据归一化到[-1,1]之间,具体函数网上可查,不一一贴图;
![](https://i-blog.csdnimg.cn/blog_migrate/9916061e590078e18481294e72c04b76.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/4ce80d3538b474c419e40d661176c0d3.png)
Pointwise Operation:这个是两个矩阵按位操作,如果是X号表示,这两个维数相同的矩阵,每个位置相同的元素相乘放到新矩阵的该位置上,如下图所示,加法也是同样。
![](https://i-blog.csdnimg.cn/blog_migrate/9ce092664a123d5a08520f7a5fc56388.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/70fdc7022e3e2b78c56d1c7f26644cea.png)
Vector Transfer:矩阵传递
Concatenate:矩阵连接,两个矩阵不做任何计算,只是连接在一起,比如原来A10维,B5维,连接之后15维,就像贪食蛇一样。
Copy:一个矩阵变成两个一模一样的;
了解了这些基本操作,大概知道基础的运行原理,我们接下来一个个解析他的四个门结构,首先看大体结构。
LSTM的大体结构
相比于原始的RNN的隐层(hidden state), LSTM增加了一个细胞状态Ct(cell state),下图是lstm中间一个时刻t的输入输出。
![](https://i-blog.csdnimg.cn/blog_migrate/62ce3f74dbf7367c43c693ef763d62d3.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/689d7f2d74dea3c5de7c0ae71214b0d5.png)
我们首先看一下LSTM在t时刻的输入与输出,首先,输入有三个: 细胞状态C_{t-1}(黄色圆圈),隐层状态h_{t-1}(紫色圆圈), t时刻输入向量X_t(蓝色圆圈),而输出有两个:细胞状态 C_t, 隐层状态 h_t。其中
- 细胞状态 C_{t-1}的信息,一直在上面那条线上传递, t时刻的隐层状态 ht与输入xt会对Ct进行适当修改,然后传到下一时刻去。
- C_{t-1}会参与 t时刻输出 h_t的计算。
- 隐层状态 h_{t-1}的信息,通过LSTM的“门”结构,对细胞状态进行修改,并且参与输出的计算。
总的来说呢,细胞状态的信息一直在上面那条线上传递,隐层状态一直在下面那条线上传递,不过它们会有一些交互,在LSTM中,通常被叫做“门”结构。由此可以看到h_t不光光是由上一个状态,和本次的输入所决定,还有一个细胞状态C_{t-1},这是其与RNN最大的不用
LSTM也是RNN的一种,输入基本没什么差别。通常我们需要一个时序的结构喂给LSTM,数据会被分成 t个部分,也就是上面图里面的 X_t,X_t可以看作是一个向量 ,在实际训练的时候,我们会用batch来训练,所以通常它的shape是**(batch_size, input_dim)**。当然我们来看这个结构的时候可以认为batch_size是1,理解和计算之类的也比较简单。
另外C_0与 h_0的值,也就是两个隐层的初始值,一般是用全0初始化。两个隐层的同样是向量的形式,在定义LSTM的时候,会定义隐层大小(hidden size),即Shape(C_t) = Shape(h_t) = HiddenSize。输出的维度与对应输入是一致的。
LSTM的门结构( 一共有3个)
1. 遗忘门 Forget Gate
![](https://i-blog.csdnimg.cn/blog_migrate/623d993b56403c7bef72b8c3356983ce.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/a85b6bef29bdc83a428f3c93625295a0.png)
首先说一下[ht−1,xt]这个东西就代表把两个向量连接起来(操作与numpy.concatenate相同)。然后f_t就是一个网络的输出,看起来还是很简单的,执行的是上图中的公式。 具体的操作如下图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/764d2512cf3e17416bc63d71e78e98f2.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/03c0f65b1a3627dcdef7d467288fb025.png)
这个图没有体现出来+b这个操作,下同
然而它为什么叫遗忘门呢,下面是我自己的看法,前面也说了,σ的输出在0到1之间,这个输出 f_t逐位与C_{t-1}的元素相乘,我们可以发现,当f_t的某一位的值为0的时候,这C_{t-1}对应那一位的信息就被干掉了,而值为(0, 1),对应位的信息就保留了一部分,只有值为1的时候,对应的信息才会完整的保留。因此,这个操作被称之为遗忘门,也算是“实至名归”,这样放到传送带上的内容是经过遗忘的,传送带我们后边会用到。
2. 更新门 Input Gate
这个门有两个部分,一个是~C_t(因为我没有会员,当时写MD笔记不能插图,所以没有用Markdown来写,符合只能放在前面了,大家凑合着看吧),这个可以看作是新的输入带来的信息,tanh这个激活函数将内容归一化到-1到1。另一个是 i_t,这个东西看起来和遗忘门的结构是一样的,这里可以看作是新的信息保留哪些部分。
![](https://i-blog.csdnimg.cn/blog_migrate/d8015bcdc2002bde57e8494bc3326822.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/aeb3140fdc4dd300f4c6048f4bb27251.png)
下面的操作就是对C_t进行更新,这个公式表示什么呢?看左边,就是前面遗忘门给出的f_t,这个值乘C_{t-1},表示过去的信息有选择的遗忘(保留)。右边也是同理,新的信息~{C_t}乘i_t表示新的信息有选择的遗忘(保留),最后再把这两部分信息加起来,就是新的状态C_t了。
![](https://i-blog.csdnimg.cn/blog_migrate/46a7f3b7e739783d9485bde51aae9d61.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/a912e90516fd4ff5b7aa67e28af8a67c.png)
具体的操作示意图如下:
![](https://i-blog.csdnimg.cn/blog_migrate/b210ed3157a7529d517bfc6d44e3f11e.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/57735555e69a27b9d2e6365cd5739d80.png)
3. 输出门层 Output Gate
最后就是lstm的输出了,此时细胞状态C_t已经被更新了,这里的o_t还是用了一个sigmoid函数,表示输出哪些内容,而C_t通过tanh缩放后与o_t相乘,这就是这一个timestep的输出了。
![](https://i-blog.csdnimg.cn/blog_migrate/86fef1431fcadbdb79677927d3f3c3be.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/6891112fe6fecede972f377826f02d3e.png)
看完公式之后,我们再来看图解,o_t比较简单,同forget门一样
![](https://i-blog.csdnimg.cn/blog_migrate/be1b3c1de406a03279a6004d705fe567.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/3ef12239ae5255b085da244ab1e74482.png)
参数
上面说了lstm的原理与公式,这里想再讲一下参数是怎么计算的。简单来说,就是上面公式的W和b包含的参数数量(上面一共有四个)。 W的话,就是输入维度乘输出维度, b的参数量就是加上输出维度。 上面的公式中,W有四个:W_f,W_i,W_c,W_o,同样,b也是四个: b_f, b_i, b_c, b_o。 我们假设输入x_t这个向量的维度是512,lstm的隐层数是 256,根据这个来实际计算一下参数量。 首先是输入 [h_{t-1}, x_t],这个前面说过,是两个向量连接起来,因此维度相加: 256 + 512 = 768。 因为隐层是 256,所以输出就是256维。 W的参数量就是 768×256=196608,b的参数是256。 所以最终的参数量就是:(768×256+256)×4=787456
另外,pytorch中的lstm实现稍有不同,其公式如下:
![](https://i-blog.csdnimg.cn/blog_migrate/0dc82d7d9eb97df3db3118fdca540c80.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/4651e8007932d3521c840b89a652031f.png)
上面的 g_t其实就是~C_t,其他符号基本是一致的。可以看到,pytorch中,x_t与h_{t-1}并没有拼接在一起,而是各自做了对应的运算,这其实就是使用了分块矩阵的技巧进行计算,结果理论上是一样的,不过这里有些不同的就是加了两个bias,因此计算偏置的参数需要乘2。