论文:You Only Look Once(CVPR 2016)
代码:https://github.com/JunshengFu/vehicle-detection
模型结构
YOLOv1的网络模型结构受 GoogLeNet 启发,采用了 1x1 和 3x3 的序列组合替代 Inception 模块,总共 24 个卷积层加上 2 个全连接层。
算法
YOLO 利用整张图作为网络的输入,直接在输出层回归 bounding box(边界框) 位置及其所属的类别:输入图像划分成S*S的格子,每个格子都预测C个类别概率和B个Bounding Box,如果目标的中心落在某个格子中,该格子就负责预测该目标,虽然目标可能覆盖多个格子。每个格子只能检测出一个目标(B个Bounding Box中选一个最好的,即与实际框IoU最大的,这也是YOLO对密集的对象检测效果不好的原因),每个Bounding Box都包含5个预测值: x , y , w , h x,y,w,h x,y,w,h和 c o n f i d e n c e confidence confidence
输出
class VGG(nn.Module):
def __init__(self, features, num_classes=1000):
super(VGG, self).__init__()
self.features = features
self.classifier = nn.Sequential(
nn.Linear(512 * 7 * 7, 4096),
nn.ReLU(True),
nn.Dropout(),
nn.Linear(4096, 4096),
nn.ReLU(True),
nn.Dropout(),
nn.Linear(4096, num_classes),
)
self._initialize_weights()
def forward(self, x):
x = self.features(x)
x = x.view(x.size(0), -1)
x = self.classifier(x)
# 从vgg16得到输出,经过sigmoid 归一化到0-1之间
x = F.sigmoid(x)
# 再改变形状,返回(xxx,7,7,30) xxx代表几张照片,(7,7,30)代表一张照片的信息
x = x.view(-1, 7, 7, 30)
return x
输出张量:
S
∗
S
∗
(
C
+
B
∗
(
4
+
1
)
)
S*S*(C+B*(4+1))
S∗S∗(C+B∗(4+1))
S
∗
S
S*S
S∗S为网格数,
C
C
C为对象类别数,
B
B
B为每个网格预测的Bounding box数量,
(
4
+
1
)
(4+1)
(4+1)每个框的4个位置信息+置信度
置信度公式为:
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)*IOU^{truth}_{pred}
Confidence=Pr(Object)∗IOUpredtruth
P
r
(
O
b
j
e
c
t
)
Pr(Object)
Pr(Object)表示含目标的概率,
I
O
U
p
r
e
d
t
r
u
t
h
IOU^{truth}_{pred}
IOUpredtruth由预测的Bounding Box与对象真实Bounding Box计算得到(文中参数为:
7
∗
7
∗
(
20
+
2
∗
(
4
+
1
)
)
7*7*(20+2*(4+1))
7∗7∗(20+2∗(4+1)))
损失函数
求Loss首先要了解Loss有哪些部分构成,其实就是分两部分:
1、实际有目标的格子:置信度、坐标、宽高、类别损失
2、实际没有目标的格子:置信度损失
L
o
s
s
=
λ
coord
∑
i
=
0
S
2
∑
j
=
0
B
1
i
j
obj
[
(
x
i
−
x
^
i
)
2
+
(
y
i
−
y
^
i
)
2
]
中心坐标误差
+
λ
coord
∑
i
=
0
S
2
∑
j
=
0
B
1
i
j
obj
[
(
w
i
−
w
^
i
)
2
+
(
h
i
−
h
^
i
)
2
]
边框宽高误差
+
∑
i
=
0
S
2
∑
j
=
0
B
1
i
j
obj
(
C
i
−
C
^
i
)
2
置信度误差( 框内含对象)
+
λ
noobj
∑
i
=
0
S
2
∑
j
=
0
B
1
i
j
noobj
(
C
i
−
C
^
i
)
2
置信度误差( 框内无对象)
+
∑
i
=
0
S
2
1
i
obj
∑
c
∈
classes
(
p
i
(
c
)
−
p
^
i
(
c
)
)
2
分类误差
\begin{aligned} {\mathcal{\Large{Loss}}} &= \lambda_{\text {coord }} \sum_{i=0}^{S^{2}} \sum_{j=0}^{B} \mathbb{1}_{i j}^{\text {obj }}\left[\left(x_{i}-\hat{x}_{i}\right)^{2}+\left(y_{i}-\hat{y}_{i}\right)^{2}\right] \qquad\qquad\quad\quad\ \ \color{blue}\text{中心坐标误差}\\ &+\lambda_{\text {coord }} \sum_{i=0}^{S^{2}} \sum_{j=0}^{B} \mathbb{1}_{i j}^{\text {obj }}\left[(\sqrt{w_{i}}-\sqrt{\hat{w}_{i}})^{2}+(\sqrt{h_{i}}-\sqrt{\hat{h}_{i}})^{2}\right] \qquad\color{blue}\text{边框宽高误差}\\ &+\sum_{i=0}^{S^{2}} \sum_{j=0}^{B} \mathbb{1}_{i j}^{\text {obj }}\left(C_{i}-\hat{C}_{i}\right)^{2}\qquad\qquad\qquad\qquad\qquad\qquad\qquad\ \ \color{blue}\text{置信度误差( 框内含对象)} \\ &+\lambda_{\text {noobj }} \sum_{i=0}^{S^{2}} \sum_{j=0}^{B} \mathbb{1}_{i j}^{\text {noobj }}\left(C_{i}-\hat{C}_{i}\right)^{2} \qquad\qquad\qquad\qquad\quad\qquad\color{blue}\text{置信度误差( 框内无对象)}\\ &+\sum_{i=0}^{S^{2}} \mathbb{1}_{i}^{\text {obj }} \sum_{c \in \text { classes }}\left(p_{i}(c)-\hat{p}_{i}(c)\right)^{2}\qquad\qquad\qquad\qquad\qquad\quad\color{blue}\text{分类误差} \end{aligned}
Loss=λcoord i=0∑S2j=0∑B1ijobj [(xi−x^i)2+(yi−y^i)2] 中心坐标误差+λcoord i=0∑S2j=0∑B1ijobj [(wi−w^i)2+(hi−h^i)2]边框宽高误差+i=0∑S2j=0∑B1ijobj (Ci−C^i)2 置信度误差( 框内含对象)+λnoobj i=0∑S2j=0∑B1ijnoobj (Ci−C^i)2置信度误差( 框内无对象)+i=0∑S21iobj c∈ classes ∑(pi(c)−p^i(c))2分类误差
- 1 i o b j \color{blue}1_i^{obj} 1iobj:网格 i i i 中存在对象。
- 1 i j o b j \color{blue}1_{ij}^{obj} 1ijobj:网格 i i i 的第 j j j 个Bounding Box中存在对象
- 1 i j n o o b j \color{blue}1_{ij}^{noobj} 1ijnoobj:网格 i i i 的第 j j j 个Bounding Box中不存在对象
-
λ
c
o
o
r
d
\color{blue}\lambda_{coord}
λcoord和
λ
n
o
o
b
j
\color{blue}\lambda_{noobj}
λnoobj分别是各个损失的权重,文中前者取5,后者取0.5
边框宽高Loss项中对宽和高取平方根是为了降低Loss对大目标的敏感度,如果直接用差值作为Loss,那么大目标的预测框就算很准,其Loss也可能比预测效果很差的小目标的Loss大
具体如何求损失?
1、生成标签
根据预测和Loss将原标签变换成相应格式,比如需要中心位置、置信度(原标签给定的只有信息有:目标框坐标及其类别 )
2、生成标签代码
def encoder(self, boxes, labels):
'''
boxes (tensor) [[x1,y1,x2,y2],[x1,y1,x2,y2],[]] 坐标已被缩放到 [0-1] 之间
labels (tensor) [...]
return 7x7x30
'''
target = torch.zeros((7, 7, 30))
cell_size = 1. / 7
# boxes[:, 2:]代表 2: 代表xmax,ymax
# boxes[:, :2]代表 :2 代表xmin,ymin
# wh代表 bbox的宽(xmax-xmin)和高(ymax-ymin)
wh = boxes[:, 2:] - boxes[:, :2]
# bbox的中心点坐标
cxcy = (boxes[:, 2:] + boxes[:, :2]) / 2
# cxcy.size()[0]代表 一张图像的物体总数
# 遍历一张图像的物体总数
for i in range(cxcy.size()[0]):
# 拿到第i行数据,即第i个bbox的中心点坐标(相对于整张图,取值在0-1之间)
cxcy_sample = cxcy[i]
# ceil返回数字的上入整数
# cxcy_sample为一个物体的中心点坐标,求该坐标位于7x7网格的哪个网格
# cxcy_sample坐标在0-1之间 现在求它再0-7之间的值,故乘以7
# ij长度为2,代表7x7框的某一个框 负责预测一个物体
ij = (cxcy_sample / cell_size).ceil() - 1
# 每行的第4和第9的值设置为1,即每个网格提供的两个真实候选框 框住物体的概率是1.
# xml中坐标理解:原图像左上角为原点,右边为x轴,下边为y轴。
# 而二维矩阵(x,y) x代表第几行,y代表第几列
# 假设ij为(1,2) 代表x轴方向长度为1,y轴方向长度为2
# 二维矩阵取(2,1) 从0开始,代表第2行,第1列的值
# 画一下图就明白了
target[int(ij[1]), int(ij[0]), 4] = 1
target[int(ij[1]), int(ij[0]), 9] = 1
# 加9是因为前0-9为两个真实候选款的值。后10-20为20分类 将对应分类标为1
target[int(ij[1]), int(ij[0]), int(labels[i]) + 9] = 1
# 匹配到的网格的左上角的坐标(取值在0-1之间)(原作者)
# 根据二维矩阵的性质,从上到下 从左到右
xy = ij * cell_size
# cxcy_sample:第i个bbox的中心点坐标 xy:匹配到的网格的左上角相对坐标
# delta_xy:真实框的中心点坐标相对于 位于该中心点所在网格的左上角 的相对坐标,此时可以将网格的左上角看做原点,你这点相对于原点的位置。取值在0-1,但是比1/7小
delta_xy = (cxcy_sample - xy) / cell_size
# x,y代表了检测框中心相对于网格边框的坐标。w,h的取值相对于整幅图像的尺寸
# 写入一个网格对应两个框的x,y, wh:bbox的宽(xmax-xmin)和高(ymax-ymin)(取值在0-1之间)
target[int(ij[1]), int(ij[0]), 2:4] = wh[i]
target[int(ij[1]), int(ij[0]), :2] = delta_xy
target[int(ij[1]), int(ij[0]), 7:9] = wh[i]
target[int(ij[1]), int(ij[0]), 5:7] = delta_xy
return target
3、根据预测和标签选择格子、确定对应的标签
注意这个时候标签并没有定下来,因为并不是所有预测框都是有效的,根据选择的框调整标签,如下图:预测的Bounding box1 效果比较好,就把标签对应置信区间位置处设置为1(标签当然百分百确定这里有目标)
3、计算各部分Loss,并分配适当权重
4、Loss代码详解
class yoloLoss(nn.Module):
'''
torch.nn.Modules相当于是对网络某种层的封装,包括网络结构以及网络参数,和其他有用的操作如输出参数
继承Modules类,需实现__init__()方法,以及forward()方法
'''
def __init__(self,S,B,l_coord,l_noobj):
super(yoloLoss,self).__init__()
self.S = S # 将图像分为SxS的网格
self.B = B # 一个网格预测B个框
self.l_coord = l_coord # 坐标损失权重
self.l_noobj = l_noobj # 无目标处置信度损失权重
def compute_iou(self, box1, box2):
“”“
这里要实现计算IoU的函数
”“”
def forward(self,pred_tensor,target_tensor):
'''
pred_tensor: (tensor) size(batchsize,S,S,Bx5+20=30) [x,y,w,h,c]
target_tensor: (tensor) size(batchsize,S,S,30)
'''
##################从标签中分别得到实际存在和实际不存在目标的掩码###############
N = pred_tensor.size()[0] # N为batchsize
# 坐标mask 4:是物体或者背景的confidence
coo_mask = target_tensor[:,:,:,4] > 0 # 标记有目标的位置
# 没有物体mask
noo_mask = target_tensor[:,:,:,4] == 0 # 标记无有目标的位置
# unsqueeze(-1) 扩展最后一维,用0填充,使得形状与target_tensor一样
# coo_mask、noo_mask形状扩充到[32,7,7,30]
# coo_mask 大部分为0 记录为1代表真实有物体的网格
# noo_mask 大部分为1 记录为1代表真实无物体的网格
coo_mask = coo_mask.unsqueeze(-1).expand_as(target_tensor)
noo_mask = noo_mask.unsqueeze(-1).expand_as(target_tensor)
###########对于实际存在的目标,需要计算置信度、坐标、宽高、类别损失##########
# coo_pred 取出预测结果中有物体的网格,并改变形状为(xxx,30) xxx代表一个batch的图片上的存在物体的网格总数 30代表2*5+20 例如:coo_pred[72,30]
coo_pred = pred_tensor[coo_mask].view(-1,30)
# 一个网格预测的两个box 30的前10即为2个x,y,w,h,c,并调整为(xxx,5) xxx为所有真实存在物体的预测框,而非所有真实存在物体的网格 例如:box_pred[144,5]
# contiguous将不连续的数组调整为连续的数组
box_pred = coo_pred[:,:10].contiguous().view(-1,5) # box[x1,y1,w1,h1,c1]
# [x2,y2,w2,h2,c2]
# 每个网格预测的类别后20
class_pred = coo_pred[:,10:]
# 对真实标签做同样操作
coo_target = target_tensor[coo_mask].view(-1,30) # 提取有目标格子及对应属性:位置、置信度、类别
box_target = coo_target[:,:10].contiguous().view(-1,5) # 提取位置、置信度
class_target = coo_target[:,10:] # 类别
########对于实际不存在的目标,预测有计算置信度损失################
# 在预测结果中拿到真实无物体的网格,并改变形状为(xxx,30) xxx代表一个batch的图片上的不存在物体的网格总数 30代表2*5+20 例如:[1496,30]
noo_pred = pred_tensor[noo_mask].view(-1,30)
noo_target = target_tensor[noo_mask].view(-1,30) # 例如:[1496,30]
# ByteTensor:8-bit integer (unsigned)
noo_pred_mask = torch.cuda.ByteTensor(noo_pred.size()) # 例如:[1496,30]
noo_pred_mask.zero_() #初始化全为0
# 预测的置信度掩码(实际无目标格子处)
# 将第4、9 即有物体的confidence位置置为1
noo_pred_mask[:,4]=1;noo_pred_mask[:,9]=1
# 拿到第4列和第9列里面的值(即拿到真实无物体的网格中,网络预测这些网格有物体的概率值) 一行有两个值(第4和第9位) 例如noo_pred_c:2992 noo_target_c:2992
# 拿到预测置信度(实际无目标格子处)
# noo pred只需要计算类别c的损失
noo_pred_c = noo_pred[noo_pred_mask]
# 拿到标签实际无目标格子处置信度(标签当然已经知道有该处有目标概率为0)
noo_target_c = noo_target[noo_pred_mask]
######## 实际没有目标的地方要计算置信度均方误差,本来没有你说有:Loos1 ########
# reduce = False,返回向量形式的 loss reduce = True, 返回标量形式的los
# size_average=True 返回 loss.mean();size_average=False 返回 loss.sum()
nooobj_loss = F.mse_loss(noo_pred_c,noo_target_c,size_average=False)
#计算包含obj损失 即本来有,预测有 和 本来有,预测无
coo_response_mask = torch.cuda.ByteTensor(box_target.size())
coo_response_mask.zero_()
coo_not_response_mask = torch.cuda.ByteTensor(box_target.size())
coo_not_response_mask.zero_()
# 选择与目标框IOU最大的那个
for i in range(0,box_target.size()[0],2):
# 转换一下框格式以计算IoU【c_x,c_y,w,h】-->【x_1,y_1,x_2,y_2】
box1 = box_pred[i:i+2]
box1_xyxy = Variable(torch.FloatTensor(box1.size()))
box1_xyxy[:,:2] = box1[:,:2] -0.5*box1[:,2:4]
box1_xyxy[:,2:4] = box1[:,:2] +0.5*box1[:,2:4]
box2 = box_target[i].view(-1,5)
box2_xyxy = Variable(torch.FloatTensor(box2.size()))
box2_xyxy[:,:2] = box2[:,:2] -0.5*box2[:,2:4]
box2_xyxy[:,2:4] = box2[:,:2] +0.5*box2[:,2:4]
iou = self.compute_iou(box1_xyxy[:,:4],box2_xyxy[:,:4]) #[2,1]预测与标签框IoU
max_iou,max_index = iou.max(0) # 得到最大IoU及其索引
max_index = max_index.data.cuda()
coo_response_mask[i+max_index] = 1 # 确定哪一个框为有效预测框
coo_not_response_mask[i+1-max_index] = 1
# 1.response loss响应损失,即本来有,预测有 有相应 坐标预测的loss (x,y,w开方,h开方)
# box_pred [144,5] coo_response_mask[144,5] box_pred_response:[72,5]
# 选择IOU最好的box来进行调整,负责检测出某物体
box_pred_response = box_pred[coo_response_mask].view(-1,5)
# 将标签中的框与有效预测框对应
box_target_response = box_target[coo_response_mask].view(-1,5)
######## 实际有目标的地方要计算置信度均方误差,本来有你说好像没:Loss2 ########
# box_pred_response:[72,5] 计算预测 有物体的概率误差,返回一个数
contain_loss = F.mse_loss(box_pred_response[:,4],box_target_response[:,4],size_average=False)
######## 实际有目标的地方要计算中心坐标和宽高误差,预测肯定有偏差:Loos3、Loss4 ########
# 计算(x,y,w开方,h开方)
loc_loss = F.mse_loss(box_pred_response[:,:2],box_target_response[:,:2],size_average=False) + F.mse_loss(torch.sqrt(box_pred_response[:,2:4]),torch.sqrt(box_target_response[:,2:4]),size_average=False)
######## 实际有目标的地方要计算类别均方误差:Loss5########
class_loss = F.mse_loss(class_pred,class_target,size_average=False)
# 除以N 即平均一张图的总损失
return (self.l_coord*loc_loss + contain_loss + self.l_noobj*nooobj_loss + class_loss)/N
训练
因为卷积层最后接了两个全连接层,输入图片要求缩放到 448 × 448 448\times448 448×448
先取 YOLO 模型前面的 20 个卷积加上 1 平均池化层和全连接层在 ImageNet 上预训练,目的是为了获取目标的特征表达能力,输入图片大小为224*224,然后使用完整的网络(全连接层处使用了dropout),在PASCAL VOC数据集上进行对象识别和定位的训练和预测,输入图片的分辨率调到 448 × 448 448\times448 448×448
预测(inference)
网络得到每张图片的pred为 1x7x7x30 ,需要的对其进行解码操作得到正常的预测框,然后挑选出一些较好的框作为最终预测结果。
解码代码
def decoder(pred):
'''
解码
pred (tensor) 1x7x7x30
return (tensor) box[[x1,y1,x2,y2]] label[...]
'''
boxes = []
cls_indexs = []
probs = []
cell_size = 1./7
pred = pred.data
pred = pred.squeeze(0) # 7x7x30
contain1 = pred[:, :, 4].unsqueeze(2)
contain2 = pred[:, :, 9].unsqueeze(2)
contain = torch.cat((contain1, contain2), 2)
mask1 = contain > 0.9 # 大于阈值
mask2 = (contain == contain.max()) # we always select the best contain_prob what ever it>0.9
mask = (mask1+mask2).gt(0)
min_score, min_index = torch.min(mask, 2) # 每个cell只选最大概率的那个预测框
for i in range(7):
for j in range(7):
for b in range(2):
index = min_index[i, j]
mask[i, j, index] = 0
if mask[i, j, b] == 1:
# print(i,j,b)
box = pred[i, j, b*5:b*5+4]
contain_prob = torch.FloatTensor([pred[i, j, b*5+4]])
xy = torch.FloatTensor([j, i])*cell_size # cell左上角 up left of cell
box[:2] = box[:2]*cell_size + xy # return cxcy relative to image
box_xy = torch.FloatTensor(box.size()) # 转换成xy形式 convert[cx,cy,w,h] to [x1,xy1,x2,y2]
box_xy[:2] = box[:2] - 0.5*box[2:]
box_xy[2:] = box[:2] + 0.5*box[2:]
max_prob, cls_index = torch.max(pred[i, j, 10:], 0)
boxes.append(box_xy.view(1, 4))
cls_indexs.append(cls_index)
probs.append(contain_prob)
boxes = torch.cat(boxes, 0) # (n,4)
probs = torch.cat(probs, 0) # (n,)
# cls_indexs = torch.cat(cls_indexs, 0) # (n,)
cls_indexs = torch.stack(cls_indexs, dim=0)
keep = nms(boxes, probs)
return boxes[keep], cls_indexs[keep], probs[keep]
NMS算法步骤如下:
1、设置一个Score的阈值,低于该阈值的候选对象排除掉(将该Score设为0)
2、遍历每一个对象类别
1)遍历该对象的98个得分
2)找到Score最大的那个对象及其bounding box,添加到输出列表
3)对每个Score不为0的候选对象,计算其与最大Score输出对象的bounding box的IOU
4)根据预先设置的IOU阈值,排除高于该阈值的候选对象
5)遍历完该对象类别所有的box,返回步骤2处理下一类对象
3、输出列表即为预测的对象
补充
激活函数采用的Leaky ReLU
f
(
x
)
=
{
x
,
i
f
x
>
0
0.1
x
,
o
t
h
e
r
w
i
s
e
\begin{aligned} f(x)=\left\{ \begin{aligned} &x,\qquad if x>0\\ &0.1x,\ \ \ otherwise \end{aligned}\right. \end{aligned}
f(x)={x,ifx>00.1x, otherwise
YOLO优点:
- 速度快
- 泛化能力强
- 基于图像的全局信息预测,误检(将背景检测为物体)率低
缺点:
- 定位不准确
- 与基于region proposal的方法相比召回率较低
YOLO的bounding box和Faster RCNN的Anchor不一样,并不是预设好的框,只是对一个对象预测出2个bounding box,挑选出预测得相对比较准(与实际框IoU大的那个)
Anchor有点像先给一个基准,然后让网络修正,而YOLO则是直接预测一个框,一开始就是瞎预测,但每次都选一个比较好的(根据与实际Bounding Box的IoU取更好的那个),然后慢慢表现越来越好
参考文献
【1】YOLO(You Only Look Once)算法详解
【2】YOLO v1深入理解
【3】Yolo三部曲解读——Yolov1
【4】YOLO slices
【5】GoogLeNet网络结构学习
【6】死磕YOLO系列,YOLOv1 的大脑、躯干和手脚