從零開始山寨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