在很长一段时间里,我一直忙于寻找一个实现LSTM网络的好教程。它们似乎很复杂,而且在此之前我从来没有使用它们做过任何东西。在互联网上快速搜索并没有什么帮助,因为我找到的都是一些幻灯片。
幸运地是,我参加了 Kaggle EEG 竞赛 ,而且我认为使用LSTM很有意思,最后还理解了它的工作原理。 这篇文章基于 我的解决方案 , 使用的是 Andrej Karpathy 的 char-rnn 代码,这也是我强烈推荐给大家的。
我感觉有一件很重要的事情一直未被大家充分强调过 (而且这也是我为什么不能使用 RNN 做我想做的事情的主要原因)。 RNN 和前馈神经网络并没有很大不同。 最容易实现RNN的一种方法就是像 前馈神经网络使用部分输入到隐含层,以及一些来自隐含层的输出。 在网络中没有任何神奇的内部状态 。 它作为输入的一部分 。
RNN 的整体结构与前馈网络的结构非常相似
本节内容将仅覆盖LSTM的正式定义。有很多其它的好博文,都详细地描述了你该如何设想并思考这些等式。
LSTM有多种变换形式,但我们只讲解一个简单的。一个Cell由三个Gate(input、forget、output)和一个cell单元组成。Gate使用一个sigmoid激活函数,而input和cell state通常会使用tanh来转换。LSTM 的cell可以使用下列的等式来定义:
Gates:
输入变换:
状态更新:
使用图片描述类似下图:
由于门控机制,Cell可以在工作时保持一段时间的信息,并在训练时保持内部梯度不受不利变化的干扰。Vanilla LSTM 没有forget gate,并在更新期间添加无变化的cell状态(它可以看作是一个恒定的权值为1的递归链接),通常被称为一个Constant Error Carousel(CEC)。这样命名是因为它解决了在RNN训练时一个严重的梯度消失和梯度爆炸问题,从而使得学习长期关系成为可能。
这篇教程的代码使用的是Torch7。如果你不了解它也不必担心。我会详细解释的,所以你可以使用你喜欢的框架来实现相同的算法。
该网络将作为nngraph.gModule模块来实现,基本上表示我们定义的一个由标准nn模块组成的神经网络计算图。我们需要以下几层:
首先,让我们来定义输入形式。在lua中类似数组的对象称为表,这个网络将接受一个类似下面的这个张量表。
local inputs = {}
-- network input
table.insert(inputs, nn.Identity()())-- c at time t-1
table.insert(inputs, nn.Identity()())-- h at time t-1
local input = inputs[1]local prev_c = inputs[2]
local prev_h = inputs[3]
Identity模块只将我们提供给网络的输入复制到图中。
为了加快我们的实现,我们会同时运用整个LSTM层转换。
local i2h = nn.Linear(input_size,4 * rnn_size)(input)-- input to hidden local h2h = nn.Linear(rnn_size,4 * rnn_size)(prev_h)-- hidden to hidden local preactivations = nn.CAddTable()({i2h,h2h})-- i2h + h2h
如果你不熟悉nngraph,你也许会觉得奇怪,在上一小节我们建立的inputs属于nn.Module,这里怎么已经用图节点调用一次了。事实上发生的是,第二次调用把nn.Module转换为nngraph.gModule,并且参数指定了该节点在图中的父节点。
preactivations输出一个向量,该向量由输入和前隐藏状态的一个线性变换生成。这些都是原始值,用来计算gate 激活函数和cell输出。这个向量被分为四个部分,每一部分的大小为rnn_size。第一部分将用于in gates,第二部分用于forget gate,第三部分用于out gate,而最后一个作为cell input(因此各个gate的下标和cell数量i的输入为{i, rnn_size+i, 2⋅rnn_size+i, 3⋅rnn_size+i})。
接下来,我们必须运用非线性,但是尽管所有的gate使用的都是sigmoid,我们仍使用tanh对输入进行预激活处理。正因为这个,我们将会使用两个nn.Narrow模块,这会选择预激活向量中合适的部分。
-- gates ) ) -- input )
在非线性操作之后,我们需要增加更多的nn.Narrow,然后我们就完成了gates。
local in_gate = nn . Narrow ( 2 , 1 , rnn_size )( all_gates ) )
有了计算好的gate值,接下来我们可以计算当前的Cell状态了。所有的这些需要的是两个nn.CMulTable模块(一个用于 ,一个用于 ),并且nn.CAddTable用于把它们加到当前的cell状态上。
-- previous cell state contribution
local c_forget = nn.CMulTable()({forget_gate,prev_c})-- input contribution
local c_input = nn.CMulTable()({in_gate,in_transform})-- next cell state
local next_c = nn.CAddTable()({c_forget,
c_input
})
最后,是时候来实现hidden 状态计算了。这是最简单的部分,因为它仅仅是把tanh应用到当前的cell 状态(nn.Tanh)并乘上output gate(nn.CMulTable)。
local c_transform = nn . Tanh ()( next_c )
现在,如果你想要导出整张图作为一个独立的模块,你可以使用下列代码把它封装起来:
-- module outputs
outputs ={}
table.insert ( outputs , next_c)
table.insert ( outputs , next_h )-- packs the graph into a convenient module with standard API (:forward(), :backward())
return nn . gModule ( inputs , outputs )LSTM layer实现可以在 这里 获得。你也可以这样使用它:
th> LSTM= require 'LSTM.lua'
[0.0224s]
th> layer= LSTM.create(3, 2)
[0.0019s]
th> layer:forward({torch.randn(1,3), torch.randn(1,2), torch.randn(1,2)})
{
1 : DoubleTensor - size: 1x2
2 : DoubleTensor - size: 1x2}
}
[0.0005s]为了制作一个多层LSTM网络,你可以在for循环中请求后续层,用上一层的next_h作为下一层的输入。你可以查看 这个例子 。
最后,如果你感兴趣,请留个评论吧,我会试着扩展这篇文章!
确实是这样!当你理解怎样处理隐藏层的时候,实现任何RNN都会很容易。仅仅把一个常规MLP层放到顶部,然后连接多个层并且把它和最后一层的隐藏层相连,你就完成了。
如果你有兴趣的话,下面还有几篇关于RNN的好论文:
原文链接: LSTM implementation explained (编译/刘帝伟 审校/赵屹华、朱正贵、李子健 责编/周建丁)
译者简介: 刘帝伟,中南大学软件学院在读研究生,关注机器学习、数据挖掘及生物信息领域。
1. 加入CSDN人工智能用户微信群,交流人工智能相关技术,加微信号“jianding_zhou”或扫下方二维码,由工作人员加入。 请注明个人信息和入群需求, 并在入群后按此格式改群名片:机构名-技术方向-姓名/昵称 。
2. 加入CSDN 人工智能技术交流QQ群,请搜索群号加入:465538150。同上注明信息。
3. CSDN高端专家微信群,采取受邀加入方式,不惧高门槛的请加微信号“jianding_zhou”或扫描下方二维码, PS:请务必带上你的BIO 。