GHM解读
收錄于AAAI2019 oral的一篇文章,主要是解決目標檢測中的樣本不平衡問題,包括正負樣本不平衡、難易樣本不平衡,和著名的Focal Loss類似,也是基于交叉熵做的改進。此外,本文成文過程參考了知乎上一個精簡的分析,歡迎訪問。
簡介
單階段跟蹤以一個更優雅的方式對待目標檢測問題,然而它也存在困擾已久的問題,那就是樣本的不均衡從而導致模型訓練效果不好,這包括正負樣本的不平衡和難易樣本的不平衡。這兩種不平衡本質上都可以從梯度的層面解釋,因此作者提出了GHM(梯度調和機制, gradient harmonizing mechanism)來處理這一現象,以此為基礎構建的GHM分類損失(GHM-C)和GHM回歸損失(GHM-R)可以輕松嵌入到如交叉熵的分類損失和如Smooth L1的回歸損失中,這兩種損失分別用于anchor的分類和邊界框的修正,實驗表明,在不經過費力挑戰超參數的前提下,GHM-C和GHM-R可以為單階段檢測器帶來實質性的改進,并且超過了使用Focal Loss和Smooth L1的SOTA方法。
-  
論文標題
Gradient Harmonized Single-stage Detector
 -  
論文地址
https://arxiv.org/abs/1811.05181
 -  
論文源碼
https://github.com/libuyu/GHM_Detection
 
