yolov5--loss.py --v5.0版本-最新代码详细解释-2021-7-1更新

简介: yolov5--loss.py --v5.0版本-最新代码详细解释-2021-7-1更新

一、简述

yolov5–v5.0版本(最新)代码解析导航


github ultralytics/yolov5

使用的yolov5为2021年6月23号的版本v5.0


此篇作为学习笔记,也花了比较大的功夫,尽可能对每一个要点进行了解释

如有一些问题或错误,欢迎大家一起交流。

简述训练target生成的过程

涉及的部分原理,可以翻到文章最后补充内容,之所以解释不放到最前,是因为涉及了代码中的一些内容,直接可能并不明白,阅读了一遍代码,再看解释的时候可能就明白了.

简述代码流程

在train.py中

1.训练前 实例化 损失类

compute_loss = ComputeLoss(model)  # init loss class

2.在训练中,调用__call__函数来返回损失

loss, loss_items = compute_loss(pred, targets.to(device))

二、代码部分

ComputeLoss中   init   部分

class ComputeLoss:
    # Compute losses
    def __init__(self, model, autobalance=False):
        super(ComputeLoss, self).__init__()
        device = next(model.parameters()).device  # get model device
        h = model.hyp  # hyperparameters
    '''
    对于目标obj损失来说无疑是用BCELoss(二分类损失)
    YOLOV5中使用的分类损失是BCEloss,对每个类别作sigmoid(而不是softmax),
    对于每个类别来说可以是一个2分类任务.
    其中nn.BCEWithLogitsLoss自带sigmoid
    '''
        # Define criteria
        BCEcls = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([h['cls_pw']], device=device))
        BCEobj = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([h['obj_pw']], device=device))
        '''
        对标签做平滑,eps=0就代表不做标签平滑,那么默认cp=1,cn=0
        后续对正类别赋值cp,负类别赋值cn
        '''
        # class label smoothing
        self.cp, self.cn = smooth_BCE(eps=h.get('label_smoothing', 0.0))  # positive, negative BCE targets
    '''
    如果g的值为0,则代表不使用focal loss
    '''
        # Focal loss
        g = h['fl_gamma']  # focal loss gamma
        if g > 0:
            BCEcls, BCEobj = FocalLoss(BCEcls, g), FocalLoss(BCEobj, g)
    '''
    balance用来设置三个特征图对应输出的损失系数
    从左到右对应大特征图(检测小物体)到小特征图(检测大物体),也就是说小物体的损失权重更大.
    当然模型不一定要三个特征图,其他情况比如四个五个特征图,同样可以设置相应的损失权重
    '''
        det = model.module.model[-1] if is_parallel(model) else model.model[-1]  # Detect() module
        self.balance = {3: [4.0, 1.0, 0.4]}.get(det.nl, [4.0, 1.0, 0.25, 0.06, .02])  # P3-P7
        self.ssi = list(det.stride).index(16) if autobalance else 0  # stride 16 index
        self.BCEcls, self.BCEobj, self.gr, self.hyp, self.autobalance = BCEcls, BCEobj, model.gr, h, autobalance
        for k in 'na', 'nc', 'nl', 'anchors':
            setattr(self, k, getattr(det, k))

ComputeLoss中build_targets

