機器學習:在PyTorch中實現Grad-CAM

一書中描述了VGG16網絡的類激活映射的實現,使用Keras實現了算法。本文將使用PyTorch重新實現CAM算法。

Grad-CAM

該算法本身來源於 論文。這是對計算機視覺分析工具的一個很好的補充。它為我們提供了一種方法來研究圖像的哪些特定部分影響(influenced)了整個模型對特定指定標籤的決策。它在分析錯誤分類的樣本時特別有用。該算法直觀、易於實現。

算法背後的直覺是基於這樣一個事實,即深度學習模型必須已經看到了一些像素(或圖像的區域),並決定圖像中出現了什麼對象。 可以用梯度來描述數學術語中的影響(Influence)。 在高層次上,這就是算法的作用。 它首先找到相對於深度模型中最新激活映射的最主要logit的梯度。 我們可以將其解釋為最終激活映射中最終激活的一些編碼特徵說服整個機器學習模型選擇特定的logit(隨後是相應的類)。 然後通過channel-wise池化梯度,並且用相應的梯度對激活通道(channels)進行加權,產生加權激活通道的集合。 通過檢查這些通道,我們可以判斷哪些通道在類決策中發揮了最重要的作用。

在這篇文章中,我將使用PyTorch重新實現Grad-CAM算法,我將使用不同的架構。

VGG19

在這部分中,我將嘗試使用一個非常相似的深度學習模型 - VGG19重現Chollet的結果。我實現的主要思想是解剖網絡,這樣我們就可以得到最後一層卷積層的激活。Keras通過Keras函數有一種非常直接的方法。然而,在PyTorch我必須跳過一些次要的步驟。

該策略的定義如下:

  • 加載VGG19模型
  • 找到它的最後一個卷積層
  • 計算最可能的類
  • 對我們剛剛得到的激活映射使用logit類的梯度
  • 池化梯度
  • 通過相應的池化梯度對映射的通道進行加權
  • 插值熱圖

我從ImageNet數據集中提取了一些圖像(包括Chollet在書(Deep Learning With Python)中使用的大象圖像)來研究這個算法。我還將Grad-CAM應用於Facebook上的一些照片,以瞭解該算法在“field”條件下的工作原理。以下是我們將要使用的原始圖像:

機器學習:在PyTorch中實現Grad-CAM

左圖:Chollet在他的書中使用的大象形象。中圖和右圖:來自ImageNet的白鯊圖像

機器學習:在PyTorch中實現Grad-CAM

來自ImageNet數據集的鬣蜥圖像

機器學習:在PyTorch中實現Grad-CAM

左:應用YOLO模型。右圖:在莫斯科乘坐火車

讓我們從torchvision模塊加載VGG19模型並準備變換和數據加載器,Python代碼如下:

import torch
import torch.nn as nn
from torch.utils import data
from torchvision.models import vgg19
from torchvision import transforms
from torchvision import datasets
import matplotlib.pyplot as plt
import numpy as np
# use the ImageNet transformation

transform = transforms.Compose([transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])])
# define a 1 image dataset
dataset = datasets.ImageFolder(root='./data/Elephant/', transform=transform)
# define the dataloader to load that single image
dataloader = data.DataLoader(dataset=dataset, shuffle=False, batch_size=1)
機器學習:在PyTorch中實現Grad-CAM

在這裡,我導入了我們用於在PyTorch中使用神經網絡的所有標準內容。我使用基本變換來使用在ImageNet數據集上訓練的機器學習模型,包括圖像歸一化。我將一次提供一個圖像,因此我將我的數據集定義為大象的圖像,以獲得與書中類似的結果。

這裡有一個棘手的部分(但不是太棘手)。 我們可以使用在torch.Tensor上調用的.backward()方法計算PyTorch中的梯度。 我將在最可能的logit上調用backward(),這是通過網絡執行圖像的forward pass獲得的。 但是,PyTorch僅緩存計算圖中葉節點的梯度,例如權重,偏差和其他參數。 與激活相關的輸出梯度僅僅是中間值,一旦梯度在返回時通過它們傳播,就會被丟棄。 那麼我們有什麼選擇呢?

