TOC
KINA

KINA-0

Start having fun with KINA right now!

深度学习基础(4):卷积神经网络 CNN

《动手学深度学习》笔记——卷积神经网络 CNN(LeNet、AlexNet、VGG、NiN、GoogLeNet、ResNet、DenseNet)

1 卷积神经网络(LeNet)

1.1 卷积的基本概念

卷积神经网络(Convolutional Neural Networks,CNN)是机器学习利用自然图像中一些已知结构的创造性方法。

1.1.1 从全连接层到卷积

MLP对一张图像的单通道处理过程:设MLP的输入为二维图像\mathbf{X},其同形状的隐藏表示 $\mathbf{H}$ 为矩阵(二维张量),用[\mathbf{X}]_{i,j}[\mathbf{H}]_{ij}分别表示输入图像和隐藏表示中位置 $(i,j)$ 处的像素。参考之前的操作,该全连接层的参数为四阶权重张量\mathsf{W}、偏置参数矩阵\mathbf{U}。则对于隐藏表示中任意位置(i,j)处的像素值[\mathbf{H}]_{ij},可通过在\mathbf{X}中以(i,j)为中心对输入像素进行上述两参数的加权求和得到,再经平移使权重变为\mathsf{V},可知结果依赖于该中心点(i,j),即

[\mathbf{H}]_{i,j} =[\mathbf{U}]_{i,j}+\sum\limits_k \sum\limits_l
[\mathsf{W} ]_{i,j,k,l}[\mathbf{X}]_{k,l} \\  
=[\mathbf{U}]_{i,j}+\sum\limits_a \sum\limits_b [\mathsf{V}
]_{i,j,a,b}[\mathbf{X}]_{i+a,j+b}\ (k=i+a,\ l=j+b)

CNN将空间不变性(Spatial Invariance)的这一概念系统化,从而使用较少的参数来学习有用的表示,相比于全连接层其模型更简洁、所需的参数更少。

(1)平移不变性(Translation Invariance):不管检测对象出现在图像中哪个位置,神经网络的前几层应对相同的图像区域具有相似的反应。
检测对象在输入\mathbf{X}中的平移应该仅导致隐藏表示\mathbf{H}中的平移,\mathsf{V}\mathbf{U}不依赖于(i,j)的值,即[\mathsf{V}]_{i,j,a,b}=[\mathbf{V}]_{i,j}\mathbf{U}为常数u。由此可简化 $\mathbf{H}$ 的定义为

[\mathbf{H}]_{i,j} =u+\sum\limits_a \sum\limits_b [\mathbf{V} ]_{a,b}[\mathbf{X}]_{i+a,j+b}

这种使得维数降低的操作即为卷积(Convolution),能够大幅减少参数。

(2)局部性(Locality):神经网络的前面几层应该只探索输入图像中的局部区域,而不过度在意图像中相隔较远区域的关系。
为了收集用来训练\mathbf{H}参数的相关信息,不应偏离到距(i,j)很远的地方,即\forall\ |a|>\Delta \text{ or } |b|>\Delta,\ [\mathbf{V}]_{a,b}=0。由此可进一步重写\mathbf{H}

[\mathbf{H}]_{i,j} =u+\sum\limits_{a=-\Delta}^\Delta \sum\limits_{b=-\Delta}^\Delta [\mathbf{V} ]_{a,b}[\mathbf{X}]_{i+a,j+b}

上述式子表示一个卷积层(Convolutional Layer)。\mathbf{V}称为卷积核(Convolution Kernel)或滤波器(Filter)或该卷积层的权重,是可学习的参数。

1.1.2 数学上的卷积

设函数f,g:\mathbb{R}^d \rightarrow \mathbb{R},则fg的卷积即为当把一个函数“翻转”并移位\mathbf{x}时,测量这两个函数之间的重叠,定义为

(f*g)(\mathbf{x})=\int f(\mathbf{z})g(\mathbf{x}-\mathbf{z})\mathrm{d}\mathbf{z}

当为离散对象时,积分就变成求和。例如,对于由索引为\mathbb{Z}、平方可和的无限维向量集合中抽取的向量,定义为

(f*g)(i)=\sum\limits_a f(a)g(i-a)

对于二维张量,则为f的索引(a,b)g的索引(i-a,j-b)上的对应加和,即

(f*g)(i,j)=\sum\limits_a\sum\limits_b f(a,b)g(i-a,j-b)

1.1.3 通道

