Caffe教程系列之元素篇

本教程旨在督促自己从头到尾重新学习一遍Caffe,主要参考Caffe主页的教程和大牛们的博文,若有不妥之处,还望告知。

Caffe系列教程 - 闲渔的文章 - 知乎专栏

本节旨在对Caffe模型中的主要元素Blobs、Layers和Nets做一些简单说明,基本上翻译Caffe官方主页的教程Caffe | Blobs, Layers, and Nets

简介

Deep Networks是由一系列相互连接的Layers构成的组合模型,这些层对数据块进行处理。Caffe按照自己模型的模式一层一层定义网络,从输入数据到损失函数自上而下地定义整个模型。Caffe采用blobs(blob是框架内使用的标准数组和统一内存操作接口)的类型存储、传播和操作那些在网络中不断正向和反向传递的数据和梯度。Layers是模型和计算的基础,Net聚集并连接模型中的Layers,而Solving则用于分离建模和优化

Blob存储和通信

Blob封装Caffe中实际处理和传递数据,并从底层同步CPU和GPU的数据。数学上,Blob是一个以C形式内存存储的N-维数组。Caffe存储和传递数据都采用Blob,Blob提供一个数据保持的统一内存接口,如一批图像、模型参数以及优化过程中的梯度。Blob隐藏了CPU/GPU混合模式下从CPU到GPU数据同步所需要的计算和编程消耗。主机和GPU设备上的内存根据需求高效分配。

常规情况下,批量图像数据的Blob维度:N \times K \times H \times W,其中N表示图像数量,K表示图像通道数,H表示图像高度,W表示图像宽度。Blob是以行为主的内存存储结构,所以最后/最右边的维度变化最快,例如,一个4D的Blob在索引(n,k,h,w)下的值实际索引是((n\times K+k)\times H + h)\times W +w

  • Number / N 是数据的批处理大小。批处理能够提高数据传递和设备处理的吞吐量。
  • Channel / K 是特征维度,比如RGB图像的 K=3

注:尽管Caffe的样例中大多数Blob都是图像应用中的4D,但对于非图像的应用同样可以利用Blob。比如在全连接层中使用2D的Blob(shape N*D),然后调用InnerProductLayer。

Blob的维度随着Layer的类型和配置而变化,对于一个拥有96个11\times 11空间维度的filters和3个输入的卷积层,Blob的维度应当是96\times 3\times 11\times 11。对于拥有1000个输出通道和1024个输入通道的inner product/fully-connected层,Blob的维度应当是1000\times 1024

整个模型中,我们一般只对数据和梯度感兴趣,所以Blob存储两块内存:datadiff。前者指的是网络中传递的数据,后者则是网络中计算的梯度值。Caffe中支持数据存储在CPU和GPU上,可以有两种方式获得这些数据:一种是不改变数据值;一种可变的,可以修改数据(GPU和CPU下操作类似)。

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

这样设计的理由是,Blob使用SyncedMem类来同步CPU和GPU中的数据,从而隐藏同步的细节并最小化数据传递。一个简便的方法是,如果不想改变数据值则尽量使用const方法,而且绝对不要在你的对象中存储指针。每次对Blob进行操作,使用函数获取指针,SyncedMem需要这种操作来确定什么时候复制数据。

实际操作中,GPU模式下:使用CPU代码从硬盘中载入数据,调用device kernel执行GPU计算,并传递给下一层,此过程忽略了维持高性能的一些细节。只要所有的Layers都实现了GPU下的方法,那么所有的中间数据和梯度数据都将留存在GPU中。

这里有一个检查Blob复制数据的样例:

// 假定数据在CPU上进行了初始化,并存储在blob中
const Dtype* foo;
Dtype* bar;
foo = blob.gpu_data();  //数据复制 cpu->gpu
foo = blob.cpu_data();  //没有数据复制,因为CPU和GPU上的数据已经同步
bar = blob.mutable_gpu_data();  //没有数据复制
// ... 一些操作 ...
bar = blob.mutable_gpu_data();  //仍然在GPU上操作,所以没有数据复制
foo = blob.cpu_data();  //数据复制 gpu->cpu,由于GPU端的数据修改了
foo = blob.gpu_data;  //没有数据复制,CPU和GPU上的数据已经同步
bar = blob.mutable_cpu_data();  // 没有复制
bar = blob.mutable_gpu_data();  // 数据复制 cpu->gpu
bar = blob.mutable_cpu_data();  // 数据复制 gpu->cpu

样例中可以看出,mutable的操作会造成数据的复制,即使没有对数据进行处理。

Layer计算和连接

Layer是模型的本质和计算的基本单元,Layers可以进行filters的卷积、池化(pool)、inner product操作、非线性激活函数(rectified-linear和sigmoid)以及一些元素级的变换、正则化、数据加载和损失计算(softmax和hinge)。可以参考Layer Catalogue查看更多操作,基本上包含了当前需要的类型。

Layer从bottom的连接接收输入,并从top端的连接输出。每个Layer定义了三种关键的计算:Setup、Forward和Backward。

  • Setup:在模型初始化时对Layer及其相关连接进行初始化。
  • Forward:给定bottom端的输入计算输出并发送给top端。
  • Backward:给定top输出端计算的梯度计算其输入端的梯度并发送到bottom,Layer计算其参数相关的梯度并存储到内存。

更具体地,Caffe实现了两个Forward和Backward函数,一个CPU的和一个GPU的。如果没有实现GPU版本的函数,则退回到CPU函数并做备份。这种操作很方便进行快速实验,尽管可能需要额外的数据传输消耗(输入数据需要从GPU复制到CPU,而输出又需要从CPU复制回GPU)。

