?Immediate versus Deferred Execution
深度學(xué)習(xí)庫的關(guān)鍵區(qū)別在于是即時執(zhí)行(Immediate Execution)還是延遲執(zhí)行(Deferred Execution)。PyTorch的易用性很大程度上取決于它如何實(shí)現(xiàn)即時執(zhí)行暮顺,因此我們在這里簡要介紹一下。
首先看一個例子,如果需要執(zhí)行勾股定理表達(dá)式,需要定義兩個變量a和b舷蟀,Python實(shí)現(xiàn)過程如下:
>>> a = 3
>>>?b?=?4
>>> c = (a**2 + b**2) ** 0.5
>>> c
5.0
像這樣獲取輸入值(a跃巡、b)后得到輸出值(c)的過程就是即時執(zhí)行。PyTorch就像普通的Python程序一樣洞难,默認(rèn)執(zhí)行方式為即時執(zhí)行(在PyTorch文檔中稱為Eager Mode)。即時執(zhí)行非常有用揭朝,如果在執(zhí)行某個表達(dá)式時出現(xiàn)錯誤队贱,Python的解釋器、調(diào)試器或者其他類似的工具都能夠直接訪問到相關(guān)的Python對象潭袱,并且在執(zhí)行出錯的地方會直接拋出異常柱嫌。
現(xiàn)在討論另一種情況,我們可以先行定義勾股定理表達(dá)式而不必關(guān)心具體的輸入是什么屯换,隨后我們在獲取到具體的輸入時再使用該表達(dá)式得到輸出编丘。我們定義的函數(shù)可以根據(jù)不同的輸入而隨時調(diào)用。比如像這樣:
>>> p = lambda a,b: (a**2 + b**2) ** 0.5
>>> p(1,2)
2.23606797749979
>>> p(3,4)
5.0
在第二個例子中彤悔,我們定義了一系列待執(zhí)行的操作嘉抓,并返回一個輸出函數(shù)(p)。然而直到我們給定了具體的輸入蜗巧,這些操作才會執(zhí)行掌眠。這個例子就是延遲執(zhí)行。延遲執(zhí)行意味著大多數(shù)異常是在函數(shù)調(diào)用時(而不是在函數(shù)定義時)拋出的幕屹。對于普通的Python程序(第二個例子)來說蓝丙,這種實(shí)現(xiàn)方式很不錯。這是因?yàn)樵诔绦虬l(fā)生錯誤時望拖,解釋器和調(diào)試器能夠完全訪問到Python的狀態(tài)渺尘。
然而,如果我們使用具有大量運(yùn)算符重載的專用類说敏,允許那些即時執(zhí)行的操作推遲到后臺運(yùn)行鸥跟,情況就會變得復(fù)雜棘手。這些專用類看上去可能長這個樣子:(作者你又開始暗指某廠了 - -)
>>> a = InputParameterPlaceholder()
>>> b = InputParameterPlaceholder()
>>> c = (a**2 + b**2) ** 0.5
>>> callable(c)
True
>>> c(3, 4)
5.0
通常,在采用上述這種函數(shù)定義形式的庫中医咨,對a和b求平方枫匾、相加再取平方根的操作不會被記錄為高級的Python字節(jié)碼。相反拟淮,這些庫更傾向于將表達(dá)式編譯成靜態(tài)計(jì)算圖(包含基本操作的圖)干茉,這種方式相比于純Python代碼的形式可能會有一些優(yōu)勢(例如出于性能原因?qū)?shù)學(xué)公式直接編譯為機(jī)器代碼)。
計(jì)算圖在某處定義很泊,卻在別處執(zhí)行角虫,這會使得調(diào)試變得更加困難。因?yàn)楫惓Mǔ笔шP(guān)于錯誤的具體信息委造,并且Python的調(diào)試工具也看不到數(shù)據(jù)的中間狀態(tài)戳鹅。另外,靜態(tài)圖通常不能很好地與標(biāo)準(zhǔn)Python控制流融合:它們實(shí)際上是基于宿主語言(本例中是Python)而實(shí)現(xiàn)的針對特定領(lǐng)域的語言昏兆。
接下來枫虏,我們將更具體地研究即時執(zhí)行和延遲執(zhí)行之間的區(qū)別,特別是在與神經(jīng)網(wǎng)絡(luò)相關(guān)的情況下亮垫。在這里我們不會深入地探索這些概念模软,而是在宏觀角度上給讀者介紹一下這些概念中的術(shù)語及其關(guān)系伟骨。理解這些概念及其關(guān)系可以幫助我們理解像PyTorch這種使用即時執(zhí)行的框架與使用延遲執(zhí)行的框架之間的區(qū)別饮潦,盡管這兩種類型框架的底層數(shù)學(xué)原理是相同的。
神經(jīng)網(wǎng)絡(luò)的基本組成單元是神經(jīng)元携狭。大量的神經(jīng)元串在一起構(gòu)成神經(jīng)網(wǎng)絡(luò)继蜡。
下圖中第一行展示的是單個神經(jīng)元典型的數(shù)學(xué)表達(dá)式:
o =?tanh(w * x + b)
在我們解釋下圖中的執(zhí)行模式前,有以下幾點(diǎn)需要說明:
x 是單個神經(jīng)元計(jì)算的輸入逛腿。
w 和 b 是神經(jīng)元的參數(shù)或者說是權(quán)重稀并,根據(jù)需求它們的值可以被更新。
為了更新參數(shù)(生成更接近我們預(yù)期的輸出)单默,我們通過反向傳播將誤差分配給每個權(quán)重碘举,然后相應(yīng)地對每個權(quán)重進(jìn)行調(diào)整。
反向傳播需要計(jì)算輸出相對于權(quán)重的梯度搁廓。
我們使用自動微分來自動計(jì)算梯度引颈,節(jié)省了手工編寫微分計(jì)算的麻煩。
在上圖中境蜕,神經(jīng)元被編譯進(jìn)一個符號圖蝙场,其中每個節(jié)點(diǎn)表示一個獨(dú)立的操作(第二行),并對輸入和輸出使用占位符粱年。然后售滤,在將具體數(shù)字放入占位符中后(在本例中,放入占位符中的數(shù)字是存儲在變量w、x和b中的值)完箩,該計(jì)算圖便進(jìn)行數(shù)值運(yùn)算(第三行)赐俗。輸出結(jié)果相對于權(quán)重的梯度是通過自動微分構(gòu)造的,該自動微分反向遍歷計(jì)算圖并在各個節(jié)點(diǎn)處乘以梯度(第四行)弊知。第五行展示了相應(yīng)的數(shù)學(xué)表達(dá)式秃励。
TensorFlow是深度學(xué)習(xí)框架中主要的競爭對手之一(作者表示我不想暗指了,沒錯就是你吉捶,hiahia)夺鲜,采用類似延遲執(zhí)行的靜態(tài)圖模式。靜態(tài)圖模式是TensorFlow 1.0中的默認(rèn)執(zhí)行模式呐舔。相比之下币励,PyTorch使用了一個按運(yùn)行定義的動態(tài)圖引擎,其中計(jì)算圖是逐節(jié)點(diǎn)構(gòu)建的珊拼,同時代碼是即時執(zhí)行的食呻。
下圖中的上半部分展示了在動態(tài)圖引擎下的計(jì)算操作,這一部分和靜態(tài)圖模式是相同的澎现。計(jì)算操作被分解成獨(dú)立的表達(dá)式仅胞,當(dāng)執(zhí)行到這些表達(dá)式時,它們會即時運(yùn)算剑辫。該程序?qū)@些計(jì)算之間的內(nèi)在聯(lián)系沒有預(yù)先的概念干旧。下圖中的下半部分展示了表達(dá)式的動態(tài)計(jì)算圖的后臺運(yùn)行場景。表達(dá)式仍然被分解為獨(dú)立的操作妹蔽,但是這些操作會即時運(yùn)算求值椎眯,進(jìn)而促使動態(tài)圖逐步建立。自動微分是通過反向遍歷運(yùn)算結(jié)果圖來實(shí)現(xiàn)的胳岂,類似于靜態(tài)計(jì)算圖编整。注意,這并不意味著動態(tài)圖自身就比靜態(tài)圖更有能力乳丰,只是動態(tài)圖通常更容易完成循環(huán)或條件判斷這些操作掌测。
動態(tài)圖在連續(xù)的前向傳遞過程中可能會發(fā)生變化。例如产园,可以根據(jù)前面節(jié)點(diǎn)的輸出情況調(diào)用不同的節(jié)點(diǎn)汞斧,而不需要在圖本身中表示出這些情況,這與靜態(tài)圖模式相比有明顯的優(yōu)勢(有理有據(jù)淆两,加十分)断箫。
目前主要的一些框架都趨向于支持以上兩種操作模式。PyTorch 1.0能夠在靜態(tài)計(jì)算圖中記錄模型的執(zhí)行情況秋冰,或通過預(yù)編譯的腳本語言對其進(jìn)行定義仲义,從而提高了性能并易于將模型部署到工業(yè)生產(chǎn)。TensorFlow也增加了"eager mode",一種新的按運(yùn)行定義的API埃撵,它增加了TensorFlow庫的靈活性赵颅。