介紹
單階段檢測的出現為目標檢測帶來了一種更加高效且優雅的范式,但是單階段方法和二階段方法的精度還是存在不小的差距的。造成這種差距的主要原因之一就是單階段檢測器的訓練存在正負樣本和難易樣本的不平衡問題,這里需要注意的是,正負樣本和難易樣本是兩回事,因此存在正易、正難、負易和負難四種樣本。 大量的簡單樣本和背景樣本使得模型的訓練不堪重負,但是二階段檢測則不存在這個問題,因為其是基于proposal篩選的。
針對這個問題,OHEM是一個基于難樣本挖掘的方法被廣泛使用,但是這種方法直接放棄了大多數樣本并且使得訓練的效率不高。后來,Focal Loss橫空出世,它通過修改經典的交叉熵損失為一個精心設計的形式來解決這個問題。但是,Focal Loss采用了兩個超參數,這兩個參數需要花費大量的精力對其進行調整,并且Focal Loss是一種靜態損失,它不能自適應于數據分布的變化,即隨訓練過程而改變。
這篇論文中,作者指出類別不平衡最終可以歸為梯度范數分布的不平衡。如果一個正樣本被很好地分類,那么它是一個簡單樣本,模型從中受益不多,換句話說,該樣本產生的梯度很小。而錯誤分類的樣本無論屬于哪個類都應該受到模型足夠的關注。因此,從全局來看,大量的負樣本是容易分類的并且難樣本往往是正樣本。因此這兩種不平衡可以粗略概括為屬性不平衡。
作者認為梯度范數的分布可以隱式表達不同屬性的樣本的不平衡。相對于梯度范數的樣本密度(下文簡稱梯度密度),梯度密度如上圖的最左側所示,變化幅度是很大的。首先看圖像的最左側,這里的樣本的梯度范數比較小但是密度很大,這表示大量的負易樣本。再看圖像最右側,這里梯度范數大,數量相對于左側的容易樣本少得多,這表示困難樣本。盡管一個容易樣本相對于困難樣本對整體損失的貢獻少,但是架不住數量大啊,大量的容易樣本的貢獻可能超越了少數困難樣本的貢獻,導致訓練效率不高。而且,難樣本的密度比中性樣本的密度稍高。這些非常難的樣本可以視作離群點,因為即使模型收斂,它們依然穩定存在。離群點可能會影響模型的穩定性,因為它們的梯度可能與其他一般樣本有很大差異。
經過上面的梯度范數分布的分析,作者提出了梯度調和機制(GHM)來高效訓練單階段目標檢測器,該策略關注于不同樣本梯度分布的平衡。GHM首先對具有相似屬性但沒有梯度密度的樣本進行統計,然后依據密度將調和參數附加(以乘法的形式)到每個樣本的梯度上。上圖最右側即為平衡后的結果,可以看到,GHM可以大大降權簡單樣本累積的大量梯度同時也可以相對降權離群點梯度,這使得各類樣本的貢獻度平衡且訓練更加穩定高效。
GHM
前人工作
首先,來回顧一下單階段檢測的一個關鍵問題,樣本類型的極度不平衡。對一個候選框而言,p∈[0,1]p \in[0,1]p∈[0,1]表示模型預測的概率分布,p?∈{0,1}p^{*} \in\{0,1\}p?∈{0,1}為一個確定的類真實標簽,那么考慮二分交叉熵的標準形式如下,它通常用于分類任務。
LCE(p,p?)={?log?(p)if?p?=1?log?(1?p)if?p?=0L_{C E}\left(p, p^{*}\right)=\left\{\begin{array}{ll} -\log (p) & \text { if } p^{*}=1 \\ -\log (1-p) & \text { if } p^{*}=0 \end{array}\right. LCE?(p,p?)={?log(p)?log(1?p)??if?p?=1?if?p?=0?
但是,這個形式的交叉熵用于正負樣本分類是不合理的,因為單階段檢測器通常會產生高達100k的目標,能和GT框匹配的正樣本少之又少,因此出現了正負樣本不平衡的問題,為了解決這個問題,下面的加權交叉熵被提出。
LCE(p,p?)={?αlog?(p),if?p?=1?(1?α)log?(1?p),if?p?=0L_{C E}\left(p, p^{*}\right)=\left\{\begin{aligned} -\alpha \log (p), & \text { if } p^{*}=1 \\ -(1-\alpha) \log (1-p), & \text { if } p^{*}=0 \end{aligned}\right. LCE?(p,p?)={?αlog(p),?(1?α)log(1?p),??if?p?=1?if?p?=0?
雖然加權交叉熵平衡了正負樣本,但是就像上文所說的,樣本還有難易之分,目標檢測的候選框其實大量是易分樣本,加權交叉熵對難易樣本的平衡并沒有太大作用。易分樣本雖然損失很低,但是數量極多,梯度的累積造成其主導了優化的方向,顯然這是不合理的。因此,Focal Loss作者認為,易分樣本(置信度高的樣本)對模型效果的提升影響非常小,模型應該主要關注那些難分樣本。這個假設其實是有一些問題的,GHM就是針對次做了探索,下文再細說。
那么,Focal Loss如何平衡難易樣本呢,其實很簡單,把高置信度的簡單樣本的損失再降低不就行了,于是有了下面這個中間結果,通過γ\gammaγ的設置可以有效衰減高置信度樣本的損失值。
FL={?(1?p)γlog?(p),if?p?=1?pγlog?(1?p),if?p?=0F L=\left\{\begin{aligned} -(1-p)^{\gamma} \log (p), & \text { if } p^{*}=1 \\ -p^{\gamma} \log (1-p), & \text { if } p^{*}=0 \end{aligned}\right. FL={?(1?p)γlog(p),?pγlog(1?p),??if?p?=1?if?p?=0?
這個公式再結合上加權交叉熵,不就同時解決了正負樣本不平衡和難易樣本不平衡兩個問題嘛,于是就有了下面這個Focal Loss的常見形式。實驗表明,γ=2\gamma=2γ=2且α=0.25\alpha=0.25α=0.25時效果最佳。
FL={?α(1?p)γlog?(p),if?p?=1?(1?α)pγlog?(1?p),if?p?=0F L=\left\{\begin{array}{ccc} -\alpha(1-p)^{\gamma} \log (p), & \text { if } & p^{*}=1 \\ -(1-\alpha) p^{\gamma} \log (1-p), & \text { if } & p^{*}=0 \end{array}\right. FL={?α(1?p)γlog(p),?(1?α)pγlog(1?p),??if??if??p?=1p?=0?
下面是mmdet實現的pytorch版本的focal loss,這個代碼也比較容易理解,主要對focal loss公式進行了拆分實現。
def py_sigmoid_focal_loss(pred,target,weight=None,gamma=2.0,alpha=0.25,reduction='mean',avg_factor=None):pred_sigmoid = pred.sigmoid()target = target.type_as(pred)pt = (1 - pred_sigmoid) * target + pred_sigmoid * (1 - target)focal_weight = (alpha * target + (1 - alpha) * (1 - target)) * pt.pow(gamma)loss = F.binary_cross_entropy_with_logits(pred, target, reduction='none') * focal_weightif weight is not None:if weight.shape != loss.shape:if weight.size(0) == loss.size(0):weight = weight.view(-1, 1)else:assert weight.numel() == loss.numel()weight = weight.view(loss.size(0), -1)assert weight.ndim == loss.ndimloss = weight_reduce_loss(loss, weight, reduction, avg_factor)returnGHM損失
然而,Focal Loss雖然極大的推動了單階段檢測和anchor-free檢測的發展,但是它也是存在問題的。首先,就是最核心的一個問題:讓模型過分關注特別難分的樣本肯定是不合適的,因為樣本中存在離群點,模型可能已經收斂,但是這些離群點的存在模型仍舊判斷失誤難以正常訓練,即使擬合了這樣的樣本模型也是不合理的。其次,Focal Loss的兩個超參數需要人為設計,且需要聯合調參,因為它倆會互相影響。為了解決這兩個問題,GHM被提了出來,不同于Focal Loss關注于置信度來衰減損失,GHM從一定置信度的樣本量的角度出發衰減損失。
那么GHM是如何做的呢?首先,假定xxx是模型輸出且通過sigmoid激活,那么可以可以得到關于xxx的交叉熵梯度如下,這其實可以視為損失的梯度即為預測和真值的差距。
?LCE?x={p?1if?p?=1pif?p?=0=p?p?\begin{aligned} \frac{\partial L_{C E}}{\partial x} &=\left\{\begin{array}{ll} p-1 & \text { if } p^{*}=1 \\ p & \text { if } p^{*}=0 \end{array}\right.\\ &=p-p^{*} \end{aligned} ?x?LCE???={p?1p??if?p?=1?if?p?=0?=p?p??
利用上式,可以定義梯度的模ggg如下,這個ggg等于相對于xxx的梯度范數,它的值表示樣本的難易屬性(越大樣本越難),同時也隱含了樣本對全局梯度的影響。這里梯度范數的定義數學上并不嚴格,只是作者為了方便的說法。
g=∣?LCE?x∣=∣p?p?∣={1?pif?p?=1pif?p?=0g= \left|\frac{\partial L_{C E}}{\partial x}\right| = \left|p-p^{*}\right|=\left\{\begin{array}{ll} 1-p & \text { if } p^{*}=1 \\ p & \text { if } p^{*}=0 \end{array}\right. g=∣∣∣∣??x?LCE??∣∣∣∣?=∣p?p?∣={1?pp??if?p?=1?if?p?=0?
下圖所示即為一個收斂的單階段模型的ggg分布情況,橫軸表示梯度的模,縱軸表示樣本量比例。很直觀地可以看到,ggg很小的樣本數量非常多,這部分為大量的容易樣本,隨著ggg增加,樣本量迅速減少,但是在ggg接近1的時候,樣本量也不少,這部分屬于困難樣本。需要注意的是,這是一個收斂的模型,也就是說即使模型收斂,這些樣本還是難易區分,它們屬于離群點,如果模型強行擬合這類樣本會導致其他正常樣本分類精度降低。
依此,GHM提出了自己的核心觀點,的確不應該關注那些數量很多的容易樣本,但是非常困難的樣本也是不正常的,也不應該過分關注,而且容易樣本和困難樣本的數量相比于中性樣本都比較多。那么如何同時衰減這兩類樣本呢,其實只要從它們數量多這個角度出發就行,就衰減那些數量多的樣本。那么如何衰減數量多的呢,那就需要定義一個變量,來衡量某個梯度或者某個梯度范圍內的樣本數量,這個概念其實很類似于“密度”這個概念,因此將其稱為梯度密度,這就有了下面這個這篇文章最核心的公式,梯度密度GD(g)GD(g)GD(g)的定義。
GD(g)=1l?(g)∑k=1Nδ?(gk,g)G D(g)=\frac{1}{l_{\epsilon}(g)} \sum_{k=1}^{N} \delta_{\epsilon}\left(g_{k}, g\right) GD(g)=l??(g)1?k=1∑N?δ??(gk?,g)
這個式子需要做一些說明。gkg_kgk?表示第kkk個樣本的梯度的模,δ?(x,y)\delta_{\epsilon}(x, y)δ??(x,y)和l?(g)l_{\epsilon}(g)l??(g)的定義式如下,前者表示xxx是否在yyy的一個鄰域內,在的話則為1否則為0,上式中求和后的含義就是gkg_kgk?在ggg的范圍內的樣本數目,后者則表示計算樣本量的這個鄰域的區間長度,它作為標準化因子。因此,梯度密度GDGDGD的含義可以理解為單位模長在ggg附近的樣本數目。
δ?(x,y)={1if?y??2<=x<y+?20otherwise?\delta_{\epsilon}(x, y)=\left\{\begin{array}{rr} 1 & \text { if } y-\frac{\epsilon}{2}<=x<y+\frac{\epsilon}{2} \\ 0 & \text { otherwise } \end{array}\right. δ??(x,y)={10??if?y?2??<=x<y+2???otherwise??
l?(g)=min?(g+?2,1)?max?(g??2,0)l_{\epsilon}(g)=\min \left(g+\frac{\epsilon}{2}, 1\right)-\max \left(g-\frac{\epsilon}{2}, 0\right) l??(g)=min(g+2??,1)?max(g?2??,0)
有了梯度密度,下面定義最終用在損失調和上的梯度密度調和參數,這里的NNN表示樣本總量,為了方便理解,其可以改寫為βi=1GD(gi)/N\beta_{i}=\frac{1}{G D\left(g_{i}\right) / N}βi?=GD(gi?)/N1?,分母GD(gi)/NG D\left(g_{i}\right) / NGD(gi?)/N表示第i個樣本具有鄰域梯度的所有樣本分數的歸一化器。如果樣本關于梯度均勻分布,對任何的gig_igi?的GD(gi)=NGD(g_i) = NGD(gi?)=N并且每個樣本的βi=1\beta_i = 1βi?=1。相反,具有較大密度的樣本則會被歸一化器降權表示。
βi=NGD(gi)\beta_{i}=\frac{N}{G D\left(g_{i}\right)} βi?=GD(gi?)N?
利用βi\beta_iβi?可以集成到分類損失和回歸損失中從而構建新的損失GHM-C Loss,這里的βi\beta_iβi?則作為第iii個樣本的損失加權,最終梯度密度調和后的分類損失如下式。其實從最后的結果來看,就是原本的交叉熵乘以該樣本梯度密度的倒數。
LGHM?C=1N∑i=1NβiLCE(pi,pi?)=∑i=1NLCE(pi,pi?)GD(gi)\begin{aligned} L_{G H M-C} &=\frac{1}{N} \sum_{i=1}^{N} \beta_{i} L_{C E}\left(p_{i}, p_{i}^{*}\right) \\ &=\sum_{i=1}^{N} \frac{L_{C E}\left(p_{i}, p_{i}^{*}\right)}{G D\left(g_{i}\right)} \end{aligned} LGHM?C??=N1?i=1∑N?βi?LCE?(pi?,pi??)=i=1∑N?GD(gi?)LCE?(pi?,pi??)??
使用GHM-C和交叉熵以及Focal Loss的梯度范數調整效果如下圖,顯然,GHM-C的抑制更加明顯合理。
這里作者還提到了EMA處理的技巧,此外魔改smooth l1可以得到下面的GHM-R損失函數,這里就不展開了。
LGHM?R=1N∑i=1NβiASL1(di)=∑i=1NASL1(di)GD(gri)\begin{aligned} L_{G H M-R} &=\frac{1}{N} \sum_{i=1}^{N} \beta_{i} A S L_{1}\left(d_{i}\right) \\ &=\sum_{i=1}^{N} \frac{A S L_{1}\left(d_{i}\right)}{G D\left(g r_{i}\right)} \end{aligned} LGHM?R??=N1?i=1∑N?βi?ASL1?(di?)=i=1∑N?GD(gri?)ASL1?(di?)??
最后,補充一個mmdet關于GHM Loss的實現,它是先將梯度模長劃分為bins個范圍,然后計算每個區域的便捷edges,接著就很容易判斷梯度模長落在哪個區間里了,然后按照公式計算權重乘以原來的交叉熵損失即可(實現思路和上面的Focal Loss差不多,都是對原本的交叉熵加權)。
class GHMC(nn.Module):def __init__(self, bins=10, momentum=0, use_sigmoid=True, loss_weight=1.0):super(GHMC, self).__init__()self.bins = binsself.momentum = momentumedges = torch.arange(bins + 1).float() / binsself.register_buffer('edges', edges)self.edges[-1] += 1e-6if momentum > 0:acc_sum = torch.zeros(bins)self.register_buffer('acc_sum', acc_sum)self.use_sigmoid = use_sigmoidif not self.use_sigmoid:raise NotImplementedErrorself.loss_weight = loss_weightdef forward(self, pred, target, label_weight, *args, **kwargs):# the target should be binary class labelif pred.dim() != target.dim():target, label_weight = _expand_onehot_labels(target, label_weight, pred.size(-1))target, label_weight = target.float(), label_weight.float()edges = self.edgesmmt = self.momentumweights = torch.zeros_like(pred)# gradient lengthg = torch.abs(pred.sigmoid().detach() - target)valid = label_weight > 0tot = max(valid.float().sum().item(), 1.0)n = 0 # n valid binsfor i in range(self.bins):inds = (g >= edges[i]) & (g < edges[i + 1]) & validnum_in_bin = inds.sum().item()if num_in_bin > 0:if mmt > 0:self.acc_sum[i] = mmt * self.acc_sum[i] \+ (1 - mmt) * num_in_binweights[inds] = tot / self.acc_sum[i]else:weights[inds] = tot / num_in_binn += 1if n > 0:weights = weights / nloss = F.binary_cross_entropy_with_logits(pred, target, weights, reduction='sum') / totreturn loss * self.loss_weight實驗
實驗配置等細節查看原文即可,我這里就給出SOTA方法的漲點結果圖,可以看到,相比于Focal Loss,漲點效果還是挺明顯的。
總結
這篇論文針對單階段目標檢測的樣本不均衡問題從梯度出發,提出了GHM這一策略,從損失的角度有效改善了此前的Focal Loss。本文也只是我本人從自身出發對這篇文章進行的解讀,想要更詳細理解的強烈推薦閱讀原論文。最后,如果我的文章對你有所幫助,歡迎一鍵三連,你的支持是我不懈創作的動力。
總結
                            
                        - 上一篇: CorrTracker解读
 - 下一篇: CSTrackV2解读