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


    你为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专攻编译器优化,被誉为是地球上让代码跑的最快的男人。

    使用方法

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

    确保3rdpartyin在环境变量中,编辑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

  • 相关阅读:
    053592
    053591
    053590
    053589
    053588
    053676
    C# WPF Border控件总结
    Android Studio 添加jar或aar依赖的两种方式
    javascript Date与string之间的转换
    C#:使用dsoframer.ocx控件实现内嵌office效果(详解)
  • 原文地址:https://www.cnblogs.com/neopenx/p/5243188.html
Copyright © 2020-2023  润新知