PyTorch中有一個回調函數:hooks。Hooks可以用在不同的場景中。PyTorch文檔告訴我們如何將鉤子附加到中間值上,以便在丟棄梯度之前將它們從模型中拉出來。文件告訴我們:

每次計算相對於張量的梯度時,都會調用鉤子。

現在我們知道我們必須將反向鉤子註冊到VGG19模型中最後一個卷積層的激活映射。

通過調用VGG19pretrained=True),我們可以很容易地觀察VGG19架構:

機器學習:在PyTorch中實現Grad-CAM

PyTorch中的預訓練機器學習模型大量使用了sequence()模塊,在大多數情況下,這使得它們很難被分解,稍後我們將看到它的示例。

在圖中,我們看到了整個VGG19架構。我突出顯示了feature塊中的最後一個卷積層(包括激活函數)。現在我們知道我們想要在網絡的特徵塊的第35層註冊backward hook。另外,值得一提的是,有必要將鉤子註冊到forward()方法中,以避免將鉤子註冊到duplicate tensor並隨後丟失梯度的問題。

正如您所看到的,在feature塊中還剩下一個最大池化層,不用擔心,我將在forward()方法中添加這個層。Python代碼如下:

class VGG(nn.Module):
def __init__(self):
super(VGG, self).__init__()

# get the pretrained VGG19 network
self.vgg = vgg19(pretrained=True)

# disect the network to access its last convolutional layer
self.features_conv = self.vgg.features[:36]

# get the max pool of the features stem
self.max_pool = nn.MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)

# get the classifier of the vgg19
self.classifier = self.vgg.classifier

# placeholder for the gradients
self.gradients = None

# hook for the gradients of the activations
def activations_hook(self, grad):
self.gradients = grad

def forward(self, x):
x = self.features_conv(x)

# register the hook
h = x.register_hook(self.activations_hook)

# apply the remaining pooling
x = self.max_pool(x)
x = x.view((1, -1))
x = self.classifier(x)
return x

# method for the gradient extraction
def get_activations_gradient(self):
return self.gradients

# method for the activation exctraction
def get_activations(self, x):
return self.features_conv(x)
機器學習:在PyTorch中實現Grad-CAM

到目前為止,這看起來很棒,我們終於可以從機器學習模型中獲得梯度和激活了。

Drawing CAM

首先,讓我們用大象的圖像在網絡中pass through,看看VGG19預測了什麼。不要忘記將你的深度學習模型設置為評估模式,否則你會得到非常隨機的結果,Python實現如下:

# initialize the VGG model
vgg = VGG()
# set the evaluation mode
vgg.eval()
# get the image from the dataloader
img, _ = next(iter(dataloader))
# get the most likely prediction of the model
pred = vgg(img).argmax(dim=1)
機器學習:在PyTorch中實現Grad-CAM

正如預期的那樣,我們得到的結果與Chollet在他的書中得到的結果相同:

Predicted: [('n02504458', 'African_elephant', 20.891441), ('n01871265', 'tusker', 18.035757), ('n02504013', 'Indian_elephant', 15.153353)]

現在,我們將使用第386類的logit進行反向傳播,該logit代表ImageNet數據集中的“African_elephant”。

# get the gradient of the output with respect to the parameters of the model
pred[:, 386].backward()
# pull the gradients out of the model
gradients = vgg.get_activations_gradient()
# pool the gradients across the channels
pooled_gradients = torch.mean(gradients, dim=[0, 2, 3])
# get the activations of the last convolutional layer
activations = vgg.get_activations(img).detach()
# weight the channels by corresponding gradients
for i in range(512):
activations[:, i, :, :] *= pooled_gradients[i]

# average the channels of the activations
heatmap = torch.mean(activations, dim=1).squeeze()
# relu on top of the heatmap
# expression (2) in https://arxiv.org/pdf/1610.02391.pdf
heatmap = np.maximum(heatmap, 0)
# normalize the heatmap
heatmap /= torch.max(heatmap)
# draw the heatmap
plt.matshow(heatmap.squeeze())
機器學習:在PyTorch中實現Grad-CAM

