CNN眼中的世界:利用Keras解釋CNN的濾波器


 轉載自:https://keras-cn.readthedocs.io/en/latest/legacy/blog/cnn_see_world/

文章信息

本文地址:http://blog.keras.io/how-convolutional-neural-networks-see-the-world.html

本文作者:Francois Chollet

 

使用Keras探索卷積網絡的濾波器

本文中我們將利用Keras觀察CNN到底在學些什么,它是如何理解我們送入的訓練圖片的。我們將使用Keras來對濾波器的激活值進行可視化。本文使用的神經網絡是VGG-16,數據集為ImageNet。本文的代碼可以在github找到

VGG-16又稱為OxfordNet,是由牛津視覺幾何組(Visual Geometry Group)開發的卷積神經網絡結構。該網絡贏得了ILSVR(ImageNet)2014的冠軍。時至今日,VGG仍然被認為是一個傑出的視覺模型——盡管它的性能實際上已經被后來的Inception和ResNet超過了。

Lorenzo Baraldi將Caffe預訓練好的VGG16和VGG19模型轉化為了Keras權重文件,所以我們可以簡單的通過載入權重來進行實驗。該權重文件可以在這里下載。國內的同學需要自備梯子。(這里是一個網盤保持的vgg16:http://files.heuritech.com/weights/vgg16_weights.h5趕緊下載,網盤什么的不知道什么時候就掛了。)

首先,我們在Keras中定義VGG網絡的結構:

from keras.models import Sequential
from keras.layers import Convolution2D, ZeroPadding2D, MaxPooling2D

img_width, img_height = 128, 128

# build the VGG16 network
model = Sequential()
model.add(ZeroPadding2D((1, 1), batch_input_shape=(1, 3, img_width, img_height)))
first_layer = model.layers[-1]
# this is a placeholder tensor that will contain our generated images
input_img = first_layer.input

# build the rest of the network
model.add(Convolution2D(64, 3, 3, activation='relu', name='conv1_1'))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(64, 3, 3, activation='relu', name='conv1_2'))
model.add(MaxPooling2D((2, 2), strides=(2, 2)))

model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(128, 3, 3, activation='relu', name='conv2_1'))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(128, 3, 3, activation='relu', name='conv2_2'))
model.add(MaxPooling2D((2, 2), strides=(2, 2)))

model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(256, 3, 3, activation='relu', name='conv3_1'))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(256, 3, 3, activation='relu', name='conv3_2'))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(256, 3, 3, activation='relu', name='conv3_3'))
model.add(MaxPooling2D((2, 2), strides=(2, 2)))

model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(512, 3, 3, activation='relu', name='conv4_1'))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(512, 3, 3, activation='relu', name='conv4_2'))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(512, 3, 3, activation='relu', name='conv4_3'))
model.add(MaxPooling2D((2, 2), strides=(2, 2)))

model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(512, 3, 3, activation='relu', name='conv5_1'))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(512, 3, 3, activation='relu', name='conv5_2'))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(512, 3, 3, activation='relu', name='conv5_3'))
model.add(MaxPooling2D((2, 2), strides=(2, 2)))

# get the symbolic outputs of each "key" layer (we gave them unique names).
layer_dict = dict([(layer.name, layer) for layer in model.layers])

注意我們不需要全連接層,所以網絡就定義到最后一個卷積層為止。使用全連接層會將輸入大小限制為224×224,即ImageNet原圖片的大小。這是因為如果輸入的圖片大小不是224×224,在從卷積過度到全鏈接時向量的長度與模型指定的長度不相符。

下面,我們將預訓練好的權重載入模型,一般而言我們可以通過model.load_weights()載入,但這里我們只載入一部分參數,如果使用該方法的話,模型和參數形式就不匹配了。所以我們需要手工載入:

import h5py

weights_path = 'vgg16_weights.h5'