图像一般包含三个通道/三种原色(红色、绿色和蓝色),一个二维图像输入实际是一个由高度、宽度和颜色组成的三维张量,因此将[\mathbf{X}]_{i,j}调整为[\mathsf{X}]_{i,j,k},将[\mathbf{V}]_{a,b}调整为[\mathsf{V}]_{a,b,c}

由于输入图像是三维的,隐藏表示\mathbf{H}也应采用三维张量,即对于每一个空间位置应采用一组隐藏表示。因此可以把隐藏表示视为一系列具有二维张量的通道(Channel),或特征映射(Feature Map),因为每个通道都向后续层提供一组空间化的学习特征。上述的坐标c表示输入通道,可再添加一个坐标d表示输出通道。

综上所述,一个输入通道为c、输出通道为d的隐藏表示定义为

[\mathbf{H}]_{i,j,d} =\sum\limits_{a=-\Delta}^\Delta \sum\limits_{b=-\Delta}^\Delta \sum\limits_c [\mathsf{V} ]_{a,b,c,d}[\mathsf{X}]_{i+a,j+b,c}

1.2 图像卷积

1.2.1 互相关运算

此处先只考虑单通道的情况。在卷积层中,输入张量与核张量通过互相关运算(Cross-correlation)产生输出张量。在二维互相关运算中,卷积窗口从输入张量的左上角开始,从左到右、从上到下滑动。 当卷积窗口滑动到新一个位置时,包含在该窗口中的部分张量与卷积核张量进行按元素相乘,所得张量再求和得到一个标量,由此可得这一位置的输出张量值。

对于输入大小n_h\times n_w、卷积核大小k_h \times k_w,输出大小为

(n_h-k_h+1)\times (n_w-k_w+1)

互相关运算

卷积层中的两个被训练的参数是卷积核权重(weight)和标量偏置(bias),参考全连接层,通常也随机初始化卷积核权重。高度和宽度分别为hw的卷积核被称为h\times w卷积(核),将该卷积层称为h\times w卷积层。

# 构造一个二维卷积层,其具有1个输出通道和形状为(1,2)的卷积核
conv2d = nn.Conv2d(1,1, kernel_size=(1, 2), bias=False)

1.2.2 填充(Padding)

填充(Padding):在输入图像的边界填充元素(通常填0)。

若添加p_h行填充(大约一半在顶部,一半在底部)和p_w列填充(左侧大约一半,右侧一半),则输出的高度和宽度分别增加p_hp_w,输出形状为

(n_h-k_h+p_h+1)\times (n_w-k_w+p_w+1)

常设置p_h=k_h-1p_w=k_w-1,使输入和输出具有相同的高度和宽度。

常取卷积核的高度k_h和宽度k_w奇数,好处为在保持空间维度的同时,可以在顶部和底部填充数量均为\frac {p_h}2的行,在左侧和右侧填充数量均为\frac {p_w}2的列。

Padding

# 当卷积核的高度和宽度不同时,可以填充不同的高度和宽度,使输出和输入具有相同的高度和宽度
conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1))

1.2.3 步幅(Stride)

步幅(Stride):每次滑动元素的数量。有时为了高效计算或是缩减采样次数,卷积窗口可以跳过中间位置,每次滑动多个元素。

通常,当垂直步幅为s_h、水平步幅为s_w时,输出形状为

\left \lfloor \frac {n_h-k_h+p_h+s_h}{s_h} \right \rfloor \times\left \lfloor \frac {n_w-k_w+p_w+s_w}{s_w} \right \rfloor

若设置了p_h=k_h-1p_w=k_w-1,则上式可简化为

\left \lfloor \frac {n_h+s_h-1}{s_h} \right \rfloor \times\left \lfloor \frac {n_w+s_w-1}{s_w} \right \rfloor

若输入的高度和宽度可被垂直和水平步幅整除,则可进一步简化为

\left \lfloor \frac {n_h}{s_h} \right \rfloor \times\left \lfloor \frac {n_w}{s_w} \right \rfloor

Stride

# 将高度和宽度的步幅设置为2,从而将输入的高度和宽度减半
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)

# 高度和宽度可设置不同的步幅
conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))

为了简洁起见,当输入高度和宽度两侧的填充数量分别为p_hp_w时称之为填充(p_h,p_w);当p_h=p_w=p时填充为p。同理,当高度和宽度上的步幅分别为s_hs_w时称之为步幅(s_h,s_w);当s_h=s_w=s时步幅为s 。默认情况下填充为,步幅为1。实践中很少使用不一致的步幅或填充,即通常有p_h=p_w,\ s_h=s_w

