今天花了一整天时间进行阅读和调试Caffe框架代码,单单是以Lenet网络进行测试就可见框架的大致工作原理。贾扬清在Caffe中大量使用了STL、模板、智能指针,有些地方为了效率也牺牲了一些代码可读性,处处彰显了大牛风范。为了他人阅读方便,现将代码流程简单梳理一下。

1.LeNet卷积神经网络模型

先看一下1989年Yann LeCun提出的LeNet卷积神经网络模型,

Caffe框架源码剖析(1)—构建网络

左侧是输入的手写图像,经过C1,S2,C3,S4两对卷积池化层,再经过C5和F6两个全连接隐层得到输出Output。

我们所使用的手写图像库来自于Yann LeCun网站
http://yann.lecun.com/exdb/mnist/
  上下载的手写数字图像库,分辨率为28*28,和模型示意图中稍有区别。其它各层的节点数也稍有不同:C1层不是6个卷积模板,而是20个;C3层为50个不同卷积模板;C5层的神经元节点数为500个。

2.开始构建网络

首先按照前一篇博文生成lmdb文件,使用Caffe自带的网络配置文件lenet_train_test.prototxt,开始启动训练网络

  1. caffe.exe train --solver=examples/mnist/lenet_solver.prototxt  

caffe.cpp文件的 main() 函数中通过宏隐式的调用了函数 train(),在函数 train() 中我们发现

  1. int train()  
  2. {  
  3.     ...  
  4.     // 创建solver  
  5.     shared_ptr<caffe::Solver<float> >  
  6.       solver(caffe::SolverRegistry<float>::CreateSolver(solver_param));  
  7.     ...  
  8. }  

我们需要关心CreateSolver()函数是如何实现的,

  1. // Get a solver using a SolverParameter.  
  2. static Solver<Dtype>* CreateSolver(const SolverParameter& param)  
  3. {  
  4.     const string& type = param.type();  
  5.     CreatorRegistry& registry = Registry();  
  6.     CHECK_EQ(registry.count(type), 1) << "Unknown solver type: " << type  
  7.         << " (known types: " << SolverTypeListString() << ")";  
  8.     return registry[type](param);  
  9. }  

关键之处在于上面代码最后一行语句,它的作用是根据配置文件创建对应的Solver对象(默认为SGDSolver子类对象)。此处工厂模式和一个关键的宏REGISTER_SOLVER_CLASS(SGD)发挥了重要作用。

  1. #define REGISTER_SOLVER_CLASS(type)                                            \  
  2.   template <typename Dtype>                                                    \  
  3.   Solver<Dtype>* Creator_##type##Solver(                                       \  
  4.       const SolverParameter& param)                                            \  
  5.   {                                                                            \  
  6.     return new type##Solver<Dtype>(param);                                     \  
  7.   }                                                                            \  
  8.   REGISTER_SOLVER_CREATOR(type, Creator_##type##Solver)  
  9.   
  10. }  

这样一个SGDSolver对象就被动态创建出来了。在Solver基类的构造函数中,调用了成员函数Init()实现初始化:

  1. // Solver类构造函数  
  2. template <typename Dtype>  
  3. Solver<Dtype>::Solver(const SolverParameter& param, const Solver* root_solver)  
  4.     : net_(), callbacks_(), root_solver_(root_solver),  
  5.       requested_early_exit_(false)  
  6.       {  
  7.           Init(param);  
  8.       }  
  9. }  
  1. template <typename Dtype>  
  2. void Solver<Dtype>::Init(const SolverParameter& param)  
  3. {  
  4.     ...  
  5.   
  6.     // 初始化训练网络  
  7.     InitTrainNet();  
  8.   
  9.     // 初始化测试网络  
  10.     InitTestNet();  
  11.   
  12.     // 迭代次数清零  
  13.     iter_ = 0;  
  14. }  

