YOLOv5的输出端(Head)详解|CSDN创作打卡

深度学习入门小菜鸟,希望像做笔记记录自己学的东西,也希望能帮助到同样入门的人,更希望大佬们帮忙纠错啦~侵权立删。

注:因为有些朋友喜欢的是逐句逐句的看代码解析,所以我整理了两份,一份是逐份逐份分析代码,一份是完整代码解析(解析全在注释里,直接复制粘贴到VScode上看会更舒服些),两份都是一样的。

目录

一、Bounding box损失函数

1、IOU_Loss

2、YOLOv5所用的损失函数 -- CIOU_Loss

 二、NMS非极大值抑制

1、提出原因

2、YOLO识别原理

3、NMS是啥呢?

三、源码分析(Yolo.py中的class Detect)

1、逐份逐份分析版

2、代码注释分析一体化


一、Bounding box损失函数

1、IOU_Loss

IOU是交并比,在这里是指预测的物体框框和真实的物体框框的交集的面积与并集的面积之比。

IOU_Loss是根据IOU的损失函数:IOU_Loss = 1 - IOU

但是它存在一些缺点:

(1)如果你的预测框和真实框完全不重合,那么你的IOU为0,没有办法呈现出你的预测框距离真实框有多远,损失函数不可导,导致无法进行优化。

(2)可能出现两个IOU一样,对应的2个框框的面积也一样,但是相交情况完全不一样,那么IOU_Loss将无法区分他们相交的不同。

2、YOLOv5所用的损失函数 -- CIOU_Loss

因为IOU_Loss的缺陷,所以YOLOv5采用的是CIOU_Loss。(其实还有几种与IOU有关的损失函数:GIOU_Loss,DIOU_Loss)

C:预测框和真实框的最小外接矩阵

Distance_2:预测框的中心点和真实框的中心点的欧氏距离

Distance_C:C的对角线距离

v(其中w为宽,h为高,gt为真实框,p为预测框)长宽比影响因子

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAdHTkuKs=,size_14,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAdHTkuKs=,size_20,color_FFFFFF,t_70,g_se,x_16

CIOU_Loss考虑了重叠面积,长宽比和中心点距离。


 二、NMS非极大值抑制

1、提出原因

(1)如果物体很大,而网格又很小,一个物体可能会被多个网格识别

(2)如何判断出这几个网格识别的是同一个物体,而不是同一类的多个物体?

2、YOLO识别原理

要说NMS是啥,就避不开谈谈YOLO的识别原理。

YOLO将图片分割成s^2个网格。每个网格的大小相同,并且让s^2个网格每个都可以预测出B个边界箱(预测框)。预测出来的每个边界箱都有5个信息量: 物体的中心位置(x,y),物体的高h,物体的宽w以及这次预测的置信度(预测这个网格里是否有目标的置信度)。每个网格不仅只预测B个边界箱,还预测这个网格是什么类别。假设我们要预测C类物体,则有C个置信度(预测是某一类目标的置信度)。那么这次预测的信息就有s*s*(5*B+C)个。

3、NMS是啥呢?

方案一:选择预测类别的置信度(预测这个网格里是否有目标的置信度)高的里留下来,其余的预测都删除。但是这种做法无法解决上述的问题(2)。

方案二:把置信度(预测这个网格里是否有目标的置信度)最高的那个网格的边界箱作为极大边界箱,计算极大边界箱和其他几个网格的边界箱的IOU,如果超过一个阈值,例如0.5,就认为这两个网格实际上预测的是同一个物体,就把其中置信度比较小的删除。nice~


三、源码分析(Yolo.py中的class Detect)

1、逐份逐份分析版

我们就按他的分法按3个板块来解说。

def __init__

