在處理圖像和圖像數據時,CNN是最常用的架構。卷積神經網絡已經被證明在深度學習和計算機視覺領域提供了許多最先進的解決方案。沒有CNN,圖像識別、目標檢測、自動駕駛汽車就不可能實現。
但當歸結到CNN如何看待和識別他們所做的圖像時,事情就變得更加棘手了。
· CNN如何判斷一張圖片是貓還是狗?
· 在圖像分類問題上,是什么讓CNN比其他模型更強大?
· 他們在圖像中看到了什么?
這是我第一次了解CNN時的一些問題。問題會隨著你的深入而增加。
那時候我聽說過過濾器和特性映射,但不知道它們是什么,它們的作用是什么。后來我知道他們是什么,但不知道他們長什么樣子,但現在我知道了。在處理深度卷積網絡時,過濾器和特征映射很重要。濾鏡是使特征被復制的東西,也是模型看到的東西。
什么是CNN的濾鏡和特性映射?
過濾器是使用反向傳播算法學習的一組權值。如果你做了很多實際的深度學習編碼,你可能知道它們也被稱作核。過濾器的尺寸可以是3×3,也可以是5×5,甚至7×7。
過濾器在一個CNN層學習檢測抽象概念,如人臉的邊界,建筑物的邊緣等。通過疊加越來越多的CNN層,我們可以從一個CNN中得到更加抽象和深入的信息。
特性映射是我們通過圖像的像素值進行濾波后得到的結果。這就是模型在圖像中看到的這個過程叫做卷積運算。將feature map可視化的原因是為了加深對CNN的了解。
選擇模型
我們將使用ResNet-50神經網絡模型來可視化過濾器和特征圖。使用ResNet-50模型來可視化過濾器和特征圖并不理想。原因是resnet模型總的來說有點復雜。遍歷內部卷積層會變得非常困難。但是在本篇文章中您將了解如何訪問復雜體系結構的內部卷積層后,您將更加適應使用類似的或更復雜的體系結構。
我使用的圖片來自pexels。這是我為了訓練我的人臉識別分類器而收集的一幅圖像。
模型結構
乍一看,模型的結構可能令人生畏,但要得到我們想要的東西確實很容易。通過了解如何提取這個模型的層,您將能夠提取更復雜模型的層。下面是模型結構。
ResNet(
(conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
(layer1): Sequential(
(0): Bottleneck(
(conv1): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(downsample): Sequential(
(0): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): Bottleneck(
(conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
...
(2): Bottleneck(
(conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
)
(avgpool): AdaptiveAvgPool2d(output_size=(1, 1))
(fc): Linear(in_features=2048, out_features=1000, bias=True)
提取CNN層
conv_layers = []
model_weights = []
model_children = list(models.resnet50().children())
counter = 0
for i in range(len(model_children)):
if type(model_children[i]) == nn.Conv2d:
counter += 1
model_weights.Append(model_children[i].weight)
conv_layers.append(model_children[i])
elif type(model_children[i]) == nn.Sequential:
for j in range(len(model_children[i])):
for child in model_children[i][j].children():
if type(child) == nn.Conv2d:
counter += 1
model_weights.append(child.weight)
conv_layers.append(child)
1. 首先,在第4行,我們初始化一個計數器變量,以跟蹤卷積層的數量。
1. 從第6行開始,我們將遍歷ResNet-50模型的所有層。
1. 具體來說,我們在三層嵌套中檢查卷積層
1. 第7行,檢查模型的直接子層中是否有卷積層。
1. 然后從第10行開始,我們檢查序列塊中的瓶頸層是否包含任何卷積層。
1. 如果上述兩個條件中有一個滿足,那么我們將該子節點和權值分別附加到convlayers和modelweights,
上面的代碼很簡單并且不言自明,但是它僅限于已經存在的模型,比如其他resnet模型resnet-18、34、101、152。對于自定義模型,情況將有所不同,假設在另一個連續層中有一個連續層,如果有一個CNN層,程序將不檢查它。這就是我編寫的extract .py模塊可能有用的地方。
Extractor類
Extractor類可以找到每一個CNN層(除了下采樣層),包括它們在任何resnet模型以及幾乎在任何自定義resnet和vgg模型中的權重。它不局限于CNN層,它可以找到線性層,如果提到了下采樣層的名字,它也可以找到。它還可以提供一些有用的信息,如CNN的數量、模型中的線性層和順序層。
如何使用
在Extractor類中,模型參數接受模型,而DSlayername參數是可選的。DSlayername參數用于查找下采樣層,通常在resnet層中名稱為"downsample",因此它保持為默認值。
extractor = Extractor(model = resnet, DS_layer_name = 'downsample')
extractor.activate())是激活程序。extractor.info()可以查看里面的方法
{'Down-sample layers name': 'downsample', 'Total CNN Layers': 49, 'Total Sequential Layers': 4, 'Total Downsampling Layers': 4, 'Total Linear Layers': 1, 'Total number of Bottleneck and Basicblock': 16, 'Total Execution time': '0.00137 sec'}
訪問權重和層
extractor.CNN_layers -----> Gives all the CNN layers in a model
extractor.Linear_layers --> Gives all the Linear layers in a model
extractor.DS_layers ------> Gives all the Down-sample layers in a model if there are any
extractor.CNN_weights ----> Gives all the CNN layer's weights in a model
extractor.Linear_weights -> Gives all the Linear layer's weights in a model
沒有任何編碼,你可以得到CNN和線性層和他們的權值在幾乎每個resnet模型。下面是類的方法
def activate(self):
"""Activates the algorithm"""
start = time.time()
self.__Layer_Extractor(self.model_children)
self.__Verbose()
self.__ex_time = str(round(time.time() - start, 5)) + ' sec'
def __Append(self, layer, Linear=False):
"""
This function will append the layers weights and
the layer itself to the appropriate variables
params: layer: takes in CNN or Linear layer
returns: None
"""
if Linear:
self.Linear_weights.append(layer.weight)
self.Linear_layers.append(layer)
else:
self.CNN_weights.append(layer.weight)
self.CNN_layers.append(layer)
def __Layer_Extractor(self, layers):
"""
This function(algorithm) finds CNN and linear layer in a Sequential layer
params: layers: takes in either CNN or Sequential or linear layer
return: None
"""
for x in range(len(layers)):
if type(layers[x]) == nn.Sequential:
# Calling the fn to loop through the layer to get CNN layer
self.__Layer_Extractor(layers[x])
self.__no_sq_layers += 1
if type(layers[x]) == nn.Conv2d:
self.__Append(layers[x])
if type(layers[x]) == nn.Linear:
self.__Append(layers[x], True)
# This statement makes sure to get the down-sampling layer in the model
if self.DS_layer_name in layers[x]._modules.keys():
self.DS_layers.append(layers[x]._modules[self.DS_layer_name])
# The below statement will loop throgh the containers and append it
if isinstance(layers[x], (self.__bottleneck, self.__basicblock)):
self.__no_containers += 1
for child in layers[x].children():
if type(child) == nn.Conv2d:
self.__Append(child)
可視化
卷積層
在這里,我們將可視化卷積層過濾器。為了簡單起見,我們將只可視化第一個卷積層的過濾器。
# Visualising the filters
plt.figure(figsize=(35, 35))
for index, filter in enumerate(extractor.CNN_weights[0]):
plt.subplot(8, 8, index + 1)
plt.imshow(filter[0, :, :].detach(), cmap='gray')
plt.axis('off')
plt.show()
第一層的濾波器尺寸為7×7,有64個通道(隱藏層)。
來自訓練好的resnet-50模型的7×7個過濾器
每個小方框的像素值在0到255之間。0是完全黑的255是白色的。范圍可以是不同的,比如0到1或-1到1,以0為平均值。
特征映射
為了使特征映射形象化,首先需要將圖像轉換為張量圖像。利用torchvision的變換,可以將圖像歸一化并轉換為張量。
# Filter Map
img = cv.cvtColor(cv.imread('filtermap/5.png'), cv.COLOR_BGR2RGB)
#transforms is imported as t
img = t.Compose([
t.ToPILImage(),
t.Resize((128, 128)),
# t.Grayscale(),
t.ToTensor(),
t.Normalize(0.5, 0.5)])(img).unsqueeze(0)
#LAST LINE
t.Normalize(0.5, 0.5)])(img).unsqueeze(0)
變換后的最后一行表示將變換應用于圖像。您可以創建一個新變量,然后應用它,但是一定要更改變量名。unsqueze(0)是給張量img增加一個額外的維數。添加批處理維度是一個重要步驟。現在圖像的大小不是[3,128,128],而是[1,3,128,128],這表示批處理中只有一個圖像。
將圖像輸入每個卷積層
下面的代碼將圖像通過每個卷積層。
featuremaps = [extractor.CNN_layers [0] (img)]
len(extractor.CNN_layers):
featuremaps.append (extractor.CNN_layers [x] (featuremaps [1]))
我們將首先把圖像作為輸入,傳遞給第一個卷積層。在此之后,我們將使用for循環將最后一層的輸出傳遞給下一層,直到到達最后一個卷積層。
· 在第1行,我們將圖像作為第一個卷積層的輸入。
· 然后我們使用for循環從第二層循環到最后一層卷積。
· 我們將最后一層的輸出作為下一個卷積層的輸入(featuremaps[-1])。
· 另外,我們將每個層的輸出附加到featuremaps列表中。
特征的可視化
這是最后一步。我們將編寫代碼來可視化特征映射。注意,最后的cnn層有很多feature map,范圍在512到2048之間。但是我們將只從每一層可視化64個特征圖,否則將使輸出真正地混亂。
# Visualising the featuremaps
for x in range(len(featuremaps)):
plt.figure(figsize=(30, 30))
layers = featuremaps[x][0, :, :, :].detach()
for i, filter in enumerate(layers):
if i == 64:
break
plt.subplot(8, 8, i + 1)
plt.imshow(filter, cmap='gray')
plt.axis('off')
# plt.savefig('featuremap%s.png'%(x))
plt.show()
· 從第2行開始,我們遍歷feature remaps。
· 然后我們得到的層是featuremaps[x][0,:,:,:].detach()。
· 從第5行開始,我們遍歷每個層中的過濾器。如果它是第64個特征圖,我們就跳出了循環。
· 在此之后,我們繪制feature map,并在必要時保存它們。
結果
可以看到,在創建圖像的feature map時,不同的濾鏡聚焦于不同的方面。
一些特征地圖聚焦于圖像的背景。另一些人則創建了圖像的輪廓。一些濾鏡創建的特征地圖,背景是黑暗的,但圖像的臉是明亮的。這是由于過濾器的相應權重。從上面的圖像可以很清楚地看出,在深層,神經網絡可以看到輸入圖像非常詳細的特征圖。
讓我們看看其他一些特征。
ResNet-50模型的第20和第10卷積層的Feature map
ResNet-50模型第40和第30卷積層的Feature map
你可以觀察到,當圖像通過層的進展,細節從圖像慢慢消失。它們看起來像噪音,但在這些特征地圖中肯定有一種模式是人眼無法察覺的,但神經網絡可以。
當圖像到達最后的卷積層時,人類就不可能知道那是什么了。最后一層輸出對于最后的全連接的神經元非常重要,這些神經元基本上構成了卷積神經網絡的分類層。
代碼:github/Rahul0128/Visualizing-Filters-and-Feature-Maps-in-Convolutional-Neural-Networks-using-PyTorch
作者 Rahulpillai
deephub翻譯組