参考链接:http://caffe.berkeleyvision.org/tutorial/net_layer_blob.html
Caffe主要就是有Blobs,Layers与Nets三部分组成,当然少不了Caffe内部的自动化机制,但是对于使用者来说,与我们密切相关的就是本文要介绍的这三个数据结构。
Caffe里面的数据存储的结构就是blob,它把用作前向传播的数据以及后向传播的梯度都保存了起来。再数学层面上,一个blob是一个内存分布连续的N维数组。
比如,对一个batch size为N,具有C通道的图像数据,blob的尺寸是N*C*H*W,那么如果下标为(n,c,h,w)的数据就存放在相对偏移为 n∗N∗C∗W+c∗C∗W+h∗W+w=((n∗C+c)∗H+h)∗W+w 的内存空间里,由此我们也可以看到,blob如果顺序访问最右边的维会是最快的,因为空间连续。
当然,Blob不仅仅用作图像计算,也可以用作非图像计算,这根据数据,相应的调整Blob的维数即可。
使用Blob的原因很大程度上是为了隐藏数据传输与同步的细节,在深度学习里,我们通常会用到CPU和GPU(CPU是无论如何都需要的),然而CPU和GPU的内存并非共享的,因此,在做GPU计算时,我们需要通过CPU发送指令,把内存的数据传输到GPU的内存里(GPU就像是一个军队,下达命令的永远是它的头CPU),然后GPU开始计算并更新了GPU内存的数据,此时CPU与GPU的数据就发生了分歧,这时就需要同步两边的数据,很简单的,就是把数据从GPU的内存又拷贝回CPU的内存去(仍然是CPU下指令)。这样的过程反反复复,对于编程者来说这是个非常头疼的事,因为编程者需要时时刻刻记住数据在什么时候可能会被修改,什么时候需要将数据进行同步等。为了解决这个问题,Caffe的blob引入了四种数据访问方式。
- const CPU: const Dtype* cpu_data() const;
- const GPU: const Dtype* gpu_data() const;
- mutable CPU: Dtype* mutable_cpu_data();
- mutable GPU: Dtype* mutable_gpu_data();
顾明思议,const的访问模式是只读的,因此如果我们不需要对数据进行修改,就尽可能的用只读,而mutable是可读可写,当执行了mutable的访问方式,Caffe会认为数据已经被修改,并在下次访问不同步的数据时自动更新。下面是来自Caffe官网的例子。
// Assuming that data are on the CPU initially, and we have a blob.
const Dtype* foo;
Dtype* bar;
foo = blob.gpu_data(); // data copied cpu->gpu.
foo = blob.cpu_data(); // no data copied since both have up-to-date contents.
bar = blob.mutable_gpu_data(); // no data copied.
// ... some operations ...
bar = blob.mutable_gpu_data(); // no data copied when we are still on GPU.
foo = blob.cpu_data(); // data copied gpu->cpu, since the gpu side has modified the data
foo = blob.gpu_data(); // no data copied since both have up-to-date contents
bar = blob.mutable_cpu_data(); // still no data copied.
bar = blob.mutable_gpu_data(); // data copied cpu->gpu.
bar = blob.mutable_cpu_data(); // data copied gpu->cpu.
我们可以看到,如果数据访问的时候使用了mutable,那么即使数据并没做任何修改,在切换到另一个模式下(CPU/GPU切换)的时候,数据都会进行重新的拷贝,当然这个过程是对我们不可见的,我们并不担心会有差错,但是我们要注意,能避免mutable就避免mutable。其次我们还需要注意的是,由于数据访问的形式是以指针的形式,我们不应该在自己的创造的数据结构里面存储这些指针,因为blob同步的时候很有可能会迁移数据,而导致之前的指针悬空,因此,需要用的时候,请通过blob来获得指针,就如上面的例子所示。
在Caffe里,绝大多数的运算发生在layer里面。一个layer可以接收下面层的blobs的data,然后通过forward计算的过程更新上层的新的blobs,也可以接收上层传来的blobs的diff,然后通过backward计算的过程更新下层blob数据。直观来看,一个典型的结果如下图所示
在Caffe中,网络的结构是从下往上的。
从实现的角度来说,一个layer的实现必须包含三个方法
其中setup方法在网络进行初始化的时候会被统一调用,用作参数的设置等。forward方法会在做前向计算的时候调用,backward方法会在做后向传播的时候调用。更具体的,forward和backward还需实现GPU版本的,否则当计算模式处于GPU的时候,会先退回到CPU里面执行,结束后在转回GPU,这个过程也是我们不可见的,但是,我们知道,这样会存在内存同步的问题,会减慢执行效率。
Caffe通过Net将blobs和layers以有向无环图的形式组合起来,注意一定是无环的,否则将无法进行后向传播。
在定义Net的时候,是通过protobuf格式定义的。protobuf是由谷歌提出的数据传输的格式,类似于json。下面引用官网的例子。
这样的一个logistc classification问题,我们可以通过下面的protobuf定义
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()执行的,在执行Init的时候有向无环图的的所有结构都会被创建(在创layer的时候会调用setup,大家还记得吗?)。
下面是在初始化上面的例子的log信息。
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
在初始化结束之后,我们通过调用Caffe::set_mode(..)即可切换计算模式。
毕竟小编也是刚学,有很多东西可能并不是讲得十分到位,希望读者如果有什么异议或建议都可以跟小编我讲,大家一起学习~:)下一节会介绍下Caffe的Solver~