不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化


卷積神經網絡(CNN)變革了計算機視覺,並將徹底改變整個世界。因此,開發解釋 CNN 的技術也同樣是一個重要的研究領域。本文將向你解釋如何僅使用 40 行 Python 代碼將卷積特徵可視化。

最近在閱讀 Jeremy Rifkin 的書《The End of Work》時,我讀到一個有趣的關於 AI 的定義。Rifkin 寫到:「今天,當科學家們談論人工智能時,他們通常是指『一門創造機器的藝術,該機器所執行的功能在人類執行時需要智能』(Kurzweil, Raymond, The Age of Intelligent Machines (Cambridge, MA: MIT Press, 1990), p. 14.)」。我很喜歡這個定義,因為它避免了類似」在人類智力意義上 AI 是否真正達到智能」的討論。

作為一名科學家,揭開大腦功能的基本原理並創造一個真正的智能機器的想法確實讓我很興奮,但是我認為認識到深度學習模型並不是大腦模型這一點非常重要。深度學習研究的目的是從數據中學習到目前為止還沒有自動化的流程的規則並實現自動化。雖然這聽起來並不是那麼讓人興奮,但它確實是一件好事。舉個例子:深度卷積神經網絡的出現徹底改變了計算機視覺和模式識別,這讓我們在醫療診斷中可以大量地引入自動化;人們可以加速為貧窮國家的人提供頂級醫療診斷,而不需要在本地培訓大量的醫生和專家。

儘管深度學習給人們帶來了許多振奮的消息,但它如何看待和解釋世界仍然是一個黑匣子。更好地理解它們如何識別特定的模式和對象,以及為什麼它們能夠表現地如此良好,可以讓我們:1)進一步改進它們;2)解決法律問題——因為在許多情況下機器所做出的決定必須能夠被人類所理解。

有兩種方法可以嘗試理解神經網絡如何識別某種模式。如果你想知道哪種模式可以顯著地激活某個特徵圖,你可以:1)嘗試在數據集中查找導致此特徵圖高於平均激活的圖像;2)嘗試通過優化隨機圖像中的像素值來生成這種模式。後者的想法是由 Erhan 等人提出的。

在本文中我將向你解釋如何僅用 40 行 Python 代碼來實現隨機圖像的像素值優化(如下圖),從而生成卷積神經網絡的特徵可視化。

不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化

本文的結構如下:首先,我將展示 VGG-16 網絡的幾個層次中的卷積特徵的可視化;然後,嘗試理解其中一些可視化,我將展示如何快速測試一個假設,即特定的濾波器會檢測到哪種模式;最後,我將解釋創建本文中提供的模式所需的代碼。

特徵可視化

神經網絡學習將輸入數據(如圖像)轉換為越來越有意義但表徵越來越複雜的連續層。

你可以將深度網絡看做一個多階段信息蒸餾操作,其中信息通過連續的濾波器並不斷被「提純」。(François Chollet, Deep Learning with Python (Shelter Island, NY: Manning Publications, 2018), p. 9)

閱讀完他的文章後,你將瞭解如何生成模式,以最大化這些層次表徵的某個層中所選特徵圖的平均激活,如何解釋其中一些可視化,以及最終如何測試所選濾波器可能響應的模式或紋理的假設。你可以在下面找到 VGG-16 網絡多個層中濾波器的特徵可視化。在查看它們時,希望你能觀察到生成模式的複雜性如何隨著進入網絡的深度而增加。

Layer 7: Conv2d(64, 128)

不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化

濾波器 12, 16, 86, 110(左上到右下,逐行)

Layer 14: Conv2d(128, 256)

不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化

濾波器 1, 6, 31, 32, 54, 77, 83, 97, 125, 158, 162, 190(左上到右下,逐行)

Layer 20: Conv2d(256, 256)

不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化

濾波器 3, 34, 39, 55, 62, 105, 115, 181, 231(左上到右下,逐行)

Layer 30: Conv2d(512, 512)

不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化


不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化

濾波器 54, 62, 67, 92, 123, 141, 150, 172, 180, 213, 233, 266, 277, 293, 331, 350, 421, 427(左上到右下,逐行)

Layer 40: Conv2d(512, 512)—top of the network

不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化


不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化

濾波器 4, 9, 34, 35, 75, 123, 150, 158, 203, 234, 246, 253, 256, 261, 265, 277, 286, 462(左上到右下,逐行)

這些模式讓我覺得非常震撼!部分原因是,它們太漂亮了,我都想立馬將它們裱起來掛在牆上;但主要的原因是,它們僅僅是通過最大化由數千張圖像訓練出的數學方程中的某個值得到的。在瀏覽通過最大化最後一層卷積層特徵圖的平均激活得到的 512 個模式時,我經常發出感慨「哇,這是一隻雞」,或「這不是一根羽毛嘛」。

