1. 程式人生 > 其它 >類啟用圖Cam和GradCam原理解讀,程式碼例項講解

類啟用圖Cam和GradCam原理解讀,程式碼例項講解

技術標籤:深度學習常用技巧類啟用圖

網上關於類啟用圖Cam以及梯度類啟用圖的講解很多,但都不是非常全面,這裡我就全面的介紹一下兩者的原理,並講解程式碼實現過程,最後通過一個例項進行演示。
CAM: CAM
Grad-CAM:Grad-CAM
類啟用圖cam(class activation map)通過視覺化的熱力圖將模型認為最顯著的結果顯示出來,因此可用於解釋模型預測的結果。卷積神經網路的最後一層卷積層包含了最豐富的空間和語義資訊,於是Cam充分利用了最後一層卷積的特徵,並將後面的全連線層和softmax層替換成了GAP層(全域性平均池化),用特徵圖所有畫素的均值代替整個特徵圖的值。每個特徵圖 A k A^k

Ak都有一個對應的權重 w k c w_k^c wkc,與GAP後的特徵圖求加權和就能得到相應類別C的類啟用圖,同時也能得到對應的預測得分 Y c Y^c Yc(softmax之前)。

C a m c = ∑ k w k c × A k Cam^c=\sum\limits_k w_k^c\times A^k Camc=kwkc×Ak

可以看到,其實Cam實現並不難,但有個很大的缺陷,由於添加了GAP改變了網路模型,與原始訓練好的模型不同。所以還需要對改變的模型進行訓練得到相應的權重,大大限制了應用場景。
於是就有了Grad-CAM,它使用梯度的全域性平均來計算權重,不需要修改模型,自然也不需要重新訓練。經過嚴格的數學推導,權重為


我們只關心那些有正向作用的畫素點,所以加了ReLU。

從公式中可以看出,我們只要對類別C的預測得分進行反向傳播得到梯度,並求全域性平均就能計算出相應的權重,因此不需要修改模型。

下面我們結合程式碼進行講解,GitHub上的GradCam非常多,我挑了其中一個,講解其中關鍵的程式碼。

由於需要計算前向傳播的特徵和反向傳播的梯度,所以介紹前需要先了解hook函式,可以不改變主體情況下,提取網路中間的輸出。這裡我就不詳細介紹了,詳情可以看這個部落格
hook函式和CAM類啟用圖

此程式碼新增前向傳播的特徵和反向傳播的梯度

def _register_hook(self):
    for (
name, module) in self.net.named_modules(): if name == self.layer_name: self.handlers.append(module.register_forward_hook(self._get_features_hook)) self.handlers.append(module.register_backward_hook(self._get_grads_hook))

在計算完GradCam後,需要釋放hook,否則計算會越來越慢。

def remove_handlers(self):
    for handle in self.handlers:
        handle.remove()

這裡就是計算類啟用圖的關鍵程式碼了,結合上述公式可以很快的理解。

def __call__(self, inputs, index):
    """
    :param inputs: [1,3,H,W]
    :param index: class id
    :return:
    """
    self.net.zero_grad()
    #模型預測得分
    output = self.net(inputs)  # [1,num_classes]
    #取最大得分對應的索引作為類別
    if index is None:
        index = np.argmax(output.cpu().data.numpy())
    target = output[0][index]
    #預測得分反向傳播
    target.backward()
    #梯度全域性平均求權重
    gradient = self.gradient[0].cpu().data.numpy()  # [C,H,W]
    weight = np.mean(gradient, axis=(1, 2))  # [C]

    feature = self.feature[0].cpu().data.numpy()  # [C,H,W]
    #特徵與對應權重加權和並ReLU
    cam = feature * weight[:, np.newaxis, np.newaxis]  # [C,H,W]
    cam = np.sum(cam, axis=0)  # [H,W]
    cam = np.maximum(cam, 0)  # ReLU

    # 數值歸一化
    cam -= np.min(cam)
    cam /= np.max(cam)
    # resize to 224*224
    cam = cv2.resize(cam, (224, 224))
    return cam