def build_targets(self, p, targets):
        # Build targets for compute_loss(), input targets(image,class,x,y,w,h)
        '''
        p:    List[torch.tensor * 3], p[i].shape = (b, 3, h, w, nc+5)
        targets:  targets.shape(nt, 6) , 6=icxywh ,  i表示第i张图片,c为类别,xywh为坐标
        '''
        na, nt = self.na, targets.shape[0]  # number of anchors, targets
        tcls, tbox, indices, anch = [], [], [], []
        '''
        gain是为了对后续的target(na,nt,7)中的归一化的xywh转为特征图中的网格坐标    
        其中7表示: i c x y w h ai
        '''
        gain = torch.ones(7, device=targets.device)  # normalized to gridspace gain    gain:(7)
        '''
        需要na个尺度,都进行训练,那么标签就需要复制na个, ai就代表每个尺度的索引,默认里面的值为0,1,2
        '''
        ai = torch.arange(na, device=targets.device).float().view(na, 1).repeat(1,
                                                                                nt)  # same as .repeat_interleave(nt)   ai:(na,nt)
        '''
        targets.repeat(na, 1, 1):  (na,nt,6)
        ai[:, :, None]:  (na,nt,1)  --广播--> (na,nt,1)
        targets:  (na,nt,7)    7: i c x y w h ai
        '''
        targets = torch.cat((targets.repeat(na, 1, 1), ai[:, :, None]), 2)  # append anchor indices
        g = 0.5  # bias
        off = torch.tensor([[0, 0],
                            [1, 0], [0, 1], [-1, 0], [0, -1],  # j,k,l,m
                            # [1, 1], [1, -1], [-1, 1], [-1, -1],  # jk,jm,lk,lm
                            ], device=targets.device).float() * g  # offsets
        for i in range(self.nl):
            '''
            self.anchors[i] :(3,3,2) 就是hyp.yaml里面看到的anchors的形状,
            3个尺度,3个形状的anchor, 2表示anchor的宽高
            anchors: (3,2)  就是当前尺度对应的3个anchor
            原来targets中xywh的值都是0~1之间,也就是原图中,归一化的坐标. 
            对 i c x y w h ai  相对应位置乘上 [1,1,特征图宽,特征图高,特征图宽,特征图高,1]
            得到的xywh就是特征图中的位置了,至于为什么是这种线性关系,
            为什么这样乘能得到特征图中的位置,最后会给出解释.
            '''
            anchors = self.anchors[i]
            gain[2:6] = torch.tensor(p[i].shape)[[3, 2, 3, 2]]  # xyxy gain  # p[i].shape:  (b,na,h,w,5+nc)
            # Match targets to anchors
            t = targets * gain  # 原始归一化坐标转化为当前特征图中的网格坐标
            if nt:
                # Matches
                '''
                t:(na,nt,7)    t[:, :, 4:6]: (na,nt,2)
                anchors: (na,2)   anchors[:, None]: (na,1,2)
                r: (na,nt,2)
                '''
                r = t[:, :, 4:6] / anchors[:, None]  # wh ratio
                '''
                torch.max(r, 1. / r)选出宽比,高比最大的
                max(2) 将宽比和高比 最大的那个选出来
                取索引[0] 是因为torch.max返回两个值,一个是value,第二个是index. 
                我们只要max的值就行了,不需要它的索引
                得到j: (na,nt)
                '''
                # GT与anchor的宽或高的比超过一定阈值,就当作为负样本
                j = torch.max(r, 1. / r).max(2)[0] < self.hyp['anchor_t']  # compare  获取宽高比小于4的网格位置
                # j = wh_iou(anchors, t[:, 4:6]) > model.hyp['iou_t']  # iou(3,n)=wh_iou(anchors(3,2), gwh(n,2))
                '''
                输入 t:(na,nt,7)  j:(na,nt)
                得到 t:(M,7)  M 代表筛选过后的目标数
                '''
                t = t[j]  # filter  初步分离正负样本(宽高比大于4的anchor均视为负样本)
                '''
                除了target所在的当前格子外,还需要有2个格子对目标进行预测(计算损失),
                也就是说一个目标,用3个格子去预测(计算损失).
                首先,必定包含目标所在的当前格子,其次是上下左右,
                四个中选择两个,作为另外两个格子,对target进行回归.
                只有对不是边上的格子才进行用3个格子去回归目标,
                因为他们可能缺少上下左右4个格子中其中一个(因为他们在边上了),
                所以后续会限制gxy或gxi>1的操作
                j:(M)   值类型为Bool       如果一个target相对应的值为True,表示该目标中心点所在的格子 
                的左边的格子也对该target进行h回归(后续要计算损失)
                k:(M)   值类型为Bool       表示目标所在格子的 上面的格子
                l:(M)   值类型为Bool       表示目标所在格子的 右边的格子
                m:(M)   值类型为Bool       表示目标所在格子的 下面的格子
                '''
                # Offsets
                gxy = t[:, 2:4]  # grid xy 得到中心点坐标xy(相对于左上角)  gxy:(M,2)
                gxi = gain[[2, 3]] - gxy  # inverse  得到中心点相对于右下角的坐标 gxi(M,2)
                j, k = ((gxy % 1. < g) & (gxy > 1.)).T  # 筛选中心坐标 左、上方偏移量小于0.5,并中心点大于1的target
                l, m = ((gxi % 1. < g) & (gxi > 1.)).T  # 筛选中心坐标 右、下方偏移量小于0.5,并且中心点大于1的target
                j = torch.stack((torch.ones_like(j), j, k, l, m))  # j:(5,M)
                '''
                输入: t:(M,7)----经过repeat--->(5,M,7)        j:(5,M)
                输出: t:(N,7)    由于1个格子变成了3个格子去回归目标,所以N<=3M(当target所属格子都不在边上的
                时候取到等号),   
                (通过j索引去获得是哪些格子需要做回归)
                '''
                t = t.repeat((5, 1, 1))[j]
                '''
                s            
                j:(5,M)
                torch.zeros_like(gxy)[None]:(1,M,2)
                off[:, None]:(5,1,2)
                (1,M,2)+(5,1,2)=(5,M,2)----[j]--->(N,2)
                offsets:(N,2)
                '''
                offsets = (torch.zeros_like(gxy)[None] + off[:, None])[j]
            else:
                t = targets[0]
                offsets = 0
            # Define
            '''
            b:(N)
            c:(N)
            gxy:(N,2)
            gwh:(N,2)
            gij:(N,2)
            gi:(N)
            gj:(N)
            '''
            b, c = t[:, :2].long().T  # image, class
            gxy = t[:, 2:4]  # grid xy
            gwh = t[:, 4:6]  # grid wh
            gij = (gxy - offsets).long()  # 获得每个标签所在的格子的索引
            '''
            比如有个目标的中心点x坐标为3.4,那么 3.4-0.5并向下取整,就为2了,那么就采用左边的格子了
            '''
            gi, gj = gij.T  # grid xy indices
            # Append
            '''
            a:(N)  尺度的索引
            b:(N)  第几张图片的索引
            indices:List(tuple*3)  tuple:(b,a,竖直方向第y个格子,竖直方向第x个格子)
            tbox:List(tensor*3)  tensor:(N,4)   4代表grid_offx,grid_offy,gw,gh
            anch:List(tensor*3)  tensor:(N)  为anchor的索引,如0,1,2,0,0,1... 因为不同尺度用不同大小的anchor
            tcls:List(tensor*3)  tensor:(N)  为类别   值为0~nc-1
            '''
            a = t[:, 6].long()  # anchor indices a:(N)  看属于第几个尺度,值为0,1,2
            indices.append((b, a, gj.clamp_(0, gain[3] - 1), gi.clamp_(0, gain[2] - 1)))  # image, anchor, grid indices
            tbox.append(torch.cat((gxy - gij, gwh), 1))  # box
            anch.append(anchors[a])  # anchors
            tcls.append(c)  # class
        return tcls, tbox, indices, anch