識別模式

我們來嘗試解釋幾個可視化的特徵。從這個開始,有沒有讓你想起些什麼?

不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化

第 40 層第 286 個濾波器

這張照片立刻讓我想起了教堂拱形天花板的圓拱。

不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化

來,讓我們檢驗一下。人造的那張圖片是通過最大化第 40 層第 286 個特徵圖的平均激活創造出來的。我們來看看當把拱門的照片輸入網絡後,第 40 層特徵圖的平均激活會是怎樣:

不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化

看到了什麼?正如期望的那樣,特徵圖上的 286 處有一個極強的尖峰。所以,這是否意味著第 40 層第 286 個濾波器是負責檢測拱形天花板的呢?這裡我們要小心一點,濾波器 286 顯然會響應圖像中的拱形結構,但請記住,這樣的拱形結構可能會在幾個不同的類別中起到重要作用。

注意:雖然我使用第 40 層(卷積層)來生成我們當前正在查看的圖像,但我使用了第 42 層來生成顯示每個特徵圖的平均激活的圖。第 41 和 42 層是 batch-norm 和 ReLU。ReLU 激活函數刪除所有負值,選擇第 42 層而不是 40 的唯一原因是,後者將顯示大量負噪聲,這使得我們很難看到我們感興趣的正峰值。

再來一例。

不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化

第 40 層第 256 個濾波器

我敢保證,這些是雞頭(或者至少是鳥頭)!從圖中我們可以看到尖尖的喙和黑色的眼睛。我們可以用下面的這個圖片來檢驗:

不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化

類似地,特徵圖上的 256 處會出現強烈的尖峰:

不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化

再來:

不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化

第 40 層第 462 個濾波器

濾波器 462 會不會對羽毛作出反應呢?來,輸入一張羽毛圖片:

不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化

Yes!濾波器 462 果然反應強烈:

不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化

猜一猜濾波器 265 會對什麼產生響應?

不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化

第 40 層第 265 個濾波器

或許是鏈條吧?來,我們輸入一張試試:

不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化

Yes,看起來猜對了!

不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化

不過從上圖可以看到,除了最大的尖峰外,還有幾個較大的次尖峰。我們看看對應的第 95 和第 303 個特徵可視化圖是什麼:

不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化

第 40 層第 95 和第 303 個濾波器

再來看張比較酷的:

不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化

第 40 層第 64 個濾波器

有許多看起來像羽毛一樣的結構,似乎還有鳥腿,左下方有個類似鳥頭的東西。腿和喙顯然比雞的長,所以可能是一隻鳥。我們將下面這幅圖輸入網絡:

不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化

得到這樣的特徵圖:

不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化

好吧,,在 64 處確實有個尖峰,但好像有許多比它更高的。讓我們再來看看其中四個特徵尖峰對應的濾波器生成的模式:

不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化

第 40 層第 172 和第 288 個濾波器

第 40 層第 437 和第 495 個濾波器

上面兩幅(172、288)圖看起來似乎有更多腿和眼睛/喙;不過下面兩幅(437、495)我實在看不出它表示了什麼。也許這些模式與圖像的背景相關,或者只是代表網絡檢測到了一些我所不能理解的鳥類的信息。我想現在這個地方仍然是黑匣子的一部分。

再來最後一張,比較可愛,之後我們就直接進入代碼部分。你能猜到這個是什麼嗎?

不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化

第 40 層第 277 個濾波器

我擼貓多年,所以我立馬看到了毛茸茸的貓耳。左上角那個較大的最為明顯。好,讓我們輸入一張貓圖:

不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化

Yes,特徵圖上 277 處確實有一個強烈的尖峰,但是旁邊更強烈的尖峰是怎麼回事?

不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化

我們快速看下特徵圖 281 對應的模式圖:

不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化

第 40 層第 281 個濾波器

也許是條紋貓的皮毛?

對於從網絡中發現上述這樣的秘密,我簡直樂此不疲。但事實上,即使在最終的卷積層,大多數濾波器生成的模式對我來說還是非常抽象的。更為嚴格的方法應該是將網絡應用於整個包含許多不同種類圖像的數據集,並跟蹤在某一層中激發特定濾波器的圖像。

代碼詳解

思路大致如下:我們從包含隨機像素的圖片開始,將它輸入到評估模式的網絡中,計算特定層中某個特徵圖的平均激活,然後計算輸入圖像像素值的梯度;知道像素值的梯度後,我們繼續以最大化所選特徵圖的平均激活的方式更新像素值。

