循环神经网络的原理分析

原理介绍

循环神经网络在大多数人看来其实很简单。的确原理是非常简单的,但是代码的实现细节却没有那么简单。往往纸上谈兵容易,实战起来才发现跟理论是有差距的。本文重点介绍RNN的原理,之后基于PyTorch框架,介绍RNN类的输入和输出,重点在于理解RNN的原理。结尾我们发现PyTorch的RNN模块其实只计算了隐藏层矩阵。

下图是一个简单RNN的单元图,x_t为t时刻的输入,y_t为t时刻的输出,h_t为隐藏层输出,W_{hh}, W_{ih}为隐藏层和输入层的权重,V为输出层权重。

-w625

h_t = \tanh(W_{hh}h_{t-1}+W_{ih}x_t) \\ y_t = V h_t

上面就是循环神经网络单元的计算公式。接下来我们来看PyTorch的实现代码

代码实现

定义RNN的输入特征数为10,输出特征数为20,2个隐藏层的神经网络。代码如下:

inputs = t.randn(5, 3, 10)

rnn = t.nn.RNN(10, 20, 2) # 这里是两层网络
h0 = t.randn(2, 3, 20) # 两层都初始化h0

output, hn = rnn(inputs, h0)
print(output.shape, hn.shape)

# (torch.Size([5, 3, 20]), torch.Size([2, 3, 20]))

解释一下输入和输出数据。
* inputs为输入数据[5, 3, 10],其中序列的长度为5,特征数为10,3个batch的数据;
* h_0为隐藏状态的初始值,由于是单向网络,网络层数数为2,所以第一个值是2, 第二个值是batch_size=3,第三个数20是hidden_size。
* h_n为隐藏层的输出状态,其shape与h_0相同,
* output为神经网络的输出,shape就是[5, 3, 20]了。

每个参数的含义如下:

inputs ==> [src_len,batch_size, input_size]
h0 ==> [num_layers * num_directions, batch_size, hidden_size]
output ==> [src_len, batch_size, hidden_size]

在某些情况下只需要RNN的最后一层输出,只需要取output[-1, :, :]就是最后一层的输出了。

进一步分析代码

文章到这里其实RNN的原理已经介绍完了,但是权重的shape还没有介绍。对于上面的例子可以输出权重的shape:

for params in rnn.parameters():
    print(params.shape)

# torch.Size([20, 10])
# torch.Size([20, 20])
# torch.Size([20])
# torch.Size([20])
# torch.Size([20, 20])
# torch.Size([20, 20])
# torch.Size([20])
# torch.Size([20])

有两层网络,因此可以考到有8个权重值,一维的[20]是bias值,[20, 10]是输入层的权重,第二个[20, 20]为隐藏层的权重,之后隐藏层权重都是[20, 20]了。到这里似乎还没有发现问题,继续往下分析,我们再来看看每一步计算的shape。回到上面的公式:

h_t = \tanh(W_{hh} h_{t-1} + W_{ih} x_t)

根据上面的例子,输入x_t的shape为[5, 3, 10],而W_{ih}的shape是[20, 10],根据矩阵的乘法应该是W_{ih}的转置乘以x_t,得到[5, 3, 20]。再来看第一项W_{hh}的shape为[20, 20], 而h_{t-1}的shape看起来是[2, 3, 20],其实第一个参数2表示num_layers * num_directions,参与计算的shape为[1, 3, 20],做矩阵乘法以后仍然是[1, 3, 20]。shape([5, 3, 20])+shape([1, 3, 20]) = shape([5, 3, 20]),注意这里是两个矩阵相加,不是cat拼接。也就是说h_{t}的shape似乎不是[1, 3, 20],

也就是说根据上面的公式,h_t的shape不知道怎么得到的,于是我去查看源码,发现这部分的矩阵是由C/C++完成的。既然查看源码有点麻烦,那就只能猜了。根据上面打印的参数来看,隐藏层应该是输出层的一部分,因为只有8个矩阵,于是比较一下最后一个输出output[-1, :, :]和最后一个隐藏层的参数hn[-1, :, :],结果发现是一致的。

output[-1:,:,:] == hn[-1, :, :]
# 返回结果都是True

那么PyTorch中RNN类真实的计算网络图应该就是下面这个了(手绘的,还请凑合看)
-w658

也就是说隐藏层是网络最后一层的输出,而输出层是包含中间状态的隐藏层。

总结

跟上面的RNN计算公式相比,隐藏层的输出再接一个线性层就是完整的RNN了。这种RNN有一个小问题,就是每个输出y_t都是独立的,如果用RNN来做序列的分类问题,没有影响,只要取最后一个输出层,做分类就可以了。如果是一个序列标注问题,就应该考虑输出之间的关系了,最简单的可以考虑y_t=f(y_{t-1}, h_t),或者双向的y_t=f(y_{t_1}, y_{t+1}, h_t).除此之外甚至可以接CRF作为序列标注的输出层。

标签:

发表评论

电子邮件地址不会被公开。