您的位置:首页 > Web前端

Caffe官方教程翻译(1):LeNet MNIST Tutorial

2018-02-24 16:33 537 查看

前言

最近打算重新跟着官方教程学习一下caffe,顺便也自己翻译了一下官方的文档。自己也做了一些标注,都用斜体标记出来了。中间可能额外还加了自己遇到的问题或是运行结果之类的。欢迎交流指正,拒绝喷子!

官方教程的原文链接:http://caffe.berkeleyvision.org/gathered/examples/mnist.html

Training LeNet on MNIST with Caffe

事先声明,我们默认你已经成功地编译了Caffe源码。如果没有,请参考Installation Page。在这篇教程中,我们也默认认为你的caffe安装在CAFFE_ROOT

准备数据集

首先,你需要从MNIST网页上下载数据并转换数据集的格式(直接下载的数据都是压缩了的)。为了做到这个,我只需要简单地在终端运行如下命令:

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


如果终端反馈报错说
wget
或是
gunzip
没有安装,你还需要分别安装他们。在运行了这些脚本之后,会生成两个数据集:mnist_train_lmdbmnist_test_lmdb

LeNet:MNIST分类模型

在我们实际运行训练程序之前,让我们先解释一下究竟会发生什么。我们将会使用LeNet网络,众所周知,这个网络在手写数字分类任务上表现得非常好。我们将会跑一个与原始的LeNet相比,稍微有些不同的版本的网络。在这个网络中,神经元的sigmoid激活函数被替换为ReLu激活函数了。(补充:线性修正单元激活函数,简称ReLu函数,嘛,一般来说ReLuctant函数可以使得网络的训练更快,提升训练效果,除了最后输出层的激活函数有时不得不使用sigmoid函数外,隐含层中激活函数一律使用ReLu函数会比使用sigmoid函数取得更好的效果)

LeNet网络的设计包含了卷积神经网络(CNN)的本质,在一些大型模型诸如用于ImageNet的模型中也依然通用。一般来说,它包含了一个卷积层后面跟着一个池化层,另外一个卷积层再跟一个池化层,加上两个全连接层(近似与卷积多层感知机),我们把网络定义在下面的文件中:

$CAFFE_ROOT/examples/mnist/lenet_train_test.prototxt


定义MNIST网络

这个部分主要讲解lenet_train_test.prototxt,其中详细定义了LeNet模型,用于手写数字分类。我们默认你已经很熟悉Google Protobuf,并且已经阅读过caffe中使用的protobuf定义(定义在
$CAFFE_ROOT/src/caffe/proto/caffe.proto
)。

接下来我们要具体根据protobuf写下caffe的神经网络,caffe::NetParameter(或是基于python,caffe.proto.caffe_pb2.NetParameter)。我们首先要给网络定义一个名字:

name: "LeNet"


编写数据层

现在,我们要从我们之前创建的lmdb数据库文件读取MNIST的数据了。下面是定义的一个数据层:

layer {
name: "mnist"
type: "Data"
transform_param {
scale:0.00390625
}
data_param {
source: "mnist_train_lmdb"
backend: LMDB
batch_size: 64
}
top: "data"
top: "label"
}


很明确地,我们可以看到,这个层的名字是mnist,类型是Data,并且他会从给定的lmdb文件中读取数据。每次抽取的batch_size为64,并且限制读入的像素的灰度范围为[0,1)[0,1)。这里可能会有疑问了,为什么是0.003906250.00390625这个数?因为它等于12561256。(补充:因为通常读入的每个像素的灰度范围都是从0到255,这里意思就是做一个归一化,目的就是可以让后面的数据也可以更好处理)最后呢,这个层会产生两个blob(补充:亲切点,叫它泡泡吧。好了不开玩笑了,就理解为一个单元就行了),一个是datablob,一个是labelblob。(补充:训练时,datablob负责读入训练数据,labelblob负责读入标签,很好理解)

编写卷积层

让我们来定义第一个卷积层:

layer {
name: "conv1"
type: "Convolution"
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"
}
}
bottom: "data"
top: "conv1"
}