f = h5py.File(weights_path)
for k in range(f.attrs['nb_layers']):
    if k >= len(model.layers):
        # we don't look at the last (fully-connected) layers in the savefile
        break
    g = f['layer_{}'.format(k)]
    weights = [g['param_{}'.format(p)] for p in range(g.attrs['nb_params'])]
    model.layers[k].set_weights(weights)
f.close()
print('Model loaded.')

下面,我們要定義一個損失函數,這個損失函數將用於最大化某個指定濾波器的激活值。以該函數為優化目標優化后,我們可以真正看一下使得這個濾波器激活的究竟是些什么東西。

現在我們使用Keras的后端來完成這個損失函數,這樣這份代碼不用修改就可以在TensorFlow和Theano之間切換了。TensorFlow在CPU上進行卷積要塊的多,而目前為止Theano在GPU上進行卷積要快一些。

from keras import backend as K

layer_name = 'conv5_1'
filter_index = 0  # can be any integer from 0 to 511, as there are 512 filters in that layer

# build a loss function that maximizes the activation
# of the nth filter of the layer considered
layer_output = layer_dict[layer_name].output
loss = K.mean(layer_output[:, filter_index, :, :])

# compute the gradient of the input picture wrt this loss
grads = K.gradients(loss, input_img)[0]

# normalization trick: we normalize the gradient
grads /= (K.sqrt(K.mean(K.square(grads))) + 1e-5)

# this function returns the loss and grads given the input picture
iterate = K.function([input_img], [loss, grads])

注意這里有個小trick,計算出來的梯度進行了正規化,使得梯度不會過小或過大。這種正規化能夠使梯度上升的過程平滑進行。

根據剛剛定義的函數,現在可以對某個濾波器的激活值進行梯度上升。

import numpy as np

# we start from a gray image with some noise
input_img_data = np.random.random((1, 3, img_width, img_height)) * 20 + 128.
# run gradient ascent for 20 steps
for i in range(20):
    loss_value, grads_value = iterate([input_img_data])
    input_img_data += grads_value * step

使用TensorFlow時,這個操作大概只要幾秒。

然后我們可以提取出結果,並可視化:

from scipy.misc import imsave

# util function to convert a tensor into a valid image
def deprocess_image(x):
    # normalize tensor: center on 0., ensure std is 0.1
    x -= x.mean()
    x /= (x.std() + 1e-5)
    x *= 0.1

    # clip to [0, 1]
    x += 0.5
    x = np.clip(x, 0, 1)

    # convert to RGB array
    x *= 255
    x = x.transpose((1, 2, 0))
    x = np.clip(x, 0, 255).astype('uint8')
    return x

img = input_img_data[0]
img = deprocess_image(img)
imsave('%s_filter_%d.png' % (layer_name, filter_index), img)

這里是第5卷基層第0個濾波器的結果:

 

可視化所有的濾波器

下面我們系統的可視化一下各個層的各個濾波器結果,看看CNN是如何對輸入進行逐層分解的。

第一層的濾波器主要完成方向、顏色的編碼,這些顏色和方向與基本的紋理組合,逐漸生成復雜的形狀。

可以將每層的濾波器想為基向量,這些基向量一般是過完備的。基向量可以將層的輸入緊湊的編碼出來。濾波器隨着其利用的空域信息的拓寬而更加精細和復雜,

 

可以觀察到,很多濾波器的內容其實是一樣的,只不過旋轉了一個隨機的的角度(如90度)而已。這意味着我們可以通過使得卷積濾波器具有旋轉不變性而顯著減少濾波器的數目,這是一個有趣的研究方向。

令人震驚的是,這種旋轉的性質在高層的濾波器中仍然可以被觀察到。如Conv4_1

 

Deep Dream(nightmare)

另一個有趣的事兒是,如果我們把剛才的隨機噪聲圖片替換為有意義的照片,結果就變的更好玩了。這就是去年由Google提出的Deep Dream。通過選擇特定的濾波器組合,我們可以獲得一些很有意思的結果。如果你對此感興趣,可以參考Keras的例子 Deep Dream和Google的博客Google blog post(牆)

 

愚弄神經網絡

