1. 程式人生 > >使用卷積神經網路進行圖片分類 2

使用卷積神經網路進行圖片分類 2

使用caffe構建卷積神經網路

一、實驗介紹

1.1 實驗內容

上一次實驗我們介紹了卷積神經網路的基本原理,本次實驗我們將學習如何使用深度學習框架caffe構建卷積神經網路,你將看到在深度學習框架上搭建和訓練模型是一件非常簡單快捷的事情(當然,是在你已經理解了基本原理的前提下)。如果上一次實驗中的一些知識點你還理解的不夠透徹,這次以及之後的實驗正是通過實際操作加深對它們理解的好機會。

1.2 實驗知識點

  • caffe 網路總體結構
  • caffe 訓練資料的準備
  • caffe network定義檔案的編寫
  • SoftmaxWithLoss 損失層
  • Accuracy 準確率層
  • ReLU 啟用函式層

1.3 實驗環境

  • python 2.7
  • caffe 1.0.0 (實驗樓環境已經預先安裝)

二、實驗步驟

2.1 專案的引入 & 網路總體結構

在正式介紹和使用caffe之前,我們先來大致確定我們的深度神經網路結構。和課程814一樣,本課程也是識別圖片中的英文字母。不過這次我們是使用caffe來訓練我們的模型,並且我們會向網路模型中加入前面介紹的卷積層。我們的網路模型大致是這樣的:

此處輸入圖片的描述

圖中有一些你還沒見過的東西,不過現在你只需要關注綠色的資料輸入層、紫色的損失函式層、三個橙色的卷積層和兩個紅色的卷積層。
可以看到,我們的神經網路先用三個卷積層提取圖片中的特徵,然後是一個含有100個節點的全連線層ip1,最後是含有26個節點的用於分類的全連線層ip2

和損失函式層。

和課程814相比,我們似乎只是多了卷積層。

2.2 caffe簡要介紹

caffe(Convolutional Architecture for Fast Feature Embedding)和好多軟體專案一樣,它看起來複雜且有些讓人摸不著頭腦的英文全稱很可能只是為了獲得一個有意思的縮寫名。所以不要被它的名字迷惑,不過名字裡的Convolutional也暗示了caffe特別擅長構建卷積神經網路。目前,caffe是使用最多的深度學習框架之一。其作者Yangqing Jia(沒錯,他是中國人)在Facebook。caffe支援使用CPUGPU進行訓練,當使用GPU時,能夠獲得更快的訓練速度(實驗樓環境中目前沒有GPU資源可以使用,不過這不影響我們的實驗)。

2.3 為caffe準備訓練資料

2.3.1 獲取資料

caffe支援多種訓練資料輸入方式,其中最常用的一種是先將訓練圖片儲存到lmdb資料庫中,在訓練的過程中直接從lmdb資料庫中讀取資料。除非你有興趣,不然你暫時不必關心如何掌握lmdb資料庫,因為caffe已經為我們提供了將圖片轉化成lmdb資料的指令碼。
我已經事先準備好了一些訓練圖片,過為了方便,我把圖片的解析度改成了16*16。你可以使用以下命令獲取這些訓練資料:

wget http://labfile.oss.aliyuncs.com/courses/820/cnndata.tar.gztar zxvf cnndata.tar.gz

注:cnndata.tar.gz中也包括之後我們會用到的network.prototxtsolver.prototxt

解壓之後,你當前的目錄結構應該如下:

.├── cnndata.tar.gz├── network.prototxt├── pic├── solver.prototxt├── test.txt├── train.txt└── validate.txt1 directory, 6 files

注意之後我們會編寫的network.prototxtsolver.prototxt也包含在了裡面,進行下面的實驗時,你可以開啟network.prototxt對照著檢視完整的網路定義。

其實這個專案對於caffe來說有些太簡單了,而且也無法完全體現出caffe的優勢,本來我是想做人臉性別識別的專案的,但最後考慮到訓練時間的問題,還是決定採用這個更加簡單的專案。完成本課程後,你可以自己嘗試其他更具有挑戰性的專案。

2.3.2 生成lmdb資料庫

實驗樓環境中的caffe安裝在/opt/caffe目錄下,在/opt/caffe/build/tools目錄下你可以找到一個名為convert_imageset的可執行程式,不過實驗樓使用的環境,在~/.zshrc檔案中已經將convert_imageset程式所在的目錄新增到了環境變數PATH中,所以你可以直接在terminal中輸入"convert_imageset"執行命令。如果你自己的電腦中安裝有caffe而沒有配置環境變數PATH, 可能無法直接執行convert_imageset命令。

