这篇文章主要是对之前一段时间里接触到的 循环神经网络 的相关知识进行一些总结,包括个人觉得初学难理解或者需要注意的问题和如何使用Pytorch的相关函数。由于这些经典结构网上资料很多,所以一些通识不再陈述,偏重代码部分。
1.RNN
很多问题都归结于序列信息的处理,例如 speech recognization,machine translation等等,RNN就是为了解决这类问题的结构,这里的RNN含义为循环神经网络(recurrent neural network)而非递归神经网络(recursive neural network)。序列信息可以看作是不同时间点输入相同格式的数据,那么使用一个结构循环处理不同时间点的数据,那么这也就是RNN网络了,所以很多介绍RNN的地方都会有那张经典的RNN展开的图了:
这一类介绍资源非常多,所以不再赘述。RNN的关键在于它的计算公式:
s
t
=
f
(
U
⋅
x
t
+
W
⋅
s
t
−
1
)
o
t
=
s
o
f
t
m
a
x
(
V
⋅
s
t
)
s_t = f(U\cdot x_t + W\cdot s_{t-1}) \\ o_t = softmax(V\cdot s_t)
st=f(U⋅xt+W⋅st−1)ot=softmax(V⋅st)
说明:
-
x
t
x_t
xt是某个时刻的输入信息,序列信息可以看作是不同时间的连续输入,所以每个时间点都会输入信息。
s
t
s_t
st 表示隐藏信息,对于序列信息的处理,很重要的一点就是上文信息会影响到下文信息,所以需要有一个结构来储存之前的所有信息。
o
t
o_t
ot表示某个时间点的输出信息。
RNN有几个特点:
- 每个时间点都会输出一个隐藏状态,但是显然我们并不需要全部的信息,例如在对文本进行分类的时候,我们往往只是使用最后一个时刻的隐藏状态,然后通过一个分类器即可。
- 权值共享,实际上是一个结构对不同时刻的信息进行处理,所以所有的权重实际上都是相同的。
- RNN也使用BP算法来更新参数,但是与之前的神经网络不同的是,这里的梯度计算需要依赖于之前的所有步,然后将梯度累加,这被称为 BPTT(Backpropation Through Time)。
RNN更像是一个理想的结构,实际上是不怎么使用的,如果实际推导一下 BPTT 的过程,就会发现在累乘的过程中,由于激活函数的累乘,会出现 梯度爆炸 和 梯度消失 的问题,并且,在实践中发现依靠隐藏单元并不能很好的保存序列信息,这些都使得 RNN 在实践中使用的并不多。但是 RNN 的很多变种在实践中得到了很广泛的使用,例如 LSTM 以及 GRU 等。
下面来看 RNN 在 Pytorch 中的实现,Pytorch中有两个函数实现 RNN:
- torch.nn.RNNCell():这个函数只能接收单步的输入,并且必须传入隐藏状态,也就是必须手动一步一步输入序列信息,所以比较麻烦
- torch.nn.RNN() :这个函数可以接收一个序列信息,对于初始隐藏状态,可以不指定(默认全0)也可以自己指定
这里介绍第二种方式,个人觉得应该注意的地方有三个:
- 第一是函数中参数都是什么意思
- 输入数据的格式,每一维度代表什么意思
- 输出数据的格式,每一维度代表什么意思
def __init__(self,
input_size: int, 输入数据的特征数
hidden_size: int, 隐藏层的特征数
num_layers: int = ..., 网络层数
bias: bool = ..., 是否使用偏置 默认是 true
batch_first: bool = ..., 如果是True 那么输入的tensor的shape是[batch_size, n_step, input_size]
输出的时候也是 [batch_size, n_step, input_size]
默认为False 也就是 [n_step, batch_size, input_size]
dropout: float = ..., 如果非零 则除了最后一层意外 其他层都会在输出时加上一个 dropout层
bidirectional: bool = ..., 是否使用双向RNN 默认是 false
nonlinearity: str = ...) 非线性激活函数,默认是 tanh
输入数据: 对于数据x: (sequence_length, batch_size, input_size)
初始隐藏层: (num_layers *(1:单向, 2 双向),batch_size, hidden_size)
输出结果: 对于输出数据(每一步都会有一个输出) (sequence_length, batch, hidden_size * 方向数量)
输出状态(这里会输出最后一步的隐藏状态) (num_layers * 方向数, batch_size, hidden_size)
RNN 单元的计算公式为 h_t = tanh(W_ih * x_t + b_in + W_hh * h_t-1 + b_nn)
这里写一些我自己的一些理解:
-
首先是 input_size, 这个参数,有些地方也会写成 feature_size, 其实就是输入数据的特征数量,举个例子,在做预测文本的时候,序列中的每个单词都会表示成为固定长度的向量(one-hot 或者是 word embedding),这个固定的长度也就是 input_size 了。
-
关于 hidden_size, 表示隐藏层特征数,或者说是隐藏层神经元数,这个参数一开始搞得我头大,因为很多这方面的介绍里都会在停在Figure 1中那样的层面,这里的 hidden_size 要在更低一点的层面上理解,在RNN单元的计算公式里我们可以看到实际上是进行两次线性变换然后做一个激活,这两个线性变换实际上就像我们之前做的全连接网络,在那里我们都知道 hidden_size 的意思,就是隐层的神经元个数,所以这里也是一样,也就是说,RNN 单元内部就相当于并列的全连接层,我觉得这样会好理解一点这个 hidden_size。
-
这里实际上依然是上一条的补充,如果我们考虑一下 RNN 单元内部的各种参数的维度,这里我在书上找到了一张图,可能会方便理解:
这张图展示的是数据的流动,如果更细一点:
但是这张图有一点与上面 RNN 的公式不同,这里先是对 某一时刻的输入 x_t 和上一时刻的隐藏状态 s_t-1 做了一个拼接,然后再做线性变换,这相当于把 W_ih 和 W_hh 合并在一起了,其实分开是一样的结果,可以按矩阵乘法自己算一下,CS224n 中在讲 RNN 的时候也提到这个问题,可以参考。另外,我觉得非常重要的一点是:虽然公式中往往都是写成
W
⋅
x
W \cdot x
W⋅x 类似 参数矩阵 乘以 数据的形式,实际上计算的时候却是
x
⋅
W
x \cdot W
x⋅W 的形式。我不清楚是不是都是这样,但是按照这样的理解,确实才能推出正确的维度变换,大家在推导的时候也小心一下这个吧,虽然也不是很大的问题。
-
batch_first,这个参数一般都直接使用默认值,也就是在填充数据的时候,往往会先 transpose 数据,把 batch_size 放在第二维。
-
注意输入输出数据的格式,这里我觉得有一个设计的问题(个人意见哈),使用函数的时候我们必须要把数据处理为规定的格式,这里可以看已经规定了 input_size,但是 RNN 的参数里却又要写一遍 input_size,我不是很懂这样做的意义,感觉是多余了。另外,在参数中也可以看到有网络层数和单双向的参数,所以注意因为这个带来的输出的改变。
我对于 RNN 的理解就是这样了,下面看一个稍微实际一点的例子:这个例子的任务是使用RNN来预测下一个单词,完整代码放在 :TextRNN
class TextRNN(nn.Module):
def __init__(self):
super(TextRNN, self).__init__()
# 两个重要的参数 input_size 是输入数据的特征数,这里每个单词都是使用的 one-hot 编码,所以特征数也就是 vocab 的长度
# hidden_size 是隐藏层的特征数 这作为超参数指定
self.rnn = nn.RNN(input_size=n_class, hidden_size=n_hidden)
# 最后要对输出的结果(最后一个output,这里没有使用最后时刻的hidden state)连一个全连接分类器
self.W = nn.Parameter(torch.randn([n_hidden, n_class]).type(dtype))
self.b = nn.Parameter(torch.randn([n_class]).type(dtype))
def forward(self, hidden, x):
# 将输入的数据维度调整为 : [n_step, batch_size, n_class]
x = x.transpose(0, 1)
# outputs : [n_step, batch_size, num_directions=1 * n_hidden]
# hidden : [num_layers(1) * num_directions(1), batch_size, n_hidden]
outputs, hidden = self.rnn(x, hidden)
# 只需要最后一个时刻的输出结果来进行分类 所以取-1
# 这里注意 output 的输出格式 正好第一维是 n_step 也就是步长 所以直接对第一维度切片就可以
outputs = outputs[-1]
# 这里 self.W 的定义是 (n_hidden, n_class) 注意如果使用的是多层或者双向的RNN 需要对应改变W的定义
model = torch.mm(outputs, self.W) + self.b
return model
2.LSTM
RNN在实际中使用的并不多,主要是因为 hidden_state 无法很好的保存历史信息,只能保存短期信息,所以为了解决长期依赖(long-term dependecy)的问题, 提出了 LSTM 的结构。
LSTM 与之前的 RNN 最大的不同就是增加了一些称为 “门(gate)”的结构,实际上就是一些通过 sigmoid 函数控制信息范围的小结构,在RNN中只有一个 tanh 函数来处理信息,而 LSTM 有三个门结构分别负责 遗忘、输入和输出。对于 LSTM 的处理过程,有很多文章写的很清楚,我推荐的是这一篇:
这篇博客对 LSTM 网络以及 门结构 解释非常清楚,并且也对 LSTM 的变种进行了介绍,可以看到很多中文的资料实际上也是参考了这篇文章,例如:
LSTM 作为 RNN 的变种,很多参数设计等等与之前的RNN一样,下面来看 Pytorch 对 LSTM 的实现:
Pytorch 中使用函数 torch.nn.LSTM()
实现LSTM,实际上与 之前介绍的 RNN 是一致的, 所以简单看一下:
torch.nn.LSTM()
def __init__(self,
input_size: int,
hidden_size: int,
num_layers: int = ...,
bias: bool = ...,
batch_first: bool = ...,
dropout: float = ...,
bidirectional: bool = ...,
nonlinearity: str = ...)
可以看到,与 torch.nn.RNN()
参数完全一致,含义也是相同的。与之前一样,要注意输入数据输出数据以及初始状态等等的定义格式:
同样需要对数据进行处理,来符合 LSTM 的要求:这里是batch_first为默认值 False
输入数据: 对于数据x: (sequence_length, batch_size, input_size)
初始hidden state: (num_layers * num_directions,batch_size, hidden_size)
初始cell state:与 hidden_state 相同
输出数据:output: (sequence_length, batch_size, hidden_size * num_directions)
hidden_state: (num_layers * num_directions, batch_size, hidden_size)
cell_state: 与 hidden_state 相同
LSTM 多的结构 cell state
的 shape 总是与 hidden_state
是相同的。
一个小例子,完整代码可以看:TextLSTM
class TextLSTM(nn.Module):
def __init__(self):
super(TextLSTM, self).__init__()
# 定义与 torch.nn.RNN() 是一样的
self.lstm = nn.LSTM(input_size=n_class, hidden_size=n_hidden)
self.W = nn.Parameter(torch.randn([n_hidden, n_class]).type(data_type))
# torch.randn() 函数指定维度的时候可以不用加 [],但是显然加上显得清晰
self.b = nn.Parameter(torch.randn([n_class]).type(data_type))
def forward(self, x):
# 调整数据从 [batch_size, n_step, dims(n_class)] 变成 [n_step, batch_size, dims(n_class)]
x = x.transpose(0, 1)
# hidden_state 和 cell_state 的维度是相同的 num_layers * num_directions = 1
# 这里使用了自定义的 hidden_state 和 cell_state 也可以不使用
init_hidden_state = Variable(torch.zeros(1, len(x), n_hidden))
init_cell_state = Variable(torch.zeros(1, len(x), n_hidden))
# 输出结果会默认输出三个:所有时刻的output,最后时刻的 hidden_state 和 cell_state
# outputs, (_, _) = self.lstm(x) 不使用自定义初始状态
outputs, (_, _) = self.lstm(x, (init_hidden_state, init_cell_state))
# 这里依然使用最后一个时刻的结果作为最后预测(分类的依据)
outputs = outputs[-1]
model = torch.mm(outputs, self.W) + self.b
return model
3. GRU
GRU也是 RNN 结构非常有名的一个变种,它的设计与 LSTM 的目的一样,也可以说是 LSTM 的一个变种, GRU 对 LSTM 的一些门结构进行了重新设计,归结成两个门结构,一个是 重置门(reset gate),另一个是 更新门(update gate)。结构如图:
在 GRU 提出的论文中也提到了 GRU 与 LSTM 的效果是差不多的,只是运算效率上有一些提升,所以 LSTM 与 GRU 没有优劣之分。
至于 Pytorch 对 GRU 的实现,其实与 LSTM 完全一致,只是少了 cell state:
torch.nn.modules.rnn.GRU : 这与之前的 RNN 和 LSTM 参数完全一样
def __init__(self,
input_size: int,
hidden_size: int,
num_layers: int = ...,
bias: bool = ...,
batch_first: bool = ...,
dropout: float = ...,
bidirectional: bool = ...,
nonlinearity: str = ...)
输入数据: 对于数据x: (sequence_length, batch_size, input_size)
初始hidden state: (num_layers * num_directions,batch_size, hidden_size)
输出数据:output: (sequence_length, batch_size, hidden_size * num_directions)
hidden_state: (num_layers * num_directions, batch_size, hidden_size)
和上面的 LSTM 一样,看一个实际的定义,完整的代码见 https://github.com/MirrorN/NLP_beginner/tree/master/LSTM_GRU
class TextGRU(nn.Module):
def __init__(self):
super(TextGRU, self).__init__()
self.gru = nn.GRU(input_size=n_class, hidden_size=n_hidden)
self.W = nn.Parameter(torch.randn([n_hidden, n_class]).type(data_type))
self.b = nn.Parameter(torch.randn([n_class]).type(data_type))
def forward(self, x):
# 调整数据从 [batch_size, n_step, dims(n_class)] 变成 [n_step, batch_size, dims(n_class)]
batch_size = len(x)
x = x.transpose(0, 1)
init_hidden_state = Variable(torch.zeros(1, batch_size, n_hidden))
# 输出结果会默认输出两个:所有时刻的output,最后时刻的 hidden_state
outputs, _ = self.gru(x, init_hidden_state)
# 这里依然使用最后一个时刻的结果作为最后预测(分类的依据)
outputs = outputs[-1]
final_outputs = torch.mm(outputs, self.W) + self.b
return final_outputs
可以看到,基本一致,区别只有两个:
- 如果自定义 初始状态,不需要定义 cell state了
- 输出要比 LSTM 少一个
4. Multi-Layers-LSTM
上面所说的 LSTM 只是单层的结构,根据之前的经验,就像之前的卷积网络,我们会使用多层卷积来提高模型的性能,所以,建立多层的 LSTM 网络也是自然而然的思路,它的结构:
至于在 Pytorch 中如何实现,在之前我们已经看到了 torch.nn.RNN
torch.nn.LSTM
torch.nn.GRU
中都有 num_layers
的参数,所以,实现上与之前的网络改动非常小,只要多指定一下这个参数就好了,然后注意 hidden_state
和 cell_state
的 shape 定义就好了,当然,最后输出的时候也会有影响:完整代码:Multi-layers-LSTM
class Text_Multi_LSTM(nn.Module):
def __init__(self):
super(Text_Multi_LSTM, self).__init__()
# 指定 num_layers 参数
self.lstm = nn.LSTM(input_size=n_class, hidden_size=n_hidden, num_layers=n_layers)
self.W = nn.Parameter(torch.randn([n_hidden, n_class]).type(data_type))
self.b = nn.Parameter(torch.randn([n_class]).type(data_type))
def forward(self, x):
batch_size = len(x)
x = x.transpose(0, 1)
# hidden_state 和 cell_state 的维度是相同的 num_layers * num_directions = 2
init_hidden_state = Variable(torch.zeros(n_layers*1, batch_size, n_hidden))
init_cell_state = Variable(torch.zeros(n_layers*1, batch_size, n_hidden))
# 这里使用最后的 output 结果进行分类预测 所以没有改变,如果要用到最后的hidden_state 和 cell_state 的话那么需要对应改变
outputs, (_, _) = self.lstm(x, (init_hidden_state, init_cell_state))
outputs = outputs[-1]
model = torch.mm(outputs, self.W) + self.b
可以看到,代码的改变非常小。
4. Bi-LSTM
除了在层次上进行改善网络结构,考虑方向是另一个进行改进的方向,毕竟之前LSTM网络是单向地处理序列信息,所以有些时候考虑文本后面地消息可能会提高模型地效果,就像是在进行单词推断地时候,也许文本后面地内容也会对单词地的推测有所帮助,所以,双向LSTM网络被提出,他是在两个方向地 LSTM 结构的组合,这种结构是在1997年被提出的。结构:
就像上面的 Multi-layers LSTM 网络一样,Bi-LSTM 借助框架的函数可以非常简单的实现,只需要注意几个参数的改变即可,例如:完整代码:Bi-LSTM
class Text_Bi_LSTM(nn.Module):
def __init__(self):
super(Text_Bi_LSTM, self).__init__()
# 指定 bidirectional = True
self.lstm = nn.LSTM(input_size=n_class, hidden_size=n_hidden, bidirectional=True)
self.W = nn.Parameter(torch.randn([2*n_hidden, n_class]).type(data_type))
# 注意这里 全连接层 的 W 是 num_directions * hidden_size
self.b = nn.Parameter(torch.randn([n_class]).type(data_type))
def forward(self, x):
batch_size = len(x)
x = x.transpose(0, 1)
# hidden_state 和 cell_state 的维度是相同的 num_layers * num_directions = 2
init_hidden_state = Variable(torch.zeros(1*2, batch_size, n_hidden))
init_cell_state = Variable(torch.zeros(1*2, batch_size, n_hidden))
outputs, (_, _) = self.lstm(x, (init_hidden_state, init_cell_state))
outputs = outputs[-1]
final_output = torch.mm(outputs, self.W) + self.b
return final_output
5.参考
本文中的图片大部分来源于《Tensorflow 实战Google深度学习框架》
代码部分参考Github项目:nlp-tutorial,主要是对源项目代码进行了一些注释
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/116683.html