从零开始山寨Caffe·伍:Protocol Buffer简易指南

你为Class外访问private对象而苦恼嘛?你为设计序列化格式而头疼嘛?git

                            ——欢迎体验Google Protocol Buffer程序员

面向对象之封装性

历史遗留问题

面向对象中最矛盾的一个特性,就是“封装性”。github

在上古时期,大牛们无聊地设计了三种访问域:编程

public、private、protected。数组

大多数C++初学者都是疑惑的,甚至是对于传统C程序员而言。安全

在C规范中,没有class(类)的概念,只有struct(结构体)的概念。数据结构

面向对象的C++中,尽管将C规范的struct移植过来了,可是这个struct是至关特殊的。机器学习

C++中的struct,和class没有多大区别,可继承/封装/多态,也支持public/private/protected。编辑器

它只有一点不一样,那就是默认访问域是public,该设计仅仅是为了兼顾熟悉C规范的程序员。函数

C规范里之因此没有public/private/protected,由于它不是面向对象语言,没有必要听从OO的封装性。

若是偏要让C规范服从面向对象,那么一切皆是public,这是C++中struct存在的意义。

编程规范

第壹章讲到了Google程序员必须听从的代码可读标准,该标准主要体如今对变量的访问上。

对于一次变量访问行为,它是常(const)访问,仍是修改(mutable)访问,这显然是两种行为。

因为变量只有一个,但访问方式却有两种,因而软件工程大师们认为,面向对象的访问要以函数为载体。

这就产生了一种面向对象封装性编程规范:

一切成员变量皆private,一切访问方法皆public。

中间还有一个protected。protected的含义在不一样语言里是不一样的(C++与Java就不一样)。

在C++中,甚至在Caffe中,咱们更鼓励使用protected替代private。

具体来说,protected既包含private对外部访问的屏蔽,又包含对继承类的开放。

Caffe中普遍使用继承类设计,而private成员变量是不会被继承的。

想象一下,Layer定义了参数W,可是继承Layer的ConvLayer竟然用不了参数W,这不是反人类么?

让咱们来考虑一下代码量,设变量A在C规范中,声明与定义占用一行,

那么在C++规范中,声明与定义占一行,const访问至少占一行(平均3行),mutable访问至少占一行(平均3行)。

这样,为了这个装逼的封装性,咱们的代码量平均要上去5倍左右。尤为是在机器学习系统中,大量数据结构的状况下,

源码中将会充斥着大量这类无聊的get(const访问)函数,set(mutable访问)函数,不得不说,是挺无奈的事。

序列化

文本数据与序列化

喜欢玩游戏的,应该都改过相似于config.ini的文件。

好比我手里的《辐射4》根目录下的Ultra.ini,就提供了编辑显示配置的高级方式。

大部分Application Framework都提供了对INI文件的解析(Parse)。

其实这并非难事,学过《编译原理》的人,应该都作过词法分析器的实验。

编译器的词法分析,论本质,它其实也是人工智能(AI),只不过它的智能必须基于特定规则。

归根结底,仍是没有超出冯诺依曼的存储程序智能范畴,离图灵的无敌图灵机还远得很。

解析平面结构的文本是简单的,如图,INI文件只由域[XXX],和域下配置项组成。

若是是层次结构呢,好比XML?固然XML有其专门的语法树。

XML语法至关冗繁,看起来就像是机器写的(实际上大部分XML真是机器写的)。

在一个机器学习系统中,显然咱们须要层次数据结构的配置。

好比Caffe中经典的层次结构:

