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,提高前景框定位误差的权重,降低背景框的权重。
接着,来介绍一下预测坐标和预测框之间的关系。预测坐标只是相对于网格的坐标,我们需要将预测坐标转换为相对于原图的坐标。下图展示了预测坐标与预测框之间的对应关系。
参数说明
: sigmoid函数
: 预定义的anchor高度
: 预定义的anchor宽度
: 物体中心所处网格左上角x坐标
: 物体中心所处网格左上角y坐标
网络预测坐标为:
最终预测框的坐标为:
<?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⎩⎪⎪⎪⎪⎨⎪⎪⎪⎪⎧x=σ(tx)+grid_xy=σ(ty)+grid_yw=etw∗Pwh=eth∗Ph
Yolov3主要代码
网络构建
网络搭建使用yolo官方的配置文件yolov3.cfg进行构建,以下结合cfg配置文件对网络中各个网络层进行简要说明:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | #-----------------------------------------# #---------------网络的超参数---------------# #-----------------------------------------# [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层
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | 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是误差计算中最重要的部分,它的功能是匹配,即匹配真实框和负责预测该真实框的预测框。它的主要过程如下:
01 02 03 04 | a. 首先将归一化的真实框转换到特征图尺寸上的真实框。 b. 计算预定义anchor和真实框的iou,获取与每个真实框的最大iou及其索引,即可得到每个真实框需要使用哪个尺寸的anchor来匹配。 c. 根据真实框所处网格位置以及每个真实框匹配的anchor,得到每个真实框匹配的预测框。 d. 根据真实框的坐标,得到真实框相对于物体所处网格的位置, 以便于与预测坐标计算误差。 |
代码如下:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | 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 |
计算误差,根据匹配好的真实框和预测框计算误差。代码如下
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | 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尺寸。
聚类距离度量采用如下的度量方式:
01 | 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_...
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异