一文看懂YOLO v8
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
2023年YOLO系列已经迭代到v8v8与v5均出自U神为了方便理解我们将通过与v5对比来讲解v8。想了解v5的可以参考文章yolov5。接下来我将把剪枝与蒸馏的工作集成到v8中大家可以期待一下。如果有什么不理解的地方可以留言。
首先回归一下yolov5
- BackboneCSPDarkNet结构主要结构思想的体现在C3模块这里也是梯度分流的主要思想所在的地方
- PAN-FPN双流的FPN这里除了上采样、CBS卷积模块最为主要的还有C3模块
- HeadCoupled Head+Anchor-baseYOLOv3、YOLOv4、YOLOv5、YOLOv7都是Anchor-Base的
- label assignment多正样本参考点采用shape匹配规则分别将GT的宽高与anchor宽高求比值比值小于阈值认定为正样本点
- Loss分类用BEC Loss回归用CIoU Loss。还有一个存在物体的置信度损失总损失为三个损失的加权和。
yolov8具体改进如下
- Backbone使用的依旧是CSP的思想不过YOLOv5中的C3模块被替换成了C2f模块实现了进一步的轻量化同时YOLOv8依旧使用了YOLOv5等架构中使用的SPPF模块
- PAN-FPNYOLOv8依旧使用了PAN的思想不过通过对比YOLOv5与YOLOv8的结构图可以看到YOLOv8将YOLOv5中PAN-FPN上采样阶段中的CBS 1*1的卷积结构删除了同时也将C3模块替换为了C2f模块
- Decoupled-HeadYOLOv8使用了Decoupled-Head即通过两个头分别输出cls与reg的输出
- Anchor-FreeYOLOv8抛弃了以往的Anchor-Base使用了Anchor-Free的思想
- LossYOLOv8使用VFL Loss作为分类损失实际训练中并未使用使用DFL Loss+CIOU Loss作为分类损失
- label assignmetYOLOv8抛弃了以往的IOU匹配或者单边比例的分配方式而是使用了Task-Aligned Assigner匹配方式。
了解yolo的朋友看完上面的对比应该对v8的结构有了大致的了解最主要的更新就是c2c2f结构以及在Detect中将cls与reg解耦并使用dfl的积分求取reg。dfl是GFL的一部分不理解的可以参考GFL。
c2f
C3模块其主要是借助CSPNet提取分流的思想同时结合残差结构的思想设计了C3 Block这里的CSP主分支梯度模块为BottleNeck模块也就是残差模块。结构如下图所示。
为了进一步轻量化v8设计了c2f结构与c3相比少了一层conv采用split将特征分层而不是conv。
class C2(nn.Module):
# CSP Bottleneck with 2 convolutions
def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): # ch_in, ch_out, number, shortcut, groups, expansion
super().__init__()
self.c = int(c2 * e) # hidden channels
self.cv1 = Conv(c1, 2 * self.c, 1, 1)
self.cv2 = Conv(2 * self.c, c2, 1) # optional act=FReLU(c2)
self.m = nn.Sequential(*(Bottleneck(self.c, self.c, shortcut, g, k=((3, 3), (3, 3)), e=1.0) for _ in range(n)))
def forward(self, x):
a, b = self.cv1(x).split((self.c, self.c), 1)
return self.cv2(torch.cat((self.m(a), b), 1))
Decoupled-head
而YOLOv8则是参考了YOLOX和YOLOV6使用了Decoupled-Head即使用两个卷积分别做分类和回归同时由于使用了DFL 的思想因此回归头的通道数也变成了4*reg_max
如代码所示其中reg是由cv2输出cls由cv3输出yolov8与v5一样有3个特征层81632倍下采样通过for x in ch 遍历特征层。gfl在coco中做了实验一般instance距离中心点的距离在81632下采样下不会超过16个像素因此self.reg_max设置为16。
self.cv2 = nn.ModuleList(
nn.Sequential(Conv(x, c2, 3), Conv(c2, c2, 3), nn.Conv2d(c2, 4 * self.reg_max, 1)) for x in ch)
self.cv3 = nn.ModuleList(nn.Sequential(Conv(x, c3, 3), Conv(c3, c3, 3), nn.Conv2d(c3, self.nc, 1)) for x in ch)
label-assignment
v8最重要的更新是采取anchor-free的方式并学习TOOD使用Task-Alignment learning对齐cls与reg任务那么下面我们对tood的label assignment进行详细解读。
正常对齐的Anchor应当可以预测高分类得分同时具有精确定位于此Tood设计了一个新的Anchor alignment metric Anchor alignment metric是cls得分以及预测框与GT的IOU相乘得来将其作为任意anchor的质量评估在Anchor level 衡量Task-Alignment的水平。并且Alignment metric 被集成在了 sample 分配和 loss function里来动态的优化每个 Anchor 的预测。
def forward(self, pd_scores, pd_bboxes, anc_points, gt_labels, gt_bboxes, mask_gt):
self.bs = pd_scores.size(0)
self.n_max_boxes = gt_bboxes.size(1)
if self.n_max_boxes == 0:
device = gt_bboxes.device
return (torch.full_like(pd_scores[..., 0], self.bg_idx).to(device), torch.zeros_like(pd_bboxes).to(device),
torch.zeros_like(pd_scores).to(device), torch.zeros_like(pd_scores[..., 0]).to(device),
torch.zeros_like(pd_scores[..., 0]).to(device))
mask_pos, align_metric, overlaps = self.get_pos_mask(pd_scores, pd_bboxes, gt_labels, gt_bboxes, anc_points,
mask_gt)
target_gt_idx, fg_mask, mask_pos = select_highest_overlaps(mask_pos, overlaps, self.n_max_boxes)
# assigned target
target_labels, target_bboxes, target_scores = self.get_targets(gt_labels, gt_bboxes, target_gt_idx, fg_mask)
# normalize
align_metric *= mask_pos
pos_align_metrics = align_metric.amax(axis=-1, keepdim=True) # b, max_num_obj
pos_overlaps = (overlaps * mask_pos).amax(axis=-1, keepdim=True) # b, max_num_obj
# norm_align_metric = (align_metric * pos_overlaps / (pos_align_metrics + self.eps)).amax(-2).unsqueeze(-1)
norm_align_metric = (align_metric / (pos_align_metrics + self.eps)).amax(-2).unsqueeze(-1)
target_scores = target_scores * norm_align_metric
return target_labels, target_bboxes, target_scores, fg_mask.bool(), target_gt_idx
在v8中label assignment分为3个步骤。首先根据self.get_pos_mask()计算出正样本的maskGT与pred_bboxes的iou以及align_metric其中
align_metric = bbox_scores.pow(self.alpha) * overlaps.pow(self.beta)
mask_pos即正样本的mask需要通过mask_topk * mask_in_gts * mask_gt相乘获得。mask_in_gts表示在GT内部的maskGT左上角与右下角的坐标分别与anchor的中心点做差获得bbox_deltas 当bbox_deltas的值均大于0则说明该点在GT内即可获得mask_in_gts。
bbox_deltas = torch.cat((xy_centers[None] - lt, rb - xy_centers[None]), dim=2).view(bs, n_boxes, n_anchors, -1)
mask_topk 这个很好理解就是align_metric的topk。align_metric同时考虑了cls与reg因此可以更好的对齐cls与reg两个任务。cls与reg是模型预测值两者结合可以很好的估计出该GT在哪些grid表现优秀用align_metric的topk来选正样本点更为合理。
def get_pos_mask(self, pd_scores, pd_bboxes, gt_labels, gt_bboxes, anc_points, mask_gt):
# get anchor_align metric, (b, max_num_obj, h*w)
align_metric, overlaps = self.get_box_metrics(pd_scores, pd_bboxes, gt_labels, gt_bboxes)
# get in_gts mask, (b, max_num_obj, h*w)
mask_in_gts = select_candidates_in_gts(anc_points, gt_bboxes)
# get topk_metric mask, (b, max_num_obj, h*w)
mask_topk = self.select_topk_candidates(align_metric * mask_in_gts,
topk_mask=mask_gt.repeat([1, 1, self.topk]).bool())
# merge all mask to a final mask, (b, max_num_obj, h*w)
mask_pos = mask_topk * mask_in_gts * mask_gt
return mask_pos, align_metric, overlaps
此时我们已经获得正样本的mask但是GT存在交叠的情况因此一个点可能对应多个GT我们需要杜绝这种情况将面积最大的GT赋值给有歧义的点。select_highest_overlaps函数可以完成这样的任务。
def select_highest_overlaps(mask_pos, overlaps, n_max_boxes):
# (b, n_max_boxes, h*w) -> (b, h*w)
fg_mask = mask_pos.sum(-2)
if fg_mask.max() > 1: # one anchor is assigned to multiple gt_bboxes
mask_multi_gts = (fg_mask.unsqueeze(1) > 1).repeat([1, n_max_boxes, 1]) # (b, n_max_boxes, h*w)
max_overlaps_idx = overlaps.argmax(1) # (b, h*w)
is_max_overlaps = F.one_hot(max_overlaps_idx, n_max_boxes) # (b, h*w, n_max_boxes)
is_max_overlaps = is_max_overlaps.permute(0, 2, 1).to(overlaps.dtype) # (b, n_max_boxes, h*w)
mask_pos = torch.where(mask_multi_gts, is_max_overlaps, mask_pos) # (b, n_max_boxes, h*w)
fg_mask = mask_pos.sum(-2)
# find each grid serve which gt(index)
target_gt_idx = mask_pos.argmax(-2) # (b, h*w)
return target_gt_idx, fg_mask, mask_pos
将mask_pos在n_max_boxes维度上叠加当fg_mask.max() > 1时说明存在歧义点。找到歧义点的索引mask_multi_gts 以及每个预测框对应的面积最大GT的索引max_overlaps_idx 将max_overlaps_idx变成onehot形式将有歧义点的值换成is_max_overlaps就可以祛除歧义。通过mask_pos.argmax(-2)能够获得target_gt_idx 即可以找到每个点对应的哪个GT。
最后我们根据target_gt_idx 可以获得计算loss的targetsget_targets逻辑较为清晰不赘述。至此v8的label assignment讲解完毕。
def get_targets(self, gt_labels, gt_bboxes, target_gt_idx, fg_mask):
"""
Args:
gt_labels: (b, max_num_obj, 1)
gt_bboxes: (b, max_num_obj, 4)
target_gt_idx: (b, h*w)
fg_mask: (b, h*w)
"""
# assigned target labels, (b, 1)
batch_ind = torch.arange(end=self.bs, dtype=torch.int64, device=gt_labels.device)[..., None]
target_gt_idx = target_gt_idx + batch_ind * self.n_max_boxes # (b, h*w)
target_labels = gt_labels.long().flatten()[target_gt_idx] # (b, h*w)
# assigned target boxes, (b, max_num_obj, 4) -> (b, h*w)
target_bboxes = gt_bboxes.view(-1, 4)[target_gt_idx]
# assigned target scores
target_labels.clamp(0)
target_scores = F.one_hot(target_labels, self.num_classes) # (b, h*w, 80)
fg_scores_mask = fg_mask[:, :, None].repeat(1, 1, self.num_classes) # (b, h*w, 80)
target_scores = torch.where(fg_scores_mask > 0, target_scores, 0)
return target_labels, target_bboxes, target_scores
Loss
对于YOLOv8其分类损失为VFL Loss其回归损失为CIOU Loss+DFL的形式这里Reg_max默认为16。
VFL主要改进是提出了非对称的加权操作FL和QFL都是对称的。而非对称加权的思想来源于论文PISA该论文指出首先正负样本有不平衡问题即使在正样本中也存在不等权问题因为mAP的计算是主正样本。VFL则为了突出正样本因此正样本采用bce而负样本采用FL来衰减loss。
如上图公式所示p是label正样本时候q为norm_align_metric计算出的值负样本时候p=0当为正样本时候其实没有采用FL而是普通的BCE只不过多了一个自适应norm_align_metric加权用于突出主样本。而为负样本时候就是标准的FL了。可以明显发现VFL比QFL更加简单主要特点是正负样本非对称加权、突出正样本为主样本。
DFLDistribution Focal Loss将坐标回归的单个值更改成输出n+1个值每个值表示对应回归距离的概率然后利用积分获得最终的回归距离。针对这里的DFL其主要是将框的位置建模成一个 general distribution让网络快速的聚焦于和目标位置距离近的位置的分布。
DFL 能够让网络更快地聚焦于目标 y 附近的值增大它们的概率
DFL的含义是以交叉熵的形式去优化与标签y最接近的一左一右2个位置的概率从而让网络更快的聚焦到目标位置的邻近区域的分布也就是说学出来的分布理论上是在真实浮点坐标的附近并且以线性插值的模式得到距离左右整数坐标的权重。