1.4 多输入多输出通道

多输入多输出通道可以用来扩展卷积层的模型。

1.4.1 多输入通道

当时输入通道c_i>1时,需构造一个具有相同输入通道数的卷积核,以便与输入数据进行互相关运算。若输入张量的形状为k_h\times k_w,则连结可得形状为c_i\times k_h \times k_w的卷积核。由此可对每个通道输入的二维张量和卷积核的二维张量进行互相关运算,再对通道求和(将c_i个结果相加)得到二维张量。

多输入通道

1.4.2 多输出通道

在最流行的神经网络架构中,随着神经网络层数的加深常会增加输出通道的维数,通过减少空间分辨率以获得更大的通道深度。直观地说,可以将每个通道看作对不同特征的响应。而现实可能更为复杂一些,因为每个通道不是独立学习的,而是为了共同使用而优化的。因此,多输出通道并不仅是学习多个单通道的检测器。

c_ic_o分别表示输入和输出通道数,k_hk_w为卷积核的高度和宽度。为获得多个通道的输出,可以为每个输出通道创建一个形状为c_i\times k_h\times k_w的卷积核张量,这样卷积核的形状即为c_o\times c_i\times k_h\times k_w

在互相关运算中,每个输出通道先获取所有输入通道,再以对应该输出通道的卷积核计算出结果。

1.4.3 1×1卷积核

1×1卷积k_h=k_w=1)失去了卷积层在高度和宽度维度上识别相邻元素间相互作用的能力,其唯一计算发生在通道上。输入和输出具有相同的高度和宽度,输出中的每个元素都是从输入图像中同一位置的元素的线性组合

作用:通常用于调整网络层的通道数量和控制模型复杂性。

当以每像素为基础应用时,1×1卷积相当于全连接层,以c_i个输入值转换为c_o个输出值。因为这仍然是一个卷积层,所以跨像素的权重是一致的。同时其需要的权重维度为c_o\times c_i,再额外加上一个偏置。

1×1卷积核

1.5池化层(汇聚层)

池化层(Pooling,汇聚层)与卷积层类似,由一个形状固定为p\times q的窗口(常称为池化窗口)遍历的每个位置计算一个输出,同样可以指定填充和步幅。但池化层不包含参数,池化运算是确定性的,通常计算池化窗口中所有元素的最大值或平均值,这些操作分别称为最大池化层(Maximum Pooling)和平均池化层(Average Pooling)。

主要作用:减轻卷积层对位置的过度敏感;使用最大池化层以及大于1的步幅,可减少空间维度(如高度和宽度)。

与互相关运算符一样,池化窗口从输入张量的左上角开始,从左往右、从上往下的在输入张量内滑动。在池化窗口到达的每个位置,它计算该窗口中输入子张量的最大值或平均值。计算最大值或平均值是取决于使用了最大池化层还是平均池化层。

池化

池化窗口形状为p\times q的池化层称为p\times q池化层,池化操作称为p\times q池化。

# 和卷积层类似,池化层同样可以设置填充和步幅
pool2d = nn.MaxPool2d(3, padding=1, stride=2)

在处理多通道输入数据时,池化层在每个输入通道上单独运算,而不是像卷积层一样在通道上对输入进行汇总。 这意味着池化层的输出通道数与输入通道数相同

1.6 LeNet-5模型

LeNet是最早发布的卷积神经网络之一。LeNet-5由以下两个部分组成

  1. 卷积编码器:由两个卷积层组成。每个卷积块中的基本单元是一个卷积层、一个sigmoid激活函数和平均汇聚层。
  2. 全连接层密集块:由三个全连接层组成。为了将卷积块的输出传递给该稠密块,须在小批量中展平每个样本。

LeNet

lenet = nn.Sequential(
    nn.Conv2d(1, 6, kernel_size=5, padding=2), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2),
    nn.Conv2d(6, 16, kernel_size=5), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2),
    nn.Flatten(),
    nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(),
    nn.Linear(120, 84), nn.Sigmoid(),
    nn.Linear(84, 10)
)

去掉部分激活的简化版LeNet如下

简化版LeNet

为了构造高性能的卷积神经网络,通常对卷积层进行排列,逐渐降低其表示的空间分辨率,同时增加通道数。在传统的卷积神经网络中,卷积块编码得到的表征在输出之前需由一个或多个全连接层进行处理。


2 现代卷积神经网络

2.1 深度卷积神经网络(AlexNet)