~/.zshrc檔案中,caffe(也包括torch)相關環境的配置如下:

此處輸入圖片的描述

輸入“convert_imageset”命令,你可以看到這個命令有哪些選項:
此處輸入圖片的描述

你可以自己研究如何利用這些選項來自定義訓練資料的生成過程,對於我們的訓練資料,可以直接輸入以下命令將圖片轉化成lmdb資料:

convert_imageset --check_size --gray -shuffle ./ train.txt trainconvert_imageset --check_size --gray -shuffle ./ validate.txt validateconvert_imageset --check_size --gray -shuffle ./ test.txt test

這三條命令的第一個引數--check_size檢查每一張圖片的尺寸是否相同。第二個引數--gray將圖片轉換為單通道灰度圖片。第三個引數-shuffle將所有圖片的順序打亂。第三個引數./指明圖片檔案所在的父目錄,由於這裡的train.txt等檔案中已經包含了字首pic,所以這裡的父目錄就是當前目錄./。第四個引數指明圖片列表檔案。第五個引數指明最後生成的lmdb資料庫資料夾的位置。

這三條命令執行完畢後,你的目錄結構應該是這樣的:

.├── cnndata.tar.gz├── network.prototxt├── pic├── solver.prototxt├── test├── test.txt├── train├── train.txt├── validate└── validate.txt4 directories, 6 files

多出的三個資料夾train validate test包含我們生成的lmdb資料庫。

2.3.3 計算圖片畫素均值

對於神經網路,我們希望輸入的資料分佈能夠有正有負(具體原因這裡不贅述,若有興趣你可以查閱資料瞭解為什麼希望資料有正有負)。而圖片畫素值都是大於0的,所以caffe為我們提供了另一個指令碼compute_image_mean,執行這個命令獲得訓練資料在每個通道上的均值,在處理訓練資料時減去這個均值就可以保證圖片有正有負且其分佈“以0為中心”。

需要注意的是,我們只對訓練集train計算均值,訓練和測試的時候都是減去這個均值,而不是對於測試集單獨計算。因為如果對於訓練集和測試集的預處理操作不一樣的話,可能會影響模型在測試集上的實際效果。

執行以下命令計算訓練集圖片均值:

compute_image_mean train train.binaryproto

此處輸入圖片的描述

其中第一個引數train指定對在我們剛生成的lmdb資料庫train中的資料計算均值,第二個引數train.binaryproto指定計算出的均值儲存在train.binaryproto檔案中。稍後我們會用到這個均值檔案。

2.4 caffe如何定義網路結構

就像課程814中所說的,好多深度學習框架都提供層次化的網路結構,網路結構中的每一層為一個小的模組,將多個模組組合起來,就構成了一個神經網路模型,就像是搭積木一樣。caffe正是如此。

2.4.1 通過protobuf檔案定義網路結構

caffe通過編寫probobuf檔案來定義網路(network)結構, protobuf是一種資料交換格式。protobuf使用起來很簡單,為了完成本課程的實驗,你不需要專門去學習protobuf,只需要參照別人寫好的protobuf檔案照葫蘆畫瓢就行了。

使用編輯器建立一個network.prototxt(注意這裡的字尾名是prototxt)檔案, 這裡使用的是vim編輯器,如果你不會使用vim, 也可以使用其他編輯器。

vim network.prototxt

在新建檔案的第一行,你可以定義網路模型的名字:

name: "AlphaNet"

2.4.2 Blobs、bottom、top

在課程814中,我們直接使用numpy ndarray儲存網路中的資料。在caffe中,網路中的資料使用Blobs儲存,其實你可以直接把Blobs看成一個n*c*h*w的四維陣列,其中n代表一個batch中圖片的數量,c代表圖片通道數(對於卷積層,代表特徵個數),h代表圖片高度,w代表圖片寬度。

caffe中的每一個網路層可以有多個bottomtop,bottom其實就是一個網路層的資料流入口,top就是一個網路層的資料流出口。當層A的bottom和層B的top相同時,就代表層A以層B的輸出作為輸入。

2.4.3 定義資料輸入層

我們已經準備好了訓練和測試資料,為了讓我們的卷積神經網路能夠讀取這些資料,需要在network.prototxt新增資料輸入層。

資料層需要從lmdb中讀取資料,然後產生兩個輸出top:一個data代表圖片資料,一個label代表該圖片的標籤。

layer{    name:"data"     type:"Data"    top:"data"    top:"label"    data_param{        source: "train"        batch_size:16        backend:LMDB    }    transform_param{        scale: 0.00390625        mean_file: "train.binaryproto"    }    include{        phase:TRAIN    }}

