Caffe源码整体结构及介绍

1. Caffe代码层次

Caffe中的源码按照模块可以大体划分为:Blob,Layer,Net,Solver这样的几大类。这四个类复杂性从低到高,贯穿了整个Caffe。把它们分为四个层次介绍。
1)Blob:作为数据传输的媒介,无论是网络权重参数,还是输入数据,都是转化为Blob数据结构来存储;
2)Layer:作为网络的基础单元,神经网络中层与层间的数据节点、前后传递都在该数据结构中被实现,层类种类丰富,比如常用的卷积层、全连接层、pooling层等等,大大地增加了网络的多样性;
3)Net:作为网络的整体骨架,决定了网络中的层次数目以及各个层的类别等信息;
4)Solver:作为网络的求解策略,涉及到求解优化问题的策略选择以及参数确定方面,修改这个模块的话一般都会是研究DL的优化求解的方向。

2. Blob

2.1 Blob的类型描述

Caffe内部采用的数据类型主要是对protocol buffer所定义的数据结构的继承,因此可以在尽可能小的内存占用下获得很高的效率,虽然追求性能的同时Caffe也会牺牲了一些代码可读性。
直观来说,可以把Blob看成一个有4维( [ n u m , c h a n n e l s , h e i g h t , w i d t h ] [num,channels,height,width] [num,channels,height,width])的结构体(包含数据和梯度),而实际上,它们只是一维的指针而已,其4维结构通过shape属性得以计算出来。

2.2 Blob的重要成员函数和变量

shared_ptr<SyncedMemory> data_ //数据
shared_ptr<SyncedMemory> diff_ //梯度

存储的数据与中间梯度。

void Blob<Dtype>::Reshape(const int num, const int channels, const int height,
const int width)

重新修改Blob的形状(4维),并根据形状来申请动态内存存储数据和梯度。

inline int count(int start_axis, int end_axis) const

计算Blob所需要的基本数据单元的数量。

inline Dtype diff_at(const int n, const int c, const int h,
      const int w) const {
    return cpu_diff()[offset(n, c, h, w)];
  }

通过坐标索引对应位置处的数据。

//read only
const Dtype* cpu_data() const;t;
const Dtype* gpu_data() const;
const Dtype* cpu_diff() const;
const Dtype* gpu_diff() const;
//can be write
Dtype* mutable_cpu_data();
Dtype* mutable_gpu_data();
Dtype* mutable_cpu_diff();
Dtype* mutable_gpu_diff();

不同读写权限的blob数据访问。

在更高一级的Layer中Blob用下面的形式表示学习到的参数:

vector<shared_ptr<Blob<Dtype> > > blobs_;

这里使用的是一个Blob的容器是因为某些Layer包含多组学习参数,比如多个卷积核的卷积层。

以及Layer所传递的数据形式,后面还会涉及到这里:

vector<Blob<Dtype>*> &bottom;
vector<Blob<Dtype>*> *top

3. Layer

3.1 5大Layer派生类型

Caffe十分强调网络的层次性,也就是说卷积操作,非线性变换(ReLU等),Pooling,权值连接等全部都由某一种Layer来表示。具体来说分为5大类Layer
1)NeuronLayer类,定义于neuron_layers.hpp中,其派生类主要是元素级别的运算(比如Dropout运算,激活函数ReLu,Sigmoid等),运算均为同址计算(in-place computation,返回值覆盖原值而占用新的内存)。
2)LossLayer类,定义于loss_layers.hpp中,其派生类会产生loss,只有这些层能够产生loss。
3)数据层,定义于data_layer.hpp中,作为网络的最底层,主要实现数据格式的转换。
4)特征表达层,我自己分的类)定义于vision_layers.hpp(为什么叫vision这个名字,我目前还不清楚),实现特征表达功能,更具体地说包含卷积操作,Pooling操作,他们基本都会产生新的内存占用(Pooling相对较小)。
5)网络连接层和激活函数(我自己分的类)定义于common_layers.hpp,Caffe提供了单个层与多个层的连接,并在这个头文件中声明。这里还包括了常用的全连接层InnerProductLayer类。

3.2 Layer的创建和初始化

3.2.1 Layer创建方式1

Layer的创建使用的是工厂模式,这里使用了宏函数:REGISTER_LAYER_CREATOR负责将创建层的函数放入LayerRegistry。

template <typename Dtype>
class LayerRegisterer {
 public:
  LayerRegisterer(const string& type,
                  shared_ptr<Layer<Dtype> > (*creator)(const LayerParameter&)) {
    // LOG(INFO) << "Registering layer type: " << type;
    LayerRegistry<Dtype>::AddCreator(type, creator);
  }
};


#define REGISTER_LAYER_CREATOR(type, creator)                                  \
  static LayerRegisterer<float> g_creator_f_##type(#type, creator<float>);     \
  static LayerRegisterer<double> g_creator_d_##type(#type, creator<double>)    \

大多数层创建的函数的生成宏REGISTER_LAYER_CLASS,可以看到宏函数比较简单的,将类型作为函数名称的一部分,这样就可以产生出一个创建函数,并将创建函数放入LayerRegistry。

