該方法的缺點是只能適用於最後一層特徵圖和全連接之間是GAP操作。如果不是,就需要用戶修改網絡並重新訓練(或 fine-tune)。所以對本文簡單概括即為:修改網絡全連接為GAP形式,利用GAP層與全連接的權重作為特徵融合權重,對特徵圖進行線性融合獲取CAM。
# 獲取全連接層的權重self._fc_weights = self.model._modules.get(fc_layer).weight.data# 獲取目標類別的權重作為特徵權重weights=self._fc_weights[class_idx, :]# 這裡self.hook_a為最後一層特徵圖的輸出batch_cams = (weights.unsqueeze(-1).unsqueeze(-1) * self.hook_a.squeeze(0)).sum(dim=0)# relu操作,去除負值batch_cams = F.relu(batch_cams, inplace=True)# 歸一化操作batch_cams = self._normalize(batch_cams)更通用的做法:Grad-CAM「論文標題」 : Grad-CAM: Visual Explanations from Deep Networks via Gradient-based Localization
「論文地址」 : https://arxiv.org/pdf/1610.02391.pdf
上文的局限性就是網絡架構裡必須有GAP層,但並不是所有模型都配GAP層的。而本文就是為克服該缺陷提出的,其基本思路是目標特徵圖的融合權重 可以表達為梯度。另外,因為熱圖關心的是對分類有正面影響的特徵,所以加上了relu以移除負值。其實並不一定要是分類問題,只要是可求導的激活函數,在其他問題也一樣使用Grad-CAM。特徵融合權重計算公式如下:
其中 為目標類別的score,後面會論述該score具體是什麼。 為需要可視化的目標特徵圖。論文裡證明了grad-CAM是上文利用GAP一般形式,這裡不再複述,具體可看論文附錄。這裡提出兩個問題並進行論述。
「A. grad-CAM和 CAM(這裡指上一篇論文的做法)的區別?」
CAM 只能用於最後一層特徵圖和輸出之間是GAP的操作,grad-CAM可適用非GAP連接的網絡結構;CAM只能提取最後一層特徵圖的熱力圖,而gard-CAM可以提取任意一層;「B. 目標類別score 是用通過softmax之後的還是之前的?」
論文原文中目標類別score是指網絡未經過softmax的得分,但是某些代碼實現當中也使用了通過softmax後的。二者有無區別?下面我們通過公式推導一下。因為這兩種做法僅僅相差一個softmax,所以對softmax的求導。假設softmax層有C個輸出,記為: 。則softmax輸出為:
則輸出 對 的偏導為,由於這裡計算的是目標類別,所以僅需計算對應輸出的偏導:
所以特徵融合的權重則變成:
可以看出,二者的梯度差異就是softmax輸出的多一項 ,而對於已經訓練好的網絡,該項為定值。後面特徵層進行加權求和,再歸一化後,該項 會被消掉。所以通過softmax和不通過softmax在理論上完全一致。但是在實際應用中我發現,加上softmax後,權重 會變得非常小,就是因為訓練的充分好的網絡,預測輸出 的值是非常接近1的,所以 的值非常非常小,存在「丟失精度」 的風險。所以「目標類別score建議使用不經過softmax的值」 。
「核心代碼解讀」 :
僅需要對目標類別的score進行求導, 然後追蹤到目標特徵圖的梯度, 對該梯隊進行element-wise求平均(GAP操作)即獲得特徵融合的權重。具體如下:
# 利用onehot的形式鎖定目標類別one_hot = np.zeros((1, output.size()[-1]), dtype=np.float32)one_hot[0][index] = 1one_hot = torch.from_numpy(one_hot).requires_grad_(True) # 獲取目標類別的輸出,該值帶有梯度連結關係,可進行求導操作one_hot = torch.sum(one_hot * output)self.model.zero_grad()one_hot.backward(retain_graph=True) # backward 求導# 獲取對應特徵層的梯度mapgrads_val = self.extractor.get_gradients()[-1].cpu().data.numpy()target = features[-1].cpu().data.numpy()[0, :] # 獲取目標特徵輸出weights = np.mean(grads_val, axis=(2, 3))[0, :] # 利用GAP操作, 獲取特徵權重cam = weights.dot(target.reshape((nc, h * w)))# relu操作,去除負值, 並縮放到原圖尺寸cam = np.maximum(cam, 0)cam = cv2.resize(cam, input.shape[2:])# 歸一化操作batch_cams = self._normalize(batch_cams)比Grad-CAM更進一步:Grad-CAM++「論文標題」 : Grad-CAM++: Improved Visual Explanations for Deep Convolutional Networks
「論文地址」 : https://arxiv.org/pdf/1710.11063.pdf
本文的提出是為了優化Grad-CAM的結果,定位會更精準,也更適用於目標類別物體在圖像中不止一個的情況。Grad-CAM是利用目標特徵圖的梯度求平均(GAP)獲取特徵圖權重,可以看做梯度map上每一個元素的貢獻是一樣。而本文認為梯度map上的「每一個元素的貢獻不同」 ,因此增加了一個額外的權重對梯度map上的元素進行加權。其中該權重 如下:
其中 為目標類別, 表示特徵圖第 層(或者說第 和channel), 為該層特徵圖上的元素位置。可以看出公式中用到了二階偏導和三階偏導,計算會更複雜。本文不做詳細推導,具體可見論文。
「核心代碼解讀:」
這裡值得說明的是, 上面的公式有二次偏導和三次偏導,論文中進行了冪次方的轉化,實現更容易。
## 獲取特徵權重的過程# 反向傳播self._backprop(scores, class_idx)# 注意,這裡用乘法,因為論文中將2次偏導和3次偏導進行了2次方和3次方的轉化grad_2 = self.hook_g.pow(2)grad_3 = self.hook_g.pow(3)# 獲取alpha權重mapalpha = grad_2 / (2 * grad_2 + (grad_3 * self.hook_a).sum(axis=(2, 3), keepdims=True))# 利用alpha 權重map去獲取特徵權重return alpha.squeeze_(0).mul_(torch.relu(self.hook_g.squeeze(0))).sum(axis=(1, 2))結合梯度平滑策略:Smooth Grad-CAM++「論文標題」 : Smooth Grad-CAM++: An Enhanced Inference Level Visualization Technique for Deep Convolutional Neural Network Models
「論文地址」 : https://arxiv.org/pdf/1908.01224.pdf
該論文做法是結合smoothGrad來優化CAM效果,首先解釋一下smoothGrad。對於分類網絡,最終分類類別取決於哪個類別的score最大,公式為:
其中 為輸入圖片, 為類別 的類別score。
對 求導,獲取:
表示 的每個像素的微小變化會對 類的分類score產生多大的影響,稱之為saliency maps,如下圖。
該方法雖然能顯示出與分類結果相關的區域,但如圖所示,存在很多噪聲,且很難探究噪聲的組成。smoothGrad的做法也很簡單,即為多次輸入加入隨機噪聲的圖片,對結果並求平均,用以消除輸出saliency maps的"噪聲",達到「引入噪聲」來「消除噪聲」的效果。核心公式如下:
為對原圖增加噪聲並前向的次數, 為高斯噪聲。可以看到,隨著 的增加,噪聲逐漸減少,saliency maps逐漸聚焦到目標物體區域。
回到Smooth Grad-CAM++,如下圖所示,和Grad-CAM++的區別在於特徵圖融合權重的求法上, 對原圖多次增加高斯噪聲,對目標類別對特徵圖的梯度求平均。
上圖中,對於Grad-CAM++中的 被替換成了 ,其中 表示為添加噪聲的原圖獲取的 ,共進行了 次操作。
「核心代碼解讀」 :
這裡給的實現smoothGrad操作是作用在alpha的2次偏導和3次偏導上, 實現方式不同,本質一樣。
for i in range(self.n_samples): # 進行n_samples次加噪聲操作 self.model.zero_grad() # 輸入圖片增加高斯噪聲 x_with_noise = torch.normal(mean=x, std=std_tensor).requires_grad_() score = self.model(x_with_noise) score[0, idx].backward(retain_graph=True) # 求梯度 activations = self.values.activations gradients = self.values.gradients n, c, _, _ = gradients.shape # 獲取alpha, 和grad-cam++一致 numerator = gradients.pow(2) denominator = 2 * gradients.pow(2) ag = activations * gradients.pow(3) denominator += \ ag.view(n, c, -1).sum(-1, keepdim=True).view(n, c, 1, 1) denominator = torch.where( denominator != 0.0, denominator, torch.ones_like(denominator)) alpha = numerator / (denominator + 1e-7) relu_grad = F.relu(score[0, idx].exp() * gradients) # 獲取weights weights = (alpha * relu_grad).view(n, c, -1).sum(-1).view(n, c, 1, 1) # 對特徵層加權融合, 並進行relu+歸一化操作 cam = (weights * activations).sum(1, keepdim=True) cam = F.relu(cam) cam -= torch.min(cam) cam /= torch.max(cam) total_cams += camtotal_cams /= self.n_samples # 求平均操作return total_cams.datagradient-free的做法:score-CAM 和 ss-CAM「論文標題」 : Score-CAM: Score-Weighted Visual Explanations for Convolutional Neural Networks
「論文地址」 : https://arxiv.org/pdf/1910.01279.pdf
本文摒棄了利用梯度獲取特徵權重的做法,作者認為:
對於深度神經網絡,梯度可能是存在噪聲的,並存在飽和問題。
利用Grad-CAM很容易找到錯誤置信度的樣本,既具有更高權重的激活圖對網絡輸出的貢獻較小的例子。
因此本文提出了gradient-free 的做法。首先,作者定義了CIC( (Increase of Confidence)的概念,既相對於baseline圖片的置信度增量,公式如下:
其中X為輸入圖片, 為baseline圖片,可以設置為0,既全黑圖片. 為輸入類別得分的神經網絡, 為第k層特徵圖, 為神經網絡第 層卷積。
本文的做法流程為:
解釋一下:對獲取的特徵圖進行channel-wise遍歷,對每層特徵圖進行上採樣+歸一化,與原始圖片進行pixel-wise相乘融合,然後送進網絡獲取目標類別score(softmax後),減去baseline的目標類別score,獲取CIC。再進行softmax操作來保證所有CIC之和為1。最後將CIC作為特徵融合權重融合需要可視化的特徵層。
「核心代碼解讀」 :
值得注意的是,計算CIC時默認使用的baseline為全黑的圖片,既全0的矩陣,因此CIC score不需要減去baseline的score。
with torch.no_grad():# gradient-free, 所以不需要計算梯度 for i in range(K): # 對K層特徵圖進行遍歷操作 # 獲取第i層特徵圖,並進行上採樣操作 saliency_map = torch.unsqueeze(activations[:, i, :, :], 1) saliency_map = F.interpolate(saliency_map, size=(h, w), mode='bilinear') # 歸一化 norm_saliency_map = (saliency_map - saliency_map.min()) / (saliency_map.max() - saliency_map.min()) # 利用第i層特徵圖作為mask覆蓋原圖,重新送入網絡獲取對應類別得分 output = self.model_arch(input * norm_saliency_map) output = F.softmax(output) score = output[0][predicted_class] # 利用該得分作為權重對該層的特徵圖進行加權線性融合, baseline默認為全0的圖,所以這裡直接 # 用該得分作為特徵權重 score_saliency_map += score * saliency_map # relu去除負值 score_saliency_map = F.relu(score_saliency_map) # 歸一化 score_saliency_map = (score_saliency_map - score_saliency_map.min())/ (score_saliency_map_max - score_saliency_map.max()) # score_saliency_map 為所求 return score_saliency_map「論文標題」 : SS-CAM: Smoothed Score-CAM for Sharper Visual Feature Localization
「論文地址」 : https://arxiv.org/pdf/2006.14255v1.pdf
本文和score-CAM的關係類似於smooth Grad-CAM++ 和 Grad-CAM++的關係。本文同樣是利用了smoothGrad技術來降低輸出噪聲。關於smoothGrad平滑策略, 本文給出了兩種做法,一種是噪聲增加在特徵圖上,一種是噪聲增加在輸入圖上。這兩種做法在論文給出的不同數據集上的測試指標也互有高低,具體選擇也依情況而定。
利用Ablation分析的方法:Ablation-CAM「論文標題」 : Ablation-CAM: Visual Explanations for Deep Convolutional Network via Gradient-free Localization
「論文地址」 : https://openaccess.thecvf.com/content_WACV_2020/papers/Desai_Ablation-CAM_Visual_Explanations_for_Deep_Convolutional_Network_via_Gradient-free_Localization_WACV_2020_paper.pdf
本文利用ablation分析來確定特徵圖上每個單元的重要程度。Ablation Study 可以理解為通過控制變量地進行移除各種組件等來探究各個因素對於模型整體貢獻的強弱多寡,找到對性能最主要的影響因素,這裡不對該理論作過多介紹。直接看文章的做法。
為特徵圖的channel個數,這裡的 是原始圖片 類別的類別score輸出, 為將特徵圖第 個channel全部設置為0後,將原始圖片再送進網絡,獲取的 類別的score。二者得分的之差,再除以 就獲取slope,其實就是特徵融合的權重。
由於計算 是個比較耗時的操作,於是有下面的簡化形式:
然後和Grad-CAM一致,對特徵圖利用該權重進行融合+relu去負值,如下:
一句話概括就是遍歷地將每層特徵圖置0後再進行網絡前向獲取目標類別得分, 該值與原始得分的相對大小作為特徵圖融合權重。論文實驗表明, 該方法是優於grad-CAM的.
後記本文針對CAM的兩種方式gradient-based和gradient-free進行了梳理,由於水平問題,可能有些地方理解不特別到位。拋磚引玉,希望能讓讀者對該任務有一個更直觀的感受。
下載:CVPR / ECCV 2020開原始碼
在CVer公眾號後臺回覆:CVPR2020, 即可下載CVPR 2020代碼開源的論文合集
在CVer公眾號後臺回覆:ECCV2020, 即可下載ECCV 2020代碼開源的論文合集
重磅!CVer- 論文寫作與投稿 交流 群成立
掃碼添加CVer助手,可申請加入CVer- 論文寫作與投稿 微信交流 群,目前已滿2400+人,旨在交流頂會(CVPR/ICCV/ECCV/NIPS/ICML/ICLR/AAAI等)、頂刊(IJCV/TPAMI/TIP等)、SCI、EI、中文核心等寫作與投稿事宜。
同時也可申請加入CVer大群和細分方向技術群,細分方向已涵蓋: 目標檢測、圖像分割、目標跟蹤、人臉檢測&識別、OCR、姿態估計、超解析度、SLAM、醫療影像、Re-ID、GAN、NAS、深度估計、自動駕駛、強化學習、車道線檢測、模型剪枝&壓縮、去噪、去霧、去雨、風格遷移、遙感圖像、行為識別、視頻理解、圖像融合、圖像檢索、論文投稿&交流、PyTorch和TensorFlow 等群。
一定要備註: 研究方向+地點+學校/公司+暱稱 (如 論文寫作 +上海+上交+卡卡) ,根據格式備註,可更快被通過且邀請進群
▲長按加微信群
▲長按關注CVer公眾號
整理不易,請給CVer點讚和在看!