Caffe的设计

Caffe遵循了神经网络的一个假设:所有的计算都是以layer形式表示的,layer的作用就是根据输入数据,输出一些计算以后的结果。以卷积为例,就是输入一副图像,然后与这一层的参数(filter)进行卷积运算,然后输出卷积的结果。每一个layer需要进行两种运算:(1)forward,从输入计算输出;(2)backward根据上面的梯度(gradient)来计算相对于输入的梯度。在每个layer都实现了这两个函数以后,我们可以将很多层连接称一个网络,这个网络做的事情就是输入我们的数据(图像或者语音或者whatever),然后来计算我们需要的输出(比如说识别的label)。在训练时,我们可以根据已有的label来计算loss和gradient,然后用gradient来update网络的参数。这个就是caffe的一个基本流程。

Caffe的主要结构

Caffe代码本身非常模块化,主要由4部分组成Blob, Layer,Net和Solver。

Blob

Blob主要用来表示网络中的数据,包括训练数据,网络各层自身的参数,网络之间传递的数据都是通过Blob来实现的,同时Blob数据也支持CPU与GPU上存储,能够在两者之间做同步。

Layer

layer是对神经网络中各种层的一个抽象,包括卷积层和下采样层,还有全连接层和各种激活函数层等等。同时每种Layer都实现了前向传播和反向传播,并通过Blob来传递数据。

Net

Net是对整个神经网络的表示,由各种Layer前后连接组合而成,也是我们要构建的网络模型。

Solver

Solver定义了针对Net网络模型的求解方法,记录神经网络的训练过程,保存神经网络模型参数,中断并恢复网络的训练过程。自定义Solver能够实现不同的神经网络求解方式。

Caffe整体架构

Caffe的架构与其他的深度学习框架稍微不同,它没有根据算法实现过程的方式来进行编码,而是以系统级的抽象作为整体架构,逐层的封装实现细节,使得上层的架构变得很清晰。Caffe的整体架构如下:

(1)SyncedMem

这个类的主要功能是封装CPU和GPU的数据交互操作。一般来说,数据的流动形式都是:硬盘->CPU内存->GPU内存->CPU内存->硬盘,所以在写代码的过程中经常会写CPU/GPU之间数据传输的代码,同时还要维护CPU和GPU两个处理端的内存指针。这些事情处理起来不会很难,但是会很繁琐。因此SyncedMem的出现就是把CPU/GPU的数据传输操作封装起来,只需要调用简单的接口就可以获得两个处理端同步后的数据。

(2)Blob

Blob是用于存储数据的对象,在Caffe中各种数据(图像输入、模型参数)都是以Blob的形式在网络中传输的,Blob提供统一的存储操作接口,可用来保存训练数据、模型参数等,同时Blob还能在CPU和GPU之间进行同步以支持CPU/GOU的混合运算。

这个类做了两个封装:一个是操作数据的封装,使用Blob可以操纵高维的数据,快速访问其中的数据,变换数据的维度等;另一个是对原始数据和更新量的风转,每一个Blob中都有data和diff两个数据指针,data用于存储原始数据,diff用于存储反向传播(Backpropagation)的梯度更新值。Blob使用了SyncedMem,这样便于访问不同的处理端。Blob基本实现了整个Caffe数据结构的封装,在Net类中可以看到所有的前后向数据和参数都用Blob来表述就足够了。数据的抽象到这个就可以了,接下来作层级的抽象。神经网络的前后向计算可以做到层与层之间完全独立,只要每个层按照一定的接口规则实现,就可以确保整个网络的正确性。

(3)Layer

Layer是网络Net的基本单元,也是Caffe中能在外部进行调整的最小网络结构单元,每个Layer都有输入Blob和输出Blob。Layer(层)是Caffe中最庞大最繁杂的模块,它是神经网络的基本计算单元。由于Caffe强调模块化设计,因此只允许每个layer完成一类特定的计算,例如convolution操作、pooling操作、非线性变换、内积运算,以及数据加载、归一化和损失计算等,也是以Layer为基础进行的。Layer是一个父类,它的下面还有各种实现特定功能的子类,例如data_layer, conv_layer, loss_layer等。Layer是通过LayFactory来创建的。

(4)Net

Net是一个完整的深度网络,包含输入层、隐藏层、输出层,在Caffe中一般是一个卷积神经网络(Convolution Neural Network, CNN)。通过定义不同类型的Layer,并用Blob将不同的Layer连接在起来,就能产生一个Net。Net将数据Blob和层Layer组合起来做进一步的封装,对外提供了初始化和前后传播的接口,使得整体看上去和一个层的功能类似,但内部的组合可以是多种多样的。值得一提的是,每一层的输入输出数据将统一保存在Net中,同时每个层内的参数指针也保存在Net中,不同的层可以通过WeightShare共享相同的参数,因此可以通过配置来实现多个神经网络层之间共享参数的功能。一个Net由多个Layer组成。一个典型的网络从data layer(从磁盘中载入数据)出发到loss layer结束。

(5)Solver

有了Net就可以进行神经网络的前后向传播计算了,但是还缺少神经网络的训练和预测功能,Solver类进一步封装了训练和预测相关的一些功能。它还提供了两个接口:一个是更新参数的接口,继承Solver可以实现不同的参数更新方法,如Momentum, Nesterov, Adagrad等,因此可以使用不同的优化算法。另一个接口是训练过程中每一轮特定状态下的可注入的一些回调函数,在代码中这个回调点的直接使用者就是多GPU训练算法。Solver定义了针对Net网络模型的求解方法,记录网络的训练过程,保存网络模型参数,中断并恢复网络的训练过程。自定义Solver能够实现不同的神经网络求解方式。阅读Solver的代码可以了解网络的求解优化过程。Solver是一个父类,它下面还有实现不同优化方法的子类,例如sgd_solver, adagrad_solver等,Solver是通过SolverFactory来创建的。

(6)Proto

caffe.proto位于.../src/caffe/proto目录下,在这个文件夹下还有一个.pb.cc和一个.pb.h文件,这两个文件都是由caffe.proto编译而来的。在caffe.proto中定义了很多结构化数据。

(7)IO

除了上面的东西之外,还需要输入数据和参数。DataReader和DtateTransformer帮助准备输入数据,Filler对参数进行初始化,一些Snapshot方法可以对模型进行持久化。