今天做實驗發現了一個非常奇怪的實驗現象,查了文獻後發現很早之前就有人注意到了這個現象,並且根據該現象發表了一系列成果。作者的實驗設計思路簡單有效,並且提出的方法也算新穎,給我很大啟發。今天就結合自己的所思所想,縷一縷這兩篇文章《Selective Convolutional Descriptor Aggregation》和《Part-based Mask-CNN》。都是很久之前的文章了,但是文章提及的思想很有啟發性。在正式梳理之前,先進行知識鋪墊,這會有助於後面的理解。
特徵提取
把一張 224 * 224 的圖片放入 VGG16 網路中,會經過 5 組卷積和 5 次 maxpool ,每次經過 maxpool 層都會得到不同尺寸大小的特徵圖,且特徵圖的通道數也不同。下圖是 VGG 網路的配置圖,其中高亮部分是 VGG16 網路的配置。
當圖片經過層層卷積和 maxpool ,到達最後一層 maxpool 下采樣層後會得到 7 * 7 且通道數為 512 的特徵圖。此處的特徵圖提取的是原圖片中較為抽象的特徵。至於每一層的特徵圖是什麼樣的,我們可以透過視覺化的辦法來觀察。視覺化方法如下:
卷積輸出視覺化
下面把一張貓的圖片放入預訓練的 VGG16 網路中去,並可視化每一層的特徵圖,透過特徵圖的響應區域來探究 VGG16 網路每層卷積的功能。視覺化參考至CSDN博主「我是小螞蟻」的原創文章,連結:
https://
blog。csdn。net/missyougo
on/java/article/details/85645195
。因為 VGG16 模型每一層通道比較多,這裡就只使用了前 25 個通道特徵圖作為展示。
原始圖片
第一層卷積提取的特徵圖
所有第一層特徵圖1:1融合後整體的特徵圖:
第二層:
所有第二層特徵圖1:1融合後整體的特徵圖:
第三層:
所有第三層特徵圖1:1融合後整體的特徵圖:
第四層:
所有第四層特徵圖1:1融合後整體的特徵圖:
第五層
第五層特徵圖1:1融合後整體的特徵圖:
可以看出淺層的卷積層獲得的特徵資訊還比較多,特徵資料與原始的影象資料很接近。隨著層數越深,得到的有效特徵越來越少,特徵也變得越來越抽象。最淺層的卷積核傾向於學習點、顏色等基礎特徵;接下來開始學習到線段、邊緣等特徵。層數越深,學習到的特徵就越具體越抽象。視覺化的程式碼為:
# -*- 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的定義
如下圖所示,把一張圖片放入卷積神經網路中後,會得到形狀為
的特徵圖,該特徵圖有
個通道,且每個通道上的特徵圖為
形狀。在文章 SCDA 中,將一個細長條稱為 Descriptor (下圖中有
個長條,且每個長條的長度為
)。例如把
圖片放入預訓練的 VGG16 網路中後,經過最後一次下采樣,會得到
形狀的特徵圖,也就是每個特徵圖形狀為
,一共 512 個通道。每個
是一個 Descriptor ,共有
個。
Descriptor的選擇
把下圖中鳥的圖片放入一個 pre-trained model (該 model 僅僅在 ImageNet 資料上訓練過)裡面,會得到類似於
的卷積啟用張量。該張量可能會啟用若干部分割槽域,啟用的區域可能是想要的區域(比如圖中鳥的區域),也有可能不是我們想要的區域(比如圖中的樹枝所在區域)。需要透過一個
選擇演算法
來選出我們想要的區域(如何選擇後面會提到)。而選擇後的區域(包含鳥的區域)的 Descriptor 就是我們想要的 Descriptor ,因為其它區域的 Descriptor 對鳥細粒度分類任務沒有增益。將選擇出來的 Descriptor
融合
起來便得到了用於 SCDA 的特徵。
該工作的作者是怎麼想到這樣的操作呢?我們可以觀察下圖,對左側的圖片提取特徵,然後視覺化一部分通道的特徵圖,觀察高響應區域。我們發現在第 108 通道上面,第一隻鳥激活了腳的部分、第二張圖片啟用的僅僅是背景、第三張圖片激活了狗的四肢、第四張圖片啟用的是周圍的草而並不是狗。在第 375 通道上面啟用的區域分別為:鳥的頭部、鳥的脖子、草、狗前後的背景。而第 284 個通道有的能啟用,有的不能啟用。
透過這四張圖片的視覺化實驗得出一個結論:
提取到的特徵圖有的通道是沒有資訊的,但是又無法透過單個特徵圖來表徵整個物體
(啟用沒有啟用整個鳥的特徵圖,而是啟用鳥身體的某一部分)
而如何根據特徵圖把有用的部分選取出來呢?在前面視覺化章節可知,當把某一層所有特徵圖疊加起來,會得到一個新的特徵圖。而這張新的特徵圖啟用的部分和原始圖片的物體、背景輪廓大體相似,尤其是在較為淺層的時候。但是對於當前任務來說,背景的資訊是沒有用處的,只需要圖片中物體的資訊即可。所以作者設定了一個閾值
,特徵圖中值比較大的不會被篩選掉,而值比較小的(視覺化時顏色較深)值會被篩選掉。這樣篩選後的特徵圖會過濾背景等無用資訊,但也可能會高響應背景資訊(如下圖
中左上角樹枝的部分)
至於該篩洗方法如何,可以看下面這幅圖。觀察第一行 mask map
和原圖疊加的結果,還是有背景資訊被高亮。例如第
中第二張右上角的屋簷、第三張中的樹枝和第五張中的礦泉水瓶。作者又進行了另一個操作,對篩選後的區域再再進行一次挑選,該操作時把區域面積較小的部分去掉。例如第5張圖中,狗的區域相比於礦泉水來說比較大,便捨棄掉礦泉水啟用的部分。
便是最後選擇的結果,保留了最大的連結部件。
下面圖便是一個完整的過程,提取特徵、特徵圖求、利用閾值篩選區域、保留最大連結部件。最大連結部分對應的 Descriptor 就是想要的描述符。
下圖是透過上面的操作生成的區域(黃色實線)和標註的區域(紅色曲線)對比,效果還是非常不錯的。
Descriptor的融合
選定區域對應的 Descriptor 融合有很多解決方案,例如 VLAD(vector of locally aggregated descriptors)、Fisher Vector 和 Pooling。
融合什麼意思呢?打個比方,如果特徵圖是
形狀的,最後選出來的區域對應的 Descriptor 可能就是 20 個 512 的向量,而融合就是把這 20 個向量融合成一條向量,而這個融合後的向量便是選擇區域的特徵表示。作者對於三種方法都做了實驗:
最後發現把 maxpool 和 avgpool 的結果進行 concatenation 得到的效果是最好的。稱該 1024 的向量為 SCDA 向量。
剛剛我們談的區域選擇都是基於 pool5 層後的特徵圖,如果往前推兩層,在 Relu5_2 層輸出特徵圖進行區域選擇會如何?如下圖
分別是兩次篩選後的結果,發現 Relu5_2 層輸出的結果能夠更好的捕捉物體的輪廓資訊。如果把 pool5 的結果和 Rele5_2 的結果同時考慮想必會提高模型的效能。於是同時考慮向量
和向量
後生成一個新的特徵表示向量
。而透過另一種水平翻轉的策略得到
,詳情可前去論文檢視。
這篇文章傳遞出來一個很重要的思想是,透過預訓練的模型提取的特徵可能會蘊含很多資訊,而細粒度分類任務只是展示出這些資訊的一小部分。
現在知道了如何提取有用的的 Descriptor,怎麼有效的使用這些 Decriptor 是一個問題,作者的工作 Part-based Mask-CNN 是一個很好的例子。如下圖所示,將一張鳥的圖片放入 FCN 中,進行一個三分類的分割。分割出背景、頭部、軀幹,便得到了三個 mask 。有了 mask就可以切割出鳥的腦袋和軀幹。
接下來把原始圖片(
),頭部圖片(
), 軀幹圖片(
)分別放入 CNN 中,得到三組 Convolutional activattion tensor ,然後進行 Descriptor 的選擇,將三組圖片的 Descriptor 選擇後的結果進行 concatenation 。得到新的特徵表徵,然後進行分類。
參考連結
連結:
https://www。
jianshu。com/p/fb3add126
da1
連結:
https://
blog。csdn。net/missyougo
on/java/article/details/85645195