4.2 图像分类基本概念和ResNet设计思想
4.2.1 图像分类
在第三章我们介绍了计算机视觉常见任务,其中一个就是图像分类。本节简单回顾,图像分类利用计算机对图像进行定量分析,把图像或图像中的每个像元或区域划归为若干个类别中的某一种,下图展示了分类经典数据集ImageNet,包含2万多类图片。
图1:ImageNet数据集图片示意图
图像是计算机视觉的核心,是物体检测、图像分割、物体跟踪、行为分析、人脸识别等其他高层次视觉任务的基础。图像分类在许多领域都有着广泛的应用,如:安防领域的人脸识别和智能视频分析等,交通领域的交通场景识别,互联网领域基于内容的图像检索、相册自动归类、商品识别、医学领域的图像识别等。
4.2.2 ResNet
ResNet是2015年ImageNet比赛的冠军,将识别错误率降低到了3.6%,这个结果甚至超出了正常人眼识别的精度。
随着深度学习的不断发展,模型的层数越来越多,网络结构也越来越复杂。那么是否加深网络结构,就一定会得到更好的效果呢?从理论上来说,假设新增加的层都是恒等映射,只要原有的层学出跟原模型一样的参数,那么深模型结构就能达到原模型结构的效果。换句话说,原模型的解只是新模型的解的子空间,在新模型解的空间里应该能找到比原模型解对应的子空间更好的结果。但是实践表明,增加网络的层数之后,训练误差往往不降反升。
Kaiming He等人提出了残差网络ResNet来解决上述问题,在残差网络中最基本的单位为残差单元,其基本思想如图2所示。
- 图6(a):表示增加网络的时候,将x映射成 y = F ( x )输出。
- 图6(b):对图6(a)作了改进,输出 y = F ( x ) + x 。这时不是直接学习输出特征 y 的表示,而是学习 y − x。
- 如果想学习出原模型的表示,只需将 F ( x ) 的参数全部设置为0,则 y = x 是恒等映射。
- F(x) = y - x 也叫做残差项,如果 x → y的映射接近恒等映射,图6(b)中通过学习残差项也比图6(a)学习完整映射形式更加容易。
图2:残差块设计思想
一个残差网络通常有很多个残差单元堆叠而成,下面我们来构建残差网络。
4.2.2.1 残差单元
残差单元也叫做残差块(Residual block),输入x通过跨层连接,能更快的向前传播数据,或者向后传播梯度。通俗的比喻,在火热的电视节目《王牌对王牌》上有一个“传声筒”的游戏,排在队首的嘉宾把看到的影视片段表演给后面一个嘉宾看,经过四五个嘉宾后,最后一个嘉宾如果能表演出更多原剧的内容,就能取得高分。我们常常会发现刚开始的嘉宾往往表演出最多的信息(类似于Loss),而随着表演的传递,有效的表演信息越来越少(类似于梯度弥散)。如果每个嘉宾都能看到原始的影视片段,那么相信传声筒的效果会好很多。类似的,由于ResNet每层都存在直连的旁路,相当于每一层都和最终的损失有“直接对话”的机会,自然可以更好的解决梯度弥散的问题。
残差块的具体设计方案如图3所示,这种设计方案也常称作瓶颈结构(BottleNeck)
1*1的卷积核可以非常方便的调整中间层的通道数,在进入3*3的卷积层之前减少通道数(256->64),经过该卷积层后再恢复通道数(64->256),可以显著减少网络的参数量。这个结构(256->64->256)像一个中间细,两头粗的瓶颈,所以被称为“BottleNeck”。
图3:残差块结构示意图
这里,我们实现一个算子ResBlock
来构建残差单元。ResNet中使用了BatchNorm层,在卷积层的后面加上BatchNorm以提升数值稳定性,首先定义卷积批归一化块ConvBNLayer
包含卷积层和BatchNorm层:
In [1]
import paddle import paddle.nn as nn class ConvBNLayer(paddle.nn.Layer): def __init__(self, num_channels, num_filters, filter_size, stride=1, groups=1, act=None): """ num_channels, 卷积层的输入通道数 num_filters, 卷积层的输出通道数 stride, 卷积层的步幅 groups, 分组卷积的组数,默认groups=1不使用分组卷积 """ super(ConvBNLayer, self).__init__() # 创建卷积层 self._conv = nn.Conv2D( in_channels=num_channels, out_channels=num_filters, kernel_size=filter_size, stride=stride, padding=(filter_size - 1) // 2, groups=groups, bias_attr=False) # 创建BatchNorm层 self._batch_norm = paddle.nn.BatchNorm2D(num_filters) self.act = act def forward(self, inputs): y = self._conv(inputs) y = self._batch_norm(y) if self.act == 'leaky': y = F.leaky_relu(x=y, negative_slope=0.1) elif self.act == 'relu': y = F.relu(x=y) return y
然后定义残差块BottleneckBlock
,每个残差块会对输入图片做三次卷积ConvBNLayer
,然后跟输入图片进行短接,如果残差块中第三次卷积输出特征图的形状与输入不一致,则对输入图片使用1 x 1卷积,将其输出形状调整成一致。
1 × 1卷积:与标准卷积完全一样,唯一的特殊点在于卷积核的尺寸是1 × 1,也就是不去考虑输入数据局部信息之间的关系,而把关注点放在不同通道间。通过使用1 × 1卷积,可以起到如下作用:
- 实现信息的跨通道交互与整合。考虑到卷积运算的输入输出都是3个维度(宽、高、多通道),所以1 × 1卷积实际上就是对每个像素点,在不同的通道上进行线性组合,从而整合不同通道的信息;
- 对卷积核通道数进行降维和升维,减少参数量。经过1 × 1卷积后的输出保留了输入数据的原有平面结构,通过调控通道数,从而完成升维或降维的作用;
- 利用1 × 1卷积后的非线性激活函数,在保持特征图尺寸不变的前提下,大幅增加非线性。
4.2.2.2 整体结构
残差网络就是将很多个残差单元串联起来构成的一个非常深的网络,下图表示出了ResNet-50的结构,一共包含49层卷积和1层全连接,所以被称为ResNet-50。
图4:ResNet-50模型网络结构示意图
ResNet的具体实现如下代码所示:
In [3]
# ResNet模型代码 import numpy as np import paddle import paddle.nn as nn import paddle.nn.functional as F # 定义ResNet模型 class ResNet(paddle.nn.Layer): def __init__(self, layers=50, class_dim=1): """ layers, 网络层数,可以是50, 101或者152 class_dim,分类标签的类别数 """ super(ResNet, self).__init__() self.layers = layers supported_layers = [50, 101, 152] assert layers in supported_layers, \ "supported layers are {} but input layer is {}".format(supported_layers, layers) if layers == 50: #ResNet50包含多个模块,其中第2到第5个模块分别包含3、4、6、3个残差块 depth = [3, 4, 6, 3] elif layers == 101: #ResNet101包含多个模块,其中第2到第5个模块分别包含3、4、23、3个残差块 depth = [3, 4, 23, 3] elif layers == 152: #ResNet152包含多个模块,其中第2到第5个模块分别包含3、8、36、3个残差块 depth = [3, 8, 36, 3] # 残差块中使用到的卷积的输出通道数 num_filters = [64, 128, 256, 512] # ResNet的第一个模块,包含1个7x7卷积,后面跟着1个最大池化层 self.conv = ConvBNLayer( num_channels=3, num_filters=64, filter_size=7, stride=2, act='relu') self.pool2d_max = nn.MaxPool2D( kernel_size=3, stride=2, padding=1) # ResNet的第二到第五个模块c2、c3、c4、c5 self.bottleneck_block_list = [] num_channels = 64 for block in range(len(depth)): shortcut = False for i in range(depth[block]): # c3、c4、c5将会在第一个残差块使用stride=2;其余所有残差块stride=1 bottleneck_block = self.add_sublayer( 'bb_%d_%d' % (block, i), BottleneckBlock( num_channels=num_channels, num_filters=num_filters[block], stride=2 if i == 0 and block != 0 else 1, shortcut=shortcut)) num_channels = bottleneck_block._num_channels_out self.bottleneck_block_list.append(bottleneck_block) shortcut = True # 在c5的输出特征图上使用全局池化 self.pool2d_avg = paddle.nn.AdaptiveAvgPool2D(output_size=1) # stdv用来作为全连接层随机初始化参数的方差 import math stdv = 1.0 / math.sqrt(2048 * 1.0) # 创建全连接层,输出大小为类别数目,经过残差网络的卷积和全局池化后, # 卷积特征的维度是[B,2048,1,1],故最后一层全连接的输入维度是2048 self.out = nn.Linear(in_features=2048, out_features=class_dim, weight_attr=paddle.ParamAttr( initializer=paddle.nn.initializer.Uniform(-stdv, stdv))) def forward(self, inputs): y = self.conv(inputs) y = self.pool2d_max(y) for bottleneck_block in self.bottleneck_block_list: y = bottleneck_block(y) y = self.pool2d_avg(y) y = paddle.reshape(y, [y.shape[0], -1]) y = self.out(y) return y
4.2.2.3 飞桨分类网络高层API
飞桨开源框架除了基础API外,还支持了高层API。通过高低融合实现灵活组网,让飞桨API更简洁、更易用、更强大。高层API支持paddle.vision.models接口,实现了对常用模型的封装,包括ResNet、VGG、MobileNet、LeNet等。使用高层API调用这些网络,可以快速完成神经网络的训练和Fine-tune。
代码示例如下:
In [1]
import paddle from paddle.vision.models import resnet50 # 调用高层API的resnet50模型 model = resnet50() # 设置pretrained参数为True,可以加载resnet50在imagenet数据集上的预训练模型 # model = resnet50(pretrained=True) # 随机生成一个输入 x = paddle.rand([1, 3, 224, 224]) # 得到残差50的计算结果 out = model(x) # 打印输出的形状,由于resnet50默认的是1000分类 # 所以输出shape是[1x1000] print(out.shape)
/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/paddle/nn/layer/norm.py:654: UserWarning: When training, we now always track global mean and variance.
"When training, we now always track global mean and variance.")
[1, 1000]
使用paddle.vision中的模型可以简单快速的构建一个深度学习任务,如下示例,仅14行代码即可实现resnet在Cifar10数据集上的训练。
# 从paddle.vision.models 模块中import 残差网络,VGG网络,LeNet网络 from paddle.vision.models import resnet50, vgg16, LeNet from paddle.vision.datasets import Cifar10 from paddle.optimizer import Momentum from paddle.regularizer import L2Decay from paddle.nn import CrossEntropyLoss from paddle.metric import Accuracy from paddle.vision.transforms import Transpose # 确保从paddle.vision.datasets.Cifar10中加载的图像数据是np.ndarray类型 paddle.vision.set_image_backend('cv2') # 调用resnet50模型 model = paddle.Model(resnet50(pretrained=False, num_classes=10)) # 使用Cifar10数据集 train_dataset = Cifar10(mode='train', transform=Transpose()) val_dataset = Cifar10(mode='test', transform=Transpose()) # 定义优化器 optimizer = Momentum(learning_rate=0.01, momentum=0.9, weight_decay=L2Decay(1e-4), parameters=model.parameters()) # 进行训练前准备 model.prepare(optimizer, CrossEntropyLoss(), Accuracy(topk=(1, 5))) # 启动训练 model.fit(train_dataset, val_dataset, epochs=50, batch_size=64, save_dir="./output", num_workers=8)
小结
在这一节里,给读者介绍了图像分类,以及ResNet的网络结构和代码实现。除此之外,还介绍了高层API直接调用常用深度神经网络的方法,方便开发者们快速完成深度学习网络迭代。