caffe代码浅析

Caffe参数及存储方式

参数类型
● 可学习参数,即权重,其值由模型初始化参数、误差方向传播过程决定。存放在.caffemodel文件中。
● 结构参数,包括网络层数、层类型、卷积核数等,一旦设定好,训练阶段不能更改,注意:caffe中可以设置训练阶段结构参数和预测阶段结构参数不同。存放在train.prototxt和test.prototxt中。
● 训练超参数,包括训练次数等,预测阶段不需要该参数。存放在Solver.prototxt中

prototxt文本
protocol buffers用于序列化和检索数据结构。caffe使用prototxt文本存储输入输出参数描述文件。
protocol buffers的序列化数据及读取数据的步骤:
1. 在一个.proto文件中定义Message格式。
2. 使用protoc编译.proto文件,编译器会生成一个类,以便后续的计算单元中包含该类
3. 使用python/C++/java protocol buffer API 读写Message
caffe模型结构参数定义在prototxt文件中,prototxt文件的后缀是.prototxt。学习好的模型会被序列化地存储在二进制 protocol buffer (binaryproto) .caffemodel 文件中。

输入输出文件
输入:
(1)slover.prototxt。描述网络训练时的各种超参数文件,如训练的策略,学习率的变化率,模型保存的频率等参数
(2)train.prototxt。描述训练网络的拓扑结构参数文件,如卷积层/池化层个数,卷积核大小,卷积核数目。
(3)test.prototxt。描述测试网络的拓扑网络结构参数文件。
输出:
VGG16.caffemodel:保存的训练好的网络可学习参数文件,如权重,偏置。

Caffe代码组成

一个Caffe由四个主要的大类构成,这些类分别是Blob类,Layer类,Net类,Solver类。
● Blob:是存放数据的结构,是用来保存学习到的参数以及网络传输过程中产生数据的类,本质上是一个N维数组。
● Layer:是网络的基本单元,由此派生出了各种层类。修改这部分的人主要是研究特征表达方向的。
● Net:是网络的搭建,将Layer所派生出层类组合成网络。
● Solver:是Net的求解方法,管理并执行Net对象的训练和测试。

Blob

Blob类原型(只截取关键数据和函数)

//Blob是一个模板类
template <typename Dtype>
class Blob{
protected:
    shared_ptr<SyncedMemory> data_;
    shared_ptr<SyncedMemory> diff_;
    vector<int> shape_;
    int count_;
    int capacity_;
public:
    Blob():data_(),diff_(),count_(0),capacity_(0){}//默认构造函数
//...
};

声明一个Blob对象如下:

Blob<float> input;

blob 表示一个4维数组,常规的维数为图像数量 N*通道数 C*图像高度 H *图像宽度W。 Blob 按行为主( row-major) 进行存储,所以一个 4 维 blob 中,索引为(n, c, h, w)的blob值转换为一维数组的索引值为((n* C+c) *H+h)*W+w。

Blob成员变量
Blob类中有两个SyncedMemory类型数据对象指针——data_(数据或权值)和diff_(梯度),一个表示形状的vector<int>,还有两个用于指示大小和总容量的count_和capacity_变量。由于数据既可存储在CPU上,也可存储在GPU上。数据访问有两种方式:静态方式,不改变数据;动态方式,改变数据。

const Dtype* cpu_data() const;
Dtype* mutable_cpu_data();

blob 使用了一个 SyncedMem 类来同步 CPU 和 GPU 上的数值, 以隐藏同步的细节和最小化传送数据。一个经验准则是,如果不想改变数值,就一直使用常量调用,而且绝不要在自定义类中存储指针。 每次操作 blob 时, 调用相应的函数来获取它的指针,因为 SyncedMem 需要用这种方式来确定何时需要复制数据。

Blob成员函数

Blob<float> input;//创建一个Blob对象
vector<int> a;
input.Reshape(1,2,3,4);//它通过整型数组改变Blob的各个维度的长度
a=input.shape();//它返回vector<int>类型的shape
cout<<input.shape_string();//它返回string类型的shape
cout<<input.num();
cout<<input.channels();
cout<<input.height();
cout<<input.width();