Layers对于整个网络的操作有两个关键的职责:前向传导(Forward Pass)接受输入并计算输出,后向传导(Backward Pass)接受关于输出的梯度并计算相关参数的梯度并逐步将梯度后向传播至之前的Layers。

自定义Layers只需要很少的工作就可以完成,这得意于网络结构的复合性和代码的模块化。定义好Layer的Setup、Forward和Backward就可以应用于Net中了。

Net定义和操作

Net通过组合和自微分定义了一个函数及其梯度。每个Layer的输出组合计算指定任务的函数,每个Layer的backward组合从loss计算梯度达到任务学习的目的。Caffe模型是一个端到端的机器学习引擎。

Net是一组计算图(有向无环图,DAG)中相互连接的Layer集合,Caffe对DAG中每个层都做记录以保证在前向和后向传递的正确性。一个典型的Net一般开始于数据层(从硬盘中加载数据)、结束于损失层(计算指定分类或重建任务的目标函数)。

Net定义为一个Layers的集合,层与层之间的连接采用纯文本建模语言。一个简单的Logistic Regression Classifier。

模型定义:

name: "LogReg"
layer {
  name: "mnist"
  type: "Data"
  top: "data"
  top: "label"
  data_param {
    source: "input_leveldb"
    batch_size: 64
  }
}
layer {
  name: "ip"
  type: "InnerProduct"
  bottom: "data"
  top: "ip"
  inner_product_param {
    num_output: 2
  }
}
layer {
  name: "loss"
  type: "SoftmaxWithLoss"
  bottom: "ip"
  bottom: "label"
  top: "loss"
}
采用Net::Init()进行模型初始化,初始化主要做两件事情:通过创建Blobs和Layer搭建DAG的整体框架(Network在整个生命周期中将始终持有Blobs和Layers)和调用Layers的Setup()函数。当然还会做一些其他事情,如检查整个Network架构的正确性。在初始化过程中,Net将记录INFO日志对其进行说明。
I0902 22:52:17.931977 2079114000 net.cpp:39] Initializing net from parameters:
name: "LogReg"
[...model prototxt printout...]
# construct the network layer-by-layer
I0902 22:52:17.932152 2079114000 net.cpp:67] Creating Layer mnist
I0902 22:52:17.932165 2079114000 net.cpp:356] mnist -> data
I0902 22:52:17.932188 2079114000 net.cpp:356] mnist -> label
I0902 22:52:17.932200 2079114000 net.cpp:96] Setting up mnist
I0902 22:52:17.935807 2079114000 data_layer.cpp:135] Opening leveldb input_leveldb
I0902 22:52:17.937155 2079114000 data_layer.cpp:195] output data size: 64,1,28,28
I0902 22:52:17.938570 2079114000 net.cpp:103] Top shape: 64 1 28 28 (50176)
I0902 22:52:17.938593 2079114000 net.cpp:103] Top shape: 64 (64)
I0902 22:52:17.938611 2079114000 net.cpp:67] Creating Layer ip
I0902 22:52:17.938617 2079114000 net.cpp:394] ip <- data
I0902 22:52:17.939177 2079114000 net.cpp:356] ip -> ip
I0902 22:52:17.939196 2079114000 net.cpp:96] Setting up ip
I0902 22:52:17.940289 2079114000 net.cpp:103] Top shape: 64 2 (128)
I0902 22:52:17.941270 2079114000 net.cpp:67] Creating Layer loss
I0902 22:52:17.941305 2079114000 net.cpp:394] loss <- ip
I0902 22:52:17.941314 2079114000 net.cpp:394] loss <- label
I0902 22:52:17.941323 2079114000 net.cpp:356] loss -> loss
# set up the loss and configure the backward pass
I0902 22:52:17.941328 2079114000 net.cpp:96] Setting up loss
I0902 22:52:17.941328 2079114000 net.cpp:103] Top shape: (1)
I0902 22:52:17.941329 2079114000 net.cpp:109]     with loss weight 1
I0902 22:52:17.941779 2079114000 net.cpp:170] loss needs backward computation.
I0902 22:52:17.941787 2079114000 net.cpp:170] ip needs backward computation.
I0902 22:52:17.941794 2079114000 net.cpp:172] mnist does not need backward computation.
# determine outputs
I0902 22:52:17.941800 2079114000 net.cpp:208] This network produces output loss
# finish initialization and report memory usage
I0902 22:52:17.941810 2079114000 net.cpp:467] Collecting Learning Rate and Weight Decay.
I0902 22:52:17.941818 2079114000 net.cpp:219] Network initialization done.
I0902 22:52:17.941824 2079114000 net.cpp:220] Memory required for data: 201476

注:Network的架构是设备无关的,Blobs和Layers都隐藏了模型定义中的实现细节。模型构建完成后,可以通过Caffe::set_mode()来设置是在CPU上运行还是在GPU上运行Network(设置Caffe::mode()),CPU和GPU模式下得出的结果是一致的。CPU/GPU模式切换是无缝的,而且独立于模型的定义,对于研究或者部署来说,模型和实现分开是最好的。

模型文件格式

模型定义在纯文本的Protocol Buffer Schema下(prototxt),训练模型则序列化为二进制的Protocol Buffer(binaryproto).caffemodel文件。

模型格式采用protobuf schema定义在caffe.proto文件中,源文件基本上是自解释的。

Google Protocol Buffer的优点:序列化时最小化二进制序列、序列化效率高、可读性好而且兼容二进制版本并且支持多种不同的语言,最常用的C++和Python。这些优点使得采用Caffe建模非常灵活而且可扩展性也很好。

发布于 2016-10-25