首先是一些参数的设置

    def __init__(self, nc=80, anchors=(), ch=(), inplace=True):  # detection layer
        #yolov5中的anchors(3个,对应Neck出来的那3个输出),初始anchor是由w,h宽高组成,用的是原图的像素尺寸,设置为每层3个,所以共有3 * 3 = 9个
        super().__init__()
        self.nc = nc  # 预测的类的数量
        self.no = nc + 5  
        # 每一个预测框(anchor)输出的数量,对应每种类的置信度(nc),预测框的高宽,中心点坐标,预测框内是否有物体的置信度,共5种信息。
        self.nl = len(anchors)  # 预测层的数量
        self.na = len(anchors[0]) // 2  #预测框的数量

yolov5中的anchors(3个,对应Neck出来的那3个输出),初始anchor是由w,h宽高组成,用的是原图的像素尺寸,设置为每层3个,所以共有3 * 3 = 9个

nc:待预测的类的数量

no:每一个预测框(anchor)输出的数量,对应每种类的置信度(nc),预测框的高宽,中心点坐标,预测框内是否有物体的置信度,共5 + 80 = 85种信息。

nl:预测层的数量

na:预测框的数量

        self.grid = [torch.zeros(1)] * self.nl  # 初始网格,对于每个预测层都有初始网格的生成
        self.anchor_grid = [torch.zeros(1)] * self.nl  # 初始预测框网格
        self.register_buffer('anchors', torch.tensor(anchors).float().view(self.nl, -1, 2))  
        # shape(nl,na,2)
        self.m = nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch)  
        # output conv,输出结果:每个预测框的输出结果 * 预测框个数
        self.inplace = inplace  # use in-place ops (e.g. slice assignment)

grid:初始网格,对于每个预测层都有初始网格的生成

anchor_grid:初始预测框网格

self.m:output conv,输出结果:每个预测框的输出结果 * 预测框个数

def  _make_grid

准备网格,所有的预测的单位长度都是基于grid层面的而不是原图,并且每一层的grid的尺寸都是不一样的,和每一层输出的尺寸w,h是一样的。

     def _make_grid(self, nx=20, ny=20, i=0):
        #准备网格,所有的预测的单位长度都是基于grid层面的而不是原图,并且每一层的grid的尺寸都是不一样的,和每一层输出的尺寸w,h是一样的。
        d = self.anchors[i].device
        if check_version(torch.__version__, '1.10.0'):  # torch>=1.10.0 meshgrid workaround for torch>=0.7 compatibility
            yv, xv = torch.meshgrid([torch.arange(ny, device=d), torch.arange(nx, device=d)], indexing='ij')
            #torch.meshgrid()生成网格,可以用于生成坐标,尺寸nx * ny;ny范围是竖向坐标;nx范围是横向坐标
        else:
            yv, xv = torch.meshgrid([torch.arange(ny, device=d), torch.arange(nx, device=d)])
        grid = torch.stack((xv, yv), 2).expand((1, self.na, ny, nx, 2)).float()
        anchor_grid = (self.anchors[i].clone() * self.stride[i]) \
            .view((1, self.na, 1, 1, 2)).expand((1, self.na, ny, nx, 2)).float()
        return grid, anchor_grid #制成网格返回

基本大框架是先检查版本,针对不同版本进行不同的同款操作

torch.meshgrid()生成网格,可以用于生成坐标,尺寸nx * ny;ny范围是竖向坐标;nx范围是横向坐标

def  forward

主结构是循环每层每层的处理

      def forward(self, x):
        z = []  # inference output
        for i in range(self.nl): #每层循环着处理

循环里面进行核心操作

x[i] = self.m[i](x[i])  # conv卷积

卷积处理

bs, _, ny, nx = x[i].shape  #bs第几个预测层的意思吧

提取一些数据出来,其中bs应该是第几个预测层的意思吧???这里不太确定

            x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
            #view()变换形状,数据不变, x(bs,255,20,20) to x(bs,3,85,20,20),将一个预测层里的3个anchor的信息分出来,每个预测框预测信息数量为self.no(这里为85)
            #permute(0, 1, 3, 4, 2),x[i]有5个维度,(2,3,4)变成(3,4,2),x(bs,3,85,20,20)to x(bs,3,20,20,85)
            #contiguous()进行一个拷贝

