在前面学习YOLOV7的过程中,我们已经学习了其网络结构,然而实际上YOLOV7项目的难点并不在于其网络模型而是在于其损失函数的设计,即如何才能训练出来合适的bbox。 神经网络模型都有训练和测试(推理)过程,在YOLOV7的训练过程中,包含模型构造,标签分配和损失函数计算。其中模型在前面以及讲过了。在测试过程中包含加载模型,损失函数计算,输出值解码,非极大值抑制,MAP计算等,今天我们先讲一下YOLOV7的训练过程。 在训练过程中主要用到这几个文件:
YOLOV7训练过程中的一个重要思想便是正样本匹配策略,其更像是YOLOV5与YOLOX的一个结合体,那么接下来我们结合代码看一下其匹配策略:
YOLOV5,V7匹配策略
yolov5,v7与yolov3、yolov4最大的不同就是v3与v4一个gt只会与一个正样本匹配,而v5,v7一个 gt 可以被分配给多个anchor,还有可能被分配到三个不同的特征图中的两个甚至三个。
匹配策略:这里指的是不带辅助头,论文中,将负责最终输出的Head为lead Head,将用于辅助训练的Head称为auxiliary Head。本博客不重点讨论,原因是论文中后面的结构实验实现提升比较有限(0.3个点)
主要是参考了YOLOV5 和YOLOV6使用的当下比较火的simOTA.
S1.训练前,会基于训练集中gt框,通过k-means聚类算法,先验获得9个从小到大排列的anchor框。(可选)
S2.将每个gt与9个anchor匹配:Yolov5为分别计算它与9种anchor的宽与宽的比值(较大的宽除以较小的宽,比值大于1,下面的高同样操作)、高与高的比值,在宽比值、高比值这2个比值中,取最大的一个比值,若这个比值小于设定的比值阈值,这个anchor的预测框就被称为正样本。一个gt可能与几个anchor均能匹配上(此时最大9个)。所以一个gt可能在不同的网络层上做预测训练,大大增加了正样本的数量,当然也会出现gt与所有anchor都匹配不上的情况,这样gt就会被当成背景,不参与训练,说明anchor框尺寸设计的不好。
S3.扩充正样本。根据gt框的中心位置,将最近的2个邻域网格也作为预测网格,也即一个groundtruth框可以由3个网格来预测;可以发现粗略估计正样本数相比前yolo系列,增加了三倍(此时最大27个匹配)。图下图浅黄色区域,其中实线是YOLO的真实网格,虚线是将一个网格四等分,如这个例子中,GT的中心在右下虚线网格,则扩充右和下真实网格也作为正样本。
S4.获取与当前gt有top10最大iou的prediction结果。将这top10 (5-15之间均可,并不敏感)iou进行sum,就为当前gt的k。k最小取1。
S5.根据损失函数计算每个GT和候选anchor损失(前期会加大分类损失权重,后面减低分类损失权重,如1:5->1:3),并保留损失最小的前K个。
S6.去掉同一个anchor被分配到多个GT的情况。
正负样本分配
正负样本分配的函数build_targets(yolo_training.py),将其分为以下结构:
├── 数据准备
└── 遍历每个特征图
├── ①anchors和gt匹配,看哪些gt是当前特征图的正样本(find_3_positive)初筛
└── ②将当前特征图的正样本分配给对应的grid(完成复筛:iou,类别)
步骤1:anchors和gt匹配,看哪些gt是当前特征图的正样本**(find_3_positive)** 这里要做的是从gt的上下左右分别偏移0.5来获取周边的单元格来进行预测,通过计算anchor的长宽比例是否合适(比例位于1/4与4之间)则认为符合,那么当前gt就能与当前特征图匹配。 如图所示:这是lead head的正样本匹配策略 YOLOV7中引入了辅助头,其正样本为: 如图:训练时,lead head和aux head中正样本分配图示(蓝色点代表着gt所处的位置,实线组成的网格代表着特征图grid,虚线代表着一个grid分成了4个象限以进行正负样本分配。如果一个gt位于蓝点位置,那么在lead head中,黄色grid将成为正样本。在aux head中,黄色+橙色grid将成为正样本)
初筛(find_3_positive)
设置偏移方向与偏移大小
g = 0.5
off = torch.tensor([
[0, 0],
[1, 0], [0, 1], [-1, 0], [0, -1],
], device=targets.device).float() * g
在yolov5,v7中,会将一个特征点分为四个象限,针对步骤1中匹配的gt,会计算该gt(上图中蓝色点)处于四个象限中的哪一个,并将邻近的两个特征点也作为正样本。以上图举例,若gt偏向于右下角的象限,就会将gt所在grid的右边、下边特征点也作为正样本。
off = torch.tensor([[0, 0],
[1, 0], [0, 1], [-1, 0], [0, -1],
], device=targets.device).float() * g
gxy = t[:, 2:4]
gxi = gain[[2, 3]] - gxy
j, k = ((gxy % 1 < g) & (gxy > 1)).T
l, m = ((gxi % 1 < g) & (gxi > 1)).T
j = torch.stack((torch.ones_like(j), j, k, l, m))
t = t.repeat((5, 1, 1))[j]
offsets = (torch.zeros_like(gxy)[None] + off[:, None])[j]
关于上面的代码语法上理解如下 这里可以看出V5和V7是很类似的,相比较yolov3和v4一个gt只会匹配一个正样本的方式,该种方法能够分配更多的正样本,有助于训练加速,正负样本平衡。 完成增加正样本后(找到了t个先验框(正样本),我们需要判断这些框是属于哪张图片,其属于哪个类别,负责该样本预测的单元格的左上坐标为多少,以及该坐标的w,h的缩放比例
b, c = t[:, :2].long().T
gxy = t[:, 2:4]
gwh = t[:, 4:6]
gij = (gxy - offsets).long()
gi, gj = gij.T
a = t[:, 6].long()
indices.append(
(b, a, gj.clamp_(0, shape[2] - 1), gi.clamp_(0, shape[3] - 1)))
anchors.append(anchors_i[a])
而且由于每一个特征图中,都会将所有的gt与当前特征图的anchor计算能否分配正样本,也就说明一个gt可能会在多个特征图中都分配到正样本。 find_3_positive的返回结果为: indices 的shape为:【3,4,正样本个数】 anchor的shape为:
复筛get_target()
至此,我们便完成了正样本的匹配即初筛工作,紧接着我们要对初筛得到的先验框进行复筛,此时便是要根据predictions的预测先验框与真实框计算IOU与类别进行复筛。
fg_pred = prediction[b, a, gj, gi]
p_obj.append(fg_pred[:, 4:5])
p_cls.append(fg_pred[:, 5:])
grid = torch.stack([gi, gj], dim=1).type_as(fg_pred)
pxy = (fg_pred[:, :2].sigmoid() * 2. - 0.5 + grid) * self.stride[i]
pwh = (fg_pred[:, 2:4].sigmoid() * 2) ** 2 * anch[i][idx] * self.stride[i]
pxywh = torch.cat([pxy, pwh], dim=-1)
pxyxy = self.xywh2xyxy(pxywh)
pxyxys.append(pxyxy)
计算当前图片中,真实框与预测框的重合程度 ou的范围为0-1,取-log后为0~inf 重合程度越大,取-log后越小 因此,真实框与预测框重合度越大,pair_wise_iou_loss越小,所得为(真实框个数*候选框个数)
pair_wise_iou = self.box_iou(txyxy, pxyxys)
pair_wise_iou_loss = -torch.log(pair_wise_iou + 1e-8)
通过计算IOU后,选择前20,若没有20,则有多少选多少
top_k, _ = torch.topk(pair_wise_iou, min(20, pair_wise_iou.shape[1]), dim=1)
dynamic_ks = torch.clamp(top_k.sum(1).int(), min=1)
gt_cls_per_image = F.one_hot(this_target[:, 1].to(torch.int64), self.num_classes).float().unsqueeze(
1).repeat(1, pxyxys.shape[0], 1)
预测类别并计算交叉熵
cls_preds_ = p_cls.float().unsqueeze(0).repeat(num_gt, 1, 1).sigmoid_() * p_obj.unsqueeze(0).repeat(num_gt,
1, 1).sigmoid_()
y = cls_preds_.sqrt_()
pair_wise_cls_loss = F.binary_cross_entropy_with_logits(torch.log(y / (1 - y)), gt_cls_per_image,
reduction="none").sum(-1)
求cost损失总和,topk函数:找前k个最大值
cost = ( pair_wise_cls_loss+ 3.0 * pair_wise_iou_loss)
matching_matrix = torch.zeros_like(cost)
for gt_idx in range(num_gt):
_, pos_idx = torch.topk(cost[gt_idx], k=dynamic_ks[gt_idx].item())
matching_matrix[gt_idx][pos_idx] = 1.0
为防止一个anchor预测多个gt,还需要将其转换一下取出最小的iou作为y预测
anchor_matching_gt = matching_matrix.sum(0)
if (anchor_matching_gt > 1).sum() > 0:
_, cost_argmin = torch.min(cost[:, anchor_matching_gt > 1], dim=0)
matching_matrix[:, anchor_matching_gt > 1] *= 0.0
matching_matrix[cost_argmin, anchor_matching_gt > 1] = 1.0
fg_mask_inboxes = matching_matrix.sum(0) > 0.0
fg_mask_inboxes = fg_mask_inboxes.to(torch.device(device))
matched_gt_inds = matching_matrix[:, fg_mask_inboxes].argmax(0)
最终我们将匹配的批次,我们得到的值为: matching_bs, matching_as, matching_gjs, matching_gis, matching_targets, matching_anchs 下面来讲解其值含义: matching_bs:匹配上的批次 matching_as:匹配上的anchor id [0,1,2] matching_gjs, matching_gis:匹配上的单元格xy坐标(负责正样本预测) matching_targets:匹配上的标签,通过标签中批次,xywh(真实框)可以与前面匹配上的anchor进行计算。在find-3-positive中其加上了anchorid,但在这里没有用到被删除了。 matching_anchs:匹配上的anchor缩放比例
计算损失
完成build-targe函数后获得上面提到的匹配的正样本信息:call函数中
bs, as_, gjs, gis, targets, anchors = self.build_targets(predictions, targets, imgs)
开始计算损失:
for i, prediction in enumerate(predictions):
b, a, gj, gi = bs[i], as_[i], gjs[i], gis[i]
tobj = torch.zeros_like(prediction[..., 0], device=device)
n = b.shape[0]
if n:
prediction_pos = prediction[b, a, gj, gi]
grid = torch.stack([gi, gj], dim=1)
xy = prediction_pos[:, :2].sigmoid() * 2. - 0.5
wh = (prediction_pos[:, 2:4].sigmoid() * 2) ** 2 * anchors[i]
box = torch.cat((xy, wh), 1)
selected_tbox = targets[i][:, 2:6] * feature_map_sizes[i]
selected_tbox[:, :2] -= grid.type_as(prediction)
iou = self.bbox_iou(box.T, selected_tbox, x1y1x2y2=False, CIoU=True)
box_loss += (1.0 - iou).mean()
tobj[b, a, gj, gi] = (1.0 - self.gr) + self.gr * iou.detach().clamp(0).type(tobj.dtype)
selected_tcls = targets[i][:, 1].long()
t = torch.full_like(prediction_pos[:, 5:], self.cn, device=device)
t[range(n), selected_tcls] = self.cp
cls_loss += self.BCEcls(prediction_pos[:, 5:], t)
obj_loss += self.BCEobj(prediction[..., 4], tobj) * self.balance[i]
box_loss *= self.box_ratio
obj_loss *= self.obj_ratio
cls_loss *= self.cls_ratio
bs = tobj.shape[0]
loss = box_loss + obj_loss + cls_loss
return loss
根据模型预测结果计算回归值,回归值计算公式 至此,YOLOV7的正样本匹配与损失函数计算过程便完成了。
|