今天做實驗發現了一個非常奇怪的實驗現象,查了文獻後發現很早之前就有人注意到了這個現象,並且根據該現象發表了一系列成果。作者的實驗設計思路簡單有效,並且提出的方法也算新穎,給我很大啟發。今天就結合自己的所思所想,縷一縷這兩篇文章《Selective Convolutional Descriptor Aggregation》和《Part-based Mask-CNN》。都是很久之前的文章了,但是文章提及的思想很有啟發性。在正式梳理之前,先進行知識鋪墊,這會有助於後面的理解。

特徵提取

把一張 224 * 224 的圖片放入 VGG16 網路中,會經過 5 組卷積和 5 次 maxpool ,每次經過 maxpool 層都會得到不同尺寸大小的特徵圖,且特徵圖的通道數也不同。下圖是 VGG 網路的配置圖,其中高亮部分是 VGG16 網路的配置。

卷積神經網路(CNN)的特徵圖視覺化和預訓練模型可解釋性的能力

當圖片經過層層卷積和 maxpool ,到達最後一層 maxpool 下采樣層後會得到 7 * 7 且通道數為 512 的特徵圖。此處的特徵圖提取的是原圖片中較為抽象的特徵。至於每一層的特徵圖是什麼樣的,我們可以透過視覺化的辦法來觀察。視覺化方法如下:

卷積輸出視覺化

下面把一張貓的圖片放入預訓練的 VGG16 網路中去,並可視化每一層的特徵圖,透過特徵圖的響應區域來探究 VGG16 網路每層卷積的功能。視覺化參考至CSDN博主「我是小螞蟻」的原創文章,連結:

https://

blog。csdn。net/missyougo

on/java/article/details/85645195

。因為 VGG16 模型每一層通道比較多,這裡就只使用了前 25 個通道特徵圖作為展示。

原始圖片

卷積神經網路(CNN)的特徵圖視覺化和預訓練模型可解釋性的能力

第一層卷積提取的特徵圖

卷積神經網路(CNN)的特徵圖視覺化和預訓練模型可解釋性的能力

所有第一層特徵圖1:1融合後整體的特徵圖:

卷積神經網路(CNN)的特徵圖視覺化和預訓練模型可解釋性的能力

第二層:

卷積神經網路(CNN)的特徵圖視覺化和預訓練模型可解釋性的能力

所有第二層特徵圖1:1融合後整體的特徵圖:

卷積神經網路(CNN)的特徵圖視覺化和預訓練模型可解釋性的能力

第三層:

卷積神經網路(CNN)的特徵圖視覺化和預訓練模型可解釋性的能力

所有第三層特徵圖1:1融合後整體的特徵圖:

卷積神經網路(CNN)的特徵圖視覺化和預訓練模型可解釋性的能力

第四層:

卷積神經網路(CNN)的特徵圖視覺化和預訓練模型可解釋性的能力

所有第四層特徵圖1:1融合後整體的特徵圖:

卷積神經網路(CNN)的特徵圖視覺化和預訓練模型可解釋性的能力

第五層

卷積神經網路(CNN)的特徵圖視覺化和預訓練模型可解釋性的能力

第五層特徵圖1:1融合後整體的特徵圖:

卷積神經網路(CNN)的特徵圖視覺化和預訓練模型可解釋性的能力

可以看出淺層的卷積層獲得的特徵資訊還比較多,特徵資料與原始的影象資料很接近。隨著層數越深,得到的有效特徵越來越少,特徵也變得越來越抽象。最淺層的卷積核傾向於學習點、顏色等基礎特徵;接下來開始學習到線段、邊緣等特徵。層數越深,學習到的特徵就越具體越抽象。視覺化的程式碼為:

# -*- coding:utf-8 -*-

import numpy as np

import tensorflow as tf

import time

from PIL import Image

import matplotlib。pyplot as plt

# VGG 自帶的一個常量,之前VGG訓練透過歸一化,所以現在同樣需要作此操作

VGG_MEAN = [103。939, 116。779, 123。68] # rgb 三通道的均值

class VGGNet():

‘’‘

建立 vgg16 網路 結構

從模型中載入引數

’‘’

def __init__(self, data_dict):

‘’‘

傳入vgg16模型

:param data_dict: vgg16。npy (字典型別)

’‘’

self。data_dict = data_dict

def get_conv_filter(self, name):

‘’‘

得到對應名稱的卷積層

:param name: 卷積層名稱

:return: 該卷積層輸出

’‘’