我們逐個對這個資料層的各部分進行解釋。

name代表這一層的名字。type代表型別,caffe中提供了很多種型別的網路層,你可以到這裡檢視有哪些層,這裡的Data指定是從資料庫中讀取資料。
兩個top代表了資料層有兩個輸出,一個是data(與層的名字相同),代表lmdb資料庫中的圖片資料,一個是label代表每張圖片對應的標籤。

data_param中的內容指定了Data型別資料層需要的引數,其中source指定資料庫的位置,在這裡就是我們之前生成的train資料夾;batch_size指定每一個batch一次性處理多少張圖片(還記得課程814裡說的batch嗎);backend指定資料庫的種類,我們使用的是lmdb資料庫,所以這裡為LMDB

接下來的transform_param指定對圖片進行的預處理操作,這裡的mean_file指定平均值檔案的位置,同時,我們指定了對圖片畫素值的縮放比例scale為0.00390625(其實就是1/255), 將畫素值的範圍縮小到1。

include包含了其他一些資訊,這裡的phase指定這個資料層是在訓練還是在測試階段使用,TRAIN表明是在訓練階段。

對於測試階段的資料,可以直接再增加一個數據層,同時設定phaseTEST就可以了,如下:

layer{    name:"data"    type:"Data"    top:"data"    top:"label"    data_param{        source: "validate"        batch_size:100        backend:LMDB    }    transform_param{        scale: 0.00390625        mean_file: "train.binaryproto"    }    include{        phase:TEST    }}

當進行訓練時,caffe就呼叫phaseTRAIN的資料層,當測試時,caffe就呼叫phaseTEST的資料層。

除了phasesourcebatch_size,第二個資料層的設定與第一個資料層一模一樣。注意這裡的兩個top名必須和第一個資料層一樣,因為後面的網路層的輸入bottom通過名稱指定資料來源,所以兩個資料層的輸出top名設定成一樣就可以保證在訓練和測試時,後面的網路層都能讀取到資料。

2.4.4 定義卷積層

我們一共有三個卷積層,讓我們先來看看第一個卷積層的定義:

layer{    name: "conv1"    type: "Convolution"    bottom: "data"    top: "conv1"    param{        lr_mult: 1    }    param{        lr_mult: 2    }    convolution_param{        num_output: 32        kernel_size: 3        stride: 1        pad: 1        weight_filler{            type: "xavier"        }        bias_filler{            type:"constant"        }    }}

和資料層有一些類似(比如name,bottom,top的作用),但又有很多不一樣的地方。首先這裡的type變成了Convolution代表這一層是卷積層。

兩個param中的lr_mult作用有些特殊,它們代表卷積層中對引數的學習速率乘以多少倍(就像它的名字暗示的那樣--learning rate multiply),其中第一個lr_mult代表對卷積核中到的引數weight學習速率相乘的值,第二個lr_mult代表對偏移量bias學習速率相乘的值,一般我們總是把第一個設定為1(即學習速率不變),第二個設定為2。這裡你可能對這兩個引數的作用感到摸不著頭腦,但以後你就會明白這兩個引數非常有用(它們在caffe的遷移學習transfer learning中發揮作用)。
資料層中的引數在data_param中定義,類似的,卷積層中的引數在convolution_param中定義。這裡kernel_sizestride pad你已經知道它們的作用了,分別代表卷積核的尺寸、卷積核移動步長、圖片邊緣填充的畫素數。而num_output其實就是我們第一次實驗所說的特徵個數feature
最後剩下weight_fillerbias_filler,這兩個引數指明weightbias使用什麼方式初始化(填充),如果typeconstant,代表用常數0填充,而xavier所代表的填充演算法就稍微有點複雜了。這裡對xavier填充演算法不做介紹,如果你有興趣,可以自己查閱資料或者檢視提出xavier演算法的論文

這裡需要注意的是,我們設定kernel_size=3 stride=1 pad=1可以保證卷積層輸出的寬和高與輸入相同,你可以代入第一次實驗給出的公式計算驗證。我們的輸入圖片尺寸為16x16,帶入公式和卷積層的引數,得到:(16+2*1-3)/1+1=16

卷積層2和卷積層3的定義與卷積層1幾乎一模一樣,除了卷積層的num_output引數被設定成64。

2.4.5 卷積神經網路中的池化層

池化(Pooling)層是卷積神經網路中幾乎必然出現的網路層,第一次實驗為了突出卷積層,沒有介紹池化層,放到了這裡來介紹。

