Caffe 源码阅读笔记一(基本代码框架)

在开始阅读Blob之前,需要预先了解Caffe的架构与protobuf,否则看代码的时候容易一头雾水

Caffe基本架构

对于任何一个网络而言,Caffe主要用4个类来构造网络:

  • BLob:存储数据,blob既可以存储网络的权重,在进行训练时修改的其实就是每个blob实例
  • Layer:构成网络的基础单元,无论是卷积层,pooling层还是全连接层等等都是从这个类派生出来的
  • Net:定义一个网络的架构,当我们把网络中每一层都建立完之后,通过Net类来搭建整个网络
  • Solver:用来训练网络的类,例如SGD就在这部分实现

Protobuf

在理清基本架构之后可能有人会问:一个网络需要那么多参数,且每个参数的数据类型不尽相同,如何解决这个问题呢?

Protobuf就是用来解决此类问题的,我们只需要像写配置文件一样把网络中每一层的参数设置好(当然配置文件必须按照一定的格式),protobuf会自动生成代码,用来解析“配置文件”中的参数结构体,代码中包括了对参数的设置、读取和序列化等等操作 另外当网络训练完成之后,存储时也使用了protobuf进行处理,把Net转化为多个Message对象进行存储

Caffe代码中参数初始化和参数操作都是基于protobuf完成,只需要定义网络参数,不再需要考虑如何把参数传递给应用程序的问题了,protobuf会帮你完成~

Blob

Blob本身其实没有特别多的内容,作为Caffe中数据的基础单元,我们可以简单地吧Blob理解成一个容器,里面存储的是多维向量及其相关的信息

首先看Blob.hpp:

Blob()
 : data_(), diff_(), count_(0), capacity_(0) {}
   

构造函数中初始化了4个对象,data_表示Blob中的数据,diff_表示反向传播时的误差,count_表示数据当前的维度(因为可以reshape),而capacity_表示数据最大的维度

接下来在Blob.cpp里主要包含了实例的初始化和reshape函数,值得注意的是如果blob中存储的是网络的参数(如全连接层和卷积层的kernel),那么Blob提供update函数用来更新自己的参数。

void Blob<Dtype>::Update() {                                                                                                                                  
  // We will perform update based on where the data is located.                                                                                               
  switch (data_->head()) {                                                                                                                                    
  case SyncedMemory::HEAD_AT_CPU:                                                                                                                             
    // perform computation on CPU                                                                                                                             
    caffe_axpy<Dtype>(count_, Dtype(-1),                                                                                                                      
      static_cast<const Dtype*>(diff_->cpu_data()),                                                                                                         
      static_cast<Dtype*>(data_->mutable_cpu_data()));                                                                                                      
    break;                                                                                                                                                    
  case SyncedMemory::HEAD_AT_GPU:                                                                                                                             
  case SyncedMemory::SYNCED:                                                                                                                                  
  #ifndef CPU_ONLY                                                                                                                                              
  // perform computation on GPU                                                                                                                             
  caffe_gpu_axpy<Dtype>(count_, Dtype(-1),                                                                                                                  
      static_cast<const Dtype*>(diff_->gpu_data()),                                                                                                         
      static_cast<Dtype*>(data_->mutable_gpu_data()));                                                                                                      
  #else                                                                                                                                                         
  NO_GPU;                                                                                                                                                   
  #endif                                                                                                                                                        
  break;                                                                                                                                                    
  default:                                                                                                                                                    
    LOG(FATAL) << "Syncedmem not initialized.";                                                                                                               
  }                                                                                                                                                           
}                            

代码中包含了GPU和CPU两种实现,其实核心就是caffe_gpu_axpy那一句更新参数

Layer

对Blob有了一定了解之后可以看Layer了,事实上Layer是caffe架构中内容最多的部分,我们在写配置文件的时候其实都是在组合layer构成一个网络,很多运算操作在caffe中都是以layer的形式存在的,如argmax和elementwise运算等等.许多对caffe的扩展其实也是继承Layer层重新定义了具有新功能的层

layer的工作模式类似数学中的函数概念,给定一个输入(bottom blobs),layer内部完成自己的功能,返回一个输出(top blobs)

caffe中的layer分两种,common layer和vision layer: - data_layers.hpp中声明了神经网络与输入数据之间的交互层,如导入/导出hdf5数据,从图像中导入数据等等 - common_layers.hpp中声明了许多常用的包含基础功能(flatten, softmax等)的层 - vision_layers.hpp中主要包含对针对图像处理的层(convolutional, pooling等) - neuron_layers.hpp中主要包含与神经元的定义与操作相关的层(dropout,ReLu等) - loss_layers.hpp中主要包含了计算网络Loss的层,除了我们常见的softmax loss之外还包括了比较常用多euclidean loss, hinge loss等等 看了下vision layer中居然还包含了SPPnet这种处理image scale的层,除此之外还有deconvolution layer,确实是很给力啊

caffe还提供了layer_factory供我们快速实现自己的layer

所有的Layer层都包含Forward和Backward的函数,这两个函数包含了整个layer的计算功能,对于开发者而言如果想实现自己的layer,其实主要就是完成这两个函数逻辑

具体的某一种Layer的代码解析不在本文的范畴里,以后会另开blog详细描述常用的layer代码实现

Net

Net中包含了对整个网络的操作实现,在看源代码之前笔者只想到了设置learning_rate、 weight_decay和迭代次数等等功能,以及控制Forward和Backward等,然而事实上caffe提供了许多非常有用的函数,如在RCNN training中提到的share weights功能,使得你可以直接使用预先训练好的网络参数进行初始化,这也是为什么在caffe的example中我们可以直接通过命令行对imagenet训练出来的网络进行fine-tuning的原因

Solver

相对而言solver的实现比较简单,创建solver对象时我们只需要关注优化算法就可以了,caffe目前提供三种梯度下降的方法: - SGD(momentum) - NesterRov - Adagrad