我是从Torch7转移到Caffe的人,仅供参考,当你阅读前你应该具备一些有关DL的基础知识,本文集中写Caffe代码结构而非介绍DL知识。
我是去年底开始看Caffe代码的,看代码的时间加在一起也不到一个月,也算半个新手,我的回答是从新手角度作一个入门阶段的经验分享。
本文只涉及Caffe结构的相关问题,不涉及具体实现技巧等细节。
==============================================================
1. 初识Caffe
1.1. Caffe相对与其他DL框架的优点和缺点:
优点:
- 速度快。Google Protocol Buffer数据标准为Caffe提升了效率。
- 学术论文采用此模型较多。不确定是不是最多,但接触到的不少论文都与Caffe有关(R-CNN,DSN,最近还有人用Caffe实现LSTM)
缺点:
- 曾更新过重要函数接口。有人反映,偶尔会出现接口变换的情况,自己很久前写的代码可能过了一段时间就不能和新版本很好地兼容了。(现在更新速度放缓,接口逐步趋于稳定,感谢 评论区王峰的建议)
- 对于某些研究方向来说的人并不适合。这个需要对Caffe的结构有一定了解,(后面提到)。
1.2. Caffe代码层次。
回答里面有人说熟悉Blob,Layer,Net,Solver这样的几大类,我比较赞同。我基本是从这个顺序开始学习的,这四个类复杂性从低到高,贯穿了整个Caffe。把他们分为三个层次介绍。
- Blob:是基础的数据结构,是用来保存学习到的参数以及网络传输过程中产生数据的类。
- Layer:是网络的基本单元,由此派生出了各种层类。修改这部分的人主要是研究特征表达方向的。
- Net:是网络的搭建,将Layer所派生出层类组合成网络。Solver:是Net的求解,修改这部分人主要会是研究DL求解方向的。
==============================================================
2. Caffe进阶
2.1. Blob:
Caffe支持CUDA,在数据级别上也做了一些优化,这部分最重要的是知道它主要是对protocol buffer所定义的数据结构的继承,Caffe也因此可以在尽可能小的内存占用下获得很高的效率。(追求性能的同时Caffe也牺牲了一些代码可读性)
在更高一级的Layer中Blob用下面的形式表示学习到的参数:
vector<shared_ptr<Blob<Dtype> > > blobs_;
这里使用的是一个Blob的容器是因为某些Layer包含多组学习参数,比如多个卷积核的卷积层。
以及Layer所传递的数据形式,后面还会涉及到这里:
vector*> ⊥
vector*> *top
2.2. Layer:
2.2.1. 5大Layer派生类型
Caffe十分强调网络的层次性,也就是说卷积操作,非线性变换(ReLU等),Pooling,权值连接等全部都由某一种Layer来表示。具体来说分为5大类Layer
- NeuronLayer类 定义于neuron_layers.hpp中,其派生类主要是元素级别的运算(比如Dropout运算,激活函数ReLu,Sigmoid等),运算均为同址计算(in-place computation,返回值覆盖原值而占用新的内存)。
- LossLayer类 定义于loss_layers.hpp中,其派生类会产生loss,只有这些层能够产生loss。
- 数据层 定义于data_layer.hpp中,作为网络的最底层,主要实现数据格式的转换。
- 特征表达层(我自己分的类)定义于vision_layers.hpp(为什么叫vision这个名字,我目前还不清楚),实现特征表达功能,更具体地说包含卷积操作,Pooling操作,他们基本都会产生新的内存占用(Pooling相对较小)。
- 网络连接层和激活函数(我自己分的类)定义于common_layers.hpp,Caffe提供了单个层与多个层的连接,并在这个头文件中声明。这里还包括了常用的全连接层InnerProductLayer类。
2.2.2. 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
}
2.2.3. Layer的重要成员变量
loss
每一层又有一个loss_值,只不多大多数Layer都是0,只有LossLayer才可能产生非0的loss_。计算loss是会把所有层的loss_相加。
learnable parameters
vector<shared_ptr<Blob<Dtype> > > blobs_;
前面提到过的,Layer学习到的参数。
2.3. Net:
Net用容器的形式将多个Layer有序地放在一起,其自身实现的功能主要是对逐层Layer进行初始化,以及提供Update( )的接口(更新网络参数),本身不能对参数进行有效地学习过程。
同样Net也有它自己的
vector<Blob<Dtype>*>& Forward(const vector<Blob<Dtype>* > & bottom,
Dtype* loss = NULL);
void Net<Dtype>::Backward();
他们是对整个网络的前向和方向传导,各调用一次就可以计算出网络的loss了。
2.4. Solver
这个类中包含一个Net的指针,主要是实现了训练模型参数所采用的优化算法,它所派生的类就可以对整个网络进行训练了。
不同的模型训练方法通过重载函数ComputeUpdateValue( )实现计算update参数的核心功能
virtual void ComputeUpdateValue() = 0;
最后当进行整个网络训练过程(也就是你运行Caffe训练某个模型)的时候,实际上是在运行caffe.cpp中的train( )函数,而这个函数实际上是实例化一个Solver对象,初始化后调用了Solver中的Solve( )方法。而这个Solve( )函数主要就是在迭代运行下面这两个函数,就是刚才介绍的哪几个函数。
ComputeUpdateValue();
net_->Update();
==============================================================
至此,从底层到顶层对Caffe的主要结构都应该有了大致的概念。为了集中重点介绍Caffe的代码结构,文中略去了大量Caffe相关的实现细节和技巧,比如Layer和Net的参数如何初始化,proto文件的定义,基于cblas的卷积等操作的实现(cblas实现卷积这一点我的个人主页 GanYuFei
中的《Caffe学习笔记5-BLAS与boost::thread加速》有介绍)等等就不一一列举了。
整体来看Layer部分代码最多,也反映出Caffe比较重视丰富网络单元的类型,然而由于Caffe的代码结构高度层次化,使得某些研究以及应用(比如研究类似非逐层连接的神经网络这种复杂的网络连接方式)难以在该平台实现。这也就是一开始说的一个不足。
另外,Caffe基本数据单元都用Blob,使得数据在内存中的存储变得十分高效,紧凑,从而提升了整体训练能力,而同时带来的问题是我们看见的一些可读性上的不便,比如forward的参数也是直接用Blob而不是设计一个新类以增强可读性。所以说性能的提升是以可读性为代价的。
最后一点也是最重要的一点,我从Caffe学到了很多。第一次看的C++项目就看到这么好的代码,实在是受益匪浅,在这里也感谢作者贾扬清等人的贡献。
甘宇飞更新于2/13/2015