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

今天花了一整天时间进行阅读和调试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类类图:




  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值