機器學習:在PyTorch中實現Grad-CAM

大象圖像的熱圖

最後,我們獲得了大象圖像的熱圖。它是一個14x14單通道圖像。大小由網絡的最後卷積層中的激活映射的空間維度決定。

現在,我們可以使用OpenCV來插入熱圖並將其投影到原始圖像上,這裡我使用了Chollet書中的Python代碼:

import cv2
img = cv2.imread('./data/Elephant/data/05fig34.jpg')
heatmap = cv2.resize(heatmap, (img.shape[1], img.shape[0]))
heatmap = np.uint8(255 * heatmap)

heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET)
superimposed_img = heatmap * 0.4 + img
cv2.imwrite('./map.jpg', superimposed_img)
機器學習:在PyTorch中實現Grad-CAM

在下面的圖像中,我們可以看到我們的VGG19網絡在決定分配給圖像的哪個類('African_elephant')時最重視的圖像區域。我們可以假設網絡採用大象的頭部和耳朵的形狀作為圖像中存在大象的強烈信號。更有趣的是,該網絡還區分了非洲象和塔斯克象和印度象。我不是大象專家,但我認為耳朵和象牙的形狀是非常好的區分標準。一般來說,這正是人類如何處理這樣的任務。專家將檢查耳朵和牙齒的形狀,也許還有一些其他微妙的特徵可以揭示它是什麼類型的大象。

機器學習:在PyTorch中實現Grad-CAM

Grad-CAM熱圖投射到原始大象圖像上

好的,讓我們用其他一些圖像重複相同的過程。

機器學習:在PyTorch中實現Grad-CAM

Left: the shark image with projected CAM heat-map

機器學習:在PyTorch中實現Grad-CAM

Another shark image with the corresponding CAM hea

鯊魚主要通過上圖圖像中的嘴/牙齒區域以及下部圖像中的體形和周圍水來識別。太酷了!

超越VGG

VGG是一個偉大的架構,因為它提出了更新更高效的圖像分類架構。在這一部分中,我們將研究其中一種架構: 。

在嘗試為密集連接網絡實施Grad-CAM時遇到了一些問題。首先,正如我已經提到的,PyTorch model zoo中的預訓練機器學習模型大多是使用嵌套塊構建的。它是可讀性和效率的絕佳選擇。請注意,VGG由2個塊組成:feature block和full connected classifier。DenseNet由多個嵌​​套塊組成,並且試圖到達最後一個卷積層的激活映射是不切實際的。我們可以通過兩種方式解決此問題:我們可以使用相應的批歸一化層獲取最後一個激活映射。這將產生非常好的結果,我們很快就會看到。我們要做的第二件事是從頭開始構建DenseNet並重新填充塊/層的權重,這樣我們就可以直接訪問這些層。第二種方法似乎太複雜和耗時,所以我避免使用它。

DenseNet CAM的Python代碼幾乎與用於VGG網絡的代碼相同,唯一的區別在於層的索引(在DenseNet的情況下為塊)我們將從以下方式獲得激活:

class DenseNet(nn.Module):
def __init__(self):
super(DenseNet, self).__init__()

# get the pretrained DenseNet201 network
self.densenet = densenet201(pretrained=True)

# disect the network to access its last convolutional layer
self.features_conv = self.densenet.features

# add the average global pool
self.global_avg_pool = nn.AvgPool2d(kernel_size=7, stride=1)

# get the classifier of the vgg19
self.classifier = self.densenet.classifier

# placeholder for the gradients
self.gradients = None

# hook for the gradients of the activations
def activations_hook(self, grad):
self.gradients = grad

def forward(self, x):
x = self.features_conv(x)

# register the hook
h = x.register_hook(self.activations_hook)

# don't forget the pooling
x = self.global_avg_pool(x)
x = x.view((1, 1920))
x = self.classifier(x)
return x

def get_activations_gradient(self):
return self.gradients

def get_activations(self, x):
return self.features_conv(x)
機器學習:在PyTorch中實現Grad-CAM

