這是我看過解釋TensorFlow最透徹的文章!

策劃編輯 | Natalie
做者 | Jacob Buckman
譯者 | 王強、無明
編輯 | Vincent、Debra
AI 前線導讀:「我叫 Jacob,是谷歌 AI Residency 項目的學者。2017 年夏天我進入這個項目的時候,我本身的編程經驗很豐富,對機器學習理解也很深入,但之前我從未使用過 Tensorflow。當時我認爲憑本身的能力能夠很快掌握 Tensorflow,但沒想到我學習它的過程居然如此跌宕起伏。甚至加入項目幾個月後我還偶爾會感到困惑,不知道怎樣用 Tensorflow 代碼實現本身的新想法。

這篇博文就像是我給過去本身寫的瓶中信:回顧當初,我但願在開始學習的時候有這樣一篇入門介紹。我也但願本文可以幫助同行,爲他們提供參考。」AI 前線將這位現谷歌大腦工程師關於學習 Tensorflow 過程當中遭遇的方方面面難題的文章作了翻譯,但願對你們有幫助。

更多幹貨內容請關注微信公衆號「AI 前線」,(ID:ai-front)
過去的教程缺乏哪些內容?

Tensorflow 發佈已經有三年,現在它已成爲深度學習生態系統的基石。然而對於初學者來講它並不怎麼簡單易懂,與 PyTorch 或 DyNet 這樣的運行即定義的神經網絡庫相比就更明顯了。node

有不少 Tensorflow 的入門教程,內容涵蓋線性迴歸、MNIST 分類乃至機器翻譯。這些內容具體、實用的指南能幫助人們快速啓動並運行 Tensorflow 項目,而且能夠做爲相似項目的切入點。但有的開發者開發的應用並無很好的教程參考,還有的項目在探索全新的路線(研究中很常見),對於這些開發者來講入門 Tensorflow 是很是容易感到困惑的。git

我寫這篇文章就想彌補這一缺口。本文不會研究某個具體任務,而是提出更加通用的方法,並解析 Tensorflow 的基礎抽象概念。掌握好這些概念後,用 Tensorflow 進行深度學習就會更加直觀易懂。github

目標受衆

本教程適用於在編程和機器學習方面有必定經驗,並想要入門 Tensorflow 的從業者。他們能夠是:想在深度學習課程的最後一個項目中使用 Tensorflow 的 CS 專業學生;剛剛被調到涉及深度學習的項目的軟件工程師;或者是一位處於困惑之中的 Google AI 新手(向 Jacob 大聲打個招呼吧)。若是你須要基礎知識入門,請參閱如下資源。這些都瞭解的話,咱們就開始吧!編程

理解 Tensorflow

Tensorflow 不是一個普通的 Python 庫。數組

大多數 Python 庫被編寫爲 Python 的天然擴展形式。當你導入一個庫時,你獲得的是一組變量、函數和類,它們補充並擴展了你的代碼「工具箱」。使用這些庫時,你知道它們將產生怎樣的結果。我認爲談及 Tensorflow 時應該拋棄這些認識,這些認知從根本上就不符合 Tensorflow 的理念,沒法反映 TF 與其它代碼交互的方式。瀏覽器

Python 和 Tensorflow 之間的聯繫,能夠類比 Javascript 和 HTML 之間的關係。Javascript 是一種全功能的編程語言,能夠實現各類出色的效果。HTML 是用於表示某種類型的實用計算抽象(這裏指的是可由 Web 瀏覽器呈現的內容)的框架。Javascript 在交互式網頁中的做用是組裝瀏覽器看到的 HTML 對象,而後在須要時經過將其更新爲新的 HTML 來與其交互。微信

與 HTML 相似,Tensorflow 是用於表示某種類型的計算抽象(稱爲「計算圖」)的框架。當咱們用 Python 操做 Tensorflow 時,咱們用 Python 代碼作的第一件事是組裝計算圖。以後咱們的第二個任務就是與它進行交互(使用 Tensorflow 的「會話」)。但重要的是,要記住計算圖不在變量內部,它處在全局命名空間內。莎士比亞曾經說過:「全部的 RAM 都是一個階段,全部的變量都只不過是指針。」網絡

第一個關鍵抽象:計算圖

咱們在瀏覽 Tensorflow 文檔時,有時會發現內容提到「圖形」和「節點」。若是你仔細閱讀、深刻挖掘,甚至可能已經發現了這個頁面,該頁面中涵蓋的內容我將以更精確和技術化的風格詳細解釋。本節將從頂層入手,把握關鍵的直覺概念,同時略過一些技術細節。數據結構

那麼什麼是計算圖?它實質上是一個全局數據結構:計算圖是一個有向圖,捕獲有關計算方法的指令。框架

