簡析CNN可視化方法——Grad-CAM

最近在應用中,發現Grad-CAM在驗證神經網絡知識表示有效性方面很好用,這篇文章總結以下該方法的原理以及實現代碼。

一、介紹

卷積神經網絡(CNN)和其他深度網絡已經在多種計算機視覺任務中實現了前所未有的突破,從圖像分類[24,16]到物體檢測[15],語義分割[27],圖像描述生成[43,6,12,21],以及最近的視覺問答[3,14,32,36]。雖然這些深度神經網絡能夠實現卓越的性能,但由於它們缺乏可分解性,不能轉化爲直觀和易於理解的組件,因此它們很難被解釋[26]。因此,當今天的智能系統出現故障時,如果沒有任何警告或解釋,它們就會失敗得令人失望,用戶盯着一個不連貫的輸出,不知道爲什麼。

可解釋性問題。爲了建立對智能系統的信任,並將他們有意義地融入我們的日常生活中,很顯然我們必須建立“透明”的模型來解釋爲什麼它們這麼預測。廣義而言,這種透明度在人工智能(AI)演變的三個不同階段非常有用。首先,當AI比人類弱得多並且還不能可靠地“部署”時(例如視覺問答[3]),透明度和解釋的目標是識別失效模式[1,17],從而幫助研究人員集中精力在最富有成果的研究方向上。其次,當人工智能與人類平等並且可靠地“可部署”時(例如,在一組類別上訓練了足夠多的數據的圖像分類[22]),目標是在用戶中建立適當的信任和置信度。第三,當AI比人類強得多時(例如國際象棋或Go [39]),解釋的目標是在機器教學中[20] - 即一臺機器教人如何做出更好的決策。

在準確性和簡單性或可解釋性之間通常存在一種平衡。傳統的基於規則或專家系統[18]是高度可解釋的,但不是非常準確(或強大)。每個階段都是手工設計的可分解管道被認爲更具可解釋性,因爲每個單獨的組件都假設了一個自然、直觀的解釋。通過使用深層模型,我們犧牲可解釋模塊來解釋不可解釋的模塊,通過更好的抽象(更多層)和更緊密的集成(端到端訓練)實現更高的性能。最近引入的深度殘差網絡(ResNets)[16]深度超過200層,並且在幾項具有挑戰性的任務中展現了最先進的性能。這種複雜性使得這些模型很難解釋。因此,深層模型開始探索解釋性和準確性之間的關係。

二、方法

以前的一些文章已經斷言,CNN中的更深層次的表現可以捕捉到更高層次的視覺結構[5,31]。 此外,卷積特徵保留了在全連接層中丟失的空間信息,因此我們可以猜想最後的卷積層在高級語義和詳細空間信息之間具有最佳折衷。
這些圖層中的神經元在圖像中查找語義類特定的信息(比如對象部分)。Grad-CAM使用流入CNN最後一層卷積層的梯度信息來理解每個神經元對於目標決定的重要性。儘管我們的技術非常通用,並且可以用來可視化深層網絡中的任何激活,但在這項工作中,我們專注於解釋網絡可能做出的決策。
Grad-CAM概述:給定一個圖像和一個目標類(例如,'虎貓’或任何其他類型的可微分輸出)作爲輸入,我們將圖像傳播通過模型的CNN部分,然後通過特定任務的計算來獲得該類別的原始分數。
對於所有類,除了所需的類(虎貓)的梯度設置爲1,其餘的梯度設置爲零。然後將該信號反向傳播到所關注的整形卷積特徵圖,其中我們結合起來計算粗糙的Grad-CAM定位(藍色熱力圖),它表明了模型需要看哪裏去做出精確決定。最後,我們將熱力圖與導向反向傳播逐點相乘,獲得高分辨率和特定概念的Guided Grad-CAM可視化。

具體流程如下:

1)求圖像經過特徵提取後最後一次卷積後得到的特徵圖(也就是VGG16 conv5_3的特徵圖(7x7x512))
2)512張featuremap在全連接層分類的權重肯定不同,利用反向傳播求出每張特徵圖的權重。注意cam和Grad-cam的不同就在於求每張特徵圖權重的方式。其他流程都一樣
3)用每張特徵圖乘以權重得到帶權重的特徵圖(7x7x512),在第三維求均值得到7x7的map(np.mean(axis=-1)),relu激活,歸一化處理(避免有些值不在0-255範圍內)。
4) 將處理後的heatmap放縮到圖像尺寸大小,便於與圖像加權