//反序列化函数原型,从BlobProto中恢复一个对象
void FromProto(const BlobProto& proto,bool reshape = true);
//序列化函数原型,将内存中的Blob对象存在BlobProto中,以便于存储在磁盘中
void ToProto(BlobProto* proto,bool write_diff = false)const;

Layer

Layer类原型(只截取关键数据和函数)

//Lyaer本身是一个抽象模板类
template <typename Dtype>
class Layer{
protected:
    LayerParameter layer_param_;//它是一个Protocol buffer对象,包含层内结构参数
    Phase phase_;//阶段,TRAIN或TEST
    vector<shared_ptr<Blob<Dtype>>> blobs_;//层的权值和偏置
    vector<Dtype> loss_;//当前层的loss值
    vector<bool> param_propagate_down_;//是否计算误差梯度的标志
public:
    //显式构造函数
    explicit Layer(const LayerParameter& param):layer_param(param),is_shared(false){
    phase_ = param.phase();
    blobs_.resize(layer_param.blobs_size());按照layer_param_设置Blob对象个数
    for(int i = 0;i<layer_param.blobs_size();i++)
    {   
        //对blobs_中每个Blob对象调整尺寸,与layer_param_Blob对象尺寸一致
        blobs_[i].reset(new Blob<Dtype>());
    blobs_i]->FroProto(layer_param_.blobs(i));
    }
    }
//...
};

5大Layer派生类
● 神经元类,定义于neuron_layers.hpp中,其派生类主要是元素级别的运算(比如Dropout运算,激活函数ReLu,Sigmoid等),运算均为同址计算(in-place computation,返回值覆盖原值而占用新的内存)。
● LossLayer类,定义于loss_layers.hpp中,其派生类会产生loss,只有这些层能够产生loss。
● 数据层类,定义于data_layer.hpp中,作为网络的最底层,主要实现数据格式的转换。
● 视觉层类,定义于vision_layers.hpp,实现抽取特征功能,具体地说包含卷积操作,Pooling操作,他们基本都会产生新的内存占用(Pooling相对较小)。
● 通用层类,定义于common_layers.hpp,Caffe提供了单个层与多个层的连接,并在这个头文件中声明。包括了常用的全连接层InnerProductLayer类。

Layer成员数据
1、loss
每一层又有一个loss值,大多数Layer都是0,只有LossLayer才可能产生非0的loss。计算loss是会把所有层的loss_相加。
2、权值和偏置
放在vector<shared_ptr<Blob<Dtype>>>类型的blobs_中,每层可能不止有一个Blob对象,对于大多数Layer来说输入和输出都各连接只有一个Blob,但是对于某些层,如下图所示,LossLayer有两个输入Blob。在网路结构定义文件(*.proto)中每一层的参数bottom和top数目就决定了vector中Blob对象的数目。

Layer成员函数
Layer是Caffe模型的本质内容和执行计算的基本单元。每一个 layer 都定义了 3 种重要的运算: setup(初始化设置), forward(前向传播),backward(反向传播)。CPU模式下,前向和反向传播函数名分别为Forward_cpu()和Backward_cpu;GPU模式下,分别为Forward_gpu()和Backward_gpu,这四个函数都是虚函数,在不同Layer的派生类中需要重写。
● Setup: 在模型初始化时重置 layers 及其相互之间的连接 ;
● Forward: 从bottom blob中接收数据,进行计算后将输出送入到top blob中;
● Backward: 给定top blob的diff,计算其相对于输入的diff,并传递到bottom blob。一个有参数的layer需要计算相对于各个参数的梯度值并存储在内部。
Layer类派生出来的层类通过这实现这两个虚函数,产生了各式各样功能的层类。Forward是从根据bottom计算top的过程,Backward则相反(根据top计算bottom)。

Net

Net类原型(只截取关键数据和函数)

template <typename Dtype>
class Net{
protected:
    string name_;//网络名称
    Phase phase_;//当前阶段

