CNN眼中的世界:利用Keras解釋CNN的濾波器文章信息本文地址:http://blog./how-convolutional-neural-networks-see-the-world.html 本文作者:Francois Chollet 使用Keras探索卷積網(wǎng)絡(luò)的濾波器本文中我們將利用Keras觀察CNN到底在學(xué)些什么,它是如何理解我們送入的訓(xùn)練圖片的。我們將使用Keras來對濾波器的激活值進(jìn)行可視化。本文使用的神經(jīng)網(wǎng)絡(luò)是VGG-16,數(shù)據(jù)集為ImageNet。本文的代碼可以在github找到 VGG-16又稱為OxfordNet,是由牛津視覺幾何組(Visual Geometry Group)開發(fā)的卷積神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)。該網(wǎng)絡(luò)贏得了ILSVR(ImageNet)2014的冠軍。時至今日,VGG仍然被認(rèn)為是一個杰出的視覺模型——盡管它的性能實際上已經(jīng)被后來的Inception和ResNet超過了。 Lorenzo Baraldi將Caffe預(yù)訓(xùn)練好的VGG16和VGG19模型轉(zhuǎn)化為了Keras權(quán)重文件,所以我們可以簡單的通過載入權(quán)重來進(jìn)行實驗。該權(quán)重文件可以在這里下載。國內(nèi)的同學(xué)需要自備梯子。(這里是一個網(wǎng)盤保持的vgg16:http://files./weights/vgg16_weights.h5趕緊下載,網(wǎng)盤什么的不知道什么時候就掛了。) 首先,我們在Keras中定義VGG網(wǎng)絡(luò)的結(jié)構(gòu): from keras.models import Sequentialfrom keras.layers import Convolution2D, ZeroPadding2D, MaxPooling2Dimg_width, img_height = 128, 128# build the VGG16 networkmodel = 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 imagesinput_img = first_layer.input# build the rest of the networkmodel.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]) 注意我們不需要全連接層,所以網(wǎng)絡(luò)就定義到最后一個卷積層為止。使用全連接層會將輸入大小限制為224×224,即ImageNet原圖片的大小。這是因為如果輸入的圖片大小不是224×224,在從卷積過度到全鏈接時向量的長度與模型指定的長度不相符。 下面,我們將預(yù)訓(xùn)練好的權(quán)重載入模型,一般而言我們可以通過 import h5pyweights_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.') 下面,我們要定義一個損失函數(shù),這個損失函數(shù)將用于最大化某個指定濾波器的激活值。以該函數(shù)為優(yōu)化目標(biāo)優(yōu)化后,我們可以真正看一下使得這個濾波器激活的究竟是些什么東西。 現(xiàn)在我們使用Keras的后端來完成這個損失函數(shù),這樣這份代碼不用修改就可以在TensorFlow和Theano之間切換了。TensorFlow在CPU上進(jìn)行卷積要塊的多,而目前為止Theano在GPU上進(jìn)行卷積要快一些。 from keras import backend as Klayer_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 consideredlayer_output = layer_dict[layer_name].outputloss = K.mean(layer_output[:, filter_index, :, :])# compute the gradient of the input picture wrt this lossgrads = K.gradients(loss, input_img)[0]# normalization trick: we normalize the gradientgrads /= (K.sqrt(K.mean(K.square(grads))) + 1e-5)# this function returns the loss and grads given the input pictureiterate = K.function([input_img], [loss, grads]) 注意這里有個小trick,計算出來的梯度進(jìn)行了正規(guī)化,使得梯度不會過小或過大。這種正規(guī)化能夠使梯度上升的過程平滑進(jìn)行。 根據(jù)剛剛定義的函數(shù),現(xiàn)在可以對某個濾波器的激活值進(jìn)行梯度上升。 import numpy as np# we start from a gray image with some noiseinput_img_data = np.random.random((1, 3, img_width, img_height)) * 20 + 128.# run gradient ascent for 20 stepsfor i in range(20): loss_value, grads_value = iterate([input_img_data]) input_img_data += grads_value * step 使用TensorFlow時,這個操作大概只要幾秒。 然后我們可以提取出結(jié)果,并可視化: from scipy.misc import imsave# util function to convert a tensor into a valid imagedef 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 ximg = input_img_data[0]img = deprocess_image(img)imsave('%s_filter_%d.png' % (layer_name, filter_index), img) 這里是第5卷基層第0個濾波器的結(jié)果: 可視化所有的濾波器下面我們系統(tǒng)的可視化一下各個層的各個濾波器結(jié)果,看看CNN是如何對輸入進(jìn)行逐層分解的。 第一層的濾波器主要完成方向、顏色的編碼,這些顏色和方向與基本的紋理組合,逐漸生成復(fù)雜的形狀。 可以將每層的濾波器想為基向量,這些基向量一般是過完備的?;蛄靠梢詫拥妮斎刖o湊的編碼出來。濾波器隨著其利用的空域信息的拓寬而更加精細(xì)和復(fù)雜, 可以觀察到,很多濾波器的內(nèi)容其實是一樣的,只不過旋轉(zhuǎn)了一個隨機(jī)的的角度(如90度)而已。這意味著我們可以通過使得卷積濾波器具有旋轉(zhuǎn)不變性而顯著減少濾波器的數(shù)目,這是一個有趣的研究方向。 令人震驚的是,這種旋轉(zhuǎn)的性質(zhì)在高層的濾波器中仍然可以被觀察到。如Conv4_1 Deep Dream(nightmare)另一個有趣的事兒是,如果我們把剛才的隨機(jī)噪聲圖片替換為有意義的照片,結(jié)果就變的更好玩了。這就是去年由Google提出的Deep Dream。通過選擇特定的濾波器組合,我們可以獲得一些很有意思的結(jié)果。如果你對此感興趣,可以參考Keras的例子Deep Dream和Google的博客Google blog post(墻) 愚弄神經(jīng)網(wǎng)絡(luò)如果我們添加上VGG的全連接層,然后試圖最大化某個指定類別的激活值呢?你會得到一張很像該類別的圖片嗎?讓我們試試。 這種情況下我們的損失函數(shù)長這樣: layer_output = model.layers[-1].get_output()loss = K.mean(layer_output[:, output_index]) 比方說我們來最大化輸出下標(biāo)為65的那個類,在ImageNet里,這個類是蛇。很快,我們的損失達(dá)到了0.999,即神經(jīng)網(wǎng)絡(luò)有99.9%的概率認(rèn)為我們生成的圖片是一條海蛇,它長這樣: 不太像呀,換個類別試試,這次選喜鵲類(第18類) OK,我們的網(wǎng)絡(luò)認(rèn)為是喜鵲的東西看起來完全不是喜鵲,往好了說,這個圖里跟喜鵲相似的,也不過就是一些局部的紋理,如羽毛,嘴巴之類的。那么,這就意味著卷積神經(jīng)網(wǎng)絡(luò)是個很差的工具嗎?當(dāng)然不是,我們按照一個特定任務(wù)來訓(xùn)練它,它就會在那個任務(wù)上表現(xiàn)的不錯。但我們不能有網(wǎng)絡(luò)“理解”某個概念的錯覺。我們不能將網(wǎng)絡(luò)人格化,它只是工具而已。比如一條狗,它能識別其為狗只是因為它能以很高的概率將其正確分類而已,而不代表它理解關(guān)于“狗”的任何外延。 革命尚未成功,同志仍需努力所以,神經(jīng)網(wǎng)絡(luò)到底理解了什么呢?我認(rèn)為有兩件事是它們理解的。 其一,神經(jīng)網(wǎng)絡(luò)理解了如何將輸入空間解耦為分層次的卷積濾波器組。其二,神經(jīng)網(wǎng)絡(luò)理解了從一系列濾波器的組合到一系列特定標(biāo)簽的概率映射。神經(jīng)網(wǎng)絡(luò)學(xué)習(xí)到的東西完全達(dá)不到人類的“看見”的意義,從科學(xué)的的角度講,這當(dāng)然也不意味著我們已經(jīng)解決了計算機(jī)視覺的問題。想得別太多,我們才剛剛踩上計算機(jī)視覺天梯的第一步。 有些人說,卷積神經(jīng)網(wǎng)絡(luò)學(xué)習(xí)到的對輸入空間的分層次解耦模擬了人類視覺皮層的行為。這種說法可能對也可能不對,但目前未知我們還沒有比較強(qiáng)的證據(jù)來承認(rèn)或否認(rèn)它。當(dāng)然,有些人可以期望人類的視覺皮層就是以類似的方式學(xué)東西的,某種程度上講,這是對我們視覺世界的自然解耦(就像傅里葉變換是對周期聲音信號的一種解耦一樣自然)【譯注:這里是說,就像聲音信號的傅里葉變換表達(dá)了不同頻率的聲音信號這種很自然很物理的理解一樣,我們可能會認(rèn)為我們對視覺信息的識別就是分層來完成的,圓的是輪子,有四個輪子的是汽車,造型炫酷的汽車是跑車,像這樣】。但是,人類對視覺信號的濾波、分層次、處理的本質(zhì)很可能和我們?nèi)蹼u的卷積網(wǎng)絡(luò)完全不是一回事。視覺皮層不是卷積的,盡管它們也分層,但那些層具有皮質(zhì)列的結(jié)構(gòu),而這些結(jié)構(gòu)的真正目的目前還不得而知,這種結(jié)構(gòu)在我們的人工神經(jīng)網(wǎng)絡(luò)中還沒有出現(xiàn)(盡管喬大帝Geoff Hinton正在在這個方面努力)。此外,人類有比給靜態(tài)圖像分類的感知器多得多的視覺感知器,這些感知器是連續(xù)而主動的,不是靜態(tài)而被動的,這些感受器還被如眼動等多種機(jī)制復(fù)雜控制。 下次有風(fēng)投或某知名CEO警告你要警惕我們深度學(xué)習(xí)的威脅時,想想上面說的吧。今天我們是有更好的工具來處理復(fù)雜的信息了,這很酷,但歸根結(jié)底它們只是工具,而不是生物。它們做的任何工作在哪個宇宙的標(biāo)準(zhǔn)下都不夠格稱之為“思考”。在一個石頭上畫一個笑臉并不會使石頭變得“開心”,盡管你的靈長目皮質(zhì)會告訴你它很開心。 總而言之,卷積神經(jīng)網(wǎng)絡(luò)的可視化工作是很讓人著迷的,誰能想到僅僅通過簡單的梯度下降法和合理的損失函數(shù),加上大規(guī)模的數(shù)據(jù)庫,就能學(xué)到能很好解釋復(fù)雜視覺信息的如此漂亮的分層模型呢。深度學(xué)習(xí)或許在實際的意義上并不智能,但它仍然能夠達(dá)到幾年前任何人都無法達(dá)到的效果?,F(xiàn)在,如果我們能理解為什么深度學(xué)習(xí)如此有效,那……嘿嘿:) @fchollet, 2016年1月 |
|