如何用C++實(shí)現(xiàn)自己的Tensorflow

姓名:韓卓成?學(xué)號(hào) :20011210097

轉(zhuǎn)載自:http://blog.csdn.net/dev_csdn/article/details/78500708

【嵌牛導(dǎo)讀】:TensorFlow是由谷歌基于DistBelief進(jìn)行研發(fā)的第二代人工智能學(xué)習(xí)系統(tǒng),其命名來源于本身的運(yùn)行原理,它完全開源,作者通過自己的一個(gè)小項(xiàng)目,闡述了如何用C++實(shí)現(xiàn)自己的TensorFlow,這篇文章看起來可能會(huì)有點(diǎn)晦澀,你需要對(duì)相關(guān)知識(shí)有所了解。

【嵌牛鼻子】:人工智能

【嵌牛提問】:1.Tensorflow的基本概念?2.C++適用于實(shí)現(xiàn)Tensorflow嗎?

【嵌牛正文】:為什么?

如果你是CS專業(yè)的人員,可能聽過這句“不要使自己陷入_”的話無(wú)數(shù)次。CS有加密、標(biāo)準(zhǔn)庫(kù)、解析器等等。我覺得現(xiàn)在還應(yīng)該包含ML庫(kù)。

不管事實(shí)如何,它仍然是一個(gè)值得學(xué)習(xí)的驚人的教訓(xùn)。人們現(xiàn)在認(rèn)為TensorFlow和類似的庫(kù)是理所當(dāng)然的;把它們當(dāng)成是一個(gè)黑盒子,讓其運(yùn)行。沒有多少人知道后臺(tái)發(fā)生了什么。這真是一個(gè)非凸的優(yōu)化問題!不要停止攪拌那堆東西,直到它看起來合適為止(結(jié)合下圖及機(jī)器學(xué)習(xí)系統(tǒng)知識(shí)去理解這句話)。

Tensorflow

TensorFlow是由Google開源的一個(gè)深度學(xué)習(xí)庫(kù)。在TensorFlow的內(nèi)核,有一個(gè)大的組件,將操作串在一起,行成一個(gè)叫做運(yùn)算符圖的東西。這個(gè)運(yùn)算符圖是一個(gè)有向圖G=(V,E),在某些節(jié)點(diǎn)u1,u2,…,un,v∈V和e1,e2,…,en∈E,ei=(ui,v)存在某些運(yùn)算符將u1,…,un映射到v。

例如,如果我們有x + y = z,那么(x,z),(y,z)∈E。

這對(duì)于評(píng)估算術(shù)表達(dá)式非常有用。我們可以通過尋找運(yùn)算符圖中的sinks來得到結(jié)果。Sinks是諸如v∈V,??e=(v,u)這樣的頂點(diǎn)。換句話說,這些頂點(diǎn)沒有到其它頂點(diǎn)的有向邊。同樣的,sources是v∈V,??e=(u,v)。

對(duì)我們來說,總是把值放在sources,值會(huì)傳播到Sinks。

反向模式求導(dǎo)

如果認(rèn)為我的解釋不夠好,這里有一些幻燈片。

求導(dǎo)是TensorFlow所需的許多模型的核心要求,因?yàn)樾枰鼇磉\(yùn)行梯度下降算法。每個(gè)高中畢業(yè)的人都知道什么是求導(dǎo); 它只是獲取函數(shù)的導(dǎo)數(shù),如果函數(shù)是由基本函數(shù)組成的復(fù)雜組合,那么就做鏈?zhǔn)椒▌t。

超級(jí)簡(jiǎn)單的概述

如果有一個(gè)這樣的函數(shù):

f(x,y) = x * y

那么關(guān)于X的求導(dǎo)將產(chǎn)生:

df(x,y)dx=y

關(guān)于Y的求導(dǎo)將產(chǎn)生:

df(x,y)dy=x

另外一個(gè)例子:

f(x1,x2,...,xn)=f(x)=xTx

這個(gè)導(dǎo)數(shù)是:

df(x)dxi=2xi

所以梯度就是:

?xf(x)=2x

鏈?zhǔn)椒▌t,譬如應(yīng)用于復(fù)雜的函數(shù)f(g(h(x))):

df(g(h(x)))dx=df(g(h(x)))dg(h(x))dg(h(x))dh(x)dh(x)x

5分鐘內(nèi)反向模式

現(xiàn)在記住運(yùn)算符圖的DAG結(jié)構(gòu),以及上一個(gè)例子中的鏈?zhǔn)椒▌t。如果要評(píng)估,我們可以看到:

x -> h -> g -> f

作為圖表。會(huì)給出答案f。但是,我們也可以采取反向求解:

dx <- dh <- dg <- df

這看起來像鏈?zhǔn)椒▌t!需要將導(dǎo)數(shù)相乘在一起,以獲得最終結(jié)果。

下圖是一個(gè)運(yùn)算符圖的例子:

所以這基本上退化成圖遍歷問題。有誰(shuí)發(fā)覺拓?fù)渑判蚝虳FS / BFS嗎?

所以要支持雙向拓?fù)渑判虻脑?,需要包含一組父節(jié)點(diǎn)和一組子節(jié)點(diǎn),Sinks是另一個(gè)方向的Sources,反之亦然

實(shí)施

在開學(xué)之前,Minh Le和我開始設(shè)計(jì)這個(gè)項(xiàng)目。我們決定使用Eigen 庫(kù)后臺(tái)進(jìn)行線性代數(shù)運(yùn)算。它們有一個(gè)稱為MatrixXd的矩陣類。我們?cè)谶@里使用它。

每個(gè)變量節(jié)點(diǎn)由var類表示:

class var {

// Forward declaration

struct impl;

public:

// For initialization of new vars by ptr

var(std::shared_ptr);

var(double);

var(const MatrixXd&);

var(op_type, const std::vector&);

...

// Access/Modify the current node value

MatrixXd getValue() const;

void setValue(const MatrixXd&);

op_type getOp() const;

void setOp(op_type);

// Access internals (no modify)

std::vector& getChildren() const;

std::vector getParents() const;

...

private:

// PImpl idiom requires forward declaration of the? ? class:

std::shared_ptr pimpl;

};

struct var::impl{

public:

impl(const MatrixXd&);

impl(op_type, const std::vector&);

MatrixXd val;

op_type op;

std::vector children;

std::vector> parents;

};

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

在這里,我們采用pImpl慣用法,這意味著“通過指針來實(shí)現(xiàn)”。這在許多方面是非常好的,例如接口解耦實(shí)現(xiàn),當(dāng)在堆棧上有一個(gè)本地shell接口時(shí),允許在堆棧上實(shí)例化。pImpl的副作用是運(yùn)行時(shí)間稍慢,但是編譯時(shí)間縮短了很多。這讓我們通過多個(gè)函數(shù)調(diào)用/返回來保持?jǐn)?shù)據(jù)結(jié)構(gòu)的持久性。像這樣的樹狀數(shù)據(jù)結(jié)構(gòu)應(yīng)該是持久的。

有幾個(gè)枚舉,告訴我們目前正在執(zhí)行哪些操作:

enum class op_type {

plus,

minus,

multiply,

divide,

exponent,

log,

polynomial,

dot,

...

none // no operators. leaf.

};

1

2

3

4

5

6

7

8

9

10

11

12

13

執(zhí)行該樹評(píng)價(jià)的實(shí)際類稱為expression:

class expression {

public:

expression(var);

...

// Recursively evaluates the tree.

double propagate();

...

// Computes the derivative for the entire graph.

// Performs a top-down evaluation of the tree.

void backpropagate(std::unordered_map& leaves);

...

private:

var root;

};

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

反向傳播的內(nèi)部,有一些類似于此的代碼:

backpropagate(node, dprev):

derivative = differentiate(node)*dprev

for child in node.children:

backpropagate(child, derivative)

1

2

3

4

5

這相當(dāng)于做一個(gè)DFS; 你看到了嗎?

為什么選擇C ++?

事實(shí)上,C ++語(yǔ)言用于此不是特別合適。我們可以花更少的時(shí)間用OCaml等功能性語(yǔ)言來開發(fā)?,F(xiàn)在我明白了為什么Scala被用于機(jī)器學(xué)習(xí),主要看你喜歡;)。

然而,C ++有明顯的好處:

Eigen

例如,可以直接使用tensorflow的線性代數(shù)庫(kù),稱之為Eigen。這是一個(gè)多模板惰性計(jì)算的線性代數(shù)庫(kù)。類似于表達(dá)式樹的樣子,構(gòu)建表達(dá)式,只有在需要時(shí)才會(huì)對(duì)表達(dá)式進(jìn)行評(píng)估。然而,對(duì)于Eigen來說,在編譯的時(shí)候就確定何時(shí)使用模板,這意味著運(yùn)行時(shí)間的減少。我特別贊賞寫Eigen的人,因?yàn)閷徱暷0宓腻e(cuò)誤,讓我的眼睛充血。