貌似這樣說不好理解,那我們換種方式:網絡權重是固定的,網絡也不會被訓練,我們試圖找到一個圖像,通過在像素值上執行梯度下降優化來最大化特定特徵圖的平均激活。

這個技術也叫做神經風格遷移。

為了實現這一點,我們需要:

  1. 從隨機圖像開始;
  2. 評估模式下的預訓練網絡;
  3. 一種訪問我們感興趣的任何隱藏層激活函數的方式;
  4. 用於計算漸梯度的損失函數和用於更新像素值的優化器。

我們先來生成一張帶噪圖作為輸入。我們可以如下這樣做:

img = np.uint8(np.random.uniform(150, 180, (sz, sz, 3)))/255

其中 sz 是圖像的長、寬,3 是顏色通道數,我們除以 255 是因為 uint8 類型的變量最大值是 255。如果你想得到更多或更少的噪聲,可以修改 150 和 180。

然後我們用

img_var = V(img[None], requires_grad=True)

將之轉化為一個需要梯度的 PyTorch 變量。像素值需要梯度,因為我們要使用反向傳播來優化它們。

接著,我們需要一個評估模式下(意味著其權重是不變的)的預訓練網絡。這可以用如下代碼:

model = vgg16(pre=True).eval

set_trainable(model, False).

再然後,我們需要一種方式來獲取隱藏層的特徵。我們可以採用在我們感興趣的某個隱藏層後進行截斷的方式獲取,這樣該隱藏層就成了輸出層。不過在 PyTorch 中有一種更好的方法來解決這個問題,稱為」hook」,可以在 PyTorch 的 Module 或 Tensor 中說明。要理解這點,你需要知道:

  1. PyTorch Module 是所有神經網絡模塊的基本類;
  2. 我們的神經網絡的每個層都是一個 Module ;
  3. 每個 Module 都有一個稱為 forward 的方法,當給 Module 一個輸入時它會計算輸出。

當我們將噪聲圖輸入到我們的網絡中時,forward 方法就會計算出第一層的輸出結果;第二層的輸入是前一層 forward 方法的輸出結果;以此類推。當我們在某個層「registerforward hook」時,在該層的 forward 方法被調用後將執行「hook」。

例如,當我們對層 i 的特徵映射感興趣時,我們在 i 層 register 一個「forword hook」;當層 i 的 forward 方法被調用後,層 i 的特徵就會保存在一個變量裡。

保存變量的類如下:

class SaveFeatures:

def __init__(self, module):

self.hook = module.register_forward_hook(self.hook_fn)

def hook_fn(self, module, input, output):

self.features = torch.tensor(output,requires_grad=True).cuda

def close(self):

self.hook.remove

當執行 hook 時,調用方法 hook_fn。hook_fn 方法會將層輸出保存在 self.features。注意這個張量是需要梯度的,因為我們要在像素值上執行反向傳播。

怎麼用 SaveFeatures 對象呢?

對層 i 的 hook 為:

activations = SaveFeatures(list(self.model.children)[i])

當你用 model(img_var) 將模型應用到圖像上後,你可以通過 hook 保存在 activations.features 來訪問特徵。注意別忘了一點,在訪問完畢後使用 close 釋放內存。

好了,現在我們已經能夠訪問層 i 的特徵圖了。特徵圖的形式為 [ 1, 512, 7, 7 ],其中 1 表示批維度,512 表示濾波器/特徵圖的個數,7 表示特徵圖的長和寬。我們的目標就是最大化選擇的某一特徵圖 j 的平均激活值。因此我們定義如下損失函數:

loss = -activations.features[0, j].mean

以及優化器:

optimizer = torch.optim.Adam([img_var], lr=lr, weight_decay=1e-6)

默認情況下,優化器可以最大限度地減少損失,因此我們只需將平均激活乘以 -1 即可告知優化器最大化損失。使用 optimizer.zero_grad 重置梯度,使用 loss.backward 計算像素值的梯度,並使用 optimizer.step 更改像素值。

我們現在已經有了所有需要的東西:從隨機圖像開始,在評估模式下定義預先訓練的網絡,執行前向傳播以獲取第 i 層的特徵,並定義了允許我們更改像素值以最大化層 i 中特徵映射 j 的平均激活的優化器和損失函數。

好,讓我們看一個例子:

不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化

第 40 層,第 265 個濾波器

