YOLO v3解读

转自:https://xmfbit.github.io/2018/04/01/paper-yolov3/

YOLO的作者又放出了V3版本,在之前的版本上做出了一些改进,达到了更好的性能。这篇博客介绍这篇论文:YOLOv3: An Incremental Improvement。下面这张图是YOLO V3与RetinaNet的比较。

YOLO v3和RetinaNet的比较

可以使用搜索功能,在本博客内搜索YOLO前作的论文阅读和代码。

YOLO v3比你们不知道高到哪里去了

YOLO v3在保持其一贯的检测速度快的特点前提下,性能又有了提升:输入图像为320×320320×320大小的图像,可以在2222ms跑完,mAP达到了28.228.2,这个数据和SSD相同,但是快了33倍。在TitanX上,YOLO v3可以在5151ms内完成,AP50AP50的值为57.957.9。而RetinaNet需要198198ms,AP50AP50近似却略低,为57.557.5

ps:啥是AP

AP就是average precision啦。在detection中,我们认为当预测的bounding box和ground truth的IoU大于某个阈值(如取为0.50.5)时,认为是一个True Positive。如果小于这个阈值,就是一个False Positive。

所谓precision,就是指检测出的框框中有多少是True Positive。另外,还有一个指标叫做recall,是指所有的ground truth里面,有多少被检测出来了。这两个概念都是来自于classification问题,通过设定上面IoU的阈值,就可以迁移到detection中了。

我们可以取不同的阈值,这样就可以绘出一条precisio vs recall的曲线,计算曲线下的面积,就是AP值。COCO中使用了0.5:0.05:0.95十个离散点近似计算(参考COCO的说明文档网页)。detection中常常需要同时检测图像中多个类别的物体,我们将不同类别的AP求平均,就是mAP。

如果我们只看某个固定的阈值,如0.5,计算所有类别的平均AP,那么就用AP50来表示。所以YOLO v3单拿出来AP50说事,是为了证明虽然我的bounding box不如你RetinaNet那么精准(IoU相对较小),但是如果你对框框的位置不是那么敏感(0.5 的阈值很多时候够用了),那么我是可以做到比你更好更快的。

Bounding Box位置的回归

这里和原来v2基本没区别。仍然使用聚类产生anchor box的长宽(下式的pwph)。网络预测四个值:txtytwth。我们知道,YOLO网络最后输出是一个M×M的feature map,对应于M×M个cell。如果某个cell距离image的top left corner距离为(cx,cy)(也就是cell的坐标),那么该cell内的bounding box的位置和形状参数为:

bxbybwbh=σ(tx)+cx=σ(ty)+cy=pwetw=pheth

PS:这里有一个问题,不管FasterRCNN还是YOLO,都不是直接回归bounding box的长宽(就像这样:bw=pwtw),而是要做一个对数变换,实际预测的是  log⁡(⋅)。这里小小解释一下。

这是因为如果不做变换,直接预测相对形变tw,那么要求tw>0,因为你的框框的长宽不可能是负数。这样,是在做一个有不等式条件约束的优化问题,没法直接用SGD来做。所以先取一个对数变换,将其不等式约束去掉,就可以了。

bounding box的回归

在训练的时候,使用平方误差损失。

另外,YOLO会对每个bounding box给出是否是object的置信度预测,用来区分objects和背景。这个值使用logistic回归。当某个bounding box与ground truth的IoU大于其他所有bounding box时,target给
1
;如果某个bounding box不是IoU最大的那个,但是IoU也大于了某个阈值(我们取
0.5
),那么我们忽略它(既不惩罚,也不奖励),这个做法是从Faster RCNN借鉴的。我们对每个ground truth只分配一个最好的bounding box与其对应(这与Faster RCNN不同)。如果某个bounding box没有倍assign到任何一个ground truth对应,那么它对边框位置大小的回归和class的预测没有贡献,我们只惩罚它的objectness,即试图减小其confidence。

分类预测

我们不用softmax做分类了,而是使用独立的logisitc做二分类。这种方法的好处是可以处理重叠的多标签问题,如Open Image Dataset。在其中,会出现诸如WomanPerson这样的重叠标签。

FPN加持的多尺度预测

之前YOLO的一个弱点就是缺少多尺度变换,使用FPN中的思路,v3在
3
个不同的尺度上做预测。在COCO上,我们每个尺度都预测
3
个框框,所以一共是
9
个。所以输出的feature map的大小是
N×N×[3×(4+1+80)]