    vector<shared_ptr<Layer<Dtype>>> layers_;//网络中间的层
    vector<string> layer_names_;

    vector<shared_ptr<Blob<Dtype>>> blobs_;//层之间的输入输出blobs
    vector<string> blob_names_;

    vector<shared_ptr<Blob<Dtype>>> params_;//网络权值

    vector<float> params_lr;//学习率
public:
    //显式构造函数
    //NetParameter对象初始化Net
    explicit Net(const NetParameter& param,const Net* root_net = NULL);
    //用网络描述文件初始化Net
    explicit Net(const string& param_file,Phase phase, const Net* root_net= NULL);
    //以输入Blob为参数进行前向传播,返回输出Blob
    const vector<Blob<Dtype>*>& Forward(const vector<Blob<Dtype>*>& bottom,Dtype* loss = NULL);
    void Backward();
    void Update();
//...
};

Net成员数据
一个Net对象包含的三种对象,分别是层数组(layers_)、Blob数组(blobs_)和权重数组(params_)。layers_是Layer的vector容器,其自身实现的功能主要是对逐层Layer进行初始化,以及提供Update( )的接口(更新网络参数)。
Net 是由一系列层组成的有向无环( DAG)计算图, Caffe 保留了计算图中所有的中间值以确保前向和反向迭代的准确性。一个典型的 Net 开始于 data layer——从磁盘中加载数据,终止于 loss layer——计算如分类和重构这些任务的目标函数。

Net成员函数
Forward(…)和Backward()这两个函数对整个网络的前向和方向传播,各调用一次就可以计算出网络的loss了。
Net::Init()进行模型的初始化。初始化主要实现两个操作:创建 blobs 和 layers 以搭建整 个网络 DAG 图,以及调用layers 的 SetUp()函数。
网络构建完之后,通过设置 Caffe::mode()函数中的 Caffe::set_mode(), 即可实现在 CPU 或 GPU 上的运行。

Solver

Solver负责整个模型的优化,让全局loss达到最小。
Solver的主要工作:
1、调用Net::Forward=>调用Net::Backward=>调用Net::Update
2、周期性测试网络。

Solver类原型

//Solver是一个抽象模板类
template <typename Dtype>
class Slover{
protected:
    SolverParameter param_;//用于从prototxt中提取参数
    int iter_;//迭代次数
    shared_ptr<Net<Dtype> > net_;//指向一个Net对象的指针,用于训练
    vector<shared_ptr<Net<Dtype>>> test_nets;//Net对象数组,用于测试
    virtual void ApplyUpdate() = 0;//更新权值
    void TestAll();//每隔一定周期对训练的网络进行一次评估
public:
    //显示构造函数
    //以SolverParameter对象构造Solver
    explicit Slover(const SolverParameter& param,const Solver* root_solver =NULL);
    //以Solver描述文件构造Solver
    explicit Solver(const string& param_file,const Solver* root_solver =NULL);
    void Init(const SolverParameter& param);//初始化
    virtual void Solve(const char* resume_file =NULL);//从一个resume_file恢复训练,默认是从iter 0开始训练
    void Step(int iters);//进行第iter次迭代
//...
};

Solver成员数据
Solver包含一个训练网络net_对象和若干个预测网络test_nets_对象。由Solver管理它们的初始化和执行。

Solver成员函数
根据训练方法的不同定义不同的Solver的派生类,不同的派生类都有一个ComputeUpdateValue( )实现计算update参数的核心功能。
最后当进行整个网络训练过程(也就是你运行Caffe训练某个模型)的时候,实际上是在运行caffe.cpp中的train( )函数,而这个函数实际上是实例化一个Solve的派生类对象,初始化后调用Solve( )函数。而这个Solve( )函数又调用派生类中的ApplyUpdate()函数,ApplyUpdate()函数就包含了ComputeUpdateValue()和net_->Update()两个函数的调用。

至此,以对象的角度分析了caffe深度学习框架源码的组成,希望给初学者一个整体的认识。如有错误,请联系博主~

猜你喜欢

转载自blog.csdn.net/chifredhong/article/details/70331923