为了更好的学习caffe,我们利用上节安装好的环境,进行单步调试,以窥caffe全貌。
准备工作:要在vs2013中单步跟踪调试caffe,需要配置caffe工程,打开【属性】-【调试】-【命令行参数】中加入输入参数。如下配置:
先贴一张caffe的整体处理流程:
一、函数入口
众所周知,caffe由c++写的,而c++的入口函数为main,我们在caffe.cpp文件中找到main函数,关键代码如下:
1 int main(int argc, char** argv) {
2 .....
3 caffe::GlobalInit(&argc, &argv);
4 ......
5 return GetBrewFunction(caffe::string(argv[1]))();
6 ....
7 }
函数进来后首先进行gflags的一些初始化,设置并打印版本信息,用户信息等。
接着进行的是GlobalInit函数,主要作用是对gflags和glog的一些初始化,该函数定义在了caffe安装目录./src/caffe/common.cpp中。 其中gflags是google的一个开源的处理命令行参数的库,而glog是google的开源日志库。
上面完成了一些初始化工作,而真正的程序入口就是下面这个GetBrewFunction函数,GetBrewFunction函数的入参为argv[1],也就是我们train字符串。而GetBrewFunction的返回值为g_brew_map。那么g_brew_map是怎么实现的呢?代码如下:
1 typedef int (*BrewFunction)();
2 typedef std::map BrewMap;
3 BrewMap g_brew_map;
首先通过 typedef定义函数指针 typedef int (*BrewFunction)(); 这个是用typedef定义函数指针方法。然后再把输入的字符串和以该字符串命名的函数用map容器关联起来,这样就实现了用户输入字符串调用相应函数的功能。
在caffe.cpp 中 BrewFunction 作为GetBrewFunction()函数的返回类型,可以是 train(),test(),device_query(),time() 这四个函数指针的其中一个。四个函数功能如下:
(1)train: 训练或者调整一个模型
(2)test : 在测试集上测试一个模型
(3)device_query : 打印GPU的调试信息
(4)time: 压测一个模型的执行时间
caffe是通过RegisterBrewFunction()实现四个函数的注册的,具体代码如下:
1 #define RegisterBrewFunction(func) \
2 namespace { \
3 class __Registerer_##func { \
4 public: /* NOLINT */ \
5 __Registerer_##func() { \
6 g_brew_map[#func] = &func; \
7 } \
8 }; \
9 __Registerer_##func g_registerer_##func; \
10 }
这里有必要解释下#的用法,相信很多人看这段宏定义代码都懵吧。在C/C++的宏中,"#"的功能是将其后面的宏参数进行字符串化操作(Stringfication),简单说就是在对它所引用的宏变量通过替换后在其左右各加上一个双引号。而”##”被称为连接符(concatenator),用来将两个子串Token连接为一个Token。注意这里连接的对象是Token就行,而不一定是宏的变量。 所谓的子串(token)就是指编译器能够识别的最小语法单元。这样以上的宏定义代码的功能就显而易见了。如果需要,也可以增加你自己的函数,然后通过RegisterBrewFunction()注册一下即可使用。
二、train函数
接着调用train()函数,train函数中主要有三个方法ReadSolverParamsFromTextFileOrDie、CreateSolver、Solve。关键代码如下所示:
1 // Train / Finetune a model.
2 int train() {
3 ......
4 caffe::SolverParameter solver_param;
5 caffe::ReadSolverParamsFromTextFileOrDie(FLAGS_solver, &solver_param);//从-solver参数读取solver_param
6 ......
7 shared_ptr >
8 solver(caffe::SolverRegistry::CreateSolver(solver_param));//从参数创建solver,同样采用string到函数指针的映射实现,用到了工厂模式
9
10 if (FLAGS_snapshot.size()) {//迭代snapshot次后保存模型一次
11 LOG(INFO) << "Resuming from " << FLAGS_snapshot;
12 solver->Restore(FLAGS_snapshot.c_str());
13 } else if (FLAGS_weights.size()) {//若采用finetuning,则拷贝weight到指定模型
14 CopyLayers(solver.get(), FLAGS_weights);
15 }
16
17 if (gpus.size() > 1) {
18 caffe::P2PSync sync(solver, NULL, solver->param());
19 sync.Run(gpus);
20 } else {
21 LOG(INFO) << "Starting Optimization";
22 solver->Solve();//开始训练网络
23 }
24 LOG(INFO) << "Optimization Done.";
25 return 0;
26 }
1、初始化
train函数进来后先进行一些参数检测工作,检测FLAGS_solver.size()是否为零,为零的话表示用户没有传入solver文件 ;接着再检查参数里面--weights和--snapshot有没有同时出现,因为--weights是 在从头启动训练的时候需要的参数,表示对模型的finetune,而--snapshot表示的是继续训练模型, 这种情况对应于用户之前暂停了模型训练,现在继续训练。因此不再需要weight参数。接着就是去获取并解析用户定义的solver.prototxt文件。
2、读取solver参数:ReadSolverParamsFromTextFileOrDie
caffe::ReadSolverParamsFromTextFileOrDie(FLAGS_solver, &solver_param)解析-solver指定的solver.prototxt的文件内容到solver_param中
3、查询GPU信息,并进行GPU初始化,如果未配置,则直接跳过
查询用户配置的GPU信息,用户可以在输入命令行的时候配置gpu信息,也可以在solver.prototxt 文件中定义GPU信息,如果用户在solver.prototxt里面配置了GPU的id,则将该id写入FLAGS_gpu中,如果用户只是说明了使用gpu模式,而没有详细指定使用的gpu的id,则将gpu的id默认为0。然后根据gpu的检测结果,如果没有gpu信息,那么则使用cpu训练,否则,就开始一些GPU训练的初始化工作。
3、构造网络训练器:CreateSolver
CreateSolver函数实现细节如下:
1 static Solver* CreateSolver(const SolverParameter& param) {
2 const string& type = param.type();
3 CreatorRegistry& registry = Registry();
4 CHECK_EQ(registry.count(type), 1) << "Unknown solver type: " << type
5 << " (known types: " << SolverTypeListString() << ")";
6 return registry[type](param);
7 }
首先通过CreatorRegistry®istry = Registry()对caffe的所有求解器进行注册,并通过map容器将求解器的名称字符串和对应函数指针联结起来。最后返回registrytype;所以该段程序的核心功能是执行了solver参数中的求解器对应的构造函数,如Creator_SGDSolver(const SolverParameter& param)函数。
class SGDSolver : public Solver
构建SGDSolver,首先执行Solver类的构造,该函数是初始化的入口,调用 void Solver
1 net_.reset(new Net(net_param));
Net构造中先执行Init()操作,该函数具体的内容如下图和源码所示:
1 template
2 void Net::Init(const NetParameter& in_param) {
3 ........//过滤校验参数FilterNet
4 FilterNet(in_param, &filtered_param);
5 .........//插入Splits层
6 InsertSplits(filtered_param, ¶m);
7 .......// 构建网络中输入输出存储结构
8 bottom_vecs_.resize(param.layer_size());
9 top_vecs_.resize(param.layer_size());
10 bottom_id_vecs_.resize(param.layer_size());
11 param_id_vecs_.resize(param.layer_size());
12 top_id_vecs_.resize(param.layer_size());
13 bottom_need_backward_.resize(param.layer_size());
14
15 for (int layer_id = 0; layer_id < param.layer_size(); ++layer_id) {
16 ...//创建层
17 layers_.push_back(LayerRegistry::CreateLayer(layer_param));
18 layer_names_.push_back(layer_param.name());
19 LOG_IF(INFO, Caffe::root_solver())
20 << "Creating Layer " << layer_param.name();
21 bool need_backward = false;
22
23 // Figure out this layer's input and output
24 for (int bottom_id = 0; bottom_id < layer_param.bottom_size();
25 ++bottom_id) {
26 const int blob_id = AppendBottom(param, layer_id, bottom_id,
27 &available_blobs, &blob_name_to_idx);
28
29
30 ........//创建相关blob
31 // If the layer specifies that AutoTopBlobs() -> true and the LayerParameter
32 // specified fewer than the required number (as specified by
33 // ExactNumTopBlobs() or MinTopBlobs()), allocate them here.
34 Layer* layer = layers_[layer_id].get();
35 if (layer->AutoTopBlobs()) {
36 const int needed_num_top =
37 std::max(layer->MinTopBlobs(), layer->ExactNumTopBlobs());
38 for (; num_top < needed_num_top; ++num_top) {
39 // Add "anonymous" top blobs -- do not modify available_blobs or
40 // blob_name_to_idx as we don't want these blobs to be usable as input
41 // to other layers.
42 AppendTop(param, layer_id, num_top, NULL, NULL);
43 }
44 }
45
46
47 .....//执行SetUp()
48 // After this layer is connected, set it up.
49 layers_[layer_id]->SetUp(bottom_vecs_[layer_id], top_vecs_[layer_id]);
50 LOG_IF(INFO, Caffe::root_solver())
51 << "Setting up " << layer_names_[layer_id];
52 for (int top_id = 0; top_id < top_vecs_[layer_id].size(); ++top_id) {
53 if (blob_loss_weights_.size() <= top_id_vecs_[layer_id][top_id]) {
54 blob_loss_weights_.resize(top_id_vecs_[layer_id][top_id] + 1, Dtype(0));
55 }
56 blob_loss_weights_[top_id_vecs_[layer_id][top_id]] = layer->loss(top_id);
57 LOG_IF(INFO, Caffe::root_solver())
58 << "Top shape: " << top_vecs_[layer_id][top_id]->shape_string();
59 if (layer->loss(top_id)) {
60 LOG_IF(INFO, Caffe::root_solver())
61 << " with loss weight " << layer->loss(top_id);
62 }
63 memory_used_ += top_vecs_[layer_id][top_id]->count();
64 }
65 LOG_IF(INFO, Caffe::root_solver())
66 << "Memory required for data: " << memory_used_ * sizeof(Dtype);
67 const int param_size = layer_param.param_size();
68 const int num_param_blobs = layers_[layer_id]->blobs().size();
69 CHECK_LE(param_size, num_param_blobs)
70 << "Too many params specified for layer " <<
SetUp是怎么构建的呢?
1 virtual void LayerSetUp(const vector*>& bottom,
2 const vector*>& top) {}
3
4 void SetUp(const vector*>& bottom,
5 const vector*>& top) {
6 InitMutex();
7 CheckBlobCounts(bottom, top);
8 LayerSetUp(bottom, top);
9 Reshape(bottom, top);
10 SetLossWeights(top);
11 }
初始化的总体流程大概就是新建一个Solver对象,然后调用Solver类的构造函数,然后在Solver的构造函数中又会新建Net类实例,在Net类的构造函数中又会新建各个layer的实例,一直具体到设置每个Blob,大概就完成了网络初始化的工作了。
(3)Solve
train函数中CreateSolver()执行完成后,接下来是具体训练过程,执行Solve()函数---->Step()--->结束
Solve的具体内容和代码:
1 template
2 void Solver::Solve(const char* resume_file) {
3 CHECK(Caffe::root_solver());
4 LOG(INFO) << "Solving " << net_->name();
5 LOG(INFO) << "Learning Rate Policy: " << param_.lr_policy();
6
7 // For a network that is trained by the solver, no bottom or top vecs
8 // should be given, and we will just provide dummy vecs.
9 int start_iter = iter_;
10 Step(param_.max_iter() - iter_);
11
12 // overridden by setting snapshot_after_train := false
13 if (param_.snapshot_after_train()
14 && (!param_.snapshot() || iter_ % param_.snapshot() != 0)) {
15 Snapshot();
16 }
17
18 // display loss
19 if (param_.display() && iter_ % param_.display() == 0) {
20 int average_loss = this->param_.average_loss();
21 Dtype loss;
22 net_->Forward(&loss);
23
24 UpdateSmoothedLoss(loss, start_iter, average_loss);
25
26
27 if (param_.test_interval() && iter_ % param_.test_interval() == 0) {
28 TestAll();
29 }
30 }
然后开始执行Step函数,具体内容和代码:
1 template
2 void Solver::Step(int iters)
3 {
4 // 起始迭代步数
5 const int start_iter = iter_;
6 // 终止迭代步数
7 const int stop_iter = iter_ + iters;
8
9 // 判断是否已经完成设定步数
10 while (iter_ < stop_iter)
11 {
12 // 将net_中的Bolb梯度参数置为零
13 net_->ClearParamDiffs();
14
15 ...
16
17 // accumulate the loss and gradient
18 Dtype loss = 0;
19 for (int i = 0; i < param_.iter_size(); ++i)
20 {
21 // 正向传导和反向传导,并计算loss
22 loss += net_->ForwardBackward();
23 }
24 loss /= param_.iter_size();
25
26 // 为了输出结果平滑,将临近的average_loss个loss数值进行平均,存储在成员变量smoothed_loss_中
27 UpdateSmoothedLoss(loss, start_iter, average_loss);
28
29 // BP算法更新权重
30 ApplyUpdate();
31
32 // Increment the internal iter_ counter -- its value should always indicate
33 // the number of times the weights have been updated.
34 ++iter_;
35 }
36 }
while循环中先调用了网络类Net::ForwardBackward()成员函数进行正向传导和反向传导,并计算loss
1 Dtype ForwardBackward() {
2 Dtype loss;
3 //正向传导
4 Forward(&loss);
5 //反向传导
6 Backward();
7 return loss;
8 }
而Fordward函数中调用了ForwardFromTo,而FordwardFromTo又调用了每个layer的Fordward。反向传导函数Backward()调用了BackwardFromTo(int start, int end)函数。正向传导和反向传导结束后,再调用SGDSolver::ApplyUpdate()成员函数进行权重更新。
(1)ForwardBackward:按顺序调用了Forward和Backward。
(2)ForwardFromTo(int start, int end):执行从start层到end层的前向传递,采用简单的for循环调用。,forward只要计算损失loss
(3)BackwardFromTo(int start, int end):和前面的ForwardFromTo函数类似,调用从start层到end层的反向传递。backward主要根据loss来计算梯度,caffe通过自动求导并反向组合每一层的梯度来计算整个网络的梯度。
(4)ToProto函数完成网络的序列化到文件,循环调用了每个层的ToProto函数
1 template
2 void SGDSolver::ApplyUpdate()
3 {
4 // 获取当前学习速率
5 Dtype rate = GetLearningRate();
6 if (this->param_.display() && this->iter_ % this->param_.display() == 0)
7 {
8 LOG(INFO) << "Iteration " << this->iter_ << ", lr = " << rate;
9 }
10
11 // 在计算当前梯度的时候,如果该值超过了阈值clip_gradients,则将梯度直接设置为该阈值
12 // 此处阈值设为-1,即不起作用
13 ClipGradients();
14
15 // 逐层更新网络中的可学习层
16 for (int param_id = 0; param_id < this->net_->learnable_params().size();
17 ++param_id)
18 {
19 // 归一化
20 Normalize(param_id);
21 // L2范数正则化添加衰减权重
22 Regularize(param_id);
23 // 随机梯度下降法计算更新值
24 ComputeUpdateValue(param_id, rate);
25 }
26 // 更新权重
27 this->net_->Update();
28 }
最后将迭代次数++iter_,继续while循环,直到迭代次数完成。 这就是整个网络的训练过程。