然后我们从两层前那里拿feature map,upsample 2x,并与更前面输出的feature map通过element-wide的相加做merge。这样我们能够从后面的层拿到更多的高层语义信息,也能从前面的层拿到细粒度的信息(更大的feature map,更小的感受野)。然后在后面接一些conv做处理,最终得到和上面相似大小的feature map,只不过spatial dimension变成了2倍。

照上一段所说方法,再一次在final scale尺度下给出预测。

代码实现

在v3中,作者新建了一个名为yolo的layer,其参数如下:

 
    
1
2
3
4
5
6
7
8
9
10
 
    
[yolo]
mask = 0,1,2
## 9组anchor对应9个框框
anchors = 10,13, 16,30, 33,23, 30,61, 62,45, 59,119, 116,90, 156,198, 373,326
classes=20 ## VOC20类
num=9
jitter=.3
ignore_thresh = .5
truth_thresh = 1
random=1

打开yolo_layer.c文件,找到forward部分代码。可以看到,首先,对输入进行activation。注意,如论文所说,对类别进行预测的时候,没有使用v2中的softmax或softmax tree,而是直接使用了logistic变换。

 
    
1
2
3
4
5
6
7
8
9
10
 
    
for (b = 0; b < l.batch; ++b){
for(n = 0; n < l.n; ++n){
int index = entry_index(l, b, n*l.w*l.h, 0);
// 对 tx, ty进行logistic变换
activate_array(l.output + index, 2*l.w*l.h, LOGISTIC);
index = entry_index(l, b, n*l.w*l.h, 4);
// 对confidence和C类进行logistic变换
activate_array(l.output + index, ( 1+l.classes)*l.w*l.h, LOGISTIC);
}
}

我们看一下如何计算梯度。

 
    
1
2
3
4
5
6
7
8
9
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
 
    
for (j = 0; j < l.h; ++j) {
for (i = 0; i < l.w; ++i) {
for (n = 0; n < l.n; ++n) {
// 对每个预测的bounding box
// 找到与其IoU最大的ground truth
int box_index = entry_index(l, b, n*l.w*l.h + j*l.w + i, 0);
box pred = get_yolo_box(l.output, l.biases, l.mask[n], box_index, i, j, l.w, l.h, net.w, net.h, l.w*l.h);
float best_iou = 0;
int best_t = 0;
for(t = 0; t < l.max_boxes; ++t){
box truth = float_to_box(net.truth + t*( 4 + 1) + b*l.truths, 1);
if(!truth.x) break;
float iou = box_iou(pred, truth);
if (iou > best_iou) {
best_iou = iou;
best_t = t;
}
}
int obj_index = entry_index(l, b, n*l.w*l.h + j*l.w + i, 4);
avg_anyobj += l.output[obj_index];
// 计算梯度
// 如果大于ignore_thresh, 那么忽略
// 如果小于ignore_thresh,target = 0
// diff = -gradient = target - output
// 为什么是上式,见下面的数学分析
l.delta[obj_index] = 0 - l.output[obj_index];
if (best_iou > l.ignore_thresh) {
l.delta[obj_index] = 0;
}
// 这里仍然有疑问,为何使用truth_thresh?这个值是1
// 按道理,iou无论如何不可能大于1啊。。。
if (best_iou > l.truth_thresh) {
// confidence target = 1
l.delta[obj_index] = 1 - l.output[obj_index];
int class = net.truth[ best_t*( 4 + 1) + b*l.truths + 4];
if (l. map) class = l. map[ class];
int class_index = entry_index(l, b, n*l.w*l.h + j*l.w + i, 4 + 1);
// 对class进行求导
delta_yolo_class(l.output, l.delta, class_index, class, l.classes, l.w*l.h, 0);
box truth = float_to_box(net.truth + best_t*( 4 + 1) + b*l.truths, 1);
// 对box位置参数进行求导
delta_yolo_box(truth, l.output, l.biases, l.mask[n], box_index, i, j, l.w, l.h, net.w, net.h, l.delta, ( 2-truth.w*truth.h), l.w*l.h);
}
}
}
}

我们首先来说一下为何confidence(包括后面的classification)的diff计算为何是target - output的形式。对于logistic regression,假设logistic函数的输入是o=f(x;θ)   。其中,θθ是网络的参数。那么输出y=h(o),其中hh指logistic激活函数(或sigmoid函数)。那么,我们有:

P(y=1|x)P(y=0|x)=h(o)=1h(o)

写出对数极大似然函数,我们有:

logL=ylogh+(1y)log(1h)

为了使用SGD,上式两边取相反数,我们有损失函数:

J=logL=ylogh(1y)log(1h)

对第ii个输入oi求导,我们有:

