使用caffe也有一段时间了,但更多是使用Python的接口,使用现有的ImageNet训练好的模型进行图片分类。为了更好的了解caffe这个框架,也为了提高自己的水平,在对卷积神经网络有了一些研究之后,终于开始研读caffe的源码了,今天看了Blob类的一些内容,做个总结。
看过caffe官方文档的话,应该会知道,它可以分为三层:Blob、Layer、Net。Blob是一个四维的数组,用于存储数据,包括输入数据、输出数据、权值等等;Layer层则是神经网络中具体的各层结构,主要是计算的作用,在根据配置文件初始化结构后,前向计算结果,反向更新参数,都是它要做的,而它的输入和输出都是Blob数据;Net的话,就是多个Layer组合而成的有向无环图结构,也就是具体的网络了。Layer和Net的代码有待深入,尤其是Layer的代码,caffe实现了差不多40种不同的Layer层,里面有不同的激活函数,这个要好好研究下。
Blob源码解析
#include "caffe/common.hpp"
#include "caffe/proto/caffe.pb.h"
#include "caffe/syncedmem.hpp"
#include "caffe/util/math_functions.hpp"
从blob.hpp
包含的四个头文件入手,其中caffe.pb.h
是google protocol buffer根据caffe.proto
自动生成的,可以到src/caffe/proto/caffe.proto
里看下caffe里面用到的各个数据的定义,比如BlobProto
,Datum
,NetParameter
等。使用这个protocol buffer看起来确实方便,一方面可以用文本文件定义结构化的数据类型,另一方面可以生成查询效率更高、占空间更小的二进制文件,具体的教程可以看看这里。
在caffe/common.hpp
,主要singleton化Caffe类,并封装了boost和CUDA随机数生成的函数,提供了统一的接口。而在caffe/syncedmem.hpp
中,定义了以下的接口:
inline void CaffeMallocHost(void** ptr, size_t size)
inline void CaffeFreeHost(void* ptr)
主要是分配内存和释放内存的。而class SyncedMemory
定义了内存分配管理和CPU与GPU之间同步的函数,也没啥特别的。
比较重要的是caffe/util/math_functions.hpp
,这里面封装了很多cblas矩阵运算,真是密密麻麻,看的我眼花缭乱、如痴如醉。比如:
void caffe_cpu_gemm<float>(const CBLAS_TRANSPOSE TransA, const CBLAS_TRANSPOSE TransB, const int M, const int N, const int K, const float alpha, const float* A, const float* B, const float beta, float* C)
封装了cblas_sgemm(CblasRowMajor, TransA, TransB, M, N, K, alpha, A, lda, B, ldb, beta, C, N)
,这个计算得到的结果为C=alphaAB+beta*C,也即是A和B两个矩阵的乘积。这里有详细的解释。
void caffe_cpu_gemv<float>(const CBLAS_TRANSPOSE TransA, const int M, const int N, const float alpha, const float* A, const float* x, const float beta, float* y)
是对cblas_sgemv
的封装,实现的矩阵与向量的乘积,结果为y=alphaAx+beta*y。
void caffe_axpy<float>(const int N, const float alpha, const float* X, float* Y)
封装了cblas_saxpy
,实现的是Y=alpha*X+Y
里面都是诸如此类的函数,基本是些矩阵和向量的一些处理函数。
回到blob
类,里面定义了data_()
,diff_()
指针,用于存放数据,而num_
, channel_
, height_
, width_
则主要用来做定位offset
和reshape
处理。对于输入(n, c, h, w)
位置的数据位置为((n*channels_+c)*height_+h)*width_+w
,可以依据位置取data_()
或diff_()
中的数据。
对blob的理解还要结合caffe.proto
里面BlobProto
的定义:
message BlobProto {
optional int32 num = 1 [default = 0];
optional int32 channels = 2 [default = 0];
optional int32 height = 3 [default = 0];
optional int32 width = 4 [default = 0];
repeated float data = 5 [packed = true];
repeated float diff = 6 [packed = true];
}
对于BlobProto
,可以看到定义了四个optional
的int32
类型的名字(name)num
、channels
、height
和width
,optional
意味着Blob
可以有一个或者没有这个参数,每个名字(name)后面都有一个数字,这个数字是其名字的一个标签。这个数字就是用来在生成的二进制文件中搜索查询的标签(怪不得会快呢^_^)。关于这个数字,1到15会花费1byte的编码空间,16到2047花费2byte。所以一般建议把那些频繁使用的名字的标签设为1到15之间的值~而后面的repeated
意味着float
类型的data
和diff
可以重复任意次,而加上[packed = true]
是为了更高效的编码。
到这里基本上Blob
就很清楚了,主要数据有两个data
和diff
,用num
、channels
、height
和width
这四个维度来确定数据的具体位置,做一些数据查询和Blobreshape
的操作。
关于Blob就这么多内容,毕竟就是一个统一的数据存取接口,后续会重点读一下Layer的源码,毕竟各层的输入输出和计算更新过程都在里面,还需要补充一些相关的知识~~
目前的感受,是学到了一些封装的手法,可以看看封装cblas函数的那个文件,以及CPU和GPU一些接口的封装上;另一方面是对于Protocol Buffer
有了一些了解,目前看起来确实方便,以后如果遇到类似的场景可以试着用一下~~