【2】深度学习之排头大兵:Caffe

Caffe: C++

作者:Gein Chen
链接: https://www.zhihu.com/question/27982282/answer/80242005
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

楼上的大神回答的都很好了,非常感谢。这里我想说一下我自己学习caffe的方式,限于时间篇幅,并不想深入到具体的实现细节,只是从大的方向上谈谈,因为讲清楚一个细节都是一篇博客的篇幅了。

1.学习程序的第一步,先让程序跑起来,看看结果,这样就会有直观的感受。
Caffe的官网上 Caffe | Deep Learning Framework 提供了很多的examples,你可以很容易地开始训练一些已有的经典模型,如LeNet。我建议先从 LeNet MNIST Tutorial开始,因为数据集很小,网络也很小但很经典,用很少的时间就可以跑起来了。当你看到terminal刷拉拉的一行行输出,看到不断减少的loss和不断上升的accuracy,训练结束你得到了99+%的准确率,感觉好厉害的样子。你可以多跑跑几个例子,熟悉一下环境和接口。

2.单步调试,跟着Caffe在网络里流动
当玩了几天之后,你对Caffe的接口有点熟悉了,对已有的例子也玩腻了,你开始想看看具体是怎么实现的了。我觉得最好的方法是通过单步调试的方式跟着程序一步一步的在网络里前向传播,然后再被当成误差信息传回来。

Caffe就像一个你平常编程中Project,你可以使用IDE或者GDB去调试它,这里我们不细说调试的过程。你可以先跟踪前向传播的过程,无非就是从高层次到低层次的调用Forward函数, Solver->Net->Layer->Specific Layer (Convolution等...).后向传播也类似,但因为你对Caffe里面的各种变量运算不熟悉,当你跟踪完前向传播时可能已经头晕眼花了,还是休息一下,消化一下整个前向传播的流程。

刚刚开始你没有必要对每个Layer的计算细节都那么较真,大概知道程序的运算流程就好,这样你才可以比较快的对Caffe有个大体的把握。

3.个性化定制Caffe
到这里,你已经可以说自己有用过Caffe了,但是还不能算入门,因为你还不知道怎么修改源码,满足自己特定的需求。我们很多时候都需要自己定义新的层来完成特定的运算,这时你需要在Caffe里添加新的层。

你一开肯定无从下手,脑子一片空白。幸运的是Caffe github上的Wiki Development · BVLC/caffe Wiki · GitHub已经有了教程了,而且这是最接近latest Caffe的源码结构的教程,你在网上搜到的Blog很多是有点过时的,因为Caffe最近又重构了代码。你可以跟着它的指导去添加自己的层。

虽然你已经知道要在哪里添加自己的东西了,但你遇到最核心的问题是如何写下面这四个函数。
  • forward_cpu()
  • forward_gpu()
  • backward_cpu()
  • backward_gpu()
你可以先模仿已有的层去实现这四个函数,而且我相信forward函数很快就可以写出来了,但backward的还是一头雾水。 这时我们就要补补神经网络里最核心的内容了——Backpropagation.

4.理解并实现Backpropagation
这个我觉得是与平台无关的,不管你是使用Caffe、Torch 7,还是Theano,你都需要深刻理解并掌握的。因为我比较笨,花了好长时间才能够适应推导中的各种符号。其实也不难,就是误差顺着Chain rule法则流回到前面的层。我不打算自己推导后向传播的过程,因为我知道我没有办法将它表达得很好,而且网上已经有很多非常好的教程了。下面是我觉得比较好的学习步骤吧。
  • 从浅层的神经网络(所谓的全连接层)的后向传播开始,因为这个比较简单,而且现在我们常说的CNN和LSTM的梯度计算也最终会回归到这里。
    • 第一个必看的是Ng深入浅出的Ufldl教程UFLDL Tutorial,还有中文版的,这对不喜欢看英语的同学是个好消息。当然你看一遍不理解,再看一遍,忘了,再看,读个几遍你才会对推导过程和数学符号熟悉。我头脑不大行,来来回回看了好多次。
    • 当然,Ufldl的教程有点短,我还发现了一个讲得更细腻清晰的教程, Michael Nielsen写的Neural networks and deep learning。它讲得实在太好了,以至于把我的任督二脉打通了。在Ufldl的基础上读这个,你应该可以很快掌握全连接层的反向传播。
    • 最后在拿出standford大牛karpathy的一篇博客Hacker's guide to Neural Networks,这里用了具体的编程例子手把手教你算梯度,并不是推导后向传播公式的,是关于通用梯度计算的。用心去体会一下。
  • 这时你跃跃欲试,回去查看Caffe源码里Convolution层的实现,但发现自己好像没看懂。虽说卷积层和全连接层的推导大同小异,但思维上还是有个gap的。我建议你先去看看Caffe如何实现卷积的,Caffe作者贾扬清大牛在知乎上的回答在 Caffe 中如何计算卷积?让我茅塞顿开。重点理解im2col和col2im.
  • 这时你知道了Convolution的前向传播,还差一点就可以弄明白后向传播怎么实现了。我建议你死磕Caffe中Convolution层的计算过程,把每一步都搞清楚,经过痛苦的过程之后你会对反向传播有了新的体会的。在这之后,你应该有能力添加自己的层了。再补充一个完整的添加新的层的教程Making a Caffe Layer • Computer Vision Enthusiast。这篇教程从头开始实现了一个Angle To Sine Cosine Layer,包含了梯度推导,前向与后向传播的CPU和GPU函数,非常棒的一个教程。
  • 最后,建议学习一下基本的GPU Cuda编程,虽然Caffe中已经把Cuda函数封装起来了,用起来很方便,但有时还是需要使用kernel函数等Cuda接口的函数。这里有一个入门的视频教程,讲得挺不错的NVIDIA CUDA初级教程视频

第二个回答,讲得还是一样的乱,谢谢观看。



作者:北川谦一
链接: https://www.zhihu.com/question/27982282/answer/39350629
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

我是从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<Blob<Dtype>*> &bottom;
vector<Blob<Dtype>*> *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
vector<Dtype> 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( )的接口(更新网络参数),本身不能对参数进行有效地学习过程。
vector<shared_ptr<Layer<Dtype> > > layers_;
同样Net也有它自己的
vector<Blob<Dtype>*>& Forward(const vector<Blob<Dtype>* > & bottom,
                              Dtype* loss = NULL);
void Net<Dtype>::Backward();
他们是对整个网络的前向和方向传导,各调用一次就可以计算出网络的loss了。
2.4. Solver
这个类中包含一个Net的指针,主要是实现了训练模型参数所采用的优化算法,它所派生的类就可以对整个网络进行训练了。
shared_ptr<Net<Dtype> > 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++项目就看到这么好的代码,实在是受益匪浅,在这里也感谢作者贾扬清等人的贡献。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值