等等,這不正是我們想要的嗎?和前面的鏈條模式很相似;如果你眯著眼睛看,就可以看到鏈條。但可以肯定的是,我們獲得的特徵圖的局部性必定非常差,因此我們必須找到一種方法來指導我們的優化器以獲得最小化模式或者或「更好看」的模式。與我前面展示的模式相反,這張圖由高頻模式佔主導,類似於對抗樣本。那麼我們該怎麼解決這個問題呢?我嘗試了不同的優化器、學習速率以及正則化,但似乎沒有任何東西可以減少高頻模式。

接下來,我變化了一下輸入噪聲圖像的尺寸:

不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化

圖像大小分別為 200x200、300x300、400x400。

在這三幅圖中,有沒有發現「鏈狀圖案」的頻率隨著圖像尺寸的增加而增加?由於卷積濾波器具有固定的尺寸,因此當圖像分辨率增大時,濾波器的相對尺寸就會變小。換句話說:假設創建的模式都是以像素為單位,當我們增加圖像尺寸,則生成圖案的相對尺寸將減小,而圖案的頻率將增加。

如果我的假設是正確的,那麼我們想要的是低分辨率樣本(甚至比上面顯示的還低)但高分辨的低頻模式。這有意義嗎?以及怎麼做?

我的解決方案是:首先從非常低分辨率的圖像開始,即 56×56 像素,優化像素值幾步,然後將圖像尺寸增加一定的係數;在對圖像放大後,再將像素值優化幾步;然後再次對圖像進行放大...

這樣獲得的結果出奇的好,一個低頻、高分辨率、且沒有太多噪音的模式圖:

不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化

第 40 層,第 265 個濾波器。

我們現在有了一個分辨率好得多的低頻模式,並且沒有太多的噪音。為什麼這樣做會有用呢?我的想法是:當我們從低分辨率開始時,我們會得到低頻模式。放大後,放大後的模式圖相比直接用大尺度圖像優化生成的模式圖有較低的頻率。因此,在下一次迭代中優化像素值時,我們處於一個更好的起點,看起來避免了局部最小值。這有意義嗎?為了進一步減少高頻模式,我在放大後稍微模糊了圖像。

我發現以 1.2 的倍數放大 12 次之後得到的結果不錯。

看看下面的代碼。你會發現我們已經將重點信息都講清了,例如創建隨機圖像、register hook、定義優化器和損失函數,以及優化像素值。唯一重要的新內容是:1)將代碼封裝到一個類中;2)我們在優化像素值後將圖像放大了幾次。

class FilterVisualizer:

def __init__(self, size=56, upscaling_steps=12, upscaling_factor=1.2):

self.size, self.upscaling_steps, self.upscaling_factor = size, upscaling_steps, upscaling_factor

self.model = vgg16(pre=True).cuda.eval

set_trainable(self.model, False)

def visualize(self, layer, filter, lr=0.1, opt_steps=20, blur=None):

sz = self.size

img = np.uint8(np.random.uniform(150, 180, (sz, sz, 3)))/255 # generate random image

activations = SaveFeatures(list(self.model.children)[layer]) # register hook

for _ in range(self.upscaling_steps): # scale the image up upscaling_steps times

train_tfms, val_tfms = tfms_from_model(vgg16, sz)

img_var = V(val_tfms(img)[None], requires_grad=True) # convert image to Variable that requires grad

optimizer = torch.optim.Adam([img_var], lr=lr, weight_decay=1e-6)

for n in range(opt_steps): # optimize pixel values for opt_steps times

optimizer.zero_grad

self.model(img_var)

loss = -activations.features[0, filter].mean

loss.backward

optimizer.step

img = val_tfms.denorm(img_var.data.cpu.numpy[0].transpose(1,2,0))

self.output = img

sz = int(self.upscaling_factor * sz) # calculate new image size

img = cv2.resize(img, (sz, sz), interpolation = cv2.INTER_CUBIC) # scale image up

if blur is not None: img = cv2.blur(img,(blur,blur)) # blur image to reduce high frequency patterns

self.save(layer, filter)

activations.close

def save(self, layer, filter):

plt.imsave("layer_"+str(layer)+"_filter_"+str(filter)+".jpg", np.clip(self.output, 0, 1))

使用 FilterVisualizer :

layer = 40

filter = 265

FV = FilterVisualizer(size=56, upscaling_steps=12, upscaling_factor=1.2)

FV.visualize(layer, filter, blur=5)

代碼默認使用英偉達的 GPU,如果沒有,可以在 google colab 上測試。

需要python教程+PDF電子書的小夥伴

添加老師微信:15804144724 即可最高抽取2000元助學金哦

請關注、轉發、私信我"學習"就能限時領取400集Python視頻教程


不可思議 40行Python代碼,實現(卷積神經網絡)卷積特徵可視化



分享到:


相關文章: