太垃圾的CSDN了,没有自动保存的功能,下次要转移了。今天是还在找工作的第不知道多少天,现在除了给自己打鸡血,和不断准备好像不知道能做什么了?可我的心里还是满满的希望啊~ 学习不易,转发注明链接
整个YOLO系列,最关键的地方就是损失函数,各大文章如何解刨YOLO,最重要还是复现文章,而复现YOLO关键还是能将YOLO的损失函数进行复现,尽管YOLO历经了1到3的变换,但是损失函数的变化并不是很大,这里我们突出分析YOLO2,首先他是第一个引入了Anchor,其次是他还是保留大量YOLOv1的损失函数的计算,承上启下吧。因为对C++不太熟悉,所以就拿了优图实验室的代码来分析。
个人将优图的代码分为四个部分吧,这是一个训练的过程:
1.Backbone出来的特征提取,将特征分别提取,掌握这部分可以知道YOLO backbone的最终feature map输出的大小
2.将预测相对值,转换为预测框,为后面的IOU计算铺垫
3.根据预测框和标注框,确定什么是背景,什么检测物体的anchor
4.损失计算
这是一个预测的过程:
1.那么输出的是1313(20+4+1)*5 然后先根据置信度筛选掉一批,然后根据NMS在筛选,然后就通过xywh得出目标框同时给出类别。 预测的过程比较简单,就是根据置信度的输出,然后在根据剩下的进行非极大值抑制就可以。
1.特征提取转化为预测值
# Get x,y,w,h,conf,cls
output = output.view(nB, nA, -1, nH*nW) #torch.Size([10, 5, 256, 169])
coord = torch.zeros_like(output[:, :, :4]) #只取了前面四个的参数分别是box的位置信息
coord[:, :, :2] = output[:, :, :2].sigmoid() # tx,ty
coord[:, :, 2:4] = output[:, :, 2:4] # tw,th
conf = output[:, :, 4].sigmoid()
上面的工作就是提取了我们的预测向量,第一项是提取前面四个数值的位置信息,第二项是提取他们的中心信息,第三项是提取他们的高和宽的信息。最后一项是置信度,可以发现这里面用了两次的sigmoid激活函数。分别的作用如下:
1.xy_offset存储的是中心坐标相对于cell左上角的x坐标和y坐标的偏置,采用sigmod是为了将偏置控制在0和1之间。
2.wh_offect存储的是预测的width和height,需要用指数函数解码
3.obj_probs存储的是置信度,采用sigmod控制在0和1之间
中心相对位置用sigmoid 操作一波的原因如下,以及如果想知道为什么BCE和sigmoid可以联合来解决回归的问题理由点击右边的链接:点我点我点我。
用来提取output的全部预测框的类别:
if nC > 1:
cls = output[:, :, 5:].contiguous().view(nB*nA, nC, nH*nW).transpose(1, 2).contiguous().view(-1, nC)
cls = output[:, :, 5:] 先取得每个anchor的 后20位,20位是类别来的
输出的结果是torch.Size([10, 5, 20, 169])
= output[:, :, 5:].contiguous().view(nB * nA, nC, nH * nW)
输出的结果是:torch.Size([10, 20, 169]) 将这些数值进行处理,
接着 输出的结果是:torch.Size([10, 169,20])#最后是torch.Size([1690, 20]) 前面就是每张照片的全部预测框,然后对应的20个类别。
基本上通过上面我们知道几个事情,首先是激活函数的不同的使用,其次是特征的数量比如13135*(5+20),25代表的是每个cell5个anchorboxes,每个anchorbox有xywhc5个预测的数值,然后就是20个类别。所以一共125。
2.将预测相对值,转换为预测框,为后面的IOU计算铺垫
基于于上面提取的预测向量,下面我们就加上我们之前设置好的anchor box的数值,最终我们就可以得到了一个完成的预测框了具体操作如下:
pred_boxes = torch.zeros(nB*nA*nH*nW, 4, dtype=torch.float, device=device)
lin_x = torch.linspace(0, nW-1, nW).to(device).repeat(nH, 1).view(nH*nW)
lin_y = torch.linspace(0, nH-1, nH).to(device).repeat(nW, 1).t().contiguous().view(nH*nW)
anchor_w = self.anchors[self.anchors_mask, 0].view(nA, 1).to(device)
anchor_h = self.anchors[self.anchors_mask, 1].view(nA, 1).to(device)
pred_boxes[:, 0] = (coord[:, :, 0].detach() + lin_x).view(-1)
pred_boxes[:, 1] = (coord[:, :, 1].detach() + lin_y).view(-1)
pred_boxes[:, 2] = (coord[:, :, 2].detach().exp() * anchor_w).view(-1)
pred_boxes[:, 3] = (coord[:, :, 3].detach().exp() * anchor_h).view(-1)
细节一:
pred_boxes = torch.zeros(nB*nA*nH*nW, 4, dtype=torch.float, device=device)
#输出的结果是,全部图的全部预测框torch.Size([8450, 4])
#如果是一张图的话,应该是torch.Size([845, 4]) 13*13*5
细节二:
pred_boxes[:, 2] = (coord[:, :, 2].detach().exp() * anchor_w).view(-1)
pred_boxes[:, 3] = (coord[:, :, 3].detach().exp() * anchor_h).view(-1)
我们看到上面的宽和高也是符合这个格式的。
其实通过这里我们可以看到了上面这个公式,里面txtytwth都是offset,也是我们预测的量,相对的量。所以需要结合anchor让他转化为预测框,算出来预测框下面我们就要进行IOU的计算,来确定哪些是背景哪些目标物体。
3.根据预测框和标注框,确定什么是背景,什么目标物体。
这一部分是整个损失函数甚至是YOLO的核心,通过这里我们知道他定义了什么,定义了哪些是背景哪些是目标物体,背景的话只计算置信度误差,目标物体的话,就计算他的全部量的误差。我们将下面的代码依然分成多个部分:
1.设置好变量(除了negative 的置信度外其他都是0)一开始认为大家都是背景
2.将GT的大小进行缩小,因为我们预测出来的数值是相对于featuremap的大小,而featuremap的大小本身就是相对原图缩小了一定的倍数,这个倍数其实就是我们downsample的数量。
3.计算预测框和标注框的IOU,这里的计算后会和一个阈值比较大于阈值negative的地方都设置为0,小于阈值的地方不变,还记得上面说的negative本身都是1,所以在这里设置了以后,剩下的就是算是背景了。所以背景是通过IOU的计算来确定的
4.接下来计算出负责检测目标的anchor,通过这里基本上就能确定哪个cell哪个anchor负责检测了,这里是用的计算时anchor和预测框里面最好的数值,其实相当于目标所在的cell,而且是cell里面IOU最大的。
5.将该标注为1的向量标注为1,该标注为0的地方标注为0,这里主要是区分好哪些是背景哪些目标框所对应的anchor。
6.注意的是我们找到了负责的anchor,我们也知道了标注框,然后我们需要计算出标注框所对应的相对值,所以我们需要对上面的公式进行逆过程,经过这里的输出,可以和我们最一开始特征层所对应的预测值直接对应起来。
上面走完我们就可以计算损失了
def __build_targets_brambox(self, pred_boxes, ground_truth, nH, nW):
""" Compare prediction boxes and ground truths, convert ground truths to network output tensors """
# Parameters
nB = len(ground_truth)
nA = self.num_anchors
nAnchors = nA * nH * nW
nPixels = nH * nW
device = pred_boxes.device
# Tensors
#
#要记住这里面第一个是图片的数量,因为我们是一个batch一个batch训练的所以有多张图
#第二个是anchor的数量
#第三个是13*13 feature的大小
conf_pos_mask = torch.zeros(nB, nA, nH * nW, requires_grad=False, device=device)
conf_neg_mask = torch.ones(nB, nA, nH * nW, requires_grad=False, device=device)
coord_mask = torch.zeros(nB, nA, 1, nH * nW, requires_grad=False, device=device)
cls_mask = torch.zeros(nB, nA, nH * nW, requires_grad=False, dtype=torch.uint8, device=device)
tcoord = torch.zeros(nB, nA, 4, nH * nW, requires_grad=False, device=device)
tconf = torch.zeros(nB, nA, nH * nW, requires_grad=False, device=device)
tcls = torch.zeros(nB, nA, nH * nW, requires_grad=False, device=device)
recall50 = 0
recall75 = 0
obj_all = 0
obj_cur = 0
iou_sum = 0
for b in range(nB):
if len(ground_truth[b]) == 0: # No gt for this image
continue
obj_all += len(ground_truth[b])
# Build up tensors
cur_pred_boxes = pred_boxes[b * nAnchors:(b + 1) * nAnchors]
if self.anchor_step == 4:
anchors = self.anchors.clone()
anchors[:, :2] = 0
else:
anchors = torch.cat([torch.zeros_like(self.anchors), self.anchors], 1)
gt = torch.zeros(len(ground_truth[b]), 4, device=device)
for i, anno in enumerate(ground_truth[b]): #这张图里面的gt
gt[i, 0] = (anno.x_top_left + anno.width / 2) / self.reduction
gt[i, 1] = (anno.y_top_left + anno.height / 2) / self.reduction
gt[i, 2] = anno.width / self.reduction
gt[i, 3] = anno.height / self.reduction
# Set confidence mask of matching detections to 0
# cur_pred_boxes里面存在(nA*nH*nW,4)这么多个box,格式为x_mid,y_mid,w,h
# 每个box都要和gt里的boxes分别计算IOU
iou_gt_pred = bbox_ious(gt, cur_pred_boxes) #通过IOU来确定谁是最佳
mask = (iou_gt_pred > self.thresh).sum(0) >= 1
# IOU大于边界线的我们则认为是可以用来预测的了
# 则它们的conf_neg应该为0,剩下的那些就是neg格子了。
conf_neg_mask[b][mask.view_as(conf_neg_mask[b])] = 0
# Find best anchor for each gt
#然后计算了GT和5个anchor中最接近的一个
gt_wh = gt.clone()
gt_wh[:, :2] = 0
iou_gt_anchors = bbox_ious(gt_wh, anchors)
_, best_anchors = iou_gt_anchors.max(1)
# Set masks and target values for each gt
# time consuming
# gi,gj 是中心的位置也就是anchor所在的cell是哪一个
# cur_n是cell的哪一个anchor
# 这样就确定了那个先验框可以代表这个真实的来做差
for i, anno in enumerate(ground_truth[b]): #这张图的gt
gi = min(nW - 1, max(0, int(gt[i, 0])))#限制坐标范围在[0,nW-1]
gj = min(nH - 1, max(0, int(gt[i, 1])))#限制[0,nH-1]
cur_n = best_anchors[i] #cur_n是一个anchors列表的索引值,明确gt有无最佳的anchor
if cur_n in self.anchors_mask:
best_n = self.anchors_mask.index(cur_n)
else:
continue
# 前情提要,我们在head网络里定义的nA一直都是len(anchors_mask[0])
# 也就是对训练批次,有效的anchor数量是anchors_mask规定的,而非anchors。
# 而nPixels=nH*nW。 所以用best_n来乘nPixels,就合情合理了。
# 而gt[i]里取出来的中心点坐标(gi,gj)对应第gj*nW+gi个格子,
# 取出gt[i]同这个格子里的预测box的IOU。
iou = iou_gt_pred[i][best_n * nPixels + gj * nW + gi] # 每一个gt,对应的最佳的anchor对应的预测框与标注框的IOU
# debug information
obj_cur += 1
recall50 += (iou > 0.5).item()
recall75 += (iou > 0.75).item()
iou_sum += iou.item()
#上面就是先找到符合的预测框们
#然后找到符合的anchor对应的预测框
#预测框所落到的位置
if anno.ignore:
conf_pos_mask[b][best_n][gj * nW + gi] = 0
conf_neg_mask[b][best_n][gj * nW + gi] = 0
else:
coord_mask[b][best_n][0][gj * nW + gi] = 2 - anno.width * anno.height / (
nW * nH * self.reduction * self.reduction)
cls_mask[b][best_n][gj * nW + gi] = 1 #第n个ground truth 落在了13*13的哪一个格子,对应的哪一个anchorbox也就是对应的predictbox,所以设置为1
#然后就是best_n是最合适的anchor对应的预测框
conf_pos_mask[b][best_n][gj * nW + gi] = 1
conf_neg_mask[b][best_n][gj * nW + gi] = 0
tcoord[b][best_n][0][gj * nW + gi] = gt[i, 0] - gi #GT的中心位置
tcoord[b][best_n][1][gj * nW + gi] = gt[i, 1] - gj #GT的中心位置
tcoord[b][best_n][2][gj * nW + gi] = math.log(gt[i, 2] / self.anchors[cur_n, 0]) #GT的高,反Log是因为我之前用e,我们等下做差是和最原始的预测值进行做的
tcoord[b][best_n][3][gj * nW + gi] = math.log(gt[i, 3] / self.anchors[cur_n, 1])
#GT的高,反Log是因为我之前用e,我们等下做差是和最原始的预测值进行做的
#因为我们在找到GT对应的预测框的过程中的过程,我们是通过预测框的转变的数值
#所以找到预测后以后我们就需要转变回去,还记得我们输入是pred_boxes,他是经过转变得到的
#我们用这个转变得到的找到一个gt对应的预测框,那么这个gt就是对应这个转变后的
#所以我们需转变回去,这就是为什么有log出现
tconf[b][best_n][gj * nW + gi] = 1
tcls[b][best_n][gj * nW + gi] = anno.class_id
# loss informaion
self.info['obj_cur'] = obj_cur
self.info['obj_all'] = obj_all
if obj_cur == 0:
obj_cur = 1
self.info['avg_iou'] = iou_sum / obj_cur
self.info['recall50'] = recall50 / obj_cur
self.info['recall75'] = recall75 / obj_cur
return coord_mask, conf_pos_mask, conf_neg_mask, cls_mask, tcoord, tconf, tcls
总的来说就是先通过预测框转换,然后转换的数值和gt进行计算IOU,这里可以找到第一波超过阈值的IOU,然后我们要从这一波超过阈值的预测框找到一个合适,这一波就是通过GT和anchor以及GT的中心坐标,然后我们就对应到了我们前面那一堆超过阈值的预测框里面,然后就可以确定那个预测框了。接着就是对应的位置的参数标上1 或者 0.表示是否参与接下来的运算。
---- 4.损失计算
这一部分就是最后的运算啦
coord_mask, conf_pos_mask, conf_neg_mask, cls_mask, tcoord, tconf, tcls = self.build_targets(pred_boxes, target, nH, nW)
# coord
coord_mask = coord_mask.expand_as(tcoord)[:,:,:2] # 0 = 1 = 2 = 3, only need first two element
coord_center, tcoord_center = coord[:,:,:2], tcoord[:,:,:2]
coord_wh, tcoord_wh = coord[:,:,2:], tcoord[:,:,2:]
if nC > 1:
tcls = tcls[cls_mask].view(-1).long()
cls_mask = cls_mask.view(-1, 1).repeat(1, nC)
cls = cls[cls_mask].view(-1, nC)
# criteria
self.bce = self.bce.to(device)
self.mse = self.mse.to(device)
self.smooth_l1 = self.smooth_l1.to(device)
self.ce = self.ce.to(device)
bce = self.bce
mse = self.mse
smooth_l1 = self.smooth_l1
ce = self.ce
# Compute losses
loss_coord_center = 2.0 * 1.0 * self.coord_scale * (coord_mask*bce(coord_center, tcoord_center)).sum()
loss_coord_wh = 2.0 * 1.5 * self.coord_scale * (coord_mask*smooth_l1(coord_wh, tcoord_wh)).sum()
self.loss_coord = loss_coord_center + loss_coord_wh
loss_conf_pos = 1.0 * self.object_scale * (conf_pos_mask * bce(conf, tconf)).sum()
loss_conf_neg = 1.0 * self.noobject_scale * (conf_neg_mask * bce(conf, tconf)).sum()
self.loss_conf = loss_conf_pos + loss_conf_neg
if nC > 1 and cls.numel() > 0:
self.loss_cls = self.class_scale * 1.0 * ce(cls, tcls)
cls_softmax = F.softmax(cls, 1)
t_ind = torch.unsqueeze(tcls, 1).expand_as(cls_softmax)
class_prob = torch.gather(cls_softmax, 1, t_ind)[:, 0]
else:
self.loss_cls = torch.tensor(0.0, device=device)
class_prob = torch.tensor(0.0, device=device)
上面的代码中我们需要学习的是:在计算中心点的位置误差还有置信置信读的误差,代码的实现都用了binary_cross_entropy,这是符合道理的还记得我们的激活函数吗?我们用的sigmoid 激活函数,用了这样的激活函数,那么损失函数只能用交叉商损失函数。为什么可以用BCE来做位置的回归,其实BCE的梯度就是一种回归的表达,如果将标注改成只有1和0那么就是分类,如果改成任意的数值那就是回归。所以人家叫做逻辑回归逻辑回归逻辑回归
还有就是WH用的smoothL1,这个回归基本上目前用的比L1 和 L2多,因为本身的鲁棒性更强。
基本上到这里我们就大概理清了yolov2的思路了,让模型不断学习,学会生成一些相对参数,这些相对参数能够很好的结合我们的先眼眶来靠近我们的真实框。YOLOV3同理哦~~~
**所以训练的时候,数据集会先转化到标准的格式,也就是上面的bxbybwbh的格式,然后通过上面的公式的逆公式给出了TxTyTwTh这样就是我我们的标签。接着我们的模型预测来的会和这四个数值进行对比。这就是回归的思想,记住数据集会先进行逆过程得到了标签相对值:TxTyTwTh,然后通过txtytwth进行回归接近我们的标签。
所以预测的时候,我们得到的是tx,ty,tw,th这四个东西通过上面的公式转化为了bxbybwbh。然后通过比例进行放大还原即可。训练的时候还要先计算IOU,来判断是背景和物体,所以需要将预测值进行第一次的预测,也就是通过上面的公式得到,IOU小于某个值的认为是背景,背景的话只会计算算置信度的误差,后者话会根据就是置信度,位置,大小和类别都算。预测的时候,先看置信度,排出一堆,然后用不同类别进行IOU的NMS得到最好的。**
这里会有点绕,因为我们在训练的时候,也会进行预测,因为需要计算IOU。所以需要做预测。
其实反思整个YOLO,很多技巧都是让模型更好的学习,指导模型更好的学习,然后就是限制模型的学习空间。包括为什么要限制中心坐标在1和0之间,为什么要引入anchor,为什么用BN层,为什么,为什么。。。。。