LSTM实现详解

前言

在很长一段时间里,我一直忙于寻找一个实现LSTM网络的好教程。它们似乎很复杂,而且在此之前我从来没有使用它们做过任何东西。在互联网上快速搜索并没有什么帮助,因为我找到的都是一些幻灯片。

幸运地是,我参加了Kaggle EEG 竞赛,而且我认为使用LSTM很有意思,最后还理解了它的工作原理。这篇文章基于我的解决方案,使用的是Andrej Karpathychar-rnn代码,这也是我强烈推荐给大家的。

RNN误区

我感觉有一件很重要的事情一直未被大家充分强调过(而且这也是我为什么不能使用RNN做我想做的事情的主要原因)。RNN和前馈神经网络并没有很大不同。最容易实现RNN的一种方法就是像前馈神经网络使用部分输入到隐含层,以及一些来自隐含层的输出。在网络中没有任何神奇的内部状态。它作为输入的一部分。


RNN的整体结构与前馈网络的结构非常相似

LSTM回顾

本节内容将仅覆盖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训练时一个严重的梯度消失和梯度爆炸问题,从而使得学习长期关系成为可能。

建立你自己的LSTM层

这篇教程的代码使用的是Torch7。如果你不了解它也不必担心。我会详细解释的,所以你可以使用你喜欢的框架来实现相同的算法。

该网络将作为nngraph.gModule模块来实现,基本上表示我们定义的一个由标准nn模块组成的神经网络计算图。我们需要以下几层:

  • nn.Identity() - 传递输入(用来存放输入数据)
  • nn.Dropout(p) - 标准的dropout模块(以1-p的概率丢弃一部分隐层单元)
  • nn.Linear(in, out) - 从in维到out维的一个仿射变换
  • nn.Narrow(dim, start, len) - 在第dim方向上选择一个子向量,下标从start开始,长度为len
  • nn.Sigmoid() - 应用sigmoid智能元素
  • nn.Tanh() - 应用tanh智能元素
  • nn.CMulTable() - 输出张量(tensor)的乘积
  • nn.CAddTable() - 输出张量的总和

输入

首先,让我们来定义输入形式。在lua中类似数组的对象称为表,这个网络将接受一个类似下面的这个张量表。


local inputs  = {}
table.insert(inputs, nn.Identity()())   -- 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模块只将我们提供给网络的输入复制到图中。

计算gate值

为了加快我们的实现,我们会同时运用整个LSTM层转换。

locali2h =nn.Linear(input_size,4 *rnn_size)(input)-- input to hidden localh2h =nn.Linear(rnn_size,4 *rnn_size)(prev_h)-- hidden to hidden localpreactivations =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
localpre_sigmoid_chunk =nn.Narrow(2,1,3 *rnn_size)(preactivations)
localall_gates =nn.Sigmoid()(pre_sigmoid_chunk)
-- input
localin_chunk =nn.Narrow(2,3 *rnn_size +1,rnn_size)(preactivations)
localin_transform =nn.Tanh()(in_chunk)
在非线性操作之后,我们需要增加更多的nn.Narrow,然后我们就完成了gates。

localin_gate =nn.Narrow(2,1,rnn_size)(all_gates)
localforget_gate =nn.Narrow(2,rnn_size +1,rnn_size)(all_gates)
localout_gate =nn.Narrow(2,2 *rnn_size +1,rnn_size)(all_gates)

Cell和hidden state

有了计算好的gate值,接下来我们可以计算当前的Cell状态了。所有的这些需要的是两个nn.CMulTable模块(一个用于,一个用于),并且nn.CAddTable用于把它们加到当前的cell状态上。

-- previous cell state contribution
localc_forget =nn.CMulTable()({forget_gate,prev_c})
-- input contribution
localc_input =nn.CMulTable()({in_gate,in_transform})
-- next cell state
localnext_c =nn.CAddTable()({
 c_forget,
 c_input
})
最后,是时候来实现hidden 状态计算了。这是最简单的部分,因为它仅仅是把tanh应用到当前的cell 状态(nn.Tanh)并乘上output gate(nn.CMulTable)。

localc_transform =nn.Tanh()(next_c)
localnext_h =nn.CMulTable()({out_gate,c_transform})

定义模块

现在,如果你想要导出整张图作为一个独立的模块,你可以使用下列代码把它封装起来:

-- 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())
returnnn.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 (编译/刘帝伟 审校/赵屹华、朱正贵、李子健 责编/周建丁)

译者简介: 刘帝伟,中南大学软件学院在读研究生,关注机器学习、数据挖掘及生物信息领域。

