网站的颜色,互联网设计公司网站,创建什么公司比较 好,移动微网站建设经典目标检测YOLO系列(一)复现YOLOV1(3)正样本的匹配及损失函数的实现
之前#xff0c;我们依据《YOLO目标检测》(ISBN:9787115627094)一书#xff0c;提出了新的YOLOV1架构#xff0c;并解决前向推理过程中的两个问题#xff0c;继续按照此书进行YOLOV1的复现。 经典目标…经典目标检测YOLO系列(一)复现YOLOV1(3)正样本的匹配及损失函数的实现
之前我们依据《YOLO目标检测》(ISBN:9787115627094)一书提出了新的YOLOV1架构并解决前向推理过程中的两个问题继续按照此书进行YOLOV1的复现。 经典目标检测YOLO系列(一)YOLOV1的复现(1)总体架构
经典目标检测YOLO系列(一)复现YOLOV1(2)反解边界框及后处理
1、正样本的匹配
1.1 正样本匹配思路
YOLOV1中正样本的匹配算法很简单就是目标边界框的中心落到feature map的哪个网格中哪个网格就是正样本。如下图黄色网格就是正样本。 后面会利用pytorch读取VOC数据集 一批图像数据的维度是 [B, 3, H, W] 分别是batch size色彩通道数图像的高和图像的宽。 标签数据是一个包含 B 个图像的标注数据的python的list变量如下所示其中每个图像的标注数据的list变量又包含了 M 个目标的信息类别和边界框。 获得了这一批数据后图片是可以直接喂到网络里去训练的但是标签不可以需要再进行处理一下。 [{boxes: tensor([[ 29., 230., 148., 321.]]), # bbox的坐标(xmin, ymin, xmax, ymax)labels: tensor([18.]), # 标签orig_size: [281, 500] # 图片的原始大小}, {boxes: tensor([[ 0., 79., 416., 362.]]), labels: tensor([1.]),orig_size: [375, 500]}
]标签处理主要包括3个部分 一是将真实框中心所在网格的置信度置为1其他网格默认为0二是真实框的标签类别为1其他类别设置为0三是真实框的bbox信息。 # 处理好的shape如下
# gt_objectness
torch.Size([2, 169, 1]) # 16913×13
# gt_classes
torch.Size([2, 169, 20])
# gt_bboxes
torch.Size([2, 169, 4])1.2 具体实现代码
# RT-ODLab/models/detectors/yolov1/matcher.pyimport torch
import numpy as np# YoloV1 正样本制作
class YoloMatcher(object):def __init__(self, num_classes):self.num_classes num_classestorch.no_grad()def __call__(self, fmp_size, stride, targets):fmp_size: (Int) input image size 用于最终检测的特征图的空间尺寸即划分网格的尺寸stride: (Int) - stride of YOLOv1 output. 特征图的输出步长targets: (Dict) dict{boxes: [...], labels: [...], orig_size: ...} 一批数据的标签targets是List类型的变量每一个元素都是一个Dict类型包含boxes和labels两个key对应的value就是【一张图片中的目标框的尺寸】和【类别标签】。# prepare# 准备一些空变量后续我们会将正样本的数据存放到其中比如gt_objectness其shape就是[B, fmp_h, fmp_w, 1]# 其中B就是batch size# [fmp_h, fmp_w] 就是特征图尺寸即网格# 1就是objectness的标签值# 所有网格的值都会初始化为0即负样本或背景在后续的处理中我们会一一确定哪些网格是正样本区域。bs len(targets)fmp_h, fmp_w fmp_sizegt_objectness np.zeros([bs, fmp_h, fmp_w, 1]) gt_classes np.zeros([bs, fmp_h, fmp_w, self.num_classes]) gt_bboxes np.zeros([bs, fmp_h, fmp_w, 4])# 第一层for循环遍历每一张图像的标签for batch_index in range(bs):# targets_per_image是python的Dict类型targets_per_image targets[batch_index]# [N,] N表示一个图像中有N个目标对象tgt_cls targets_per_image[labels].numpy()# [N, 4]tgt_box targets_per_image[boxes].numpy()# 第二层for循环遍历这张图像标签的每一个目标数据for gt_box, gt_label in zip(tgt_box, tgt_cls):x1, y1, x2, y2 gt_box# xyxy - cxcywhxc, yc (x2 x1) * 0.5, (y2 y1) * 0.5bw, bh x2 - x1, y2 - y1# checkif bw 1. or bh 1.:continue # grid 计算这个目标框中心点所在的网格坐标xs_c xc / strideys_c yc / stridegrid_x int(xs_c)grid_y int(ys_c)if grid_x fmp_w and grid_y fmp_h:# objectness标签采用01离散值gt_objectness[batch_index, grid_y, grid_x] 1.0# classification标签采用one-hot格式cls_ont_hot np.zeros(self.num_classes)cls_ont_hot[int(gt_label)] 1.0gt_classes[batch_index, grid_y, grid_x] cls_ont_hot# box标签采用目标框的坐标值gt_bboxes[batch_index, grid_y, grid_x] np.array([x1, y1, x2, y2])# [B, M, C]gt_objectness gt_objectness.reshape(bs, -1, 1)gt_classes gt_classes.reshape(bs, -1, self.num_classes)gt_bboxes gt_bboxes.reshape(bs, -1, 4)# to tensorgt_objectness torch.from_numpy(gt_objectness).float()gt_classes torch.from_numpy(gt_classes).float()gt_bboxes torch.from_numpy(gt_bboxes).float()return gt_objectness, gt_classes, gt_bboxesif __name__ __main__:matcher YoloMatcher(num_classes20)targets [{boxes: torch.tensor([[ 29., 230., 148., 321.]]), # bbox的坐标(xmin, ymin, xmax, ymax)labels: torch.tensor([18.]), # 标签orig_size: [281, 500] # 图片的原始大小},{boxes: torch.tensor([[ 0., 79., 416., 362.]]),labels: torch.tensor([1.]),orig_size: [375, 500]}
]gt_objectness, gt_classes, gt_bboxes matcher(fmp_size(13, 13),stride32, targetstargets )print(gt_objectness.shape)print(gt_classes.shape)print(gt_bboxes.shape)关键代码解释
对于bbox标签我们没有去计算中心点偏移量和宽高的log值而是直接赋予了原始的坐标值。这是因为后续在计算损失的时候我们将会采用当下流行的GIoU损失届时会计算预测的边界框坐标值和真实的目标框坐标值之间的GIoU。之前已经给出了我们的YOLOv1在结算坐标时用到的公式尽管没有直接给出中心点偏移量和log处理后的宽高值的标签但在回归时我们已经用sigmoid和exp约束了模型的预测的偏移量。因此在训练时模型仍旧会学习到我们希望他们能学习的正确形式即预测的偏移量在sigmoid和exp处理后会是合理的值。两层for循环全部执行完毕后准备好的空变量中就已经存好了正样本的标签。此前我们已经将模型的预测都从[B, C, H, W]的格式reshape成了的[B, M, C] 的格式即将空间的二维尺寸拉平了为了方便后续的计算我们也对标签数据做这样的处理也得到对应的[B, M, C]的格式。最后我们将这些标签数据都转换成torch.Tensor类型输出即可。以上就是训练阶段制作正样本的方法对于某次训练迭代所给的一批标签经过YoloMatcher类的处理后我们得到了包含objectness标签、classification标签、bbox标签的变量gt_objectness、gt_classes、gt_bboxes 。下面我们就可以编写计算训练的损失的代码。
2、损失函数的实现
2.1 损失函数的实现
这里修改损失函数将YOLOV1原本的MSE loss分类分支替换为BCE loss回归分支替换为GIou loss。
对于objectness损失所有的正样本和负样本都要参与进来计算对于classification损失我们只计算正样本处的这部分损失对于bbox损失同样只取出正样本处的预测和标签然后计算损失
# RT-ODLab/models/detectors/yolov1/loss.pyimport torch
import torch.nn.functional as F
from .matcher import YoloMatcher
from utils.box_ops import get_ious
from utils.distributed_utils import get_world_size, is_dist_avail_and_initializedclass Criterion(object):def __init__(self, cfg, device, num_classes80):self.cfg cfgself.device deviceself.num_classes num_classesself.loss_obj_weight cfg[loss_obj_weight]self.loss_cls_weight cfg[loss_cls_weight]self.loss_box_weight cfg[loss_box_weight]# matcherself.matcher YoloMatcher(num_classesnum_classes)def loss_objectness(self, pred_obj, gt_obj):# 此函数内部会自动做数值稳定版本的sigmoid操作# 因此输入给该函数的预测值不需要预先做sigmoid函数处理这也就是为什么在此前搭建的YOLOv1模型中的forward函数中看不到对objectness预测和classification预测做sigmoid处理# 当然在推理时我们还是要这么手动做的这一点也能够在YOLOv1模型的inference函数中看到。loss_obj F.binary_cross_entropy_with_logits(pred_obj, gt_obj, reductionnone)return loss_objdef loss_classes(self, pred_cls, gt_label):loss_cls F.binary_cross_entropy_with_logits(pred_cls, gt_label, reductionnone)return loss_clsdef loss_bboxes(self, pred_box, gt_box):# regression lossious get_ious(pred_box,gt_box,box_modexyxy,iou_typegiou)loss_box 1.0 - iousreturn loss_boxdef __call__(self, outputs, targets, epoch0):device outputs[pred_cls][0].devicestride outputs[stride]fmp_size outputs[fmp_size](gt_objectness, gt_classes, gt_bboxes,) self.matcher(fmp_sizefmp_size, stridestride, targetstargets)# List[B, M, C] - [B, M, C] - [BM, C]# 为了方便后续的计算将预测和标签的shape都从[B, M, C]调整成[BM, C]# 这一步没有任何数学意义仅仅是出于计算的方便。pred_obj outputs[pred_obj].view(-1) # [BM,]pred_cls outputs[pred_cls].view(-1, self.num_classes) # [BM, C]pred_box outputs[pred_box].view(-1, 4) # [BM, 4]gt_objectness gt_objectness.view(-1).to(device).float() # [BM,]gt_classes gt_classes.view(-1, self.num_classes).to(device).float() # [BM, C]gt_bboxes gt_bboxes.view(-1, 4).to(device).float() # [BM, 4]pos_masks (gt_objectness 0)num_fgs pos_masks.sum() # 正样本的数量if is_dist_avail_and_initialized():torch.distributed.all_reduce(num_fgs)# 考虑到我们可能会用到多张GPU因此我们需要将所有GPU上的正样本数量num_fgs做个平均。 num_fgs (num_fgs / get_world_size()).clamp(1.0)# obj loss# objectness损失由于这一损失是全局操作即所有的正样本和负样本都要参与进来因此没有特殊的操作直接计算即可然后做归一化。loss_obj self.loss_objectness(pred_obj, gt_objectness)loss_obj loss_obj.sum() / num_fgs# cls loss# 对于classification损失我们只计算正样本处的这部分损失# 因此我们需要先使用先前得到的pos_masks取出正样本处的预测和标签然后再去计算损失和归一化。pred_cls_pos pred_cls[pos_masks]gt_classes_pos gt_classes[pos_masks]loss_cls self.loss_classes(pred_cls_pos, gt_classes_pos)loss_cls loss_cls.sum() / num_fgs# box loss# 对于bbox损失操作基本同上取出正样本处的预测和标签然后计算损失最后再做一次归一化。pred_box_pos pred_box[pos_masks]gt_bboxes_pos gt_bboxes[pos_masks]loss_box self.loss_bboxes(pred_box_pos, gt_bboxes_pos)loss_box loss_box.sum() / num_fgs# total losslosses self.loss_obj_weight * loss_obj \self.loss_cls_weight * loss_cls \self.loss_box_weight * loss_box# 最后将所有的损失加权求和存放在一个Dict变量里去输出即可loss_dict dict(loss_obj loss_obj,loss_cls loss_cls,loss_box loss_box,losses losses)return loss_dict2.2 GIou loss
损失函数的实现比较简单这里重点介绍下GIou loss
MSE作为损失函数的缺点 原版YOLOV1中使用MSE作为损失函数之后Fast R-CNN提出了Smooth L1的损失函数它们的共同点都是使用两个角点四个坐标作为计算损失函数的变量我们称之为ln-norm算法。 绿色框是ground truth黑色框是预测bounding box。我们假设预测框的左下角是固定的只要右上角在以ground truth为圆心的圆周上这些预测框都有相同的 ln 损失值但是很明显它们的检测效果的差距是非常大的。与之对比的是IoU和GIoU则在这几个不同的检测框下拥有不同的值比较真实的反应了检测效果的优劣。 因此 ln -norm损失函数不和检测结果强相关。
IOU作为损失函数
从上图中我们可以看出IoU损失要比 ln 损失更能反应检测效果的优劣IoU损失函数可以表示为式 L I o U 1 − ∣ A ⋂ B ∣ ∣ A ⋃ B ∣ L_{IoU}1-\frac{|A \bigcap B|}{|A \bigcup B|} LIoU1−∣A⋃B∣∣A⋂B∣ IOU作为损失函数的特点 尺度不变性IoU反应的是两个检测框交集和并集之间的比例因此和检测框的大小无关。而 ln 则是和尺度相关的对于相同损失值的大目标和小目标大目标的检测效果要优于小目标因此IoU损失对于小目标的检测也是有帮助的 IoU是一个距离这个距离是指评估两个矩形框之间的一个指标这个指标具有distance的一切特性包括对称性非负性同一性三角不等性。 IoU损失的最大问题是当两个物体没有互相覆盖时损失值都会变成1而不同的不覆盖情况明显也反应了检测框的优劣如下图所示。可以看出当ground truth黑色框和预测框绿色框没有交集时IOU的值都是0而GIoU则拥有不同的值而且和检测效果成正相关。
GIOU作为损失函数 GIoU损失拥有IoU损失的所有优点但是也具有IoU损失不具有的一些特性。 GIoU也就有尺度不变性;GIoU也是一个距离因此拥有对称性非负性同一性和三角不等性GIoU是IoU的下界即 GIoU(A,B)≤IoU(A,B) 且当A和B的距离越接近GIoU和IoU的值越接近 GIoU的值域是 −1 到 1 。当 A 和 B 完美重合时 GIoU(A,B)1 当 A 和 B 的距离特别远此时它们的闭包趋近于无穷大此时 IoU(A,B)0 , 因此GIoU(A,B)-1 GIoU的目标相当于在损失函数中加入了一个ground truth和预测框构成的闭包的惩罚它的惩罚项是闭包减去两个框的并集后的面积在闭包中的比例越小越好。 如下所示闭包是红色虚线的矩形我们要最小化阴影部分的面积除以闭包的面积。 计算公式如下先计算两个框的最小闭包区域面积 Ac (通俗理解同时包含了预测框和真实框的最小框的面积)再计算出IoU再计算闭包区域中不属于两个框的区域占闭包区域的比重最后用IoU减去这个比重得到GIoU。 G I o U I o U − ∣ A c − U ∣ ∣ A c ∣ G_{IoU}IoU-\frac{|A_c - U|}{|A_c|} GIoUIoU−∣Ac∣∣Ac−U∣
GIoU损失优化的是当两个矩形框没有重叠时候的情况而当两个矩形框的位置非常接近时GIoU损失和IoU损失的值是非常接近的因此在某些场景下使用两个损失的模型效果应该比较接近但是GIoU应该具有更快的收敛速度。从GIoU的性质中我们可以看出GIoU在两个矩形没有重叠时它的优化目标是最小化两个矩形的闭包或是增大预测框的面积但是第二个目标并不是十分直接。
当然还有别的损失函数可以参考
IoU、GIoU、DIoU、CIoU损失函数的那点事儿 - 知乎 (zhihu.com)
GIoU的实现:
import numpy as np
import torchdef get_giou(bboxes1, bboxes2):计算GIOU值:param bboxes1: 预测框:param bboxes2: 真实框:return:eps torch.finfo(torch.float32).epsbboxes1_area (bboxes1[..., 2] - bboxes1[..., 0]).clamp_(min0) \* (bboxes1[..., 3] - bboxes1[..., 1]).clamp_(min0) # 所有预测框的面积bboxes2_area (bboxes2[..., 2] - bboxes2[..., 0]).clamp_(min0) \* (bboxes2[..., 3] - bboxes2[..., 1]).clamp_(min0) # 所有真实框的面积w_intersect (torch.min(bboxes1[..., 2], bboxes2[..., 2]) # 相交区域的w- torch.max(bboxes1[..., 0], bboxes2[..., 0])).clamp_(min0)h_intersect (torch.min(bboxes1[..., 3], bboxes2[..., 3])- torch.max(bboxes1[..., 1], bboxes2[..., 1]) # 相交区域的h).clamp_(min0)area_intersect w_intersect * h_intersect # 相交区域面积area_union bboxes2_area bboxes1_area - area_intersect # 两个区域的并集ious area_intersect / area_union.clamp(mineps) # 计算预测框和真实框之间的IOU值g_w_intersect torch.max(bboxes1[..., 2], bboxes2[..., 2]) - torch.min(bboxes1[..., 0], bboxes2[..., 0]) # 两个区域并集的wg_h_intersect torch.max(bboxes1[..., 3], bboxes2[..., 3]) - torch.min(bboxes1[..., 1], bboxes2[..., 1]) # 两个区域并集的hac_uion g_w_intersect * g_h_intersect # Acgious ious - (ac_uion - area_union) / ac_uion.clamp(mineps)return giousif __name__ __main__:pred_box torch.tensor(np.asarray([[1, 1, 3, 3.2],[1, 1, 4, 4]]) * 100)gt_box torch.tensor(np.asarray([[2, 2, 4.4, 4.5],[1, 1, 4, 4]]) * 100)print(get_giou(bboxes1pred_box, bboxes2gt_box))现在我们已经搭建好了模型也写好了标签分配和计算损的代码下一步即可准备开始训练我们的模型。
不过至今为止我们都还没有详细讲数据一环包括数据读取、数据预处理和数据增强等十分重要的操作。因此在正式开始训练我们的模型之前还需要进行数据操作。