如果我們添加上VGG的全連接層,然后試圖最大化某個指定類別的激活值呢?你會得到一張很像該類別的圖片嗎?讓我們試試。

這種情況下我們的損失函數長這樣:

layer_output = model.layers[-1].get_output()
loss = K.mean(layer_output[:, output_index])

比方說我們來最大化輸出下標為65的那個類,在ImageNet里,這個類是蛇。很快,我們的損失達到了0.999,即神經網絡有99.9%的概率認為我們生成的圖片是一條海蛇,它長這樣:

不太像呀,換個類別試試,這次選喜鵲類(第18類)

OK,我們的網絡認為是喜鵲的東西看起來完全不是喜鵲,往好了說,這個圖里跟喜鵲相似的,也不過就是一些局部的紋理,如羽毛,嘴巴之類的。那么,這就意味着卷積神經網絡是個很差的工具嗎?當然不是,我們按照一個特定任務來訓練它,它就會在那個任務上表現的不錯。但我們不能有網絡“理解”某個概念的錯覺。我們不能將網絡人格化,它只是工具而已。比如一條狗,它能識別其為狗只是因為它能以很高的概率將其正確分類而已,而不代表它理解關於“狗”的任何外延。

革命尚未成功,同志仍需努力

所以,神經網絡到底理解了什么呢?我認為有兩件事是它們理解的。

其一,神經網絡理解了如何將輸入空間解耦為分層次的卷積濾波器組。其二,神經網絡理解了從一系列濾波器的組合到一系列特定標簽的概率映射。神經網絡學習到的東西完全達不到人類的“看見”的意義,從科學的的角度講,這當然也不意味着我們已經解決了計算機視覺的問題。想得別太多,我們才剛剛踩上計算機視覺天梯的第一步。

有些人說,卷積神經網絡學習到的對輸入空間的分層次解耦模擬了人類視覺皮層的行為。這種說法可能對也可能不對,但目前未知我們還沒有比較強的證據來承認或否認它。當然,有些人可以期望人類的視覺皮層就是以類似的方式學東西的,某種程度上講,這是對我們視覺世界的自然解耦(就像傅里葉變換是對周期聲音信號的一種解耦一樣自然)【譯注:這里是說,就像聲音信號的傅里葉變換表達了不同頻率的聲音信號這種很自然很物理的理解一樣,我們可能會認為我們對視覺信息的識別就是分層來完成的,圓的是輪子,有四個輪子的是汽車,造型炫酷的汽車是跑車,像這樣】。但是,人類對視覺信號的濾波、分層次、處理的本質很可能和我們弱雞的卷積網絡完全不是一回事。視覺皮層不是卷積的,盡管它們也分層,但那些層具有皮質列的結構,而這些結構的真正目的目前還不得而知,這種結構在我們的人工神經網絡中還沒有出現(盡管喬大帝Geoff Hinton正在在這個方面努力)。此外,人類有比給靜態圖像分類的感知器多得多的視覺感知器,這些感知器是連續而主動的,不是靜態而被動的,這些感受器還被如眼動等多種機制復雜控制。

下次有風投或某知名CEO警告你要警惕我們深度學習的威脅時,想想上面說的吧。今天我們是有更好的工具來處理復雜的信息了,這很酷,但歸根結底它們只是工具,而不是生物。它們做的任何工作在哪個宇宙的標准下都不夠格稱之為“思考”。在一個石頭上畫一個笑臉並不會使石頭變得“開心”,盡管你的靈長目皮質會告訴你它很開心。

總而言之,卷積神經網絡的可視化工作是很讓人着迷的,誰能想到僅僅通過簡單的梯度下降法和合理的損失函數,加上大規模的數據庫,就能學到能很好解釋復雜視覺信息的如此漂亮的分層模型呢。深度學習或許在實際的意義上並不智能,但它仍然能夠達到幾年前任何人都無法達到的效果。現在,如果我們能理解為什么深度學習如此有效,那……嘿嘿:)

 

附:一張完整的可視化圖片(119M):https://share.weiyun.com/5uQPFr3

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM