Caffe简明教程5:训练你的第一个Caffe模型-MNIST分类器

您可以查看所有文章的索引:Caffe简明教程0:文章列表


如果你已经根据前面几篇文章成功地编译了Caffe,那么现在是时候训练你的第一个模型了。我准备借用Caffe官网的LeNet例子来写这篇文章,您也可以访问原始的文档:Training LeNet on MNIST with Caffe

Caffe在编译完成之后,在caffe根目录下有个examples文件夹,里面包含了很多Caffe的例子,其中就有MNIST。


1 准备数据

这次实验使用的是MNIST数据集,相信做计算机视觉的朋友都知道,MNIST数据集是一个由Yann LeCun及其同事整理和开放出来的手写数字图片的数据集。我们将使用Caffe来训练一个能够识别手写数字的模型。

首先我们需要下载MNIST数据集,运行caffe提供下载数据集的shell脚本:

cd $CAFFE_ROOT             # $CAFFE_ROOT是你caffe的根目录
./data/mnist/get_mnist.sh  # 此脚本将下载MNIST数据集

运行上面的脚本之后,目录$CAFFE_ROOT/data/mnist/下将出现以下四个文件:

  • train-images-idx3-ubyte(训练样本)
  • train-labels-idx1-ubyte(训练样本标签)
  • t10k-images-idx3-ubyte(测试样本)
  • t10k-labels-idx1-ubyte(测试样本标签)

接着,我们需要把上面的数据转换为Caffe需要的数据形式(lmdb数据库形式,不了解lmdb的话可以暂时放在这里,后面的文章会详细解释),运行Caffe提供的数据转换脚本,将MNIST数据集转换为Caffe所需的lmdb文件:

./examples/mnist/create_mnist.sh  # 将MNIST数据集转换为Caffe所需的lmdb文件

打开目录$CAFFE_ROOT/examples/mnist你会发现多了两个文件夹:mnist_test_lmdbmnist_train_lmdb,这两个文件夹分别保存了MNIST的以lmdb形式存储的测试集和训练集。

Trouble shooting

如果报错wget或者gnuzip没有安装,那么使用命令sudo apt-get install wget gzip安装它们。
wget用于从远程服务器获取文件,gzip用于解压缩文件。


2 LeNet: 用于MNIST数据集的分类模型

LeNet是一个由Yann LeCun及其同事于1998年发明的手写数字识别模型,论文地址http://yann.lecun.com/exdb/publis/pdf/lecun-01a.pdf。LeNet是一个并不太复杂的模型,其示意图如下 所示:

这里写图片描述

简单介绍一下LeNet的结构:

  • 输入为32x32的灰度图片
  • 接着是一个卷积层
  • 接着是一个采样层(池化层)
  • 又是一个卷积层
  • 又是一个采样层(池化层)
  • 最后有一个10维的softmax输出层(分别对应数字0-9)

Andrew的图可能更清晰一点:

这里写图片描述

OK,了解了LeNet的结构之后,我们来看看如何在Caffe中定义这网络。


3 在Caffe中定义LeNet

在Caffe中定义一个网络的结构可能是入门者的大难题,但是,只要我们静下心仔细学习,你会发现在Caffe中定义一个网络其实是非常方便的,而且不需要我们写任何C++或者Python代码。

在Caffe中,定义网络的结构需要用的到Google的Protocol Buffer。我们现在可以先不急着去深入了解Protocol Buffer,我们只需要知道,Protocal Buffer就是一种用来描述数据结构的简单的语言(类似XML,但是比XML强大得多)。

幸运的是,Caffe的MNIST例子中已经有写好了的LeNet网络结构,这个网络结构的定义在这里:
$CAFFE_ROOT/examples/mnist/lenet_train_test.prototxt
现在你可以打开它查看一下,看看里面的内容是什么,是不是很像C语言里面的结构(struct)呀?看看自己能否看出来这些内容的含义。
注意:Protocol Buffer文件一般都是以.prototxt结尾的文本文件。

现在,我们复制lenet_train_test.prototxt中的内容,打开网址:http://ethereon.github.io/netscope/#/editor,粘帖进去,然后按shift+enter,看看这个网络到底是什么样的。

看不出来没关系,下面会仔细讲解这个文件的内容。另外,虽然这里有个现成的LeNet结构定义文件,但是光看它的内容是不能掌握Caffe的,你还是应该亲自动手,在目录$CAFFE_ROOT/examples/mnist下创建一个空白的文件,并命名为my_lenet.prototext。下面我们就在my_lenet.prototext中亲自定义一个LeNet。


3.1 给你的网络取个好听的名字

第一步,当然得给网络取个好听的名字,对吧?
用你喜欢的编辑器打开你刚刚创建的文件:$CAFFE_ROOT/examples/mnist/my_lenet.prototext。现在,该文件是空的,我们在第一行写上:

name: "LeNet"

那么取名这个事情就完成了。


3.2 网络当然要有输入数据才能训练

名字取好之后,在新的一行,我们来定义数据的输入层,层(layer)这个概念在Caffe中是非常重要的,现在我们使用关键字layer来定义网络的一个层,这里我们定义数据输入层。在文件$CAFFE_ROOT/examples/mnist/my_lenet.prototext中接着添加如下内容:

layer {
  name: "mnist" // 网络层的名字
  type: "Data"  // 网络层的类型,这里Data指的是数据层,后面你还会看到其他类型的层
  top: "data"   // top属性指明本层的数据将输出到何处,这里数据将保存到data中
  top: "label"  // 数据的标签将保存在label中
  include {  // 此属性用于确定在哪个过程中使用本层
    phase: TRAIN  // 只在训练的过程中使用本层
  }
  transform_param { // 这个是网络层的数据转换属性,在Caffe的网络层中传输数据时,可以对数据进行处理
    scale: 0.00390625  // 用于修改数据范围,就是所有输入的数据都乘以这个值,0.00390625=1/256
                       // 因为灰度图是0~255,乘以scale,那么所有的数据都在0~1之间,方便处理。
  }
  data_param {  // 这个用于定义数据的一些属性
    source: "examples/mnist/mnist_train_lmdb"  // 保存训练集的文件夹
    backend: LMDB  // 后端使用的是LMDB来保存数据,在文章开头讲过Caffe的数据集形式
    batch_size: 64 // 这个就不用解释了吧。每一批输入64个样本
  }
}

上面的层定义是不是看起来很头大,没关系,不是还有注释嘛,快看注释(.prototxt文件的注释和C语言中的注释方法相同)。

数据(data)和标签(label):Caffe规定,最开始的数据层必须有一个名为data的数据输出(top);另外还必须有一个名为label的标签输出(top)。Caffe从数据集中读取数据后,就将样本和标签分别保存在data和label中以便后面的层使用,这就是此例中Data层的作用。

datalabel其实是Caffe内部的名为Blob的类的实例,现在可以把Blob当作数组即可,后面会细讲,现在不用深究。

刚刚我们定义了训练数据的输入层(include中的phase属性指明什么时候使用该层),现在我们要定义测试阶段时数据的输入层。在文件$CAFFE_ROOT/examples/mnist/my_lenet.prototext中接着添加如下内容:

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 // 每批测试100个样本
    backend: LMDB
  }
}

总结一下,目前,我们定义了两个layer,它们的类型都是Data,即数据层,都用于网络的输入层。第一个用于训练阶段的数据输入,第二个用于测试阶段的数据输入。


3.3 定义卷积层

定义了数据输入层之后,我们需要对样本进行卷积操作,那么接下来就让我们定义第一个卷积层,在文件$CAFFE_ROOT/examples/mnist/my_lenet.prototext中接着添加如下内容:

layer {
  name: "conv1"        // 该层的名字,可以自己随便取
  type: "Convolution"  // 该层的类型为卷积层
  bottom: "data"       // bottom和top对应,bottom表示数据的输入来源是什么,本层的输入为data
  top: "conv1"         // 执行卷积后,数据保存到conv1层中
  param {
    lr_mult: 1         // 第一个param中的lr_mult用于设置weights的学习速率
                       // 与待会儿后面要设置的训练学习速率的比值
  }
  param {
    lr_mult: 2         // 第二个param中的lr_mult用于设置bias的学习速率
                       // 与待会儿后面要设置的训练学习速率的比值,设置为2收敛得更好
  }
  convolution_param {  // 设置卷积层的相关属性
    num_output: 20     // 输出的通道数
    kernel_size: 5     // 卷积核的大小
    stride: 1          // 步长
    weight_filler {    // weights的初始化的方法
      type: "xavier"   // 使用xavier初始化算法,此方法可以根据网络的规模自动初始化合适的参数值
    }
    bias_filler {      // bias的初始化方法
      type: "constant" // 使用常数初始化偏置项,默认初始化为0
    }
  }
}

上面的注释已经比较清楚了,说一下lr_mult,待会儿定义好网络结构之后,我们还需要定义一个训练文件,里面会设置网络的学习速率,这里的lr_mult即是这个学习速率的倍数。