这一层接收了了前面的数据层送来的数据,并生成了conv1层。上面定义了,生成的conv1层的输出num_output有20个通道,卷积核的大小kernel_size为5(5×55×5的卷积核),步长stride为1(1×11×1 的步长)。

上面定义了两个filler,一个是weight_filler,一个是bias_filler。这些filler让我们可以初始化权重和偏置参数。对于weight_filler来说,我们使用xavier算法来自动根据输入参数数量和输出神经元个数决定初始化参数。对于bias_filler来说,我们很简单地将其初始化为常数,默认的初始化的值是0。

还有几个参数。lr_mult是当前层的可学习参数的学习率的调整参数。这么理解可能比较绕,举个例子说明会好一些。在这个例子中,我们设置第一个可学习参数,即权重参数,其学习率与程序运行时给的学习率相同(1倍);然后是第二个可学习参数,即偏置参数,其学习率是程序运行时给的学习率的2倍。这么做通常能让神经网络更好地收敛。(补充:学习率会在后面定义,这里只要知道训练时caffe会送进来网络的学习率,在前面定义的lr_mult中,权重参数与那个学习率相同吧,而偏置参数会是那个学习率的2倍)

编写池化层

呸呸呸(文档上就是这么写的,皮这一下很开心?),池化层更加容易定义:

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


上面这一段定义了:我们对数据进行最大化池化(max pooling),池化核的大小kernel_size为2(2×22×2 的池化核),步长stride为2(2×22×2的步长)。(不难看出相邻池化区域之间没有重叠部分)

相似地也可以照葫芦画瓢,编写出第二个卷积层和池化层。可以到
$CAFFE_ROOT/examples/mnist/lenet_train_test.prototxt
中查看详细的定义。

编写全连接层

编写全连接层也没什么难的了:

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


这里定义了一个全连接层(要知道在Caffe中全连接层用InnerProduct层表示,直译就是内积),输出有500个(补充:这里不表示通道数了,表示输出神经元个数)。其他所有的行都很眼熟,对吧?

编写ReLu层

一个ReLu层也很简单的:

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


既然ReLu是一个对元素的操作。那么我们可以就地对它处理,以节省一些内存。这里是通过直接把相同的名字给到bottomtopblob**实现的(补充:嘛,就是直接自己连自己)。不过,当然不能直接复制这个**blob的名字给其他类型的层。

ReLu层之后,我们再编写另外一个内积层(全连接层):

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


(补充:基本都和前面一样,但是输出num_output是10个,为什么?因为手写数字是0-9,每一个输出对应一个数字,如果识别的是某个数字,那就输出1就行了,这就是我们常说的one-hot编码)

编写损失层

最后我们还要写一下损失层:

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


这个softmax_loss层声明了softmax和多项logistic损失(这样可以解决时间并提高数值的稳定性)。它需要输入两个blob,第一个会给出预测的结果,第二个是由2数据层提供的标签(还记得不?就在最开始的那个层定义的)。它并不会产生任何输出值,它所做的就只是计算损耗函数的值,当反向传播开始时报告它(补充:命令行上可以看到),并且根据ip2层初始化梯度。这就是魔术真正开始实施的地方。

额外的提示:编写层的规则

每一层的定义可以包含是否和何时他们会被网络包含进去的规则,就像下面这个:

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


这是一项规则,基于当前神经网络的状态,会判断这一层是否应该包含在网络中。你也可以查看$CAFFE_ROOT/src/caffe/proto/caffe.proto来获取更多有关层定义规则和模型架构的信息。

在上面这个例子中,这一层只会在训练(TRAIN phase)阶段被包含进网络。如果我们把TRAIN换成TEST,那么这一层将只会在测试阶段被包含进网络。默认来说,那里也不会有层级规则,一个层总是会被包含进网络。另外,lenet_train_test.prototxt中定义了两个数据层(也有不同的batch_size),一个是用于训练阶段的,而另一个是用于测试阶段的。就如在lenet_solver.prototxt中定义的一样,还有一个Accuracy层,仅仅是在测试阶段(TEST phase)中被引入,以反馈模型在每100步迭代过程中的准确率。

定义MNIST的解决方案

在下面的prototxt 文件中查看定义的一些信息:

