caffe安裝以及LeNet實現手寫數字體識別

0 引言

今天開始正式跳入深度學習的坑,希望自己兩年半的研究生生涯中,能夠在深度學習方面取得一點成績。今天開始在服務器上弄caffe的時候遇到了很多問題。看了很多博客,最終解決了問題。現在把遇到的問題以及解決方案總結一下。本文是基於服務器已經成功安裝了Python各種常用包、OpenCV、cuda和caffe的環境,只是自己在申請到服務器賬號以後,在自己的用戶目錄中添加caffe。加黑斜體是我的心得體會。
安裝過程主要參考博客
LeNet主要參考博客

1 下載caffe

cd ~
mkdir git  //在home下新建一個git文件夾,用來存放那些從github上git下來的文zong件
git clone https://github.com/BVLC/caffe.git    //從github上git caffe

2 開始安裝

cd caffe    //打開到剛剛git下來的caffe
cp Makefile.config.example Makefile.config //將Makefile.config.example的內容複製到Makefile.config
//因爲make指令只能make   Makefile.config文件,而Makefile.config.example是caffe給出的makefile例子
gedit Makefile.config      //打開Makefile.config文件

如果服務器安裝的不是gedit而是vim,則最後一句應是 vim Makefile.config
仔細閱讀makefile中的註釋語句其實就知道該怎麼操作了,爲了方便理解,筆者還是介紹一各個配置說明。
在打開的Makefile.config修改如下內容:

//如果你不使用GPU的話,就將
# CPU_ONLY := 1
修改成:
CPU_ONLY := 1

//若使用cudnn,則將
# USE_CUDNN := 1
修改成:
USE_CUDNN := 1

//若使用的opencv版本是3的,則將
# OPENCV_VERSION := 3
修改爲:
OPENCV_VERSION := 3

//若要使用python來編寫layer,則需要將
# WITH_PYTHON_LAYER := 1
修改爲
WITH_PYTHON_LAYER := 1

//重要的一項# Whatever else you find you need goes here.下面的
INCLUDE_DIRS := $(PYTHON_INCLUDE) /usr/local/include
LIBRARY_DIRS := $(PYTHON_LIB) /usr/local/lib /usr/lib
修改爲:
INCLUDE_DIRS :=  $(PYTHON_INCLUDE) /usr/local/include /usr/include/hdf5/serial
LIBRARY_DIRS := $(PYTHON_LIB) /usr/local/lib /usr/lib /usr/lib/x86_64-linux-gnu /usr/lib/x86_64-linux-gnu/hdf5/serial
//這是因爲ubuntu16.04的文件包含位置發生了變化,尤其是需要用到的hdf5的位置,所以需要更改這一路徑

在這裏只修改了Python和cudnn還有最後的最重要的東西,因爲我們服務器opencv是2.4.9.1.要用到GPU加速。

3 make 各種文件

cd ..   \\此時位置應該處於caffe文件夾下

make all -j8  //j8代表計算機cpu有8個核,因此可以多線程一起make,這樣make的速度會快很多。一般常用的還有j4

make test -j8

make runtest -j8//可跳過

make pycaffe   //如果以後用python來開發的話必須執行這一句,一般不管你是否用python,都會執行這一句

make distribute

至此,安裝caffe差不都就已經完成了。但是筆者在安裝的過程中,遇到了各種各樣問題,有時候解決了這個問題,那個問題又出現了,幾近崩潰的邊緣,究其原因還是對ubuntu這個系統熟悉。在這次配置過程中我主要是不熟悉shell、Ubuntu系統、winscp軟件。下面介紹幾個常用的vim命令:
***u 撤銷上一步的操作
Ctrl+r 恢復上一步被撤銷的操作
退出Vim
Vim的退出方式也不少,根據情況的不同,主要有7種,執行退出操作之前都需要回到一般模式(按Esc鍵)。
1、結合Shift鍵輸入“:”,切換到命令模式,輸入“q”後回車。這屬於正常退出,只有當文件未被修改或者修改已經被保存的情況下方能成功退出,否則將提示有修改尚未保存而無法退出。
2、切換到命令模式,輸入“q!”後回車。這屬於強制退出,當有文件被修改而尚未保存,則強制退出將捨棄修改。
3、切換到命令模式,輸入“wq”後回車。這屬於先保存後退出,只有對打開的文件可寫且有編輯權限的情況下方能成功退出,否則將提示權限不夠或者文件只讀等警告。
4、切換到命令模式,輸入“wq!”後回車。這屬於強制保存並退出,只有對打開的文件有編輯權限的情況下方能成功退出,否則將提示權限不夠等警告。有訪問權限的只讀文件即便修改過,也能成功保存並退出。
6、在一般模式下輸入“ZZ”。這屬於先保存後退出,同“wq”。
7、在一般模式下輸入“ZQ”。這屬於強制退出,同“q!”。*

4 準備數據集

下面的內容主要來自Caffe作者Yangqing Jia網站給出的examples。