該步最重要的是relu激活(relu只保留大於0的值),relu後只保留該類別有用的特徵。正數認爲是該類別有用的特徵,負數是其他類別的特徵(或無用特徵)。如下圖,假設某類別最後加權後爲0.8965,類別值越大則是該類別的概率就越高,那麼屬於該類別的特徵既爲wx值大於0的特徵。小於0的特徵可能是其他類的特徵。通俗理解,假如圖像中出現一個貓頭,那麼該特徵在貓類別中爲正特徵,在狗類別中爲負特徵,要增加貓的置信度,降低狗的置信度。

關鍵代碼

class CamExtractor():
    """
        Extracts cam features from the model
    """
    def __init__(self, model, target_layer):
        self.model = model
        self.target_layer = target_layer
        self.gradients = None

    def save_gradient(self, grad):
        self.gradients = grad

    def forward_pass_on_convolutions(self, x):
        """
            Does a forward pass on convolutions, hooks the function at given layer
        """
        conv_output = None
        for module_pos, module in self.model.features._modules.items():
            x = module(x)  # Forward
            if int(module_pos) == self.target_layer:
                x.register_hook(self.save_gradient)
                conv_output = x  # Save the convolution output on that layer
        return conv_output, x

    def forward_pass(self, x):
        """
            Does a full forward pass on the model
        """
        # Forward pass on the convolutions
        conv_output, x = self.forward_pass_on_convolutions(x)
        x = x.view(x.size(0), -1)  # Flatten
        # Forward pass on the classifier
        x = self.model.classifier(x)
        return conv_output, x


class GradCam():
    """
        Produces class activation map
    """
    def __init__(self, model, target_layer):
        self.model = model
        self.model.eval()
        # Define extractor
        self.extractor = CamExtractor(self.model, target_layer)

    def generate_cam(self, input_image, target_class=None):
        # Full forward pass
        # conv_output is the output of convolutions at specified layer
        # model_output is the final output of the model (1, 1000)
        conv_output, model_output = self.extractor.forward_pass(input_image)
        if target_class is None:
            target_class = np.argmax(model_output.data.numpy())
        # Target for backprop
        one_hot_output = torch.FloatTensor(1, model_output.size()[-1]).zero_()
        one_hot_output[0][target_class] = 1
        # Zero grads
        self.model.features.zero_grad()
        self.model.classifier.zero_grad()
        # Backward pass with specified target
        model_output.backward(gradient=one_hot_output, retain_graph=True)
        # Get hooked gradients
        guided_gradients = self.extractor.gradients.data.numpy()[0]
        # Get convolution outputs
        target = conv_output.data.numpy()[0]
        # Get weights from gradients
        weights = np.mean(guided_gradients, axis=(1, 2))  # Take averages for each gradient
        # Create empty numpy array for cam
        cam = np.ones(target.shape[1:], dtype=np.float32)
        # Multiply each weight with its conv output and then, sum
        for i, w in enumerate(weights):
            cam += w * target[i, :, :]
        cam = np.maximum(cam, 0)
        cam = (cam - np.min(cam)) / (np.max(cam) - np.min(cam))  # Normalize between 0-1
        cam = np.uint8(cam * 255)  # Scale between 0-255 to visualize
        cam = np.uint8(Image.fromarray(cam).resize((input_image.shape[2],
                       input_image.shape[3]), Image.ANTIALIAS))/255
        # ^ I am extremely unhappy with this line. Originally resizing was done in cv2 which
        # supports resizing numpy matrices with antialiasing, however,
        # when I moved the repository to PIL, this option was out of the window.
        # So, in order to use resizing with ANTIALIAS feature of PIL,
        # I briefly convert matrix to PIL image and then back.
        # If there is a more beautiful way, do not hesitate to send a PR.

        # You can also use the code below instead of the code line above, suggested by @ ptschandl
        # from scipy.ndimage.interpolation import zoom
        # cam = zoom(cam, np.array(input_image[0].shape[1:])/np.array(cam.shape))
        return cam

參考文獻:

[1]、https://blog.csdn.net/stu_sun/article/details/80628406
[2]、https://arxiv.org/abs/1610.02391
[3]、https://github.com/utkuozbulak/pytorch-cnn-visualizations#convolutional-neural-network-filter-visualization

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章