3.4 定义池化层

在LeNet中,第一个卷积层后面是池化层,在文件$CAFFE_ROOT/examples/mnist/my_lenet.prototext中接着添加如下内容:

layer {
  name: "pool1"     
  type: "Pooling"   // 类型为Pooling,即池化层
  bottom: "conv1"   // 该层的输入来自前面的卷积层conv1
  top: "pool1"      // 输出保存在本层中
  pooling_param {   // 池化层的参数
    pool: MAX       // 池化方法(采样方法)
    kernel_size: 2  // 核大小
    stride: 2       // 步长
  }
}

池化层很简单对不对。


3.5 第二个卷积层和池化层

上面的看懂了,接下来就不难啦。我们接着添加第二个卷积层,以及该卷积层后面的池化层。在文件$CAFFE_ROOT/examples/mnist/my_lenet.prototext中接着添加如下内容:

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
  }
}

好了,目前我们已经定义了LeNet的数据输入层、卷积层以及池化层。还剩下两个全连接层和一个Softmax输出层。


3.6 定义全连接层

全连接层即Fully Connected Layer,一般简单地缩写为FC。全连接层很简单,就是标准神经网络中的网络层。在文件$CAFFE_ROOT/examples/mnist/my_lenet.prototext中接着添加如下内容:

layer {
  name: "ip1"
  type: "InnerProduct"  // InnerProduct即Caffe中的全连接层,参数和输入做内积
  bottom: "pool2"       // 输入来自前面的池化层
  top: "ip1"            // 输出保存在本层
  param {
    lr_mult: 1
  }
  param {
    lr_mult: 2
  }
  inner_product_param { // 全连接层的参数
    num_output: 500     // 输出神经元个数
    weight_filler {
      type: "xavier"
    }
    bias_filler {
      type: "constant"
    }
  }
}

很简单吧。我们知道标准神经网络中都有激活函数,LeNet原始论文使用的是Sigmoid,但是实践证明ReLU效果更佳。所以我们要对上面的全连接层计算激活值。


3.6 定义ReLU层

在文件$CAFFE_ROOT/examples/mnist/my_lenet.prototext中接着添加如下内容:

layer {
  name: "relu1"
  type: "ReLU"  // 该层类型为ReLU
  bottom: "ip1" // 输入为之前的全连接层
  top: "ip1"    // 输出保存在该全连接层中(覆盖之前的值)
}

3.7 第二个全连接层

下面是第二个全连接层,在文件$CAFFE_ROOT/examples/mnist/my_lenet.prototext中接着添加如下内容:

layer {
  name: "ip2"
  type: "InnerProduct"
  bottom: "ip1"
  top: "ip2"
  param {
    lr_mult: 1
  }
  param {
    lr_mult: 2
  }
  inner_product_param {
    num_output: 10  // 输出神经元的数量为10
    weight_filler {
      type: "xavier"
    }
    bias_filler {
      type: "constant"
    }
  }
}

到此,LeNet的主体已经定义完成了。但是我们还需要定义loss,以及用于计算准确率的层。Caffe正好提供了各种各样的层供我们使用。


3.8 定义用于计算准确率的层

下面的层能够在测试阶段,计算模型的准确率。在文件$CAFFE_ROOT/examples/mnist/my_lenet.prototext中接着添加如下内容:

layer {
  name: "accuracy"
  type: "Accuracy"  // 该层的类型为Accuracy,计算准确率
  bottom: "ip2"     // 第一个输入为之前的全连接层
  bottom: "label"   // 第二个输入为样本的标签
  top: "accuracy"   // 输出保存在本层中
  include {
    phase: TEST     // 只在测试阶段使用本层
  }
}

3.9 定义loss

马上就要大功告成了,定义LeNet的最后一步,即定义loss。Caffe可以根据loss自动计算梯度,并进行反向传播。在文件$CAFFE_ROOT/examples/mnist/my_lenet.prototext中接着添加如下内容:

layer {
  name: "loss"
  type: "SoftmaxWithLoss"  // 该层的类型为Softmax Loss
  bottom: "ip2"            // 第一个输入为上面的第二个全连接层
  bottom: "label"          // 第二个输入为样本标签
  top: "loss"              // 输出保存在本层中
}

Nice!整个LeNet已经定义完了。下面是文件my_lenet.prototext的完整内容,看看自己少了什么没有,没有注释,看自己能不能看懂,有疑惑一定要尽情地google之:

