轉發:https://blog.csdn.net/WYXHAHAHA123/article/details/87915098
在物體檢測問題中,主要分為兩類檢測器模型:one stage detector(SSD,YOLO系列,retinanet)和two stage detector(faster RCNN系列及其改進模型),然而無論是一個階段的檢測器還是兩個階段的檢測器,都使用到了anchor機制,即在特征圖上密集地畫anchor boxes,根據先驗知識設定的IOU閾值將這些anchor划分為正樣本和負樣本,再對於正樣本anchor boxes進行位置編碼,從而得到訓練檢測器所需要的ground truth label,其中存在很大的問題在於,
(1)前景anchor和背景anchor類別不平衡:在數量巨大的anchor boxes中,僅僅有少部分的anchor boxes是正樣本(前景anchor),大量的anchor boxes是負樣本,故而進行classification時會存在嚴重的類別不平衡問題(前景和背景anchor的類別不平衡)。對此,focal loss采用的方法是在各個類別的loss值之前加上權重,而
(2)負樣本anchor太多,如何訓練?
focal loss是通過在loss前面加上系數實現的,它能夠自動地把更多注意力關注到分類錯誤的前景anchor和背景anchor上去,OHEM是通過對於所有負樣本的classification loss值由大到小排序,取出前面loss較大的損失值(即分類錯誤程度較大的負樣本)。
anchor機制存在的問題 | focal loss | OHEM |
前景背景類別不平衡 | 留下所有的正負樣本,在每個類別的loss前加權重系數alpha | 設定閾值確定anchor正負樣本后,正樣本全部保留,按照正負樣本1:3比例采樣出loss值排序最大的負樣本 |
如何讓optimizer更多關注分類錯誤的樣本 | 在所有正樣本前加上權重,權重數值與(1-pt)正相關,即分類錯誤的概率值越接近ground truth,則權重值越小 | 只能讓optimizer關注更多困難負樣本,即對於所有負樣本的classification loss值由大到小排序,取出前面loss較大的損失值(即分類錯誤程度較大的負樣本) |
一、focal loss原理及代碼實現
kaiming大神的focal loss
以二分類的cross entropy為例:loss=-y*log(p)-(1-y)*log(1-p),當ground truth label=0時,loss=-log(1-p),最小化loss值即最大化1-p,則希望p接近於0,當ground truth label=1時,loss=-log(p),最小化loss值即最大化p,則希望p接近於1.
1.focal loss解決class imbalanced問題(alpha)
類別不平衡問題好像現在只能加權重了。代碼里面比較好的計算方式是在每個batch size進行訓練的時候,自動地統計當前batch size中每個類別的樣本數(由於它只想解決正負樣本的不平衡,並沒有涉及到所有正樣本前景類別的不平衡,故而可以動態地統計每個batch size內的正負樣本數),並計算每個類別的頻率,然后以一定的函數關系式取反比例,得到每個類別的權重(alpha)。
其實很奇怪,focal loss做了很多實驗都是設定固定的alpha值來判斷效果好,難道不是動態地在每個batch size內的正負樣本數,然后再取反比例嗎?我之前做過一個multi-class segmentation ,師兄說他是在每個mini-batch內部動態計算頻率的,他說動態計算和對於整個數據集事先訓練好區別不大,但是總歸是要基於數據集本身決定各個類別權值的。作者經過實驗觀察得到,alpha=0.75時得到的效果最好,也就是(所有類別的)前景正樣本anchor classification loss權重為0.75,而負樣本的權重為0.25.感覺這莫名地和OHEM中的選擇正負樣本比例1:3不謀而合了。
2.focal loss解決訓練hard example的問題(gamma)
當p_t接近於0的時候,則-log(pt)接近於無窮大,說明當前樣本的classification loss很大,應該花更多的注意力在這里,則(1-p_t)接近於1,則分類嚴重錯誤,是hard example困難樣本,則會給當前的分類損失值賦予較大的權重;當p_t接近於1的時候,則-log(pt)接近於0,說明當前樣本的classification loss很小,分類正確,是easy example簡單樣本,則(1-p_t)接近於0,會給當前的分類損失值賦予較小的權重,因為簡單樣本很容易分類正確所以不需要關注太多。
由於我的數據集中存在每個前景類別的不平衡,而在每個batch size中又不能保證每個類別的ground truth boxes都出現,所以決定事先統計出每個類別的ground truth boxes在原始數據集中的頻率,然后計算出每個類別的權重,最后把所有前景類別的權重值加起來除以3,就得到背景類別的權重。
-
#coding=gbk
-
import torch
-
import torch.nn
as nn
-
import torch.nn.functional
as F
-
from torch.autograd
import Variable
-
import numpy
as np
-
-
def compute_class_weights(histogram):
-
classWeights = np.ones(histogram.shape[
0], dtype=np.float32)
-
# print(classWeights.shape)
-
normHist = histogram / np.sum(histogram)
-
for i
in range(histogram.shape[
0]):
-
classWeights[i] =
1 / (np.log(
1.10 + normHist[i]))
-
return classWeights
-
-
class FocalLoss(nn.Module):
-
def __init__(self):
-
super(FocalLoss, self).__init__()
-
def forward(self, cls_prob,rois_label):
-
'''
-
:param cls_prob:經過softmax激活函數作用后的,shape [128,8] num_class=8
-
:param targets:ground truth class類別
-
:return:focal loss classification loss
-
'''
-
proposal_num=cls_prob.shape[
0]
-
num_class=cls_prob.shape[
1]
-
-
class_mask=cls_prob.data.new(proposal_num,num_class)
-
class_mask = Variable(class_mask)
-
ids=rois_label.view(
-1,
1)
-
class_mask.scatter_(
1, ids.data,
1.)
-
'''
-
class_mask shape [proposal_num,num_class]
-
表示對於ids進行one-hot編碼,對應類別的概率值為1
-
'''
-
#alpha 是用來解決物體檢測中類別不平衡問題的
-
-
frequency=torch.zeros(num_class,
1)
-
for ij
in range(torch.max(rois_label)+
1):
-
frequency[ij]=torch.sum(rois_label==ij)
-
frequency=frequency.numpy()
-
classWeights=compute_class_weights(frequency)
-
alpha=Variable(torch.from_numpy(classWeights).view(
-1,
1)).cuda()
#shape [num_class,]
-
alpha_class=alpha[ids.view(
-1)]
-
gamma =
2
-
-
probs=(cls_prob*class_mask).sum(
1).view(
-1,
1)
-
-
log_p=probs.log()
-
-
batch_loss = -alpha_class * (torch.pow((
1 - probs), self.gamma)) * log_p
-
loss = batch_loss.mean()
-
return loss
二、OHEM原理及代碼實現
OHEM(在線困難樣本挖掘),通常是 on line hard negative mining,對於困難的負樣本進行在線挖掘,它與focal loss的目標一樣,都是為了處理物體檢測問題中的類別不均衡問題.這里的類別不均衡,是指由於基於anchor 的檢測器都是使用密集檢測的策略,如RPN以及所有的one-stage detector,則在數量眾多的anchor中有大量的anchor框是負樣本——背景框,只有少量的anchor框是正樣本,如果不使用任何的策略,直接將所有的anchor直接計算cross entropy的classification loss,則由於很多背景anchor的特征對應到輸入圖像上的感受野部分就是背景,故而是很容易被分類出來的,這一類很容易被分類正確的負樣本被稱為簡單負樣本(對應到代碼里面就是那些分類損失值比較小的負樣本anchor),在所有的負樣本anchor中,簡單負樣本的數量占據了絕大多數,故而網絡最終訓練好了,分類損失函數值下降,可能就是由於將大量簡單負樣本分類正確所導致的分類損失函數值小,但這並不是訓練detector的目標,我們的目標是檢測器能正確區分正負樣本並且能夠對於所有的正樣本進行多個類別的正確划分,故而需要挖掘出那些困難的負樣本(其實困難的負樣本可以理解為就是和前景ground truth boxes的IOU數值比較大的,但是又沒有超過所設定的正樣本閾值的那部分anchor boxes),簡單負樣本就是那些與前景ground truth boxes的IOU數值較小的,很容易被分類成背景anchor的負樣本。所以on line hard example mining是對於負樣本進行的,通常的做法是,先對於所有的anchor boxes計算出分類損失值(size_average=False),也就是計算出每個anchor boxes所對應的classification loss,然后將所有正樣本的classification loss值取出,將正樣本anchor boxes的數量記作pos_num,再對於剩下的所有負樣本anchor boxes的classification loss進行從大到小的排序,最后從排序好的負樣本anchor boxes的loss中取出前3*num_pos個classification loss,再與所有的正樣本分類損失值相加,最終除以的分母是num_pos。
SSD訓練過程中這里引用torchcv中的ssd_loss.py代碼進行解釋:
-
from __future__
import print_function
-
-
import torch
-
import torch.nn
as nn
-
import torch.nn.functional
as F
-
-
-
class SSDLoss(nn.Module):
#損失函數
-
def __init__(self, num_classes):
-
super(SSDLoss, self).__init__()
-
self.num_classes = num_classes
#類別總數,對於VOC數據集而言,是21類
-
-
def _hard_negative_mining(self, cls_loss, pos):
-
'''Return negative indices that is 3x the number as postive indices.
-
-
Args:
-
cls_loss: (tensor) cross entroy loss between cls_preds and cls_targets, sized [N,#anchors]. 分類損失值
-
pos: (tensor) positive class mask, sized [N,#anchors].
-
-
Return:
-
(tensor) negative indices, sized [N,#anchors].
-
'''
-
cls_loss = cls_loss * (pos.float() -
1)
-
#對於正樣本,損失值為0,得到對於負樣本計算出的損失值,損失值越大的負樣本,cls_loss值越小
-
#正樣本損失值 0
-
#負樣本損失值=之前的負樣本損失值*(-1)
-
#這是因為_hard_negative_mining只返回所有的負樣本classification loss
-
#從所有的負樣本中采樣出前(3*num_positive)個負樣本的loss
-
#這些負樣本的classification loss最大,是困難的負樣本
-
-
_, idx = cls_loss.sort(
1)
# sort by negative losses
-
'''
-
cls_loss: [N,#anchors] 正樣本的損失值為0,對於負樣本,損失值越大,cls_loss越小
-
tensor.sort方法返回sort之后的按升序排列的tensor和對應的indices
-
對每一行,遍歷所有的列,則得到的每一行按照升序排列,即對於每個input images,得到其按照升序排列的分類損失idx
-
-
idx同樣是[N,#anchors].的tensor,其中的每一行的值范圍為 [0,1,2,……,8732]
-
表示當前input image 的所有anchors的負樣本的分類損失 由大到小的索引排序
-
-
'''
-
_, rank = idx.sort(
1)
# [N,#anchors]
-
-
num_neg =
3*pos.sum(
1)
# [N,]
-
#num_neg為長度為batch size 的tensor,其中的每個元素表示3*當前input image中的正樣本個數
-
-
neg = rank < num_neg[:,
None]
# [N,#anchors] neg中的數值為1或者0 如果是hard negative examples,則對應位置處的值為1
-
-
'''
-
對於當前batch size張圖像中的每一張(每一張圖像中的正樣本不同)
-
找到是當前圖像中正樣本數量3倍的負樣本,並且固定數量的負樣本是通過在線困難樣本挖掘得到的
-
這主要是為了解決計算分類損失函數時樣本不均衡的問題,因為比如說SSD300這種模型中8732個default boxes
-
中的正樣本數量很少(與ground truth 的overlap大於0.5,在box_coder.encode函數中設置)
-
為了保證在同一張圖像中的正負樣本比例在1:3,故而使用在線困難樣本挖掘(在線指的是在訓練過程中,這意味着
-
在每次訓練過程中,每次挖掘到的困難負樣本可能是不同的,要根據網絡模型預測的輸出值決定)
-
算法如下:
-
首先取出所有的負樣本,對於當前batch_size*#anchors ,對於每一行(每張訓練圖像)的分類損失值進行排序
-
按照當前圖像中正樣本的數量的3倍取出loss值排在前面的負樣本)
-
負樣本的分類損失值計算:np.log(p) 小 p小,就是說對於負樣本預測為背景類的概率值小,就是預測為前景的概率值大
-
這些是很容易被分類錯的負樣本,被稱為困難負樣本,這些樣本的loss值很大,對於網絡模型的參數更新非常有效
-
而那些很容易就能被分類正確的負樣本對於最終權值更新效果不大,故而舍棄
-
'''
-
return neg
-
-
def forward(self, loc_preds, loc_targets, cls_preds, cls_targets):
-
'''Compute loss between (loc_preds, loc_targets) and (cls_preds, cls_targets).
-
計算分類損失和回歸損失
-
-
Args:
-
loc_preds: (tensor) predicted locations, sized [N, #anchors, 4].
-
對於當前batch size的圖像所預測出來的localization
-
N=batch_size
-
#anchors表示default boxes的數量
-
loc_targets: (tensor) encoded target locations, sized [N, #anchors, 4].
-
cls_preds: (tensor) predicted class confidences, sized [N, #anchors, #classes].
-
對於當前batch size的圖像所預測出來的classification
-
N=batch_size,#anchors表示default boxe數量,
-
#classes表示數據集類別總數
-
cls_targets: (tensor) encoded target labels, sized [N, #anchors].
-
batch_size行,#anchors列,
-
第i行第j列的元素表示
-
對於第i個訓練樣本圖像,SSD預測出來的第j個default boxes的GT類別標號(一個int類型整數)
-
-
loss:
-
(tensor) loss = SmoothL1Loss(loc_preds, loc_targets) + CrossEntropyLoss(cls_preds, cls_targets).
-
位置回歸損失 交叉熵分類損失
-
'''
-
pos = cls_targets >
0
# [N,#anchors] pos中的數值是 0 1
-
'''
-
cls_targets是經過編碼之后的classification ground truth
-
表示與ground truth bounding boxes的IOU值最大或者大於一定的閾值的anchor boxes則會被認為是正樣本,為1
-
負樣本為-1
-
-
在encoder階段,會計算出當前anchor 與當前輸入圖像中所有ground truth boxes的IOU,並將anchor與所有gt boxes
-
最大的IOU值記作當前anchor的overlap值,如果anchor的overlap值大於閾值0.5,則將anchor記作為正樣本
-
IOU小於0.5為負樣本
-
'''
-
-
batch_size = pos.size(
0)
#每個batch 中包含多少張訓練圖片
-
num_pos = pos.sum().item()
#對pos 2-Dtensor求和,得到當前batch size的訓練圖片中共有多少個anchor boxes為正樣本
-
#當前batch size 數量的輸入圖像中,positive examples(這里的正樣本指的是default boxes而不是一整張圖像)的數量
-
-
#===============================================================
-
# loc_loss = SmoothL1Loss(pos_loc_preds, pos_loc_targets)
-
#===============================================================
-
mask = pos.unsqueeze(
2).expand_as(loc_preds)
# [N,#anchors,4]
-
loc_loss = F.smooth_l1_loss(loc_preds[mask], loc_targets[mask], size_average=
False)
#只對正樣本進行回歸損失的計算
-
#mask是# [N,#anchors,4]的3-dimension tensor,擴展的第2維度與之前的數值相同,即對於正樣例(batch size中的第i幅圖片中的第j個anchors)
-
#mask[i,j,:]=1,如果為負樣本則mask[i,j,:]=0
-
#mask作下標則表示其中元素值為1的下標,即所有的正樣本所在的下標(4)
-
-
#===============================================================
-
# cls_loss = CrossEntropyLoss(cls_preds, cls_targets)
-
#===============================================================
-
cls_loss = F.cross_entropy(cls_preds.view(
-1,self.num_classes), \
-
cls_targets.view(
-1), reduce=
False)
# [N*#anchors*num_classes,]
-
'''
-
cls_preds:[N,#anchors,num_classes] view cls_preds:[(N*#anchors),num_classes]
-
cls_targets:[N*#anchors,]
-
計算多分類的交叉損失函數是cross_entropy,reduce參數為false,則返回值cls_loss維度為(N*#anchors)
-
分別給出了這一個batch size中每張圖像所有anchor boxes的分類損失值得
-
'''
-
cls_loss = cls_loss.view(batch_size,
-1)
#cls_loss:[N,#anchors]
-
cls_loss[cls_targets<
0] =
0
# set ignored loss to 0 現將所有負樣本的分類損失變成0,這是為了使用hard negative mining算法挑選出困難負樣本
-
neg = self._hard_negative_mining(cls_loss, pos)
# [N,#anchors]
-
cls_loss = cls_loss[pos|neg].sum()
-
'''
-
正樣本具有分類損失和回歸損失,SSD中的正樣本包括最大的IOU和IOU值大於0.5的region proposal
-
一般的負樣本沒有分類損失,也沒有回歸損失
-
hard negative examples具有分類損失,不具有回歸損失
-
實際上訓練時采用的正負樣本是所有的正樣本和所有的hard negative examples,
-
'''
-
-
print(
'loc_loss: %.3f | cls_loss: %.3f' % (loc_loss.item()/num_pos, cls_loss.item()/num_pos), end=
' | ')
-
loss = (loc_loss+cls_loss)/num_pos
-
return loss
三、OHEM應用到faster RCNN
如何將OHEM(on line hard example/negative mining)用到faster RCNN中?
可以在原始的faster RCNN代碼實現中加入了OHEM,值得注意的是,OHEM是在計算RPN的classification loss時使用的,在計算RCNN的classification loss使用的是全部的2000個region proposal。原始的faster RCNN代碼中並沒有加入困難樣本挖掘,而是:對於所有的anchor boxes,IOU大於0.7為正樣本 ,IOU小於0.3為負樣本,然后隨機在一個batch size的輸入圖像中采樣出128個正樣本和128個負樣本(比例1:1),這里並沒有使用困難樣本挖掘,因為IOU小於0.3很大概率是簡單負樣本。
個人感覺OHEM比較適合false positive 很多的情況,這種就是把背景框划分為前景框了,對於背景框的分類不准確,這是由於訓練負樣本時都是使用的簡單負樣本的原因,需要加入更多的困難負樣本進行訓練。
四、focal loss應用到faster RCNN
由於在現在的實驗中,RPN部分的classification loss效果比較好,就是說前景背景二分類准確率在95%左右,故而這里我不在加入任何策略(是用最原始的策略,IOU大於0.7正樣本,IOU小於0.3是負樣本),故而在RCNN部分的classification loss使用了focal loss。