我們之前說過,合理的設定卷積層的引數,可以保證卷積層的輸出和輸入在寬和高上不變。但我們有時候又希望能減小訓練資料的尺寸,這樣可以降低模型的複雜度,減少引數的數量,讓模型訓練的更快,池化層就具有這樣的作用。
池化層其實和卷積層非常相似,也是有一個“池化核”對整張圖片中的所有可能位置進行計算,不同的是,池化層中沒有引數,一般池化層會返回“池化核”中最大(或者最小,或者隨機)的數字,且池化層的步長stride一般設定成與“池化核”的尺寸相同,返回“池化核”內最大數值的池化層效果如下:

此處輸入圖片的描述

在第一個卷積層之後,就有一個池化層。

layer{    name:"pool1"    type: "Pooling"    bottom: "conv1"    top: "pool1"    pooling_param{        pool: MAX        kernel_size:2        stride: 2    }}

其中引數的作用已經很明顯了,注意pool設定為MAX代表這個“池化核”返回最大值。

注意

池化層的作用不止是降低模型複雜度,比如你可以把返回最大值池化層理解為只保留一個區域最明顯的特徵。如果你想弄清楚池化層更深層次的作用,請自行查閱資料理解。

2.4.5 caffe中的內積層

caffe將全連線層稱為內積層(InnerProduct), 其計算方式大體上與課程814中的全連線層一樣,內積層的定義如下:

layer{    name: "ip1"    type: "InnerProduct"    bottom: "conv3"    top: "ip1"    param{        lr_mult:1    }    param{        lr_mult:2    }    inner_product_param{        num_output: 100        weight_filler{            type: "xavier"        }        bias_filler{            type:"constant"        }    }}

其中的引數我們都已經見過,注意這裡的typeInnerProduct代表是全連線層,num_output代表的是全連線層的輸出節點的數量。

這裡我們可以感受到深度學習框架帶給我們的便利,課程814中我們為了實現全連線層絞盡腦汁,而這裡只需要十幾行的定義就可以了,其他一切都由caffe幫我們實現。

2.4.5 損失函式層

課程814中,我們介紹了QuadraticLossCrossEntropyLoss兩種損失函式,但這裡我們打算使用caffe提供的另一種損失函式:SoftmaxWithLoss。 為了弄清楚SoftmaxWithLoss損失函式是如何工作的,我們先要介紹什麼是SoftmaxSoftmax函式的表示式如下:
此處輸入圖片的描述
不要被它的表示式嚇到,其實Softmax的計算很簡單,就是對一個數組中的每一個元素先求它對於自然數e的指數e^x,然後每一個元素的Softmax函式值就是e^xi除以所有元素對自然數e的指數的和。
Softmax函式的性質也很好分析,陣列中原本最大的數的函式值最接近1,最小的數的函式值最接近0,同時,陣列中所有元素的Softmax函式值加起來為1。這剛好可以作為概率來看待,實際上,caffe中有單獨的Softmax層存在,我們可以直接用Softmax層的輸出作為我們的模型對每個類別(比如這裡是26個類別)的概率值的預測。

現在我們清楚了Softmax的作用,但你可能仍然會困惑,Softmax的名字從何而來,為什麼把它叫做“軟的最大”呢?其實,與Softmax對應的還有一種“硬的最大”函式,這裡我把它叫做Hardmax, 它的表示式如下:

此處輸入圖片的描述
觀察它的表示式,你會發現HardmaxSoftmax的性質非常相似,都是陣列中原本最大的元素的函式值最大,但不同的是,對於Hardmax,陣列中最大的元素的函式值固定為1,最小的元素的函式值固定為0。最大元素的函式值一定為1,所以我把它稱為“硬最大”,而Softmax卻相對更加“柔和”,更加的“軟”。Softmax的這種特性恰恰適合於作為神經網路中的概率輸出,而Hardmax則會總是把最大可能的類別概率值設定為1,這是不合理的。

搞清楚了Softmax的作用,理解SoftmaxWithLoss損失函式就非常簡單了。caffe中的SoftmaxWithLoss損失函式層其實就是在Softmax層上增加了一些運算,SoftmaxWithLoss層的定義如下:

layer{    name: "loss"    type: "SoftmaxWithLoss"    bottom: "ip2"    bottom: "label"    top: "loss"}

SoftmaxWithLoss層有兩個bottom, 一個ip2是我們模型對於每種類別可能性大小的預測,注意這個預測值在經過Softmax層之前是不能作為概率值的(可能有負值,和可能不為1)。另一個label就是我們資料層的輸出label, 代表一個訓練圖片上實際英文字母的類別。

