正確debug的TensorFlow的姿勢
點擊上方“小白學(xué)視覺”,選擇加"星標(biāo)"或“置頂”
重磅干貨,第一時間送達(dá)
TensorFlow代碼很難調(diào)試,這個大家已達(dá)成共識,不過,就算是難,也還是需要調(diào)試的,畢竟誰也沒有把握不出bug,看看這篇文章能不能讓你減輕一點調(diào)試時的痛苦。

當(dāng)討論在tensorflow上編寫代碼時,總是將其與PyTorch進行比較,討論框架有多復(fù)雜,以及為什么要使用tf.contrib的某些部分,做得太爛了。此外,我認(rèn)識很多數(shù)據(jù)科學(xué)家,他們只用Github上已有的repo來用tensorflow。對這個框架持這種態(tài)度的原因是非常不同的,但是今天讓我們關(guān)注更實際的問題:調(diào)試用tensorflow編寫的代碼并理解它的主要特性。
?
計算圖。第一個抽象是計算圖
tf.Graph,它使框架能夠處理惰性評估模式(不是立即執(zhí)行,這是“傳統(tǒng)”命令式Python編程實現(xiàn)的)。基本上,這種方法允許程序員創(chuàng)建tf.Tensor(邊)和tf.Operation(節(jié)點),它不是立即計算的,而是在執(zhí)行圖形時才計算的。這種構(gòu)造機器學(xué)習(xí)模型的方法在許多框架中都很常見(例如,在Apache Spark中使用了類似的思想),并且具有不同的優(yōu)缺點,這在編寫和運行代碼時表現(xiàn)得非常明顯。最主要和最重要的優(yōu)點是,數(shù)據(jù)流圖可以很容易地實現(xiàn)并行性和分布式執(zhí)行,而無需顯式地使用multiprocessing模塊。在實踐中,編寫良好的tensorflow模型在啟動時立即使用所有核心的資源,而不需要任何額外的配置。然而,這個工作流的一個非常明顯的缺點是,一旦你構(gòu)建圖的時候,沒有用提供的輸入來運行,你就不能確保它不會崩潰。它肯定會崩潰。此外,除非你已經(jīng)執(zhí)行了圖形,否則你無法估計它的運行時間。
計算圖的主要組成部分是圖集合和圖結(jié)構(gòu)。嚴(yán)格地說,圖的結(jié)構(gòu)是前面討論的特定的節(jié)點集和邊集,而圖集合是可以邏輯地分組的變量集。例如,檢索圖形的可訓(xùn)練變量的常用方法是'
tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES)。會話 。第二個抽象與第一個抽象高度相關(guān),并且有更復(fù)雜的解釋:tensorflow會話的
tf.Session用于客戶端程序和c++運行時之間的連接(如你所知,tensorflow是用c++編寫的)。為什么是c++ ?答案是,通過這種語言實現(xiàn)的數(shù)學(xué)運算可以得到很好的優(yōu)化,因此,計算圖運算可以得到很好的處理。如果你正在使用低級的tensorflow API(大多數(shù)Python開發(fā)人員都在使用),則tensorflow session將作為上下文管理器調(diào)用:
with tf.Session() as sess:。沒有參數(shù)傳遞給構(gòu)造函數(shù)(如前一個示例)的會話僅使用本地機器的資源和默認(rèn)的tensorflow圖,但它也可以通過分布式tensorflow運行時訪問遠(yuǎn)程設(shè)備。在實踐中,如果沒有會話,計算圖就不能存在(沒有會話,它就不能執(zhí)行),而會話總是有一個指向全局圖的指針。深入研究運行會話的細(xì)節(jié),值得注意的主要一點是它的語法:
tf.Session.run()。它可以作為張量、操作或類張量對象的參數(shù)獲取(或獲取列表)。另外,可以傳遞feed_dict(這個可選參數(shù)是tf.placeholder的對象及其值)以及一組選項。
?
會話加載并通過預(yù)先訓(xùn)練的模型進行預(yù)測。這就是瓶頸,我花了幾周的時間來理解、調(diào)試和修復(fù)它。我想高度關(guān)注這個問題,并描述兩種重新加載預(yù)訓(xùn)練模型(圖和會話)并使用它的可能技術(shù)。
首先,當(dāng)我們談?wù)摷虞d模型時,我們真正的意思是什么?當(dāng)然,為了做到這一點,我們需要事先訓(xùn)練并保存它。后者通常是通過
tf.train.Saver.save完成的。因此,我們有3個二進制文件.index、.meta和.data-00000-of-00001,其中包含恢復(fù)會話和圖所需的所有數(shù)據(jù)。要加載以這種方式保存的模型,需要通過
tf.train.import_meta_graph()(參數(shù)是.meta的文件)來恢復(fù)。按照前一段描述的步驟之后,所有變量(包括所謂的“隱藏”變量,稍后將討論)都將被移植到當(dāng)前圖中。檢索某個有自己名字的張量(記住,它可能不同于你初始化它時使用的張量,這取決于創(chuàng)建張量的范圍和操作的結(jié)果)應(yīng)該執(zhí)行graph.get_tensor_by_name()。這是第一種方法。第二種方法更顯式,也更難實現(xiàn)(對于我一直在使用的模型的架構(gòu),我還沒有成功用起來),它的主要思想是將圖的邊(張量)顯式地保存到
.npy或.npz文件中,然后將它們加載回圖中(并根據(jù)創(chuàng)建它們的范圍分配適當(dāng)?shù)拿Q)。這種方法的問題在于它有兩個巨大的缺點:首先,當(dāng)模型架構(gòu)變得非常復(fù)雜時,它也變得很難控制和保存所有的權(quán)重矩陣。其次,有一種“隱藏的”張量,它是在沒有顯式初始化的情況下創(chuàng)建的。例如,當(dāng)你創(chuàng)建tf.nn.rnn_cell.BasicLSTMCell時。它創(chuàng)建了所有需要的權(quán)值和偏差來實現(xiàn)LSTM cell。變量名也是自動分配的。這種行為看起來還可以,但實際上,在很多情況下,并不是很好用。這種方法的主要問題是,當(dāng)你查看圖的集合時,看到一堆變量,你不知道它們的來源,你實際上不知道應(yīng)該保存什么以及在哪里加載它們。坦率地說,很難將隱藏變量放到圖中正確的位置并適當(dāng)?shù)夭僮魉鼈儭?/span>
在沒有任何警告的情況下兩次創(chuàng)建同名張量(通過自動添加_index結(jié)尾)。我認(rèn)為這個問題不像前一個問題那么重要,但是這個問題確實困擾著我,因為它會導(dǎo)致很多圖執(zhí)行錯誤。為了更好地解釋這個問題,我們來看一下這個例子。
例如,你用
tf.get_variable(name=’char_embeddings’, dtype=…)來創(chuàng)建張量。然后保存它并在新會話中加載。你已經(jīng)忘記了這個變量是可訓(xùn)練的,并通過tf.get_variable()以相同的方式再次創(chuàng)建了它。在圖執(zhí)行過程中,會發(fā)生的這樣的錯誤:FailedPreconditionError (see above for traceback): Attempting to use uninitialized value char_embeddings_2。原因是,你已經(jīng)創(chuàng)建了一個空變量,但是并沒有將它放到模型的適當(dāng)?shù)奈恢茫鴮嶋H上只要它已經(jīng)包含在計算圖中,就可以放到模型的某個地方。正如你所看到的,由于開發(fā)人員創(chuàng)建了同名張量兩次(甚至Windows也會這樣做),所以沒有出現(xiàn)錯誤或警告。也許這一點只對我很重要,但這是tensorflow的特性,我并不喜歡它的這個行為。
在編寫單元測試和其他問題時手動重置圖。由于許多原因,測試用tensorflow編寫的代碼總是很困難。第一個— 也是最明顯的一個,已經(jīng)在這一段的開頭提到過了,可能聽起來很傻,但對我來說,讓人很惱火。由于在運行期間訪問的所有模塊的所有張量只有一個默認(rèn)的tensorflow圖,因此不可能在不重置這個圖的情況下,使用不同的參數(shù)(例如)測試相同的功能。它只是一行代碼
tf.reset_default_graph(),但是知道它應(yīng)該寫在大多數(shù)方法的頂部,這個解決方案就變成了某種惡作劇,當(dāng)然,一個明顯的代碼復(fù)制示例。我還沒有發(fā)現(xiàn)任何可能的方法處理這個問題(除了使用reuse參數(shù),我們將在后面討論),只要所有的張量與默認(rèn)圖并沒有辦法隔離(當(dāng)然,可以有一個單獨的tensorflow圖方法,但在我看來這不是最佳實踐)。為tensorflow寫單元測試的代碼也困擾我很多的事情,在這種情況下,計算圖的一部分是不應(yīng)該被執(zhí)行的(里面有張量未初始化,因為模型沒有訓(xùn)練)。但是,計算圖本身并不知道我們應(yīng)該測試什么。我的意思是
self.assertEqual()的參數(shù)不清楚(我們應(yīng)該測試輸出張量的名稱或它們的形狀嗎?如果形狀為None呢?如果張量的名字或形狀不足以得出這個結(jié)論,那該怎么辦?)。在我的例子中,我只是簡單地斷言張量的名稱、形狀和維數(shù),但是我確信,在不執(zhí)行圖的情況下,只檢查這部分功能是不合理的。混淆張量的名字。很多人會說這個關(guān)于tensorflow的評論是一種另外的抱怨方式,但是我們并不能說出來在做了某種運算之后得到的張量的名字是什么。我的意思是,名稱
bidirectional_rnn/bw/bw/while/Exit_4:0清楚嗎?對我來說,絕對不清楚。我得到這個張量是對動態(tài)雙向RNN的后向單元進行某種操作的結(jié)果,但是如果沒有顯式地調(diào)試代碼,就無法知道執(zhí)行了哪些操作以及操作的順序。另外,索引的結(jié)尾也不太好理解,只要你想知道數(shù)字4是從哪里來的,就需要閱讀tensorflow文檔并深入研究計算圖。前面討論的“隱藏”變量的情況也是一樣的:為什么這里有
bias和kernel名稱?也許這是我的水平不夠,但是這樣的調(diào)試案例對我來說很不自然。tf.AUTO_REUSE,可訓(xùn)練的變量,重新編譯的庫和其他的東西。這個列表的最后,我們看一些小的細(xì)節(jié),這些細(xì)節(jié)只有在錯誤中才能學(xué)習(xí)到。第一件事是reuse=tf.AUTO_REUSE參數(shù)的作用域,它允許自動處理已經(jīng)創(chuàng)建的變量,如果它們已經(jīng)存在,就不會創(chuàng)建兩次。事實上,在許多情況下,它可以解決本段第二點所述的問題。但是,在實踐中,這個參數(shù)應(yīng)該謹(jǐn)慎使用,并且只有在開發(fā)人員知道代碼的某些部分需要運行兩次或多次時才使用。第二點是可訓(xùn)練變量,這里最重要的一點是:所有的張量在默認(rèn)情況下都是可訓(xùn)練的。有時這可能是一個頭痛的行為,因為有時候我們并不想要所有的張量都可訓(xùn)練,但是又很容易忘記,他們是都可以訓(xùn)練的。
第三件事只是一個優(yōu)化技巧,我建議每個人都這么做:幾乎在所有情況下,當(dāng)你使用通過pip安裝的包時,你都會收到這樣的警告:
Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX AVX2。如果你看到這類消息,最好卸載tensorflow,然后使用你喜歡的選項通過bazel重新編譯它。這樣做的主要好處是計算速度的提高和更好的總體性能。
?
我希望這篇文章能夠?qū)δ切┱陂_發(fā)他們的第一個tensorflow模型的數(shù)據(jù)科學(xué)家有所幫助,他們正在努力解決框架中某些部分的不明顯的行為,這些行為很難理解,而且調(diào)試起來相當(dāng)復(fù)雜。要點我想說的是,使用這個庫工作的時候,犯一些錯誤也不是壞事(對于其他的事情也是一樣的),這可以讓我們多問些問題,深入查看文檔和調(diào)試每一行代碼,這樣也是很好的。
就像跳舞或游泳一樣,任何事情都需要練習(xí),我希望我能讓這種練習(xí)變得更愉快和有趣。
交流群
歡迎加入公眾號讀者群一起和同行交流,目前有SLAM、三維視覺、傳感器、自動駕駛、計算攝影、檢測、分割、識別、醫(yī)學(xué)影像、GAN、算法競賽等微信群(以后會逐漸細(xì)分),請掃描下面微信號加群,備注:”昵稱+學(xué)校/公司+研究方向“,例如:”張三?+?上海交大?+?視覺SLAM“。請按照格式備注,否則不予通過。添加成功后會根據(jù)研究方向邀請進入相關(guān)微信群。請勿在群內(nèi)發(fā)送廣告,否則會請出群,謝謝理解~
