cover_image

[DL] PyTorch 折桂 11:CNN & RNN

我是老宅 花解语NLP 2020年06月09日 11:00

往期汇总:

  1. PyTorch 折桂 1:张量的性质
  2. PyTorch 折桂 2:张量的运算 1
  3. PyTorch 折桂 3:张量的运算 2
  4. PyTorch 折桂 4:torch.autograph
  5. PyTorch 折桂 5:PyTorch 模块总览 & torch.utils.data
  6. PyTorch 折桂 6:torch.nn 总览 & torch.nn.Module
  7. PyTorch 折桂 7:nn.Linear & 常用激活函数
  8. PyTorch 折桂 8:torch.nn.init
  9. PyTorch 折桂 9:损失函数
  10. PyTorch 折桂 10:torch.optim

本文将介绍 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] ,供读者参考。

1. 卷积神经网络(CNN)工作原理

从工程实现的角度来说,一个 CNN 网络可以分成两部分:特征学习阶段与分类阶段。图片特征学习层由多层卷积层与池化层叠加,之间使用 relu 作为激活函数。卷积层的作用是使信息变深(层数增加),通常会使层的长宽减小;池化层的作用是使信息变窄,提取主要信息。之后进入分类层,将信息变成一维向量,经过 1-3 层全连接层与 relu 之后,经过最终的 softmax 层进行分类;若目标为二分类,则也可以经过 sigmoid 层。

1.1 convolution layer 卷积层

卷积层有三个类,分别是:

  • torch.nn.Conv1d
  • torch.nn.Conv2d
  • torch.nn.Conv3d这三个类分别对应了文本(一维数据)、图片(二维数据)和视频(三维数据)。它们的维度如下:
  • 一维数据是一个 3 维张量:batch * channel * feature;
  • 二维数据是一个 4 维张量:batch * channel * weight * height;
  • 三维数据是一个 5 维张量:batch * channel * frame * weight * height。

可见,三个类处理的数据的前两维是完全一致的。此外,三个类的参数也完全一致,以 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:填充的方法。

当所有尺寸均为矩形的时候,输出张量的长和宽的数值为:

  • 一个 trick:当 的时候,输入张量和输出张量的长宽是不变的。

池化层的权重是随机初始化的,不过我们也可以手动设定。

>>> conv = torch.nn.Conv2d(113, 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(1144# 定义一个张量
>>> 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),重复卷积运算;既然达到末尾,返回左侧向下滑动一个单位,继续运算,直到到达末尾。

1.2 pool layer 池化层

与卷积层对应的,池化层分为最大池化和平均池化两种,每种也有三个类:

  • 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.]]]])

2. 循环神经网络(RNN)

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(152, batch_first=True)

然后创建一个两批、每批 3 个数据点、每个数据点一个特征的张量:

>>> a = torch.rand(231)
>>> 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([235])
>>> 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([225])
>>> 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]

参考资料

[1]

读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


Pytorch · 目录
上一篇[DL] PyTorch 折桂 10:torch.optim下一篇[DL] PyTorch 折桂 12:其它功能

微信扫一扫
关注该公众号

继续滑动看下一个
花解语NLP
向上滑动看下一个