原文:ImageNet Classification with Deep Convolutional Neural Networks

AlexNet一举打破了计算机视觉研究的现状,以很大的优势赢得了2012年ImageNet图像识别挑战赛,其设计体现了计算机视觉方法论的改变。

AlexNet是更大更深的LeNet-5,由八层组成:五个卷积层、两个全连接隐藏层和一个全连接输出层。引入了Dropout、ReLU、最大池化和数据增强。10x参数个数,260x计算复杂度。

AlexNet

alexnet = nn.Sequential(
    # 使用11*11的更大窗口来捕捉对象。同时步幅为4,以减少输出的高度和宽度。输出通道的数目远大于LeNet
    nn.Conv2d(1, 96, kernel_size=11, stride=4, padding=1), nn.ReLU(),
    nn.MaxPool2d(kernel_size=3, stride=2),
    # 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,且增大输出通道数
    nn.Conv2d(96, 256, kernel_size=5, padding=2), nn.ReLU(),
    nn.MaxPool2d(kernel_size=3, stride=2),
    # 使用三个连续的卷积层和较小的卷积窗口。
    # 除了最后的卷积层,输出通道的数量进一步增加。
    # 在前两个卷积层之后,汇聚层不用于减少输入的高度和宽度
    nn.Conv2d(256, 384, kernel_size=3, padding=1), nn.ReLU(),
    nn.Conv2d(384, 384, kernel_size=3, padding=1), nn.ReLU(),
    nn.Conv2d(384, 256, kernel_size=3, padding=1), nn.ReLU(),
    nn.MaxPool2d(kernel_size=3, stride=2),
    nn.Flatten(),
    # 这里,全连接层的输出数量是LeNet中的好几倍。使用dropout层来减轻过拟合
    nn.Linear(6400, 4096), nn.ReLU(),
    nn.Dropout(p=0.5),
    nn.Linear(4096, 4096), nn.ReLU(),
    nn.Dropout(p=0.5),
    # 最后是输出层。由于这里使用Fashion-MNIST,所以用类别数为10,而非论文中的1000
    nn.Linear(4096, 10)
)

2.2 使用块的网络(VGG)

VGG网络是更大更深的AlexNet,与之一样可以分为两大部分:第一部分为一系列卷积块;第二部分由全连接层组成。卷积块与AlexNet的设计类似,同样主要由卷积层和汇聚层组成,为以下这个序列

  1. 带填充以保持分辨率的卷积层
  2. 非线性激活函数,如ReLU
  3. 汇聚层,如最大汇聚层

卷积块可以复用,使得网络定义的非常简洁,可以有效设计复杂的网络。不同的卷积块个数可得不同复杂度的变种架构,如VGG-11、VGG-16、VGG-19等。VGG网络的计算要比AlexNet慢得多。

VGG

原始的VGG网络使用8个卷积层和3个全连接层,因此通常被称为VGG-11,代码实现如下

def vgg_block(num_convs, in_channels, out_channels):
    """返回一个VGG块"""
    layers = []
    for _ in range(num_convs):
        layers.append(nn.Conv2d(in_channels, out_channels,
                                kernel_size=3, padding=1))
        layers.append(nn.ReLU())
        in_channels = out_channels
    layers.append(nn.MaxPool2d(kernel_size=2,stride=2))
    return nn.Sequential(*layers)

def vgg11(conv_arch):
    """返回一个VGG-11网络"""
    conv_blks = []
    in_channels = 1
    # 卷积层部分
    for (num_convs, out_channels) in conv_arch:
        conv_blks.append(vgg_block(num_convs, in_channels, out_channels))
        in_channels = out_channels

    return nn.Sequential(*conv_blks, nn.Flatten(),
                         # 全连接层部分
                         nn.Linear(out_channels * 7 * 7, 4096), nn.ReLU(), nn.Dropout(0.5),
                         nn.Linear(4096, 4096), nn.ReLU(), nn.Dropout(0.5),
                         nn.Linear(4096, 10))

conv_arch = ((1, 64), (1, 128), (2, 256), (2, 512), (2, 512))   
net = vgg11(conv_arch)  # conv_arch指定各VGG块中卷积层个数和输出通道数

2.3 网络中的网络(NiN)

NiN(网络中的网络)对VGG进行了改进,为了避免全连接层可能会完全放弃表征的空间结构的问题,在每个像素的通道上分别使用多层感知机,可以将其视1\times 1卷积层(注意与VGG的区别,参考下图)。

