本文将介绍 CNN(卷积神经网络)和 RNN(循环神经网络)。需要说明的是, 本系列文章是介绍如何用锤子敲钉子的,而不是如何造锤子或者为什么要敲的。所以 CNN 和 RNN 的原理与使用场景在这里从略,仅讨论 CNN 和 RNN 的 PyTorch 实现。CNN 独有的层包括卷积层(convolution layer),池化层(pooling layer),转置卷积层(transposed convolution layer),反池化层(unpooling layer)。卷积层与池化层在 CNN 中最常用,而转置卷积层与反池化层通常用于计算机视觉应用里的图像再生,对于 NLP 来说应用不多,在此不再赘述。而 RNN 的拓扑结构与 MLP、CNN 完全不同,因此学习起来会有很大的困扰,了解 RNN 的工作原理对正确使用 RNN 大有裨益,所以在此附上参考资料 1[1] 2[2] 3[3] ,供读者参考。
从工程实现的角度来说,一个 CNN 网络可以分成两部分:特征学习阶段与分类阶段。特征学习层由多层卷积层与池化层叠加,之间使用 relu 作为激活函数。卷积层的作用是使信息变深(层数增加),通常会使层的长宽减小;池化层的作用是使信息变窄,提取主要信息。之后进入分类层,将信息变成一维向量,经过 1-3 层全连接层与 relu 之后,经过最终的 softmax 层进行分类;若目标为二分类,则也可以经过 sigmoid 层。
卷积层有三个类,分别是:
torch.nn.Conv1d
torch.nn.Conv2d
torch.nn.Conv3d
这三个类分别对应了文本(一维数据)、图片(二维数据)和视频(三维数据)。它们的维度如下:可见,三个类处理的数据的前两维是完全一致的。此外,三个类的参数也完全一致,以 torch.nn.Conv2d
为例:
torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True, padding_mode='zeros')
in_channels
:输入张量的层数;out_channels
:输出张量的层数;kernel_size
:卷积核的大小,整数或元组;stride
:卷积的步长,整数或元组;padding
:填充的宽度,整数或元组;dilation
:稀释的跨度,整数或元组;groups
:卷积的分组;bias
:偏置项;padding_mode
:填充的方法。当所有尺寸均为矩形的时候,输出张量的长和宽的数值为:
池化层的权重是随机初始化的,不过我们也可以手动设定。
>>> conv = torch.nn.Conv2d(1, 1, 3, bias=0.) # 定义一个 3x3 的卷积核
>>> nn.init.constant_(conv.weight.data, 1.) # 卷积核的权重设为 1.
>>> print(Convolutional.weight.data)
tensor([[[[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]]]])
>>> tensor = torch.linspace(16., 1., 16).reshape(1, 1, 4, 4) # 定义一个张量
>>> print(tensor)
tensor([[[[16., 15., 14., 13.],
[12., 11., 10., 9.],
[ 8., 7., 6., 5.],
[ 4., 3., 2., 1.]]]])
>>> conv(tensor) # 卷积操作
tensor([[[[99., 90.],
[63., 54.]]]], grad_fn=<MkldnnConvolutionBackward>)
上例中,卷积核是一个 的全 1 张量;在卷积运算中,卷积核先与张量中前三排中的前三个元素进行 elementwise 的乘法,然后相加,得到输出张量中的第一个元素。然后向右滑动一个元素(因为 stride
默认是 1),重复卷积运算;既然达到末尾,返回左侧向下滑动一个单位,继续运算,直到到达末尾。
与卷积层对应的,池化层分为最大池化和平均池化两种,每种也有三个类:
torch.nn.MaxPool1d
torch.nn.MaxPool2d
torch.nn.MaxPool3d
torch.nn.AvgPool1d
torch.nn.AvgPool2d
torch.nn.AvgPool3d
所谓“池化”,就是按照一定的规则(选取最大值或计算平均值)在输入层的窗口里计算数据,返回计算结果。它们的参数也一致,最大池化层只有三个参数:
kernel_size
:卷积核的大小,整数或元组;stride
:卷积的步长,整数或元组;padding
:填充的宽度,整数或元组;一维平均池化层有额外的两个参数:
ceil_mode
:对结果进行上取整;count_include_pad
:是否将 padding 纳入计算;二维及三维平均池化层有额外的一个参数:
divisor_override
:指定一个除数。
一个 trick:当 , 的时候,输出张量的尺寸是输入张量的一半。
>>> pool = torch.nn.MaxPool2d(2) # 定义一个大小为 2x2 的核
>>> pool(tensor) # 池化操作
tensor([[[[16., 14.],
[ 8., 6.]]]])
RNN(recurrent neural network)擅长处理序列内容,因此在 NLP 中应用较多。
RNN 主要有三个实现:原始 RNN 和 RNN 的改进版 LSTM 和 GRU。一个循环神经网络主要由输入层、隐藏层(RNN 层)、输出层构成,两层之间由激活函数相连。不像 MLP、CNN 那样多个隐藏层必须显式地写出来,RNN 的隐藏层可以以一个 RNN 的参数表示。所以 RNN 网络的格式是:
而 RNN、LSTM 和 GRU 的类也是大同小异:
torch.nn.RNN(input_size, hidden_size, num_layers=1, nonlinearity='tanh', bias=True, batch_first=False, dropout=0, bidirectional=False)
torch.nn.LSTM(input_size, hidden_size, num_layers=1, bias=True, batch_first=False, dropout=0, bidirectional=False)
torch.nn.GRU(input_size, hidden_size, num_layers=1, bias=True, batch_first=False, dropout=0, bidirectional=False)
可以看到,torch.nn.RNN
比其它两个类就多了一个参数 nonlinearity
,这是因为 RNN 里的激活函数可以是 tanh
也可以说 relu
,而另外两个类的激活函数已经定义好了。下面逐一说明一下:
input_size
:输入 x 中的特征数;hidden_size
:隐藏层的特征数;num_layers
:隐藏层的数量;bias
:是否有偏置项;batch_first
:数据维度中批是否在第一项;dropout
:是否有 dropout;bidirectional
:RNN 是单向还是双向。RNN 实例接受的参数有两个:一个张量和上一次的隐藏层:
>>> rnn = RNN(input_size, hidden_size)
>>> output, hidden_current = rnn(input, hidden_previous)
RNN 的输出有两个,分别是输出值和当前的隐藏层。在 batch_first=True
的时候,当前的隐藏层的维度为 (batch, seq_len, num_directions*hidden_size)
,而前一个隐藏层的维度为 batch, num_layers*num_directions, hidden_size
。我们来看一个例子:我们首先创建一个接受维度为 (1, 5, 2)
(每批一个数据点,每个数据点有 5 个特征,两个隐藏层)的 RNN 层,其它参数使用默认参数:
>>> rnn = torch.nn.LSTM(1, 5, 2, batch_first=True)
然后创建一个两批、每批 3 个数据点、每个数据点一个特征的张量:
>>> a = torch.rand(2, 3, 1)
>>> print(a)
tensor([[[0.9472],
[0.1003],
[0.7684]],
[[0.8318],
[0.7707],
[0.2214]]])
将这个张量喂给 RNN:
>>> out, h = rnn(a)
这里我们没有给 RNN 网络是一个隐藏层的数值,所以 RNN 自动创建了一个权重全为 0 的隐藏层。我们看一下输出:
>>> print(out.size())
torch.Size([2, 3, 5])
>>> print(out)
tensor([[[ 0.0620, 0.0790, -0.0028, -0.1094, 0.1258],
[ 0.0840, 0.0963, -0.0315, -0.1287, 0.1837],
[ 0.0983, 0.1190, -0.0491, -0.1257, 0.2184]],
[[ 0.0612, 0.0764, -0.0047, -0.1101, 0.1244],
[ 0.0865, 0.1130, -0.0228, -0.1283, 0.1899],
[ 0.0992, 0.1151, -0.0485, -0.1235, 0.2183]]],
grad_fn=<TransposeBackward0>)
>>> print(h[0].size())
torch.Size([2, 2, 5])
>>> print(h)
(tensor([[[-0.0562, -0.0368, -0.1863, -0.2322, 0.0921],
[-0.0424, -0.0347, -0.1600, -0.1809, 0.1258]],
[[ 0.0983, 0.1190, -0.0491, -0.1257, 0.2184],
[ 0.0992, 0.1151, -0.0485, -0.1235, 0.2183]]],
grad_fn=<StackBackward>),
tensor([[[-0.1437, -0.0643, -0.3578, -0.3889, 0.1648],
[-0.1044, -0.0650, -0.3243, -0.3031, 0.2357]],
[[ 0.1939, 0.1787, -0.0983, -0.2349, 0.3685],
[ 0.1932, 0.1733, -0.0973, -0.2295, 0.3687]]],
grad_fn=<StackBackward>))
为什么会是这样呢?模型和输入张量的维度分别为:
LSTM(input_size, hidden_size, num_layer)
1 5 2
trnsor(batch (if 'batch_first=True'), seq_len, input_size)
2 3 1
输出张量的维度为:
out.shape: 2, 3, 5
batch, seq_len, num_directions*hidden_size
hidden.shape: 2, 2, 5
num_layers*num_directions, batch, hidden_zie
是不是一目了然?这里要注意,RNN 在内部运算的时候,张量的维度是 (inpu_size, batch, hidden_size)
,虽然我们设置 batch_first=True
将输入和输出的张量的 batch 放到了第一维,输入和输出的 hidden 的 batch 仍然在第二维。
RNN 的改进版 LSTM 和 GRU 的原理可以看这里 4[4] 5[5] 6[6]。
读PyTorch源码学习RNN(1): https://zhuanlan.zhihu.com/p/32103001
[2]PyTorch 学习笔记(十一):循环神经网络(RNN): https://zhuanlan.zhihu.com/p/80866196
[3]零基础入门深度学习(5) - 循环神经网络: https://zybuluo.com/hanbingtao/note/541458
[4]LSTM细节分析理解(pytorch版): https://zhuanlan.zhihu.com/p/79064602
[5]LSTM神经网络输入输出究竟是怎样的?: https://www.zhihu.com/question/41949741/answer/318771336
[6]超生动图解LSTM和GRU:拯救循环神经网络的记忆障碍,就靠它们了: https://zhuanlan.zhihu.com/p/46981722
微信扫一扫
关注该公众号