0%

[DL] PyTorch 折桂 12:CNN

本文尽量不涉及 CNN(卷积神经网络)的原理,仅讨论 CNN 的 PyTorch 实现。CNN 独有的层包括卷积层(convolution layer),池化层(pooling layer),转置卷积层(transposed convolution layer),反池化层(unpooling layer)。卷积层与池化层在 CNN 中最常用,而转置卷积层与反池化层通常用于计算机视觉应用里的图像再生,对于 NLP 来说应用不多,不再赘述。

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

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

2. 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 为例:

1
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:填充的方法。

当所有尺寸均为矩形的时候,输出张量的长和宽的数值为:
$$dimension=\frac{H_{in}+2\times padding-dilution\times (kernel_size-1)-1}{stride}$$

  • 一个 trick:当 $kernel_size=3$,$padding=1$,$stride=1$ 的时候,输入张量和输出张量的长宽是不变的。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> 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>)

上例中,卷积核是一个 $3\times3$ 的全 1 张量;在卷积运算中,卷积核先与张量中前三排中的前三个元素进行 elementwise 的乘法,然后相加,得到输出张量中的第一个元素。然后向右滑动一个元素(因为 stride 默认是 1),重复卷积运算;既然达到末尾,返回左侧向下滑动一个单位,继续运算,直到到达末尾。

3. 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:当 $\text{kernel_size}=2$,$\text{stride}=2$ 的时候,输出张量的尺寸是输入张量的一半。

    1
    2
    3
    4
    >>> pool = torch.nn.MaxPool2d(2) # 定义一个大小为 2x2 的核
    >>> pool(tensor) # 池化操作
    tensor([[[[16., 14.],
    [ 8., 6.]]]])

    4. CNN 实战

    我们还是使用《[DL] PyTorch 折桂 11:使用全连接网络进行手写数字识别》 里的任务,只不过这一次我们使用 CNN 搭建神经网络。除了第 2、5 步,其它代码都是一样的,所以这里只有这两步的代码,其它代码请看前文。

构建神经网络时唯一要注意的是最后的全连接层的入度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
self.conv = nn.Conv2d(1, 4, 3, 1, 1) # 维度不变
self.pool = nn.MaxPool2d(2, 2) # 维度减半
self.fc = nn.Linear(28*28, 10)
self.softmax = nn.LogSoftmax(dim=1)

def forward(self, x):
x = F.relu(self.conv(x))
x = F.relu(self.pool(x))
x = self.fc(x.view(x.shape[0], -1))
out = self.softmax(x)
return out

这个模型里,每一个 batch 经过卷积层以前的维度是 [batch, 1, 28, 28],经过卷积层后长宽不变而通道数变成了 4;通过池化层以后每个 batch 的维度变成了 [batch, 4, 14, 14],所以全连接层的入度不变。还有一点要注意的是因为 CNN 接受一个二维张量。所以打平这个操作要放在模型里面的全连接层之前,而不是训练中。训练 15 个 epoch:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Epoch 0 - Training loss: 0.5372020660528242
Epoch 1 - Training loss: 0.25464658567836795
Epoch 2 - Training loss: 0.19804853362156383
Epoch 3 - Training loss: 0.1687760797144571
Epoch 4 - Training loss: 0.15073536825316675
Epoch 5 - Training loss: 0.13678724837126033
Epoch 6 - Training loss: 0.1266822514833132
Epoch 7 - Training loss: 0.11664468624781985
Epoch 8 - Training loss: 0.10935285677617071
Epoch 9 - Training loss: 0.1023956656144229
Epoch 10 - Training loss: 0.09896873006684535
Epoch 11 - Training loss: 0.09299984435115986
Epoch 12 - Training loss: 0.08871795376762748
Epoch 13 - Training loss: 0.08644302016886662
Epoch 14 - Training loss: 0.08259313310315805

上一次使用全连接神经网络训练 15 轮后的 loss 是 0.27,看来 CNN 网络的效果好很多。测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> correct_count, all_count = 0, 0

>>> with torch.no_grad():
... for data in valloader:
... images, labels = data
... outputs = cnn(images)
... _, predicted = torch.max(outputs.data, 1)
... all_count += labels.size(0)
... correct_count += (predicted == labels).sum().item()

... print("Number Of Images Tested =", all_count)
... print("\nModel Accuracy =", (correct_count/all_count))
Number Of Images Tested = 10000

Model Accuracy = 0.9705

准确率果然超过了 97%。CNN YES!

欢迎关注我的微信公众号“花解语 NLP”:

欢迎关注我的其它发布渠道