ComputeLoss中   call   部分

def __call__(self, p, targets):  # predictions, targets, model
        """
        p: 网络输出,List[torch.tensor * 3], p[i].shape = (b, 3, h, w, nc+5), hw分别为特征图的长宽
        ,b为batch-size
        targets: targets.shape = (nt, 6) , 6=icxywh,i表示第一张图片,c为类别,然后坐标xywh为(在原图中)归一
        化后的GT框
        model: 模型
        """
        device = targets.device
        '''
        lcls,lbox,lobj分别记录分类损失,box损失,obj损失
        tcls:List(tensor*3)  tensor:(N)   为类别   值为0~nc-1
        tbox:List(tensor*3)  tensor:(N,4)   4代表grid_offx,grid_offy,gw,gh
        indices:List(tuple*3)  tuple:(b,a,竖直方向第y个格子,竖直方向第x个格子)   为特征图中格子的索引
        anch:List(tensor*3)  tensor:(N)    为anchor的索引,如0,1,2,0,0,1...  因为不同尺度用不同大小的anchor
        '''
        lcls, lbox, lobj = torch.zeros(1, device=device), torch.zeros(1, device=device), torch.zeros(1, device=device)
        tcls, tbox, indices, anchors = self.build_targets(p, targets)  # targets #创建target
        # Losses
        for i, pi in enumerate(p):  # layer index, layer predictions
            '''
            b:(N) a:(N) gi:(N) gj:(N)
            这是之前target中有需要做回归的格子的索引,后续可以利用索引将网络中相应部分索引出来,
            从而使得objloss针对所有格子,而boxloss和clsloss只针对有目标的格子.
            (也就是说无对应obj的格子不进行分类和回归损失计算)
            '''
            b, a, gj, gi = indices[i]  # image, anchor, gridy, gridx
            '''
            先设置全为0,后续根据,b,a,gi,gj这些索引,将tobj中填充正样本,其他均为负样本
            pi:(b, 3, h, w, nc+5)   最后一个维度 是按  x y h w obj + nc 这个顺序来的,所以取索引0就是取x
            ,但是我们只是要它的形状而已
            这里取索引4也是同样的结果
            tobj:(b, 3, h, w)
            '''
            tobj = torch.zeros_like(pi[..., 0], device=device)  # target obj
            # n的值为N
            n = b.shape[0]  # number of targets
            if n:
                '''
                pi:(b, 3, h, w, nc+5) 
                pi[b, a, gj, gi]    根据(batch,anchor,y,x)  找到GT(ground truth)的anchors所对应的anchor
                通过索引得到ps:(N,nc+5)  
                '''
                ps = pi[b, a, gj, gi]  # prediction subset corresponding to targets
                # Regression
                '''
                ps:(N,nc+5)
                pxy:(N,2)
                pwh:(N,2)
                xy的预测范围为-0.5~1.5,因为除了自身格子外,还有可能还有相邻的两个格子
                wh的预测范围是0~4 ,是由于shape的过滤规则,wh预测输出也不再是任意范围
                '''
                pxy = ps[:, :2].sigmoid() * 2. - 0.5
                pwh = (ps[:, 2:4].sigmoid() * 2) ** 2 * anchors[i]
                pbox = torch.cat((pxy, pwh), 1)  # predicted box
                '''
                只有当CIOU=True时,才计算CIOU,否则默认为GIOU
                '''
                iou = bbox_iou(pbox.T, tbox[i], x1y1x2y2=False, CIoU=True)  # iou(prediction, target)
                lbox += (1.0 - iou).mean()  # iou loss 获得iou loss
                '''
                tobj:(b,na,h,w)
                将target_obj正样本给加入,原本初始化均为0,为负样本
                默认model.gr=1,也就是说使用iou(giou,ciou等)作为obj的置信度,否则使用常数,更加hard的标签
                , 或者以某种比例.
                由于CIOU还是GIOU等 ,都是IOU-...    ,结果都是小于等于1,还可能为负,所以在0处截断一下
                '''
                # Objectness
                tobj[b, a, gj, gi] = (1.0 - self.gr) + self.gr * iou.detach().clamp(0).type(tobj.dtype)  # iou ratio
                # Classification
                if self.nc > 1:  # cls loss (only if multiple classes)
                    '''
                    ps:(N,5+nc)  ps[:, 5:]:(N,80)
                    t:(N,80)
                    tcls[i]:(N)
                    '''
                    t = torch.full_like(ps[:, 5:], self.cn, device=device)  # targets
                    t[range(n), tcls[i]] = self.cp  # 对正样本填充相应的类别
                    lcls += self.BCEcls(ps[:, 5:], t)  # BCE  #计算分类损失
                # Append targets to text file
                # with open('targets.txt', 'a') as file:
                #     [file.write('%11.5g ' * 4 % tuple(x) + '\n') for x in torch.cat((txy[i], twh[i]), 1)]
            '''
            pi:(b,na,h,w,5+nc)
            pi[..., 4]:(b,na,h,w)
            tobj:(b,na,h,w)
            self.balance[i] 对应特征图尺度的损失系数
            '''
            obji = self.BCEobj(pi[..., 4], tobj)
            lobj += obji * self.balance[i]  # obj loss
            if self.autobalance:
                self.balance[i] = self.balance[i] * 0.9999 + 0.0001 / obji.detach().item()
        if self.autobalance:
            self.balance = [x / self.balance[self.ssi] for x in self.balance]
        '''
        对损失再进行一些调整
        '''
        lbox *= self.hyp['box']
        lobj *= self.hyp['obj']
        lcls *= self.hyp['cls']
        bs = tobj.shape[0]  # batch size
        loss = lbox + lobj + lcls
        return loss * bs, torch.cat((lbox, lobj, lcls, loss)).detach()

