开源项目名称:Caffe—— deep learning framework
语言:C++
时间:10月22日(周六)早11:00-12:00
参与方式:源码分析微信群内直播链接
主讲人:寒小阳-资深深度学习工程师
我们将直接进行caffe代码结构和设计分析,直播地址将在直播当天(周六)上午提前公布。
欢迎大家阅读正文先行了解项目。
目前的图像和自然语言处理很多地方用到了神经网络/深度学习相关的知识,神奇的效果让广大身处IT一线的程序猿GG们跃跃欲试,不过看到深度学习相关一大串公式之后头皮发麻,又大有放弃的想法。
从工业使用的角度来说,不打算做最前沿的研究,只是用已有的方法或者成型的框架来完成一些任务,也不用一上来就死啃理论,倒不如先把神经网络看得简单一点,视作一个搭积木的过程,所谓的卷积神经网络(CNN)或者循环神经网络(RNN)等无非是积木块不一样(层次和功能不同)以及搭建的方式不一样,再者会有一套完整的理论帮助我们把搭建的积木模型最好地和需要完成的任务匹配上。
大量的数学背景知识可能大家都忘了,但是每天敲代码的习惯并没有落下,所以说不定以优秀的深度学习开源框架代码学习入手,也是一个很好的神经网络学习切入点。
这里给大家整理和分享的是使用非常广泛的深度学习框架caffe,这是一套最早起源于Berkeley的深度学习框架,广泛应用于神经网络的任务当中,大量paper的实验都是用它完成的,而国内电商等互联网公司的大量计算机视觉应用也是基于它完成的。代码结构清晰,适合学习。
典型的神经网络是层次结构,每一层会完成不同的运算(可以简单理解为有不同的功能),运算的层叠完成前向传播运算,“比对标准答案”之后得到“差距(loss)”,还需要通过反向传播来求得修正“积木块结构(参数)”所需的组件,继而完成参数调整。
所以caffe也定义了环环相扣的类,来更好地完成上述的过程。我们看到这里一定涉及数据,网络层,网络结构,最优化网络几个部分,在caffe中同样是这样一个想法,caffe的源码目录结构如下。
在很多地方都可以看到介绍说caffe种贯穿始终的是Blob,Layer,Net,Solver这几个大类。这四个大类分别负责数据传输、网络层次、网络骨架与参数求解策略,呈现一个自下而上,环环相扣的状态。在源码中可以找到对应这些名称的实现,详细说来,这4个部分分别负责:
在对整体的结构有一个大致的印象以后,就可以开始阅读源码了,一个参考的阅读顺序大概是:
Step 1. caffe.proto:对应目录 caffe-master\src\caffe\proto\caffe.proto
Step 2. Hpp文件:包括
上面d,c,b,a这4个部分实际上是自底向上的结构。
Step 3. Cpp/cu文件:对应上面提到的blob、net、solver的具体实现,所以你会看到blob.cpp,net.cpp,solver.cpp,但是注意,没有layer.cpp,而是可以看到\src\caffe\layers下有派生出的各种对应各种神经网络层的类,比如\src\caffe\layers\data_layer.cpp, conv_layer.cpp, relu_layer.cpp, pooling_layer.cpp, inner_product_layer.cpp等。(通常说来,caffe框架已经实现了很通用的网络结构,如果有自己的需求,添加一些新的层次即可)
Step 4. tools文件caffe提供的工具,目录在caffe-master\tools,例如计算图像均值,调优网络,可视化等。
caffe.proto是建议第一个阅读的部分,它位于…\src\caffe\proto目录下。首先要说明的是Google Protocol Buffer(简称 Protobuf) 是Google 公司内部的混合语言数据标准,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。用来做数据存储或 RPC 数据交换格式。caffe.proto运行后会生成caffe.pb.cc和caffe.pb.h两个文件,包含了很多结构化数据。
caffe.proto的一个message定义了一个需要传输的参数结构体,Package caffe可以把caffe.proto里面的所有文件打包存在caffe类里面。大致的代码框架如下:
一个message定义了一个需要传输的参数结构体,Required是必须有值的, optional是可选项,repeated表示后面单元为相同类型的一组向量。比如:
Caffe.proto每个message在编译后都会自动生成一些函数,大概是这样一个命名规范:Set_+field 设定值的函数命名,has_ 检查field是否已经被设置, clear_用于清理field,mutable_用于设置string的值,_size用于获取 重复的个数。
大概有这么些Message类别:
2.3.2 Blob
前面说到了Blob是最基础的数据结构,用来保存网络传输 过程中产生的数据和学习到的一些参数。比如它的上一层Layer中会用下面的形式表示学习到的参数:vector<shared_ptr<Blob<Dtype> > > blobs_;
里面的blob就是这里定义的类。 部分代码如下:
其中template <typename Dtype>
表示函数模板,Dtype可以表示int,double等数据类型。Blob是四维连续数组(4-D contiguous array, type = float32), 如果使用(n, k, h, w)表示的话,那么每一维的意思分别是:
实际Blob在(n, k, h, w)位置的值物理位置为((n * K + k) * H + h) * W + w。
Blob内部有两个字段data和diff。Data表示流动数据(输出数据),而diff则存储BP的梯度。
关于blob引入的头文件可以参考下面说明做理解: #include “caffe/common.hpp”单例化caffe类,并且封装了boost和cuda随 机数生成的函数,提供了统一接口。 #include “caffe/proto/caffe.pb.h”上一节提到的头文件 #include “caffe/syncedmem.hpp”主要是分配内存和释放内存的。而class SyncedMemory定义了内存分配管理和CPU与GPU之间同步的函数。Blob会使用SyncedMem自动决定什么时候去copy data以提高运行效率,通常情况是仅当gnu或cpu修改后有copy操作。 #include “caffe/util/math_functions.hpp”封装了很多cblas矩阵运算,基本是矩阵和向量的处理函数。
关于Blob里定义的函数的简单说明如下:
Reshape()
可以改变一个blob的大小;
ReshapeLike()
为data和diff重新分配一块空间,大小和另一个blob的一样;
Num_axes()
返回的是blob的大小;
Count()
计算得到count=numchannelsheight*width。
Offset()
可得到输入blob数据(n,c,h,w)的偏移量位置;
CopyFrom()
从source拷贝数据,copy_diff来作为标志区分是拷贝data还是 diff。
FromProto()
从proto读数据进来,其实就是反序列化。
ToProto()
把blob数据保存到proto中。 ShareDate()/ShareDiff()从other的blob复制data和diff的值;
Layer是网络的基本单元(“积木”),由此派生出了各种层类。如果做数据特征表达相关的研究,需要修改这部分。Layer类派生出来的层类通过这 实现两个虚函数Forward()
和Backward()
,产生了各种功能的 层类。Forward是从根据bottom计算top的过程,Backward则刚好相反。 在网路结构定义文件(*.proto)中每一层的参数bottom和top数目 就决定了vector中元素数目。
一起来看看Layer.hpp
Layer中三个重要参数:
LayerParameter layer_param_;
这个是protobuf文件中存储的layer参数。
vector<share_ptr<Blob<Dtype>>> blobs_;
这个存储的是layer学习到的参数。
vector<bool> param_propagate_down_;
这个bool表示是否计算各个 blob参数的diff,即传播误差。
包含了一些基本函数:
Layer()
尝试从protobuf读取参数; SetUp()
根据实际的参数设置进行实现,对各种类型的参数初始化;
Forward()
和Backward()对应前向计算和反向更新,输入统一都是 bottom,输出为top,其中Backward里面有个propagate_down参数, 用来表示该Layer是否反向传播参数。
Caffe::mode()
具体选择使用CPU或GPU操作。
Net是网络的搭建部分,将Layer所派生出层类组合成网络。 Net用容器的形式将多个Layer有序地放在一起,它自己的基本功能主要 是对逐层Layer进行初始化,以及提供Update( )的接口用于更新网络参数, 本身不能对参数进行有效地学习过程。
Net也有它自己的Forward()
和Backward()
,他们是对整个网络的前向和反向传导,调用可以计算出网络的loss。 Net由一系列的Layer组成(无回路有向图DAG),Layer之间的连接由一个文本文件描述。模型初始化Net::Init()会产生blob和layer并调用Layer::SetUp。 在此过程中Net会报告初始化进程。这里的初始化与设备无关,在初始化之后通过Caffe::set_mode()设置Caffe::mode()来选择运行平台CPU或 GPU,结果是相同的。
里面比较重要的函数的简单说明如下:
Init()
初始化函数,用于创建blobs和layers,用于调用layers的setup函数来初始化layers。
ForwardPrefilled()
用于前馈预先填满,即预先进行一次前馈。
Forward()
把网络输入层的blob读到net_input_blobs_,然后进行前馈,计 算出loss。Forward的重载,只是输入层的blob以string的格式传入。
Backward()
对整个网络进行反向传播。
Reshape()
用于改变每层的尺寸。 Update()更新params_中blob的值。
ShareTrainedLayersWith(Net* other)
从Other网络复制某些层。
CopyTrainedLayersFrom()
调用FromProto函数把源层的blob赋给目标 层的blob。
ToProto()
把网络的参数存入prototxt中。
bottom_vecs_存每一层的输入blob指针
bottom_id_vecs_存每一层输入(bottom)的id
top_vecs_存每一层输出(top)的blob
params_lr()
和params_weight_decay()
学习速率和权重衰减;
blob_by_name()
判断是否存在名字为blob_name的blob;
FilterNet()
给定当前phase/level/stage,移除指定层。
StateMeetsRule()
中net的state是否满足NetStaterule。
AppendTop()
在网络中附加新的输入或top的blob。
AppendBottom()
在网络中附加新的输入或bottom的blob。
AppendParam()
在网络中附加新的参数blob。
GetLearningRateAndWeightDecay()
收集学习速率和权重衰减,即更 新params_、params_lr_和params_weight_decay_ ;
更多细节可以参考这篇博客
Solver是Net的求解部分,研究深度学习求解与最优化的同学会修改这部分内容。Solver类中包含一个Net指针,主要实现了训练模型参数所采用的优化算法,它所派生的类完成对整个网络进行训练。
不同的模型训练方法通过重载函数ComputeUpdateValue( )
实现计算update参数的核心功能。 最后当进行整个网络训练过程(即运行Caffe训练模型)的时 候,会运行caffe.cpp中的train( )函数,而这个train函数实际上是实 例化一个Solver对象,初始化后调用了Solver中的Solve( )方法。
头文件主要包括solver.hpp、sgd_solvers.hpp、solver_factory.hpp
包含的主要函数和介绍如下:
Solver()
构造函数,初始化net和test_net两个net类,并调用init函数初始化网络,解释详见官方文档页; Solve()训练网络有如下步骤:
对于第7步,训练时的迭代,有如下的过程:
关于Test() 测试网络。有如下过程:
基本函数的一个简易介绍如下:
Snapshot()
输出当前网络状态到一个文件中;
Restore()
从一个文件中读入网络状态,并可以从那个状态恢复;
GetLearningRate()
得到学习率; PreSolve()提前训练,详见网页链接;
ComputeUpdateValue()
用随机梯度下降法计算更新值;
未完待续…
我们希望营造一起
阅读代码、学习开源项目、共同进步
的氛围。