其中一些常见的损失函数包括:
分类损失(cls_loss):该损失用于判断模型是否能够准确地识别出图像中的对象,并将其分类到正确的类别中。
置信度损失(obj_loss):该损失用于衡量模型预测的框(即包含对象的矩形)与真实框之间的差异。
边界框损失(box_loss):该损失用于衡量模型预测的边界框与真实边界框之间的差异,这有助于确保模型能够准确地定位对象。
这些损失函数在训练模型时被组合使用,以优化模型的性能。通过使用这些损失函数,YOLOv5可以准确地识别图像中的对象,并将其定位到图像中的具体位置。
1. 导入需要的包
importoneflowasflow importoneflow.nnasnn fromutils.metricsimportbbox_iou fromutils.oneflow_utilsimportde_parallel
2. smooth_BCE
这个函数是一个标签平滑的策略(trick),是一种在 分类/检测 问题中,防止过拟合的方法。
如果要详细理解这个策略的原理,请参阅博文:《trick 1》Label Smoothing(标签平滑)—— 分类问题中错误标注的一种解决方法.
smooth_BCE函数代码:
#标签平滑 defsmooth_BCE(eps=0.1):#https://github.com/ultralytics/yolov3/issues/238#issuecomment-598028441 """用在ComputeLoss类中 标签平滑操作[1,0]=>[0.95,0.05] :paramseps:平滑参数 :returnpositive,negativelabelsmoothingBCEtargets两个值分别代表正样本和负样本的标签取值 原先的正样本=1负样本=0改为正样本=1.0-0.5*eps负样本=0.5*eps """ #returnpositive,negativelabelsmoothingBCEtargets return1.0-0.5*eps,0.5*eps
通常会用在分类损失当中,如下ComputeLoss类的__init__函数定义:
self.cp,self.cn=smooth_BCE(eps=h.get("label_smoothing",0.0))#positive,negativeBCEtargets
ComputeLoss类的__call__函数调用:
#Classification ifself.nc>1:#clsloss(onlyifmultipleclasses) t=flow.full_like(pcls,self.cn,device=self.device)#targets #t[range(n),tcls[i]]=self.cp t[flow.arange(n,device=self.device),tcls[i]]=self.cp lcls=lcls+self.BCEcls(pcls,t)#BCE
3. BCEBlurWithLogitsLoss
这个函数是BCE函数的一个替代,是yolov5作者的一个实验性的函数,可以自己试试效果。
使用起来直接在ComputeLoss类的__init__函数中替代传统的BCE函数即可:
classBCEBlurWithLogitsLoss(nn.Module): """用在ComputeLoss类的__init__函数中 BCEwithLogitLoss()withreducedmissinglabeleffects. https://github.com/ultralytics/yolov5/issues/1030 Theideawastoreducetheeffectsoffalsepositive(missinglabels)就是检测成正样本了但是检测错了 """ def__init__(self,alpha=0.05): super(BCEBlurWithLogitsLoss,self).__init__() self.loss_fcn=nn.BCEWithLogitsLoss(reduction='none')#mustbenn.BCEWithLogitsLoss() self.alpha=alpha defforward(self,pred,true): loss=self.loss_fcn(pred,true) pred=flow.sigmoid(pred)#probfromlogits #dx=[-1,1]当pred=1true=0时(网络预测说这里有个obj但是gt说这里没有),dx=1=>alpha_factor=0=>loss=0 #这种就是检测成正样本了但是检测错了(falsepositive)或者missinglabel的情况这种情况不应该过多的惩罚->loss=0 dx=pred-true#reduceonlymissinglabeleffects #如果采样绝对值的话会减轻pred和gt差异过大而造成的影响 #dx=(pred-true).abs()#reducemissinglabelandfalselabeleffects alpha_factor=1-flow.exp((dx-1)/(self.alpha+1e-4)) loss*=alpha_factor returnloss.mean()
4. FocalLoss
FocalLoss损失函数来自 Kaiming He在2017年发表的一篇论文:Focal Loss for Dense Object Detection. 这篇论文设计的主要思路: 希望那些hard examples对损失的贡献变大,使网络更倾向于从这些样本上学习。防止由于easy examples过多,主导整个损失函数。
优点:
解决了one-stage object detection中图片中正负样本(前景和背景)不均衡的问题;降低简单样本的权重,使损失函数更关注困难样本;函数公式:
FocalLoss函数代码:
classFocalLoss(nn.Module): """用在代替原本的BCEcls(分类损失)和BCEobj(置信度损失) Wrapsfocallossaroundexistingloss_fcn(),i.e.criteria=FocalLoss(nn.BCEWithLogitsLoss(),gamma=1.5) 论文:https://arxiv.org/abs/1708.02002 https://blog.csdn.net/qq_38253797/article/details/116292496 TFimplementationhttps://github.com/tensorflow/addons/blob/v0.7.1/tensorflow_addons/losses/focal_loss.py """ #Wrapsfocallossaroundexistingloss_fcn(),i.e.criteria=FocalLoss(nn.BCEWithLogitsLoss(),gamma=1.5) def__init__(self,loss_fcn,gamma=1.5,alpha=0.25): super().__init__() self.loss_fcn=loss_fcn#mustbenn.BCEWithLogitsLoss()定义为多分类交叉熵损失函数 self.gamma=gamma#参数gamma用于削弱简单样本对loss的贡献程度 self.alpha=alpha#参数alpha用于平衡正负样本个数不均衡的问题 self.reduction=loss_fcn.reduction#self.reduction:控制FocalLoss损失输出模式sum/mean/none默认是Mean #focalloss中的BCE函数的reduction='None'BCE不使用Sum或者Mean #需要将Focalloss应用于每一个样本之中 self.loss_fcn.reduction="none"#requiredtoapplyFLtoeachelement defforward(self,pred,true): #正常BCE的loss:loss=-log(p_t) loss=self.loss_fcn(pred,true) #p_t=flow.exp(-loss) #loss*=self.alpha*(1.000001-p_t)**self.gamma#non-zeropowerforgradientstability #TFimplementationhttps://github.com/tensorflow/addons/blob/v0.7.1/tensorflow_addons/losses/focal_loss.py pred_prob=flow.sigmoid(pred)#probfromlogits p_t=true*pred_prob+(1-true)*(1-pred_prob) alpha_factor=true*self.alpha+(1-true)*(1-self.alpha) modulating_factor=(1.0-p_t)**self.gamma#这里代表Focalloss中的指数项 #返回最终的loss=BCE*两个参数(看看公式就行了和公式一模一样) loss=loss*alpha_factor*modulating_factor #最后选择focalloss返回的类型默认是mean ifself.reduction=="mean": returnloss.mean() elifself.reduction=="sum": returnloss.sum() else:#'none' returnloss
这个函数用在代替原本的BCEcls和BCEobj:
#Focalloss g=h["fl_gamma"]#focallossgammag=0代表不用focalloss ifg>0: BCEcls,BCEobj=FocalLoss(BCEcls,g),FocalLoss(BCEobj,g)
5. QFocalLoss
公式:
QFocalLoss函数代码:
classQFocalLoss(nn.Module): """用来代替FocalLoss QFocalLoss来自GeneralFocalLoss论文:https://arxiv.org/abs/2006.04388 WrapsQualityfocallossaroundexistingloss_fcn(), i.e.criteria=FocalLoss(nn.BCEWithLogitsLoss(),gamma=1.5) """ #WrapsQualityfocallossaroundexistingloss_fcn(),i.e.criteria=FocalLoss(nn.BCEWithLogitsLoss(),gamma=1.5) def__init__(self,loss_fcn,gamma=1.5,alpha=0.25): super().__init__() self.loss_fcn=loss_fcn#mustbenn.BCEWithLogitsLoss() self.gamma=gamma self.alpha=alpha self.reduction=loss_fcn.reduction self.loss_fcn.reduction="none"#requiredtoapplyFLtoeachelement defforward(self,pred,true): loss=self.loss_fcn(pred,true) pred_prob=flow.sigmoid(pred)#probfromlogits alpha_factor=true*self.alpha+(1-true)*(1-self.alpha) modulating_factor=flow.abs(true-pred_prob)**self.gamma loss=loss*(alpha_factor*modulating_factor) ifself.reduction=="mean": returnloss.mean() elifself.reduction=="sum": returnloss.sum() else:#'none' returnloss
使用 QFolcalLoss 直接在 ComputeLoss 类中使用 QFolcalLoss替换掉 FocalLoss 即可:(也就是说用 QFolcalLoss 替换如下图代码处的FocalLoss )
6. ComputeLoss类
6.1 __init__函数
sort_obj_iou=False#后面筛选置信度损失正样本的时候是否先对iou排序 #Computelosses def__init__(self,model,autobalance=False): #获取模型所在的设备 device=next(model.parameters()).device #获取模型的超参数 h=model.hyp #定义分类损失和置信度损失 BCEcls=nn.BCEWithLogitsLoss(pos_weight=flow.tensor([h["cls_pw"]],device=device)) BCEobj=nn.BCEWithLogitsLoss(pos_weight=flow.tensor([h["obj_pw"]],device=device)) #标签平滑eps=0代表不做标签平滑->cp=1cn=0/eps!=0代表做标签平滑 #cp代表正样本的标签值cn代表负样本的标签值 #请参考:Classlabelsmoothinghttps://arxiv.org/pdf/1902.04103.pdfeqn3 self.cp,self.cn=smooth_BCE(eps=h.get("label_smoothing",0.0))#positive,negativeBCEtargets #Focalloss g=h["fl_gamma"]#FocalLoss的超参数gamma ifg>0: #如果g>0将分类损失和置信度损失(BCE)都换成FocalLoss损失函数 BCEcls,BCEobj=FocalLoss(BCEcls,g),FocalLoss(BCEobj,g) #m:返回的是模型的3个检测头分别对应产生的3个输出特征图 m=de_parallel(model).model[-1]#Detect()module """self.balance用来实现obj,box,clsloss之间权重的平衡 {3:[4.0,1.0,0.4]}表示有三个layer的输出,第一个layer的weight是4.0,第二个1.0,第三个以此类推。 如果有5个layer的输出,那么权重分别是[4.0,1.0,0.25,0.06,0.02] """ self.balance={3:[4.0,1.0,0.4]}.get(m.nl,[4.0,1.0,0.25,0.06,0.02])#P3-P7 #三个检测头的下采样率m.stride:[8,16,32].index(16):求出下采样率stride=16的索引 #这个参数会用来自动计算更新3个featuremap的置信度损失系数self.balance self.ssi=list(m.stride).index(16)ifautobalanceelse0#stride16index self.BCEcls,self.BCEobj,self.gr,self.hyp,self.autobalance=( BCEcls, BCEobj, 1.0, h, autobalance, ) self.na=m.na#numberofanchors每个grid_cell的anchor数量=3 self.nc=m.nc#numberofclasses数据集的总类别=80 self.nl=m.nl#numberoflayers检测头的个数=3 #anchors:形状[3,3,2]代表3个featuremap每个featuremap上有3个anchor(w,h) #这里的anchors尺寸是相对featuremap的 self.anchors=m.anchors self.device=device
6.2 build_targets
这个函数是用来为所有GT筛选相应的anchor正样本。
筛选条件是比较GT和anchor的宽比和高比,大于一定的阈值就是负样本,反之正样本。
筛选到的正样本信息(image_index, anchor_index, gridy, gridx),传入 __call__ 函数,
通过这个信息去筛选 pred 里每个 grid 预测得到的信息,保留对应 grid_cell 上的正样本。
通过 build_targets 筛选的 GT 中的正样本和 pred 筛选出的对应位置的预测样本 进行计算损失。
补充理解:
这个函数的目的是为了每个 GT 匹配对应的高质量 Anchor 正样本参与损失计算,
j = flow.max(r, 1. / r).max(2)[0] < self.hyp["anchor_t"] 这步的比较是为了将 GT 分配到不同层上去检测,(详细解释请看下面的逐行代码注释)
后面的步骤是为了确定在这层检测的 GT 中心坐标,
进而确定这个 GT 在这层哪个 grid cell 进行检测。
做到这一步也就做到了为每个 GT 匹配 Anchor 正样本的目的。
#--------------------------------------------------------- #build_targets函数用于获得在训练时计算loss所需要的目标框,也即正样本。与yolov3/v4的不同,yolov5支持跨网格预测。 #对于任何一个GTbbox,三个预测特征层上都可能有先验框匹配,所以该函数输出的正样本框比传入的targets(GT框)数目多 #具体处理过程: #(1)首先通过bbox与当前层anchor做一遍过滤。对于任何一层计算当前bbox与当前层anchor的匹配程度,不采用IoU,而采用shape比例。如果anchor与bbox的宽高比差距大于4,则认为不匹配,此时忽略相应的bbox,即当做背景; #(2)根据留下的bbox,在上下左右四个网格四个方向扩增采样(即对bbox计算落在的网格所有anchors都计算loss(并不是直接和GT框比较计算loss)) #注意此时落在网格不再是一个,而是附近的多个,这样就增加了正样本数。 #yolov5没有conf分支忽略阈值(ignore_thresh)的操作,而yoloy3/v4有。 #-------------------------------------------------------- defbuild_targets(self,p,targets): """所有GT筛选相应的anchor正样本 这里通过 p:list([16,3,80,80,85],[16,3,40,40,85],[16,3,20,20,85]) targets:targets.shape[314,6] 解析build_targets(self,p,targets):函数 Buildtargetsforcompute_loss() :paramsp:p[i]的作用只是得到每个featuremap的shape 预测框由模型构建中的三个检测头Detector返回的三个yolo层的输出 tensor格式list列表存放三个tensor对应的是三个yolo层的输出 如:list([16,3,80,80,85],[16,3,40,40,85],[16,3,20,20,85]) [bs,anchor_num,grid_h,grid_w,xywh+class+classes] 可以看出来这里的预测值p是三个yolo层每个grid_cell(每个grid_cell有三个预测值)的预测值,后面肯定要进行正样本筛选 :paramstargets:数据增强后的真实框[63,6][num_target,image_index+class+xywh]xywh为归一化后的框 :returntcls:表示这个target所属的classindex tbox:xywh其中xy为这个target对当前grid_cell左上角的偏移量 indices:b:表示这个target属于的imageindex a:表示这个target使用的anchorindex gj:经过筛选后确定某个target在某个网格中进行预测(计算损失)gj表示这个网格的左上角y坐标 gi:表示这个网格的左上角x坐标 anch:表示这个target所使用anchor的尺度(相对于这个featuremap)注意可能一个target会使用大小不同anchor进行计算 """ #Buildtargetsforcompute_loss(),inputtargets(image,class,x,y,w,h) #na=3;nt=314 na,nt=self.na,targets.shape[0]#numberofanchors,targets tcls,tbox,indices,anch=[],[],[],[] #gain.shape=[7] gain=flow.ones(7,device=self.device)#normalizedtogridspacegain #ai.shape=(na,nt)生成anchor索引 #anchor索引,后面有用,用于表示当前bbox和当前层的哪个anchor匹配 #需要在3个anchor上都进行训练所以将标签赋值na=3个 #ai代表3个anchor上在所有的target对应的anchor索引就是用来标记下当前这个target属于哪个anchor #[1,3]->[3,1]->[3,314]=[na,nt]三行第一行63个0第二行63个1第三行63个2 #ai.shape=[3,314] ai=flow.arange(na,device=self.device).float().view(na,1).repeat(1,nt)#sameas.repeat_interleave(nt) #[314,6][3,314]->[3,314,6][3,314,1]->[3,314,7]7:[image_index+class+xywh+anchor_index] #对每一个featuremap:这一步是将target复制三份对应一个featuremap的三个anchor #先假设所有的target都由这层的三个anchor进行检测(复制三份)再进行筛选并将ai加进去标记当前是哪个anchor的target #targets.shape=[3,314,7] targets=flow.cat((targets.repeat(na,1,1),ai[...,None]),2)#appendanchorindices #这两个变量是用来扩展正样本的因为预测框预测到target有可能不止当前的格子预测到了 #可能周围的格子也预测到了高质量的样本我们也要把这部分的预测信息加入正样本中 #设置网格中心偏移量 g=0.5#bias #附近的4个框 #以自身+周围左上右下4个网格=5个网格用来计算offsets off=( flow.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=self.device, ).float() *g )#offsets #对每个检测层进行处理 #遍历三个feature筛选gt的anchor正样本 foriinrange(self.nl):#self.nl:numberofdetectionlayersDetect的个数=3 #anchors:当前featuremap对应的三个anchor尺寸(相对featuremap)[3,2] anchors,shape=self.anchors[i],p[i].shape #gain:保存每个输出featuremap的宽高->gain[2:6]=flow.tensor(shape)[[3,2,3,2]] #[1,1,1,1,1,1,1]->[1,1,112,112,112,112,1]=image_index+class+xywh+anchor_index gain[2:6]=flow.tensor(p[i].shape,device=self.device)[[3,2,3,2]].float()#xyxygain #Matchtargetstoanchors #t.shape=[3,314,7]将target中的xywh的归一化尺度放缩到相对当前featuremap的坐标尺度 #[3,314,image_index+class+xywh+anchor_index] t=targets*gain#shape(3,n,7) ifnt:#如果有目标就开始匹配 #Matches #所有的gt与当前层的三个anchor的宽高比(w/wh/h) #r.shape=[3,314,2] r=t[...,4:6]/anchors[:,None]#whratio #筛选条件GT与anchor的宽比或高比超过一定的阈值就当作负样本 #flow.max(r,1./r)=[3,314,2]筛选出宽比w1/w2w2/w1高比h1/h2h2/h1中最大的那个 #.max(2)返回宽比高比两者中较大的一个值和它的索引[0]返回较大的一个值 #j.shape=[3,314]False:当前anchor是当前gt的负样本True:当前anchor是当前gt的正样本 j=flow.max(r,1/r).max(2)[0]< self.hyp["anchor_t"] # compare # yolov3 v4的筛选方法: wh_iou GT与anchor的wh_iou超过一定的阈值就是正样本 # j = wh_iou(anchors, t[:, 4:6]) >model.hyp['iou_t']#iou(3,n)=wh_iou(anchors(3,2),gwh(n,2)) #根据筛选条件j,过滤负样本,得到所有gt的anchor正样本(batch_size张图片) #知道当前gt的坐标属于哪张图片正样本对应的idx也就得到了当前gt的正样本anchor #t:[3,314,7]->[555,7][num_Positive_sample,image_index+class+xywh+anchor_index] t=t[j]#filter #Offsets筛选当前格子周围格子找到2个离target中心最近的两个格子 #可能周围的格子也预测到了高质量的样本我们也要把这部分的预测信息加入正样本中 #除了target所在的当前格子外,还有2个格子对目标进行检测(计算损失) #也就是说一个目标需要3个格子去预测(计算损失) #首先当前格子是其中1个再从当前格子的上下左右四个格子中选择2个 #用这三个格子去预测这个目标(计算损失) #featuremap上的原点在左上角向右为x轴正坐标向下为y轴正坐标 #gridxy取target中心的坐标xy(相对featuremap左上角的坐标) #gxy.shape=[555,2] gxy=t[:,2:4]#gridxy #inverse得到target中心点相对于右下角的坐标gain[[2,3]]为当前featuremap的wh #gxi.shape=[555,2] gxi=gain[[2,3]]-gxy#inverse #筛选中心坐标距离当前grid_cell的左、上方偏移小于g=0.5 #且中心坐标必须大于1(坐标不能在边上此时就没有4个格子了) #j:[555]bool如果是True表示当前target中心点所在的格子的左边格子也对该target进行回归(后续进行计算损失) #k:[555]bool如果是True表示当前target中心点所在的格子的上边格子也对该target进行回归(后续进行计算损失) j,k=((gxy%1< g) & (gxy >1)).T #筛选中心坐标距离当前grid_cell的右、下方偏移小于g=0.5且中心坐标必须大于1(坐标不能在边上此时就没有4个格子了) #l:[555]bool如果是True表示当前target中心点所在的格子的右边格子也对该target进行回归(后续进行计算损失) #m:[555]bool如果是True表示当前target中心点所在的格子的下边格子也对该target进行回归(后续进行计算损失) l,m=((gxi%1< g) & (gxi >1)).T #j.shape=[5,555] j=flow.stack((flow.ones_like(j),j,k,l,m)) #得到筛选后所有格子的正样本格子数<=3*555 都不在边上等号成立 # t: [555, 7] ->复制5份target[5,555,7]分别对应当前格子和左上右下格子5个格子 #使用j筛选后t的形状:[1659,7] t=t.repeat((5,1,1))[j] #flow.zeros_like(gxy)[None]:[1,555,2]off[:,None]:[5,1,2]=>[5,555,2] #得到所有筛选后的网格的中心相对于这个要预测的真实框所在网格边界 #(左右上下边框)的偏移量,然后通过j筛选最终offsets的形状是[1659,2] offsets=(flow.zeros_like(gxy)[None]+off[:,None])[j] else: t=targets[0] offsets=0 #Define #bc.shape=[1659,2] #gxy.shape=[1659,2] #gwh.shape=[1659,2] #a.shape=[1659,1] bc,gxy,gwh,a=t.chunk(4,1)#(image,class),gridxy,gridwh,anchors #a,(b,c)=a.long().view(-1),bc.long().T#anchors,image,class #a.shape=[1659] #(b,c).shape=[1659,2] a,(b,c)=( a.contiguous().long().view(-1), bc.contiguous().long().T, )#anchors,image,class #gij=(gxy-offsets).long() #预测真实框的网格所在的左上角坐标(有左上右下的网格) #gij.shape=[1659,2] gij=(gxy-offsets).contiguous().long() #这里的拆分我们可以用下面的示例代码来进行解释: #importoneflowasflow #x=flow.randn(3,2) #y,z=x.T #print(y.shape) #print(z.shape) #=>oneflow.Size([3]) #=>oneflow.Size([3]) #因此: #gi.shape=[1659] #gj.shape=[1659] gi,gj=gij.T#gridindices #Append #indices.append((b,a,gj.clamp_(0,shape[2]-1),gi.clamp_(0,shape[3]-1)))#image,anchor,grid #gi.shape=[1659] #gj.shape=[1659] gi=gi.clamp(0,shape[3]-1) gj=gj.clamp(0,shape[2]-1) #b:imageindexa:anchorindexgj:网格的左上角y坐标gi:网格的左上角x坐标 indices.append((b,a,gj,gi))#image,anchor,grid #tbix:xywh其中xy为这个target对当前grid_cell左上角的偏移量 tbox.append(flow.cat((gxy-gij,gwh),1))#box anch.append(anchors[a])#anchors对应的所有anchors tcls.append(c)#class returntcls,tbox,indices,anch
6.3 __call__函数
这个函数相当于 forward 函数,在这个函数中进行损失函数的前向传播。
def__call__(self,p,targets):#predictions,targets """ 这里通过输入 p:list([16,3,80,80,85],[16,3,40,40,85],[16,3,20,20,85]) targets:targets.shape[314,6] 为例解析__call__函数 :paramsp:预测框由模型构建中的Detect层返回的三个yolo层的输出(注意是训练模式才返回三个yolo层的输出) tensor格式list列表存放三个tensor对应的是三个yolo层的输出 如:([16,3,80,80,85],[16,3,40,40,85],[16,3,20,20,85]) [bs,anchor_num,grid_h,grid_w,xywh+class+classes] 可以看出来这里的预测值p是三个yolo层每个grid_cell 的预测值(每个grid_cell有三个预测值),后面要进行正样本筛选 :paramstargets:数据增强后的真实框[314,6][num_object,batch_index+class+xywh] :paramsloss*bs:整个batch的总损失(一个列表)进行反向传播 :paramsflow.cat((lbox,lobj,lcls,loss)).detach(): 回归损失、置信度损失、分类损失和总损失这个参数只用来可视化参数或保存信息 """ #初始化各个部分损失始化lcls,lbox,lobj三种损失值tensor([0.]) #lcls.shape=[1] lcls=flow.zeros(1,device=self.device)#classloss #lbox.shape=[1] lbox=flow.zeros(1,device=self.device)#boxloss #lobj.shape=[1] lobj=flow.zeros(1,device=self.device)#objectloss #获得标签分类,边框,索引,anchors #每一个都是列表,有featuremap个 #都是当前这个featuremap中3个anchor筛选出的所有的target(3个grid_cell进行预测) #tcls:表示这个target所属的classindex #tbox:xywh其中xy为这个target对当前grid_cell左上角的偏移量 #indices:b:表示这个target属于的imageindex #a:表示这个target使用的anchorindex #gj:经过筛选后确定某个target在某个网格中进行预测(计算损失) #gj表示这个网格的左上角y坐标 #gi:表示这个网格的左上角x坐标 #anch:表示这个target所使用anchor的尺度(相对于这个featuremap) #可能一个target会使用大小不同anchor进行计算 """shape p:list([16,3,80,80,85],[16,3,40,40,85],[16,3,20,20,85]) targets:[314,6] tcls:list([1659],[1625],[921]) tbox:list([1659,4],[1625,4],[921,4]) indices:list(list([1659],[1659],[1659],[1659]),list([1625],[1625],[1625],[1625]),list([921],[921],[921],[921])) anchors:list([1659,2],[1625,2],[921,2]) """ tcls,tbox,indices,anchors=self.build_targets(p,targets)#targets #Losses依次遍历三个featuremap的预测输出pi fori,piinenumerate(p):#layerindex,layerpredictions #这里通过pi形状为[16,3,80,80,85]进行解析 """shape b:[1659] a:[1659] gj:[1659] gi:[1659] """ b,a,gj,gi=indices[i]#image,anchor,gridy,gridx #tobj=flow.zeros(pi.shape[:4],dtype=pi.dtype,device=self.device)#targetobj #初始化target置信度(先全是负样本后面再筛选正样本赋值) #tobj.shape=[16,3,80,80] tobj=flow.zeros((pi.shape[:4]),dtype=pi.dtype,device=self.device)#targetobj #n=1659 n=b.shape[0]#numberoftargets ifn: #精确得到第b张图片的第a个featuremap的grid_cell(gi,gj)对应的预测值 #用这个预测值与我们筛选的这个grid_cell的真实框进行预测(计算损失) #pxy,pwh,_,pcls=pi[b,a,gj,gi].tensor_split((2,4,5),dim=1) """shape pxy:[1659,2] pwh:[1659,2] _:[1659,1] pcls:[1659,80] """ pxy,pwh,_,pcls=pi[b,a,gj,gi].split((2,2,1,self.nc),1)#target-subsetofpredictions #Regressionloss只计算所有正样本的回归损失 #新的公式:pxy=[-0.5+cx,1.5+cx]pwh=[0,4pw]这个区域内都是正样本 #Getmorepositivesamples,accelerateconvergenceandbemorestable #pxy.shape=[1659,2] pxy=pxy.sigmoid()*2-0.5 #https://github.com/ultralytics/yolov3/issues/168 #pwh.shape=[1659,2] pwh=(pwh.sigmoid()*2)**2*anchors[i]#和论文里不同这里是作者自己提出的公式 #pbox.shape=[1659,4] pbox=flow.cat((pxy,pwh),1)#predictedbox #这里的tbox[i]中的xy是这个target对当前grid_cell左上角的偏移量[0,1]而pbox.T是一个归一化的值 #就是要用这种方式训练传回loss修改梯度让pbox越来越接近tbox(偏移量) #iou.shape=[1659] iou=bbox_iou(pbox,tbox[i],CIoU=True).squeeze()#iou(prediction,target) #lbox.shape=[1] lbox=lbox+(1.0-iou).mean()#iouloss #Objectness #iou.detach()不会更新iou梯度iou并不是反向传播的参数所以不需要反向传播梯度信息 #iou.shape=[1659] iou=iou.detach().clamp(0).type(tobj.dtype) #这里对iou进行排序再做一个优化:当一个正样本出现多个GT的情况也就是同一个grid中有两个gt(密集型且形状差不多物体) #TheremaybeseveralGTsmatchthesameanchorwhencalculateComputeLossinthescenewithdensetargets ifself.sort_obj_iou: #https://github.com/ultralytics/yolov5/issues/3605 #TheremaybeseveralGTsmatchthesameanchorwhencalculateComputeLossinthescenewithdensetargets j=iou.argsort() #如果同一个grid出现两个GT那么经过排序之后每个grid中的score_iou都能保证是最大的 #(小的会被覆盖因为同一个grid坐标肯定相同)那么从时间顺序的话,最后一个总是和最大的iou去计算loss b,a,gj,gi,iou=b[j],a[j],gj[j],gi[j],iou[j] #预测信息有置信度但是真实框信息是没有置信度的所以需要我们人为的给一个标准置信度 #self.gr是iouratio[0,1]self.gr越大置信度越接近iouself.gr越小置信度越接近1(人为加大训练难度) ifself.gr< 1: iou = (1.0 - self.gr) + self.gr * iou tobj[b, a, gj, gi] = iou # iou ratio # Classification 只计算所有正样本的分类损失 # self.nc = 80 if self.nc >1:#clsloss(onlyifmultipleclasses) #targets原本负样本是0这里使用smoothlabel就是cn #t.shape=[1659,80] t=flow.full_like(pcls,self.cn,device=self.device)#targets #t[range(n),tcls[i]]=self.cp筛选到的正样本对应位置值是cp t[flow.arange(n,device=self.device),tcls[i]]=self.cp #lcls.shape=[1] lcls=lcls+self.BCEcls(pcls,t)#BCE #Appendtargetstotextfile #withopen('targets.txt','a')asfile: #[file.write('%11.5g'*4%tuple(x)+' ')forxinflow.cat((txy[i],twh[i]),1)] #置信度损失是用所有样本(正样本+负样本)一起计算损失的 obji=self.BCEobj(pi[...,4],tobj) #每个featuremap的置信度损失权重不同要乘以相应的权重系数self.balance[i] #一般来说,检测小物体的难度大一点,所以会增加大特征图的损失系数,让模型更加侧重小物体的检测 lobj=lobj+(obji*self.balance[i])#objloss ifself.autobalance: #自动更新各个featuremap的置信度损失系数 self.balance[i]=self.balance[i]*0.9999+0.0001/obji.detach().item() ifself.autobalance: self.balance=[x/self.balance[self.ssi]forxinself.balance] #根据超参中的损失权重参数对各个损失进行平衡防止总损失被某个损失主导 """shape lbox:[1] lobj:[1] lcls:[1] """ lbox*=self.hyp["box"] lobj*=self.hyp["obj"] lcls*=self.hyp["cls"] bs=tobj.shape[0]#batchsize #loss=lbox+lobj+lcls平均每张图片的总损失 #loss*bs:整个batch的总损失 #.detach()利用损失值进行反向传播 return(lbox+lobj+lcls)*bs,flow.cat((lbox,lobj,lcls)).detach()
使用:
train.py初始化损失函数类:
compute_loss = ComputeLoss(model) # init loss class
调用执行损失函数,计算损失:
loss, loss_items = compute_loss(pred, targets.to(device)) # loss scaled by batch_size
总结
我们认为 yolov5/one-yolov5 工程实现最重要的就是 ComputeLoss 类了。但代码其实还是非常难的,尤其 build_target 里面花里胡哨的矩阵操作和slice操作非常多, pytorch或者oneflow不熟的人会看的比较痛苦,但是如果你坚持看下来我们的注释再加上自己的冥想,应该是能想明白的。
审核编辑:刘清
-
predator
+关注
关注
0文章
4浏览量
3861
原文标题:《YOLOv5全面解析教程》十二,Loss 计算详细解析
文章出处:【微信号:GiantPandaCV,微信公众号:GiantPandaCV】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论