今天花了一整天时间进行阅读和调试Caffe框架代码,单单是以Lenet网络进行测试就可见框架的大致工作原理。贾扬清在Caffe中大量使用了STL、模板、智能指针,有些地方为了效率也牺牲了一些代码可读性,处处彰显了大牛风范。为了他人阅读方便,现将代码流程简单梳理一下。
1.LeNet卷积神经网络模型
先看一下1989年Yann LeCun提出的LeNet卷积神经网络模型,
左侧是输入的手写图像,经过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,开始启动训练网络
- caffe.exe train --solver=examples/mnist/lenet_solver.prototxt
caffe.cpp文件的 main() 函数中通过宏隐式的调用了函数 train(),在函数 train() 中我们发现
- int train()
- {
- ...
- // 创建solver
- shared_ptr<caffe::Solver<float> >
- solver(caffe::SolverRegistry<float>::CreateSolver(solver_param));
- ...
- }
我们需要关心CreateSolver()函数是如何实现的,
- // Get a solver using a SolverParameter.
- static Solver<Dtype>* CreateSolver(const SolverParameter& param)
- {
- const string& type = param.type();
- CreatorRegistry& registry = Registry();
- CHECK_EQ(registry.count(type), 1) << "Unknown solver type: " << type
- << " (known types: " << SolverTypeListString() << ")";
- return registry[type](param);
- }
关键之处在于上面代码最后一行语句,它的作用是根据配置文件创建对应的Solver对象(默认为SGDSolver子类对象)。此处工厂模式和一个关键的宏REGISTER_SOLVER_CLASS(SGD)发挥了重要作用。
- #define REGISTER_SOLVER_CLASS(type) \
- template <typename Dtype> \
- Solver<Dtype>* Creator_##type##Solver( \
- const SolverParameter& param) \
- { \
- return new type##Solver<Dtype>(param); \
- } \
- REGISTER_SOLVER_CREATOR(type, Creator_##type##Solver)
- }
这样一个SGDSolver对象就被动态创建出来了。在Solver基类的构造函数中,调用了成员函数Init()实现初始化:
- // Solver类构造函数
- template <typename Dtype>
- Solver<Dtype>::Solver(const SolverParameter& param, const Solver* root_solver)
- : net_(), callbacks_(), root_solver_(root_solver),
- requested_early_exit_(false)
- {
- Init(param);
- }
- }
- template <typename Dtype>
- void Solver<Dtype>::Init(const SolverParameter& param)
- {
- ...
- // 初始化训练网络
- InitTrainNet();
- // 初始化测试网络
- InitTestNet();
- // 迭代次数清零
- iter_ = 0;
- }
构建网络的代码便藏身在成员函数InitTrainNet()中,我们继续往内部追踪,
- template <typename Dtype>
- void Solver<Dtype>::InitTrainNet()
- {
- ...
- // 从文件读取网络参数
- NetParameter net_param;
- ReadNetParamsFromTextFileOrDie(param_.net(), &net_param);
- // 构造网络
- net_.reset(new Net<Dtype>(net_param));
- }
最后一行语句动态创建Net对象,并构造了智能指针对象net_。锲而不舍,继续追踪Net类的构造函数,
- template <typename Dtype>
- Net<Dtype>::Net(const NetParameter& param, const Net* root_net)
- : root_net_(root_net) {
- Init(param);
- }
内幕马上就要揭晓了,真相就隐藏在Net::Init()成员函数中,
- // Initialize a network with a NetParameter
- template <typename Dtype>
- void Net<Dtype>::Init(const NetParameter& in_param)
- {
- NetParameter filtered_param;
- // 过滤掉PHASE为TEST的layer
- FilterNet(in_param, &filtered_param);
- // 建立网络的bottom_blob数组和top_blob数组
- bottom_vecs_.resize(param.layer_size());
- top_vecs_.resize(param.layer_size());
- // 对余下的layer进行遍历
- for (int layer_id = 0; layer_id < param.layer_size(); ++layer_id)
- {
- // 创建layer
- layers_.push_back(LayerRegistry<Dtype>::CreateLayer(layer_param));
- // 网络中添加新的bottom blob
- AppendBottom(param, layer_id, bottom_id, &available_blobs, &blob_name_to_idx);
- // 网络中添加新的top blob
- AppendTop(param, layer_id, top_id, &available_blobs, &blob_name_to_idx);
- // 构建网络
- layers_[layer_id]->SetUp(bottom_vecs_[layer_id], top_vecs_[layer_id]);
- // 将可学习权重的blob添加到learnable_params_中,用于在Net::Update()时遍历更新使用
- AppendParam(const NetParameter& param, const int layer_id, const int param_id);
- // 为需要反向传导的层设置标识(除了数据层,其它层都需要反向传导)
- layer_need_backward_.push_back(need_backward);
- }
- // 网络初始化完成
- LOG_IF(INFO, Caffe::root_solver()) << "Network initialization done.";
- }
我们发现这里关键在于layer的构造。lenet_train_test.prototxt文件中共定义了11个layer,而用于构建网络的是9个(除去了一个用于测试数据的layer和统计计算精度的Accuracy layer)。在对这9个layer进行遍历时,首先构造其底部的Blob,再构造顶部Blob,然后调用Layer::SetUp()函数进行初始化和Reshape操作。Layer::SetUp()函数的实现细节为下面的这段代码。
- // Calls LayerSetUp to do special layer setup for individual layer types,
- // followed by Reshape to set up sizes of top blobs and internal buffers
- template <typename Dtype>
- void Layer<Dtype>::SetUp(const vector<Blob<Dtype>*>& bottom, const vector<Blob<Dtype>*>& top)
- {
- // 初始化互斥量
- InitMutex();
- CheckBlobCounts(bottom, top);
- // 调用虚函数,逐层进行配置
- LayerSetUp(bottom, top);
- Reshape(bottom, top);
- // 设置损失权重
- SetLossWeights(top);
- }
至此,我们的网络构建就大功告成了!
3.构建完成的网络模型
让我们来看一下构建好的网络模型是什么样子,11个layer示意图如下所示
Blob关系图:
最后附上一张Layer类类图:
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:Caffe框架源码剖析(1)—构建网络 - Python技术站