全卷積神經網路FCN詳解(附帶Tensorflow詳解程式碼實現)
一.導論
在影象語義分割領域,困擾了電腦科學家很多年的一個問題則是我們如何才能將我們感興趣的物件和不感興趣的物件分別分割開來呢?比如我們有一隻小貓的圖片,怎樣才能夠通過計算機自己對影象進行識別達到將小貓和圖片當中的背景互相分割開來的效果呢?如下圖所示:
而在2015年出來的FCN,全卷積神經網路完美地解決了這個問題,將曾經mean IU(識別平均準確度)只有百分之40的成績提升到了百分之62.2(在Pascal VOC資料集上跑的結果,FCN論文上寫的),畫素級別識別精確度則是90.2%。這已經是一個相當完美的結果了,幾乎超越了人類對影象進行區分,分割的能力。如上圖所示,小貓被分割為了背景,小貓,邊緣這三個部分,因此影象當中的每一個畫素最後只有三個預測值,是否為小貓,背景,或者邊緣。全卷積網路要做的就是這種進行畫素級別的分類任務。那麼這個網路是如何設計和實現的呢?
二.網路的實現
這個網路的實現雖然聽名字十分霸氣,全卷積神經網路。不過事實上使用這個名字無非是把卷積網路的最後幾層用於分類的全連線層換成了1*1的卷積網路,所以才叫這個名字。這個網路的首先對圖片進行卷積——>卷積——>池化,再卷積——>卷積——>池化,直到我們的影象縮小得夠小為止。這個時候就可以進行上取樣,恢復影象的大小,那麼什麼是上取樣呢?你估計還沒有聽說過,等下咱們一 一道來。這個網路的結構如下所示(附上論文上的原圖):
從中可以看到我們輸入了一個小貓和小狗在一起時的圖片,最後再前向傳播,在這個前向傳播網路的倒數第三層,卷積神經網路的長度就變成了N*N*21,因為在VOC資料集上一共有21個softmax分類的結果,因此每個類別都需要有一個相關概率(置信度)的輸出。而前面這個前向傳播的卷積神經網路可以是VGG16,也可以是AlexNet,Google Inception Net,甚至是ResNet,論文作者在前三個Net上都做了相應的嘗試,但是因為ResNet當時還沒出來,也就沒有嘗試過在前面的網路當中使用它。當我們的網路變成了一個N*N*21的輸出時,我們將影象進行上取樣,上採用就相當於把我們剛才得到的具有21個分類輸出的結果還原成一個和原影象大小相同,channel相同的圖。這個圖上的每個畫素點都代表了21個事物類別的概率,這樣就可以得到這個圖上每一個畫素點應該分為哪一個類別的概率了。那麼什麼是影象的上取樣呢?
三.影象的上取樣
影象的上取樣正好和卷積的方式相反,我們可以通過正常的卷積讓影象越來越小,而上取樣則可以同樣通過卷積將影象變得越來越大,最後縮放成和原影象同樣大小的圖片,關於上取樣的論文在這這篇論文當專門做了詳解:https://arxiv.org/abs/1603.07285。上取樣有3種常見的方法:雙線性插值(bilinear),反捲積(Transposed Convolution),反池化(Unpooling)。在全卷積神經網路當中我們採用了反捲積來實現了上取樣。我們先來回顧一下正向卷積,也稱為下采樣,正向的卷積如下所示,首先我們擁有一個這樣的3*3的卷積核:
然後對一個5*5的特徵圖利用滑動視窗法進行卷積操作,padding=0,stride=1,kernel size=3,所以最後得到一個3*3的特徵圖:
那麼上取樣呢?則是這樣的,我們假定輸入只是一個2*2的特徵圖,輸出則是一個4*4的特徵圖,我們首先將原始2*2的map進行周圍填充pading=2的操作,筆者查閱了很多資料才知道,這裡周圍都填充了數字0,周圍的padding並不是通過神經網路訓練得出來的數字。然後用一個kernel size=3,stride=1的感受野掃描這個區域,這樣就可以得到一個4*4的特徵圖了!:
我們甚至可以把這個2*2的feature map,每一個畫素點隔開一個空格,空格里的數字填充為0,周圍的padding填充的數字也全都為零,然後再繼續上取樣,得到一個5*5的特徵圖,如下所示:
這樣咱們的反捲積就完成了。那麼什麼是1*1卷積呢?
四.1*1卷積
在我們的卷積神經網路前向傳播的過程當中,最後是一個N*N*21的輸出,這個21是可以我們進行人為通過1*1卷積定義出來的,這樣我們才能夠得到一個21個類別,每個類別出現的概率,最後輸出和原圖影象大小一致的那個特徵圖,每個畫素點上都有21個channel,表示這個畫素點所具有的某個類別輸出的概率值。吳恩達教授在講解卷積神經網路的時候,用到了一張十分經典的影象來表示1*1卷積:
原本的特徵圖長寬為28,channel為192,我們可以通過這種卷積,使用32個卷積核將28*28*192變成一個28*28*32的特徵圖。在使用1*1卷積時,得到的輸出長款保持不變,channel數量和卷積核的數量相同。可以用抽象的3d立體圖來表示這個過程:
因此我們可以通過控制卷積核的數量,將資料進行降維或者升維。增加或者減少channel,但是feature map的長和寬是不會改變的。我們在全卷積神經網路(FCN)正向傳播,下采樣的最後一步(可以檢視本部落格的第一張圖片)就是將一個N*N*4096的特徵圖變成了一個N*N*21的特徵圖。
五.全卷積神經網路的跳級實現(skip)
我們如果直接採用首先卷積,然後上取樣得到與原圖尺寸相同特徵圖的方法的話,進行語義分割的效果經過實驗是不太好的。因為在進行卷積的時候,在特徵圖還比較大的時候,我們提取到的影象資訊非常豐富,越到後面影象的資訊丟失得就越明顯。我們可以發現經過最前面的五次卷積和池化之後,原圖的分別率分別縮小了2,4,8,16,32倍。對於最後一層的的影象,需要進行32倍的上取樣才能夠得到和原圖一樣的大小,但僅依靠最後一層影象做上取樣,得到的結果還是不太準確,一些細節依然很不準確。因此作者採用了跳級連線的方法,即將在卷積的前幾層提取到的特徵圖分別和後面的上取樣層相連,然後再相加繼續網上往上上取樣,上取樣多次之後就可以得到和原圖大小一致的特徵圖了,這樣也可以在還原影象的時候能夠得到更多原圖所擁有的資訊。如下圖所示:
作者最先提出的跳級連線是把第五層的輸出進行上取樣,然後和池化層4的預測相結合起來,最後得到原圖的策略,這個策略叫做FCN-16S,之後又嘗試了和所有池化層結合起來預測的方法叫做FCN-8S,發現這個方法準確率是最高的。如下圖所示:
Groud Truth表示原始影象的人為標註,前面的都是神經網路做出的預測。跳級連線,我們這類給出的原圖的大小是500*500*3,這個尺寸無所謂,因為全卷積神經網路可接受任意尺寸大小的圖片。我們首先從前面綠色剛剛從池化層做完maxpool的特徵圖上做一次卷積然後,然後再把下一個綠色的特徵圖做卷積,最後把16*16*21,已經做完1*1卷積的輸出,把這個三個輸出相加在一起,這樣就實現了跳級(skip)輸入的實現,再把這幾個輸入融合之後的結果進行上取樣,得到一個568*568*21的圖,將這個圖通過一個softmax層變成500*500*21的特徵圖,因此影象的長寬和原圖一模一樣了,每一個畫素點都有21個概率值,表示這個畫素點屬於某個類別的概率,除了和原圖的channel不同之外沒啥不同的。
然後我們來看基於Tensorflow的程式碼實現。
六.Tensorflow程式碼實現全卷積神經網路
首先導包並讀取圖片資料:
import tensorflow as tf import matplotlib.pyplot as plt import numpy as np import os import glob images=glob.glob(r"F:\UNIVERSITY STUDY\AI\dataset\FCN\images\*.jpg") #然後讀取目標影象 anno=glob.glob(r"F:\UNIVERSITY STUDY\AI\dataset\FCN\annotations\trimaps\*.png")
glob庫可以用於讀取本地的圖片並用來製作每一個batch的資料,我把資料集放在了F:\UNIVERSITY STUDY\AI\dataset\FCN\,這個資料夾下。
冪image資料夾用於裝載訓練集的圖片,annatation資料夾用於裝載人們標註邊界的資料集。
標註的圖片顯示如下:
原始圖是一個小狗的圖片,原始圖在下面:
然後製作dataset,batch資料,以及讀取圖片檔案的函式,包括png和jpg分別進行解析為三維矩陣:
#現在對讀取進來的資料進行製作batch np.random.seed(2019) index=np.random.permutation(len(images)) images=np.array(images)[index] anno=np.array(anno)[index] #建立dataset dataset=tf.data.Dataset.from_tensor_slices((images,anno)) test_count=int(len(images)*0.2) train_count=len(images)-test_count data_train=dataset.skip(test_count) data_test=dataset.take(test_count) def read_jpg(path): img=tf.io.read_file(path) img=tf.image.decode_jpeg(img,channels=3) return img def read_png(path): img=tf.io.read_file(path) img=tf.image.decode_png(img,channels=1) return img #現在編寫歸一化的函式 def normal_img(input_images,input_anno): input_images=tf.cast(input_images,tf.float32) input_images=input_images/127.5-1 input_anno-=1 return input_images,input_ann #載入函式 def load_images(input_images_path,input_anno_path): input_image=read_jpg(input_images_path) input_anno=read_png(input_anno_path) input_image=tf.image.resize(input_image,(224,224)) input_anno=tf.image.resize(input_anno,(224,224)) return normal_img(input_image,input_anno) data_train=data_train.map(load_images,num_parallel_calls=tf.data.experimental.AUTOTUNE) data_test=data_test.map(load_images,num_parallel_calls=tf.data.experimental.AUTOTUNE) #現在開始batch的製作 BATCH_SIZE=3#根據視訊記憶體進行調整 data_train=data_train.repeat().shuffle(100).batch(BATCH_SIZE) data_test=data_test.batch(BATCH_SIZE)
然後我們使用VGG16進行卷積操作,同時使用imagenet的預訓練模型進行遷移學習,搭建神經網路和跳級連線:
conv_base=tf.keras.applications.VGG16(weights='imagenet', input_shape=(224,224,3), include_top=False) #現在建立子model用於繼承conv_base的權重,用於獲取模型的中間輸出 #使用這個方法居然能夠繼承,而沒有顯式的指定到底繼承哪一個模型,確實神奇 #確實是可以使用這個的,這個方法就是在模型建立完之後再進行的呼叫 #這樣就會繼續自動繼承之前的網路結構 #而如果定義 sub_model=tf.keras.models.Model(inputs=conv_base.input, outputs=conv_base.get_layer('block5_conv3').output) #現在建立多輸出模型,三個output layer_names=[ 'block5_conv3', 'block4_conv3', 'block3_conv3', 'block5_pool' ] layers_output=[conv_base.get_layer(layer_name).output for layer_name in layer_names] #建立一個多輸出模型,這樣一張圖片經過這個網路之後,就會有多個輸出值了 #不過輸出值雖然有了,怎麼能夠進行跳級連線呢? multiout_model=tf.keras.models.Model(inputs=conv_base.input, outputs=layers_output) multiout_model.trainable=False inputs=tf.keras.layers.Input(shape=(224,224,3)) #這個多輸出模型會輸出多個值,因此前面用多個引數來接受即可。 out_block5_conv3,out_block4_conv3,out_block3_conv3,out=multiout_model(inputs) #現在將最後一層輸出的結果進行上取樣,然後分別和中間層多輸出的結果進行相加,實現跳級連線 #這裡表示有512個卷積核,filter的大小是3*3 x1=tf.keras.layers.Conv2DTranspose(512,3, strides=2, padding='same', activation='relu')(out) #上取樣之後再加上一層卷積來提取特徵 x1=tf.keras.layers.Conv2D(512,3,padding='same', activation='relu')(x1) #與多輸出結果的倒數第二層進行相加,shape不變 x2=tf.add(x1,out_block5_conv3) #x2進行上取樣 x2=tf.keras.layers.Conv2DTranspose(512,3, strides=2, padding='same', activation='relu')(x2) #直接拿到x3,不使用 x3=tf.add(x2,out_block4_conv3) #x3進行上取樣 x3=tf.keras.layers.Conv2DTranspose(256,3, strides=2, padding='same', activation='relu')(x3) #增加捲積提取特徵 x3=tf.keras.layers.Conv2D(256,3,padding='same',activation='relu')(x3) x4=tf.add(x3,out_block3_conv3) #x4還需要再次進行上取樣,得到和原圖一樣大小的圖片,再進行分類 x5=tf.keras.layers.Conv2DTranspose(128,3, strides=2, padding='same', activation='relu')(x4) #繼續進行卷積提取特徵 x5=tf.keras.layers.Conv2D(128,3,padding='same',activation='relu')(x5) #最後一步,影象還原 preditcion=tf.keras.layers.Conv2DTranspose(3,3, strides=2, padding='same', activation='softmax')(x5) model=tf.keras.models.Model( inputs=inputs, outputs=preditcion )
編譯和fit模型:
model.compile( optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['acc']#這個引數應該是用來列印正確率用的,現在終於理解啦啊 ) model.fit(data_train, epochs=1, steps_per_epoch=train_count//BATCH_SIZE, validation_data=data_test, validation_steps=train_count//BATCH_SIZE)
輸出:
Train for 1970 steps, validate for 1970 steps 1969/1970 [============================>.] - ETA: 1s - loss: 0.3272 - acc: 0.8699WARNING:tensorflow:Your input ran out of data; interrupting training. Make sure that your dataset or generator can generate at least `steps_per_epoch * epochs` batches (in this case, 1970 batches). You may need to use the repeat() function when building your dataset. 1970/1970 [==============================] - 3233s 2s/step - loss: 0.3271 - acc: 0.8699 - val_loss: 0.0661 - val_acc: 0.8905
結果只用了一個epoch,畫素精確度就已經達到了百分之89了,是不是很神奇呢?