一个NiN块以一个普通卷积层开始,后面是两个1\times 1卷积层。这两个1\times 1卷积层充当带有ReLU激活函数的逐像素全连接层。 第一层的卷积窗口形状通常由用户设置。 随后的卷积窗口形状固定为1\times 1

NiN和AlexNet之间的一个显著区别是NiN完全取消了容易造成过拟合的全连接层,将它们替换为全局平均汇聚层(Global Average Pooling Layer),其通道数量为所需的输出数量,生成一个对数几率 (Logits)。该设计能显著减少模型所需参数的数量,然而在实践中有时会增加训练模型的时间。

NiN

def nin_block(in_channels, out_channels, kernel_size, strides, padding):
    """返回一个NiN块"""
    return nn.Sequential(nn.Conv2d(in_channels, out_channels, kernel_size, strides, padding),
                         nn.ReLU(),
                         nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU(),
                         nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU())

nin_net = nn.Sequential(
    nin_block(1, 96, kernel_size=11, strides=4, padding=0),
    nn.MaxPool2d(3, stride=2),
    nin_block(96, 256, kernel_size=5, strides=1, padding=2),
    nn.MaxPool2d(3, stride=2),
    nin_block(256, 384, kernel_size=3, strides=1, padding=1),
    nn.MaxPool2d(3, stride=2),
    nn.Dropout(0.5),
    # 标签类别数是10
    nin_block(384, 10, kernel_size=3, strides=1, padding=1),
    nn.AdaptiveAvgPool2d((1, 1)),
    # 将四维的输出转成二维的输出,其形状为(批量大小,10)
    nn.Flatten()
)

2.4 含并行连结的网络(GoogLeNet)

2.4.1 Inception块

GoogLeNet吸收了NiN中串联网络的思想,基本的卷积块称为Inception块(Inception Block),相当于一个有4条路径的子网络,通过不同窗口形状的卷积层和最大汇聚层来并行抽取信息,并使用1\times 1卷积层减少每像素级别上的通道维数从而降低模型复杂度。

Inception

class Inception(nn.Module):
    """Inception块"""
    def __init__(self, in_channels, c1, c2, c3, c4, **kwargs):
        """c1--c4是每条路径的输出通道数"""
        super(Inception, self).__init__(**kwargs)
        # 线路1,单1x1卷积层
        self.p1_1 = nn.Conv2d(in_channels, c1, kernel_size=1)
        # 线路2,1x1卷积层后接3x3卷积层
        self.p2_1 = nn.Conv2d(in_channels, c2[0], kernel_size=1)
        self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1)
        # 线路3,1x1卷积层后接5x5卷积层
        self.p3_1 = nn.Conv2d(in_channels, c3[0], kernel_size=1)
        self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2)
        # 线路4,3x3最大汇聚层后接1x1卷积层
        self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
        self.p4_2 = nn.Conv2d(in_channels, c4, kernel_size=1)

    def forward(self, x):
        p1 = F.relu(self.p1_1(x))
        p2 = F.relu(self.p2_2(F.relu(self.p2_1(x))))
        p3 = F.relu(self.p3_2(F.relu(self.p3_1(x))))
        p4 = F.relu(self.p4_2(self.p4_1(x)))
        # 在通道维度上连结输出
        return torch.cat((p1, p2, p3, p4), dim=1)

2.4.2 GoogLeNet模型

GoogLeNet在2014年的ImageNet图像识别挑战赛中大放异彩,和它的后继者们一度是ImageNet上最有效的模型之一:它以较低的计算复杂度提供了类似的测试精度。如下图所示,一共使用9个Inception块和其他层的堆叠来生成其估计值。

GoogLeNet