@article{jia2014caffe,
Author = { Jia, Yangqing and Shelhamer, Evan and Donahue, Jeff and Karayev, Sergey and Long, Jonathan and Girshick, Ross and Guadarrama, Sergio and Darrell, Trevor},
Journal = {arXiv preprint arXiv:1408.5093},
Title = {Caffe: Convolutional Architecture for Fast Feature Embedding},
Year = {2014}
}

首先從MNIST網站上下載數據集,運行:

cd $CAFFE_ROOT 
./data/mnist/get_mnist.sh 

下載到四個文件,從左至右依次是測試集圖像、測試集標籤、訓練集圖像、訓練集標籤:
這裏寫圖片描述
轉換數據格式:

./examples/mnist/create_mnist.sh

在 examples/mnist下出現兩個新的文件夾:
這裏寫圖片描述
create_mnist.sh這個腳本是將訓練集和測試集分別轉換成了lmdb格式。
這裏寫圖片描述

5 LeNet: MNIST分類模型

本實驗用的網絡模型是LeNet,它是公認在數字分類任務上效果很好的網絡。實驗中在原始 LeNet基礎上做了一點改動,對於神經元的激活,用ReLU替換了sigmoid。
LeNet的設計包括一個卷積層,後隨一個pooling層,再一個卷積層,後隨一個pooling層,再兩個全連接層,類似於傳統的多層感知器。經典LeNet如圖:
這裏寫圖片描述
這些層的定義在examples/mnist/lenet_train_test.prototxt中。

6 定義MNIST網絡

在定義自己的網絡之前可以運行示例中給出的代碼訓練網絡:

sh examples/mnist/train_lenet.sh

過程與CIFAR-10中的一樣,所用solver是examples/mnist/lenet_solver.prototxt,在這個solver中可以看到對訓練與測試的簡單設置,所用的網絡定義就是examples/mnist/lenet_train_test.prototxt。
下面詳細學習examples/mnist/lenet_train_test.prototxt的模型定義,學習的基礎是對Google Protobuf比較熟悉,可參考Google Protocol Buffer的使用和原理,還要讀過Caffe使用的protobuf定義,這個定義在src/caffe/proto/caffe.proto中。
爲了更深入地瞭解網絡是怎麼定義的,我們自己寫一個caffe網絡參數的protobuf。首先新建一個prototxt文件,我這裏的命名是lenet_train_lml.prototxt。給網絡取個名字:

name: "LeNet"

6.1 寫數據層

現在要從之前創建的lmdb中讀取MNIST數據,定義如下的數據層:

layer { 
    name: "mnist"  #該層的名字
    type: "Data"    #輸入的類型
    data_param {    #數據參數
        source: "mnist_train_lmdb"  #數據來源,從 mnist_train_lmdb中讀入數據
        backend: LMDB   
        batch_size: 64  #批次大小,即一次處理64條數據
        scale: 0.00390625   #像素灰度歸一化參數,1/256
    } 
    top: "data"     #該層生成兩個blob,分別是data和label
    top: "label" 
}

6.2 寫卷積層

定義第一個卷積層:

layer { 
    name: "conv1"   #該層的名字
    type: "Convolution"     #該層的類型
    param { lr_mult: 1 }    #學習率,權重的和solver的一樣
    param { lr_mult: 2 }    #偏移的是solver的兩倍
    convolution_param { 
        num_output: 20  #卷積核個數
        kernel_size: 5  #卷積核大小
            stride: 1   #步長
            weight_filler {  
            type: "xavier"  #用xavier算法基於輸入輸出神經元數自動初始化權重
        } 
            bias_filler { 
            type: "constant"    #簡單初始化偏移爲常數,默認爲0 
        } 
    } 
    bottom: "data"  #該層的上一層是 data
    top: "conv1"    #該層生成conv1 blob
}

6.3 寫pooling層

pooling層就好定義多了:

layer { 
    name: "pool1"   #該層的名字
    type: "Pooling"     #該層的類型
    pooling_param {     
        kernel_size: 2  #pooling的核是2×2
        stride: 2   #步長是2,也就是說相鄰pooling區域之間沒有重疊
        pool: MAX   #pooling的方式
    } 
    bottom: "conv1"     #該層的上一層是conv1
    top: "pool1"    #該層生成pool1 blob
}

然後就可以自己寫第二個卷積層和pooling層了,細節參考examples/mnist/lenet_train_test.prototxt。

layer { 
    name: "conv2" 
    type: "Convolution" 
    bottom: "pool1" 
    top: "conv2" 
    param {  lr_mult: 1 } 
    param {  lr_mult: 2 } 
    convolution_param { 
        num_output: 50 #卷積核變成了50個
        kernel_size: 5 
        stride: 1 
        weight_filler { 
            type: "xavier" 
        } 
        bias_filler { 
            type: "constant" 
        } 
    } 
} 
layer { 
    name: "pool2" 
    type: "Pooling" 
    bottom: "conv2" 
    top: "pool2" 
    pooling_param { 
        pool: MAX 
        kernel_size: 2 
        stride: 2 
    } 
}