三、补充内容:

1.为什么原图上归一化的框的坐标*特征图的大小就是在特征图上的坐标呢?

比如box在原图上归一化的坐标为(0.2,0.3,0.4,0,3),对应的是xywh

特征图的大小为8080,那么转化为特征图上的坐标为16,24,32,24,是否想过为啥是线性关系呢?

从下面这行中,似乎得出了疑惑.

t = targets*gain 

这种情况是在整个模型的卷积核大小等于步长满足的情况下才有的.


那么整个模型的卷积核大小怎么计算呢?

  • 如果输入NN大小的图片得到结果是11,那么卷积核大小是NN

那么整个模型的步长怎么计算的呢?

  • 看下采样次数,比如yolov5,下采样5次,每次2倍,那么下采样倍率是32,于是模型的步长为32

当有了步长=卷积核大小,就可以用这种线性关系了.

下面来证明一下:

情况1.当卷积核大小不等于步长 时

下图是左边原图经过一次卷积,变成右侧特征图,可以看到红色的区域,对应的就是蓝色的区域

下面例子中卷积核大小KK,其中K=2

步长为stride,这边为stride=1

很显然,蓝色框的左上角,对应红色框的左上角,蓝色框的右下角,对应红色框的右下角.

因为卷积的滑动计算的缘故.