咱們來看看如何構建一個示例。下圖中,上半部分是咱們運行的代碼和它的輸出,下半部分是結果計算圖。

顯然,僅僅導入 Tensorflow 並不會給咱們生成一個有趣的計算圖,而只有一個孤獨的,空白的全局變量。可是當咱們調用一個 Tensorflow 操做時會發生什麼呢?

快看!咱們獲得了一個節點,它包含常量:2。我知道你很驚訝,驚訝的是一個名爲 tf.constant 的函數。當咱們打印這個變量時,咱們看到它返回一個 tf.Tensor 對象,它是一個指向咱們剛建立的節點的指針。爲了強調這一點,這裏是另外一個例子:

每次咱們調用 tf.constant 的時候,咱們都會在圖中建立一個新節點。即便節點在功能上與現有節點徹底相同,即便咱們將節點從新分配給同一個變量,甚至咱們根本沒有將其分配給變量,結果都同樣。

相反,若是建立一個新變量並將其設置爲與現有節點相等,則只需將該指針複製到該節點,而且不會向該圖添加任何內容:

好的,咱們更進一步。

如今咱們來看——這纔是咱們要的真正的計算圖表!請注意,+ 操做在 Tensorflow 中過載,因此同時添加兩個張量會在圖中增長一個節點,儘管它看起來不像是 Tensorflow 操做。

好的,因此 two_node 指向包含 2 的節點,three_node 指向包含 3 的節點,而 sum_node 指向包含... + 的節點?什麼狀況?它不是應該包含 5 嗎?

事實證實,沒有。計算圖只包含計算步驟,不包含結果。至少...... 尚未!

第二個關鍵抽象:會話

若是錯誤地理解 TensorFlow 抽象也有個瘋狂三月競賽(美國大學籃球繁忙冠軍賽季),那麼「會話」將成爲每一年排名第一的種子選手。能獲此尷尬的榮譽,是由於會話的命名反直覺,應用卻如此普遍——幾乎每一個 Tensorflow 程序都至少會調用一次 tf.Session() 。

會話的做用是處理內存分配和優化,使咱們可以實際執行由圖形指定的計算。能夠將計算圖想象爲咱們想要執行的計算的「模板」:它列出了全部的步驟。爲了使用這個圖表,咱們還須要發起一個會話,它使咱們可以實際地完成任務。例如,遍歷模板的全部節點來分配一組用於存儲計算輸出的存儲器。爲了使用 Tensorflow 進行各類計算,咱們既須要圖也須要會話。

會話包含一個指向全局圖的指針,該指針經過指向全部節點的指針不斷更新。這意味着在建立節點以前仍是以後建立會話都無所謂。

建立會話對象後,可使用 sess.run(node) 返回節點的值,而且 Tensorflow 將執行肯定該值所需的全部計算。

精彩!咱們還能夠傳遞一個列表,sess.run([node1,node2,...]),並讓它返回多個輸出:

通常來講,sess.run() 調用每每是最大的 TensorFlow 瓶頸之一,因此調用它的次數越少越好。能夠的話在一個 sess.run() 調用中返回多個項目,而不是進行多個調用。

佔位符和 feed_dict

咱們迄今爲止所作的計算一直很乏味:沒有機會得到輸入,因此它們老是輸出相同的東西。一個實用的應用可能涉及構建這樣一個計算圖:它接受輸入,以某種(一致)方式處理它,並返回一個輸出。

最直接的方法是使用佔位符。佔位符是一種用於接受外部輸入的節點。

……這是個糟糕的例子,由於它引起了一個異常。佔位符預計會被賦予一個值,但咱們沒有提供,所以 Tensorflow 崩潰了。

爲了提供一個值,咱們使用 sess.run() 的 feed_dict 屬性。

好多了。注意傳遞給 feed_dict 的數值格式。這些鍵應該是與圖中佔位符節點相對應的變量(如前所述,它實際上意味着指向圖中佔位符節點的指針)。相應的值是要分配給每一個佔位符的數據元素——一般是標量或 Numpy 數組。第三個關鍵抽象:計算路徑下面是另外一個使用佔位符的例子:

爲何第二次調用 sess.run() 會失敗?咱們並無在檢查 input_placeholder,爲何會引起與 input_placeholder 相關的錯誤?答案在於最終的關鍵 Tensorflow 抽象:計算路徑。還好這個抽象很是直觀。

當咱們在依賴於圖中其餘節點的節點上調用 sess.run() 時,咱們也須要計算這些節點的值。若是這些節點有依賴關係,那麼咱們須要計算這些值(依此類推......),直到達到計算圖的「頂端」,也就是全部的節點都沒有前置節點的狀況。

