其中一些常見的損失函數包括:
分類損失(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]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%11)).T #篩選中心坐標距離當前grid_cell的右、下方偏移小于g=0.5且中心坐標必須大于1(坐標不能在邊上此時就沒有4個格子了) #l:[555]bool如果是True表示當前target中心點所在的格子的右邊格子也對該target進行回歸(后續進行計算損失) #m:[555]bool如果是True表示當前target中心點所在的格子的下邊格子也對該target進行回歸(后續進行計算損失) l,m=((gxi%11)).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.gr1: ????????????????????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瀏覽量
3864
原文標題:《YOLOv5全面解析教程》?十二,Loss 計算詳細解析
文章出處:【微信號:GiantPandaCV,微信公眾號:GiantPandaCV】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論