从零开始山寨Caffe·玖:BlobFlow
听说Google出了TensorFlow,那么Caffe应该叫什么?
——BlobFlow
神经网络时代的传播数据结构
我的代码
我最早手写神经网络的时候,Flow结构是这样的:
struct Data
{
vector<double> feature;
int y;
Data(vector<double> feature,int y):feature(feature),y(y) {}
};
vector<double> u_i,v_i,u_j,v_j;
很简陋的结构,主要功能就是利用vector存一下每层正向传播的值。
Word2Vec
后来我看了Google的Mikolov大神的Word2Vec的源码,它的Flow结构是这样的:
real *neu1 = (real *)calloc(doc->layer1_size, sizeof(real));
然后我吐槽了一下,这功能不是比我还弱么,vector起码还能提供STL的基础功能。
(注:Word2Vec源码是以CPU多线程和内存操作快而著称的,简陋但速度快)
Theano
再后来,我学习了Theano,它的Flow结构是这样的:
input=theano.tensor.matrix('x')
class DataLayer(object):
def __init__(self,input,batch_size,size):
self.batch_size=batch_size
self.size=size
self.input=input
self.params=None
def get_output(self):
output=self.input
if type(self.size) is tuple: #Mode: 2D
output=output.reshape((self.batch_size,self.size[2],self.size[1],self.size[0]))
else: #Mode: 1D
output=output.reshape((self.batch_size,self.size))
return output
Bengio组模仿物理学的张量(Tensor)的概念,创建了Theano的Tensor系统。
Dim为0的叫常量,Dim为1的叫向量,Dim=2的叫矩阵,Dim>2就没名字了,且Dim可以无限扩大。
Tensor的出现,很好地规避了机器学习研究者不会写代码的问题(比如上节出现的简陋结构)。
同时,随着mini-batch、conv等方法在深度学习中的大规模使用,我们的Flow结构显然需要多维化。
由于是操作多维空间,经常需要维度切换,reshape函数自然成了Tensor的核心函数。
(reshape的概念最早应该来自Python的科学计算库numpy,Theano的Tensor系统,很大程度上在重写numpy)
TensorFlow
再后来,Google把Andrew Ng开发的一代深度学习框架DistBelief给换掉了,第二代叫TensorFlow。
按照官方的说法,取名TensorFlow(2015)的原因是因为系统里主要是Tensor在Flow。
推测一下DistBelief(2011)和Theano(NIPS2012)的公布时间,我们大概推测,DistBelief的Flow结构估计相当Low。
按照Caffe(2013)作者贾大神的说法,他参与了TensorFlow的主体开发。
所以,TensorFlow里的Tensor结构,不难看出来,是借鉴了Theano(2012)和Caffe(2013)的综合体。
符号系统
尽管Caffe(2013)具有类似Tensor的Blob结构,但是和Theano(2012)、TensorFlow(2015)的Tensor相比,
还是比较弱的。核心原因是,Tensor的出发点是建立在符号系统上的,而Caffe(2013)只是最暴力的执行代码。
按照MXNet的陈天奇大神在MS研究院内部的讲座说法:
Caffe(2013)属于Imperative Programme(命令程序)
Theano(2012)、TensorFlow(2015)、MXNet(2015)属于Declaretive Programme(声明程序)
符号系统需要内建一套数学式语法解析结构,针对原始的命令语句做一个深度的Wrapper,从白盒变成黑盒。
其难度和代码量还是有的。与之相比,Blob读起来,还是要比Tensor要简单地多的。
浅析Blob设计原理
存储性质
无论是正向传播的输出,还是反向传播的残差,还是神经元参数,这些都需要不同的结构去存储。
Blob广义上极力规避设计多种结构的问题,这点上是参考Tensor的。
你可以自由规划1D、2D、3D、4D甚至nD的多维数组存储空间,这种存储具有相当不错的灵活性。
功能性质
不幸的是,操作多维数组在编程中是件麻烦事。
朴素C语言提供的多维数组,功能很弱,比如你想获知大小(size)就是一件难事。
使用STL是一个不错的注意,嵌套STL,从数据结构角度就变成了广义表。
尽管广义表的功能较朴素C语言多维数组要多,不过看起来也不尽如人意。
——————————————————————————————————————————————————
另外,最恼人的是CUDA不推荐GPU操作多维数组,最多可以申请到3维数组的显存优化。
如果不使用CUDA提供的多维数组内存对齐优化,那么IO指令取址将会非常频繁,导致IO速度严重退化。
从内存角度理解,显然线性内存空间访问便捷,nD内存空间就十分糟糕了。
——————————————————————————————————————————————————
从SyncedMemory的设计中,几乎就可以推测,Caffe为了速度,完全使用线性内存/显存。
因而,为使线性内存模拟出nD内存,就需要在内存访问上做点偏移(Offset)计算。
Blob的大部分功能,便是扩展线性SyncedMemory的逻辑功能,使其变成逻辑上的多维数组。
张量·轴设计
在早期神经网络编程中,通常采用的是1D空间,每个样本拥有一个输入向量。
上个世纪末,LeCun等人倡导在SGD中,替代单样本为mini-batch,才使得轴设计得以派上用场。
axis=0用于batch_size,batch中每个样本的向量移到axis=1。
这种空间在今天的神经网络NLP(NNNLP)任务中,仍然是主要采用的。
上个世纪90年代初,LeCun将Fukushima的神经机结合导师Hinton的BP算法,演化成可以训练的CNN,使得轴进一步扩展。
CNN所扩展的轴,称之为空间轴(spatial axes),放置于axis=2,....之后。
原神经网络的axis=1轴,结合图像文件的通道(channels)概念、CNN的特征图概念,被替换成channels axis。
这样,在Blob中,就构成了使用最频繁的4轴空间(batch_size,channels,height,width)。
在Caffe中,batch_size用num替代,这个名字理解起来更泛性一点。
各轴都具有一定的轴长,描述轴空间需要shape功能,轴空间变形则需要reshape功能。
代码实战
从Blob开始,为了便于阅读,代码将在不同章逐步扩展,以下仅提供适用于本章的精简代码。
完整代码见本章最后的Github链接。
建立blob.hpp
数据结构
template <typename Dtype>
class Blob{
public:
Blob():data_(),diff_(),count_(), capacity_() {}
Blob(const vector<int>& shape) :count_(),capacity_() { reshape(shape); }
void reshape(int num, int channels, int height, int width);
void reshape(vector<int> shape);
void reshape(const BlobShape& blob_shape);
void reshapeLike(const Blob& blob);
const Dtype* cpu_data() const;
const Dtype *gpu_data() const;
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();
int num() const { return shape(); }
int channels() const { return shape(); }
int height() const { return shape(); }
int width() const { return shape(); }
int count() const{ return count_; }
int count(int start_axis, int end_axis) const {
CHECK_GE(start_axis, );
CHECK_LE(start_axis, end_axis);
CHECK_LE(start_axis, num_axes());
CHECK_LE(end_axis, num_axes());
int cnt = ;
for (int i = start_axis; i < end_axis; i++) cnt *= shape(i);
return cnt;
}
int count(int start_axis) const{ return count(start_axis, num_axes()); }
const vector<int> &shape() const{ return shape_; }
int shape(int axis) const{ return shape_[canonicalAxisIndex(axis)]; }
int offset(const int n, const int c = , const int h = ,
const int w = ){
CHECK_GE(n, );
CHECK_LE(n, num());
CHECK_GE(channels(), );
CHECK_LE(c, channels());
CHECK_GE(height(), );
CHECK_LE(h, height());
CHECK_GE(width(), );
CHECK_LE(w, width());
return ((n * channels() + c) * height() + h) * width() + w;
}
int num_axes() const { return shape_.size(); }
// idx ranges [-axes,axes)
// idx(-1) means the last axis
int canonicalAxisIndex(int axis) const{
CHECK_GE(axis, -num_axes());
CHECK_LT(axis, num_axes());
if (axis < ) return axis + num_axes();
else return axis;
}
const boost::shared_ptr<SyncedMemory>& data() const { return data_; }
const boost::shared_ptr<SyncedMemory>& diff() const { return diff_; }
// change the shared_ptr object and will recycle the memory if need
void shareData(const Blob& blob) {
CHECK_EQ(count(), blob.count());
data_ = blob.data();
}
void shareDiff(const Blob& blob) {
CHECK_EQ(count(), blob.count());
diff_ = blob.diff();
}void FromProto(const BlobProto& proto, bool need_reshape = true);
void ToProto(BlobProto* proto, bool write_diff = false);
protected:
boost::shared_ptr<SyncedMemory> data_, diff_;
vector<int> shape_;
int count_, capacity_;
};
先说说几个成员变量:
count、capacity用于reshape中的计算,前者是新reshape的大小,后者是历史reshape大小。
Blob的任何构造函数中,一定要将这个两个值置0,否则reshape会失败。
线性内存空间以shared_ptr绑定,因此Blob不需要析构函数,Blob销毁后,指针空间会被自动回收。
默认有2个线性内存空间,data、diff,分别用于存储数据/残差。
vector<int> shape用于存各个轴的轴长。
——————————————————————————————————————————————————
然后看轴相关函数:
num、channels、height、width、count、shape都是简单的封装,注意设成常成员函数。
由于Blob会作为const引用的参数,比如sharedData/shareDiff,这些访问接口必须保证this指针一致。
这点在第壹章时,略微提醒过。
count和shape都是重载函数,提供不同的访问方式。
轴访问canonicalAxisIndex函数上,借鉴了Python的负轴访问方式,如果你没有Python的习惯,可以写简单点。
——————————————————————————————————————————————————
对SyncedMemory的封装,主要目的是将void*型内存转换为计算类型的内存。
void*型内存以数组下标方式访问时,每个单元占用8Bit(1字节),这种单元内存是不能直接使用的。
因为一个int/float单元占用32Bit(4字节),一个double单元占用64Bit(8字节)。
C/C++通过对数组首元素指针的强制转换,可以改变下标索引的单元访问模式。
——————————————————————————————————————————————————
reshape函数看起来重载了很多,实际上主体设在 void reshape(vector<int> shape)里。
其它都是简单的封装。
——————————————————————————————————————————————————
offset函数是非常重要的,它目的是计算相对偏移量,形成逻辑上的多维空间结构。
在DataLayer中,由Datum组织Blob一个例子如下:
for (int i = ; i < batch_size; i++){
// must refer use '&' to keep data vaild(!!!important)
Datum &datum = *(reader.full().pop("Waiting for Datum data"));
int offset = batch->data.offset(i);
// share a part of a blob memory
transformed_data.set_cpu_data(base_data + offset);
// transform datum and copy its value to the part of blob memory
if (has_labels) base_label[i] = datum.label();
ptr_transformer->transform(datum, &transformed_data);
//let the reader to read new datum
reader.free().push(&datum);
}
在这里,对batch里的每一个样本,每次偏移channels*height*width个单位,立刻跳转到下一张图的首元素。
更一般的,令base_data+=data.offset(0,1),就跳转到了下一个channel的首元素。
由于线性空间是连续的,这种偏移仅仅需要加法器一次运算,就能模拟出多维空间,十分廉价。
——————————————————————————————————————————————————
两个share函数用于直接替换掉data_,diff_,由于使用了shared_ptr,SyncedMemory会自动释放。
当神经网络需要交叉验证时,从训练网络copy参数到测试网络是没有必要的。
此时,只要将训练网络的全部参数Blob,一一对应share给测试网络即可。
——————————————————————————————————————————————————
FromProto和ToProto用于反序列化/序列化至protobuff格式。
唯一用处是对神经网络的参数Blob进行snapshot(截图),以便继续训练或者离线测试。
实现
给出几个比较重要的实现。
template<typename Dtype>
void Blob<Dtype>::reshape(vector<int> shape){
count_ = ;
shape_.resize(shape.size());
for (int i = ; i < shape.size(); ++i) {
count_ *= shape[i];
shape_[i] = shape[i];
}
if (count_ > capacity_) {
capacity_ = count_;
data_.reset(new SyncedMemory(capacity_ * sizeof(Dtype)));
diff_.reset(new SyncedMemory(capacity_ * sizeof(Dtype)));
}
}
可以看到,reshape为SyncedMemory准备了capacity*sizeof(Dtype)个字节单元。
同时,你需要回忆一下,SyncedMemory(size)并不会立刻启动状态转移自动机申请内存/显存。
只有执行Blob:: cpu_data/gpu_data/mutable_cpu_data/mutable_gpu_data,才会申请。
这有点像函数式编程里的Lazy思想,胡乱写Blob其实问题不大,只要该Blob没有使用,就不会有内存空间损耗。
template<typename Dtype>
void Blob<Dtype>::ToProto(BlobProto* proto, bool write_diff){
proto->clear_shape();
proto->clear_data();
proto->clear_diff();
//do not use proto->shape() cause it is a const method
for (int i = ; i < shape_.size(); i++) proto->mutable_shape()->add_dim(shape_[i]);
const Dtype *data = cpu_data();
const Dtype *diff = cpu_diff();
for (int i = ; i < count_; i++) proto->add_data(data[i]);
if (write_diff)
for (int i = ; i < count_; i++) proto->add_diff(diff[i]);
}
ToProto里,首次出现了如何向protobuff结构写数据的例子。
以proto->mutable_shape()为例,切记不要写成proto->shape(),因为proto->shape()是常成员函数。
其内部不能修改,这点上,同Blob::cpu_data/mutable_cpu_data的原理是一致的。
对于message的repeated类型,使用add_name函数可以填充数组数据。
针对Caffe的精简
- 移除SyncedMemory形式的shape_data,与vector<int> shape_作用重复
- 移除基本没什么用的CopyFrom函数
完整代码
注:关于Blob中的update等在底层计算的函数会在后期补充讲解。
blob.hpp
https://github.com/neopenx/Dragon/blob/master/Dragon/include/blob.hpp
https://github.com/neopenx/Dragon/blob/master/Dragon/src/blob.cpp
从零开始山寨Caffe·玖:BlobFlow的更多相关文章
- 从零开始山寨Caffe·陆:IO系统(一)
你说你学过操作系统这门课?写个无Bug的生产者和消费者模型试试! ——你真的学好了操作系统这门课嘛? 在第壹章,展示过这样图: 其中,左半部分构成了新版Caffe最恼人.最庞大的IO系统. 也是历来最 ...
- 从零开始山寨Caffe·零:必先利其器
工作环境 巧妇有了米炊 众所周知,Caffe是在Linux下写的,所以长久以来,大家都认为跑Caffe,先装Linux. niuzhiheng大神发起了caffe-windows项目(解决了一些编译. ...
- 从零开始山寨Caffe·拾贰:IO系统(四)
消费者 回忆:生产者提供产品的接口 在第捌章,IO系统(二)中,生产者DataReader提供了外部消费接口: class DataReader { public: ......... Blockin ...
- 从零开始山寨Caffe·壹:仰望星空与脚踏实地
请以“仰望星空与脚踏实地”作为题目,写一篇不少于800字的文章.除诗歌外,文体不限. ——2010·北京卷 仰望星空 规范性 Caffe诞生于12年末,如果偏要形容一下这个框架,可以用"须敬 ...
- 从零开始山寨Caffe·拾:IO系统(三)
数据变形 IO(二)中,我们已经将原始数据缓冲至Datum,Datum又存入了生产者缓冲区,不过,这离消费,还早得很呢. 在消费(使用)之前,最重要的一步,就是数据变形. ImageNet Image ...
- 从零开始山寨Caffe·捌:IO系统(二)
生产者 双缓冲组与信号量机制 在第陆章中提到了,如何模拟,以及取代根本不存的Q.full()函数. 其本质是:除了为生产者提供一个成品缓冲队列,还提供一个零件缓冲队列. 当我们从外部给定了固定容量的零 ...
- 从零开始山寨Caffe·柒:KV数据库
你说你会关系数据库?你说你会Hadoop? 忘掉它们吧,我们既不需要网络支持,也不需要复杂关系模式,只要读写够快就行. ——论数据存储的本质 浅析数据库技术 内存数据库——STL的map容器 关 ...
- 从零开始山寨Caffe·伍:Protocol Buffer简易指南
你为Class外访问private对象而苦恼嘛?你为设计序列化格式而头疼嘛? ——欢迎体验Google Protocol Buffer 面向对象之封装性 历史遗留问题 面向对象中最矛盾的一个特性,就是 ...
- 从零开始山寨Caffe·贰:主存模型
你左手是内存,右手是显存,内存可以打死显存,显存也可以打死内存. —— 请协调好你的主存 从硬件说起 物理之觞 大部分Caffe源码解读都喜欢跳过这部分,我不知道他们是什么心态,因为这恰恰是最重要的一 ...
随机推荐
- linux 下安装mongodb
1.初始化docker: -v 设置docker和host共享目录,格式hostPath:dockerContainerPath -p 端口映射 --name,容器名称 cen ...
- 服务器通过微信公众号Token验证测试的代码(Python版)
我在阿里云租了一个云服务器,然后想把这个作为我的微信公众号的后台,启用微信公众号开发者需要正确的响应微信服务器的Token验证,为此把这个验证的Python代码贴出来,只要在服务器上运行这段代码,注意 ...
- libvirt 安装篇
1. 环境:Centos 7 python 2.7 2. 安装 sudo yum install gcc python-devel libvirt libvirt-devel sudo ...
- bootstrap的table调用本列ID
我们是用json解析数据. 后台传送data数据~ String data = JSON.toJSONString(baseInfoService.list());request.setAttribu ...
- Git 常用命令详解
Git 是一个很强大的分布式版本管理工具,它不但适用于管理大型开源软件的源代码(如:linux kernel),管理私人的文档和源代码也有很多优势(如:wsi-lgame-pro) Git 的更多介绍 ...
- java 多态和内部类
接口跟接口之间存在继承关系 一个接口可以继承多个接口 一个非抽象类:必须实现接口中的所有方法 一个抽象类实现接口 可以不实现接口中的方法 但是继承与抽象类的类必须要是实现接口中的方法 多态:一个对 ...
- JS判断有无网络(移动端)
ar EventUtil = { addHandler: function (element, type, handler) { if (element.addEventListener) { ele ...
- *** wechat-php-sdk 微信公众平台php开发包
wechat-php-sdk 微信公众平台php开发包,细化各项接口操作,支持链式调用,欢迎Fork此项目weixin developer SDK. 项目地址:https://github.com/d ...
- Android Studio一些常用快捷键及快捷键冲突解决
1. 最近在自学Android,也是边看书边写一些Demo,由于知识点越来越多,脑子越来越记不清楚,所以打算写成读书笔记,供以后查看,也算是把自己学到所理解的东西写出来,献丑,如有不对的地方,希望大家 ...
- ExtJS基础知识总结:常用控件使用方式(一)
概述 最近一直在做相关ExtJs方面的项目,遇到了ExtJs使用方面的一系列问题,现在将使用技巧做个记录汇总,以便于下次能够快速使用.以下都是ExtJs控件的常用方法,做简单汇总,俗话说,好记星不如烂 ...