part one 初步使用caffe
参考链接:http://caffe.berkeleyvision.org/gathered/examples/mnist.html
网易云课堂(程序中文件放在目录~/Documents/caffe/projects/mnist中):https://study.163.com/course/courseLearn.htm?courseId=1003491001#/learn/video?lessonId=1004081127&courseId=1003491001
caffe操作步骤:准备数据(数据)->定义Net(模型)->配置Solver(算法)->Run->分析结果 然后循环以上步骤
Caffe主要由Blob,Layer,Net,Solver这几个部分组成。
Blob:主要用来表示网络中的数据,包括训练数据,网络各层自身的参数(包括权值、偏置以及它们的梯度)
Layer:是对神经网络中各种层的一个抽象
Net:对整个网络的表示,由各种Layer前后连接组合而成,也是我们所构建的网络模型
Solver:定义了针对Net网络模型的求解方法,自定义Solver能够实现不同的网络求解方式
LeNet:MNIST分类模型
组成:卷积层-池化层-卷积层-池化层-全连接层-全连接层
将层定义在$CAFFE_ROOT/examples/mnist/lenet_train_test.prototxt
定义MNIST网络
top表示输出,bottom表示输入
①数据层:类型为Data,读入数据为lmdb格式,即bottom,top为两个blobs,一个是数据blob,一个是标签blob
②卷积层:类型为Convolution,bottom为data,top为conv1,使用定义weight_filler和bias_filler来进行初始化,lr_mults表示学习率
③池化层:类型为Pooling,bottom为conv1,top为pool1
④全连接层:类型为InnerProduct,bottom为pool2,top为ip1
⑤ReLU层:类型为ReLU,bottom为ip1,top为ip1(用了相同的变量,为了节省内存)
⑥Loss层:类型为SoftmaxWithLoss,bottom是ip2和label
(The softmax_loss
layer implements both the softmax and the multinomial logistic loss (that saves time and improves numerical stability).)
定义MNIST Solver
$CAFFE_ROOT/examples/mnist/lenet_solver.prototxt
训练和测试模型
在你写好定义网络的protobuf和solver protobuf文件,即lenet_train_test.prototxt
和lenet_solver.prototxt两个文件,训练模型就变得简单了。
只需要运行:
cd $CAFFE_ROOT
./examples/mnist/train_lenet.sh
会出现以下:
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是迭代的learning rate,loss是训练函数
测试阶段的输出:score 0是accuracy,score 1是测试阶段的loss function
几分钟之后,训练成功,得到一个最终的模型
lenet_iter_10000.caffemodel
这个训练好的模型就可以在实际的应用中进行使用。
part two 源码分析
代码分析参考链接:https://buptldy.github.io/2016/10/09/2016-10-09-Caffe_Code/
训练LeNet
build/tools/caffe
是Caffe框架的主要框架,由tools/caffe.cpp
文件编译而来
在Caffe中,网络模型的描述及其求解、模型的参数实现加载和存储、包括 CPU 与 GPU 之间的无缝切换都是通过 protobuf配置来实现的,不需要通过硬编码的方式实现。
网络初始化
网络的初始化就是从下面一行代码新建一个solver
指针开始一步一步的调用Solver
,Net
,Layer
,Blob
类的构造函数,完成整个网络的初始化。
在训练LeNet的例子中,传入的第二个参数为train,所以调用的函数为caffe.cpp
中的int train()
函数,其中构造了solver指针,总体的流程大概就是新建一个Solver
对象,然后调用Solver
类的构造函数,然后在Solver
的构造函数中又会新建Net
类实例,在Net
类的构造函数中又会新建各个Layer
的实例,一直具体到设置每个Blob
,大概就介绍完了网络初始化的工作。
训练过程
完成初始化之后,就可以开始对网络进行训练了,指向Solver
类的指针solver
开始调用Solver
类的成员函数Solve()
,Solve函数其实主要就是调用了Solver
的另一个成员函数Step()
来完成实际的迭代训练过程。
!!!在GPU显存不够用的时候如何处理?
Step()
函数的主要代码,首先是一个大循环设置了总的迭代次数,在每次迭代中训练iter_size
x batch_size
个样本,这个设置是为了在GPU的显存不够的时候使用,比如我本来想把batch_size设置为128,iter_size是默认为1的,但是会out_of_memory,借助这个方法,可以设置batch_size=32,iter_size=4,那实际上每次迭代还是处理了128个数据。
Step()
函数主要分为三部分:
loss += net_->ForwardBackward();
调用了Net中的代码,主要完成了前向后向的计算,前向用于计算模型的最终输出和Loss,后向用于计算每一层网络和参数的梯度
UpdateSmoothedLoss();
这个函数主要做Loss的平滑。由于Caffe的训练方式是SGD,我们无法把所有的数据同时放入模型进行训练,那么部分数据产生的Loss就可能会和全样本的平均Loss不同,在必要时候将Loss和历史过程中更新的Loss求平均就可以减少Loss的震荡问题
ApplyUpdate();
这个函数是
Solver
类的纯虚函数,需要派生类来实现,比如
SGDSolver
类实现的
ApplyUpdate();
函数如下,主要内容包括:设置参数的学习率;对梯度进行Normalize;对反向求导得到的梯度添加正则项的梯度;最后根据SGD算法计算最终的梯度;最后的最后把计算得到的最终梯度对权值进行更新。