链接:深入浅出LSTM神经网络

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
LSTM(长短期记忆)是一种适用于序列数据的深度学习模型。它可以对序列中的长期依赖进行建模,并且在处理时序数据时表现出色。本篇文章将详细介绍基于Pytorch实现LSTM的代码。 首先,我们需要导入所需的库: ```python import torch import torch.nn as nn ``` 接下来,我们定义一个LSTM模型类: ```python class LSTM(nn.Module): def __init__(self, input_size, hidden_size, num_layers, output_size): super(LSTM, self).__init__() self.hidden_size = hidden_size self.num_layers = num_layers self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True) self.fc = nn.Linear(hidden_size, output_size) def forward(self, x): h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).requires_grad_() c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).requires_grad_() out, (hn, cn) = self.lstm(x, (h0.detach(), c0.detach())) out = self.fc(out[:, -1, :]) return out ``` 这里,我们定义了一个LSTM类,它继承了Pytorch的nn.Module类。LSTM类有四个参数:输入大小(input_size)、隐藏大小(hidden_size)、LSTM层数(num_layers)和输出大小(output_size)。在初始化函数中,我们定义了一个nn.LSTM层和一个nn.Linear层。nn.LSTM层接受输入和隐藏大小作为输入,并返回输出和最后的隐藏状态。我们使用nn.Linear层将LSTM的最后一个输出映射到所需的输出大小。 在前向函数中,我们首先定义了初始的隐藏状态h0和记忆状态c0。然后,我们将输入x和初始隐藏状态h0、记忆状态c0传递给nn.LSTM层,并获得输出out和最终隐藏状态hn、记忆状态cn。我们使用out[:, -1, :]来获取LSTM的最后一个输出。最后,我们将LSTM的最后一个输出传递给nn.Linear层,并返回输出。 接下来,我们可以实例化LSTM类并定义一些超参数: ```python input_size = 28 hidden_size = 128 num_layers = 2 output_size = 10 learning_rate = 0.001 ``` 在这里,我们定义了输入大小为28(MNIST数据集中的图像大小为28x28),隐藏大小为128,LSTM层数为2,输出大小为10(MNIST数据集中的类别数为10),学习率为0.001。 接下来,我们可以加载MNIST数据集并定义一个训练函数: ```python train_dataset = torchvision.datasets.MNIST(root='./data', train=True, transform=transforms.ToTensor(), download=True) test_dataset = torchvision.datasets.MNIST(root='./data', train=False, transform=transforms.ToTensor()) train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=100, shuffle=True) test_loader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=100, shuffle=False) model = LSTM(input_size, hidden_size, num_layers, output_size) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate) def train(model, train_loader, criterion, optimizer): for epoch in range(num_epochs): for i, (images, labels) in enumerate(train_loader): images = images.view(-1, sequence_length, input_size) labels = labels optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() if (i+1) % 100 == 0: print('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}'.format(epoch+1, num_epochs, i+1, len(train_loader), loss.item())) ``` 在这里,我们使用Pytorch内置的MNIST数据集来加载数据。我们使用torch.utils.data.DataLoader来创建数据加载器。我们实例化了我们之前定义的LSTM模型,并定义了损失函数和优化器。在训练函数中,我们使用for循环来迭代训练数据集。我们将输入图像reshape为(batch_size,sequence_length,input_size)的形状。然后,我们将标签传递给模型并计算输出。我们使用损失函数计算损失,并使用反向传播和优化器来更新模型参数。最后,我们打印出训练损失。 最后,我们可以定义一个测试函数来测试模型的性能: ```python def test(model, test_loader): with torch.no_grad(): correct = 0 total = 0 for images, labels in test_loader: images = images.view(-1, sequence_length, input_size) labels = labels outputs = model(images) _, predicted = torch.max(outputs.data, 1) total += labels.size(0) correct += (predicted == labels).sum().item() print('Accuracy of the model on the 10000 test images: {} %'.format(100 * correct / total)) ``` 在这里,我们使用with torch.no_grad()上下文管理器来关闭梯度计算。我们使用for循环迭代测试数据集,并将输入图像reshape为(batch_size,sequence_length,input_size)的形状。然后,我们将标签传递给模型并计算输出。我们使用torch.max函数来获取每个输出张量中的最大值,并返回一个元组(值,索引)。我们使用索引作为预测值,并将其与真实标签进行比较。最后,我们打印出测试准确率。 最后,我们可以训练和测试我们的LSTM模型: ```python num_epochs = 2 sequence_length = 28 train(model, train_loader, criterion, optimizer) test(model, test_loader) ``` 在这里,我们定义了训练周期数为2,序列长度为28。我们使用train函数来训练模型,并使用test函数来测试模型的性能。 这就是基于Pytorch实现LSTM的详细说明。在实际应用中,您可以使用此代码作为基础,并根据需要进行修改。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值