本文的主要目标是为优化不太复杂的任务的各种对象检测模型提供指导。我想帮助你选择更有效的配置,以降低计算成本,而不影响平均精度 (mAP)。
背景
我的目标之一是开发一种计算要求最低的手语识别系统。该系统的一个关键组成部分是预处理阶段,其中涉及对译员的手和面部的检测,如下图所示:
如图所示,这个问题相对简单,只涉及图像中两个不同的类和三个同时出现的对象。因此,我的目标是优化模型的超参数,以保持较高的 mAP,同时降低计算成本,从而实现在智能手机等边缘设备上的高效执行。
对象检测架构和设置
在这个项目中,我们测试了以下目标检测架构:EfficientDetD0,Faster-RCNN,SDD320,SDD640和YoloV7。然而,这里介绍的概念可以应用于适应各种其他架构。
对于模型开发,我主要使用了Python 3.8和TensorFlow框架,但YoloV7使用了PyTorch。虽然这里提供的大多数示例与TensorFlow相关,但你可以将这些原则调整为你喜欢的框架。
在硬件方面,测试使用了RTX 3060 GPU和Intel Core i5–10400 CPU。
微调目标检测器
在使用TensorFlow进行目标检测时,了解所有超参数都存储在名为“pipeline.config”的文件中是很重要的。这个protobuf文件保存了用于训练和评估模型的配置,你可以在从TF Model Zoo下载的任何预训练模型中找到它。在这个背景下,我将描述我在管道文件中实施的修改来优化目标检测器。
有一点需要注意的是,这里提供的超参数是专为手部和面部检测(2类,3个对象)设计的。请确保根据自己的问题域进行调整。
一般简化
可以应用于所有模型的第一个改变是将每个类别的最大预测数和生成的边界框数从100个分别减少为2个和4个。你可以通过调整“train_config”对象中的“max_number_of_boxes”属性来实现这一点。
...
train_config {
batch_size: 128
sync_replicas: true
optimizer { ... }
fine_tune_checkpoint: "PATH_TO_BE_CONFIGURED"
num_steps: 50000
startup_delay_steps: 0.0
replicas_to_aggregate: 8
max_number_of_boxes: 4 # <------------------ change this line
unpad_groundtruth_tensors: false
fine_tune_checkpoint_type: "classification"
fine_tune_checkpoint_version: V2
}
...
在此之后,更改物体检测器中“post_processing”内的“max_total_detections”和“max_detections_per_class”:
post_processing {
batch_non_max_suppression {
score_threshold: 9.99999993922529e-09
iou_threshold: 0.6000000238418579
max_detections_per_class: 2 # <------------------ change this line
max_total_detections: 4 # <------------------ change this line
use_static_shapes: false
}
score_converter: SIGMOID
}
这些更改非常重要,尤其对我而言,因为图像中同时出现了三个物体和两个类别。通过减少预测数量,需要较少的迭代次数来消除重叠的边界框,通过非最大值抑制(NMS)来实现。因此,如果你要检测的类别数量有限且场景中出现的物体较少,改变这个超参数可能是一个不错的主意。
另外,还对每个目标检测模型的具体架构细节进行了个别调整。
单次多盒检测器(SSD)
在进行目标检测时,测试不同的分辨率总是一个好主意。在这个项目中,我使用了两个版本的模型,SSD320和SSD640,输入图像分辨率分别为320x320像素和640x640像素。
对于这两个模型,主要的修改之一是通过删除最浅的层次,将特征金字塔网络(FPN)的深度从5降低到4。FPN是一种强大的特征提取机制,适用于多种特征图尺寸。然而,对于较大的物体来说,为更高的图像分辨率设计的最浅层可能是不必要的。也就是说,如果你要检测的对象不是太小的话,删除这一层可能是个好主意。要实现这个更改,请在“fpn”对象中将“min_level”属性从3调整为4。
...
feature_extractor {
type: "ssd_mobilenet_v2_fpn_keras"
depth_multiplier: 1.0
min_depth: 16
conv_hyperparams {
regularizer { ... }
initializer { ... }
activation: RELU_6
batch_norm {...}
}
use_depthwise: true
override_base_feature_extractor_hyperparams: true
fpn {
min_level: 4 # <------------------ change this line
max_level: 7
additional_layer_depth: 108 # <------------------ change this line
}
}
...
我还对更高分辨率的模型(SSD640)进行了简化,将“additional_layer_depth”从128减少到108。同样地,我调整了两个模型的“multiscale_anchor_generator”深度,从5层减少到4层,如下所示:
...
anchor_generator {
multiscale_anchor_generator {
min_level: 4 # <------------------ change this line
max_level: 7
anchor_scale: 4.0
aspect_ratios: 1.0
aspect_ratios: 2.0
aspect_ratios: 0.5
scales_per_octave: 2
}
}
...
最终,负责生成边界框预测(“box_predictor”)的网络层数从4层减少到3层。对于SSD640,边界框预测器的深度也从128减少到96,如下所示:
...
box_predictor {
weight_shared_convolutional_box_predictor {
conv_hyperparams {
regularizer { ... }
initializer { ... }
activation: RELU_6
batch_norm { ... }
}
depth: 96 # <------------------ change this line
num_layers_before_predictor: 3 # <------------------ change this line
kernel_size: 3
class_prediction_bias_init: -4.599999904632568
share_prediction_tower: true
use_depthwise: true
}
}
...
这些简化是由于我们只有有限数量的不同类别,并且这些类别具有相对简单的模式可供检测。因此,可以减少模型的层数和深度,因为即使使用较少的特征图,我们仍然可以有效地从图像中提取所需的特征。
EfficientDet-D0
关于EfficientDet-D0,我将双向特征金字塔网络(Bi-FPN)的深度从5减少到4。此外,我将Bi-FPN的迭代次数从3减少到2,将特征图的核心从64减少到48。Bi-FPN是一种复杂的多尺度特征融合技术,可以产生出色的结果。然而,这也带来了更高的计算需求,对于更简单的问题来说可能会浪费资源。要实现上述调整,只需更新“bifpn”对象的属性如下:
...
bifpn {
min_level: 4 # <------------------ change this line
max_level: 7
num_iterations: 2 # <------------------ change this line
numyaml_filters: 48 # <------------------ change this line
}
...
除此之外,降低“multiscale_anchor_generator”的深度也是很重要的,就像我们在SSD中所做的一样。最后,我将盒子预测网络的层数从3层减少到2层。
...
box_predictor {
weight_shared_convolutional_box_predictor {
conv_hyperparams {
regularizer { ... }
initializer { ... }
activation: SWISH
batch_norm { ... }
force_use_bias: true
}
depth: 64
num_layers_before_predictor: 2 # <------------------ change this line
kernel_size: 3
class_prediction_bias_init: -4.599999904632568
use_depthwise: true
}
}
...
Faster R-CNN
Faster R-CNN模型依赖于区域生成网络(Region Proposal Network,RPN)和锚框作为其主要技术。锚框是在主干卷积神经网络(backbone CNN)的最后一个特征图上迭代的滑动窗口的中心点。对于每一次迭代,分类器确定了包含物体的候选框的概率,而回归器调整边界框的坐标。为了确保检测器具有平移不变性,它采用了三个不同的尺度和三个宽高比的锚框,这增加了每次迭代的候选框数量。
尽管这只是一个浅显的解释,但显然这个模型比其他模型复杂得多,因为它具有两阶段的检测过程。然而,可以简化它并提高其速度,同时保持高准确性。
为了做到这一点,首要的重要修改是将生成的候选框数量从300减少到50。这种减少是可行的,因为图像中同时存在的物体很少。你可以通过调整“first_stage_max_proposals”属性来实现此更改,如下所示:
...
first_stage_box_predictor_conv_hyperparams {
op: CONV
regularizer { ... }
initializer { ... }
}
first_stage_nms_score_threshold: 0.0
first_stage_nms_iou_threshold: 0.7
first_stage_max_proposals: 50 # <------------------ change this line
first_stage_localization_loss_weight: 2.0
first_stage_objectness_loss_weight: 1.0
initial_crop_size: 14
maxpool_kernel_size: 2
maxpool_stride: 2
...
之后,我从模型中删除了最大的anchor box比例(2.0)。这一改变是因为手和脸的尺寸由于翻译员与摄像机的固定距离而保持一致,而大型anchor box可能对提案生成并不有用。此外,鉴于我的对象在数据集中形状相似且变化很小,我移除了anchor box的一个宽高比。这些调整如下图所示:
first_stage_anchor_generator {
grid_anchor_generator {
scales: [0.25, 0.5, 1.0] # <------------------ change this line
aspect_ratios: [0.5, 1.0] # <------------------ change this line
height_stride: 16
width_stride: 16
}
}
尽管如此,考虑目标物体的大小和纵横比十分重要。这样的考虑可以帮助你排除那些用处不大的锚框,并显著降低模型的计算成本。
YoloV7
相比之下,对于YoloV7的修改非常小,以保持该架构的功能性。主要的修改涉及简化负责特征提取的卷积神经网络,在骨干和模型头部的卷积层中都进行了简化。为了实现这一点,我减少了几乎每个卷积层的内核/特征图数量,创建了以下模型:
backbone:
# [from, number, module, args]
[[-1, 1, Conv, [22, 3, 1]], # 0
[-1, 1, Conv, [44, 3, 2]], # 1-P1/2
[-1, 1, Conv, [44, 3, 1]],
[-1, 1, Conv, [89, 3, 2]], # 3-P2/4
[-1, 1, Conv, [44, 1, 1]],
[-2, 1, Conv, [44, 1, 1]],
[-1, 1, Conv, [44, 3, 1]],
[-1, 1, Conv, [44, 3, 1]],
[-1, 1, Conv, [44, 3, 1]],
[-1, 1, Conv, [44, 3, 1]],
[[-1, -3, -5, -6], 1, Concat, [1]],
[-1, 1, Conv, [179, 1, 1]], # 11
[-1, 1, MP, []],
[-1, 1, Conv, [89, 1, 1]],
[-3, 1, Conv, [89, 1, 1]],
[-1, 1, Conv, [89, 3, 2]],
[[-1, -3], 1, Concat, [1]], # 16-P3/8
[-1, 1, Conv, [89, 1, 1]],
[-2, 1, Conv, [89, 1, 1]],
[-1, 1, Conv, [89, 3, 1]],
[-1, 1, Conv, [89, 3, 1]],
[-1, 1, Conv, [89, 3, 1]],
[-1, 1, Conv, [89, 3, 1]],
[[-1, -3, -5, -6], 1, Concat, [1]],
[-1, 1, Conv, [512, 1, 1]], # 24
[-1, 1, MP, []],
[-1, 1, Conv, [89, 1, 1]],
[-3, 1, Conv, [89, 1, 1]],
[-1, 1, Conv, [89, 3, 2]],
[[-1, -3], 1, Concat, [1]], # 29-P4/16
[-1, 1, Conv, [89, 1, 1]],
[-2, 1, Conv, [89, 1, 1]],
[-1, 1, Conv, [89, 3, 1]],
[-1, 1, Conv, [89, 3, 1]],
[-1, 1, Conv, [89, 3, 1]],
[-1, 1, Conv, [89, 3, 1]],
[[-1, -3, -5, -6], 1, Concat, [1]],
[-1, 1, Conv, [716, 1, 1]], # 37
[-1, 1, MP, []],
[-1, 1, Conv, [256, 1, 1]],
[-3, 1, Conv, [256, 1, 1]],
[-1, 1, Conv, [256, 3, 2]],
[[-1, -3], 1, Concat, [1]], # 42-P5/32
[-1, 1, Conv, [128, 1, 1]],
[-2, 1, Conv, [128, 1, 1]],
[-1, 1, Conv, [128, 3, 1]],
[-1, 1, Conv, [128, 3, 1]],
[-1, 1, Conv, [128, 3, 1]],
[-1, 1, Conv, [128, 3, 1]],
[[-1, -3, -5, -6], 1, Concat, [1]],
[-1, 1, Conv, [716, 1, 1]], # 50
]
# yolov7 head
head:
[[-1, 1, SPPCSPC, [358]], # 51
[-1, 1, Conv, [179, 1, 1]],
[-1, 1, nn.Upsample, [None, 2, 'nearest']],
[37, 1, Conv, [179, 1, 1]], # route backbone P4
[[-1, -2], 1, Concat, [1]],
[-1, 1, Conv, [179, 1, 1]],
[-2, 1, Conv, [179, 1, 1]],
[-1, 1, Conv, [89, 3, 1]],
[-1, 1, Conv, [89, 3, 1]],
[-1, 1, Conv, [89, 3, 1]],
[-1, 1, Conv, [89, 3, 1]],
[[-1, -2, -3, -4, -5, -6], 1, Concat, [1]],
[-1, 1, Conv, [179, 1, 1]], # 63
[-1, 1, Conv, [89, 1, 1]],
[-1, 1, nn.Upsample, [None, 2, 'nearest']],
[24, 1, Conv, [89, 1, 1]], # route backbone P3
[[-1, -2], 1, Concat, [1]],
[-1, 1, Conv, [89, 1, 1]],
[-2, 1, Conv, [89, 1, 1]],
[-1, 1, Conv, [44, 3, 1]],
[-1, 1, Conv, [44, 3, 1]],
[-1, 1, Conv, [44, 3, 1]],
[-1, 1, Conv, [44, 3, 1]],
[[-1, -2, -3, -4, -5, -6], 1, Concat, [1]],
[-1, 1, Conv, [89, 1, 1]], # 75
[-1, 1, MP, []],
[-1, 1, Conv, [89, 1, 1]],
[-3, 1, Conv, [89, 1, 1]],
[-1, 1, Conv, [89, 3, 2]],
[[-1, -3, 63], 1, Concat, [1]],
[-1, 1, Conv, [179, 1, 1]],
[-2, 1, Conv, [179, 1, 1]],
[-1, 1, Conv, [89, 3, 1]],
[-1, 1, Conv, [89, 3, 1]],
[-1, 1, Conv, [89, 3, 1]],
[-1, 1, Conv, [89, 3, 1]],
[[-1, -2, -3, -4, -5, -6], 1, Concat, [1]],
[-1, 1, Conv, [179, 1, 1]], # 88
[-1, 1, MP, []],
[-1, 1, Conv, [179, 1, 1]],
[-3, 1, Conv, [179, 1, 1]],
[-1, 1, Conv, [179, 3, 2]],
[[-1, -3, 51], 1, Concat, [1]],
[-1, 1, Conv, [179, 1, 1]],
[-2, 1, Conv, [179, 1, 1]],
[-1, 1, Conv, [128, 3, 1]],
[-1, 1, Conv, [128, 3, 1]],
[-1, 1, Conv, [128, 3, 1]],
[-1, 1, Conv, [128, 3, 1]],
[[-1, -2, -3, -4, -5, -6], 1, Concat, [1]],
[-1, 1, Conv, [358, 1, 1]], # 101
[75, 1, RepConv, [179, 3, 1]],
[88, 1, RepConv, [358, 3, 1]],
[101, 1, RepConv, [716, 3, 1]],
[[102,103,104], 1, IDetect, [nc, anchors]], # Detect(P3, P4, P5)
]
如前所述,对于简单问题,通常可以通过从检测器中移除一些层和特征图的方法来简化问题,因为特征提取器最初设计用于在各种场景中检测数十个甚至数百个类别,需要更强大的模型来应对这些复杂性,并确保高准确性。
通过这些调整,我将参数数量从3640万减少到仅1410万,约降低了61%。此外,我在原始论文的建议基础上使用了512x512像素的输入分辨率,而非640x640像素。
结论
在计算资源有限或任务需要快速执行的情况下,我们可以进一步优化开源物体检测模型,找到一组能够减少计算要求而不影响结果的超参数组合,从而为不同的问题领域提供合适的解决方案。