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

                            ——欢迎体验Google Protocol Buffer

面向对象之封装性

历史遗留问题

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

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

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=;
optional int32 height=;
optional int32 width=;
optional int32 label=;
optional bytes data=;
repeated float float_data=;
optional bool encoded= [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= [packed=true];
} message BlobProto{
optional BlobShape shape=;
repeated float data=;
repeated float diff=;
repeated double double_data=;
repeated double double_diff=;
}

BlobShape用于存储Blob Shape信息。

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

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

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

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

完整代码

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

从零开始山寨Caffe·伍:Protocol Buffer简易指南的更多相关文章

  1. protocol buffer开发指南(官方)

    欢迎来到protocol buffer的开发者指南文档,一种语言无关.平台无关.扩展性好的用于通信协议.数据存储的结构化数据序列化方法. 本文档是面向计划将protocol buffer使用的到自己的 ...

  2. 【转】protocol buffer开发指南

    这个作者的其它golang的文章也值得一读 原文:https://www.cnblogs.com/charlieroro/p/9011900.html protocol buffer开发指南 ---- ...

  3. 从零开始山寨Caffe·壹:仰望星空与脚踏实地

    请以“仰望星空与脚踏实地”作为题目,写一篇不少于800字的文章.除诗歌外,文体不限. ——2010·北京卷 仰望星空 规范性 Caffe诞生于12年末,如果偏要形容一下这个框架,可以用"须敬 ...

  4. 从零开始山寨Caffe·拾:IO系统(三)

    数据变形 IO(二)中,我们已经将原始数据缓冲至Datum,Datum又存入了生产者缓冲区,不过,这离消费,还早得很呢. 在消费(使用)之前,最重要的一步,就是数据变形. ImageNet Image ...

  5. 从零开始山寨Caffe·捌:IO系统(二)

    生产者 双缓冲组与信号量机制 在第陆章中提到了,如何模拟,以及取代根本不存的Q.full()函数. 其本质是:除了为生产者提供一个成品缓冲队列,还提供一个零件缓冲队列. 当我们从外部给定了固定容量的零 ...

  6. 从零开始山寨Caffe·柒:KV数据库

    你说你会关系数据库?你说你会Hadoop? 忘掉它们吧,我们既不需要网络支持,也不需要复杂关系模式,只要读写够快就行.    ——论数据存储的本质 浅析数据库技术 内存数据库——STL的map容器 关 ...

  7. 从零开始山寨Caffe·陆:IO系统(一)

    你说你学过操作系统这门课?写个无Bug的生产者和消费者模型试试! ——你真的学好了操作系统这门课嘛? 在第壹章,展示过这样图: 其中,左半部分构成了新版Caffe最恼人.最庞大的IO系统. 也是历来最 ...

  8. 从零开始山寨Caffe·零:必先利其器

    工作环境 巧妇有了米炊 众所周知,Caffe是在Linux下写的,所以长久以来,大家都认为跑Caffe,先装Linux. niuzhiheng大神发起了caffe-windows项目(解决了一些编译. ...

  9. protocol buffer开发指南

    ProtoBuf 是一套接口描述语言(IDL)和相关工具集(主要是 protoc,基于 C++ 实现),类似 Apache 的 Thrift).用户写好 .proto 描述文件,之后使用 protoc ...

随机推荐

  1. 菜鸟笔记:java变量命名及峰驼式命名法

    如同酒店会给每个房间起个性化的名字一样,程序中的变量也需要用合理的名字进行管理---变量名! 需要注意,给酒店房间起名字时可以是数字,如"802",也可以是有趣的名字,如" ...

  2. java中面向对象的一些知识(二)

    一. 封装的讲解 什么是封装?为什么要封装?怎么实现封装? 封装的目的是为了提高程序的安全性.封装就是把不想让第三者看的属性,方法隐藏起来. 封装的实现方法是: 1.修改属性的可见性,限制访问. 2. ...

  3. linux Mint 安装tomcat8

    先安装jdk,由于我这以安装jdk这里就不做详细描述: 到官网下载和自己jdk对应版本的tomcat包(tomcat.apache.org) 解压tomcat包到/opt/tomcat8下 tar - ...

  4. HTML DOM Event 对象

    var event;if (document.createEvent){event = document.createEvent("HTMLEvents");event.initE ...

  5. Requests 乱码

    当使用Requests请求网页时,出现下面图片中的一些乱码,我就一脸蒙逼. 程序是这样的. def getLinks(articleUrl): headers = { "Uset-Agent ...

  6. getAttribute、setAttribute、removeAttribute

    1.函数语法 elementNode.attributes:属性返回包含被选节点属性的 NamedNodeMap. elementNode.getAttribute(name):方法通过名称获取属性的 ...

  7. 解决上一篇jquery中on的疑惑

    内容都是来自:http://www.365mini.com/page/jquery-on.htm.这里做一下收藏.文章的最后  疑问和解答可以解决所有的疑惑  看了之后能更好的整篇文章. on()函数 ...

  8. wget 扒站

    在Linux下,通过一个命令就可以把整个站相关的文件全部下载下来. wget -r -p -k -np [网址] 参数说明: -r : 递归下载 -p : 下载所有用于显示 HTML 页面的图片之类的 ...

  9. Linux 昨天时间

    今天date +%F昨天date -d yesterday +%F明天date -d tomorrow +%F七天前date -d "7 days ago" +%F

  10. 模拟搭建Web项目的真实运行环境(四)

    本篇介绍如何部署mongodb环境,主要分为三个部分: 第一部分 介绍如何在ubuntu下安装mongodb, 第二部分 介绍如何在windows下安装使用MongoChef客户端, 第三部分 介绍在 ...