class GoogLeNet(nn.Module):
    def __init__(self):
        super(Inception, self).__init__()
        # 1个64通道、7x7的卷积层
        self.b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
                                nn.ReLU(),
                                nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
        # 2个卷积层:第1个为64个通道、1x1卷积层,第2个使用将通道数量增加三倍的3x3卷积层
        # 与Inception块中的第二条路线相同
        self.b2 = nn.Sequential(nn.Conv2d(64, 64, kernel_size=1),
                                nn.ReLU(),
                                nn.Conv2d(64, 192, kernel_size=3, padding=1),
                                nn.ReLU(),
                                nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
        # 2个Inception块
        self.b3 = nn.Sequential(Inception(192, 64, (96, 128), (16, 32), 32),
                                Inception(256, 128, (128, 192), (32, 96), 64),
                                nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
        # 5个Inception块
        self.b4 = nn.Sequential(Inception(480, 192, (96, 208), (16, 48), 64),
                                Inception(512, 160, (112, 224), (24, 64), 64),
                                Inception(512, 128, (128, 256), (24, 64), 64),
                                Inception(512, 112, (144, 288), (32, 64), 64),
                                Inception(528, 256, (160, 320), (32, 128), 128),
                                nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
        # 2个Inception块
        self.b5 = nn.Sequential(Inception(832, 256, (160, 320), (32, 128), 128),
                                Inception(832, 384, (192, 384), (48, 128), 128))
        # 全局平均汇聚层+最终输出
        self.out = nn.Sequential(nn.AdaptiveAvgPool2d((1,1)),
                                 nn.Flatten(),
                                 nn.Linear(1024, 10))

    def forward(self, x):
        x = self.out(self.b5(self.b4(self.b3(self.b2(self.b1(x))))))
        return x

2.5 批量规范化

批量规范化(Batch Normalization)可持续加速深层网络的收敛速度。利用小批量的均值和标准差,不断调整神经网络的中间输出,使整个神经网络各层的中间输出值更加稳定。

\mathbf{x} \in \mathcal{B}为一个来自给定样本均值为\hat \mu_\mathcal{B}、标准差为\hat \sigma_\mathcal{B}的小批量\mathcal{B}的输入,则批量规范化\text{BN}定义为

\text{BN}(\mathbf{x})=\mathbf{\gamma} \odot \frac{\mathbf{x}-\mathbf{\hat \mu_\mathcal{B}}}{\mathbf{\hat \sigma_\mathcal{B}}}+\mathbf{\beta}

其中\mathbf{\gamma}拉伸(Scale),\mathbf{\beta}偏移(Shift),它们的形状与\mathbf{x}相同,会与其他模型参数一起学习。应用标准化后,生成的小批量的平均值为0、单位方差为1。由于在训练过程中,中间层的变化幅度不能过于剧烈,而批量规范化将每一层主动居中并将它们重新调整为给定的平均值和大小。

可以计算出\mathbf{\hat \mu_\mathcal{B}}\mathbf{\hat \sigma_\mathcal{B}}(在方差估计值中添加一个噪声\epsilon>0,以确保上式中除数始终非零),即

\mathbf{\hat \mu_\mathcal{B}}=\frac 1{|\mathcal{B}|}\sum\limits_{\mathbf{x} \in \mathcal{B}}\mathbf{x}\\
\mathbf{\hat \sigma^2_\mathcal{B}}=\frac 1{|\mathcal{B}|}\sum\limits_{\mathbf{x} \in \mathcal{B}}(\mathbf{x}-\mathbf{\hat \mu_\mathcal{B}})^2+\epsilon

对于全连接层,常将批量规范化层置于仿射变换和激活函数之间,形如\mathbf{h}=\text{ReLU}(\text{BN}(\mathbf{Wx}+\mathbf{b}))

对于卷积层,常在卷积层之后和非线性激活函数之前应用批量规范化。当卷积有多个输出通道时,需要对这些通道的每个输出执行批量规范化,每个通道都有自己的拉伸和偏移(均为标量)。

训练模式(通过小批量统计数据规范化)和预测模式(通过数据集统计规范化)中,批量规范化层的计算结果也是不一样的。

# 使用nn.BatchNorm2d或1d等来实现,需传入相应的通道数
net = nn.Sequential(
    nn.Conv2d(1, 6, kernel_size=5), nn.BatchNorm2d(6), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2),
    nn.Conv2d(6, 16, kernel_size=5), nn.BatchNorm2d(16), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2), nn.Flatten(),
    nn.Linear(256, 120), nn.BatchNorm1d(120), nn.Sigmoid(),
    nn.Linear(120, 84), nn.BatchNorm1d(84), nn.Sigmoid(),
    nn.Linear(84, 10)
)

2.6 残差网络(ResNet)

原文:Deep Residual Learning for Image Recognition

2.6.1 函数类

设有一类特定的神经网络架构\mathcal{F},所有f \in \mathcal{F}可由训练得到。设f^*为希望找到的函数,最理想情况为f^* \in \mathcal{F},但现实往往不会那么理想,因此常改为找\mathcal{F}中的最佳选择f^*_\mathcal{F},设其可由\mathbf{X}特性和\mathbf{y}标签的的数据集训练得到,则

f^*_\mathcal{F} := \argmax\limits_f L(\mathbf{X},\mathbf{y},f) \text{ subject to } f \in \mathcal{F}

要想得到更近似真正的f^*的函数,需要设计一个更强大的架构\mathcal{F'},使得f^*_\mathcal{F'}f^*_\mathcal{F}更近似。
如下图所示,设复杂度由\mathcal{F}_{1}\mathcal{F}_{6}递增。对于非嵌套函数(Non-nested Function)类, 常有\mathcal{F}_{i-1} \nsubseteq \mathcal{F}_{i},无法保证新的体系“更近似”,较复杂的函数类并不总向f^*靠拢。但对于嵌套函数(Nested Function)类,总有\mathcal{F}_{1} \subseteq \mathcal{F}_{2} \subseteq \cdots \subseteq \mathcal{F}_{6},可以避免上述问题。

嵌套函数

因此,只有当较复杂的函数类包含较小的函数类时,才能确保提高它们的性能。 对于深度神经网络,若能将新添加的层训练成恒等映射(identity function)f(\mathbf{x})=\mathbf{x},新模型和原模型将同样有效。同时由于新模型可能得出更优的解来拟合训练数据集,因此添加层似乎更容易降低训练误差。

综上,何恺明等人提出了残差网络(ResNet),其在2015年的ImageNet图像识别挑战赛夺魁,并深刻影响了后来的深度神经网络的设计。

2.6.2 残差块

ResNet的核心思想:每个附加层都应该更容易地包含原始函数作为其元素之一。由此便产生了残差块(Residual Blocks)。

设原始输入为\mathbf{x},希望学成的理想映射为f(\mathbf{x})。如下图所示,正常块直接拟合出该映射\mathbf{x},而残差块则拟合出残差映射f(\mathbf{x})-\mathbf{x}。残差映射可以更容易地学习同一函数,例如将权重层中的参数近似为零。

利用残差块可以训练出一个更有效的深层神经网络:输入可通过跨层数据线路更快地向前传播。

残差块

ResNet的残差块沿用了VGG完整的3\times 3卷积层设计,可以选择加入快速通道(1\times1卷积层),如下所示

快速通道

各种不同的情况

class Residual(nn.Module):
    """残差块"""
    def __init__(self, input_channels, num_channels, use_1x1conv=False, strides=1):
        super().__init__()
        self.conv1 = nn.Conv2d(input_channels, num_channels,
                               kernel_size=3, padding=1, stride=strides)
        self.conv2 = nn.Conv2d(num_channels, num_channels,
                               kernel_size=3, padding=1)
        if use_1x1conv:
            self.conv3 = nn.Conv2d(input_channels, num_channels,
                                   kernel_size=1, stride=strides)
        else:
            self.conv3 = None
        self.bn1 = nn.BatchNorm2d(num_channels)
        self.bn2 = nn.BatchNorm2d(num_channels)

    def forward(self, X):
        Y = F.relu(self.bn1(self.conv1(X)))
        Y = self.bn2(self.conv2(Y))
        if self.conv3:
            X = self.conv3(X)
        Y += X
        return F.relu(Y)

2.6.3 ResNet-18模型

ResNet-18的前两层跟之前介绍的GoogLeNet中的一样,不同之处在于ResNet每个卷积层后增加了批量规范化层。GoogLeNet在后面接了4个由Inception块组成的模块。 ResNet则使用4个由残差块组成的模块,每个模块使用若干个同样输出通道数的残差块。

ResNet-18

class ResNet(nn.Module):
    """ResNet-18"""
    def __init__(self):
        super(ResNet, self).__init__()
        self.b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
                                nn.BatchNorm2d(64), nn.ReLU(),
                                nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
        self.b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=True))
        self.b3 = nn.Sequential(*resnet_block(64, 128, 2))
        self.b4 = nn.Sequential(*resnet_block(128, 256, 2))
        self.b5 = nn.Sequential(*resnet_block(256, 512, 2))
        self.out = nn.Sequential(nn.AdaptiveAvgPool2d((1,1)),
                                 nn.Flatten(),
                                 nn.Linear(512, 10))

    def resnet_block(self, input_channels, num_channels, num_residuals, first_block=False):
        """生成残差块"""
        blk = []
        for i in range(num_residuals):
            if i == 0 and not first_block:
                blk.append(Residual(input_channels, num_channels,
                                    use_1x1conv=True, strides=2))
            else:
                blk.append(Residual(num_channels, num_channels))
        return blk

    def forward(self, x):
        x = self.out(self.b5(self.b4(self.b3(self.b2(self.b1(x))))))
        return x