考察 sum_node 的計算路徑:

全部三個節點都須要評估以計算 sum_node 的值。最重要的是,這裏麪包含了咱們未填充的佔位符,並解釋了例外狀況!

相反,考察 three_node 的計算路徑:

根據圖的結構,咱們不須要計算全部的節點也能夠評估咱們想要的節點!由於咱們不須要評估 placeholder_node 來評估 three_node,因此運行 sess.run(three_node) 不會引起異常。

Tensorflow 僅經過必需的節點自動路由計算這一事實是它的巨大優點。若是計算圖很是大而且有許多沒必要要的節點,它就能節約大量運行時間。它容許咱們構建大型的「多用途」圖形,這些圖形使用單個共享的核心節點集合根據採起的計算路徑來作不一樣的任務。對於幾乎全部應用程序而言,根據所採用的計算路徑考慮 sess.run() 的調用方法是很重要的。

變量和反作用

到目前爲止,咱們已經看到兩種類型的「無祖先」節點:tf.constant(每次運行都同樣)和 tf.placeholder(每次運行都不同)。還有第三種節點:一般狀況下具備相同的值,但也能夠更新成新值。這個時候就要用到變量。

瞭解變量對於使用 Tensorflow 進行深度學習來講相當重要,由於模型的參數就是變量。在訓練期間,你但願經過梯度降低在每一個步驟更新參數,但在計算過程當中,你但願保持參數不變,並將大量不一樣的測試輸入集傳入到模型中。模型全部的可訓練參數頗有可能都是變量。

要建立變量,請使用 tf.get_variable()。tf.get_variable() 的前兩個參數是必需的,其他是可選的。它們是 tf.get_variable(name,shape)。name 是一個惟一標識這個變量對象的字符串。它在全局圖中必須是惟一的,因此要確保不會出現重複的名稱。shape 是一個與張量形狀相對應的整數數組,它的語法很直觀——每一個維度對應一個整數,並按照排列。例如,一個 3×8 的矩陣可能具備形狀 [3,8]。要建立標量,請使用空列表做爲形狀:[]。

發現另外一個異常。一個變量節點在首次建立時,它的值基本上就是「null」,任未嘗試對它進行計算的操做都會拋出這個異常。咱們只能先給一個變量賦值後才能用它作計算。有兩種主要方法能夠用於給變量賦值:初始化器和 tf.assign()。咱們先看看 tf.assign():

與咱們迄今爲止看到的節點相比,tf.assign(target,value) 有一些獨特的屬性:

  • 標識操做。tf.assign(target,value) 不作任何計算,它老是與 value 相等。

  • 反作用。當計算「流經」assign_node 時,就會給圖中的其餘節點帶來反作用。在這種狀況下,反作用就是用保存在 zero_node 中的值替換 count_variable 的值。

  • 非依賴邊。即便 count_variable 節點和 assign_node 在圖中是相連的,二者都不依賴於其餘節點。這意味着在計算任一節點時,計算不會經過該邊迴流。不過,assign_node 依賴 zero_node,它須要知道要分配什麼。

「反作用」節點充斥在大部分 Tensorflow 深度學習工做流中,所以,請確保你對它們瞭解得一清二楚。當咱們調用 sess.run(assign_node) 時,計算路徑將通過 assign_node 和 zero_node。

當計算流經圖中的任何節點時,它還會讓該節點控制的反作用(綠色所示)起效。因爲 tf.assign 的特殊反作用,與 count_variable(以前爲「null」)關聯的內存如今被永久設置爲 0。這意味着,當咱們下一次調用 sess.run(count_variable) 時,不會拋出任何異常。相反,咱們將獲得 0。

接下來,讓咱們來看看初始化器:

這裏都發生了什麼?爲何初始化器不起做用?

問題在於會話和圖之間的分隔。咱們已經將 get_variable 的 initializer 屬性指向 const_init_node,但它只是在圖中的節點之間添加了一個新的鏈接。咱們尚未作任何與致使異常有關的事情:與變量節點(保存在會話中,而不是圖中)相關聯的內存仍然爲「null」。咱們須要經過會話讓 const_init_node 更新變量。

爲此,咱們添加了另外一個特殊節點:init = tf.global_variables_initializer()。與 tf.assign() 相似,這是一個帶有反作用的節點。與 tf.assign() 不同的是,咱們實際上並不須要指定它的輸入!tf.global_variables_initializer() 將在其建立時查看全局圖,自動將依賴關係添加到圖中的每一個 tf.initializer 上。當咱們調用 sess.run(init) 時,它會告訴每一個初始化器完成它們的任務,初始化變量,這樣在調用 sess.run(count_variable) 時就不會出錯。

變量共享