6.4 寫全連接層

全連接層也比較簡單:

layer { 
    name: "ip1" #全連接層的名字
    type: "InnerProduct"    #全連接層的類型
    param { lr_mult: 1 } 
    param { lr_mult: 2 } 
    inner_product_param { 
        num_output: 500     #輸出500個節點
        weight_filler { 
            type: "xavier" 
        } 
        bias_filler { 
            type: "constant" 
        } 
    } 
    bottom: "pool2" 
    top: "ip1" 
}

6.5 寫ReLU層

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

因爲ReLU是元素級操作,所以可以用一種叫做in-place(猜測可以翻譯爲在原位置,也就是不開闢新內存)的操作來節省內存,這是通過簡單地把bottom blob和top blob設成同樣的名字來實現,當然了,不要在其他類型的層中這麼幹。
然後再寫一個全連接層:

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

6.6 寫loss層

最後寫一個loss層:

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

softmax_loss層實現了Softmax和多項Logistic損失(節省了時間,同時提高了數據穩定性)。它需要兩個blob,第一個是預測,第二個是數據層生成的label。該層不產生輸出,只是計算loss函數的值,在反向傳播的時候使用,並初始化關於ip2的梯度。

6.7 寫層次規則

層次定義包含的規則是這些層是否以及什麼時候包含在網絡定義中,像這樣:

layer { 
    #...layer definition... 
    include: { phase: TRAIN } 
}

這個規則基於現有網絡狀態,控制網絡中的層次包含關係,可以查看src/caffe/proto/caffe.proto來獲取層次規則及模型概要的更多信息。
在上面的例子中,這個層只會包含在TRAIN階段中,如果把TRAIN改爲TEST,這個層就只會被用於TEST階段。如果不寫TRAIN或TEST的話,那麼這個層TRAIN階段和TEST階段都會被用到,所以lenet_train_test.prototxt中定義了兩個DATA層,我們參考它也分別定義兩個DATA層:

layer {
    name: "mnist"
    type: "Data"
    top: "data"
    top: "label"
    include {
        phase: TRAIN
    }
    transform_param {
        scale: 0.00390625
    }
    data_param {
        source: "examples/mnist/mnist_train_lmdb"
        batch_size: 64
        backend: LMDB
    }
}
layer {
    name: "mnist"
    type: "Data"
    top: "data"
    top: "label"
    include {
        phase: TEST
    }
    transform_param {
        scale: 0.00390625
    }
    data_param {
        source: "examples/mnist/mnist_test_lmdb"
        batch_size: 100
        backend: LMDB
    }
}

另外,TEST階段有一個Accuracy層,它是用來每100次迭代報告一次模型準確率的,lenet_solver.prototxt中給出了定義,我們同樣也把它加上:

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

7 定義MNIST的solver文件

再看一下lenet_solver.prototxt中都定義了些什麼:

# 訓練/檢測網絡的protobuf定義
net: "examples/mnist/lenet_train_lml.prototxt"
# test_iter指的是測試的迭代次數,這裏是100,測試批次大小也是100,這樣就覆蓋了10000個測試圖像
test_iter: 100
# 每訓練迭代500次就測試一次
test_interval: 500
# 學習率,動量,權重下降
base_lr: 0.01
momentum: 0.9
weight_decay: 0.0005
# 學習率規則
lr_policy: "inv"
gamma: 0.0001
power: 0.75
# 每迭代100次顯示一次
display: 100
# 最大迭代次數
max_iter: 10000
# 每5000次迭代存儲一次快照
snapshot: 5000
snapshot_prefix: "examples/mnist/lenet"
# 選擇CPU還是GPU模式
solver_mode: GPU

8 訓練與測試模型

在寫完網絡定義和solver之後,就可以訓練模型了,運行:

./examples/mnist/train_lenet.sh

會在終端看到這樣的消息,這些消息顯示了每一層的細節,即連接關係與輸出的形狀,在調試的時候是很有用的:
這裏寫圖片描述
初始化以後就開始訓練了:
這裏寫圖片描述
solver中設置每100次迭代打印出訓練的loss,每1000次迭代打印出測試的loss:
這裏寫圖片描述
迭代完結果就出來了:
這裏寫圖片描述
最後的模型存儲在一個二進制的protobuf文件lenet_iter_10000.caffemodel中,在訓練其他數據集的時候可以把它作爲基礎模型。

9 其他

通過這個實驗,終於知道網絡要怎麼設置了,還有其他不同的solver值得一試。例如autoencoder網絡,運行命令:

./examples/mnist/train_mnist_autoencoder.sh

或者:

./examples/mnist/train_mnist_autoencoder_adagrad.sh

或者:

./examples/mnist/train_mnist_autoencoder_nesterov.sh
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章