原文链接:https://colah.github.io/posts/2015-08-Understanding-LSTMs/
人类并不是每时每秒都从零开始思考的。当你读这篇文章时,你根据你对以前单词的理解来理解每个单词。你不会把所有东西都扔掉,从零开始思考。你的想法是持久的。
传统的神经网络不能做到这一点,这似乎是一个主要的缺点。例如,想象一下,您希望对电影中每个点发生的事件进行分类。目前还不清楚传统神经网络如何利用其关于电影中以前事件的推理来告知以后的事件。
RNN解决了此问题。它们是带有循环的网络,允许信息持续存在。
在上图中,我们可以使用一个对A(神经网络模块)正在读取信息 x i x_i xi,并输出一个值 h i h_i hi,但这种循环允许信息从网络的一个步骤传递到下一步骤中。
下图中展示了一个递推式的神经网络,这类网络类似于同一个网络的多个副本,每个副本都会将消息传递给继承者。
RNN 的吸引力之一是,它们可能能够将以前的信息连接到当前任务,有时,我们只需要查看最近的信息来执行当前任务。
例如,考虑一个语言模型,尝试根据以前的单词预测下一个单词。如果我们试图预测"云在天空中"中的最后一个词,我们不需要任何进一步的上下文——很明显,下一个词将是天空。在这种情况下,如果相关信息与所需位置之间的差距很小,RNN 可以学会使用过去的信息。
不幸的是,随着这一差距的扩大,RNN无法学会连接信息。
值得庆幸的是,LSTM能够处理这些“长期依赖”的问题!
RNN 的输入为 x, 网络参数包括 W h h W_{hh} Whh、 W x h W_{xh} Wxh、 W h y W_{hy} Why, 中间迭代的 hidden 向量为 h t h_t ht.
公式显示如下:
输入的 x t x_t xt 会 与 W x h W_{xh} Wxh 计算dot, 相当于onehot 的 lookup, 得到一个embedding 向量( W x h W_{xh} Wxh),同时,中间上一个阶段的隐藏向量 h t − 1 h_{t-1} ht−1与 W h h W_{hh} Whh 计算dot, 然后结果与 x t x_t xt 的 embedding 相加后,过一层 激活函数 得到新的隐藏层 h t h_t ht 。最后, y t y_t yt 是由 W h y W_{hy} Why 与隐藏向量 h t h_{t} ht 计算dot得到的。
LSTM (长期依赖问题)明确设计旨在避免长期依赖问题。
所有循环神经网络都以神经网络重复模块链的形式存在。在标准 RNN 中,此重复模块将具有非常简单的结构,例如单个 tanh 层。
上图是标准的RNN中的重复模块包含的单个层。
LSTM 也有类似结构的链条,但重复模块具有不同的结构。而不是有一个单一的神经网络层,这里有四个,以非常特殊的方式互动。
其中符号表达的意思是:粉红色圆圈表示按位 pointwise 的操作,如矢量添加,而黄色框则表示神经网络层。
LSTM 的关键是每个细胞状态,横向线穿过图表顶部。细胞状态有点像传送带,它直接沿着整个链条运行。
LSTM 确实能够删除或添加信息到细胞状态。
"×”表示向量之间的点乘,“+”表示着相加。
通过sigmoid函数变换后,得到对了0和1的数值,1表示:能够通过,而0表示不能通过。
Step1:决定从细胞中丢掉哪些信息
在我们 LSTM 中的第一步是决定我们会从细胞状态中丢弃什么信息。这个决定通过一个称为忘记门层。在运行完这一步后,运行sigmoid函数后得到了0和1的值。
这个忘记的决定取决于sigmoid函数,得到了在细胞 C t − 1 C_{t-1} Ct−1中对信息 x t x_t xt的选择过程,得到的 f t f_t ft=1表示要留住信息,否者丢弃信息。
Step2:决定哪些新信息需要存储
这一步是确定什么样的新信息被存放在细胞状态中。这里包含两个部分。
第一:sigmoid 层称 “输入门层” 决定什么值我们将要更新。
第二:一个 tanh 层创建一个新的候选值向量,会被加入到状态中,创建这些新值的载体 C t ∼ \mathop {{C_t}}\limits^ \sim Ct∼.
Step3:更新旧细胞状态
这一步中,主要内容是更新就细胞状态: C t − 1 C_{t-1} Ct−1更新为 C t C_{t} Ct。
我们把旧状态 C t − 1 C_{t-1} Ct−1与 f t f_t ft相乘,丢弃掉我们确定需要丢弃的信息。接着加上 i t ∗ C t ∼ {i_t} * \mathop {{C_t}}\limits^ \sim it∗Ct∼。这就是新的候选值 C t C_{t} Ct,根据我们决定更新每个状态的程度进行变化。
输入为上一阶段的 C t − 1 C_{t-1} Ct−1,得到的是输入到下个阶段的 C t C_{t} Ct
这个过程的理解:删除掉有关旧信息同时添加一些新的信息。
Step4:决定输出什么
最终,我们需要确定输出什么值。这个输出将会基于我们的细胞状态,但是也是一个过滤后的版本。首先,我们运行一个 sigmoid 层来确定细胞状态的哪个部分将输出出去。接着,我们把细胞状态 C t C_t Ct通过 tanh 进行处理(得到一个在 0到 1之间的值)并将它和 sigmoid 门的输出相乘,最终我们仅仅会输出我们确定输出的那部分。
在语言模型的例子中,因为他就看到了一个 代词,可能需要输出与一个 动词 相关的信息。例如,可能输出是否代词是单数还是负数,这样如果是动词的话,我们也知道动词需要进行的词形变化.
(1)其中一个流形的 LSTM 变体,就是由 Gers & Schmidhuber (2000) 提出的,增加了 “peephole connection”。是说,我们让 门层 也会接受细胞状态的输入。下面的图例中,我们增加了 peephole 到每个门上。
(2)另一个变体是通过使用 coupled 忘记和输入门。不同于之前是分开确定什么忘记和需要添加什么新的信息,这里是一同做出决定。我们仅仅会当我们将要输入在当前位置时忘记。我们仅仅输入新的值到那些我们已经忘记旧的信息的那些状态 。
(3)另一个改动较大的变体是 Gated Recurrent Unit (GRU),这是由 Cho, et al. (2014) 提出。它将忘记门( r t r_t rt)和输入门( z t z_t zt)合成了一个单一的更新门。同样还混合了细胞状态和隐藏状态,和其他一些改动。最终的模型比标准的 LSTM 模型要简单,也是非常流行的变体。
import torch
import torch.nn as nn
import torch.nn.functional as F
# implementation
import math
class NaiveCustomLSTM(nn.Module):
def __init__(self, input_sz: int, hidden_sz: int):
super().__init__()
self.input_size = input_sz
self.hidden_size = hidden_sz
#i_t
self.W_i = nn.Parameter(torch.Tensor(input_sz, hidden_sz))
self.U_i = nn.Parameter(torch.Tensor(hidden_sz, hidden_sz))
self.b_i = nn.Parameter(torch.Tensor(hidden_sz))
#f_t
self.W_f = nn.Parameter(torch.Tensor(input_sz, hidden_sz))
self.U_f = nn.Parameter(torch.Tensor(hidden_sz, hidden_sz))
self.b_f = nn.Parameter(torch.Tensor(hidden_sz))
#c_t
self.W_c = nn.Parameter(torch.Tensor(input_sz, hidden_sz))
self.U_c = nn.Parameter(torch.Tensor(hidden_sz, hidden_sz))
self.b_c = nn.Parameter(torch.Tensor(hidden_sz))
#o_t
self.W_o = nn.Parameter(torch.Tensor(input_sz, hidden_sz))
self.U_o = nn.Parameter(torch.Tensor(hidden_sz, hidden_sz))
self.b_o = nn.Parameter(torch.Tensor(hidden_sz))
self.init_weights()
def init_weights(self):
stdv = 1.0 / math.sqrt(self.hidden_size)
for weight in self.parameters():
weight.data.uniform_(-stdv, stdv)
def forward(self,
x,
init_states=None):
"""
assumes x.shape represents (batch_size, sequence_size, input_size)
"""
bs, seq_sz, _ = x.size()
hidden_seq = []
if init_states is None:
h_t, c_t = (
torch.zeros(bs, self.hidden_size).to(x.device),
torch.zeros(bs, self.hidden_size).to(x.device),
)
else:
h_t, c_t = init_states
for t in range(seq_sz):
x_t = x[:, t, :]
i_t = torch.sigmoid(x_t @ self.W_i + h_t @ self.U_i + self.b_i)
f_t = torch.sigmoid(x_t @ self.W_f + h_t @ self.U_f + self.b_f)
g_t = torch.tanh(x_t @ self.W_c + h_t @ self.U_c + self.b_c)
o_t = torch.sigmoid(x_t @ self.W_o + h_t @ self.U_o + self.b_o)
c_t = f_t * c_t + i_t * g_t
h_t = o_t * torch.tanh(c_t)
hidden_seq.append(h_t.unsqueeze(0))
#reshape hidden_seq p/ retornar
hidden_seq = torch.cat(hidden_seq, dim=0)
hidden_seq = hidden_seq.transpose(0, 1).contiguous()
return hidden_seq, (h_t, c_t)
layer = NaiveCustomLSTM(4, 5)
t = torch.ones(10, 3, 4) #batch_size, seq_size, input_size
import math
class CustomLSTM(nn.Module):
def __init__(self, input_sz, hidden_sz):
super().__init__()
self.input_sz = input_sz
self.hidden_size = hidden_sz
self.W = nn.Parameter(torch.Tensor(input_sz, hidden_sz * 4))
self.U = nn.Parameter(torch.Tensor(hidden_sz, hidden_sz * 4))
self.bias = nn.Parameter(torch.Tensor(hidden_sz * 4))
self.init_weights()
def init_weights(self):
stdv = 1.0 / math.sqrt(self.hidden_size)
for weight in self.parameters():
weight.data.uniform_(-stdv, stdv)
def forward(self, x,
init_states=None):
"""Assumes x is of shape (batch, sequence, feature)"""
bs, seq_sz, _ = x.size()
hidden_seq = []
if init_states is None:
h_t, c_t = (torch.zeros(bs, self.hidden_size).to(x.device),
torch.zeros(bs, self.hidden_size).to(x.device))
else:
h_t, c_t = init_states
HS = self.hidden_size
for t in range(seq_sz):
x_t = x[:, t, :]
# batch the computations into a single matrix multiplication
gates = x_t @ self.W + h_t @ self.U + self.bias
i_t, f_t, g_t, o_t = (
torch.sigmoid(gates[:, :HS]), # input
torch.sigmoid(gates[:, HS:HS*2]), # forget
torch.tanh(gates[:, HS*2:HS*3]),
torch.sigmoid(gates[:, HS*3:]), # output
)
c_t = f_t * c_t + i_t * g_t
h_t = o_t * torch.tanh(c_t)
hidden_seq.append(h_t.unsqueeze(0))
hidden_seq = torch.cat(hidden_seq, dim=0)
# reshape from shape (sequence, batch, feature) to (batch, sequence, feature)
hidden_seq = hidden_seq.transpose(0, 1).contiguous()
return hidden_seq, (h_t, c_t)
参考文献:
[1] https://colah.github.io/posts/2015-08-Understanding-LSTMs/
[2] LSTM如何解决梯度消失问题:https://zhuanlan.zhihu.com/p/28749444
代码 github:https://github.com/Stormzudi/Deep-Learning/tree/master/LSTM