遵循DenseNet的架構設計非常重要,因此我在分類器之前將全局平均池化添加到網絡中。

機器學習:在PyTorch中實現Grad-CAM

我將通過密集連接網絡傳遞兩個鬣蜥圖像,以便找到分配給圖像的類:

Predicted: [('n01698640', 'American_alligator', 14.080595), ('n03000684', 'chain_saw', 13.87465), ('n01440764', 'tench', 13.023708)]

在這裡,網絡預測這是“美國短吻鱷”的形象。嗯,讓我們運行我們的Grad-CAM算法來對抗'American Alligator'類。在下面的圖像中,我顯示了熱圖和熱圖在圖像上的投影。我們可以看到,網絡主要是關注“生物”。很明顯鱷魚看起來像鬣蜥,因為它們都有共同的體形和整體結構。

機器學習:在PyTorch中實現Grad-CAM

常見的鬣蜥被誤分類為美洲短吻鱷

但是,請注意圖像的另一部分影響了類的分數。照片中的攝影師可能會用他的位置和姿勢把網絡搞亂。模型在做出選擇時,兼顧了鬣蜥和人。讓我們看看如果我們把拍照者從圖像中裁剪出來會發生什麼。以下是對裁剪後的圖像的前3類預測:

Predicted: [('n01677366', 'common_iguana', 13.84251), ('n01644900', 'tailed_frog', 11.90448), ('n01675722', 'banded_gecko', 10.639269)]


機器學習:在PyTorch中實現Grad-CAM

裁切後的鬣蜥圖像現在被分類為常見的鬣蜥

我們現在看到,從圖像中裁剪人實際上有助於獲得圖像的正確類別標籤。這是Grad-CAM的最佳應用之一:能夠獲得錯誤分類圖像中可能出錯的信息。一旦我們弄清楚可能發生了什麼,我們就可以有效地調試機器學習模型。

第二隻鬣蜥被正確分類,這裡是相應的熱圖和投影。

機器學習:在PyTorch中實現Grad-CAM

第二隻鬣蜥通過其背部的尖刺圖案來識別

超越ImageNet

讓我們嘗試一下我從Facebook頁面下載的一些圖像。我將使用我們的DenseNet201來實現此目的。

機器學習:在PyTorch中實現Grad-CAM

抱著貓的形象分類如下:

Predicted: [('n02104365', 'schipperke', 12.584991), ('n02445715', 'skunk', 9.826308), ('n02093256', 'Staffordshire_bullterrier', 8.28862)]

我們來看看這個圖像的類激活映射。

在下面的圖像中,我們可以看到模型正在尋找正確的位置。

機器學習:在PyTorch中實現Grad-CAM

YOLO applied

讓我們看看把人去掉是否有助於分類。

機器學習:在PyTorch中實現Grad-CAM

Predicted: [('n02123597', 'Siamese_cat', 6.8055286), ('n02124075', 'Egyptian_cat', 6.7294292), ('n07836838', 'chocolate_sauce', 6.4594917)]

現在至少被預測為貓,它更貼近真實的標籤。

我們要看的最後一張照片。

圖像分類正確:

Predicted: [('n02917067', 'bullet_train', 10.605988), ('n04037443', 'racer', 9.134802), ('n04228054', 'ski', 9.074459)]

我們確實在一輛火車前面。讓我們看一下類激活映射。

機器學習:在PyTorch中實現Grad-CAM

需要注意的是,DenseNet的最後一層卷積層生成了7x7的空間激活映射(與VGG網絡中的14x14相比),因此當將熱圖投影回圖像空間時,熱圖的分辨率可能有些誇張(對應於臉上的紅色)。

另一個可能出現的問題是我們為什麼不直接計算logit類關於輸入圖像的梯度呢。請記住,卷積神經網絡作為特徵提取器工作,網絡的更深層在越來越抽象的空間中運行。我們想知道哪些特徵實際影響了模型對類的選擇,而不僅僅是單個圖像像素。這就是為什麼對更深層的卷積層進行激活映射是至關重要的。


分享到:


相關文章: