本文是对YOLO1的学习笔记,主要是从术语、网络结构、损失函数三个方面对这个网络进行学习,YOLO3据说比一代更优秀,所以我也是简单学了学基础的概念,没有去深入研究如何实现,下面是我对YOLO1的理解,相关参考我已给出链接,个人理解的内容肯定有不够准确的地方,希望能和大家交流讨论。
基本术语
网格
YOLO将输入图像划分为S*S的网格,每个网格负责检测中心落在该栅格中的物体。每一个栅格预测B个bounding boxes,以及这些 bounding boxes 的 confidence scores。这个 confidence scores 反映了模型对于这个网格的预测:该网格是否含有物体,以及这个box的坐标预测的有多准。
bounding box
YOLO的预测框,一个 bounding box 包含(x,y,h,w,confidence )五个值,同样,每一组(x,y,h,w,confidence)确定一个bounding box 框
confidence scores
c o n f i d e n c e = P r ( O b j e c t ) ∗ I O U p r e d t r u t h confidence=Pr(Object)\ast IOU_{pred}^{truth} confidence=Pr(Object)∗IOUpredtruth
若bounding box包含物体,则 P r ( o b j e c t ) = 1 Pr(object) = 1 Pr(object)=1;否则 P r ( o b j e c t ) = 0 Pr(object) = 0 Pr(object)=0。
下面损失函数中的 C ^ \hat C C^ 就是包含物体的 bounding box 的 confidence 分。
IOU
IOU(intersection over union)为检测结果(最终生成的框)与真实值Ground Truth的交集比上它们的并集。
参考:https://blog.csdn.net/weixin_40922744/article/details/102988751
def calculate_IOU(rec1,rec2):
""" 计算两个矩形框的交并比
Args:
rec1: [left1,top1,right1,bottom1] # 其中(left1,top1)为矩形框rect1左上角的坐标,(right1, bottom1)为右下角的坐标,下同。
rec2: [left2,top2,right2,bottom2]
Returns:
交并比IoU值
"""
left_max = max(rec1[0],rec2[0])
top_max = max(rec1[1],rec2[1])
right_min = min(rec1[2],rec2[2])
bottom_min = min(rec1[3],rec2[3])
#两矩形相交时计算IoU
if (left_max < right_min or bottom_min > top_max): # 判断时加不加=都行,当两者相等时,重叠部分的面积也等于0
rect1_area = (rec1[2]-rec1[0])*(rec1[3]-rec1[1])
rect2_area = (rec2[2]-rec2[0])*(rec2[3]-rec2[1])
area_cross = (bottom_min - top_max)*(right_min - left_max)
return area_cross / (rect1_area + rect2_area - area_cross)
else:
return 0
类别条件概率
P r ( C l a s s i ∣ O b j e c t ) P_r(Class_i |Object) Pr(Classi∣Object) 表示网格内存在物体且属于第i类的概率
一个网格中的物体属于i类的概率是
P
r
(
C
l
a
s
s
i
∣
O
b
j
e
c
t
)
∗
c
o
n
f
i
d
e
n
c
e
P_r(Class_i |Object) *confidence
Pr(Classi∣Object)∗confidence,即:
P
r
(
C
l
a
s
s
i
∣
O
b
j
e
c
t
)
∗
P
r
(
O
b
j
e
c
t
)
∗
I
O
U
p
r
e
d
t
r
u
t
h
P_r(Class_i|Object) ∗ P_r(Object) ∗ IOU^{truth}_{pred} %3D P_r(Class_i) ∗ IOU^{truth}_{pred}
Pr(Classi∣Object)∗Pr(Object)∗IOUpredtruth
输入数据与预处理方法
数据要求
输入数据应该统一大小且带标签,标签包含目标框位置和物体类别,这里注意,目标框的位置和大小(x,y,h,w)都是相对于图片大小(图片大小为应该为448×448)的比例值,比如假设实际位置是(1,2,3,4),则标注位置应该为(1/448,2/448,3/448,4/448),其中x,y是中心点坐标,h,w是目标框的高和宽。
给数据打标
打标可以用软件labelImg,打标后可以得到每张图片对应的XML文件,文件内容类似
但是此时XML文件中标注的(x,y,h,w)不是YOLOv1想要的那种,所以还得对标准文件进行转换
import xml.etree.ElementTree as ET
import pickle
import os
from os import listdir, getcwd
from os.path import join
sets=[('2018', 'VOC')]
classes = ["zuoyuting"] #需要处理的标签
def convert(size, box):
dw = 1./size[0]
dh = 1./size[1]
x = (box[0] + box[1])/2.0
y = (box[2] + box[3])/2.0
w = box[1] - box[0]
h = box[3] - box[2]
x = x*dw
w = w*dw
y = y*dh
h = h*dh
return (x,y,w,h)
def convert_annotation(year, image_id):
in_file = open('Annotations/%s.xml'%(image_id), encoding='UTF-8')
out_file = open('labels/%s.txt'%(image_id), 'w', encoding='UTF-8')
#从xml文件中获取图片标注的宽与高
tree=ET.parse(in_file)
root = tree.getroot()
size = root.find('size') #size结点
w = int(size.find('width').text) #宽和高
h = int(size.find('height').text)
for obj in root.iter('object'): #一张图片可能有多个类别标签,遍历
difficult = obj.find('difficult').text
cls = obj.find('name').text
#过滤掉难以识别和不在训练类别范围内的物体
if cls not in classes or int(difficult) == 1:
continue
cls_id = classes.index(cls)
xmlbox = obj.find('bndbox')
#获取目标框的四角坐标点
b = (float(xmlbox.find('xmin').text), float(xmlbox.find('xmax').text), float(xmlbox.find('ymin').text), float(xmlbox.find('ymax').text))
#由目标框的四角坐标的生成我们想要的目标框标签
bb = convert((w,h), b)
#按一定格式输出新的目标狂暴标签
#out_file.write(str(cls_id) + " " + " ".join([str(a) for a in bb]) + '\n')
更多参考:
https://blog.csdn.net/qq_34806812/article/details/81673798
https://blog.csdn.net/shuiyixin/article/details/82623613
https://blog.csdn.net/shuiyixin/article/details/82915105
网络结构
论文中的结构图:
论文中的结构图有误,第二个特征图深度应该是64,第三个特征图深度是192,第三个特征图以后的深度就没问题了。
pytroch 源码 中的模型结构:
YOLO(
(features): DarkNet(
(net): Sequential(
(0): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3))
(1): LeakyReLU(negative_slope=0.1, inplace)
(2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(3): Conv2d(64, 192, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(4): LeakyReLU(negative_slope=0.1, inplace)
(5): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(6): Conv2d(192, 128, kernel_size=(1, 1), stride=(1, 1))
(7): LeakyReLU(negative_slope=0.1, inplace)
(8): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(9): LeakyReLU(negative_slope=0.1, inplace)
(10): Conv2d(256, 256, kernel_size=(1, 1), stride=(1, 1))
(11): LeakyReLU(negative_slope=0.1, inplace)
(12): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(13): LeakyReLU(negative_slope=0.1, inplace)
(14): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(15): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1))
(16): LeakyReLU(negative_slope=0.1, inplace)
(17): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(18): LeakyReLU(negative_slope=0.1, inplace)
(19): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1))
(20): LeakyReLU(negative_slope=0.1, inplace)
(21): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(22): LeakyReLU(negative_slope=0.1, inplace)
(23): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1))
(24): LeakyReLU(negative_slope=0.1, inplace)
(25): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(26): LeakyReLU(negative_slope=0.1, inplace)
(27): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1))
(28): LeakyReLU(negative_slope=0.1, inplace)
(29): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(30): LeakyReLU(negative_slope=0.1, inplace)
(31): Conv2d(512, 512, kernel_size=(1, 1), stride=(1, 1))
(32): LeakyReLU(negative_slope=0.1, inplace)
(33): Conv2d(512, 1024, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(34): LeakyReLU(negative_slope=0.1, inplace)
(35): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(36): Conv2d(1024, 512, kernel_size=(1, 1), stride=(1, 1))
(37): LeakyReLU(negative_slope=0.1, inplace)
(38): Conv2d(512, 1024, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(39): LeakyReLU(negative_slope=0.1, inplace)
(40): Conv2d(1024, 512, kernel_size=(1, 1), stride=(1, 1))
(41): LeakyReLU(negative_slope=0.1, inplace)
(42): Conv2d(512, 1024, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(43): LeakyReLU(negative_slope=0.1, inplace)
)
)
(yolo): Sequential(
(0): Conv2d(1024, 1024, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): LeakyReLU(negative_slope=0.1)
(2): Conv2d(1024, 1024, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1))
(3): LeakyReLU(negative_slope=0.1)
(4): Conv2d(1024, 1024, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(5): LeakyReLU(negative_slope=0.1)
(6): Conv2d(1024, 1024, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(7): LeakyReLU(negative_slope=0.1)
)
(flatten): Flatten()
(fc1): Linear(in_features=81920, out_features=4096, bias=True)
(dropout): Dropout(p=0.5)
(fc2): Linear(in_features=4096, out_features=539, bias=True)
)
模型的最后一层使用线性激活函数,而所有其它的层使用下面的leaky rectified activation:
目标函数
其中 1 i o b j 1_i^{obj} 1iobj 表示目标是否出现在网格单元i中, 1 i j o b j 1_{ij}^{obj} 1ijobj 表示单元格i中的第j个边界框预测器“负责”该预测。注意,如果目标存在于该网格单元中(前面讨论的条件类别概率),则损失函数仅惩罚分类错误。如果预测器“负责”实际边界框(即该网格单元中具有最高IOU的预测器),则它也仅惩罚边界框坐标错误。
这里的出现/负责意味着值取 1 , 否则取0; 1 i j n o o b j 1_{ij}^{noobj} 1ijnoobj 则相反,出现取0,不出现取1。
一个 bounding box(边界框预测器) 对object负责,其实就是这个网格非极大值抑制(NMS)后剩下的最好的那个bounding box,详见原论文:
YOLO为每个网格单元预测多个边界框。在训练时,每个目标我们只需要一个边界框预测器来负责。若某预测器的预测值与目标的实际值的IOU值最高,则这个预测器被指定为“负责”预测该目标。这导致边界框预测器的专业化。每个预测器可以更好地预测特定大小,方向角,或目标的类别,从而改善整体召回率。
补充解释一下:
- LOSS函数建模目的可以整体理解为:如果预测框内包含物体(即真实框的中心落在了预测框所在的网格中),那么就通过梯度使预测框的(x,y,h,w)向真实框趋近,且让预测框的confidence向IOU(预测框,真实框)趋近,让类别概率分布向真实概率分布趋近。如果预测框不包含物体,那么就只需要让confidence向0趋近。
- confidence 受预测框的(x,y,h,w)影响,为何还在LOSS函数中增加关于confidence的惩罚项?原因是YOLO网络最终输出的张量内的confidence值,并非是由输出的预测框的(x,y,h,w)计算而来,而是在梯度下降过程中,网络不断尝试拟合 c o n f i d e n c e = P r ( O b j e c t ) ∗ I O U p r e d t r u t h confidence=Pr(Object)\ast IOU_{pred}^{truth} confidence=Pr(Object)∗IOUpredtruth 得到一系列权重参数,通过图片特征与这些权重参数进行运算得到。在Loss函数中增加关于confidence的惩罚项的目的,就是希望网络能够学会在不确切知道真是框的位置时也能够计算预测框的置信度。
其他细节:
我们对模型输出的平方和误差进行优化。我们选择使用平方和误差,是因为它易于优化,但是它并不完全符合最大化平均精度(average precision)的目标。它给分类误差与定位误差的权重是一样的,这点可能并不理想。另外,每个图像都有很多网格单元并没有包含任何目标,这将这些单元格的“置信度”分数推向零,通常压制了包含目标的单元格的梯度。这可能导致模型不稳定,从而导致训练在早期就发散。为了弥补平方和误差的缺陷,我们增加了边界框坐标预测的损失,并减少了不包含目标的框的置信度预测的损失。 我们使用两个参数λcoord和λnoobj来实现这一点。 我们设定λcoord= 5和λnoobj= .5。
使用平方根的原因是,平方根的特点是数值越小导数越大,数值越大,导数越小,所以误差分析这,使用平方根说明当w和h很大时所带来的惩罚比w和h很小时带来的惩罚小。
平方和误差对大框和小框的误差权衡是一样的,而我们的错误指标应该要体现出,大框的小偏差的重要性不如小框的小偏差的重要性。为了部分解决这个问题,我们直接预测边界框宽度和高度的平方根,而不是宽度和高度
参考:
https://blog.csdn.net/c20081052/article/details/80236015
https://www.zhihu.com/tardis/sogou/art/35416826
最终输出
原论文最终输出的是7*7*30的张量,7*7表示的,一开始将图片分成了7*7个网格。最终是每一个网格内还有30个数值标量,这30个数值标量分别是两个bounding box的(x,y,w,h,confidence),和20个类别各自出现的概率(p1,…,p20)
所以假设分成S*S的网格,每个网格B个bounding box,需要识别C个类别,则最后输出的形状应该是(S,S,5B+C)
通过对最终输出的张量切片可以知道每一个网格的bounding box位置、大小、是否存在物体、属于各个类别的概率。
预测时判断某一个网格内物体属于某类应该用类别条件概率乘存在置信度作为判断类别的标准:
参考:https://www.jianshu.com/p/13ec2aa50c12