2.7 稠密连接网络(DenseNet)

2.7.1 逻辑拓展

根据任意函数的泰勒展开式(Taylor expansion),即当x \rightarrow 0时,有

f(x)=f(0)+f'(0)x+\frac{f''(0)}{2!}x^2+\frac{f'''(0)}{3!}x^3+\cdots

ResNet将函数展开为f(\mathbf{x})=\mathbf{x}+g(\mathbf{x}),即将f分解为一个简单的线性项和一个复杂的非线性项。要想继续拓展成更多项,需使用DenseNet,如下所示

DenseNet

ResNet和DenseNet的关键区别在于,DenseNet输出是连结[,])而不是如ResNet的简单相加。 因此在应用越来越复杂的函数序列后,执行从\mathbf{x}到其展开式的映射

\mathbf{x}\rightarrow[\mathbf{x},f_1(\mathbf{x}),f_2([\mathbf{x},f_1(\mathbf{x})]),f_3(\mathbf{x},f_1(\mathbf{x}),f_2([\mathbf{x},f_1(\mathbf{x})])),\cdots]

最后将这些展开式“稠密连接”起来结合到多层感知机中再次减少特征的数量。

稠密连接

稠密连结网络(DenseNet)在某种程度上是ResNet的逻辑扩展,使用了ResNet改良版的“批量规范化、激活和卷积”架构,主要由2部分构成:稠密块(定义如何连接输入和输出)、过渡层(控制通道数量,使其不会太复杂)