$CAFFE_ROOT/examples/mnist/lenet_solver.prototxt

# The train/test net protocol buffer definition
net: "examples/mnist/lenet_train_test.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
# Carry out testing every 500 training iterations.
test_interval: 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"
gamma: 0.0001
power: 0.75
# Display every 100 iterations
display: 100
# The maximum number of iterations
max_iter: 10000
# snapshot intermediate results
snapshot: 5000
snapshot_prefix: "examples/mnist/lenet"
# solver mode: CPU or GPU
solver_mode: GPU


补充:

上面也就是定义了一些训练的设置,比如学习率,迭代次数,snapshot(意思是每迭代多少次保存一次模型)等等,不做赘述了。

为了省事,在我的笔记本上只是装了CPU版本的caffe,所以跑的时候用的是CPU版本的跑。

下面这个东西自己看着改改就行了,后面会提到详细信息:

# solver mode: CPU or GPU
solver_mode: CPU


训练和测试我们的模型

在你编写了网络定义(network definition protobuf)和解决方案(solver protobuf)文件之后,训练模型对你来说就很简单了。直接运行train_lenet.sh脚本文件,或者说自己直接用命令行来训练也行:

cd $CAFFE_ROOT
./examples/mnist/train_lenet.sh


当你运行代码时,你会看到很多像这样的信息一闪而过:

I1203 net.cpp:66] Creating Layer conv1
I1203 net.cpp:76] conv1 <- data
I1203 net.cpp:101] conv1 -> conv1
I1203 net.cpp:116] Top shape: 20 24 24
I1203 net.cpp:127] conv1 needs backward computation.


这些信息告诉了你每一层的细节信息:它的连接方式和输出的大小,这些东西都很方便你调试。在初始化网络结束之后,就会开始进行训练:

I1203 net.cpp:142] Network initialization done.
I1203 solver.cpp:36] Solver scaffolding done.
I1203 solver.cpp:44] Solving LeNet


基于前面对解决方案的设置,我们会打印训练过程中每100次迭代的loss函数的输出值,并且每过500次迭代测试一次网络。你会看到类似下面的信息:

I1203 solver.cpp:204] Iteration 100, lr = 0.00992565
I1203 solver.cpp:66] Iteration 100, loss = 0.26044
...
I1203 solver.cpp:84] Testing net
I1203 solver.cpp:111] Test score #0: 0.9785
I1203 solver.cpp:111] Test score #1: 0.0606671


对于训练中每一次迭代,lr是那次迭代的学习率,并且loss就是训练中损失函数输出的值。对于测试阶段的输出来说,score 0输出的是准确率,score 1输出的是损失函数。

过了几分钟,训练就完成了。(补充:GPU哈,CPU不可能的)

I1203 solver.cpp:84] Testing net
I1203 solver.cpp:111] Test score #0: 0.9897
I1203 solver.cpp:111] Test score #1: 0.0324599
I1203 solver.cpp:126] Snapshotting to lenet_iter_10000
I1203 solver.cpp:133] Snapshotting solver state to lenet_iter_10000.solverstate
I1203 solver.cpp:78] Optimization Done.


补充:可以看到这里的准确率是0.9897,我在训练时得到的是0.991左右的准确率,这个结果可以说比较理想了。

最后的模型,会存储为二进制protobuf文件,存为:

lenet_iter_10000


如果你在某一个现实应用的数据集中训练的话,你可以在你的应用中将它作为一个训练好的模型直接调用。

额…怎么用GPU训练?

你刚刚就是用GPU训练的!所有的训练都是运行在GPU上的。事实上,如果你想在CPU上训练,你可以很简单地改变lenet_solver.prototxt中的一行:

# solver mode: CPU or GPU
solver_mode: CPU


然后你就可以在CPU上进行训练了。这不是很简单吗?

MNIST是一个很小的数据集,所以就通信开销来说使用GPU训练并不显得有多大有时。在更大的数据集上训练那些更大更复杂的模型时,比如ImageNet,CPU和GPU计算速度的差距会变得十分明显。

如何在特定的步骤中减小学习率?

请查阅 lenet_multistep_solver.prototxt文件。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: