转自:http://blog.youkuaiyun.com/wuqingshan2010/article/details/71211467
解析./build/tools/caffe train --solver=examples/mnist/lenet_solver.prototxt
第一个参数build/tools/caffe是Caffe框架的主要框架,由tools/caffe.cpp文件编译而来,第二个参数train表示是要训练网络,第三个参数是 solver的protobuf描述文件。
在Caffe中,网络模型的描述及其求解都是通过 protobuf 定义的,模型的参数也是通过 protobuf 实现加载和存储,包括 CPU 与 GPU 之间的无缝切换,不需要通过硬编码的方式实现。在caffe.cpp中main函数之外,通过宏RegisterBrewFunction将train(),test(),device_query(),time()等函数及其对应的函数指针添加到了g_brew_map中, 通过GetBrewFunction可以得到需要调用的函数的函数指针。
1.网络初始化过程
第二个参数train调用caffe.cpp中的int train()函数。在train函数中:
- 1
- 2
首先定义了一个指向Solver的shared_ptr,然后其通过调用SolverRegistry类的静态成员函数CreateSolver得到一个指向Solver的指针来构造shared_ptr类型的solver。
由于C++多态性,尽管solver是一个指向基类Solver类型的指针,通过solver这个智能指针来调用各个子类(SGDSolver等)的函数。在caffe.proto文件中默认的优化type为SGD,所以上面的代码会实例化一个SGDSolver的对象,SGDSolver类继承于Solver类,在新建SGDSolver对象时会调用其构造函数如下所示:
- 1
- 2
- 3
其中会先调用父类的Solver的构造函数。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
Solver类的构造函数通过Init(param)函数来初始化网络。在Init(param)函数中,又主要是通过InitTrainNet()和InitTestNets()函数分别来搭建训练网络结构和测试网络结构。训练网络只能有一个,在InitTrainNet()函数中首先会设置一些基本参数,包括设置网络的状态为TRAIN,确定训练网络只有一个等,然后会通过net_.reset(new
Net<Dtype>(net_param))
新建了一个Net对象。新建了Net对象之后会调用Net类的构造函数:
- 1
- 2
- 3
- 4
Net类的构造函数是通过Init(param)函数来初始化网络结构的。
在Init()函数中,LayerRegistry<Dtype>::CreateLayer(layer_param)
主要是通过调用LayerRegistry这个类的静态成员函数CreateLayer得到一个指向Layer类的shared_ptr类型指针,并把每一层的指针保存到vector<shared_ptr<Layer<Dtype>>>layers_
指针容器里。即根据每层的参数layer_param实例化了对应的各个子类层,比如conv_layer(卷积层)和pooling_layer(池化层),实例化了各层就会调用每个层的构造函数。
Init()函数主要有四部分:
- AppendBottom:设置每一层的输入数据 。
- AppendTop:设置每一层的输出数据。
- layers_[layer_id]->SetUp:对上面设置的输入输出数据计算分配空间,并设置每层的可学习参数(权值和偏置)。
- AppendParam:对上面申请的可学习参数进行设置,主要包括学习率和正则率等。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
Layer类的Setup()函数,对每一层的设置主要由两个函数组成:
LayerSetUp(bottom, top):由Layer类派生出的特定类都需要重写这个函数,主要功能是设置权值参数(包括偏置)的空间以及对权值参数经行随机初始化。
Reshape(bottom, top):根据输出blob和权值参数计算输出blob的维数,并申请空间。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
经过上述过程基本上就完成了初始化的工作,总体的流程是新建一个Solver对象,然后调用Solver类的构造函数,然后在Solver的构造函数中又会新建Net类实例,在Net类的构造函数中又会新建各个Layer的实例,一直具体到设置每个Blob。
2.训练过程
网络的初始化即创建一个solver指针并逐步调用Solver、Net、Layer、Blob类的构造函数,完成整个网络的初始化。完成初始化工作之后,指向Solver类的指针solver开始调用Solver类的成员函数Solve():solver->Solve()
。
Solve函数其实主要就是调用了Solver的另一个成员函数Step()来完成实际的迭代训练过程。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
Step()函数主要分为三个部分,首先是一个大循环设置了总的迭代次数,在每次迭代中训练iter_size * batch_size个样本(在GPU的显存不够的时候使用),例如设置batch_size为128,iter_size是默认为1的,但是会out_of_memory,借助这个方法,可以设置batch_size=32,iter_size=4,那实际上每次迭代还是处理了128个数据。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
ForwardBackward()函数如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
Forward(&loss)函数最终会执行到如下代码:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
具体的每一层Layer的派生类均会重写Forward()函数来实现不同层的前向计算功能。Backward()反向求导函数也和Forward()类似,调用不同层的Backward()函数来计算每层的梯度。
ApplyUpdate()函数是Solver类的纯虚函数,需要派生类来实现。SGDSolver类实现的ApplyUpdate()函数:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
ApplyUpdate()函数主要完成以下工作:
- 设置参数的学习率;
- 对梯度进行Normalize;
- 对反向求导得到的梯度添加正则项的梯度;
- 最后根据SGD算法计算最终的梯度;
- 最后的最后把计算得到的最终梯度对权值进行更新。