view()变换形状,数据不变, x(bs,255,20,20) to x(bs,3,85,20,20),将一个预测层里的3个anchor的信息分出来,每个预测框预测信息数量为self.no(这里为85)

permute(0, 1, 3, 4, 2),x[i]有5个维度,(2,3,4)变成(3,4,2),x(bs,3,85,20,20)to x(bs,3,20,20,85)

contiguous()进行一个拷贝

然后又进入一个是否进行训练的框架中(if not self.training:)

            if not self.training:  # inference
                if self.onnx_dynamic or self.grid[i].shape[2:4] != x[i].shape[2:4]:
                    self.grid[i], self.anchor_grid[i] = self._make_grid(nx, ny, i)
                    #制作第几预测层的网格

判断是否需要制作第i预测层的网格

 y = x[i].sigmoid() 
#激活函数,完成逻辑回归的软判决,变量映射到0,1之间的S型函数,所以最后的y就是相对于网格占了几分之几的意思(对center的x,y,w,h都做了归一化处理)

激活函数,完成逻辑回归的软判决,变量映射到0,1之间的S型函数,所以最后的y就是相对于网格占了几分之几的意思(对center的x,y,w,h都做了归一化处理)

                if self.inplace:
                    y[..., 0:2] = (y[..., 0:2] * 2 - 0.5 + self.grid[i]) * self.stride[i]  # xy
                    #box center的x,y的预测被乘以2并减去了0.5,让他的预测范围变成(-0.5,1.5)就是能跨半个网格预测
                    #然后加上self.grid[i],就是加上网格的宽度/高度
                    #最后乘上self.stride[i],就是步长,定位到原先预测的那个点
                    y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i]  # wh
                    #对预测框高宽的处理
                else:  # for YOLOv5 on AWS Inferentia https://github.com/ultralytics/yolov5/pull/2953
                    xy = (y[..., 0:2] * 2 - 0.5 + self.grid[i]) * self.stride[i]  # xy
                    wh = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i]  # wh
                    y = torch.cat((xy, wh, y[..., 4:]), -1)
                

这里分析self.inplace的情况:

第一句是对中心点x,y的操作。

(1)box center的x,y的预测被乘以2并减去了0.5,让他的预测范围变成(-0.5,1.5)就是能跨半个网格预测

(2)然后加上self.grid[i],就是加上网格的宽度/高度

(3)最后乘上self.stride[i],就是步长,定位到原先预测的那个点

第二句是对预测框的宽高进行操作。

z.append(y.view(bs, -1, self.no))
#将结果填入z:(第几层的预测层),(预测出的center的x,y,以及预测框的w和h),(对应的85种信息--这里个人认为这85种信息中的x,y,w,h不会再用到,主要是取出置信度信息)

将结果填入z:(第几层的预测层),(预测出的center的x,y,以及预测框的w和h),(对应的85种信息--这里个人认为这85种信息中的x,y,w,h不会再用到,主要是取出置信度信息)

return x if self.training else (torch.cat(z, 1), x)

最后来个判断后return,如果还要训练就是返回x,如果训练完毕,那就返回(torch.cat(z, 1), x)

2、代码注释分析一体化

