1、EfficientDet简介
在简介部分,作者提出了 “鱼与熊掌俺能兼得乎?”,要知道在此之前,实际目标检测算法家族已经提出了很多很多经典的算法,有two-stage方法的,主要是早期的一些算法,如Fast R-CNN、Faster R-CNN,一般检测精度较高但速度慢,为了加快速度,后来逐步发展为one-stage,从RoI的提取到识别检测全部融合在一个框架下,实现 end to end,加快检测速度,但一般是以牺牲精度换速度的。
因此在FPN及EfficientNet 的影响下,作者分别基于此在 FPN 基础上进行优化提出BiFPN以及全方位的模型缩放探索。
作者提出两个方法:
BiFPN: 这个毋庸置疑,肯定是从 FPN 发展过来的,至于 Bi 就是双向,原始的FPN实现的自顶向下(top-down)融合,所谓的BiFPN就是两条路线既有top-down也有down-top。
在融合过程中,之前的一些模型方法没有考虑到各级特征对融合后特征的g共享度问题,即之前模型认为各级特征的贡献度相同,而本文作者认为它们的分辨率不同,其对融合后特征的贡献度不同,因此在特征融合阶段引入了weight。
复合缩放方法(compound scaling method):这个主要灵感来自于 EfficientNet,即在基线网络上同时对多个维度进行缩放(一般都是放大),这里的维度体现在主干网络、特征网络、以及分类/回归网络全流程的整体架构上整体网络由主干网络、特征网络以及分类/回归网络组成,可以缩放的维度比 EfficientNet 多得多,所以用网络搜索方式不合适了,作者提出一些启发式方法,可以参照论文的 Table 1。
该方法可以统一地对所有主干网、特征网络和预测网络的分辨率、深度和宽度进行缩放。基于这些优化,开发了一个新的对象检测器家族,称为EfficientDet。
2、EfficientDet原理与PyTorch实现
2.1、EfficientNet模型
2.1.1、EfficientNet简介
模型的基础网络架构是通过使用神经网络架构搜索(neural architecture search)设计得到。为了研究系统的模型缩放,谷歌大脑的研究人员针对EfficientNets的基础网络模型提出了一种全新的模型缩放方法,该方法使用简单而高效的复合系数来权衡网络深度、宽度和输入图片分辨率。
通过放大EfficientNets基础模型,获得了一系列EfficientNets模型。该系列模型在效率和准确性上战胜了之前所有的卷积神经网络模型。尤其是EfficientNet-B7在ImageNet数据集上得到了top-1准确率84.4%和top-5准确率97.1%的结果。且它和当时准确率最高的其它模型对比,大小缩小了8.4倍,效率提高了6.1倍。且通过迁移学习,EfficientNets在多个知名数据集上均达到了当时最先进的水平。
2.1.2、EfficientNet网络模型结构
1)移动翻转瓶颈卷积
移动翻转瓶颈卷积也是通过神经网络架构搜索得到的,该模块结构与深度分离卷积(depthwise separable convolution)相似,该移动翻转瓶颈卷积首先对输入进行1x1的逐点卷积并根据扩展比例(expand ratio)改变输出通道维度(如扩展比例为3时,会将通道维度提升3倍。但如果扩展比例为1,则直接省略该1x1的逐点卷积和其之后批归一化和激活函数)。
接着进行kxk的深度卷积(depthwise convolution)。如果要引入压缩与激发操作,该操作会在深度卷积后进行。再以1x1的逐点卷积结尾恢复原通道维度。
最后进行连接失活(drop connect)和输入的跳越连接(skip connection),这一做法源于论文《Deep networks with stochastic depth》,它让模型具有了随机的深度,剪短了模型训练所需的时间,提升了模型性能(注意,在EfficientNets中,只有当相同的移动翻转瓶颈卷积重复出现时,才会进行连接失活和输入的跳越连接,且还会将其中的深度卷积步长变为1),连接失活是一种类似于随机失活(dropout)的操作,并且在模块的开始和结束加入了恒等跳越。注意该模块中的每一个卷积操作后都会进行批归一化,激活函数使用的是Swish激活函数。总流程如图1所示,是扩展比例为6,深度卷积大小为5x5,步长为2x2(MBConv6,k5x5,stride2x2)的移动翻转瓶颈卷积模块。
图 MBConv6,k5x5,stride2x2结构示意图
PyTorch实现MBConv模块:
class MBConvBlock(nn.Module): """ Mobile Inverted Residual Bottleneck 模块 Args: block_args (namedtuple): 模型模块参数 global_params (namedtuple): 全局参数 Attributes: has_se (bool): 是否存在SENet """ def __init__(self, block_args, global_params): super().__init__() self._block_args = block_args # EfficientNet全局参数 self._bn_mom = 1 - global_params.batch_norm_momentum # 训练时的动量参数 self._bn_eps = global_params.batch_norm_epsilon # BN的参数 self.has_se = (self._block_args.se_ratio is not None) and (0 < self._block_args.se_ratio <= 1) self.id_skip = block_args.id_skip # 连接失活和跳跃连接 # 恒等宽高卷积操作 Conv2d = get_same_padding_conv2d(image_size=global_params.image_size) # 通道拓展操作 inp = self._block_args.input_filters # number of input channels oup = self._block_args.input_filters * self._block_args.expand_ratio # number of output channels if self._block_args.expand_ratio != 1: self._expand_conv = Conv2d(in_channels=inp, out_channels=oup, kernel_size=1, bias=False) self._bn0 = nn.BatchNorm2d(num_features=oup, momentum=self._bn_mom, eps=self._bn_eps) # 深度卷积操作 k = self._block_args.kernel_size s = self._block_args.stride # groups用于设置depthwise卷积(一个卷积核负责一个通道) self._depthwise_conv = Conv2d(in_channels=oup, out_channels=oup, groups=oup, kernel_size=k, stride=s, bias=False) self._bn1 = nn.BatchNorm2d(num_features=oup, momentum=self._bn_mom, eps=self._bn_eps) # SENet模块 if self.has_se: num_squeezed_channels = max(1, int(self._block_args.input_filters * self._block_args.se_ratio)) self._se_reduce = Conv2d(in_channels=oup, out_channels=num_squeezed_channels, kernel_size=1) self._se_expand = Conv2d(in_channels=num_squeezed_channels, out_channels=oup, kernel_size=1) # 最终输出模块 final_oup = self._block_args.output_filters self._project_conv = Conv2d(in_channels=oup, out_channels=final_oup, kernel_size=1, bias=False) self._bn2 = nn.BatchNorm2d(num_features=final_oup, momentum=self._bn_mom, eps=self._bn_eps) self._swish = MemoryEfficientSwish() def forward(self, inputs, drop_connect_rate=None): """ MBConv模块的流程: 0、输入 1、深度卷积操作 2、批归一化操作 3、Swish激活函数操作 4、深度卷积操作 5、批归一化操作 6、Swish激活函数操作 7、SENet操作 8、Depthwise Convolution操作 9、批归一化操作 10、连接失活和跳越连接操作 11、输出 """ # 0、输入 x = inputs # 1+2+3、深度卷积+批归一化+Swish操作 if self._block_args.expand_ratio != 1: x = self._swish(self._bn0(self._expand_conv(inputs))) # 4+5+6、Depthwise Convolution+批归一化+Swish操作 x = self._swish(self._bn1(self._depthwise_conv(x))) # 7、SENet操作 if self.has_se: x_squeezed = F.adaptive_avg_pool2d(x, 1) x_squeezed = self._se_expand(self._swish(self._se_reduce(x_squeezed))) x = torch.sigmoid(x_squeezed) * x # 8+9、批归一化+Depthwise Convolution x = self._bn2(self._project_conv(x)) # 10、连接失活和跳越连接操作 input_filters, output_filters = self._block_args.input_filters, self._block_args.output_filters if self.id_skip and self._block_args.stride == 1 and input_filters == output_filters: if drop_connect_rate: x = drop_connect(x, p=drop_connect_rate, training=self.training) # 连接失活 x = x + inputs # 跳越连接操作 # 11、输出 return x
2)EfficientNet-B0结构说明
EfficientNet-B0结构由16个移动翻转瓶颈卷积模块,2个卷积层,1个全局平均池化层和1个分类层构成。其结构如图所示,图中不同的颜色代表了不同的阶段。
图 EfficientNet-B0结构图
第一阶段:对输入的224x224x3的图像按顺序进行以下操作得到第一阶段的结果:
1) 卷积(卷积核为32核3×3×3,步长为2×2,填充为“same”即输出的宽和高缩小一半),该卷积运算的输出是一个维度为(112×112×32)的特征图。因该层不含偏置项,故该层需要训练学习的参数共计864(32x3x3x3)个。
2) 批归一化层(Batch Normalization,BN),该层输入为(112×112×32)的特征图,故该层含参数总数为128个(32x4),其中需要训练学习的参数为64个。
3) Swish激活函数
第一阶段,总计参数128+864=992个,需要训练学习的参数928个。
第二阶段:对前一阶段输出的112x112x32的特征图进行移动翻转瓶颈卷积
一次移动翻转瓶颈卷积(扩张比例为1,深度卷积核大小为3x3,核步长为1x1,包含压缩与激发操作,无连接失活和连接跳越),并输出第二阶段的结果:
1) 由于扩张比例为1,故跳过一开始的逐点卷积,直接进行深度卷积(卷积核为32核3×3×3,步长为1×1,填充为“same”即输出的宽和高不变)。深度卷积输出是一个维度为(112×112×32)的特征图。因该层不含偏置项,故该层需要训练学习的参数共计288(32x3x3x1)个。
2) 批归一化层(Batch Normalization,BN),该层输入为(112×112×32)的特征图,故该层含参数总数为128个(32x4),其中需要训练学习的参数为64个。
3) Swish激活函数。
4) 全局平均池化层(global average pooling),该层在通道维度方向上进行全局平均池化,输出为(1x1x32)的特征图。
5) 卷积(压缩与激发模块中的第一个卷积,卷积核为8核1x1x32,步长为1×1,填充为“same”即输出的宽和高不变),该卷积运算的输出是一个维度为(1×1×8)的特征图。因该层包含偏置项,故该层需要训练学习的参数共计264(8x1x1x32+8)个。
6) Swish激活函数。
7) 卷积(压缩与激发模块中的第二个卷积,卷积核为32核1x1x8,步长为1×1,填充为“same”即输出的宽和高不变),该卷积运算的的输出是一个维度为(1×1×32)的特征图。因该层包含偏置项,故该层需要训练学习的参数共计288(32x1x1x8+32)个。
8) Sigmoid激活函数
9) 与步骤3)的结果相乘,得到112x112x32的特征图。
10) 逐点卷积(卷积核为16核1×1×32,步长为1×1,填充为“same”即输出的宽和高不变)该卷积运算的输出是一个维度为(112×112×16)的特征图。因该层不含偏置项,故该层需要训练学习的参数共计512(16x1x1x32)个。
11) 批归一化层(Batch Normalization,BN),该层输入为(112×112×16)的特征图,故该层含参数总数为64个(16x4),其中需要训练学习的参数为32个。
第二阶段,总计参数288+128+264+288+512+64=1544个,需要训练学习的参数1448个。
第三阶段:对前一阶段输出的112x112x16的特征图进行两次移动翻转瓶颈卷积
第一个(扩张比例为6,深度卷积核大小为3x3,核步长为2x2,包含压缩与激发操作,无连接失活核连接跳越);
第二个(扩张比例为6,深度卷积核大小为3x3,核步长为1x1,包含压缩与激发操作,有连接失活和连接跳越),并输出第二阶段的结果;
第三阶段,总计参数17770个,需要训练学习的参数16705个。
第四阶段:对前一阶段输出的56x56x24的特征图进行两次移动翻转瓶颈卷积
第一个(扩张比例为6,深度卷积核大小为5x5,核步长为2x2,包含压缩与激发操作,无连接失活核连接跳越);
第二个(扩张比例为6,深度卷积核大小为5x5,核步长为1x1,包含压缩与激发操作,有连接失活和连接跳越);
输出是一个28x28x40的特征图。总计参数48336个,需要训练学习的参数46640个。
第五阶段:对前一阶段输出的28x28x40的特征图进行三次移动翻转瓶颈卷积
第一个(扩张比例为6,深度卷积核大小为3x3,核步长为2x2,包含压缩与激发操作,无连接失活核连接跳越);
第二个(扩张比例为6,深度卷积核大小为3x3,核步长为1x1,包含压缩与激发操作,有连接失活核连接跳越);
第三个(扩张比例为6,深度卷积核大小为3x3,核步长为1x1,包含压缩与激发操作,有连接失活核连接跳越);
输出是一个14x14x80的特征图。总计参数248210个,需要训练学习的参数242930个。
第六阶段:对前一阶段输出的14x14x80的特征图进行三次移动翻转瓶颈卷积
第一个(扩张比例为6,深度卷积核大小为5x5,核步长为1x1,包含压缩与激发操作,无连接失活核连接跳越);
第二个(扩张比例为6,深度卷积核大小为5x5,核步长为1x1,包含压缩与激发操作,有连接失活核连接跳越);
第三个(扩张比例为6,深度卷积核大小为5x5,核步长为1x1,包含压缩与激发操作,有连接失活核连接跳越);
输出是一个14x14x112的特征图。总计参数551116个,需要训练学习的参数543148个。
第七阶段:对前一阶段输出的14x14x112的特征图进行四次移动翻转瓶颈卷积
第一个(扩张比例为6,深度卷积核大小为5x5,核步长为2x2,包含压缩与激发操作,无连接失活核连接跳越);
第二个(扩张比例为6,深度卷积核大小为5x5,核步长为1x1,包含压缩与激发操作,有连接失活核连接跳越);
第三个(扩张比例为6,深度卷积核大小为5x5,核步长为1x1,包含压缩与激发操作,有连接失活核连接跳越);
第四个(扩张比例为6,深度卷积核大小为5x5,核步长为1x1,包含压缩与激发操作,有连接失活核连接跳越);
输出是一个7x7x192的特征图。总计参数2044396个,需要训练学习的参数2026348个。
第八阶段,对前一阶段输出的7x7x192的特征图进行一次移动翻转瓶颈卷积
(扩张比例为6,深度卷积核大小为3x3,核步长为1x1,包含压缩与激发操作,无连接失活核连接跳越);
输出是一个7x7x320的特征图。总计参数722480个,需要训练学习的参数717232个。
第九阶段:对输入的7x7x320的图像按顺序进行以下操作得到模型最终的结果:
1) 卷积(卷积核为1280核1×1×320,步长为1×1,填充为“same”即输出的宽和高不变),该卷积运算的输出是一个维度为(7×7×1280)的特征图。因该层不含偏置项,故该层需要训练学习的参数共计409600(1280x1x1x320)个。
2) 批归一化层(Batch Normalization,BN),该层输入为(7×7×1280)的特征图,故该层含参数总数为5120个(1280x4),其中需要训练学习的参数为2560个。
3) Swish激活函数
4) 全局平均池化层(global average pooling),该层在通道维度方向上进行全局平均池化,输出为(1x1x1280)的特征图。
5) 随机失活dropout
6) 全连接层,该层有1000个神经元。因该层包含偏置项,总参数个数为1281000(1000x1280+1000)
7) Softmax激活函数,输出分类结果。
第九阶段,总计参数1695720个,需要训练学习的参数1693160个。
除了EfficientNet-B0外,EfficientNet系列还有其它7个网络B0-B7,主要涉及三个参数深度参数、广度参数和输入分辨率参数,通过这三个参数来控制模型的缩放。
其中:
深度参数通过与EfficientNet-B0中各阶段的模块重复次数相乘,得到更深层的网络架构;
广度系数通过与EfficientNet-B0中各卷积操作输入的核个数相乘,得到表现能力更强的网络模型;
输入分辨率参数控制的则是网络的输入图片的长宽大小。
PyTorch实现EfficientNet-B0模型:
class EfficientNet(nn.Module): """ EfficientNet model框架流程(B0为例): 输入 ——> ——> 第一阶段:卷积层 ——> 批归一化 ——> Swish激活函数 ——> 第二阶段:1个MBConvBlock ——> 第三阶段:2个MBConvBlock ——> 第四阶段:2个MBConvBlock ——> 第五阶段:3个MBConvBlock ——> 第六阶段:3个MBConvBlock ——> 第七阶段:4个MBConvBlock ——> 第八阶段:1个MBConvBlock ——> 第九阶段:卷积层 ——> 批归一化 ——> Swish激活函数 ——> 全局平均池化 ——> 随机失活 ——> 全连接层 ——> Softmax 层 ——> 输出 """ def __init__(self, blocks_args=None, global_params=None): super().__init__() assert isinstance(blocks_args, list), 'blocks_args should be a list' assert len(blocks_args) > 0, 'block args must be greater than 0' self._global_params = global_params self._blocks_args = blocks_args # 恒等宽高卷积操作 Conv2d = get_same_padding_conv2d(image_size=global_params.image_size) # BN参数 bn_mom = 1 - self._global_params.batch_norm_momentum bn_eps = self._global_params.batch_norm_epsilon in_channels = 3 # rgb # 输出通道数目 out_channels = round_filters(32, self._global_params) self._conv_stem = Conv2d(in_channels, out_channels, kernel_size=3, stride=2, bias=False) self._bn0 = nn.BatchNorm2d(num_features=out_channels, momentum=bn_mom, eps=bn_eps) # 建立ModuleList of MBConvBlock 模块,方便多次使用和调用 self._blocks = nn.ModuleList([]) for i in range(len(self._blocks_args)): # Update block input and output filters based on depth multiplier. self._blocks_args[i] = self._blocks_args[i]._replace( input_filters=round_filters(self._blocks_args[i].input_filters, self._global_params), output_filters=round_filters(self._blocks_args[i].output_filters, self._global_params), num_repeat=round_repeats(self._blocks_args[i].num_repeat, self._global_params)) self._blocks.append(MBConvBlock(self._blocks_args[i], self._global_params)) if self._blocks_args[i].num_repeat > 1: self._blocks_args[i] = self._blocks_args[i]._replace(input_filters=self._blocks_args[i].output_filters, stride=1) for _ in range(self._blocks_args[i].num_repeat - 1): self._blocks.append(MBConvBlock(self._blocks_args[i], self._global_params)) # 输出模块 in_channels = self._blocks_args[len(self._blocks_args)-1].output_filters out_channels = round_filters(1280, self._global_params) self._conv_head = Conv2d(in_channels, out_channels, kernel_size=1, bias=False) self._bn1 = nn.BatchNorm2d(num_features=out_channels, momentum=bn_mom, eps=bn_eps) # 最后的全连接层以及输出阶段 self._avg_pooling = nn.AdaptiveAvgPool2d(1) self._dropout = nn.Dropout(self._global_params.dropout_rate) self._fc = nn.Linear(out_channels, self._global_params.num_classes) self._swish = MemoryEfficientSwish() def set_swish(self, memory_efficient=True): """Sets swish function as memory efficient (for training) or standard (for export)""" self._swish = MemoryEfficientSwish() if memory_efficient else Swish() for block in self._blocks: block.set_swish(memory_efficient) def extract_features(self, inputs): """ Returns output of the final convolution layer """ # 第一阶段 x = self._swish(self._bn0(self._conv_stem(inputs))) P = [] index = 0 num_repeat = 0 # 第二、三、四、五、六、七、八阶段 for idx, block in enumerate(self._blocks): drop_connect_rate = self._global_params.drop_connect_rate if drop_connect_rate: drop_connect_rate *= float(idx) / len(self._blocks) x = block(x, drop_connect_rate=drop_connect_rate) # x = self._swish(self._bn1(self._conv_head(x))) num_repeat = num_repeat + 1 if num_repeat == self._blocks_args[index].num_repeat: num_repeat = 0 index = index + 1 P.append(x) return P def forward(self, inputs): """ Calls extract_features to extract features, applies final linear layer, and returns logits. """ bs = inputs.size(0) # Convolution layers x = self.extract_features(inputs) # Pooling and final linear layer # x = self._avg_pooling(x) # x = x.view(bs, -1) # x = self._dropout(x) # x = self._fc(x) return x @classmethod # 自身的预训练模型加载 def from_name(cls, model_name, override_params=None): cls._check_model_name_is_valid(model_name) blocks_args, global_params = get_model_params(model_name, override_params) return cls(blocks_args, global_params) @classmethod # 下载的预训练模型加载 def from_pretrained(cls, model_name, num_classes=1000, in_channels=3): model = cls.from_name(model_name, override_params={'num_classes': num_classes}) load_pretrained_weights( model, model_name, load_fc=(num_classes == 1000)) if in_channels != 3: Conv2d = get_same_padding_conv2d(image_size=model._global_params.image_size) out_channels = round_filters(32, model._global_params) model._conv_stem = Conv2d(in_channels, out_channels, kernel_size=3, stride=2, bias=False) return model @classmethod # load预训练权重 def from_pretrained(cls, model_name, num_classes=1000): model = cls.from_name(model_name, override_params={'num_classes': num_classes}) load_pretrained_weights(model, model_name, load_fc=(num_classes == 1000)) return model @classmethod # 获取图像的shape def get_image_size(cls, model_name): cls._check_model_name_is_valid(model_name) _, _, res, _ = efficientnet_params(model_name) return res @classmethod # 检验预训练模型的名称是否正确 def _check_model_name_is_valid(cls, model_name, also_need_pretrained_weights=False): """ Validates model name. None that pretrained weights are only available for the first four models (efficientnet-b{i} for i in 0,1,2,3) at the moment. """ num_models = 4 if also_need_pretrained_weights else 8 valid_models = ['efficientnet-b'+str(i) for i in range(num_models)] if model_name not in valid_models: raise ValueError('model_name should be one of: ' + ', '.join(valid_models)) # 获取卷积后的特征图 def get_list_features(self): list_feature = [] for idx in range(len(self._blocks_args)): list_feature.append(self._blocks_args[idx].output_filters) return list_feature