編者按:2017年夏季,CMU CS碩士生Jacob Buckman入選Google AI居留計劃,在谷歌總部開啟了自己為期12月的培訓(xùn)生活,主攻NLP和強化學習。Jacob擁有豐富的編程經(jīng)驗,而且在機器學習上也造詣頗多。雖然從未接觸過Tensorflow,但他相信依靠自己的學識背景,掌握一個工具是很輕松的一件事。很可惜,現(xiàn)實打了他的臉……
簡介
自發(fā)布三年來,Tensorflow已經(jīng)成為深度學習生態(tài)系統(tǒng)的基石,然而相比PyTorch、DyNet這樣基于動態(tài)圖“define-by-run”的庫,它對初學者來說卻并不直觀。
從線性回歸、MNIST分類到機器翻譯,Tensorflow的教程無處不在,它們是幫助新手開啟項目的優(yōu)質(zhì)資源,也是新人接觸機器學習的敲門磚。但對于機器學習還未涉足的空白領(lǐng)域,如果開發(fā)者想做一些原創(chuàng)性的突破,Tensorflow可能會讓他們望而生畏。
本文的目的是填補這一領(lǐng)域的空白,文章內(nèi)容將緊緊圍繞一般方法,并解釋支撐Tensorflow的基本概念,而不是專注于某個特定任務(wù)。掌握這些概念后,開發(fā)者可以更直觀地用Tensorflow進行深度學習研究。
注:本教程適合在編程和機器學習上有一定經(jīng)驗,且必須要用到Tensorflow的開發(fā)者。
了解Tensorflow
Tensorflow不是一個普通的Python庫
大多數(shù)Python庫其實是Python的擴展。當你導(dǎo)入一個庫時,你得到的是一組變量、函數(shù)和類,它們實際上只是充當代碼的“工具箱”,滿足開發(fā)者的現(xiàn)實需要。但Tensorflow不是。如果我們一開始就抱著如何和代碼交互的想法去研究Tensorflow,那就相當于在本質(zhì)上走入歧途。
要說Python和Tensorflow之間的關(guān)系,我們可以把它簡單類比成Javascript和HTML。Javascript是一種用途廣泛的編程語言,我們可以用它實現(xiàn)很多東西。而HTML是一個框架,可以表示一些抽象計算(比如描述網(wǎng)頁上呈現(xiàn)的內(nèi)容)。當用戶打開一個網(wǎng)頁時,Javascript的作用是使他看到HTML對象,并且在網(wǎng)頁迭代時用新的HTML對象代替舊的對象。
和HTML類似,Tensorflow也是一個用于表示抽象計算的框架。當我們用Python操作Tensorflow時,代碼做的第一件事是組裝計算圖,第二件事是和計算圖進行交互(Tensorflow里的會話sessions)。但計算圖不在變量內(nèi)部,而在全局名稱空間中。正如莎士比亞當年說過:所有RAM都是一個階段,所有變量都只是指針。(莎士比亞一臉懵逼)
第一個關(guān)鍵概念:計算圖
在瀏覽Tensorflow文檔時,你會發(fā)現(xiàn)其中有大量關(guān)于“graphs”和“nodes”的描述。如果足夠細心,也許你也已經(jīng)在圖和會話這個頁面找到了所有關(guān)于數(shù)據(jù)流圖的詳細介紹。這個頁面的內(nèi)容是我們下文要重點解釋的,不同的是,官方文檔的表述充滿“技術(shù)感”,而我們會犧牲一些技術(shù)細節(jié),重點捕捉其中的直覺。
那么什么是計算圖?事實上,計算圖表示的是全局數(shù)據(jù)結(jié)構(gòu):它一個有向圖,包含數(shù)據(jù)計算流程的所有信息。
我們先來看一個示例:
import tensorflow as tf
計算圖:
導(dǎo)入Tensorflow后,我們得到了一個空白的計算圖,表示一個孤立的、空白的全局變量。在這個基礎(chǔ)上,我們再進行一些“Tensorflow操作”:
代碼:
import tensorflow as tf
two_node = tf.constant(2)
print two_node
輸出:
Tensor("Const:0", shape=(), dtype=int32)
計算圖:
這里我們得到了一個節(jié)點(node),它包含常數(shù)2,這個2是函數(shù)tf.constant帶來的。當我們print變量時,可以看到它返回了一個tf.Tensor對象,它是我們剛創(chuàng)建的節(jié)點的指針。為了驗證這一點,這里是另一個例子:
代碼:
import tensorflow as tf
two_node = tf.constant(2)
another_two_node = tf.constant(2)
two_node = tf.constant(2)
tf.constant(3)
計算圖:
即便前后函數(shù)功能一致,即便這些函數(shù)只是簡單地給同一個對象重復(fù)賦值,甚至即便它們根本沒有被分配給變量,對于每次調(diào)用函數(shù)tf.constant,計算圖中都會創(chuàng)建一個新節(jié)點。
相反地,如果我們創(chuàng)建了一個新變量,并把它賦值一個存在的節(jié)點,這就相當于把指針復(fù)制到該節(jié)點,這時計算圖上是不會出現(xiàn)新節(jié)點的:
代碼:
import tensorflow as tf
two_node = tf.constant(2)
another_pointer_at_two_node = two_node
two_node = None
print two_node
print another_pointer_at_two_node
輸出:
None
Tensor("Const:0", shape=(), dtype=int32)
計算圖:
接下來,我們嘗試一些有趣的東西:
代碼:
import tensorflow as tf
two_node = tf.constant(2)
three_node = tf.constant(3)
sum_node = two_node + three_node ## 相當于 tf.add(two_node, three_node)
計算圖:
上圖已經(jīng)是一幅真正意義上的計算圖了。需要注意的是,TensorFlow對常見數(shù)學運算符進行了重載,比如上面的tf.add。雖然它表面上沒有新增節(jié)點,但它確實把兩個張量一起添加進了一個新節(jié)點。
所以two_node指向包含2的節(jié)點,three_node指向包含3的節(jié)點,sum_node指向包含+的節(jié)點——是不是覺得有些不尋常,為什么sum_node里會是+,而不是5呢?
事實上,計算圖只包含步驟,不包含結(jié)果!至少……現(xiàn)在還不包含!
第二個關(guān)鍵概念:會話
如果說TensorFlow中存在bug重災(zāi)區(qū),那會話(session)一定排名首位。由于缺乏明確的命名,再加上函數(shù)的通用性,幾乎每個Tensorflow程序都要調(diào)用不止一次tf.Session()。
會話的作用是管理程序運行時的所有資源,如內(nèi)存分配和優(yōu)化,以便我們能按照計算圖的指示進行實際計算。你可以把計算圖想象成計算“模板”,上面列出了所有詳細步驟。所以每次在啟動計算圖前,我們都要先進行一個會話,分配資源,完成任務(wù);在計算結(jié)束后,我們又得關(guān)閉會話來幫助系統(tǒng)回收資源,防止資源泄露。
會話包含一個指向全局的指針,這個指針會基于計算圖中所有指向節(jié)點的指針不斷更新。這意味著會話和節(jié)點的創(chuàng)建不存在時間先后問題。
創(chuàng)建會話對象后,我們可以用sess.run(node)返回節(jié)點的值,并且Tensorflow會執(zhí)行確定該值所需的所有計算。
代碼:
import tensorflow as tf
two_node = tf.constant(2)
three_node = tf.constant(3)
sum_node = two_node + three_node
sess = tf.Session()
print sess.run(sum_node)
輸出:
5
計算圖:
我們也可以寫成sess.run([node1, node2,...]),讓它返回多個輸出:
代碼:
import tensorflow as tf
two_node = tf.constant(2)
three_node = tf.constant(3)
sum_node = two_node + three_node
sess = tf.Session()
print sess.run([two_node, sum_node])
輸出:
[2, 5]
計算圖:
一般來說,sess.run()是TensorFlow的最大瓶頸,你用的越少,程序就越好。只要有可能,我們應(yīng)該讓它一次性輸出多個結(jié)果,而不是頻繁使用,千萬不要把它放進復(fù)雜循環(huán)。
占位符和feed_dict
到目前為止,我們做的計算沒有輸入,所以一直得到相同的輸出。下面我們會進行更有意義的探索,比如構(gòu)建一個能接受輸入的計算圖,讓它經(jīng)過某種方式的處理,最后返回一個輸出。
要做到這一點,最直接的方法是使用占位符(Placeholders),這是一種用于接受外部輸入的節(jié)點。
代碼:
import tensorflow as tf
input_placeholder = tf.placeholder(tf.int32)
sess = tf.Session()
print sess.run(input_placeholder)
輸出:
Traceback (most recent call last):
...
InvalidArgumentError (see above for traceback): You must feed a value for placeholder tensor 'Placeholder'with dtype int32
[[Node: Placeholder = Placeholder[dtype=DT_INT32, shape=
計算圖:
...不是個好兆頭。這是一個典型的失敗案例,因為占位符本身沒有初始值,再加上我們沒有對它賦值,Tensorflow出現(xiàn)了個bug。
在會話sess.run()中,占位符可以用feed_dict饋送數(shù)據(jù)。
代碼:
import tensorflow as tf
input_placeholder = tf.placeholder(tf.int32)
sess = tf.Session()
print sess.run(input_placeholder, feed_dict={input_placeholder: 2})
輸出:
2
計算圖:
注意feed_dict的格式,它是一個字典,對于計算圖中所有存在的占位符,它都要給出相應(yīng)的取值(如前所述,它其實是指向圖中占位符節(jié)點的指針),這些值一般是標量或Numpy數(shù)組。
第三個關(guān)鍵概念:計算路徑
讓我們試試另一個涉及占位符的例子:
代碼:
import tensorflow as tf
input_placeholder = tf.placeholder(tf.int32)
three_node = tf.constant(3)
sum_node = input_placeholder + three_node
sess = tf.Session()
print sess.run(three_node)
print sess.run(sum_node)
輸出:
3
Traceback (most recent call last):
...
InvalidArgumentError (see above for traceback): You must feed a value for placeholder tensor 'Placeholder_2'with dtype int32
[[Node: Placeholder_2 = Placeholder[dtype=DT_INT32, shape=
計算圖:
我們又在輸出中看到了失敗標志...,那么為什么第二個sess.run這次出現(xiàn)bug了呢?為什么我們沒有評估input_placeholder,最后卻引發(fā)了一個關(guān)于它的錯誤?這兩個問題的答案就在于Tensorflow的第三個關(guān)鍵概念:計算路徑。好在這塊內(nèi)容總體比較直觀。
當我們調(diào)用sess.run()時,我們計算的不只是當前節(jié)點,還有一些和它相關(guān)的節(jié)點的值。如果這個節(jié)點依賴于其他節(jié)點,那我們就要一步步上溯計算,直到達到計算圖的“頂端”,也就是不再有其他節(jié)點會對目標節(jié)點施加影響。
下圖是sum_node節(jié)點的計算路徑:
為了計算sum_node,我們要評估所有三個節(jié)點的值,其中包括我們沒有賦值的占位符,這解釋了出現(xiàn)錯誤的原因。
相反地,three_node的計算路徑比較單一:
只要評估一個節(jié)點就夠了,所以即便input_placeholder沒有賦值,它也不會對sess.run(three_node)造成影響。
Tensorflow的框架優(yōu)勢離不開計算路徑設(shè)計。想象一下,如果我們手里有一幅巨型計算圖,其中包含大量不必要的節(jié)點,通過這樣的計算方式,我們可以繞過大多數(shù)點,只計算必要內(nèi)容,這就為節(jié)省大量運行時間提供了可能性。此外,它還允許我們構(gòu)建大型的“多用途”計算圖,這些圖中可以有一些共享的核心節(jié)點,但我們可以通過不同計算路徑來進行不同的計算。
變量和副作用
截至目前,我們接觸了兩種“沒有祖先”的節(jié)點:tf.constant和tf.placeholder。其中前者每輪都是一個定值;后者每輪都不一樣。除此之外,我們還需要考慮第三種情況:它可以連續(xù)幾輪都是個定制,但如果出現(xiàn)了一個新值,它也可以更新。這就是我們要引入的變量(Variables)概念。
如果想用Tensorflow進行深入學習,了解變量至關(guān)重要,因為模型的參數(shù)基本上都是變量。在訓(xùn)練期間,我們會用梯度下降更新參數(shù);但在評估過程中,我們卻要保持參數(shù)不變,并將大量不同的測試集輸入模型中。所以如果有可能的話,我們會希望所有可訓(xùn)練的參數(shù)都是變量。
創(chuàng)建變量的方法是tf.get_variable(),其中前兩個參數(shù)tf.get_variable(name, shape)是固定的,其他的都是可選參數(shù)。name是標識變量對象的字符串,它必須是獨一無二的,要確保沒有重復(fù)名稱。shape是與張量形狀對應(yīng)的整數(shù)矩陣,它按順序排列,每個維度只有一個整數(shù),例如一個3×8矩陣的shape應(yīng)該是[3, 8]。如果創(chuàng)建的是標量,記得符號是[]。
代碼:
import tensorflow as tf
count_variable = tf.get_variable("count", [])
sess = tf.Session()
print sess.run(count_variable)
輸出:
Traceback (most recent call last):
...
tensorflow.python.framework.errors_impl.FailedPreconditionError: Attempting to use uninitialized value count
[[Node: _retval_count_0_0 = _Retval[T=DT_FLOAT, index=0, _device="/job:localhost/replica:0/task:0/device:CPU:0"](count)]]
計算圖:
又出問題了,這次又是為什么呢?當我們首次創(chuàng)建變量時,它的初始值是“Null”,這時評估它都是會出bug的。變量要先賦值,再評估。這里賦值的方法有兩種,一是設(shè)定一個初始值,二是tf.assign()。我們來看tf.assign():
代碼:
import tensorflow as tf
count_variable = tf.get_variable("count", [])
zero_node = tf.constant(0.)
assign_node = tf.assign(count_variable, zero_node)
sess = tf.Session()
sess.run(assign_node)
print sess.run(count_variable)
輸出:
0
計算圖:
和上文提到的節(jié)點相比,tf.assign(target, value)這個節(jié)點有點特殊:
它不做計算,總是等于value;
副作用(Side Effects)。上圖顯示了這個操作的副作用,當計算流通過assign_node時,count_variable節(jié)點里的值被強行替換成了zero_node節(jié)點的值;
即便count_variable節(jié)點和assign_node之間存在連接,但兩者互不依賴(虛線)。
因為“副作用”節(jié)點支撐著大部分Tensorflow深度學習計算流,所以真正理解其中的原理是很有必要的,當我們運行sess.run(assign_node)時,計算路徑經(jīng)過assign_node和zero_node:
計算圖:
之前提到了,我們計算目標節(jié)點時會一起計算和它相關(guān)的節(jié)點,這之中包括副作用。如上圖中的綠色部分所示,由于tf.assign帶來的特定副作用,原先儲存“Null”的count_variable現(xiàn)在已經(jīng)被永久設(shè)置成了0,這意味著下次我們調(diào)用sess.run(count_variable)時,它會輸出0,而不是反饋bug。
接下來,讓我們看看設(shè)定初始值:
代碼:
import tensorflow as tf
const_init_node = tf.constant_initializer(0.)
count_variable = tf.get_variable("count", [], initializer=const_init_node)
sess = tf.Session()
print sess.run([count_variable])
輸出:
Traceback (most recent call last):
...
tensorflow.python.framework.errors_impl.FailedPreconditionError: Attempting to use uninitialized value count
[[Node: _retval_count_0_0 = _Retval[T=DT_FLOAT, index=0, _device="/job:localhost/replica:0/task:0/device:CPU:0"](count)]]
計算圖:
好的,為什么這里又沒有初始化呢?
答案在于會話和計算圖之間的割裂。我們?yōu)樽兞吭O(shè)置了一個初始值const_init_node,但它反映在計算圖上卻只是兩個節(jié)點間的虛線連接。這是因為我們在會話中根本沒有初始化操作,沒有為它分配計算資源。我們需要在會話中把變量更新成const_init_node。
代碼:
import tensorflow as tf
const_init_node = tf.constant_initializer(0.)
count_variable = tf.get_variable("count", [], initializer=const_init_node)
init = tf.global_variables_initializer()
sess = tf.Session()
sess.run(init)
print sess.run(count_variable)
輸出:
0.
計算圖:
為此,我們添加了另一個特殊節(jié)點:init = tf.global_variables_initializer()。和tf.assign()類似,這也是一個帶有副作用的節(jié)點,但它不需要指定輸入內(nèi)容。tf.global_variables_initializer()從創(chuàng)建之初就縱觀全圖,并自動為圖中的每個tf.initializer添加依賴關(guān)系。當我們開始執(zhí)行sess.run(init)時,它會完成全圖初始化,從而避免報錯。
變量共享
在實際操作中,有時我們也會遇到Tensorflow代碼與變量共享,它涉及創(chuàng)建范圍并設(shè)置“reuse = True”,但我們強烈不建議你這么做。如果你想在多個地方使用單個變量,只需以編程方式跟蹤指向該變量節(jié)點的指針,并在需要時重新使用它。換句話說,對于你打算存儲在內(nèi)存中的每個參數(shù),你應(yīng)該只調(diào)用一次tf.get_variable()。
除了以上三點,文章還介紹了優(yōu)化和debug過程中容易遇到的錯誤,考慮到代碼過長影響閱讀體驗,如果讀者感興趣,可以關(guān)注【論智】知乎專欄,明天小編會整理更新。
希望這篇文章能夠幫助你更好地理解Tensorflow是什么、它是如何工作的,以及如何使用它。
-
python
+關(guān)注
關(guān)注
56文章
4797瀏覽量
84729 -
強化學習
+關(guān)注
關(guān)注
4文章
266瀏覽量
11262 -
tensorflow
+關(guān)注
關(guān)注
13文章
329瀏覽量
60537
原文標題:進了谷歌門才領(lǐng)悟的Tensorflow教程:答疑解惑(一)
文章出處:【微信號:jqr_AI,微信公眾號:論智】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論