return tf。constant(self。data_dict[name][0], name=‘conv’)

def get_fc_weight(self, name):

‘’‘

獲得名字為name的全連線層權重

:param name: 連線層名稱

:return: 該層權重

’‘’

return tf。constant(self。data_dict[name][0], name=‘fc’)

def get_bias(self, name):

‘’‘

獲得名字為name的全連線層偏置

:param name: 連線層名稱

:return: 該層偏置

’‘’

return tf。constant(self。data_dict[name][1], name=‘bias’)

def conv_layer(self, x, name):

‘’‘

建立一個卷積層

:param x:

:param name:

:return:

’‘’

# 在寫計算圖模型的時候,加一些必要的 name_scope,這是一個比較好的程式設計規範

# 可以防止命名衝突, 二視覺化計算圖的時候比較清楚

with tf。name_scope(name):

# 獲得 w 和 b

conv_w = self。get_conv_filter(name)

conv_b = self。get_bias(name)

# 進行卷積計算

h = tf。nn。conv2d(x, conv_w, strides=[1, 1, 1, 1], padding=‘SAME’)

‘’‘

因為此刻的 w 和 b 是從外部傳遞進來,所以使用 tf。nn。conv2d()

tf。nn。conv2d(input, filter, strides, padding, use_cudnn_on_gpu = None, name = None) 引數說明:

input 輸入的tensor, 格式[batch, height, width, channel]

filter 卷積核 [filter_height, filter_width, in_channels, out_channels]

分別是:卷積核高,卷積核寬,輸入通道數,輸出通道數

strides 步長 卷積時在影象每一維度的步長,長度為4

padding 引數可選擇 “SAME” “VALID”

’‘’

# 加上偏置

h = tf。nn。bias_add(h, conv_b)

# 使用啟用函式

h = tf。nn。relu(h)

return h

def pooling_layer(self, x, name):

‘’‘

建立池化層

:param x: 輸入的tensor

:param name: 池化層名稱

:return: tensor

’‘’

