從零開始山寨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),但又有一些內容