solver{

  net{

    layer{

      blob{

考虑一个更特殊的状况,solver配置和net配置显然须要写在不一样文件里,加强迁移性。

XML解析器显然没有这么高级的功能,可以整合多个XML文件。

这样,XML解析器之上,起码还须要二次编程,至关坑爹。

格式化数据与序列化

何为格式化数据?简而言之,就是:

C++写的东西,Python能用,MATLAB也能用。

目前普遍使用的格式化数据主要有两种,Binary(C++、Python)、HDF5(MATLAB)。

你确定会问,ACM比赛不都是用文本格式存数据,为何不用文本格式作格式化数据?

答案其实很无语:文本格式的体积要比二进制格式体积大5倍左右,读取速度也要相应慢上几倍。

因此,一个机器学习系统,能够从文本IN数据,可是千万不要尝试将数据OUT成文本格式。

文本格式除了体积问题,还存在安全性问题。文本型数据很容易被逆向破解掉。

相反,二进制等格式易于作位运算的特色,很是适合,且基本支持二进制序列化的API,

都对二进制数据进行了加密(好比Qt的QDataStream),固然安全性不是咱们考虑的重点。

 

二进制虽然体积小,可是须要人工设计封装格式。这给序列化(编码),反序列(解码),带来麻烦。

在传统C++大型程序中,咱们都能看到序列化和反序列化代码至关冗长。

程序员写到最后,都不知道本身到底IN进了什么数据,OUT出了什么数据,代码显得十分笨拙。

尤为是在机器学习系统中,考虑到咱们须要将参数W保存到硬盘。

首先,参数W有多少个?是什么格式?顺序是什么?这些都要先记录。

记录完了以后,才能将最宝贵的参数W写到文件,是否是很蠢,很蠢,很蠢?

Google Protocol Buffer

不错的工具

Protocol Buffer是由Jeff Dean领衔开发的神奇工具。

它不只有着很是不错的格式化数据的序列化/反序列速度,同时也支持文本格式。

更重要的是,它在自动生成序列化格式的同时,也封装了部分变量的访问接口。

使得Caffe的总体源码中,没必要充斥着大量的get/set。

最后,Jeff Dean出品,速度必然是有保障的。

这位Google首席技术员,PHD专攻编译器优化,被誉为是地球上让代码跑的最快的男人。

使用方法

这玩意在墙外,在第零章提供的包里,3rdparty\bin下protoc.exe就是在Windows下本体。

确保3rdparty\bin在环境变量中,编辑proto-make.cmd脚本:

@echo off
set SRC_DIR=C:\PROTO
set DST_DIR=C:\PROTO
set PROTO_NAME=dragon
echo Check Source Proto Path:  %SRC_DIR%
echo Check Destination Proto Path:  %DST_DIR%
echo Check Proto Files Name :  %PROTO_NAME%.proto
echo ——————————————————————————————————
echo Protocol Buffer:Compliing for dragon.proto.....
start protoc -I=%SRC_DIR% --cpp_out=%DST_DIR% %SRC_DIR%\%PROTO_NAME%.proto
echo Protocol Buffer:Compliing complete!
pause

SRC_DIR为proto脚本的源路径,DST_DIR为生成路径。

proto脚本是操纵protoc.exe的惟一方式,Google为proto脚本设计了一种新的语言,很是相似于C/C++。

protoc版本会根据proto脚本生成h和cc文件,分别是数据结构的声明和定义,随时能够嵌入到你的代码中。

protoc的命令参数摘自墙外的官网,咱们一般只须要设置源目录、目标目录、以及proto脚本路径:

protoc -I=%SRC_DIR% --cpp_out=%DST_DIR% %SRC_DIR%\%PROTO_NAME%.proto

第一步

在你喜欢的源目录下,新建dragon.proto,用文本编辑器打开它,

定义第一个数据结构Datum:

message Datum{
    optional int32 channels=1;
    optional int32 height=2;
    optional int32 width=3;
    optional int32 label=4;
    optional bytes data=5;
    repeated float float_data=6;
    optional bool encoded=7 [default=false];
}

Datum算是最基本的存储单元了,它其实表示的就是一张图像。

proto语言与C语言差异不是很大,结构体struct字段换成message,

变量以前须要追加optional和repeated标记字段。分别表示的是单变量,仍是容器数组变量。

值得一提的是,proto提供requireed字段,可是Google程序员都懒得用,常常会出现奇怪bug,

因此一概用optional替代requireed。

repeated标记以后,本质是数组,但实际实现多是相似于STL容器,它提供了很多相似容器的操做。

[default]能够提供默认值,对于基本数据类型,不设默认值将会同C语言同样产生相似默认值。

但咱们不推荐使用proto自身提供的默认值,一般会以前接一个has_xxx(),来检测该变量是否被设置。

人工指定的默认值,has_xxx()会返回true,而proto提供的自动默认值,则是false。

另外,对于repeated int32 or int64,使用[packed=true]彷佛能够优化速度,对于float实际上是无效的。

Caffe里有些repeat float也打上了[packed=true],其实没什么意义。

最后,全部数据结构变量,都须要一个惟一的id,id从1开始。

这与proto内部编码系统有关,1~20编码长度小,访问速度快。随着id值增长,后续变量访问速度会递减。

 

再看Datum自己,channels、height、width都是咱们熟悉的。

data和float_data的区别在于,前者用于uint8数据,好比MNIST和cifar10/100,

它们的像素值能够被压缩为一个字符串,而bytes类型在C++里,刚好就是string类型。

float_data则用于存储散装的float值了。

最后的encoded能够被忽略,我还没见过什么图像须要编码的。

Caffe须要OpenCV,主要是因为考虑到图像须要解码,省略这一步,OpenCV能够无视掉。

第二步

咱们还须要为Blob提供一个序列化容器,用于存储训练参数。

message BlobShape{
    repeated int64 dim=1 [packed=true];
}

message BlobProto{
    optional BlobShape shape=1;
    repeated float data=2;
    repeated float diff=3;
    repeated double double_data=4;
    repeated double double_diff=5;
}

BlobShape用于存储Blob Shape信息。

BlobProto才是咱们须要关注的,除了shape,它由四个容器数组组成。

大部分状况下,咱们只会使用其中两个。

由于只有Tesla系列显卡,才支持double运算,而GTX玩家显卡,只能使用float运算。

data用于存储参数数据,diff用于存储残差,实际上diff基本是不会用的,记录参数的残差没有多少意义。

完整代码

见:https://github.com/neopenx/Dragon/blob/master/proto/dragon.proto

相关文章
相关标签/搜索