可以发现红色框的左上角坐标为(i×stride,j×stride)

红色框的右下角坐标为(i×stride+k,j×stride+k)

其中i j是特征图中格子的索引

情况2.当卷积核大小等于步长时

kstride=k

红色框的左上角坐标为(i×stride,j×stride)不变

红色框的右下角坐标变为((i+1)×stride,(j+1)×stride)

可以发现坐标可以变得更加通用一点,就变成下面这个样子了

image.png

也正是YOLOV5中下图的样子了,对于下采样32倍的特征图来说,每一个格子对应着原图上(h/32,w/32)的大小,其中h,w是原图的高和宽

2.YOLOV5中(非边界格子)每个target训练的时候,除了自身的格子,还需要相邻的两个格子作回归

下图是特征图上的一个格子,物体中心点的位置位于格子的左上方

此时(假设该物体是第i个)那么

image.png


即用上边、左边、和自身所在三个格子回归target

深蓝色的就是另外两个格子来回归红点的.

蓝灰色的框代表左侧格子的预测范围,可以看到红点是包括在内的

粉色框代表着上侧格子的预测范围

黄色代表着下侧格子的预测范围

如果以格子的左上角作为原点,那么预测范围就是(-0.5,1.5)之间

相关文章
|
2月前
|
机器学习/深度学习 编解码 计算机视觉
深入 YOLOv8:探索 block.py 中的模块,逐行代码分析(一)
深入 YOLOv8:探索 block.py 中的模块,逐行代码分析(一)
|
2月前
|
机器学习/深度学习 PyTorch 算法框架/工具
深入 YOLOv8:探索 block.py 中的模块,逐行代码分析(三)
深入 YOLOv8:探索 block.py 中的模块,逐行代码分析(三)
|
2月前
|
机器学习/深度学习 编解码 PyTorch
深入 YOLOv8:探索 block.py 中的模块,逐行代码分析(四)
深入 YOLOv8:探索 block.py 中的模块,逐行代码分析(四)
|
2月前
|
机器学习/深度学习 编解码 PyTorch
深入 YOLOv8:探索 block.py 中的模块,逐行代码分析(二)
深入 YOLOv8:探索 block.py 中的模块,逐行代码分析(二)
|
2月前
|
索引
yolov5--detect.py --v5.0版本-最新代码详细解释-2021-6-29号更新
yolov5--detect.py --v5.0版本-最新代码详细解释-2021-6-29号更新
124 0
yolov5--detect.py --v5.0版本-最新代码详细解释-2021-6-29号更新
|
2月前
yolov5--datasets.py --v5.0版本-数据集加载 最新代码详细解释2021-7-5更新
yolov5--datasets.py --v5.0版本-数据集加载 最新代码详细解释2021-7-5更新
150 0
|
2月前
yolov5--train.py --v5.0版本-2021-7-6更新
yolov5--train.py --v5.0版本-2021-7-6更新
41 0
|
机器学习/深度学习 测试技术 TensorFlow
dataset.py代码解释
这段代码主要定义了三个函数来创建 TensorFlow 数据集对象,这些数据集对象将被用于训练、评估和推断神经网络模型。
|
11月前
|
数据可视化 PyTorch 计算机视觉
YOLOv5源码逐行超详细注释与解读(3)——训练部分train.py
YOLOv5源码逐行超详细注释与解读(3)——训练部分train.py
1910 2
YOLOv5源码逐行超详细注释与解读(3)——训练部分train.py
|
9月前
|
前端开发 芯片 Python
【python脚本】ICer的脚本入门训练——gen_tc
【python脚本】ICer的脚本入门训练——gen_tc