class Detect(nn.Module):
    stride = None  # strides computed during build
    onnx_dynamic = False  # ONNX export parameter

    def __init__(self, nc=80, anchors=(), ch=(), inplace=True):  # detection layer
        #yolov5中的anchors(3个,对应Neck出来的那3个输出),初始anchor是由w,h宽高组成,用的是原图的像素尺寸,设置为每层3个,所以共有3 * 3 = 9个
        super().__init__()
        self.nc = nc  # 预测的类的数量
        self.no = nc + 5  
        # 每一个预测框(anchor)输出的数量,对应每种类的置信度(nc),预测框的高宽,中心点坐标,预测框内是否有物体的置信度,共5种信息。
        self.nl = len(anchors)  # 预测层的数量
        self.na = len(anchors[0]) // 2  #预测框的数量
        self.grid = [torch.zeros(1)] * self.nl  # 初始网格,对于每个预测层都有初始网格的生成
        self.anchor_grid = [torch.zeros(1)] * self.nl  # 初始预测框网格
        self.register_buffer('anchors', torch.tensor(anchors).float().view(self.nl, -1, 2))  
        # shape(nl,na,2)
        self.m = nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch)  
        # output conv,输出结果:每个预测框的输出结果 * 预测框个数
        self.inplace = inplace  # use in-place ops (e.g. slice assignment)

    def forward(self, x):
        z = []  # inference output
        for i in range(self.nl): #每层循环着处理
            x[i] = self.m[i](x[i])  # conv卷积
            bs, _, ny, nx = x[i].shape  
            #bs第几个预测层的意思吧
            x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
            #view()变换形状,数据不变, x(bs,255,20,20) to x(bs,3,85,20,20),将一个预测层里的3个anchor的信息分出来,每个预测框预测信息数量为self.no(这里为85)
            #permute(0, 1, 3, 4, 2),x[i]有5个维度,(2,3,4)变成(3,4,2),x(bs,3,85,20,20)to x(bs,3,20,20,85)
            #contiguous()进行一个拷贝
            if not self.training:  # inference
                if self.onnx_dynamic or self.grid[i].shape[2:4] != x[i].shape[2:4]:
                    self.grid[i], self.anchor_grid[i] = self._make_grid(nx, ny, i)
                    #制作第几预测层的网格

                y = x[i].sigmoid()
                #激活函数,完成逻辑回归的软判决,变量映射到0,1之间的S型函数,所以最后的y就是相对于网格占了几分之几的意思(对center的x,y,w,h都做了归一化处理)
                if self.inplace:
                    y[..., 0:2] = (y[..., 0:2] * 2 - 0.5 + self.grid[i]) * self.stride[i]  # xy
                    #box center的x,y的预测被乘以2并减去了0.5,让他的预测范围变成(-0.5,1.5)就是能跨半个网格预测
                    #然后加上self.grid[i],就是加上网格的宽度/高度
                    #最后乘上self.stride[i],就是步长,定位到原先预测的那个点
                    y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i]  # wh
                    #对预测框高宽的处理
                else:  # for YOLOv5 on AWS Inferentia https://github.com/ultralytics/yolov5/pull/2953
                    xy = (y[..., 0:2] * 2 - 0.5 + self.grid[i]) * self.stride[i]  # xy
                    wh = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i]  # wh
                    y = torch.cat((xy, wh, y[..., 4:]), -1)
                z.append(y.view(bs, -1, self.no))
                #将结果填入z:(第几层的预测层),(预测出的center的x,y,以及预测框的w和h),(对应的85种信息--这里个人认为这85种信息中的x,y,w,h不会再用到,主要是取出置信度信息)

        return x if self.training else (torch.cat(z, 1), x)

    def _make_grid(self, nx=20, ny=20, i=0):
        #准备网格,所有的预测的单位长度都是基于grid层面的而不是原图,并且每一层的grid的尺寸都是不一样的,和每一层输出的尺寸w,h是一样的。
        d = self.anchors[i].device
        if check_version(torch.__version__, '1.10.0'):  # torch>=1.10.0 meshgrid workaround for torch>=0.7 compatibility
            yv, xv = torch.meshgrid([torch.arange(ny, device=d), torch.arange(nx, device=d)], indexing='ij')
            #torch.meshgrid()生成网格,可以用于生成坐标,尺寸nx * ny;ny范围是竖向坐标;nx范围是横向坐标
        else:
            yv, xv = torch.meshgrid([torch.arange(ny, device=d), torch.arange(nx, device=d)])
        grid = torch.stack((xv, yv), 2).expand((1, self.na, ny, nx, 2)).float()
        anchor_grid = (self.anchors[i].clone() * self.stride[i]) \
            .view((1, self.na, 1, 1, 2)).expand((1, self.na, ny, nx, 2)).float()
        return grid, anchor_grid #制成网格返回

欢迎大家在评论区批评指正,谢谢大家~

你可能感兴趣的