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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章