姓名:韓卓成?學(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是由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。
如果認(rèn)為我的解釋不夠好,這里有一些幻燈片。
求導(dǎo)是TensorFlow所需的許多模型的核心要求,因?yàn)樾枰鼇磉\(yùn)行梯度下降算法。每個(gè)高中畢業(yè)的人都知道什么是求導(dǎo); 它只是獲取函數(shù)的導(dǎo)數(shù),如果函數(shù)是由基本函數(shù)組成的復(fù)雜組合,那么就做鏈?zhǔn)椒▌t。
如果有一個(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
現(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,反之亦然。
在開學(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; 你看到了嗎?
事實(shí)上,C ++語(yǔ)言用于此不是特別合適。我們可以花更少的時(shí)間用OCaml等功能性語(yǔ)言來開發(fā)?,F(xiàn)在我明白了為什么Scala被用于機(jī)器學(xué)習(xí),主要看你喜歡;)。
然而,C ++有明顯的好處:
例如,可以直接使用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)化。
用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)的很好。
有一些東西,你可以在這個(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)練的。
用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。