1. 程式人生 > >從零開始山寨Caffe·玖:BlobFlow

從零開始山寨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_(0), capacity_(0) {}
    Blob(const vector<int>& shape) :count_(0),capacity_(0) { 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(0); } int channels() const { return shape(1); } int height() const { return shape(2); } int width() const { return shape(3); } int count() const{ return count_; } int count(int start_axis, int end_axis) const { CHECK_GE(start_axis, 0); CHECK_LE(start_axis, end_axis); CHECK_LE(start_axis, num_axes()); CHECK_LE(end_axis, num_axes()); int cnt = 1; 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 = 0, const int h = 0, const int w = 0){ CHECK_GE(n, 0); CHECK_LE(n, num()); CHECK_GE(channels(), 0); CHECK_LE(c, channels()); CHECK_GE(height(), 0); CHECK_LE(h, height()); CHECK_GE(width(), 0); 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 < 0) 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 = 0; 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_ = 1;
    shape_.resize(shape.size());
    for (int i = 0; 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 = 0; i < shape_.size(); i++)  proto->mutable_shape()->add_dim(shape_[i]);
    const Dtype *data = cpu_data();
    const Dtype *diff = cpu_diff();
    for (int i = 0; i < count_; i++)  proto->add_data(data[i]);
    if (write_diff)
        for (int i = 0; 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

相關推薦

開始山寨Caffe·BlobFlow

聽說Google出了TensorFlow,那麼Caffe應該叫什麼?                           ——BlobFlow 神經網路時代的傳播資料結構 我的程式碼 我最早手寫神經網路的時候,Flow結構是這樣的: struct Data { vector<d

開始山寨Caffe·貳主存模型

本文轉自:https://www.cnblogs.com/neopenx/p/5190282.html 從硬體說起 物理之觴 大部分Caffe原始碼解讀都喜歡跳過這部分,我不知道他們是什麼心態,因為這恰恰是最重要的一部分。 記憶體的管理不擅,不僅會導致程式的立即崩潰,還會導致記憶體的

開始山寨Caffe·柒KV資料庫

你說你會關係資料庫?你說你會Hadoop? 忘掉它們吧,我們既不需要網路支援,也不需要複雜關係模式,只要讀寫夠快就行。                                         ——論資料儲存的本質 淺析資料庫技術 記憶體資料庫——STL的map容器 關係資料庫橫行已久,似乎大

開始山寨Caffe·陸IO系統(一)

你說你學過作業系統這門課?寫個無Bug的生產者和消費者模型試試!                               ——你真的學好了作業系統這門課嘛? 在第壹章,展示過這樣圖: 其中,左半部分構成了新版Caffe最惱人、最龐大的IO系統。 也是歷來最不重視的一部分。 第伍章又對左半

開始山寨Caffe·捌IO系統(二)

生產者 雙緩衝組與訊號量機制 在第陸章中提到了,如何模擬,以及取代根本不存的Q.full()函式。 其本質是:除了為生產者提供一個成品緩衝佇列,還提供一個零件緩衝佇列。 當我們從外部給定了固定容量的零件之後,生產者的產能就受到了限制。 由兩個阻塞佇列組成的QueuePair,並不是Caffe的獨創,

開始山寨Caffe·伍Protocol Buffer簡易指南

你為Class外訪問private物件而苦惱嘛?你為設計序列化格式而頭疼嘛?                             ——歡迎體驗Google Protocol Buffer 面向物件之封裝性 歷史遺留問題 面向物件中最矛盾的一個特性,就是“封裝性”。 在上古時期,大牛們無聊地設計了

開始山寨Caffe·拾IO系統(三)

資料變形 IO(二)中,我們已經將原始資料緩衝至Datum,Datum又存入了生產者緩衝區,不過,這離消費,還早得很呢。 在消費(使用)之前,最重要的一步,就是資料變形。 ImageNet ImageNet提供的資料相當Raw,不僅影象尺寸不一,ROI焦點內容比例也不一,如圖: [Krizhev

開始山寨Caffe·拾貳IO系統(四)

消費者 回憶:生產者提供產品的介面 在第捌章,IO系統(二)中,生產者DataReader提供了外部消費介面: class DataReader { public: ......... BlockingQueue<Datum*>& free() const

開始caffe(七)利用GoogleNet實現影象識別

一、準備模型 在這裡,我們利用已經訓練好的Googlenet進行物體影象的識別,進入Googlenet的GitHub地址,進入models資料夾,選擇Googlenet 點選Googlenet的模型下載地址下載該模型到電腦中。 模型結構 在這裡,我們利用之前講

開始caffe(十)caffe中snashop的使用

在caffe的訓練期間,我們有時候會遇到一些不可控的以外導致訓練停止(如停電、裝置故障燈),我們就不得不重新開始訓練,這對於一些大型專案而言是非常致命的。在這裡,我們介紹一些caffe中的snashop。利用snashop我們就可以實現訓練的繼續進行。 在之前我們訓練得到的檔案中,我們發現

開始caffe(九)在Windows下實現影象識別

本系列文章主要介紹了在win10系統下caffe的安裝編譯,運用CPU和GPU完成簡單的小專案,文章之間具有一定延續性。 step1:準備資料集 資料集是進行深度學習的第一步,在這裡我們從以下五個連結中下載所需要的資料集: animal flower plane hou

開始caffe(八)Caffe在Windows環境下GPU版本的安裝

之前我們已經安裝過caffe的CPU版本,但是在MNIST手寫數字識別中,我們發現caffe的CPU版本執行速度較慢,訓練效率不高。因此,在這裡我們安裝了caffe的GPU版本,並使用GPU版本的caffe同樣對手寫MNIST數字集進行訓練。 step1: 安裝CUDA

開始caffe(四)mnist手寫數字識別網路結構模型和超引數檔案的原始碼閱讀

下面為網路結構模型 %網路結構模型 name: "LeNet" #網路的名字"LeNet" layer { #定義一個層 name: "mnist" #層的名字"mnist" type:

開始caffe(二)caffe在win10下的安裝編譯

環境要求 作業系統:64位windows10 編譯環境:Visual Studio 2013 Ultimate版本 安裝流程 step1:檔案的下載 從GitHub新增連結描述中下載Windows版本的caffe,並進行解壓到電腦中。 step2:檔案修改 將壓縮包

開始系列-Caffe入門到精通之一 環境搭建

python 資源暫時不可用 強制 rec htm color 查看 cpu blog 先介紹下電腦軟硬件情況吧: 處理器:Intel? Core? i5-2450M CPU @ 2.50GHz × 4 內存:4G 操作系統:Ubuntu Kylin(優麒麟) 16.04

Redis開始學習教程三key值的有效期

圖片 com edi 數據 key值 一次 時間 inf 系統 Redis 是一種存儲系統,類似數據庫,和緩存的差別是,緩存有有效期,而Redis默認無有效期,或者說,默認有效期為永久 但是Redis可以當做緩存使用。這時候需要針對各個key設置有效期。 有效期單位默認為S

【視訊】Kubernetes1.12開始(六)程式碼編譯到自動部署

作者: 李佶澳   轉載請保留:原文地址   釋出時間:2018/11/10 16:14:00 說明 kubefromscratch-ansible和kubefromscratch介紹 使用前準備

開始理解caffe網路的引數

LeNet網路介紹 LeNet網路詳解 網路名稱 name: "LeNet" # 網路(NET)名稱為LeNet mnist層-train layer {

開始學習Servlet(1) 作用和生命週期

Servlet 作用 Servlet 是實現了 javax.servlet.Servlet 介面的 Java 類, 負責處理客戶端的 HTTP 請求。是客戶端 與 資料庫或後臺應用程式之間互動的媒介 。功能: 1. 讀取客戶端傳送的資料 2. 處理

ubuntu 14.04 開始安裝caffe

一、前言 很多人不太喜歡看官方教程,但其實 caffe 的官方安裝指導做的非常好。我在看到 2) 之前,曾根據官方指導在 OSX 10.9, 10.10, Ubuntu 12.04, 14.04 下安裝過 10 多次不同版本的 caffe,都成功了。 本文有不少內容參考了 1)和 2),但又有一些內容