<一>概述
从今天开始,我将记录下自己阅读caffe源代码的一些列笔记。首先,我们来看一下caffe这个深度学习框架中的三大基本数据结构。Caffe中的三大基本数据结构从小到大分别为:【Blob】、【Layer】、【Net】。在caffe中,一个具体的CNN模型是通过【Net】表示的;而【Net】是由多个层【Layer】(输入层,卷积层,池化层,全连接层,损失层等)堆叠而成的;【Layer】与【Layer】之间的联系和数据流动是由【Blob】这一caffe中最基本的数据结构完成的。
我们举个形象的例子可以说明caffe中这三大基本数据结构的联系。Caffe的万丈高楼【Net】是按照我们自己设计的图纸【prototxt】(网络模型的描述文件),用【Blob】这些砖块筑成一层层【Layer】楼房的,最后通过SGD方法【Solver】(超参数配置文件)方法进行简单装修【train】、精装修【Finetune】实现的。下面我们将开始学习caffe大厦的砖石结构---【Blob】【Layer】【Net】的基本概念和源代码解读。
深度神经网络是一种模块化的模型,它由一些列作用在【数据块】之上的【内部连接层】组合而成。Caffe基于自己的模型架构,通过逐层定义(Layer-by-Layer)的方式定义一个网络【Nets】(比如:我们在网络模型的配置文件prototxt中,数据输出层、卷积层、池化层、RELU层、全连接层等等一层层的用文本的描述性语言一层一层的定义整个网络模型的配置文件)。网络【Net】从数据输入层搭配损失层自上而下地定义整个网络模型。Caffe使用【Blob】结构来存储、交换和处理网络中正向和反向迭代时的数据【data】和导数信息【diff】;【Blob】是标准的数组结构,它提供了统一的内存接口,详细描述了如何在【Layer】和【Net】之间进行数据的交换和存储;【Layer】是caffe模型基本的计算单元;【Net】是一些列Layers和其连接的集合。【Solver:求解方法】单独配置。
<二>Blob简述
Blob是caffe中处理和传递实际数据的【数据封装包】,并且在CPU和GPU之间具有同步处理功能。从数学意义上说,blob是按C风格连续存储的N维数组。
Caffe基于【Blob】存储和交换数据。为了便于优化,【Blob】提供了统一的内存接口来存储某种类型的数据,例如:批量图像数据、模型参数以及用来进行优化的导数。
【Blob】可根据CPU主机到GPU设备的同步需要,屏蔽CPU/GPU混合运算在计算上的开销。主机和设备上的内存按需求进行分配,以提高内存的使用效率。
对于批量图像数据来说,【Blob】常规的维数为【图像数量N】*【图像通道数K】*【图像高度H】*【图像宽度W】。【Blob】按行以行为主进行存储,所以一个4维的【Blob】中,坐标为(n,k,h,w)的物理位置为((n*k+k)*h+j)*w+w,这也使得最后面和最右边的维度更新更快。
【1】Number/N是每个批次处理的数据量。批量处理信息有利于提高设备处理和交换的数据吞吐量。在ImageNet上,每个训练批量为256张图像,则N=256.
【2】Channel/K是特征维度,例如对RGB图像来说,K=3.
虽然caffe的图像应用例子中很多【Blob】都是4维的坐标,但是对于非图像应用任务,【Blobs】也完全可以照常使用。例如,如果你仅仅需要类似传统传统多层感知机那样的全连接网络,使用2维的【Blob】(N,D),之后再调用InnerProductLayer(全连接层)。
参数【Blob】的维度是根据层的类型和配置而变化的。一个卷积层中若有96个空间维度为11*11,输入为3通道的滤波器,那么【Blob】维度是96*3*11*11。对于一个输入是1024维(输入通道数),输出是1000(输出通道数)的内积层/全连接层,参数【Blob】维度是1000*1024.
对于一些特定数据,自己设计输入工具或数据层是很有必要的。但是无论怎样,一旦你的数据准备完毕,各种层模型将会帮助您完成剩下的工作。
<三>Blob详述和源码解读
Caffe使用称为【Blob】的【4维数组】用于存储和交换数据。Blob提供了统一的存储器接口,持有一批图像图像或者其他数据、权值、权值更新值。其他深度学习框架也有与Blob对应的数据结构,如Torch/Theano/TensorFlow中的Tensor、MxNet中的NDArray、cuda-convert中的NVMatrix等。
【Blob】在内存中表示4维数组,维度从低到高(width,height,cahnnels,num),如果想不通就当做视频流吧,width和height表示图像的宽和高,channels表示图像的颜色通道,num表示第几帧,用于存储数据和权值(data)或者权值增量(diff),在进行网络计算是,每层的输入,输出都需要通过Blob对象缓冲。Blob是caffe的基本存储单元。
【1】Blob的基本用法
为了更好的理解caffe的源代码,我们先建立对Blob的感性认识,学会如何使用Blob,猜测其实现过程,最后在通过阅读原代码获得答案。
/**************************************************************************************************************
文件说明:
【1】caffe三大基本的数据结构Blob的基本用法
【2】在使用Blob之前,需要先包含头文件#include<caffe/blob.hpp>,在通过using namespace caffe,使用命名空间
【3】Blob是一个模板类,所以创建对象的时候需要定制模板的类型参数,下面我们写一个简单的测试程序
文件功能:
Blob模板类的一个测试程序
开发环境:
Win10+caffe+cuda7.5+opencv+vs2013
时间地点:
陕西师范大学 文津楼 2017.8.5
作 者:
九 月
***************************************************************************************************************/
#include<vector>
#include<iostream>
#include<caffe/blob.hpp>
using namespace caffe;
using namespace std;
int main(void)
{
caffe::Blob<float> blobVar; //【1】Blob是一个模板类,需要定制类型参数
std::cout << "[1]Blob Size = " << blobVar.shape_string() << std::endl;
blobVar.Reshape(1, 2, 3, 4); //【2】模板类调用自己的Reshape()方法
std::cout << "[1]Blob Size = " << blobVar.shape_string() << std::endl;
std::system("pause"); //【3】执行DOS命令
return 0;
}
/**************************************************************************************************************
文件说明:
【1】caffe三大基本的数据结构Blob的基本用法
【2】在使用Blob之前,需要先包含头文件#include<caffe/blob.hpp>,在通过using namespace caffe,使用命名空间
【3】Blob是一个模板类,所以创建对象的时候需要定制模板的类型参数,下面我们写一个简单的测试程序
文件功能:
创建Blob模板类对象之后,可以通过mutable_cpu[gpu]_data[diff]函数修改其内部数值
开发环境:
Win10+caffe+cuda7.5+opencv+vs2013
时间地点:
陕西师范大学 文津楼 2017.8.5
作 者:
九 月
***************************************************************************************************************/
#include<vector>
#include<iostream>
#include<caffe/blob.hpp>
using namespace caffe;
using namespace std;
int main(void)
{
caffe::Blob<float> blobVar; //【1】Blob是一个模板类,需要定制类型参数
std::cout << "[1]Blob Size = " << blobVar.shape_string() << std::endl;
blobVar.Reshape(1, 2, 3, 4); //【2】模板类调用自己的Reshape()方法
std::cout << "[1]Blob Size = " << blobVar.shape_string() << std::endl;
float* p = blobVar.mutable_cpu_data();
for (int i = 0; i < blobVar.count(); i++) //【3】Blob总共的维度
{
p[i] = i;
}
for (int u = 0; u < blobVar.num(); u++) //【4】帧号
{
for (int v = 0; v < blobVar.channels(); v++) //【5】通道
{
for (int w = 0; w < blobVar.width(); w++) //【6】高
{
for (int h = 0; h < blobVar.height(); h++) //【7】宽
{
std::cout << "blobVar[" << u << "][" << v << "][" << w << "][" << h << "] = " << blobVar.data_at(u, v, w, h) << std::endl;
}//h
}//w
}//int v
}//int u
std::system("pause");
return 0;
}
<四>Blob数据结构的描述
打开src/caffe/proto/caffe.proto,首先映入眼帘的便是与Blob相关的描述,可见该数据结构的重要性,数据结构类型【Blob】是其他大部分数据结构的依赖项。
在我们具体分析caffe.proto中和Blob有关的数据结构之前,我们先可以了解一下caffe.proto的作用。
<五>caffe.proto的简介
文件caffe.Proto位于...\src\caffe\proto目录下,caffe源代码在编译时,caffe.proto会生成两个文件caffe.pb.cc和caffe.pb.h文件。
在caffe.proto中定义了很多的结构化数据,包括:
/**************************************************************************************************************
文件说明:
caffe.proto中重要的结构化数据的定义
开发环境:
Win10+caffe+cuda7.5+opencv+vs2013
时间地点:
陕西师范大学 文津楼 2017.8.7
作 者:
九 月
***************************************************************************************************************/
【1】BlobProto----------------------【1】caffe中处理和传递数据的数据封装包------四大数据结构之一
【2】Datum
【3】FillerParameter----------------【2】滤波器参数数据结构
【4】NetParameter-------------------【3】caffe中描述网络的Net数据结构-----------四大数据结构之一
【5】SolverParameter----------------【4】caffe中Solver求解器参数----------------四大数据结构之一
【6】SolverState
【7】LayerParameter-----------------【5】Layer是caffe模型中基本的计算单元-------四大数据结构之一
【8】ConcatParameter
【9】ConvolutionParameter-----------【6】卷基层
【10】DataParameter-----------------【7】数据层
【11】DropoutParameter--------------【8】Dropout层
【12】HDF5DataParameter
【13】HDF5OutputParameter
【14】ImageDataParameter------------【9】图像数据层
【15】InfogainLossParameter
【16】InnerProductParameter---------【10】caffe的全连接层
【17】LRNParameter------------------【11】caffe的局部响应值归一化层
【18】MemoryDataParameter
【19】PoolingParameter--------------【12】caffe的池化层
【20】PowerParameter
【21】WindowDataParameter
【22】V0LayerParameter
(1)什么是protocol buffer
Protocol Buffer是由Google开发的一种可以实现内存和非易失性存储介质(如硬盘文件)进行数据交换的协议接口。Caffe源代码中大量使用了ProtoBuffer作为【权重】和【模型参数】的载体。一般开发者对于参数的管理各有好恶,有些人喜欢舒勇TXT文件,有些人喜欢使用GUI,有些事喜欢使用bin,但是在一个项目中,不一致的参数管理会给项目的推进带来很多问题,因此Google开发了一款约定俗称的数据参数通讯规范,这种具有统一数据访问接口的数据规范就是我们称的通讯协议。ProtoBuffer工具完建立美的解决了这个问题,用户只需要建立统一的【参数描述文件】proto文件,然后利用proto编译就能统一的数据结构访问代码,节省了大量的开发时间。使用ProtoBuffer还可以实现跨语言(C++/Python/java)传递相同的数据结构,让团队合作更有效率。ProtiBuffer现在只提供三种语言的API(C++/JAVA/Python)。
(2)ProtoBuffer一个简单的例子
我打算使用 Protobuf 和 C++ 开发一个十分简单的例子程序来说明这个问题。该程序由两部分组成。第一部分被称为 Writer,第二部分叫做 Reader。Writer 负责将一些结构化的数据写入一个磁盘文件,Reader 则负责从该磁盘文件中读取结构化数据并打印到屏幕上。准备用于演示的结构化数据是 HelloWorld,它包含两个基本数据:ID,为一个整数类型的数据Str,这是一个字符串
package lm;
message helloworld
{
required int32 id = 1; // ID
required string str = 2; // str
optional int32 opt = 3; //optional field
}
(3)书写 .proto 文件
首先我们需要编写一个 proto 文件,定义我们程序中需要处理的结构化数据,在 protobuf 的术语中,结构化数据被称为 Message。proto 文件非常类似 java 或者 C 语言的数据定义。代码清单 1 显示了例子应用中的 proto 文件内容。
清单 1. proto 文件
一个比较好的习惯是认真对待 proto 文件的文件名。比如将命名规则定于
packageName.MessageName.proto在上例中,package 名字叫做 lm,定义了一个消息 helloworld,该消息有三个成员,类型为 int32 的 id,另一个为类型为 string 的成员 str。opt 是一个可选的成员,即消息中可以不包含该成员。
(4)编译 .proto 文件
写好 proto 文件之后就可以用 Protobuf 编译器将该文件编译成目标语言了。本例中我们将使用 C++。假设您的 proto 文件存放在 $SRC_DIR 下面,您也想把生成的文件放在同一个目录下,则可以使用如下命令:
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto
命令将生成两个文件:
lm.helloworld.pb.h , 定义了 C++ 类的头文件
lm.helloworld.pb.cc , C++ 类的实现文件
在生成的头文件中,定义了一个 C++ 类 helloworld,后面的 Writer 和 Reader 将
用这个类来对消息进行操作。诸如对消息的成员进行赋值,将消息序列化等等都有相应的方法
<六>Blob源代码的解读
/**************************************************************************************************************
文件说明:
caffe中的caffe.proto文件中和Blob有关的数据结构
开发环境:
Win10+caffe+cuda7.5+opencv+vs2013
时间地点:
陕西师范大学 文津楼 2017.8.7
作 者:
九 月
***************************************************************************************************************/
//【1】Specifies the shape (dimensions) of a Blob.
//【1】该结构详细描述了Blob的【形状信息】
message BlobShape
{ //【0】packed表示这些值在内存中紧密排布,没有空洞
repeated int64 dim = 1 [packed = true]; //【1】只包括若干int64类型值,分别表示Blob每个维度的大小
}
//【2】该结构描述Blob在磁盘中序列化后的形态
message BlobProto
{
optional BlobShape shape = 7; //【0】可选参数,包含一个BlobShape对象
repeated float data = 5 [packed = true]; //【1】包含若干浮点元素,存储数据或权值,元素数目由shape或
// (num,channels,height,width)确定,这些元素在内存中紧
// 密排布
repeated float diff = 6 [packed = true]; //【2】包括若干浮点元素,用于存储增量信息,维度与data数组一致
repeated double double_data = 8 [packed = true]; //【3】与data并列,只是数据类型为double
repeated double double_diff = 9 [packed = true]; //【4】与diff并且,只是数据类型为double
// 4D dimensions -- deprecated. Use "shape" instead.//【5】以下为可选的维度信息,新版本caffe推荐使用shape
optional int32 num = 1 [default = 0];
optional int32 channels = 2 [default = 0];
optional int32 height = 3 [default = 0];
optional int32 width = 4 [default = 0];
}
// The BlobProtoVector is simply a way to pass multiple blobproto instances
// around.
message BlobProtoVector
{
repeated BlobProto blobs = 1;
}
message Datum
{
optional int32 channels = 1;
optional int32 height = 2;
optional int32 width = 3;
// the actual image data, in bytes
optional bytes data = 4;
optional int32 label = 5;
// Optionally, the datum could also hold float data.
repeated float float_data = 6;
// If true data contains an encoded image that need to be decoded
optional bool encoded = 7 [default = false];
}
/************************************************************************************************************************
文件说明:
【1】blob.hpp文件位于../libcaffe/include.hpp下
【2】Blob是caffe中实际处理和传递数据的数据封装包,并且在CPU和GPU之间有数据同步能力
【3】Blob是一个模板类,所以在实例化Blob对象时,需要传递对象的类型参数
开发环境:
Win10+caffe+cuda7.5+opencv+vs2013
时间地点:
陕西师范大学 文津楼 2017.8.7
作 者:
九 月
*************************************************************************************************************************/
#ifndef CAFFE_BLOB_HPP_ //【1】【预处理命令】--->【条件编译】防止文件被重复包含编译
#define CAFFE_BLOB_HPP_
#include <algorithm> //【2】C++标准库STL头文件,C标准库头文件带.h
#include <string> //【3】STL中的基本字符序列容器头文件
#include <vector> //【4】STL中的vector向量容器头文件
#include "caffe/common.hpp"
#include "caffe/proto/caffe.pb.h"
#include "caffe/syncedmem.hpp" //【5】CPU/GPU共享内存类,用于数据同步
const int kMaxBlobAxes = 32;
namespace caffe
{
/********************************************************************************************************************
模块描述:
【1】Blob是一个模板类,封装了syncedMemory类,作为一个基本的计算单元,服务于Layer,Net,Solver。
【2】A wrapper around SyncedMemory holders serving as the basic computational unit through which Layer%s,
Net%s, and Solver%s interact.
*********************************************************************************************************************/
template <typename Dtype>
class Blob
{
public:
Blob(): data_(), diff_(), count_(0), capacity_(0) {} //【1】带初始化列表的构造函数
explicit Blob(const int num, //【2】显式的构造函数
const int channels,
const int height,
const int width);
explicit Blob(const vector<int>& shape); //【3】显式的复制构造函数,避免隐式的数据类型转换
/****************************************************************************************************************
函数说明:
【1】改变一个Blob的大小
【2】改变Blob的维度,如果需要分配新的内存
函数功能:
变形函数,根据输入的参数,重新设置当前Blob的形状,必要时重新分配内存。
函数参数:
形参做了const限定,对象不允许在函数中被修改
*****************************************************************************************************************/
void Reshape(const int num,
const int channels,
const int height,
const int width);
void Reshape(const vector<int>& shape);
void Reshape(const BlobShape& shape);
void ReshapeLike(const Blob& other);
inline string shape_string() const //【5】得到Blob的形状字符串,用于打印log
{
ostringstream stream;
for (int i = 0; i < shape_.size(); ++i)
{
stream << shape_[i] << " ";
}
stream << "(" << count_ << ")";
return stream.str();
}
inline const vector<int>& shape() const //【6】返回Blob的形状
{
return shape_;
}
inline int shape(int index) const //【8】返回Blob某一维度的尺寸
{
return shape_[CanonicalAxisIndex(index)];
}
inline int num_axes() const //【9】返回维度的数目
{
return shape_.size();
}
inline int count() const //【10】返回Blob中元素的总数
{
return count_;
}
inline int count(int start_axis, int end_axis) const //【11】返回Blob中某几维子集的元素总数
{
CHECK_LE(start_axis, end_axis);
CHECK_GE(start_axis, 0);
CHECK_GE(end_axis, 0);
CHECK_LE(start_axis, num_axes());
CHECK_LE(end_axis, num_axes());
int count = 1;
for (int i = start_axis; i < end_axis; ++i)
{
count *= shape(i);
}
return count;
}
inline int count(int start_axis) const //【12】计算从某一维度开始的元素总数
{
return count(start_axis, num_axes());
}
//【13】转换坐标轴索引[-N,N]为普通索引[0,N]
inline int CanonicalAxisIndex(int axis_index) const
{
CHECK_GE(axis_index, -num_axes())
<< "axis " << axis_index << " out of range for " << num_axes()
<< "-D Blob with shape " << shape_string();
CHECK_LT(axis_index, num_axes())
<< "axis " << axis_index << " out of range for " << num_axes()
<< "-D Blob with shape " << shape_string();
if (axis_index < 0)
{
return axis_index + num_axes();
}
return axis_index;
}
inline int num() const //【14】获取Shape某一维度(num,channels,height,width)的尺寸
{
return LegacyShape(0);
}
inline int channels() const
{
return LegacyShape(1);
}
inline int height() const
{
return LegacyShape(2);
}
inline int width() const
{
return LegacyShape(3);
}
inline int LegacyShape(int index) const
{
CHECK_LE(num_axes(), 4)<< "Cannot use legacy accessors on Blobs with > 4 axes.";
CHECK_LT(index, 4);
CHECK_GE(index, -4);
if (index >= num_axes() || index < -num_axes())
{
return 1;
}
return shape(index);
}
inline int offset(const int n, const int c = 0, const int h = 0,const int w = 0) const
{
CHECK_GE(n, 0);
CHECK_LE(n, num());
CHECK_GE(channels(), 0);
CHECK_LE(c, channels());
CHECK_GE(height(), 0);
CHECK_LE(h, height());
CHECK_GE(width(), 0);
CHECK_LE(w, width());
return ((n * channels() + c) * height() + h) * width() + w;
}
inline int offset(const vector<int>& indices) const
{
CHECK_LE(indices.size(), num_axes());
int offset = 0;
for (int i = 0; i < num_axes(); ++i)
{
offset *= shape(i);
if (indices.size() > i)
{
CHECK_GE(indices[i], 0);
CHECK_LT(indices[i], shape(i));
offset += indices[i];
}
}
return offset;
}
/**
* @brief Copy from a source Blob.
*
* @param source the Blob to copy from
* @param copy_diff if false, copy the data; if true, copy the diff
* @param reshape if false, require this Blob to be pre-shaped to the shape
* of other (and die otherwise); if true, Reshape this Blob to other's
* shape if necessary
*/
/****************************************************************************************************************
函数说明:
【1】改变一个Blob的大小
【2】改变Blob的维度,如果需要分配新的内存
函数功能:
从Source拷贝数据,copy_diff作为标志,用来区分是拷贝data还是拷贝diff
函数参数:
形参做了const限定,对象不允许在函数中被修改
*****************************************************************************************************************/
void CopyFrom(const Blob<Dtype>& source,
bool copy_diff = false,
bool reshape = false);
inline Dtype data_at(const int n, const int c, const int h,const int w) const
{
return cpu_data()[offset(n, c, h, w)];
}
inline Dtype diff_at(const int n, const int c, const int h, const int w) const
{
return cpu_diff()[offset(n, c, h, w)];
}
inline Dtype data_at(const vector<int>& index) const {
return cpu_data()[offset(index)];
}
inline Dtype diff_at(const vector<int>& index) const
{
return cpu_diff()[offset(index)];
}
inline const shared_ptr<SyncedMemory>& data() const
{
CHECK(data_);
return data_;
}
inline const shared_ptr<SyncedMemory>& diff() const
{
CHECK(diff_);
return diff_;
}
const Dtype* cpu_data() const; //【1】只访问CPU Data
void set_cpu_data(Dtype* data); //【2】设置CPU Data
const int* gpu_shape() const;
const Dtype* gpu_data() const; //【1】访问GPU Data
const Dtype* cpu_diff() const;
const Dtype* gpu_diff() const;
Dtype* mutable_cpu_data();
Dtype* mutable_gpu_data();
Dtype* mutable_cpu_diff();
Dtype* mutable_gpu_diff();
void Update();
void FromProto(const BlobProto& proto, bool reshape = true);
void ToProto(BlobProto* proto, bool write_diff = false) const;
/// @brief Compute the sum of absolute values (L1 norm) of the data.
Dtype asum_data() const;
/// @brief Compute the sum of absolute values (L1 norm) of the diff.
Dtype asum_diff() const;
/// @brief Compute the sum of squares (L2 norm squared) of the data.
Dtype sumsq_data() const;
/// @brief Compute the sum of squares (L2 norm squared) of the diff.
Dtype sumsq_diff() const;
/// @brief Scale the blob data by a constant factor.
void scale_data(Dtype scale_factor);
/// @brief Scale the blob diff by a constant factor.
void scale_diff(Dtype scale_factor);
/**
* @brief Set the data_ shared_ptr to point to the SyncedMemory holding the
* data_ of Blob other -- useful in Layer%s which simply perform a copy
* in their Forward pass.
*
* This deallocates the SyncedMemory holding this Blob's data_, as
* shared_ptr calls its destructor when reset with the "=" operator.
*/
void ShareData(const Blob& other);
/**
* @brief Set the diff_ shared_ptr to point to the SyncedMemory holding the
* diff_ of Blob other -- useful in Layer%s which simply perform a copy
* in their Forward pass.
*
* This deallocates the SyncedMemory holding this Blob's diff_, as
* shared_ptr calls its destructor when reset with the "=" operator.
*/
void ShareDiff(const Blob& other);
bool ShapeEquals(const BlobProto& other);
protected:
shared_ptr<SyncedMemory> data_;
shared_ptr<SyncedMemory> diff_;
shared_ptr<SyncedMemory> shape_data_;
vector<int> shape_;
int count_;
int capacity_;
DISABLE_COPY_AND_ASSIGN(Blob);
}; // class Blob
} // namespace cffe
#endif // CAFFE_BLOB_HPP_