Eigen的代碼看起來像:

Matrix A(...), B(...);

auto lazy_multiply = A.dot(B);

typeid(lazy_multiply).name(); // the class name is something like Dot_Matrix_Matrix.

Matrix(lazy_multiply); // functional-style casting forces evaluation of this matrix.

1

2

3

4

5

Eigen庫(kù)是非常強(qiáng)大的,這就是為什么它是tensorflow自我使用的主要后臺(tái)。這意味著除了這種惰性計(jì)算技術(shù)之外,還有其他方面的優(yōu)化。

運(yùn)算符重載

用Java開發(fā)這些庫(kù)會(huì)非常好—沒有shared_ptrs, unique_ptrs, weak_ptrs代碼;我們可以采取實(shí)際的,能勝任的,GC算法。使用Java開發(fā)可以節(jié)省許多開發(fā)時(shí)間,更不用說執(zhí)行速度也會(huì)變得更快??墒?,Java不允許運(yùn)算符重載,因而它們就不能這樣:

// These 3 lines code up an entire neural network!

var sigm1 = 1 / (1 + exp(-1 * dot(X, w1)));

var sigm2 = 1 / (1 + exp(-1 * dot(sigm1, w2)));

var loss = sum(-1 * (y * log(sigm2) + (1-y) * log(1-sigm2)));

1

2

3

4

5

順便說一下,上面的是實(shí)際代碼。這不是很漂亮嗎?我認(rèn)為這比用于TensorFlow的python包裝更漂亮。只想讓你知道,這些也都是矩陣。

在Java語(yǔ)言中,這將是極其丑陋的,有著一堆a(bǔ)dd(), divide()…等等代碼。更為重要的是,用戶將被隱式強(qiáng)制使用PEMDAS(括號(hào) ,指數(shù)、乘、除、加、減),這一點(diǎn)上,C++的運(yùn)算符表現(xiàn)的很好。

性能,而不是Bug

有一些東西,你可以在這個(gè)庫(kù)中實(shí)際指定,TensorFlow沒有明確的API,或者我不知道。比如,如果想訓(xùn)練某個(gè)特定子集的權(quán)重,可以只反向傳播到感興趣的具體來源。這對(duì)于卷積神經(jīng)網(wǎng)絡(luò)的轉(zhuǎn)移學(xué)習(xí)非常有用,一些大的網(wǎng)絡(luò),如VGG19網(wǎng)絡(luò),很容易用TensorFlow實(shí)現(xiàn),其附加的幾個(gè)額外的層的權(quán)重是根據(jù)新的域樣本進(jìn)行訓(xùn)練的。

基準(zhǔn)

用Python的Tensorflow庫(kù),在Iris數(shù)據(jù)集上對(duì)10000個(gè)歷史紀(jì)元進(jìn)行分類訓(xùn)練,這些歷史紀(jì)元具有相同的超參數(shù),結(jié)果是:

Tensorflow的神經(jīng)網(wǎng)絡(luò)23812.5 ms

Scikit的神經(jīng)網(wǎng)絡(luò)庫(kù):22412.2 ms

Autodiff的神經(jīng)網(wǎng)絡(luò),迭代,優(yōu)化:25397.2 ms

Autodiff的神經(jīng)網(wǎng)絡(luò),具有迭代,無(wú)優(yōu)化:29052.4 ms

Autodiff的神經(jīng)網(wǎng)絡(luò),具有遞歸,無(wú)優(yōu)化:28121.5 ms

如此看來,令人驚訝的是,Scikit在所有這些中運(yùn)行最快。這可能是因?yàn)槲覀儧]有做大量的矩陣乘法運(yùn)算。也可能是因?yàn)閠ensorflown不得不通過變量初始化采用額外的編譯步驟。或者,也許可能不得不在python中運(yùn)行循環(huán),而不是在C語(yǔ)言中(python循環(huán)真的很糟糕!)。我自己也不確定這到底是因?yàn)槭裁础?/p>

我完全意識(shí)到這絕對(duì)不是一個(gè)全面的基準(zhǔn)測(cè)試,因?yàn)樗贿m用于在特定情況下的單個(gè)數(shù)據(jù)點(diǎn)。不過,這個(gè)庫(kù)的性能并不是最先進(jìn)的技術(shù),因?yàn)槲覀儾幌M炎约壕磉M(jìn)TensorFlow。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容