构建网络的代码便藏身在成员函数InitTrainNet()中,我们继续往内部追踪,

  1. template <typename Dtype>  
  2. void Solver<Dtype>::InitTrainNet()  
  3. {  
  4.     ...  
  5.   
  6.     // 从文件读取网络参数  
  7.     NetParameter net_param;  
  8.     ReadNetParamsFromTextFileOrDie(param_.net(), &net_param);  
  9.       
  10.     // 构造网络  
  11.     net_.reset(new Net<Dtype>(net_param));  
  12. }  

最后一行语句动态创建Net对象,并构造了智能指针对象net_。锲而不舍,继续追踪Net类的构造函数,

  1. template <typename Dtype>  
  2. Net<Dtype>::Net(const NetParameter& param, const Net* root_net)  
  3.     : root_net_(root_net) {  
  4.   Init(param);  
  5. }  

内幕马上就要揭晓了,真相就隐藏在Net::Init()成员函数中,

  1. // Initialize a network with a NetParameter  
  2. template <typename Dtype>  
  3. void Net<Dtype>::Init(const NetParameter& in_param)  
  4. {  
  5.     NetParameter filtered_param;  
  6.     // 过滤掉PHASE为TEST的layer  
  7.     FilterNet(in_param, &filtered_param);  
  8.   
  9.     // 建立网络的bottom_blob数组和top_blob数组  
  10.     bottom_vecs_.resize(param.layer_size());  
  11.     top_vecs_.resize(param.layer_size());  
  12.       
  13.     // 对余下的layer进行遍历  
  14.     for (int layer_id = 0; layer_id < param.layer_size(); ++layer_id)  
  15.     {  
  16.         // 创建layer  
  17.         layers_.push_back(LayerRegistry<Dtype>::CreateLayer(layer_param));  
  18.           
  19.         // 网络中添加新的bottom blob  
  20.         AppendBottom(param, layer_id, bottom_id, &available_blobs, &blob_name_to_idx);  
  21.           
  22.         // 网络中添加新的top blob  
  23.         AppendTop(param, layer_id, top_id, &available_blobs, &blob_name_to_idx);  
  24.           
  25.         // 构建网络  
  26.         layers_[layer_id]->SetUp(bottom_vecs_[layer_id], top_vecs_[layer_id]);  
  27.           
  28.         // 将可学习权重的blob添加到learnable_params_中,用于在Net::Update()时遍历更新使用  
  29.         AppendParam(const NetParameter& param, const int layer_id, const int param_id);  
  30.           
  31.         // 为需要反向传导的层设置标识(除了数据层,其它层都需要反向传导)  
  32.         layer_need_backward_.push_back(need_backward);  
  33.     }  
  34.       
  35.     // 网络初始化完成  
  36.     LOG_IF(INFO, Caffe::root_solver()) << "Network initialization done.";  
  37. }  

我们发现这里关键在于layer的构造。lenet_train_test.prototxt文件中共定义了11个layer,而用于构建网络的是9个(除去了一个用于测试数据的layer和统计计算精度的Accuracy layer)。在对这9个layer进行遍历时,首先构造其底部的Blob,再构造顶部Blob,然后调用Layer::SetUp()函数进行初始化和Reshape操作。Layer::SetUp()函数的实现细节为下面的这段代码。

  1. // Calls LayerSetUp to do special layer setup for individual layer types,  
  2. // followed by Reshape to set up sizes of top blobs and internal buffers  
  3. template <typename Dtype>  
  4. void Layer<Dtype>::SetUp(const vector<Blob<Dtype>*>& bottom, const vector<Blob<Dtype>*>& top)  
  5. {  
  6.     // 初始化互斥量  
  7.     InitMutex();  
  8.     CheckBlobCounts(bottom, top);  
  9.     // 调用虚函数,逐层进行配置  
  10.     LayerSetUp(bottom, top);  
  11.     Reshape(bottom, top);  
  12.     // 设置损失权重  
  13.     SetLossWeights(top);  
  14. }  

至此,我们的网络构建就大功告成了!

3.构建完成的网络模型

让我们来看一下构建好的网络模型是什么样子,11个layer示意图如下所示

Caffe框架源码剖析(1)—构建网络

Blob关系图:

Caffe框架源码剖析(1)—构建网络

最后附上一张Layer类类图:

Caffe框架源码剖析(1)—构建网络