(給資料分析與開發加星標,提升資料技能)
英文:Galina Olejnik,編譯:機器之心
到底是選 TensorFlow 還是 PyTorch?蘿蔔青菜各有所愛。雖然很多人吐槽 TensorFlow 框架的複雜以及除錯程式碼的痛苦,但選擇 TensorFlow 人還是很多。大概,這就是真愛吧!本文作者透過對 TensorFlow 程式碼進行百般調戲,哦除錯,總結了一套讓你感覺不那麼痛苦的除錯方法,趁熱圍觀吧↓↓
當談到在 TensorFlow 上寫程式碼時,我們總會將它和 PyTorch 進行對比,然後討論 TensorFlow 框架是多麼的複雜以及 tf.contrib 的某些部分為什麼那麼糟糕。此外,我還認識許多資料科學家,他們只用預先寫好的、可以克隆的 GitHub 庫和 TensorFlow 互動,然後成功使用它們。對 TensorFlow 框架持有這種態度的原因各不相同,想要說清楚的話恐怕還得另外寫個長篇,現在我們要關註的是更實際的問題:除錯用 TensorFlow 寫的程式碼,並理解其主要特性。
核心概念
計算圖。計算圖 tf.Graph 讓框架能夠處理惰性求值正規化(不是 eager execution,一種指令式程式設計環境)。基本上,這種方法允許程式員建立 tf.Tensor(邊) 和 tf.Operation(節點),但它們不會立刻進行運算,只有在執行圖時才會計算。這種構建機器學習模型的方法在許多框架中都很常見(例如,Apache Spark 中就用了類似的想法),這種方法也有不同的優缺點,這些優缺點在編寫和執行程式碼時都很明顯。最主要也是最重要的優點是,資料流圖可以在不明確使用 multiprocessing 模組的情況下,實現並行和分散式執行。實際上,寫得好的 TensorFlow 模型無需任何額外配置,一啟動就可以呼叫所有核的資源。
但這個工作流程有個非常明顯的缺點:只要你在構建圖時沒提供任何輸入來執行這個圖,你就無法判斷它是否會崩潰。而它很有可能會崩潰。此外,除非你已經執行了這個圖,否則你也無法估計它的執行時間。
計算圖的主要組成部分是圖集合和圖結構。嚴格地說,圖結構是之前討論過的節點和邊的特定集合,而圖集合則是變數的集合,可以根據邏輯對這些變數進行分組。例如,檢索圖的可訓練變數的常用方法是:tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES)。
會話。它與計算圖高度相關,但解釋起來卻要更複雜一些:TensorFlow 會話 tf.Session 是用來連線客戶端程式和 C++執行時的(記住,TensorFlow 是用 C++ 寫的)。為什麼是 C++呢?因為透過這種語言實現的數學運算很好最佳化,因此計算圖運算可以得到很好的處理。
如果你用的是低階 TensorFlow API(大多數 Python 開發人員使用的都是),那 TensorFlow 會話將會作為背景關係管理器呼叫:使用 with tf.Session() as sess: 句法。如果傳遞給建構式的會話沒有引數,那麼就只會使用本地機器的資源和預設的 TensorFlow 圖,但它也可以透過分散式 TensorFlow 執行時使用遠端裝置。事實上,沒有會話,圖就不能存在(圖沒有會話就無法執行),而且會話一般都有一個指向全域性圖的指標。
更深入地研究執行會話的細節,值得註意的要點是它的句法:tf.Session.run()。它可以將張量、運算或類似張量的物件作為引數(或引數串列)提取。此外,feed_dict(這個可選引數是 tf.placeholder 物件到其值的對映)可以和一組選項一起傳遞。
可能遇到的問題及其解決方案
透過預訓練模型載入會話併進行預測。這是一個瓶頸,我花了好幾周來理解、除錯和修改這個問題。我高度關註這個問題,並提出了兩個重新載入和使用預訓練模型(圖和會話)的技巧。
首先,我們談到載入模型時我們真正的意思是什麼?當然,為了實現這一點,我們需要先訓練和儲存模型。後者一般是透過 tf.train.Saver.save 功能實現的,因此,我們有三個二進位制檔案,它們的副檔名分別是 .index,.m*e*ta 和 .data-00000-of-00001,這其中包含了還原會話和圖所需的所有資料。
為了載入以這種方式儲存的模型,首先要透過 tf.train.import_meta_graph()(引數是副檔名為 .meta 的檔案)還原圖。按照前面的描述操作後,會將所有變數(包括後面將會提到的「隱藏」變數)傳入當前的圖中。執行 graph.get_tensor_by_name 來檢索具有名稱的張量(記住,由於張量的建立範圍和運算,它可能和你初始化後的那個不同)。這是第一種方法。
第二種方法更明確,但是也更難實現(我一直都在研究模型架構,但我從沒成功地用這種方法執行圖),這種方法的主要思路是在 .npy 或 .npz 檔案中明確地儲存圖的邊(張量),之後再將它們載入回圖中(同時根據它們的建立範圍給它們分配恰當的名稱)。這種方法有兩個巨大的缺點:首先,當模型架構變得非常複雜時,控制和保持所有的權重矩陣也變得很難。其次,還有一類「隱藏」張量,它們是在沒有明確初始化的情況下建立的。例如,當你建立 tf.nn.rnn_cell.BasicLSTMCell 時,它為了實現 LSTM 單元,會偷偷建立所有必需的權重和偏差。變數名稱也是自動分配的。
這種行為看似沒什麼問題(只要這兩個張量是權重,且它們是用框架處理而非手動建立的),但是事實上,在許多情況下都並非如此。該方法的主要問題是當你看圖的集合時,你也會看到一大堆來源不明的變數,實際上你並不知道應該把什麼儲存下來,也不知道應該從哪載入它。坦率地講,將隱變數放在圖中正確的位置並恰當地操作是很難的。這比你本身的需求還要難。
在沒有任何警告的情況下建立了兩個名字相同的張量(透過自動新增_index 結尾)。我認為這個問題並不像前面那個那麼重要,但它造成的大量圖運算錯誤問題也確實給我帶來了困擾。為了更好地解釋這個問題,我們來看個例子。
例如,你用 tf.get_variable(name=’char_embeddings‘, dtype=…) 建立了張量,然後將它儲存下來,併在新的會話中載入它。你忘了這個變數是可訓練的,然後透過 tf.get_variable() 又以同樣的方式建立了一次。在圖執行期間,會報這樣的錯:FailedPreconditionError (see above for traceback): Attempting to use uninitialized value char_embeddings_2。發生這個錯誤的原因是,你已經建立了一個空變數但沒有把它放在模型中合適的地方,而只要它在圖中,就可以進行傳輸。
你可能沒見過開發人員因為建立了兩個名字相同的張量(即便是 Windows 也會這麼做)而引發任何錯誤或警告。也許這一點只是對我而言很重要,但這是 TensorFlow 的特點,而且是我很不喜歡的一點。
在寫單元測試還有一些其他問題時要手動重置圖形。由於一些原因,很難測試用 TensorFlow 寫的程式碼。第一個——也是最明顯的一點在本段開頭已經提到了,這聽起來可能很傻,但對我來說,它太令人惱火了。舉個例子,由於在執行時訪問的所有模組的所有張量只有一個預設的 tensorflow 圖,因此無法在不重置圖的情況下用不同的引數測試相同的功能。雖然 tf.reset_default_graph() 寫成程式碼只有一行,但是它要寫在大多數方法的頂部,這個解決方法變成了重覆性的工作,即明顯的複製程式碼。我沒發現任何可以解決這個問題的方法(除了使用範圍的 reuse 引數,這個會在後面討論),只要將所有張量連結到預設圖即可,但是沒有方法可以將它們分隔開(當然,每種方法都可以用單獨的 TensorFlow 圖,但在我看來,它們都不是最佳實現)。
關於 TensorFlow 程式碼的單元測試問題也讓我困擾已久:當不需要執行構建圖的一部分(因為模型尚未訓練所以其中有未初始化的張量)時,我不知道應該測試些什麼。我的意思是 self.assertEqual() 的引數不清楚(我們是否要測試輸出張量的名字或形狀?如果形狀是 None 呢?如果僅憑張量名稱或形狀無法推斷程式碼是否執行良好呢?)。就我個人而言,我只是簡單地測試了張量的名稱、形狀和維度,但我確信,在一些沒有執行圖的情況中,只檢查這部分功能並不合理。
令人困惑的張量名稱。許多人可能認為這樣評價 TensorFlow 的效能不太好,但有時沒人說得出來在執行某些操作後得到的張量名稱是什麼。舉個例子,你知道 bidirectional_rnn/bw/bw/while/Exit_4:0 是什麼意思嗎?對我來說,這簡直莫名其妙。我知道這個張量是對動態雙向 RNN 的後向單元進行某種運算得到的結果,但如果沒有明確地除錯程式碼,你就無法得知到底是按什麼樣的順序執行了什麼樣的運算。此外,索引的結尾也令人無法理解,如果想知道數字 4 來自哪裡,你得閱讀 TensorFlow 檔案並深入研究計算圖。
對前面討論過的「隱」變數來說,情況也是一樣的:為什麼我們會有 bias 和 kernel 的名稱呢?也許這是我的資歷和技術水平問題,但對我來說這樣的除錯情況是很不自然的。
tf.AUTO_REUSU 是可訓練變數,可以重新編譯庫和其他不好的東西。這部分的最後一點是簡要介紹我透過錯誤和嘗試方法學到的一些小細節。首先是範圍的引數 reuse=tf.AUTO_REUSE,它允許自動處理已經建立的變數,如果這些變數已經存在的話就不會進行二次建立。事實上,在許多情況下,它都可以解決本段提出的第二個問題。但在實際情況中,只有當開發人員知道程式碼的某些部分需要執行兩次或兩次以上時,才應該謹慎地使用這一引數。
第二點是關於可訓練變數,這裡最重要的點是:預設情況下所有張量都是可訓練的。有時候你可能不需要對其進行訓練,而且很容易會忘記它們都可以訓練。這一點有時令人頭疼。
第三點只是一個最佳化技巧,我建議每個人都這麼做:幾乎在所有情況下,當你使用透過 pip 安裝的軟體包時,會收到如下警告:Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX AVX2。如果看到這類資訊,最好解除安裝 TensorFlow,再根據你需要的選項透過 bazel 重新編譯它。這樣做的主要好處是可以提升計算速度,而且可以更好地提高框架的總體效能。
總結
希望本文能夠幫助那些首次開發 TensorFlow 模型的資料科學家。他們可能正掙扎於框架的某些部分,這些部分很難理解而且除錯起來很複雜。我想說的是,不要擔心在使用這個庫時犯很多錯誤(也別擔心其他的),只要提出問題,深入研究官方檔案,除錯出錯的程式碼就可以了。
這些與跳舞或者游泳一樣,都需要熟能生巧,我希望能夠讓這種練習變得更愉快也更有趣一些。