name: "LeNet"
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
  }
}
layer {
  name: "conv1"
  type: "Convolution"
  bottom: "data"
  top: "conv1"
  param {
    lr_mult: 1
  }
  param {
    lr_mult: 2
  }
  convolution_param {
    num_output: 20
    kernel_size: 5
    stride: 1
    weight_filler {
      type: "xavier"
    }
    bias_filler {
      type: "constant"
    }
  }
}
layer {
  name: "pool1"
  type: "Pooling"
  bottom: "conv1"
  top: "pool1"
  pooling_param {
    pool: MAX
    kernel_size: 2
    stride: 2
  }
}
layer {
  name: "conv2"
  type: "Convolution"
  bottom: "pool1"
  top: "conv2"
  param {
    lr_mult: 1
  }
  param {
    lr_mult: 2
  }
  convolution_param {
    num_output: 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
  }
}
layer {
  name: "ip1"
  type: "InnerProduct"
  bottom: "pool2"
  top: "ip1"
  param {
    lr_mult: 1
  }
  param {
    lr_mult: 2
  }
  inner_product_param {
    num_output: 500
    weight_filler {
      type: "xavier"
    }
    bias_filler {
      type: "constant"
    }
  }
}
layer {
  name: "relu1"
  type: "ReLU"
  bottom: "ip1"
  top: "ip1"
}
layer {
  name: "ip2"
  type: "InnerProduct"
  bottom: "ip1"
  top: "ip2"
  param {
    lr_mult: 1
  }
  param {
    lr_mult: 2
  }
  inner_product_param {
    num_output: 10
    weight_filler {
      type: "xavier"
    }
    bias_filler {
      type: "constant"
    }
  }
}
layer {
  name: "accuracy"
  type: "Accuracy"
  bottom: "ip2"
  bottom: "label"
  top: "accuracy"
  include {
    phase: TEST
  }
}
layer {
  name: "loss"
  type: "SoftmaxWithLoss"
  bottom: "ip2"
  bottom: "label"
  top: "loss"
}

没问题了?那么我们还需要写一个solver,即网络的解法,或者说我们要告诉Caffe如何去训练LeNet。


4 定义LeNet的解法文件

Caffe给的例子中已经写了,其内容如下,强烈推荐你自己试着写一遍。文件$CAFFE_ROOT/examples/mnist/lenet_solver.prototxt的内容如下:

# The train/test net protocol buffer definition
net: "examples/mnist/my_lenet.prototxt" # 这里换成我们定义的网络结构文件
# test_iter specifies how many forward passes the test should carry out.
# In the case of MNIST, we have test batch size 100 and 100 test iterations,
# covering the full 10,000 testing images.
test_iter: 100 # 每次测试执行100次前向传播
# Carry out testing every 500 training iterations.
test_interval: 500 # 训练每迭代500次就测试一次
# The base learning rate, momentum and the weight decay of the network.
base_lr: 0.01  # 基本学习速率
momentum: 0.9  # 冲量梯度下降参数
weight_decay: 0.0005 # 正则化参数
# The learning rate policy
lr_policy: "inv" # 学习速率的更新模式
# inv: return base_lr * (1 + gamma * iter) ^ (- power) 此方法可逐渐降低学习速率,防止发散
gamma: 0.0001
power: 0.75
# Display every 100 iterations
display: 100 # 100次显示一次当前的loss
# The maximum number of iterations
max_iter: 10000 # 最多迭代10000次
# snapshot intermediate results
snapshot: 5000 # 5000次迭代保存一次模型
snapshot_prefix: "examples/mnist/lenet"
# solver mode: CPU or GPU
solver_mode: GPU # 使用GPU训练

上面的注释解释了这些设置的含义。总结一下,在Caffe中想要训练一个网络,必须提供下面这些东西:

  • 数据(LMDB方式存储)
  • 网络结构的定义
  • 网络的solver(其中有个参数net的值即为网络结构定义文件)

5 训练LeNet模型

训练模型非常简单。编译Caffe时已经在目录$CAFFE_ROOT/build/tools目录下生成了一个名为caffe的可执行文件,它就是我们用于训练网络的工具,输入如下命令进行训练:

$ cd $CAFFE_ROOT
$ ./build/tools/caffe train --solver=examples/mnist/lenet_solver.prototxt

当然你也可以使用caffe写好的训练脚本(内容和上面的差不多):

$ ./examples/mnist/train_lenet.sh

然后你就会看到刷拉拉地一行行训练信息不停地显示出来。MNIST数据集不大,很快就能训练完成。


下一篇文章:待续


欢迎加群交流,点击链接加入群【Python爱好者交流】
这里写图片描述

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页