Blob代码分析
本文主要介绍代码相关内容,应用可参考caffe源码分析–Blob应用
1.原理分析
Caffe使用Blob结构在CNN网络中存储、传递、修改数据。
对于批量2D图像数据,Blob的维度为: 图像数量N × 通道数C × 图像高度H × 图像宽度W
在此种场景下,Blob使用4维坐标定位数据,如(n, c, h, w),其中n为图像序号(0到N-1),c为通道序号(0到C-1),h为图像行序(0到H-1),w为图像列序(0到W-1)。那么我们如何根据这个坐标找到对应的数据呢?要想得到这个问题的答案,就得弄清楚Blob在内存中的数据组织形式,也就是这批量的2D图像在内存中是如何存储的。其实它的存储方式很简单,见下图:
图像数据依序存储,单张图像数据按通道序依次存储,组织形式简单明了。上图给了三张图像的存储例子,每张图像的通道数C为3,H为8,W为16。坐标(2, 1, 3, 9),代表这是第3张图像、第2个通道、第4行、第10列的像素值,实际存储位置为:
(((2 × C) + 1) × H + 3) × W + 9 = (((2 × 3) + 1) × 8 + 3) × 16 + 9 = 953
通俗地讲,在内存中,第一张图像的第0个像素值存储在内存的第0个位置,Blob按照从左到右,从上到下的顺序,逐列、逐行、逐通道、逐张图像,将每个像素值存入内存:
b. blob中数据的dimentions为Num Nchannel K * Height H * Width W.内存是行优先的(row-major)。访问数据的时候按照如下的规则来访问index(n,k,h,w) 在物理上位于index((nK + k) *H + h)*W + w. 这里要注意,index(n,k,h,w)实际上访问的是内存中(n+1,k+1,h+1,w+1)位置的数据,这是因为索引是从0开始的。
也就是说Blob的组织格式并无特别之处,顺序存储而已。坐标位置(n, c, h, w)与具体内存读取位置M的换算公式如下:
M = (((n × C) + c) × H + h) × W + w
blob数据结构,首先找到存储在内存中的数据,然后对数据进行修改操作,现在开始分析blob源码。
整理了一个关于Blob函数图,方便查看。
成员变量
在blob.hpp代码中定义
protected:
shared_ptr<SyncedMemory> data_; //存储前向传递权重数据
shared_ptr<SyncedMemory> diff_; //存储反向传播的梯度数据
shared_ptr<SyncedMemory> shape_data_;//shape_data_存储Blob的形状
vector<int> shape_; //shape_存储Blob的形状
int count_; //count_表示Blob中的元素个数,也就是个数*通道数*高度*宽度
int capacity_;//capacity表示当前的元素个数,因为Blob可能会reshape
shared_ptr为C++的智能指针,是C++11提供的一种智能指针类,使用智能指针,可以在任何地方都不使用时自动删除相关指针,解决内存泄漏和悬空指针的问题。
SyncedMemory是caffe中用来管理内存分配和CPU、GPU数据及同步的类,只服务于Blob类 ,可参考caffe源码分析–SyncedMemory 内存管理机制
*shape_data_*是利用智能指针的存储blob形状的数据结构,*shape_*是一种vector类的数据结构,用来存储blob的数据结构,blob数据结构为【图像数量N × 通道数C × 图像高度H × 图像宽度W】。
构造函数和Reshape函数
初始化shape_ 和shape_data_里的cpu_ptr
Blob() //构造函数:初始化列表 {空函数体}
: data_(), diff_(), count_(0), capacity_(0) {}
//当构造函数被声明 explicit 时,编译器将不使用它作为转换操作符。
explicit Blob(const int num, const int channels, const int height, const int width);
//可以通过设置数据维度(N,C,H,W)初始化
//const 传递过来的参数在函数内不可以改变(无意义,因为本身就是形参)
explicit Blob(const vector<int>& shape); //也可以通过传入vector<int>直接传入维数
根据shape来初始化shape_和shape_data_,以及为data_ 和diff_ 分配空间。
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);
具体实现:
template <typename Dtype>
void Blob<Dtype>::Reshape(const int num, const int channels, const int height,
const int width) {
vector<int> shape(4);
shape[0] = num;
shape[1] = channels;
shape[2] = height;
shape[3] = width;
Reshape(shape);
}
template <typename Dtype>
// 完成blob形状shape_的记录,大小count_的计算,合适大小capacity_存储的申请
void Blob<Dtype>::Reshape(const vector<int>& shape) {
CHECK_LE(shape.size(), kMaxBlobAxes);
count_ = 1;
shape_.resize(shape.size());
if (!shape_data_ || shape_data_->size() < shape.size() * sizeof(int)) {
shape_data_.reset(new SyncedMemory(shape.size() * sizeof(int)));
}
int* shape_data = static_cast<int*>(shape_data_->mutable_cpu_data());
for (int i = 0; i < shape.size(); ++i) {
CHECK_GE(shape[i], 0);
CHECK_LE(shape[i], INT_MAX / count_) << "blob size exceeds INT_MAX";
count_ *= shape[i];
shape_[i] = shape[i];
shape_data[i] = shape[i];
}
if (count_ > capacity_) {
capacity_ = count_;
data_.reset(new SyncedMemory(capacity_ * sizeof(Dtype)));
// 只是构造了SyncedMemory对象,并未真正分配内存和显存
diff_.reset(new SyncedMemory(capacity_ * sizeof(Dtype)));
// 真正分配是在第一次访问数据时
}
}
template <typename Dtype>
void Blob<Dtype>::Reshape(const BlobShape& shape) {
CHECK_LE(shape.dim_size(), kMaxBlobAxes);
vector<int> shape_vec(shape.dim_size());
for (int i = 0; i < shape.dim_size(); ++i) {
shape_vec[i] = shape.dim(i);
}
Reshape(shape_vec);
}
template <typename Dtype>
void Blob<Dtype>::ReshapeLike(const Blob<Dtype>& other) {
Reshape(other.shape());
}
template <typename Dtype>
Blob<Dtype>::Blob(const int num, const int channels, const int height,
const int width)
// capacity_ must be initialized before calling Reshape
: capacity_(0) {
Reshape(num, channels, height, width);
}
template <typename Dtype>
Blob<Dtype>::Blob(const vector<int>& shape)
// capacity_ must be initialized before calling Reshape
: capacity_(0) {
Reshape(shape);
}
构造函数就是先初始化capacity_,然后再调用Reshape函数,该函数的功能是改变blob的维,若新的存储空间变大,则重新分配存储空间。
成员变量设置函数
//打印数据维度,即blob的形状
inline string shape_string() const { //
ostringstream stream;
for (int i = 0; i < shape_.size(); ++i) {
stream << shape_[i] << " ";
}
stream << "(" << count_ << ")";
return stream.str();
}
inline const vector<int>& shape() const { return shape_; }//获取shape_
inline int shape(int index) const;//获取index维的大小
inline int num_axes() const { return shape_.size(); }//返回Blob维度数,对于维数(N,C,H,W),返回4
inline int count() const { return count_; }//返回Blob维度数,对于维数(N,C,H,W),返回N×C×H×W
inline int count(int start_axis, int end_axis) const;//获取某几维数据的大小
inline int count(int start_axis) const;//获取某一维到结束数据的大小
inline int CanonicalAxisIndex(int axis_index) const;//标准化索引,主要是对参数索引进行标准化,以满足要求
inline int LegacyShape(int index) const;//data_维数不大于4时才能使用,功能同shape()类似。
inline int offset(const int n, const int c = 0, \
const int h = 0,const int w = 0) const;
//计算物理偏移量,(n,c,h,w)的偏移量为((n∗C+c)∗H+h)∗W+w
inline int offset(const vector<int>& indices) const;//同上,只是参数不同
inline Dtype data_at(const int n, const int c, const int h,
const int w) const;//获取某位置的data_数据
inline Dtype data_at(const vector<int>& index);//同上
inline Dtype diff_at(const int n, const int c, const int h,
const int w) const;//获取某位置的diff_数据
inline Dtype diff_at(const vector<int>& index) const;//同上
inline const shared_ptr<SyncedMemory>& data() const;//获取data_
inline const shared_ptr<SyncedMemory>& diff() const;//获取diff_
const Dtype* cpu_data() const;//获取data_ cpu指针
const int* gpu_shape() const;//获取shape_data_的gpu指针
const Dtype* gpu_data() const;//获取data_的gpu指针
const Dtype* cpu_diff() const;//获取diff_的cpu指针
const Dtype* gpu_diff() const;//获取diff_的gpu指针
成员变量处理函数
void CopyFrom(const Blob<Dtype>& source, bool copy_diff =false,bool reshape = false);
/**功能:由source Blob拷贝到本Blob。
参数:source 源Blob
copy_diff 如果是false,拷贝data_,如果是true,拷贝diff_
reshape 如果是false,则需要源Blob与本Blob形状相同,如果是
**/
void set_cpu_data(Dtype* data);//设置data_的cpu指针,只是修改了指针
Dtype* mutable_cpu_data();//见SyncedMemory的mutable_cpu_data();
Dtype* mutable_gpu_data();//见SyncedMemory的mutable_gpu_data();
Dtype* mutable_cpu_diff();//见SyncedMemory的mutable_cpu_data();
Dtype* mutable_gpu_diff();//见SyncedMemory的mutable_gpu_data();
void Update();
/**
其中用到math_functions.hpp中的函数caffe_axpy(),该函数封装了cblas_saxpy,实现的是Y=alpha*X+Y。由此,知该函数的功能是data_=(data_-diff_)。
另外,该函数只实现了对double和float型数据,对于unsigned int和int由于该函数主要是在Net中被调用,只有Blob<float>和Blob<double>型式,因此没有定义unsigned int和int。
**/
void FromProto(const BlobProto& proto, bool reshape = true);
//由BlobProto对Blob进行赋值操作。reshape代表是否允许修改shape_的大小。需要注意的是再这里有double和float两种类型的数据 ,在代码中可以看到具体的体现
void ToProto(BlobProto* proto, bool write_diff = false) const;//针对double和float有两个实现的函数,这里只举例说明其中的一个。将Blob中的数据存入BlobProto中,write_diff表示是否存diff_。
Dtype asum_data() const;
/**
功能:计算L1范数
说明:其中用到了math_function.hpp中的函数caffe_cpu_asum()和caffe_gpu_asum,实现的功能是对向量X求其每个元素绝对值的和,不同的是X分别在cpu和gpu中。
**/
Dtype asum_diff() const;//同上,不同的是针对的是diff_
Dtype sumsq_data() const;
/**
功能:计算L2范数。
说明:用到了math_function.hpp中的caffe_cpu_dot(),caffe_cpu_strided_dot(),caffe_gpu_dot(), caffe_gpu_strided_dot()。具体就是就向量X的平方和。
**/
Dtype sumsq_diff() const;//同上,不同的是针对的是diff_。
void scale_data(Dtype scale_factor);
/**
功能:正规化data_。
说明:用到math_function.hpp中的caffe_scal()和caffe_gpu_scal()函数,我的理解就是对向量X乘上一个因子。
**/
void scale_diff(Dtype scale_factor);//同上,不同在于针对diff_。
void ShareData(const Blob& other);//本Blob共享other的data_
void ShareDiff(const Blob& other);//本Blob共享other的diff_
bool ShapeEquals(const BlobProto& other);//判断other与本Blob形状是否相同。
几个重要的函数实现如下:
Update()函数:更新单个参数的权值
template <typename Dtype>
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
// 参数更新,新参数(data_) = 原参数(data_) - 梯度(diff_)
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.";
}
}
asum_data() :计算data_的L1范数
template <typename Dtype>
Dtype Blob<Dtype>::asum_data() const {
if (!data_) { return 0; }
switch (data_->head()) {
case SyncedMemory::HEAD_AT_CPU:
return caffe_cpu_asum(count_, cpu_data());
case SyncedMemory::HEAD_AT_GPU:
case SyncedMemory::SYNCED:
#ifndef CPU_ONLY
{
Dtype asum;
caffe_gpu_asum(count_, gpu_data(), &asum);
return asum;
}
#else
NO_GPU;
#endif
case SyncedMemory::UNINITIALIZED:
return 0;
default:
LOG(FATAL) << "Unknown SyncedMemory head state: " << data_->head();
}
return 0;
}
sumsq_data():计算data_ L2范式
template <typename Dtype>
Dtype Blob<Dtype>::sumsq_data() const {
Dtype sumsq;
const Dtype* data;
if (!data_) { return 0; }
switch (data_->head()) {
case SyncedMemory::HEAD_AT_CPU:
data = cpu_data();
sumsq = caffe_cpu_dot(count_, data, data);
break;
case SyncedMemory::HEAD_AT_GPU:
case SyncedMemory::SYNCED:
#ifndef CPU_ONLY
data = gpu_data();
caffe_gpu_dot(count_, data, data, &sumsq);
#else
NO_GPU;
#endif
break;
case SyncedMemory::UNINITIALIZED:
return 0;
default:
LOG(FATAL) << "Unknown SyncedMemory head state: " << data_->head();
}
return sumsq;
}
scale_data(Dtype scale_factor): 对data_乘上某个因子
template <typename Dtype>
void Blob<Dtype>::scale_data(Dtype scale_factor) {
Dtype* data;
if (!data_) { return; }
switch (data_->head()) {
case SyncedMemory::HEAD_AT_CPU:
data = mutable_cpu_data();
caffe_scal(count_, scale_factor, data);
return;
case SyncedMemory::HEAD_AT_GPU:
case SyncedMemory::SYNCED:
#ifndef CPU_ONLY
data = mutable_gpu_data();
caffe_gpu_scal(count_, scale_factor, data);
return;
#else
NO_GPU;
#endif
case SyncedMemory::UNINITIALIZED:
return;
default:
LOG(FATAL) << "Unknown SyncedMemory head state: " << data_->head();
}
}
参考:
【1】 Caffe中的Blob数据结构