你可能會碰到帶有變量共享的 Tensorflow 代碼,代碼有它們的做用域,並設置「reuse=True」。我強烈建議你不要在代碼中使用變量共享。若是你想在多個地方使用單個變量,只須要使用指向該變量節點的指針,並在須要時使用它。換句話說,對於打算保存在內存中的每一個參數,應該只調用一次 tf.get_variable()。

優化器

最後:進行真正的深度學習!若是你還在狀態,那麼其他的概念對於你來講應該是很是簡單的。

在深度學習中,典型的「內循環」訓練以下:

  • 獲取輸入和 true_output

  • 根據輸入和參數計算出一個「猜想」

  • 根據猜想和 true_output 之間的差別計算出一個「損失」

  • 根據損失的梯度更新參數

讓咱們把全部東西放在一個腳本里,解決一個簡單的線性迴歸問題:

正如你所看到的,損失基本上沒有變化,並且咱們對真實參數有了很好的估計。這部分代碼只有一兩行對你來講是新的:

既然你對 Tensorflow 的基本概念已經有了很好的理解,這段代碼應該很容易解釋!第一行,optimizer = tf.train.GradientDescentOptimizer(1e-3) 不會向圖中添加節點。它只是建立了一個 Python 對象,包含了一些有用的函數。第二行 train_op = optimizer.minimize(loss),將一個節點添加到圖中,並將一個指針賦給 train_op。train_op 節點沒有輸出,但有一個很是複雜的反作用:

train_op 回溯其輸入的計算路徑,尋找變量節點。對於找到的每一個變量節點,它計算與損失相關的變量梯度。而後,它爲該變量計算新值:當前值減去梯度乘以學習率。最後,它執行一個賦值操做來更新變量的值。

基本上,當咱們調用 sess.run(train_op) 時,它爲咱們對全部的變量作了一個梯度降低的操做。固然,咱們還須要使用 feed_dict 來填充輸入和輸出佔位符,而且咱們還但願打印這些損失,由於這樣方便調試。

用 tf.Print 進行調試

當你開始使用 Tensorflow 作更復雜的事情時,你須要進行調試。通常來講,檢查計算圖中發生了什麼是很困難的。你不能使用常規的 Python 打印語句,由於你永遠沒法訪問到要打印的值——它們被鎖定在 sess.run() 調用中。舉個例子,假設你想檢查一個計算的中間值,在調用 sess.run() 以前,中間值還不存在。可是,當 sess.run() 調用返回時,中間值不見了!

咱們來看一個簡單的例子。

咱們看到告終果是 5。可是,若是咱們想檢查中間值 two_node 和 three_node,該怎麼辦?檢查中間值的一種方法是向 sess.run() 添加一個返回參數,該參數指向要檢查的每一箇中間節點,而後在返回後打印它。

這樣作一般沒有問題,但當代碼變得愈來愈複雜時,這可能有點尷尬。更方便的方法是使用 tf.Print 語句。使人困惑的是,tf.Print 其實是 Tensorflow 的一種節點,它有輸出和反作用!它有兩個必需的參數:一個要複製的節點和一個要打印的內容列表。「要複製的節點」能夠是圖中的任何節點,tf.Print 是與「要複製的節點」相關的標識操做,也就是說,它將輸出其輸入的副本。不過,它有個反作用,就是會打印「打印清單」裏全部的值。

有關 tf.Print 的一個重要卻有些微妙的點:打印其實只是它的一個反作用。與全部其餘反作用同樣,只有在計算流經 tf.Print 節點時纔會進行打印。若是 tf.Print 節點不在計算路徑中,則不會打印任何內容。即便 tf.Print 節點正在複製的原始節點位於計算路徑上,但 tf.Print 節點自己可能不是。這個問題要注意!當這種狀況發生時,它會讓你感到很是沮喪,你須要費力地找出問題所在。通常來講,最好在建立要複製的節點後當即建立 tf.Print 節點。

這裏(https://wookayin.github.io/tensorflow-talk-debugging/#1)有一個很好的資源,提供了更多實用的調試建議。

結 論

但願這篇文章可以幫助你更好地理解 Tensorflow,瞭解它的工做原理以及如何使用它。畢竟,這裏介紹的概念對全部 Tensorflow 程序來講都很重要,但這些還都只是表面上的東西。在你的 Tensorflow 探險之旅中,你可能會遇到各類你想要使用的其餘有趣的東西:條件、迭代、分佈式 Tensorflow、變量做用域、保存和加載模型、多圖、多會話和多核數據加載器隊列等。

原文連接:

https://jacobbuckman.com/post/tensorflow-the-confusing-parts-1/#understanding-tensorflow

相關文章
相關標籤/搜索