Joi=Jhihioi=[yi/hi(yi1)/(1hi)]hioi=hiyihi(1hi)hioi




其中,

hi即为logistic激活后的输出, yi为target。由于YOLO代码中均使用 diff,也就是 -gradient,所以有 delta = target - output

关于logistic回归,还可以参考我的博客:CS229 简单的监督学习方法

下面,我们看下两个关键的子函数,delta_yolo_classdelta_yolo_box的实现。

 
    
1
2
3
4
5
6
7
8
9
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
 
    
// class是类别的ground truth
// classes是类别总数
// index是feature map一维数组里面class prediction的起始索引
void delta_yolo_class(float *output, float *delta, int index,
int class, int classes, int stride, float *avg_cat) {
int n;
// 这里暂时不懂
if (delta[index]){
delta[index + stride* class] = 1 - output[index + stride* class];
if(avg_cat) *avg_cat += output[index + stride* class];
return;
}
for(n = 0; n < classes; ++n){
// 见上,diff = target - prediction
delta[index + stride*n] = ((n == class)? 1 : 0) - output[index + stride*n];
if(n == class && avg_cat) *avg_cat += output[index + stride*n];
}
}
// box delta这里没什么可说的,就是square error的求导
float delta_yolo_box(box truth, float *x, float *biases, int n,
int index, int i, int j, int lw, int lh, int w, int h,
float *delta, float scale, int stride) {
box pred = get_yolo_box(x, biases, n, index, i, j, lw, lh, w, h, stride);
float iou = box_iou(pred, truth);
float tx = (truth.x*lw - i);
float ty = (truth.y*lh - j);
float tw = log(truth.w*w / biases[ 2*n]);
float th = log(truth.h*h / biases[ 2*n + 1]);
delta[index + 0*stride] = scale * (tx - x[index + 0*stride]);
delta[index + 1*stride] = scale * (ty - x[index + 1*stride]);
delta[index + 2*stride] = scale * (tw - x[index + 2*stride]);
delta[index + 3*stride] = scale * (th - x[index + 3*stride]);
return iou;
}

上面,我们遍历了每一个prediction的bounding box,下面我们还要遍历每个ground truth,根据IoU,为其分配一个最佳的匹配。

 
    
1
2
3
4
5
6
7
8
9
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
 
    
// 遍历ground truth
for(t = 0; t < l.max_boxes; ++t){
box truth = float_to_box(net.truth + t*( 4 + 1) + b*l.truths, 1);
if(!truth.x) break;
// 找到iou最大的那个bounding box
float best_iou = 0;
int best_n = 0;
i = (truth.x * l.w);
j = (truth.y * l.h);
box truth_shift = truth;
truth_shift.x = truth_shift.y = 0;
for(n = 0; n < l.total; ++n){
box pred = { 0};
pred.w = l.biases[ 2*n]/net.w;
pred.h = l.biases[ 2*n+ 1]/net.h;
float iou = box_iou(pred, truth_shift);
if (iou > best_iou){
best_iou = iou;
best_n = n;
}
}
int mask_n = int_index(l.mask, best_n, l.n);
if(mask_n >= 0){
int box_index = entry_index(l, b, mask_n*l.w*l.h + j*l.w + i, 0);
float iou = delta_yolo_box(truth, l.output, l.biases, best_n,
box_index, i, j, l.w, l.h, net.w, net.h, l.delta,
( 2-truth.w*truth.h), l.w*l.h);
int obj_index = entry_index(l, b, mask_n*l.w*l.h + j*l.w + i, 4);
avg_obj += l.output[obj_index];
// 对应objectness target = 1
l.delta[obj_index] = 1 - l.output[obj_index];
int class = net.truth[t*( 4 + 1) + b*l.truths + 4];
if (l. map) class = l. map[ class];
int class_index = entry_index(l, b, mask_n*l.w*l.h + j*l.w + i, 4 + 1);
delta_yolo_class(l.output, l.delta, class_index, class, l.classes, l.w*l.h, &avg_cat);
++count;
++class_count;
if(iou > .5) recall += 1;
if(iou > .75) recall75 += 1;
avg_iou += iou;
}
}

Darknet网络架构

引入了ResidualNet的思路(3×1×1的卷积核,shortcut连接),构建了Darknet-53网络。
darknet-63

YOLO的优势和劣势

把YOLO v3和其他方法比较,优势在于快快快。当你不太在乎IoU一定要多少多少的时候,YOLO可以做到又快又好。作者还在文章的结尾发起了这样的牢骚:

Russakovsky et al report that that humans have a hard time distinguishing an IOU of .3 from .5! “Training humans to visually inspect a bounding box with IOU of 0.3 and distinguish it from one with IOU 0.5 is surprisingly difficult.” [16] If humans have a hard time telling the difference, how much does it matter?

