Yolo v3目标检测网络详解
Yolo介绍
Yolo(You only look once)是经典的单阶段目标检测方法, 它于2016年提出第一版Yolov1,至今仍有许多基于它的改进模型。本文主要介绍的Yolov3就是其中之一。
首先来介绍Yolo v1、v2(Yolo9000)以及Yolov3之间有什么区别及改进之处。
Yolov1首先将目标检测作为一个回归问题来解决,使用单个神经网络直接从整个图片中预测边界框以及类别概率,它速度很快,可以做到实时目标检测。
Yolo v2相比yolo v1更快,而且更准。它有以下几点改进:
1、使用了BatchNorm,让网络更容易拟合。
2、在预训练的ImageNet模型上,使用更高分辨率图片对模型进行fine-tune,得到了更好的分类器。
3、使用了anchor,去除了yolo v1中的全连接层,此处借鉴了faster rcnn的anchor策略,全部改为卷积层来预测边界框。
4、使用了维度聚类的方法(以iou为距离度量,对先验框进行kmeans聚类),获得了更好的先验框,让网络得到了更好的预测结果。
5、多尺度训练,使用了不同尺寸的图片进行训练,使网络可以适应不同分辨率大小的数据。
6、直接预测坐标,直接预测物体中心相对于所处网格的坐标。
7、跳过连接,融合不同尺度的特征。
Yolo v3相比于yolo v2精度更高,速度稍有下降。它主要在以下几个方面进行了改进:
1、特征提取网络采用了残差结构,并且层数更多。
2、yolo v3在3个尺度上进行检测,依次检测大、中、小物体。
Yolo v3网络结构
首先介绍Yolov3中的backbone网络,如下图所示,左边为Yolov2使用的backbone(Darknet19),右边为Yolov3使用的backbone(Darknet53),Darknet53为全卷积结构,它去掉了所有的MaxPooling层,并且增加了更多的卷积层。它共包含了23个残差块,经过了5次下采样,最后backbone的输出大小为网络输入的1/32。由于网络的加深,Yolov3的速度比Yolov2稍慢。
Yolo v3的网络结构如下图所示,Conv2D block包含了5个卷积层,整个网络结构相对比较简单,比两阶段检测方法,比如faster rcnn要容易许多。
Yolo v3实现过程
- 输入图片通过backbone得到3个尺度的特征图(从上往下:feat1 -> (256 * 52 * 52), feat2 -> (512 * 26 * 26), feat3 -> (1024 * 13 * 13)),分别在3种尺度上进行检测。
- 3个特征图经过5层卷积(Conv2D Block)后,分别进入不同的分支,一条分支进行卷积+上采样,得到的特征图与上层的特征图进行通道合并(Concat),另一条分支通过两层卷积直接输出预测结果。
- 最后一个卷积层为1 * 1卷积,卷积核尺寸为(B * (5 + C)) * 1 * 1,B表示一个网格可以预测边界框的数目,C代表C个类别概率,5表示了4个坐标值(tx,ty,tw, th)和1个物体置信度。对于coco数据集,C=80,B=3。最终3个尺度的检测结果的尺寸分别是255 * 52 * 52、255 * 26 * 26和255 * 52 * 52。
下图展示了在feat3特征图上的检测结果,特征图上的一个像素对应原图中的一个网格,每个尺度定义了3种anchor,即每个网格会有3个预测框,每个预测框具有(5 + C)个属性。网络在3个尺度上检测,所以整个网络共检测13 * 13 * 3 + 26 * 26 * 3 + 52 * 52 * 3 = 10647个边界框。
Yolov3误差
Yolov3的误差包括了置信度误差、分类误差和定位误差。前面提到每个预测框具有(5 + C)个属性,5表示了4个坐标值(tx,ty,tw,th)和1个物体置信度,这里的4个坐标是相对于物体中心所处网格的左上角而言的,物体置信度表示该预测框包含物体的概率,包含物体则置信度为1,否则为0。C表示C个类别概率。
如下图表示了Yolov1网络(没找到Yolov3的误差的公式)的误差组成,Yolov3把后面三项都改成了二分类交叉熵误差了。
绿色框和黄色框表示置信度误差(conf_loss),绿色框表示了在第i个网格中的第j个预测框包含物体时的conf_loss,黄色框表示在第i个网格中的第j个预测框不包含物体时的conf_loss。
红色框表示定位误差(loc_loss),只有预测框包含物体才计算定位误差,定位误差中。
紫色框表示了分类误差(cls_loss),Pi(c)表示第i个网格中属于第c个类别的条件概率。只有物体出现在第i个网格内,该网格才负责检测这个物体,才计算分类误差。
置信度误差和分类误差都使用二分类损失误差(BCE)。至于分类误差并未使用多分类交叉熵误差,原文是这样解释的,使用单独的逻辑分类器有助于网络在开放数据集上的表现,这些数据集内通常包含重叠的标签(比如人和男人),使用softmax就意味着每个预测框必须是确定的某一个类别,而使用多标签的方法可以更好对数据建模。定位误差使用平方根损失误差(MSE)。
另外,为了缓解背景框和前景框的不平衡问题(在所有的预测框中,前景框只有极少一部分),增加了额外两个参数λcoor和λnoobj,在Yolov1中λcoor=5,λnoobj=0.5,提高前景框定位误差的权重,降低背景框的权重。
接着,来介绍一下预测坐标和预测框之间的关系。预测坐标只是相对于网格的坐标,我们需要将预测坐标转换为相对于原图的坐标。下图展示了预测坐标与预测框之间的对应关系。
参数说明
$\sigma$: sigmoid函数
$P_h$: 预定义的anchor高度
$P_h$: 预定义的anchor宽度
$grid\_x$: 物体中心所处网格左上角x坐标
$grid\_y$: 物体中心所处网格左上角y坐标
网络预测坐标为:$t_x,t_y,t_w,t_h$
最终预测框的坐标为:
<?XML:NAMESPACE PREFIX = "[default] http://www.w3.org/1998/Math/MathML" NS = "http://www.w3.org/1998/Math/MathML" />{x=σ(tx)+grid_xy=σ(ty)+grid_yw=etw∗Pwh=eth∗Ph\begin{cases}x = \sigma(t_x) + grid\_x \\y = \sigma(t_y) + grid\_y\\ w = e^{t_w} * P_w \\h = e^{t_h} * P_h\end{cases}⎩⎪⎪⎪⎪⎨⎪⎪⎪⎪⎧x=σ(tx)+grid_xy=σ(ty)+grid_yw=etw∗Pwh=eth∗Ph
Yolov3主要代码
网络构建
网络搭建使用yolo官方的配置文件yolov3.cfg进行构建,以下结合cfg配置文件对网络中各个网络层进行简要说明:
#-----------------------------------------# #---------------网络的超参数---------------# #-----------------------------------------# [net] # Testing #batch=1 #subdivisions=1 # Training batch=16 subdivisions=1 # 网络输入图片的宽度 width=416 # 网络输入图片的高度 height=416 channels=3 momentum=0.9 decay=0.0005 angle=0 saturation = 1.5 exposure = 1.5 hue=.1 learning_rate=0.001 burn_in=1000 max_batches = 500200 policy=steps steps=400000,450000 scales=.1,.1 #-----------------------------------------# #-------------------卷积层-----------------# #-----------------------------------------# [convolutional] batch_normalize=1 filters=32 size=3 stride=1 pad=1 activation=leaky #-----------------------------------------# #-------------------捷径层-----------------# #-----------------------------------------# # 残差连接 [shortcut] # 指示前一层输出与哪一层进行残差连接 # -3从后往前第3层 from=-3 activation=linear #-----------------------------------------# #-------------------捷径层-----------------# #-----------------------------------------# # 跳跃连接 [route] # 指示前一层输出与哪一层进行跳跃连接(concat) # -3从后往前第3层 layers = -4 #-----------------------------------------# #------------------上采样层----------------# #-----------------------------------------# # 上采样 [upsample] stride=2 #-----------------------------------------# #-------------------yolo层-----------------# #-----------------------------------------# [yolo] # 指示yolo层采用anchors中的哪些anchor mask = 6,7,8 # 表示网络共有9种预定义anchor大小 anchors = 10,13, 16,30, 33,23, 30,61, 62,45, 59,119, 116,90, 156,198, 373,326 # 数据集类别数 classes=80 # 预定义anchor数目 num=9 jitter=.3 ignore_thresh = .7 truth_thresh = 1 random=1
Yolo层
class YoloLayer(nn.Module): """ Params anchors(list): 预定义的anchor,列表中的每一个元素对应一个anchor大小 num_class(integer): 类别数目 img_dim(integer): 输入图片尺寸(长宽一致) """ def __init__(self, anchors, num_class, img_dim): super(YoloLayer, self).__init__() self.anchors = anchors self.num_anchors = len(anchors) self.num_class = num_class self.img_dim = img_dim # 每个anchors具有的属性向量长度为 ({x, y, h, w, Pobj} + 类别数), self.bbox_attrs = 5 + self.num_class def forward(self, x, calc_loss=True): """ Params: x(tensor): shape -> (batch_size, 5 + class_num, img_dim / x.shape[2], img_dim / x.shape[2]) Return output(tensor): shape -> (batch_size, img_dim *img_dim / (x.shape[2]*x.shape[2]), 5 + class_num) """ # 预定义anchor数目 num_anchors = self.num_anchors batch_size = x.shape[0] # 特征图大小 size = x.shape[2] # 计算步幅,即缩放倍数 stride = self.img_dim / size prediction = x.view(batch_size, num_anchors, self.bbox_attrs, size, size).permute(0, 1, 3, 4, 2).contiguous() # 确保坐标的偏移量在0-1范围内 pred_conf = t.sigmoid(prediction[..., 4]) pred_cls = t.sigmoid(prediction[..., 5:]) # 构建网格,根据预定义anchor尺寸,在每个网格上生成num_anchors个anchor grid_x, grid_y = t.meshgrid(t.arange(size, dtype=t.float32), t.arange(size, dtype=t.float32)) grid_x, grid_y = grid_x.view(1, 1, size, size), grid_y.view(1, 1, size, size) # 将对应到原图的anchor大小缩放到对应到特征图的尺度 scaled_anchors = t.tensor([[anchor_w / stride, anchor_h / stride] \ for anchor_w, anchor_h in self.anchors], dtype=t.float32) anchor_w = scaled_anchors[:, 0].view(1, num_anchors, 1, 1) anchor_h = scaled_anchors[:, 1].view(1, num_anchors, 1, 1) pred_boxes = t.FloatTensor(prediction[..., :4].shape) # 基于预测坐标得到预测框 pred_boxes[..., 0] = grid_x + t.sigmoid(prediction[..., 0]) pred_boxes[..., 1] = grid_y + t.sigmoid(prediction[..., 1]) pred_boxes[..., 2] = t.exp(prediction[..., 2]) * anchor_w pred_boxes[..., 3] = t.exp(prediction[..., 3]) * anchor_h # 合并预测框、置信度以及类别概率 output = t.cat( (pred_boxes.view(batch_size, -1, 4) * stride, pred_conf.view(batch_size, -1, 1), pred_cls.view(batch_size, -1, self.num_class)), -1) if calc_loss: return pred_boxes, pred_cls, pred_conf, prediction[..., :4], output return output
网络误差
build_target是误差计算中最重要的部分,它的功能是匹配,即匹配真实框和负责预测该真实框的预测框。它的主要过程如下:
a. 首先将归一化的真实框转换到特征图尺寸上的真实框。 b. 计算预定义anchor和真实框的iou,获取与每个真实框的最大iou及其索引,即可得到每个真实框需要使用哪个尺寸的anchor来匹配。 c. 根据真实框所处网格位置以及每个真实框匹配的anchor,得到每个真实框匹配的预测框。 d. 根据真实框的坐标,得到真实框相对于物体所处网格的位置, 以便于与预测坐标计算误差。
代码如下:
def build_targets(pred_boxes, pred_cls, target, anchors, ignore_thres, eps=1e-8): """ Params: pred_boxes: 预测框,shape --> `(batch_size, anchors_num, grid_size, grid_size, 4)` pred_cls: , 预测的类别概率,shape --> `(batch_size, anchors_num, grid_size, grid_size, num_class)` target: 真实地面框,每一个元素包含了对应的batch、类别以及边界框坐标,shape --> `(batch_size, 6)` anchors: 缩放后的预定义anchor的大小,shape --> `(3, 2)` ignore_thres: 前景阈值 Return: iou_scores: 预测框与真实框的iou,shape --> `(batch_size, anchors_num, grid_size, grid_size)` class_mask: 预测的类别掩膜,shape --> `(batch_size, anchors_num, grid_size, grid_size)` obj_mask: shape --> `(batch_size, anchors_num, grid_size, grid_size)` noobj_mask: shape --> `(batch_size, anchors_num, grid_size, grid_size)` txy: 变换后真实框的坐标,shape --> `(batch_size, anchors_num, grid_size, grid_size, 2)` twh: 变换后真实框的长宽,shape --> `(batch_size, anchors_num, grid_size, grid_size, 2)` tcls: 真实的分类结果, hot-encode形式,shape --> `(batch_size, anchors_num, grid_size, grid_size num_class)`, tconf: shape --> `(batch_size, anchors_num, grid_size, grid_size)` """ batch_size = pred_boxes.shape[0] num_anchors = pred_boxes.shape[1] num_class = pred_cls.shape[-1] size = pred_boxes.shape[2] # output tensors # 初始化 # λobj, anchor包含物体, 即为1,默认为0 obj_mask = t.ByteTensor(batch_size, num_anchors, size, size).fill_(0) # λnoobj, anchor不包含物体, 则为1,默认为1 noobj_mask = t.ByteTensor(batch_size, num_anchors, size, size).fill_(1) # 类别掩膜,类别预测正确即为1,默认全为0, class_mask = t.FloatTensor(batch_size, num_anchors, size, size).fill_(0), # 预测框与真实框的iou得分 iou_score = t.FloatTensor(batch_size, num_anchors, size, size).fill_(0) # 真实框相对于网格的位置 txy = t.FloatTensor(batch_size, num_anchors, size, size, 2).fill_(0) twh = t.FloatTensor(batch_size, num_anchors, size, size, 2).fill_(0) tcls = t.FloatTensor(batch_size, num_anchors, size, size, num_class).fill_(0) # target --> (target_num, 6), 归一化的坐标值 # 将归一化的真实框缩放到特征图尺寸上的真实框 target_boxes = target[:, 2:] * size # 真实框的坐标 xy = target_boxes[:, :2] # 真实框的尺寸 wh = target_boxes[:, 2:] # 计算预定义anchor和真实框的iou,shape -> (len(anchors), len(wh)) # Note: 这里只需要确定哪个预定义anchor的尺寸和真实框的尺寸最接近, 最后用尺寸最接近的anchor来对真实框进行回归, 因此只需要宽高即可 iou = iou_wh(anchors, wh) # 根据iou,得到与每个真实框最大的iou及其索引 best_iou, best_idx = iou.max(0) # 获取真实框对应的batch和类别标签 b, target_label = target[:, :2].long().t() # 获取真实框所处网格的位置,即匹配每个真实框到对应的网格,得到每个真实框需要使用哪个网格进行预测 gi, gj = xy.long().t() # 预测框中包含物体的mask obj_mask[b, best_idx, gj, gi] = 1 noobj_mask[b, best_idx, gj, gi] = 0 for i, anchor_iou in enumerate(iou.t()): noobj_mask[b[i], anchor_iou > ignore_thres, gj[i], gi[i]] = 0 # 根据真实框的坐标和尺寸, 得到真实框相对于所处网格的位置, 以便于与预测坐标计算误差 txy[b, best_idx, gj, gi] = xy - xy.floor() twh[b, best_idx, gj, gi] = t.log(wh / (eps + anchors[best_idx][:, :2])) # 将真实框的标签转换为one-hot编码形式 tcls[b, best_idx, gj, gi, target_label] = 1 class_mask[b, best_idx, gj, gi] = (pred_cls[b, best_idx, gj, gi].argmax(-1) == target_label).float() # 计算真实框相匹配的预测框和真实框之间的iou得分 iou_score[b, best_idx, gj, gi] = bbox_iou(pred_boxes[b, best_idx, gj, gi], target_boxes) # 真实框的置信度 tconf = obj_mask.float() return iou_score, class_mask, obj_mask, noobj_mask, txy, twh, tcls, tconf
计算误差,根据匹配好的真实框和预测框计算误差。代码如下
def calc_loss(prediction_coor, pred_conf, pred_cls, obj_mask, noobj_mask, txy, twh, tcls, tconf): """ annotate size: 特征图大小 Params prediction_coor: 网络预测坐标, shape -> (batch_size, num_anchors, size, size, 4) pred_conf: 预测的置信度, shape -> (batch_size, num_anchors, size, size) pred_cls: 预测的类别概率, shape -> (batch_size, num_anchors, size, size, class_num) obj_mask: 物体掩膜, 预测框包含物体即为1, shape -> (batch_size, num_anchors, size, size) noobj_mask: 非物体掩膜, 预测框不包含物体即为1, shape -> (batch_size, num_anchors, size, size) txy: 真实框中心坐标, shape -> (batch_size, num_anchors, size, size, 2) twh: 真实框大小, shape -> (batch_size, num_anchors, size, size, 2) tcls: 真实框类别one-hot编码, shape -> (batch_size, num_anchors, size, size, class_num) tconf: 真实框的置信度, shape -> (batch_size, num_anchors, size, size) Return total_loss(float): 总误差 """ xy = t.sigmoid(prediction_coor[..., :2]) wh = prediction_coor[..., 2:4] # 定位误差 # Note: 计算loc_loss之前,需要把坐标转换到所处网格的相对坐标和尺寸 loss_xy = self.mse_loss(xy[obj_mask], txy[obj_mask]) loss_wh = self.mse_loss(wh[obj_mask], twh[obj_mask]) # 置信度误差,只能包含0和1 loss_conf_obj = self.bce_loss(pred_conf[obj_mask], tconf[obj_mask]) loss_conf_noobj = self.bce_loss(pred_conf[noobj_mask], tconf[noobj_mask]) # 给与包含物体的置信度误差和不包含物体的置信度误差不同的权重 loss_conf = self.obj_scale * loss_conf_obj + self.noobj_scale * loss_conf_noobj # 分类误差 loss_cls = self.bce_loss(pred_cls[obj_mask], tcls[obj_mask]) # 总误差等于所有误差的总和 total_loss = loss_xy + loss_wh + loss_conf + loss_cls return total_loss
Yolov3总结
相比于Yolov2,Yolov3加深了网络,并且采用了3种尺度进行检测,使用了更多的anchor,速度明显更慢(推理时间为29ms),但精度却提升了不少(mAP-50达到了55.3),并且对于小目标的检测,效果更好。
other triks
Anchor的选择
Yolov3共使用了9种尺寸的anchor,那么9种尺寸是如何选择的呢。yolov3使用了维度聚类策略,对coco数据集中的所有真实框进行kmeans聚类,最终产生9个镞,得到9个anchor尺寸。
聚类距离度量采用如下的度量方式:
d(box; centroid) = 1 - IOU(box; centroid)
Reference
https://arxiv.org/abs/1506.02640
https://arxiv.org/abs/1612.08242
https://arxiv.org/abs/1804.02767
https://github.com/eriklinder...
https://blog.csdn.net/weixin_...