#define REGISTER_LAYER_CLASS(type)                                             \
  template <typename Dtype>                                                    \
  shared_ptr<Layer<Dtype> > Creator_##type##Layer(const LayerParameter& param) \
  {                                                                            \
    return shared_ptr<Layer<Dtype> >(new type##Layer<Dtype>(param));           \
  }                                                                            \
  REGISTER_LAYER_CREATOR(type, Creator_##type##Layer)

上面的宏函数会在每个层的CPP文件的末尾调用,比如elu_layer

INSTANTIATE_CLASS(ELULayer);
REGISTER_LAYER_CLASS(ELU);

3.2.1 Layer创建方式2

上面介绍到的是第一种不适用CUDNN的创建方法,但是一些使用了CUDNN的Layer就不是这样的方式了,具体在layer_factory.cpp中使用工厂模型实现的。比如这里作为例子的卷积层的实现。
在这里插入图片描述
这样两种类型的Layer的创建函数都有了对应的声明。这里直接说明除了有CUDNN实现的层,其他层都是采用第一种方式实现的创建函数,而带有CUDNN实现的层都采用的第二种方式实现的创建函数。

3.2.3 Layer的初始化

关键函数Setup此函数在进行网络初始化,在函数NetInit中被调用的。

  void SetUp(const vector<Blob<Dtype>*>& bottom,
      const vector<Blob<Dtype>*>& top) {
    CheckBlobCounts(bottom, top);
    LayerSetUp(bottom, top);
    Reshape(bottom, top);
    SetLossWeights(top);
  }

这样整个Layer初始化的过程中,CheckBlobCounts被最先调用,然后接下来是LayerSetUp,后面才是Reshape,最后才是SetLossWeights。这样Layer初始化的生命周期大家就有了了解。

3.3 Layer的重要成员函数

在Layer内部,数据主要有两种传递方式,正向传导(Forward)和反向传导(Backward)。Forward和Backward有CPU和GPU(部分有)两种实现。Caffe中所有的Layer都要用这两种方法传递数据。

virtual void Forward(const vector<Blob<Dtype>*> &bottom,                     
                     vector<Blob<Dtype>*> *top) = 0;
virtual void Backward(const vector<Blob<Dtype>*> &top,
                      const vector<bool> &propagate_down, 
                      vector<Blob<Dtype>*> *bottom) = 0;

Layer类派生出来的层类通过这实现这两个虚函数,产生了各式各样功能的层类。Forward是从根据bottom计算top的过程,Backward则相反(根据top计算bottom)。注意这里为什么用了一个包含Blob的容器(vector),对于大多数Layer来说输入和输出都各连接只有一个Layer,然而对于某些Layer存在一对多的情况,比如LossLayer和某些连接层。在网路结构定义文件(*.proto)中每一层的参数bottom和top数目就决定了vector中元素数目。

layers {
  bottom: "decode1neuron"   // 该层底下连接的第一个Layer
  bottom: "flatdata"        // 该层底下连接的第二个Layer
  top: "l2_error"           // 该层顶上连接的一个Layer
  name: "loss"              // 该层的名字
  type: EUCLIDEAN_LOSS      // 该层的类型
  loss_weight: 0
}

3.4 Layer的重要成员变量

1) loss

 vector<Dtype> loss_;

每一层又有一个loss_值,只不多大多数Layer都是0,只有LossLayer才可能产生非0的loss_。计算loss是会把所有层的loss_相加。
2)learnable parameters

vector<shared_ptr<Blob<Dtype> > > blobs_;

前面提到过的,Layer学习到的参数。

4. Net

Net用容器的形式将多个Layer有序地放在一起,其自身实现的功能主要是对逐层Layer进行初始化,以及提供Update( )的接口(更新网络参数),本身不能对参数进行有效地学习过程。

vector<shared_ptr<Layer<Dtype> > > layers_;

同样Net也有它自己的

vector<Blob<Dtype>*>& Forward(const vector<Blob<Dtype>* > & bottom,
                              Dtype* loss = NULL);
void Net<Dtype>::Backward();

它们是对整个网络的前向和方向传导,各调用一次就可以计算出网络的loss了。

5. Solver

这个类中包含一个Net的指针,主要是实现了训练模型参数所采用的优化算法,它所派生的类就可以对整个网络进行训练了。

shared_ptr<Net<Dtype> > net_;

不同的模型训练方法通过重载函数ComputeUpdateValue( )实现计算update参数的核心功能

virtual void ComputeUpdateValue() = 0;

最后当进行整个网络训练过程(也就是你运行Caffe训练某个模型)的时候,实际上是在运行caffe.cpp中的train( )函数,而这个函数实际上是实例化一个Solver对象,初始化后调用了Solver中的Solve( )方法。而这个Solve( )函数主要就是在迭代运行下面这两个函数,就是刚才介绍的哪几个函数。

ComputeUpdateValue();
net_->Update();

6. 总结

至此,从底层到顶层对Caffe的主要结构都应该有了大致的概念。但是,在其中Solver、Net它们是怎么在训练网络的时候运作起来的,其中的基理这篇博客中并未讲解到,具体的运作流程再后序的博客中进行剖析。

7. 参考

  1. caffe源码阅读(1)_整体框架和简介(摘录)
  2. 深度学习框架Caffe源码解析
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值