使用了多尺度预测,v3对于小目标的检测结果明显变好了。不过对于medium和large的目标,表现相对不好。这是需要后续工作进一步挖局的地方。

下面是具体的数据比较。
具体数据比较

我们是身经百战,见得多了

作者还贴心地给出了什么方法没有奏效。

  • anchor box坐标(x,y的预测。预测anchor box的offset,no stable,不好。
  • 线性offset预测,而不是logistic。精度下降。
  • focal loss。精度下降。
  • 双IoU阈值,像Faster RCNN那样。效果不好。

参考资料

下面是一些可供利用的参考资料:

  • 4
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: yolo v3 pytorch版源码解读是指对yolo v3算法在pytorch框架下的实现进行解析和说明。该算法是一种目标检测算法,可以在图像中检测出物体的位置和类别。在pytorch框架下,该算法的实现主要包括网络结构的搭建、数据预处理、训练和测试等步骤。通过对源码的解读,可以深入理解yolo v3算法的原理和实现细节,从而更好地应用该算法解决实际问题。 ### 回答2: YOLO (You Only Look Once)是一种基于卷积神经网络的目标检测算法,具有实时性和高准确率等优点。而YOLO v3则是YOLO系列最新版本,其采用了一系列改进措施来提升检测性能,如采用多尺度特征图、SPP结构和残差连接等。此外,由于PyTorch成为了深度学习领域流行的开源框架,因此许多研究者也将YOLO v3的代码移植到了PyTorch实现。 YOLO v3 PyTorch版源码在实现上比较复杂,需要结合相关论文和理论知识进行理解。其主要包括以下几个部分: 1.网络结构:YOLO v3采用了Darknet-53作为特征提取层,并将输入图像分别缩放到三个尺度,然后对不同尺度的特征图进行检测。 2.特征图处理:在进行检测前,需要对特征图进行处理,主要包括三个方面。第一,对于不同尺度的特征图需要分别调整每个锚点框的大小和形状。第二,为了提高检测质量,需要引入SPP结构(Spatial Pyramid Pooling),该结构能够获取不同大小的感受野。第三,引入残差连接(Residual Connection),可以在网络中学习更加精细的特征。 3.检测流程:YOLO v3采用的是Multi-scale Training的训练方式,即在多个不同尺度下分别训练,并且将在不同尺度下的检测结果进行融合。对于每个尺度分别计算每个位置预测框的置信度、类别得分和框的坐标信息。然后通过非极大抑制算法,去除重复检测的框,并提取概率最高的框作为最终检测结果。 总的来说,YOLO v3 PyTorch版源码实现较为复杂,需要对目标检测算法、卷积神经网络、图像处理等技术领域有深入的理解。对于初学者来说,可以先从代码的框架结构入手,逐步了解各个模块的作用和实现细节。而要深入理解YOLO v3的原理和算法,还需要结合相关论文和多方面的参考资料进行学习。 ### 回答3: YOLOv3是目前最快的目标检测算法之一,它采用了纯卷积的方式实现,并且其结果也非常准确。在目标检测中,YOLOv3是一个非常重要的算法,因此了解其源码非常有必要。下面将就yolo v3 pytorch版源码解读做一些简要分析。 首先,需要了解的是,YOLOv3主要分为三个部分:输入预处理、特征提取和后处理。输入预处理是将原图像扩展为模型输入的固定尺寸。在特征提取阶段,YOLOv3采用的是多尺度特征融合的方法,通过不同的尺度获得不同的特征图。在后处理阶段,YOLOv3对特征图进行预测,通过置信度评分、NMS和筛选等操作输出最终的检测结果。 其次,需要注意的是,YOLOv3使用的是Darknet53作为基础网络,该网络由很多卷积层、max-pooling层、residual层等组成。在网络最后一层,将会有三个不同大小的盒子来检测不同大小的目标,每个盒子会预测3个不同的类别,总共预测9个类别。对于每个目标,预测将会包括坐标、置信度、类别等信息。在训练过程中,对于每个目标,YOLOv3会使用交叉熵损失函数来计算误差,然后进行反向传播。 最后,需要提到的是,YOLOv3源码的实现非常复杂,需要熟悉深度学习、神经网络等相关领域才能理解。同时,在实际使用中,需要根据自己的需求进行修改和优化,以达到最佳检测效果。当然,在理解源码的过程中,最重要的还是深入理解YOLOv3的算法原理和各个模块之间的关系,这样才能更好地掌握该算法及其实现。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值