引言
??本實驗基于FNC(全卷積神經(jīng)網(wǎng)絡(luò))及PASCAL-VOC數(shù)據(jù)集做圖像語義分割。圖像語義分割(Semantic Segmentation)是圖像處理和是機器視覺技術(shù)中關(guān)于圖像理解的重要一環(huán),也是 AI 領(lǐng)域中一個重要的分支。語義分割即是對圖像中每一個像素點進行分類,確定每個點的類別(如屬于背景、人或車等),從而進行區(qū)域劃分。目前,語義分割已經(jīng)被廣泛應(yīng)用于自動駕駛、無人機落點判定等場景中。
??圖像分類是圖像級別的:圖像中的局部抽象特征對物體的大小、位置和方向等敏感性更低,從而有助于分類性能的提高。這些抽象的特征對分類很有幫助,可以很好地判斷出一幅圖像中包含什么類別的物體。
??圖像語義分割是像素級別的:與分類不同的是,語義分割需要判斷圖像每個像素點的類別,進行精確分割。但是由于CNN在進行convolution和pooling過程中丟失了圖像細節(jié),即feature map size逐漸變小,所以不能很好地指出物體的具體輪廓、指出每個像素具體屬于哪個物體,無法做到精確的分割。
??Jonathan Long等人提出了Fully Convolutional Networks(FCN)用于圖像語義分割。自從提出后,F(xiàn)CN已經(jīng)成為語義分割的基本框架,后續(xù)算法其實都是在這個框架中改進而來。本實驗就基于FNC做圖像語義分割。
卷積神經(jīng)網(wǎng)絡(luò)
??卷積神經(jīng)網(wǎng)絡(luò)(CNN)是一種經(jīng)典的深度學(xué)習(xí)架構(gòu),受生物自然視覺認知機制啟發(fā)而來。1959年,Hubel & Wiesel 發(fā)現(xiàn),貓對于物體的感知在視覺皮層中是分層處理的。受此啟發(fā),1980年 Kunihiko Fukushima 提出了CNN的前身——neocognitron 。
??20世紀(jì) 90 年代,LeCun等人發(fā)表論文,確立了CNN的現(xiàn)代結(jié)構(gòu),后來又對其進行完善。他們設(shè)計了一種多層的人工神經(jīng)網(wǎng)絡(luò),取名叫做LeNet-5,可以對手寫數(shù)字做分類。和其他神經(jīng)網(wǎng)絡(luò)一樣, LeNet-5 也能使用反向傳播算法訓(xùn)練。
??卷積神經(jīng)網(wǎng)絡(luò)能夠得出原始圖像的有效特征,因而其能夠直接從原始像素中,經(jīng)過極少的預(yù)處理,識別視覺層面的規(guī)律。然而,由于當(dāng)時缺乏大規(guī)模訓(xùn)練數(shù)據(jù),計算機的計算能力不足等因素,卷積神經(jīng)網(wǎng)絡(luò)并未得到科學(xué)界的重視。
??進入21世紀(jì)后,隨著科學(xué)的發(fā)展,計算機的計算能力也逐漸得到了巨大的提升,而卷積神經(jīng)網(wǎng)絡(luò)的在圖像分類問題上的應(yīng)用也開始逐漸得到人們的重視。ImageNet是被譽為圖像分類領(lǐng)域里的奧林匹克比賽,2012年,通過卷積神經(jīng)網(wǎng)絡(luò)構(gòu)建的圖像分類算法在這場比賽中脫穎而出,將圖像分類識別的準(zhǔn)確度一下提升了許多,卷積神經(jīng)網(wǎng)絡(luò),也因此死灰復(fù)燃,再次得到了學(xué)術(shù)界的關(guān)注。
??此后,卷積神經(jīng)網(wǎng)絡(luò)也在不斷地被計算機科學(xué)家們優(yōu)化著,出現(xiàn)了諸如GoogLeNet、VGG等較之前更為優(yōu)秀結(jié)構(gòu)。計算機圖像分類能力也因此得到巨大提升,這是這種強大的圖像分類、處理能力的誕生,才推動了現(xiàn)代深度增強學(xué)習(xí)的發(fā)展。
??卷積神經(jīng)網(wǎng)絡(luò)的基本結(jié)構(gòu)一般包括三層,特征提取層,特征映射層,以及全連接層。對特征提取層來講,每一個神經(jīng)元的輸入與前一層的局部接受域相連,并提取該局部的特征。當(dāng)該局部特征被提取后,它與其它特征間的位置關(guān)系也隨之確定下來;對特征映射層來講,網(wǎng)絡(luò)的每個計算層由多個特征映射組成,每個特征映射是一個平面,平面上所有神經(jīng)元的權(quán)值相等。特征映射結(jié)構(gòu)采用影響函數(shù)核小的sigmoid函數(shù)作為卷積網(wǎng)絡(luò)的激活函數(shù),使得特征映射具有位移不變性。此外,由于一個映射面上的神經(jīng)元共享權(quán)值,因而減少了網(wǎng)絡(luò)自由參數(shù)的個數(shù)。卷積神經(jīng)網(wǎng)絡(luò)中的每一個卷積層都緊跟著一個用來求局部平均與二次提取的計算層,這種特有的兩次特征提取結(jié)構(gòu)減小了特征分辨率。最后,還包括一層全連接層。
??卷積神經(jīng)網(wǎng)絡(luò)主要用來識別位移、縮放及其他形式扭曲不變性的二維圖形。由于卷積神經(jīng)網(wǎng)絡(luò)的特征檢測層通過訓(xùn)練數(shù)據(jù)進行學(xué)習(xí),所以在使用卷積神經(jīng)網(wǎng)絡(luò)時,避免了顯示的特征抽取,而隱式地從訓(xùn)練數(shù)據(jù)中進行學(xué)習(xí);再者由于同一特征映射面上的神經(jīng)元權(quán)值相同,所以網(wǎng)絡(luò)可以并行學(xué)習(xí),這也是卷積網(wǎng)絡(luò)相對于神經(jīng)元彼此相連網(wǎng)絡(luò)的一大優(yōu)勢。卷積神經(jīng)網(wǎng)絡(luò)以其局部權(quán)值共享的特殊結(jié)構(gòu)在語音識別和圖像處理方面有著獨特的優(yōu)越性,其布局更接近于實際的生物神經(jīng)網(wǎng)絡(luò),權(quán)值共享降低了網(wǎng)絡(luò)的復(fù)雜性,特別是多維輸入向量的圖像可以直接輸入網(wǎng)絡(luò)這一特點避免了特征提取和分類過程中數(shù)據(jù)重建的復(fù)雜度。
??卷積神經(jīng)網(wǎng)絡(luò)的三種組成部分:卷積層、池化層,以及全連接層如圖2-1所示,該圖展示了一個較為完整的卷積神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)圖。
卷積層
??卷積神經(jīng)網(wǎng)絡(luò)處理的對象是圖片,而圖片在計算機上又是以像素值的形式存取的。對于彩色圖片來說,通常其具有RGB三個通道,對于灰色通道來講,其只有一個灰度值通道。當(dāng)知道了圖像每一個像素點的值,我們就能還原出圖像的原貌。而卷積神經(jīng)網(wǎng)絡(luò),就是對這些像素值進行處理的,本質(zhì)上還是進行數(shù)值的計算。
??圖2-2展示了一個簡明的卷積計算。左側(cè)方框中,標(biāo)有0和1的方框是我們的數(shù)據(jù),這里,我們將其當(dāng)作圖像像素值,圖中較深陰影部分,即為卷積核,卷積核在圖像滾動,每滾動一次,計算一次卷積核的值與對應(yīng)位置像素值的乘積,再相加。得到圖片右側(cè)的卷積特征(Convolved Feature)。
池化層
??在通過卷積獲得了特征 (features) 之后,下一步我們希望利用這些特征去做分類。理論上講,人們可以用所有提取得到的特征去訓(xùn)練分類器,例如 softmax 分類器,但這樣做,面臨計算量的挑戰(zhàn)。例如:對于一個 96×96 像素的圖像,假設(shè)我們已經(jīng)學(xué)習(xí)得到了400個定義在8×8輸入上的特征,每一個特征和圖像卷積都會得到一個 (96 ? 8 + 1) × (96 ? 8 + 1) = 7921 維的卷積特征,由于有 400 個特征,所以每個樣例都會得到一個 892 × 400 = 3,168,400 維的卷積特征向量。學(xué)習(xí)一個擁有超過 3 百萬特征輸入的分類器十分不便,并且容易出現(xiàn)過擬合 (over-fitting)。
??為了解決這個問題,我們需要對不同位置的特征進行聚合統(tǒng)計,例如,人們可以計算圖像一個區(qū)域上的某個特定特征的平均值 (或最大值)。這些概要統(tǒng)計特征不僅具有低得多的維度 (相比使用所有提取得到的特征),同時還會改善結(jié)果(不容易過擬合)。這種聚合的操作就叫做池化 (pooling),有時也稱為平均池化或者最大池化。
全連接層
??為了使我們的卷積神經(jīng)網(wǎng)絡(luò)能夠起到分類器的作用,一般地,我們需要在卷積神經(jīng)網(wǎng)絡(luò)的后面來添加全連接層。如果說卷積層、池化層等操作是將原始數(shù)據(jù)映射至隱層特征空間的話,全連接層則起到將學(xué)到的“分布式特征表示”映射到樣本標(biāo)記空間的作用。
優(yōu)化算法與反向傳播算法
??以梯度下降法為代表的很多優(yōu)化算法可以用來解決上述的最小化問題。諸如AdamOptimizer,RMSPropOptimizer等優(yōu)化器,在神經(jīng)網(wǎng)絡(luò)框架TensorFlow、Keras中都可以直接調(diào)用,本課題中,我們選用AdamOptimizer對神經(jīng)網(wǎng)絡(luò)進行優(yōu)化。
??Adam 的全稱為Adaptive moment estimation,即自適應(yīng)矩估計。在概率論中,矩的含義是:如果一個隨機變量 X 服從某個分布,X 的一階矩是,也就是樣本平均值,X 的二階矩即為
,也就是樣本平方的平均值。Adam 算法根據(jù)損失函數(shù)對每個參數(shù)的梯度的一階矩估計和二階矩估計動態(tài)調(diào)整針對于每個參數(shù)的學(xué)習(xí)速率。Adam 也是基于梯度下降的方法,但是每次迭代參數(shù)的學(xué)習(xí)步長都有一個確定的范圍,不會因為很大的梯度導(dǎo)致很大的學(xué)習(xí)步長,參數(shù)的值比較穩(wěn)定。
??在優(yōu)化過程中,這些算法通常需要求解偏導(dǎo)數(shù)。反向傳播算法就是用來計算這些偏導(dǎo)數(shù)的。
1986年,深度學(xué)習(xí)之父Hinton,和他的合作者發(fā)表了論文Learning Representations by Back-propagating errors, 首次系統(tǒng)地描述了如何利用反向傳播算法(Backpropagation)有訓(xùn)練神經(jīng)網(wǎng)絡(luò). 從這一年開始,反向傳播算法在有監(jiān)督的神經(jīng)網(wǎng)絡(luò)算法中占著核心地位。它描述了如何利用錯誤信息,從最后一層(輸出層)開始到第一個隱層,逐步調(diào)整權(quán)值參數(shù),達到學(xué)習(xí)的目的。
??反向傳播算法是目前用來訓(xùn)練人工神經(jīng)網(wǎng)絡(luò)(ANN)的最常用且最有效的算法。其主要思想是:
??(1)將訓(xùn)練集數(shù)據(jù)輸入到ANN的輸入層,經(jīng)過隱藏層,最后達到輸出層并輸出結(jié)果,這是ANN的前向傳播過程;
??(2)由于ANN的輸出結(jié)果與實際結(jié)果有誤差,則計算估計值與實際值之間的誤差,并將該誤差從輸出層向隱藏層反向傳播,直至傳播到輸入層;
??(3)在反向傳播的過程中,根據(jù)誤差調(diào)整各種參數(shù)的值;不斷迭代上述過程,直至收斂。
FCN網(wǎng)絡(luò)結(jié)構(gòu)
??1.image經(jīng)過多個conv和+一個max pooling變?yōu)閜ool1 feature,寬高變?yōu)?/2 (conv只提特征,pool進行下降操作)
??2.pool1 feature再經(jīng)過多個conv+一個max pooling變?yōu)閜ool2 feature,寬高變?yōu)?/4
??3.pool2 feature再經(jīng)過多個conv+一個max pooling變?yōu)閜ool3 feature,寬高變?yōu)?/8
??4.pool3 feature再經(jīng)過多個conv+一個max pooling變?yōu)閜ool4 feature,寬高變?yōu)?/16
??5.pool4 feature再經(jīng)過多個conv+一個max pooling變?yōu)閜ool5 feature,寬高變?yōu)?/32
??直接對pool4 feature進行32倍上采樣獲得32x upsampled feature,再對32x upsampled feature每個點做softmax prediction獲得32x upsampled feature prediction(即分割圖)。
FCN網(wǎng)絡(luò)結(jié)構(gòu)的Python代碼解析
??對輸入圖像做卷積,卷積核個數(shù)為64個,卷積核大小為(3, 3),過濾的模式為same,做兩次這樣的卷積,之后為空域信號施加最大值池化,池化窗口大小為(2, 2)將使圖片在兩個維度上均變?yōu)樵L的一半,激活函數(shù)選擇ReLu,得到layer_3:
layer_1 = Convolution2D(64, (3, 3), activation='relu', padding='same', name='Conv1Block1')(input_image)
layer_2 = Convolution2D(64, (3, 3), activation='relu', padding='same', name='Conv2Block1')(layer_1)
layer_3 = MaxPooling2D((2, 2), strides=(2, 2), name='MaxPoolBlock1')(layer_2)
??之后再對其做一次類似的操作,選取卷積核個數(shù)為128個,激活函數(shù)選擇ReLu,得到layer_6:
layer_4 = Convolution2D(128, (3, 3), activation='relu', padding='same', name='Conv1Block2')(layer_3)
layer_5 = Convolution2D(128, (3, 3), activation='relu', padding='same', name='Conv2Block2')(layer_4)
layer_6 = MaxPooling2D((2, 2), strides=(2, 2), name='MaxPoolBlock2')(layer_5)
??之后再對其做一次類似的操作,選取卷積核個數(shù)為256個,做三次卷積一次最大池化,激活函數(shù)選擇ReLu,得到layer_10:
layer_7 = Convolution2D(256, (3, 3), activation='relu', padding='same', name='Conv1Block3')(layer_6)
layer_8 = Convolution2D(256, (3, 3), activation='relu', padding='same', name='Conv2Block3')(layer_7)
layer_9 = Convolution2D(256, (3, 3), activation='relu', padding='same', name='Conv3Block3')(layer_8)
layer_10 = MaxPooling2D((2, 2), strides=(2, 2), name='MaxPoolBlock3')(layer_9)
??之后再對其做一次類似的操作,選取卷積核個數(shù)為512個,做三次卷積一次最大池化,激活函數(shù)選擇ReLu,得到layer_14:
layer_11 = Convolution2D(512, (3, 3), activation='relu', padding='same', name='Conv1Block4')(layer_10)
layer_12 = Convolution2D(512, (3, 3), activation='relu', padding='same', name='Conv2Block4')(layer_11)
layer_13 = Convolution2D(512, (3, 3), activation='relu', padding='same', name='Conv3Block4')(layer_12)
layer_14 = MaxPooling2D((2, 2), strides=(2, 2), name='MaxPoolBlock4')(layer_13)
??之后再對其做一次類似的操作,選取卷積核個數(shù)為512個,做三次卷積一次最大池化,激活函數(shù)選擇ReLu,得到layer_18:
layer_15 = Convolution2D(512, (3, 3), activation='relu', padding='same', name='Conv1Block5')(layer_14)
layer_16 = Convolution2D(512, (3, 3), activation='relu', padding='same', name='Conv2Block5')(layer_15)
layer_17 = Convolution2D(512, (3, 3), activation='relu', padding='same', name='Conv3Block5')(layer_16)
layer_18 = MaxPooling2D((2, 2), strides=(2, 2), name='MaxPoolBlock5')(layer_17)
??之后我們再次對其卷積,不做全連接,來獲取對每個像素的預(yù)測分數(shù),選取卷積核個數(shù)為4096個,大小為(7, 7)做第一次卷積,之后再選取卷積核個數(shù)同樣為4096個大小為(1, 1)得到layer_20,再做一個卷積核為21個,大小(1, 1)的卷積,輸出得到layer_21:
#Making the network fully convolutional
layer_19=Convolution2D(4096,kernel_size=(7,7),padding="same",activation="relu",name="FullConv1")(layer_18)
layer_20=Convolution2D(4096,kernel_size=(1,1),padding="same",activation="relu",name="FullConv2")(layer_19)
#For obtaining the semantic segmentation scores
layer_21=Convolution2D(21,kernel_size=(1,1),padding="same",activation="relu",name="Score1")(layer_20)
??之后進行裝置卷積:
layer_22 = Deconvolution2D(21,kernel_size=(4,4),strides = (2,2),padding = "valid",activation=None,name = "Score2")(layer_21)
??裁剪一下:
layer_23 = Cropping2D(cropping=((0,2),(0,2)))(layer_22)
??跳躍結(jié)構(gòu)(Skip Architecture),將全卷積后的結(jié)果上采樣后得到的結(jié)果通常是很粗糙的。所以這一結(jié)構(gòu)主要是用來優(yōu)化最終結(jié)果的,思路就是將不同池化層的結(jié)果進行上采樣,然后結(jié)合這些結(jié)果來優(yōu)化輸出:
skip_con=Convolution2D(21,kernel_size=(1,1),padding="same",activation=None, name="SkipConn")
summed=add(inputs = [skip_con(layer_14),layer_23])
??上采樣得到輸出:
layer_24=Deconvolution2D(21,kernel_size=(32,32),strides=(16,16),padding="valid",activation = None,name = "Upsampled")
??裁剪一下使其適應(yīng)輸出:
crop = Cropping2D(cropping = ((0,16),(0,16)))
??定義網(wǎng)絡(luò)模型:
model = Model(inputs = input_image, outputs = crop(layer_24(summed)))
??之后加載訓(xùn)練好的模型即可跑前向網(wǎng)絡(luò),獲得預(yù)測輸出結(jié)果,并將其顯示出來:
model = Model(inputs = input_image, outputs = crop(layer_24(summed)))
model.summary()
model.load_weights("weights.h5")
test_image = Image.open('TestingImages/2007_000170.jpg')
test_image = test_image.resize((512,512))
image_arr = np.array(test_image).astype(np.float32)
image_arr = np.expand_dims(image_arr, axis=0)
preds=model.predict(image_arr)
imclass = np.argmax(preds, axis=3)[0,:,:]
plt.figure(figsize = (15, 7))
plt.subplot(1,3,1)
plt.imshow( np.asarray(test_image) )
plt.subplot(1,3,2)
plt.imshow( imclass )
plt.subplot(1,3,3)
plt.imshow( np.asarray(test_image) )
masked_imclass = np.ma.masked_where(imclass == 0, imclass)
plt.imshow( imclass, alpha=0.5 )
plt.show()
??完整Python代碼如下所示:
import numpy as np
from keras.models import Model
from keras.layers import Convolution2D, MaxPooling2D, Deconvolution2D, Cropping2D
from keras.layers import Input, add
import matplotlib.pyplot as plt
from PIL import Image
input_image = Input(shape = (512,512,3))
# Block 1
layer_1 = Convolution2D(64, (3, 3), activation='relu', padding='same', name='Conv1Block1')(input_image)
layer_2 = Convolution2D(64, (3, 3), activation='relu', padding='same', name='Conv2Block1')(layer_1)
layer_3 = MaxPooling2D((2, 2), strides=(2, 2), name='MaxPoolBlock1')(layer_2)
# Block 2
layer_4 = Convolution2D(128, (3, 3), activation='relu', padding='same', name='Conv1Block2')(layer_3)
layer_5 = Convolution2D(128, (3, 3), activation='relu', padding='same', name='Conv2Block2')(layer_4)
layer_6 = MaxPooling2D((2, 2), strides=(2, 2), name='MaxPoolBlock2')(layer_5)
#Block 3
layer_7 = Convolution2D(256, (3, 3), activation='relu', padding='same', name='Conv1Block3')(layer_6)
layer_8 = Convolution2D(256, (3, 3), activation='relu', padding='same', name='Conv2Block3')(layer_7)
layer_9 = Convolution2D(256, (3, 3), activation='relu', padding='same', name='Conv3Block3')(layer_8)
layer_10 = MaxPooling2D((2, 2), strides=(2, 2), name='MaxPoolBlock3')(layer_9)
#Block 4
layer_11 = Convolution2D(512, (3, 3), activation='relu', padding='same', name='Conv1Block4')(layer_10)
layer_12 = Convolution2D(512, (3, 3), activation='relu', padding='same', name='Conv2Block4')(layer_11)
layer_13 = Convolution2D(512, (3, 3), activation='relu', padding='same', name='Conv3Block4')(layer_12)
layer_14 = MaxPooling2D((2, 2), strides=(2, 2), name='MaxPoolBlock4')(layer_13)
#Block 5
layer_15 = Convolution2D(512, (3, 3), activation='relu', padding='same', name='Conv1Block5')(layer_14)
layer_16 = Convolution2D(512, (3, 3), activation='relu', padding='same', name='Conv2Block5')(layer_15)
layer_17 = Convolution2D(512, (3, 3), activation='relu', padding='same', name='Conv3Block5')(layer_16)
layer_18 = MaxPooling2D((2, 2), strides=(2, 2), name='MaxPoolBlock5')(layer_17)
#Making the network fully convolutional
layer_19 = Convolution2D(4096,kernel_size=(7,7),padding = "same",activation = "relu",name = "FullConv1")(layer_18)
layer_20 = Convolution2D(4096,kernel_size=(1,1),padding = "same",activation = "relu",name = "FullConv2")(layer_19)
#For obtaining the semantic segmentation scores
layer_21 = Convolution2D(21,kernel_size=(1,1),padding="same",activation="relu",name = "Score1")(layer_20)
layer_22 = Deconvolution2D(21,kernel_size=(4,4),strides = (2,2),padding = "valid",activation=None,name = "Score2")(layer_21)
#Cropping the image to make it compatible for addition
layer_23 = Cropping2D(cropping=((0,2),(0,2)))(layer_22)
#Adding a skip connection
skip_con = Convolution2D(21,kernel_size=(1,1),padding = "same",activation=None, name = "SkipConn")
#Adding the layers
summed = add(inputs = [skip_con(layer_14),layer_23])
#Upsampling the output
layer_24 = Deconvolution2D(21,kernel_size=(32,32),strides = (16,16),padding = "valid",activation = None,name = "Upsampled")
#Cropping the output to match the input size
crop = Cropping2D(cropping = ((0,16),(0,16)))
#Defining the model with the layers
model = Model(inputs = input_image, outputs = crop(layer_24(summed)))
model.summary()
model.load_weights("weights.h5")
test_image = Image.open('TestingImages/2007_000256.jpg')
test_image = test_image.resize((512,512))
image_arr = np.array(test_image).astype(np.float32)
image_arr = np.expand_dims(image_arr, axis=0)
preds=model.predict(image_arr)
imclass = np.argmax(preds, axis=3)[0,:,:]
plt.figure(figsize = (15, 7))
plt.subplot(1,3,1)
plt.imshow( np.asarray(test_image) )
plt.subplot(1,3,2)
plt.imshow( imclass )
plt.subplot(1,3,3)
plt.imshow( np.asarray(test_image) )
masked_imclass = np.ma.masked_where(imclass == 0, imclass)
plt.imshow( imclass, alpha=0.5 )
plt.show()
??其網(wǎng)絡(luò)結(jié)構(gòu)如圖2-3所示:
??其實驗結(jié)果如圖2-4所示:
??完整實驗代碼,公眾號后臺回復(fù):數(shù)字圖像作業(yè)二。
我的微信公眾號名稱:深度學(xué)習(xí)與先進智能決策
微信公眾號ID:MultiAgent1024
公眾號介紹:主要研究分享深度學(xué)習(xí)、機器博弈、強化學(xué)習(xí)等相關(guān)內(nèi)容!期待您的關(guān)注,歡迎一起學(xué)習(xí)交流進步!