SoftmaxWithLoss層先用Softmax函式計算出模型對每種類別的預測概率。再根據label的值,選擇出預測值中的對應概率。比如Softmax的輸出在這裡是一個長度為26的陣列,而label中的值為0(代表圖片上的字母實際為A),就選擇出Softmax函式輸出陣列中的第一個概率值。顯然,當這個概率值接近1的時候,說明我們的模型預測的比較準確,SoftmaxWithLoss的輸出值應該接近於0,當這個概率值接近於0的時候,說明我們的模型預測的不太準確,SoftmaxWithLoss的輸出值應該是一個很大的正值。

實際上,SoftmaxWithLoss層對概率值進行的運算很簡單,就是對該值求負對數, 這樣就滿足了上面說的,預測越準損失函式值越小的要求。即SoftmaxWithLoss層的運算大致如下:

此處輸入圖片的描述

2.4.6 準確率層

之前我們說過,為了檢驗模型的泛化效能,需要在驗證/測試集上檢驗模型的預測準確率。caffe為我們提供了Accuracy準確率層,其定義如下:

layer{    name: "accuracy"    type: "Accuracy"    bottom: "ip2"    bottom: "label"    top: "accuracy"    include{        phase:TEST    }}

bottom同樣有兩個,但注意這裡的第一個bottomip2而不需要是概率值,因為Accuracy層只需要第一個bottom的最大值的下標與第二個bottom相同就認為預測是準確的。

2.4.7 ReLU 啟用函式層

在課程814中我們說過,Sigmoid啟用函式容易導致梯度消失問題,消失的梯度使得神經網路的訓練變得非常困難。而這裡我們將介紹的ReLU(Rectified Linear Unit)啟用函式層則非常好的避免了梯度消失問題。

ReLU的函式表示式如下:
此處輸入圖片的描述
SigmoidReLU函式影象對比如下:

此處輸入圖片的描述
ReLU執行的運算非常簡單,就是隻讓大於0的節點的值向前傳遞。你需要特別注意的是,當反向傳播梯度的時候,大於0的節點的梯度是由之前的“部分梯度”乘以1得到的,而小於等於0的節點的梯度則為0。對於大於0的節點,ReLU不會導致梯度值減小,非常有效的避免了梯度消失問題。

同時,ReLU的計算非常簡單,而Sigmoid涉及到的求指數運算對於計算機來說則非常複雜,ReLU啟用函式具有更高的執行速度。

我們的網路中層與層之間都存在著ReLU啟用函式層,其定義如下:

layer{    name: "relu1"    type: "ReLU"    bottom: "conv1"    top: "conv1"}

ReLU不需要引數,所以這裡的定義非常簡單,不過你需要注意的是,這裡的bottomtop名字可以設定成一樣的,當設定成一樣的時候,ReLU的輸出結果會儲存到輸入的Blobs裡,這樣能節省視訊記憶體(或記憶體)。

2.5 caffe網路定義總結

至此我們的專案中會用到的各種網路層都已經介紹過了,完整的網路定義請你開啟network.prototxt檢視。我建議你仔細一行一行的檢視這個檔案,理解每個網路層的功能和它的特性,理解每一個引數的作用。這對你之後自己動手編寫神經網路定義檔案非常重要。

我們現在只寫好了網路定義檔案,但為了讓模型開始訓練,我們還有一些東西沒有確定,比如超引數學習速率、測試間隔、最大訓練週期(epoch)等,下次實驗,我們將講解如何編寫solver.prototxt檔案。

三、實驗總結

雖然看起來我們的網路定義檔案network.prototxt的行數有點多,但和我們之前自己動手實現神經網路比起來,這裡的網路模型構建還是簡單多了,熟練之後你可以在幾分鐘之內就搭建好一個神經網路。caffe還提供了很多其他種類的網路層,如果你有興趣可以到caffe官網檢視。理解這些層的原理是科學合理地使用這些網路層的基礎。

本次實驗,我們學習了:

  • caffe中的資料由Blobs承載,Blobs可以被看成一個四維陣列(或者四維張量)。
  • caffe通過編寫network.prototxt檔案構建神經網路。
  • 池化層能夠縮小圖片尺寸,降低模型計算量。
  • SoftmaxWithLoss損失函式就是對概率值取負對數。
  • ReLU啟用函式可以有效避免梯度消失。

四、課後作業

  1. [選做]如果你打算深入研究caffe,可能會發現caffe官網的文件並不是十分全面,你可以檢視/opt/caffe/src/caffe/proto/caffe.proto檔案,裡面包含了caffe中所有引數的定義。