return tf。nn。max_pool(x,

ksize=[1, 2, 2, 1], # 核引數, 注意:都是4維

strides=[1, 2, 2, 1],

padding=‘SAME’,

name=name

def fc_layer(self, x, name, activation=tf。nn。relu):

‘’‘

建立全連線層

:param x: 輸入tensor

:param name: 全連線層名稱

:param activation: 啟用函式名稱

:return: 輸出tensor

’‘’

with tf。name_scope(name, activation):

# 獲取全連線層的 w 和 b

fc_w = self。get_fc_weight(name)

fc_b = self。get_bias(name)

# 矩陣相乘 計算

h = tf。matmul(x, fc_w)

# 新增偏置

h = tf。nn。bias_add(h, fc_b)

# 因為最後一層是沒有啟用函式relu的,所以在此要做出判斷

if activation is None:

return h

else:

return activation(h)

def flatten_layer(self, x, name):

‘’‘

展平

:param x: input_tensor

:param name:

:return: 二維矩陣

’‘’

with tf。name_scope(name):

# [batch_size, image_width, image_height, channel]

x_shape = x。get_shape()。as_list()

# 計算後三維合併後的大小

dim = 1

for d in x_shape[1:]:

dim *= d

# 形成一個二維矩陣

x = tf。reshape(x, [-1, dim])

return x

def build(self, x_rgb):

‘’‘

建立vgg16 網路

:param x_rgb: [1, 224, 224, 3]

:return:

’‘’

start_time = time。time()

print(‘模型開始建立……’)

# 將輸入影象進行處理,將每個通道減去均值

r, g, b = tf。split(x_rgb, [1, 1, 1], axis=3)

‘’‘

tf。split(value, num_or_size_split, axis=0)用法:

value:輸入的Tensor

num_or_size_split:有兩種用法:

1。直接傳入一個整數,代表會被切成幾個張量,切割的維度有axis指定

2。傳入一個向量,向量長度就是被切的份數。傳入向量的好處在於,可以指定每一份有多少元素

axis, 指定從哪一個維度切割

因此,上一句的意思就是從第4維切分,分為3份,每一份只有1個元素

’‘’

# 將 處理後的通道再次合併起來

x_bgr = tf。concat([b - VGG_MEAN[0], g - VGG_MEAN[1], r - VGG_MEAN[2]], axis=3)

# assert x_bgr。get_shape()。as_list()[1:] == [224, 224, 3]

# 開始構建卷積層

# vgg16 的網路結構

# 第一層:2個卷積層 1個pooling層

# 第二層:2個卷積層 1個pooling層

# 第三層:3個卷積層 1個pooling層

# 第四層:3個卷積層 1個pooling層

# 第五層:3個卷積層 1個pooling層

# 第六層: 全連線

# 第七層: 全連線

# 第八層: 全連線

# 這些變數名稱不能亂取,必須要和vgg16模型保持一致

# 另外,將這些卷積層用self。的形式,方便以後取用方便

self。conv1_1 = self。conv_layer(x_bgr, ‘conv1_1’)

self。conv1_2 = self。conv_layer(self。conv1_1, ‘conv1_2’)

self。pool1 = self。pooling_layer(self。conv1_2, ‘pool1’)

self。conv2_1 = self。conv_layer(self。pool1, ‘conv2_1’)

self。conv2_2 = self。conv_layer(self。conv2_1, ‘conv2_2’)

self。pool2 = self。pooling_layer(self。conv2_2, ‘pool2’)

self。conv3_1 = self。conv_layer(self。pool2, ‘conv3_1’)

self。conv3_2 = self。conv_layer(self。conv3_1, ‘conv3_2’)

self。conv3_3 = self。conv_layer(self。conv3_2, ‘conv3_3’)

self。pool3 = self。pooling_layer(self。conv3_3, ‘pool3’)

self。conv4_1 = self。conv_layer(self。pool3, ‘conv4_1’)

self。conv4_2 = self。conv_layer(self。conv4_1, ‘conv4_2’)

self。conv4_3 = self。conv_layer(self。conv4_2, ‘conv4_3’)

self。pool4 = self。pooling_layer(self。conv4_3, ‘pool4’)

self。conv5_1 = self。conv_layer(self。pool4, ‘conv5_1’)

self。conv5_2 = self。conv_layer(self。conv5_1, ‘conv5_2’)

self。conv5_3 = self。conv_layer(self。conv5_2, ‘conv5_3’)

self。pool5 = self。pooling_layer(self。conv5_3, ‘pool5’)

print(‘建立模型結束:%4ds’ % (time。time() - start_time))

# 指定 model 路徑

vgg16_npy_pyth = ‘。/vgg16。npy’

# 內容影象 路徑

content_img_path = ‘。/mao。jpeg’

def read_img(img_name):

‘’‘

讀取圖片

:param img_name: 圖片路徑

:return: 4維矩陣

’‘’

img = Image。open(img_name)

np_img = np。array(img) # 224, 224, 3

# 需要傳化 成 4 維

np_img = np。asarray([np_img], dtype = np。int32) # 這個函式作用不太理解 (1, 224, 224, 3)

return np_img

def get_row_col(num_pic):

‘’‘

計算行列的值

:param num_pic: 特徵圖的數量

:return:

’‘’

squr = num_pic ** 0。5

row = round(squr)

col = row + 1 if squr - row > 0 else row

return row, col

def visualize_feature_map(feature_batch):

‘’‘

建立特徵子圖,建立疊加後的特徵圖

:param feature_batch: 一個卷積層所有特徵圖

:return:

’‘’

feature_map = np。squeeze(feature_batch, axis=0)

feature_map_combination = []

plt。figure(figsize=(8, 7))

# 取出 featurn map 的數量,因為特徵圖數量很多,這裡直接手動指定了。

#num_pic = feature_map。shape[2]

row, col = get_row_col(25)

# 將 每一層卷積的特徵圖,拼接層 5 × 5

for i in range(0, 25):

feature_map_split = feature_map[:, :, i]

feature_map_combination。append(feature_map_split)

plt。subplot(row, col, i+1)

plt。imshow(feature_map_split)

plt。axis(‘off’)

#plt。savefig(‘。/mao_feature/feature_map2。png’) # 儲存影象到本地

plt。show()

def visualize_feature_map_sum(feature_batch):

‘’‘

將每張子圖進行相加

:param feature_batch:

:return:

’‘’

feature_map = np。squeeze(feature_batch, axis=0)

feature_map_combination = []

# 取出 featurn map 的數量

num_pic = feature_map。shape[2]

# 將 每一層卷積的特徵圖,拼接層 5 × 5

for i in range(0, num_pic):

feature_map_split = feature_map[:, :, i]

feature_map_combination。append(feature_map_split)

# 按照特徵圖 進行 疊加程式碼

feature_map_sum = sum(one for one in feature_map_combination)

plt。imshow(feature_map_sum)

#plt。savefig(‘。/mao_feature/feature_map_sum2。png’) # 儲存影象到本地

plt。show()

# 讀取 內容影象

content_val = read_img(content_img_path)

print(content_val。shape)

content = tf。placeholder(tf。float32, shape = [1, 792, 1024, 3])

# 載入模型, 注意:在python3中,需要新增一句: encoding=‘latin1’

data_dict = np。load(vgg16_npy_pyth, encoding=‘latin1’)。item()

# 建立影象的 vgg 物件

vgg_for_content = VGGNet(data_dict)

# 建立 每個 神經網路

vgg_for_content。build(content)

content_features = [vgg_for_content。conv1_2,

vgg_for_content。conv2_2,

vgg_for_content。conv3_3,

vgg_for_content。conv4_3,

vgg_for_content。conv5_3,

init_op = tf。global_variables_initializer()

with tf。Session() as sess:

sess。run(init_op)

content_features = sess。run([content_features],

feed_dict = {

content:content_val

})

conv1 = content_features[0][0]

conv2 = content_features[0][1]

conv3 = content_features[0][2]

conv4 = content_features[0][3]

conv5 = content_features[0][4]

# 檢視 每個 特徵 子圖

visualize_feature_map(conv1)

# 檢視 疊加後的 特徵圖

visualize_feature_map_sum(conv1)

Descriptor的定義

如下圖所示,把一張圖片放入卷積神經網路中後,會得到形狀為

h \times w \times d

的特徵圖,該特徵圖有

d

個通道,且每個通道上的特徵圖為

h \times w

形狀。在文章 SCDA 中,將一個細長條稱為 Descriptor (下圖中有

 h \times w

個長條,且每個長條的長度為

d

)。例如把

224 \times 224

圖片放入預訓練的 VGG16 網路中後,經過最後一次下采樣,會得到

7 \times 7 \times 512

形狀的特徵圖,也就是每個特徵圖形狀為

7 \times 7

,一共 512 個通道。每個

1 \times 1 \times 512

是一個 Descriptor ,共有

49

個。

卷積神經網路(CNN)的特徵圖視覺化和預訓練模型可解釋性的能力

Descriptor的選擇

把下圖中鳥的圖片放入一個 pre-trained model (該 model 僅僅在 ImageNet 資料上訓練過)裡面,會得到類似於

(b)

的卷積啟用張量。該張量可能會啟用若干部分割槽域,啟用的區域可能是想要的區域(比如圖中鳥的區域),也有可能不是我們想要的區域(比如圖中的樹枝所在區域)。需要透過一個

選擇演算法

來選出我們想要的區域(如何選擇後面會提到)。而選擇後的區域(包含鳥的區域)的 Descriptor 就是我們想要的 Descriptor ,因為其它區域的 Descriptor 對鳥細粒度分類任務沒有增益。將選擇出來的 Descriptor

融合

起來便得到了用於 SCDA 的特徵。

卷積神經網路(CNN)的特徵圖視覺化和預訓練模型可解釋性的能力

該工作的作者是怎麼想到這樣的操作呢?我們可以觀察下圖,對左側的圖片提取特徵,然後視覺化一部分通道的特徵圖,觀察高響應區域。我們發現在第 108 通道上面,第一隻鳥激活了腳的部分、第二張圖片啟用的僅僅是背景、第三張圖片激活了狗的四肢、第四張圖片啟用的是周圍的草而並不是狗。在第 375 通道上面啟用的區域分別為:鳥的頭部、鳥的脖子、草、狗前後的背景。而第 284 個通道有的能啟用,有的不能啟用。

透過這四張圖片的視覺化實驗得出一個結論:

提取到的特徵圖有的通道是沒有資訊的,但是又無法透過單個特徵圖來表徵整個物體

(啟用沒有啟用整個鳥的特徵圖,而是啟用鳥身體的某一部分)

卷積神經網路(CNN)的特徵圖視覺化和預訓練模型可解釋性的能力

而如何根據特徵圖把有用的部分選取出來呢?在前面視覺化章節可知,當把某一層所有特徵圖疊加起來,會得到一個新的特徵圖。而這張新的特徵圖啟用的部分和原始圖片的物體、背景輪廓大體相似,尤其是在較為淺層的時候。但是對於當前任務來說,背景的資訊是沒有用處的,只需要圖片中物體的資訊即可。所以作者設定了一個閾值

 \overline a

,特徵圖中值比較大的不會被篩選掉,而值比較小的(視覺化時顏色較深)值會被篩選掉。這樣篩選後的特徵圖會過濾背景等無用資訊,但也可能會高響應背景資訊(如下圖

(c)

中左上角樹枝的部分)

卷積神經網路(CNN)的特徵圖視覺化和預訓練模型可解釋性的能力

至於該篩洗方法如何,可以看下面這幅圖。觀察第一行 mask map

M

和原圖疊加的結果,還是有背景資訊被高亮。例如第

(a)

中第二張右上角的屋簷、第三張中的樹枝和第五張中的礦泉水瓶。作者又進行了另一個操作,對篩選後的區域再再進行一次挑選,該操作時把區域面積較小的部分去掉。例如第5張圖中,狗的區域相比於礦泉水來說比較大,便捨棄掉礦泉水啟用的部分。

(b) \  Visualization \ of \  the \ mask \ map

便是最後選擇的結果,保留了最大的連結部件。

卷積神經網路(CNN)的特徵圖視覺化和預訓練模型可解釋性的能力

下面圖便是一個完整的過程,提取特徵、特徵圖求、利用閾值篩選區域、保留最大連結部件。最大連結部分對應的 Descriptor 就是想要的描述符。

卷積神經網路(CNN)的特徵圖視覺化和預訓練模型可解釋性的能力

下圖是透過上面的操作生成的區域(黃色實線)和標註的區域(紅色曲線)對比,效果還是非常不錯的。

卷積神經網路(CNN)的特徵圖視覺化和預訓練模型可解釋性的能力

Descriptor的融合

選定區域對應的 Descriptor 融合有很多解決方案,例如 VLAD(vector of locally aggregated descriptors)、Fisher Vector 和 Pooling。

卷積神經網路(CNN)的特徵圖視覺化和預訓練模型可解釋性的能力

融合什麼意思呢?打個比方,如果特徵圖是

7 \times 7 \times 512

形狀的,最後選出來的區域對應的 Descriptor 可能就是 20 個 512 的向量,而融合就是把這 20 個向量融合成一條向量,而這個融合後的向量便是選擇區域的特徵表示。作者對於三種方法都做了實驗:

卷積神經網路(CNN)的特徵圖視覺化和預訓練模型可解釋性的能力

最後發現把 maxpool 和 avgpool 的結果進行 concatenation 得到的效果是最好的。稱該 1024 的向量為 SCDA 向量。

剛剛我們談的區域選擇都是基於 pool5 層後的特徵圖,如果往前推兩層,在 Relu5_2 層輸出特徵圖進行區域選擇會如何?如下圖

(c) (d)

分別是兩次篩選後的結果,發現 Relu5_2 層輸出的結果能夠更好的捕捉物體的輪廓資訊。如果把 pool5 的結果和 Rele5_2 的結果同時考慮想必會提高模型的效能。於是同時考慮向量

 SCDA_{pool_5}

和向量

SCDA_{relu_{5\_2}}

後生成一個新的特徵表示向量

SCDA^{+}

。而透過另一種水平翻轉的策略得到

SCDA\_{flip^{+}}

,詳情可前去論文檢視。

卷積神經網路(CNN)的特徵圖視覺化和預訓練模型可解釋性的能力

這篇文章傳遞出來一個很重要的思想是,透過預訓練的模型提取的特徵可能會蘊含很多資訊,而細粒度分類任務只是展示出這些資訊的一小部分。

現在知道了如何提取有用的的 Descriptor,怎麼有效的使用這些 Decriptor 是一個問題,作者的工作 Part-based Mask-CNN 是一個很好的例子。如下圖所示,將一張鳥的圖片放入 FCN 中,進行一個三分類的分割。分割出背景、頭部、軀幹,便得到了三個 mask 。有了 mask就可以切割出鳥的腦袋和軀幹。

卷積神經網路(CNN)的特徵圖視覺化和預訓練模型可解釋性的能力

接下來把原始圖片(

448 \times 448

),頭部圖片(

224 \times 224

), 軀幹圖片(

224 \times 224

)分別放入 CNN 中,得到三組 Convolutional activattion tensor ,然後進行 Descriptor 的選擇,將三組圖片的 Descriptor 選擇後的結果進行 concatenation 。得到新的特徵表徵,然後進行分類。

卷積神經網路(CNN)的特徵圖視覺化和預訓練模型可解釋性的能力

參考連結

連結:

https://www。

jianshu。com/p/fb3add126

da1

連結:

https://

blog。csdn。net/missyougo

on/java/article/details/85645195