提要
yolov3 在网络最后的输出中,对于每个grid cell产生3个bounding box,每个bounding box的输出有三类参数:
- 一个是对象的box参数,一共是四个值,即box的中心点坐标(x,y)和box的宽和高(w,h);
- 一个是置信度,这是个区间在[0,1]之间的值;
- 最后一个是一组条件类别概率,都是区间在[0,1]之间的值,代表概率。
假如一个图片被分割成 SxS 个grid cell,我们有B个anchor box,也就是说每个grid cell有B个bounding box, 每个bounding box内有4个位置参数,1个置信度,classes个类别概率,那么最终的输出维数是:S∗S∗[B∗(4+1+classes)]
。
下面分别具体介绍这三个参数的意义。
anchor box
概念理解
anchor box是从训练集的所有ground truth box中统计(使用k-means)出来的在训练集中最经常出现的几个box形状和尺寸。
anchor box其实就是对预测的对象范围进行约束,并加入了尺寸先验经验,从而实现多尺度学习的目的。
-
量化anchor box
维度聚类(dimension cluster):使用k-means算法在训练集中所有样本的ground truth box中聚类出具有代表性形状的宽和高。
到底找出几个anchor box算是最佳的具有代表性的形状?
做实验,聚类出多个数量不同anchor box组,分别应用到模型中,最终找出最优的在模型的复杂度和高召回率(high recall)之间折中的那组anchor box。作者在COCO数据集中使用了9个anchor box。
YOLO3为每种FPN预测特征图(13x13,26x26,52x52)设定3种anchor box,总共聚类出9种尺寸的anchor box。在COCO数据集这9个anchor box是:(10x13),(16x30),(33x23),(30x61),(62x45),(59x119),(116x90),(156x198),(373x326)。
分配上,在最小的13x13特征图上由于其感受野最大故应用最大的anchor box (116x90),(156x198),(373x326),(这几个坐标是针对416x416下的,当然要除以32把尺度缩放到13x13下),适合检测较大的目标。
中等的26x26特征图上由于其具有中等感受野故应用中等的anchor box (30x61),(62x45),(59x119),适合检测中等大小的目标。
较大的52x52特征图上由于其具有较小的感受野故应用最小的anchor box(10x13),(16x30),(33x23),适合检测较小的目标。
同Faster-Rcnn一样,特征图的每个像素(即每个grid)都会有对应的三个anchor box,如13x13特征图的每个grid都有三个anchor box (116x90),(156x198),(373x326)(这几个坐标需除以32缩放尺寸) -
怎么在实际的模型中加入anchor box的先验经验呢?
YOLO的做法是不让bounding box直接预测实际box的宽和高(w,h),而是将预测的宽和高分别与anchor box的宽和高绑定,这样不管一开始bounding box输出的(w,h)是怎样的,经过转化后都是与anchor box的宽和高相关,这样经过很多次惩罚训练后,每个bounding box就知道自己该负责怎样形状的box预测了。这个绑定的关系是什么?就涉及到了anchor box的计算。
anchor box的计算
前提需要知道,
c
x
c_x
cx,
c
y
c_y
cy的坐标是(0,0) (0,1),(0,2),(0,3)…(0,13)
(1,0),(1,1),(1,2),(1,3)…(1,13)等等
bouding box的输出应当为:
t
x
t_x
tx,
t
y
t_y
ty,
t
w
t_w
tw,
t
h
t_h
th
而真实的预测box应当是:
b
x
b_x
bx,
b
y
b_y
by(中心坐标),
b
w
b_w
bw,
b
h
b_h
bh(宽高)
还有就是
c
x
c_x
cx,
c
y
c_y
cy的每一个都是1,也就是说,每个格子grid cell是以1为一个范围,每个grid cell的大小实际是1∗1
绑定的关系是什么?就是下面这个公式:
b
w
=
a
w
e
t
w
b_w=a_we^{t_w}
bw=awetw
b
h
=
a
h
e
t
h
b_h=a_he^{t_h}
bh=aheth
其中,
a
w
a_w
aw,
a
h
a_h
ah为anchor box的宽和高,
t
w
t_w
tw,
t
h
t_h
th为bounding box直接预测出的宽和高,
b
w
b_w
bw,
b
h
b_h
bh为转换后预测的实际宽和高,
前面提到过box中心坐标总是落在相应的grid cell中的,所以bounding box直接预测出的
t
x
t_x
tx,
t
y
t_y
ty也是相对grid cell来说的,要想转换成最终输出的绝对坐标,需要下面的转换公式:
b
x
=
σ
(
t
x
)
+
c
x
b_x=\sigma(t_x)+c_x
bx=σ(tx)+cx
b
y
=
σ
(
t
y
)
+
c
y
b_y=\sigma(t_y)+c_y
by=σ(ty)+cy
其中,
σ
(
t
x
)
\sigma(t_x)
σ(tx)为sigmoid函数,
c
x
c_x
cx,
c
y
c_y
cy分别为grid cell方格左上角点相对整张图片的坐标。
公式tx,ty为何要sigmoid一下
主要是因为在训练时如果没有将
t
x
t_x
tx,
t
y
t_y
ty 压缩到(0,1)区间内的话,模型在训练前期很难收敛。用sigmoid将
t
x
t_x
tx,
t
y
t_y
ty压缩到[0,1]区间內,可以有效的确保目标中心处于执行预测的网格单元中,防止偏移过多。
网络不会预测边界框中心的确切坐标而是预测与预测目标的grid cell左上角相关的偏移
t
x
t_x
tx,
t
y
t_y
ty 。如13∗13的feature map中,某个目标的中心点预测为(0.4,0.7)【都小于1】,它的
c
x
c_x
cx,
c
y
c_y
cy,即中心落入的grid cell坐标是(6,6),则该物体的在feature map中的中心实际坐标显然是(6.4,6.7).这种情况没毛病,但若
t
x
t_x
tx,
t
y
t_y
ty大于1,比如(1.2,0.7),则该物体在feature map的的中心实际坐标是(7.2,6.7),注意这时候该物体中心在这个物体所属grid cell外面了,但(6,6)这个grid cell却检测出我们这个单元格内含有目标的中心(yolo是采取物体中心归哪个grid cell整个物体就归哪个grid celll了),这样就矛盾了,因为左上角为(6,6)的grid cell负责预测这个物体,这个物体中心必须出现在这个grid cell中而不能出现在它旁边网格中,一旦
t
x
t_x
tx,
t
y
t_y
ty 算出来大于1就会引起矛盾,因而必须归一化。
另外如果不是[0,1]区间,yolo的每个bbox的维度都是85,前5个属性是(Cx,Cy,w,h,confidence),后80个是类别概率,如果坐标不归一化,和这些概率值一起训练肯定不收敛啊
坐标变换
最终可以得出实际输出的box参数公式如下,这个也是在推理时将输出转换为最终推理结果的公式:
b
x
=
σ
(
t
x
)
+
c
x
b_x=\sigma(t_x)+c_x
bx=σ(tx)+cx
b
y
=
σ
(
t
y
)
+
c
y
b_y=\sigma(t_y)+c_y
by=σ(ty)+cy
b
w
=
a
w
e
t
w
b_w=a_we^{t_w}
bw=awetw
b
h
=
a
h
e
t
h
b_h=a_he^{t_h}
bh=aheth
其中,Cx,Cy是feature map中grid cell的左上角坐标,在yolov3中每个grid cell在feature map中的宽和高均为1。如下图1的情形时,这个bbox边界框的中心属于第二行第二列的grid cell,它的左上角坐标为(1,1),故Cx=1,Cy=1.公式中的Pw、Ph是预设的anchor box映射到feature map中的宽和高 (anchor box原本设定是相对于416*416坐标系下的坐标,在yolov3.cfg文件中写明了,代码中是把cfg中读取的坐标除以stride如32映射到feature map坐标系中)。
最终得到的边框坐标值是bx,by,bw,bh即边界框bbox相对于feature map的位置和大小
边框回归最简单的想法就是通过平移加尺度缩放进行微调嘛。
接下来还需要约束bbox的位置预测值到[0,1],会使得模型更容易稳定训练(如果不是[0,1]区间,yolo的每个bbox的维度都是85,前5个属性是(Cx,Cy,w,h,confidence),后80个是类别概率,如果坐标不归一化,和这些概率值一起训练肯定不收敛啊)
b
x
=
(
σ
(
t
x
)
+
c
x
)
/
w
b_x=(\sigma(t_x)+c_x)/w
bx=(σ(tx)+cx)/w
b
y
=
(
σ
(
t
y
)
+
c
y
)
/
h
b_y=(\sigma(t_y)+c_y)/h
by=(σ(ty)+cy)/h
b
w
=
a
w
e
t
w
/
w
b_w=a_we^{t_w}/w
bw=awetw/w
b
h
=
a
h
e
t
h
/
h
b_h=a_he^{t_h}/h
bh=aheth/h
其中w,h为feature map的大小
box get_yolo_box(float *x, float *biases, int n, int index, int i, int j, int lw, int lh, int w, int h, int stride)
{
box b;
b.x = (i + x[index + 0*stride]) / lw;
// 此处相当于知道了X的index,要找Y的index,向后偏移l.w*l.h个索引
b.y = (j + x[index + 1*stride]) / lh;
b.w = exp(x[index + 2*stride]) * biases[2*n] / w;
b.h = exp(x[index + 3*stride]) * biases[2*n+1] / h;
return b;
}
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]);
scale = 2 - groundtruth.w * groundtruth.h
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;
}
输出坐标变化
得到除以了W,H后的bx,by,bw,bh,如果将这4个值分别乘以输入网络的图片的宽和高(如416x416)就可以得到bbox相对于坐标系(416x416)位置和大小了。但还要将相对于输入网络图片(416x416)的边框属性变换成原图按照纵横比不变进行缩放后的区域的坐标(416x312)。应该将方框的坐标转换为相对于填充后的图片中包含原始图片区域的计算方式。
scaling_factor * img_w
和scaling_factor * img_h
是图片按照纵横比不变进行缩放后的图片,即原图是768x576按照纵横比长边不变缩放到了416x372。
经坐标换算,得到的坐标还是在输入网络的图片(416x416)坐标系下的绝对坐标,但是此时已经是相对于416x372这个区域的坐标了,而不再相对于(0,0)原点。
#scaling_factor*img_w和scaling_factor*img_h是图片按照纵横比不变进行缩放后的图片,即原图是768x576按照纵横比长边不变缩放到了416*372。
#经坐标换算,得到的坐标还是在输入网络的图片(416x416)坐标系下的绝对坐标,但是此时已经是相对于416*372这个区域的坐标了,而不再相对于(0,0)原点。
output[:,[1,3]] -= (inp_dim - scaling_factor*im_dim_list[:,0].view(-1,1))/2#x1=x1−(416−scaling_factor*img_w)/2,x2=x2-(416−scaling_factor*img_w)/2
output[:,[2,4]] -= (inp_dim - scaling_factor*im_dim_list[:,1].view(-1,1))/2#y1=y1-(416−scaling_factor*img_h)/2,y2=y2-(416−scaling_factor*img_h)/2
其实代码的含义就是把y1,y2减去图2灰色部分的一半,y1=y1-(416-416/768576)/2=y1-(416-312)/2,把x1,x2,y1,y2的坐标系换算到了针对实际红框的坐标系(416312)下了。这样保证bbox不会扭曲,
w和h的loss计算
yolov1里作者在loss里对宽高都做了开根号处理,是为了使得大小差别比较大的边框差别减小
而在yolov2和v3里,损失函数进行了改进,不再简单地加根号了,而是用scale = 2 - groundtruth.w * groundtruth.h加大对小框的损失。
输入图片resize细节
这里需要注意的是,虽然输入尺寸是416416,但原图是按照纵横比例缩放至416416的, 取 min(w/img_w, h/img_h)这个比例来缩放,保证长的边缩放为需要的输入尺寸416,而短边按比例缩放不会扭曲,img_w,img_h是原图尺寸768,576, 缩放后的尺寸为new_w, new_h=416,312,需要的输入尺寸是w,h=416*416.如图所示:
剩下的灰色区域用(128,128,128)填充即可构造为416x416。不管训练还是测试时都需要这样操作原图
def letterbox_image(img, inp_dim):
"""
lteerbox_image()将图片按照纵横比进行缩放,将空白部分用(128,128,128)填充,调整图像尺寸
具体而言,此时某个边正好可以等于目标长度,另一边小于等于目标长度
将缩放后的数据拷贝到画布中心,返回完成缩放
"""
img_w, img_h = img.shape[1], img.shape[0]
w, h = inp_dim#inp_dim是需要resize的尺寸(如416*416)
# 取min(w/img_w, h/img_h)这个比例来缩放,缩放后的尺寸为new_w, new_h,即保证较长的边缩放后正好等于目标长度(需要的尺寸),另一边的尺寸缩放后还没有填充满.
new_w = int(img_w * min(w/img_w, h/img_h))
new_h = int(img_h * min(w/img_w, h/img_h))
resized_image = cv2.resize(img, (new_w,new_h), interpolation = cv2.INTER_CUBIC) #将图片按照纵横比不变来缩放为new_w x new_h,768 x 576的图片缩放成416x312.,用了双三次插值
# 创建一个画布, 将resized_image数据拷贝到画布中心。
canvas = np.full((inp_dim[1], inp_dim[0], 3), 128)#生成一个我们最终需要的图片尺寸hxwx3的array,这里生成416x416x3的array,每个元素值为128
# 将wxhx3的array中对应new_wxnew_hx3的部分(这两个部分的中心应该对齐)赋值为刚刚由原图缩放得到的数组,得到最终缩放后图片
canvas[(h-new_h)//2:(h-new_h)//2 + new_h,(w-new_w)//2:(w-new_w)//2 + new_w, :] = resized_image
return canvas
label归一化
而且我们注意yolov3需要的训练数据的label是根据原图尺寸归一化了的,这样做是因为怕大的边框的影响比小的边框影响大,因此做了归一化的操作,这样大的和小的边框都会被同等看待了,而且训练也容易收敛。
训练细节(误差计算)
关于box参数的转换还有一点值得一提,作者在训练中并不是将
t
x
t_x
tx,
t
y
t_y
ty,
t
w
t_w
tw,
t
h
t_h
th 转换为
b
x
b_x
bx,
b
y
b_y
by,
b
w
b_w
bw,
b
h
b_h
bh 后与ground truth box的对应参数求误差,而是使用上述公式的逆运算将ground truth box的参数转换为与
t
x
t_x
tx,
t
y
t_y
ty,
t
w
t_w
tw,
t
h
t_h
th 对应的
g
x
g_x
gx,
g
y
g_y
gy,
g
w
g_w
gw,
g
h
g_h
gh ,然后再计算误差。
也就是说,我们训练的输出是:
t
x
t_x
tx,
t
y
t_y
ty,
t
w
t_w
tw,
t
h
t_h
th, 那么在计算误差时,也是利用真实框的
t
^
x
\hat t_x
t^x,
t
^
y
\hat t_y
t^y,
t
^
w
\hat t_w
t^w,
t
^
h
\hat t_h
t^h 这几个值计算误差。
如果预测十分准确,那么
g
x
g_x
gx,
g
y
g_y
gy,
g
w
g_w
gw,
g
h
g_h
gh和
b
x
b_x
bx,
b
y
b_y
by,
b
w
b_w
bw,
b
h
b_h
bh二者完全相等。代入图中的公式可得
g
x
=
σ
(
t
^
x
)
+
c
x
g_x=\sigma(\hat t_x)+c_x
gx=σ(t^x)+cx
g
y
=
σ
(
t
^
y
)
+
c
y
g_y=\sigma(\hat t_y)+c_y
gy=σ(t^y)+cy
g
w
=
a
w
e
t
^
w
g_w=a_we^{\hat t_w}
gw=awet^w
g
h
=
a
h
e
t
^
h
g_h=a_he^{\hat t_h}
gh=ahet^h
计算中由于sigmoid函数的反函数难计算,所以并没有计算sigmoid的反函数,而是计算输出对应的sigmoid函数值。
σ
(
t
^
x
)
=
g
x
−
c
x
\sigma(\hat t_x)=g_x-c_x
σ(t^x)=gx−cx
σ
(
t
^
y
)
=
g
y
−
c
y
\sigma(\hat t_y)=g_y-c_y
σ(t^y)=gy−cy
t
^
w
=
l
o
g
(
g
w
/
a
w
)
\hat t_w=log(g_w/a_w)
t^w=log(gw/aw)
t
^
h
=
l
o
g
(
g
h
/
a
h
)
\hat t_h=log(g_h/a_h)
t^h=log(gh/ah)
这样,就可以求误差了
如何选择对应的anchor
一个尺度的feature map有三个anchors,那么对于某个ground truth框,究竟是哪个anchor负责匹配它呢?和YOLOv1一样,对于训练图片中的ground truth,若其中心点落在某个cell内,那么该cell内的3个anchor box负责预测它,具体是哪个anchor box预测它,需要在训练中确定,即由那个与ground truth的IOU最大的anchor box预测它,而剩余的2个anchor box不与该ground truth匹配。YOLOv3需要假定每个cell至多含有一个grounth truth,而在实际上基本不会出现多于1个的情况。与ground truth匹配的anchor box计算坐标误差、置信度误差(此时target为1)以及分类误差,而其它的anchor box只计算置信度误差(此时target为0)。
为什么要做尺度缩放
不管Faster-RCNN还是YOLO,都不是直接回归bounding box的长宽而是尺度缩放到对数空间,是怕训练会带来不稳定的梯度。因为如果不做变换,直接预测相对形变tw,那么要求tw>0,因为你的框的宽高不可能是负数。这样,是在做一个有不等式条件约束的优化问题,没法直接用SGD来做。所以先取一个对数变换,将其不等式约束去掉,就可以了。
置信度(confidence)
还存在一个很关键的问题:在训练中我们挑选哪个bounding box的准则是选择预测的box与ground truth box的IOU最大的bounding box做为最优的box,但是在预测中并没有ground truth box,怎么才能挑选最优的bounding box呢?这就需要另外的参数了,那就是下面要说到的置信度。
置信度是每个bounding box输出的其中一个重要参数,作者对他的作用定义有两重:
- 一重是:代表当前box是否有对象的概率 P r ( O b j e c t ) P_r(Object) Pr(Object),注意,是对象,不是某个类别的对象,也就是说它用来说明当前box内只是个背景(backgroud)还是有某个物体(对象);
- 另一重:表示当前的box有对象时,它自己预测的box与物体真实的box可能的 I O U p r e d t r u t h IOU^{truth}_{pred} IOUpredtruth 的值,注意,这里所说的 物体真实的box实际是不存在的 ,这只是 模型表达自己框出了物体的自信程度。
以上所述,也就不难理解作者为什么将其称之为置信度了,因为不管哪重含义,都表示一种自信程度:框出的box内确实有物体的自信程度和框出的box将整个物体的所有特征都包括进来的自信程度。经过以上的解释,其实我们也就可以用数学形式表示置信度的定义了:
C
i
j
=
P
r
(
o
b
j
e
c
t
)
∗
I
O
U
p
r
e
d
t
r
u
t
h
C^j_i=P_r(object)*IOU^{truth}_{pred}
Cij=Pr(object)∗IOUpredtruth
其中,
C
i
j
C^j_i
Cij 表示第
i
i
i 个grid cell的第
j
j
j 个bounding box的置信度。
那么如何训练
C
i
j
C^j_i
Cij ?
训练中,
C
i
j
C^j_i
Cij 表示真实值,
C
i
j
C^j_i
Cij 的取值是由grid cell的bounding box有没有负责预测某个对象决定的。如果负责,那么
C
i
j
=
1
C^j_i=1
Cij=1 否则,
C
i
j
=
0
C^j_i=0
Cij=0
下面我们来说明如何确定某个grid cell的bounding box是否负责预测该grid cell中的对象:前面在说明anchor box的时候提到每个bounding box负责预测的形状是依据与其对应的anchor box(bounding box prior)相关的,那这个anchor box与该对象的ground truth box的IOU在所有的anchor box(与一个grid cell中所有bounding box对应,COCO数据集中是9个)与ground truth box的IOU中最大,那它就负责预测这个对象,因为这个形状、尺寸最符合当前这个对象,这时
C
i
j
=
1
C^j_i=1
Cij=1,其他情况下
C
i
j
=
0
C^j_i=0
Cij=0。注意,你没有看错,就是所有anchor box与某个ground truth box的IOU最大的那个anchor box对应的bounding box负责预测该对象,与该bounding box预测的box没有关系。
对象条件类别概率(conditional class probabilities)
对象条件类别概率是一组概率的数组,数组的长度为当前模型检测的类别种类数量,它的意义是当bounding box认为当前box中有对象时,要检测的所有类别中每种类别的概率.
其实这个和分类模型最后使用softmax函数输出的一组类别概率是类似的,只是二者存在两点不同:
- YOLO的对象类别概率中没有background一项,也不需要,因为对background的预测已经交给置信度了,所以它的输出是有条件的,那就是在置信度表示当前box有对象的前提下,所以条件概率的数学形式为 P r ( c l a s s i ∣ o b j e c t ) P_r(class_i|object) Pr(classi∣object)
- 分类模型中最后输出之前使用softmax求出每个类别的概率,也就是说各个类别之间是互斥的,而YOLOv3算法的每个类别概率是单独用逻辑回归函数(sigmoid函数)计算得出了,所以每个类别不必是互斥的,也就是说一个对象可以被预测出多个类别。这个想法其实是有一些YOLO9000的意思的,因为YOLOv3已经有9000类似的功能,不同只是不能像9000一样,同时使用分类数据集和对象检测数据集,且类别之间的词性是有从属关系的。