2.7.2 稠密块体

一个稠密块(Dense Block)由多个卷积块组成,每个卷积块使用相同数量的输出通道。 在前向传播中将每个卷积块的输入和输出在通道维上连结。

class DenseBlock(nn.Module):
    """稠密块"""
    def __init__(self, num_convs, input_channels, num_channels):
        super(DenseBlock, self).__init__()
        layer = []
        for i in range(num_convs):
            layer.append(conv_block(num_channels * i + input_channels, num_channels))
        self.net = nn.Sequential(*layer)

    def conv_block(input_channels, num_channels):
        """返回一个卷积块"""
        return nn.Sequential(nn.BatchNorm2d(input_channels),
                             nn.ReLU(),
                             nn.Conv2d(input_channels, num_channels, kernel_size=3, padding=1))

    def forward(self, X):
        for blk in self.net:
            Y = blk(X)
            # 连接通道维度上每个块的输入和输出
            X = torch.cat((X, Y), dim=1)
        return X

2.7.3 过渡层

由于每个稠密块都会带来通道数的增加,使用过多则会过于复杂化模型。
过渡层(Transition Layer)通过1\times1卷积层来减小通道数,并使用步幅为2的平均汇聚层减半高和宽,从而减少通道的数量,降低模型复杂度。

def transition_block(input_channels, num_channels):
    """返回一个过渡层"""
    return nn.Sequential(nn.BatchNorm2d(input_channels), nn.ReLU(),
                         nn.Conv2d(input_channels, num_channels, kernel_size=1),
                         nn.AvgPool2d(kernel_size=2, stride=2))

2.7.4 DenseNet模型

DenseNet与ResNet类似,使用一样的单卷积层和最大汇聚层

class DenseNet(nn.Module):
    def __init__(self):
        super(DenseNet, self).__init__()
        b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
                           nn.BatchNorm2d(64), nn.ReLU(),
                           nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

        # num_channels为当前的通道数
        num_channels, growth_rate = 64, 32
        num_convs_in_dense_blocks = [4, 4, 4, 4]
        blks = []
        for i, num_convs in enumerate(num_convs_in_dense_blocks):
            blks.append(DenseBlock(num_convs, num_channels, growth_rate))
            # 上一个稠密块的输出通道数
            num_channels += num_convs * growth_rate
            # 在稠密块之间添加一个转换层,使通道数量减半
            if i != len(num_convs_in_dense_blocks) - 1:
                blks.append(transition_block(num_channels, num_channels // 2))
                num_channels = num_channels // 2

        self.net = nn.Sequential(b1, *blks,
                                 nn.BatchNorm2d(num_channels), nn.ReLU(),
                                 nn.AdaptiveAvgPool2d((1, 1)),
                                 nn.Flatten(),
                                 nn.Linear(num_channels, 10))

    def forward(self, x):
        x = self.net(x)
        return x

《深度学习基础(4):卷积神经网络 CNN》有1条评论

发表评论