從概念到實踐,我們該如何構建自動微分庫

2020-12-05 機器之心Pro

像 PyTorch 或 TensorFlow 這樣通用的自動微分框架是非常有用和高效的,而且在大多數情況下,幾乎不需要再寫一些更專門化的東西。然而本文作者構建了一個自動微分庫,以高效地計算小批量數據上的訓練。此外,作者還詳細描述了在構建自動微分庫中的過程與思考,是理解自動微分理念的優秀博文。

我最近開始寫自己的 autodiff 程序包。這篇博客文章記錄了我一路以來學到的東西,並把它當成 Julia Evans 的「窮人版」博客文章。

因為有許多博客文章都解釋了自動微分的機制,比我解釋的要好得多,所以這裡我跳過了解釋。此外,在構建神經網絡結構方面還有其他一些有趣的文章,因此,雖然我的庫遵循非常相似的模式(靜態計算圖和依賴類型),但我並沒有過多地關注類型系統。

最後,如果你想直接跳轉到代碼部分,最終的結果在以下 GitHub 地址,同時還有一個基於神經網絡的 FizzBuzz 解決方案。

自動微分代碼:https://github.com/maciejkula/wyrm

FizzBuzz:https://github.com/maciejkula/fizzbuzz

動機

關於為什麼我想要有自己的 autodiff/backprop 框架,而不是直接用 PyTorch 或者 TensorFlow 的原因有以下幾點。

1.PyTorch 和 TF 在擬合每個 x 小批量所需計算量很少的模型時非常慢。因為在計算機視覺問題中,每個小批量處理的計算量非常大,以至於框架開銷幾乎不成問題。但這在矩陣分解式的模型中卻不可忽略,這種模型在推薦系統中是有用的。且即使在 GPU 上,擬合這些模型也很慢。

2. 我希望能夠用我的 autodiff 庫像 Python 包那樣以最小的依賴關係來編寫和構造模型。這個庫能夠生成一個相當小的、且獨立的二進位文件,這是相對於繁瑣的 TF 和 PyTorch 依賴的優勢。

3. 這是一個有趣的學習經驗,並且讓我更詳細地了解神經網絡庫的內部工作機制。

受到對推薦模型(也可能是 NLP) 有效的輕量級解決方案需求的啟發,我編寫了一系列的設計約束條件(design constraints)。

1. 我希望框架能夠自然地支持稀疏梯度:即絕大多數梯度都為零的情況。這在 NLP 和使用大型嵌入層的推薦模型中非常常見。在任何給定的小批量中,只有很小一部分嵌入層被使用,其餘記錄的梯度均為零。在執行梯度更新時能夠跳過零對於快速創建這些模型非常重要。

2. 我希望除實際計算之外,框架有最小的開銷。因為我主要想要擬合小的、稀疏的模型,所以開銷是關鍵。在 PyTorch 中,此類模型的運行時間以 Python 中的循環為主要開銷。為了避免這種情況,我的庫必須在它的擬合循環中放棄 Python,並且需要完全用編譯語言編寫以充分利用編譯器優化的性質。

3. 模型圖必須逐個定義,就像 Chainer 或者 PyTorch 一樣。這種方法的可用性和可調試性對我來說是非常有價值的,以至於我甚至不想回到 TensorFlow 的處理方式。同時,我很高興圖形一旦被定義就是靜態的。這有助於保持較小的開銷:我可以分配一次中間計算緩衝區並繼續使用它們,而不是寫一個複雜的緩衝池系統(或者,更糟糕的是,在每次傳遞的時候不斷地分配和釋放內存)。

4. 我希望性能可以與可用 CPU 內核的數量大致呈線性關係。這意味著在整個圖形的層次上進行並行化,而不是對單獨的操作。每個計算線程將有它自己的計算圖副本,但在更新時寫入共享參數緩衝區。這實際上是 Hogwild! 方法,這個方法中多個計算線程同時更新共享參數緩衝區而沒有任何鎖定。只要梯度相對稀疏,就可以在模型質量下降很少的情況下進行近線性的縮放。

這裡還列出了一些我現在不想添加或不太關心的事情:

1.GPU 支持。我主要想要擬合小型模型(或者至少有很多參數但每個小批量的計算很少的模型)。

2.CNNs,或者,實際上具有兩個維度以上的張量。

考慮到需求(和非需求)列表,我們就能自然地得出一些設計決策。

1. 整個事情將用一種編譯語言(compiled language)編寫,這種編譯語言能夠生成沒有運行時間的本地共享對象,模型也將用相同的語言來定義。

2. 這個語言將會是 Rust,這是一門令人驚嘆的語言,而且非常適合這種任務。因此下面的許多東西都有 Rust 的味道。然而,我所描述的設計權衡在 C++、其他靜態類型和 AOT 編譯的程式語言中是相同的。

3. 我將會使用反向模式自動微分。這樣,我可以很容易地通過多輸入的任意(靜態)計算圖進行反向傳播。

在編寫庫時,我經常想到 API,我希望能夠將這個微分庫公開並獲得社區的幫助。在這種情況下,我想寫如下內容:

let slope = Parameter::new(1.0);let intercept = Parameter::new(0.0);let x = Input::new(3.0);let y = Input::new(2.0 * 3.0 + 1.0);let loss = (y—(slope * x + intercept)).square();loss.backward();

並讓它工作。

準備工作完成之後,我們可以進入有趣的部分:弄清楚如何實現計算圖。

表示計算圖

我們選擇什麼樣的數據結構來表示計算圖?我了解有以下兩種方案:

1. 基於向量:所有計算節點都被連續地存儲在一個向量中,並使用索引來尋址它們的父節點。例如,在創建輸入節點時,對象 InputNode 被壓入向量,且索引為 0。如果隨後將該節點平方,SquareNode 將被壓入索引為 1 的分量,並知道它的父節點是索引 0。在正向傳播過程中,SquareNode 將使用該索引來獲取其輸入的值。

2. 基於圖形。節點被放置在內存中的任意位置,並用指向其父節點的索引來維護計算圖的結構。(向量表示可以看作是圖模型的線性化。)

基於矢量的方法有很多優點。

1. 所有的節點都在同一個地方。他們連續地儲存在內存中,可能會減少內存的尋址問題。

2. 他們的所有權很容易解釋。這使得克隆計算圖圖非常簡單:只需克隆節點向量即可。這一點很重要,因為我依靠於為我的並行處理方法提供多個圖的副本。

3. 節點按拓撲順序排列。我們可以通過簡單地沿著向量向前迭代來正確地執行前向傳播,且沒有重複的工作。

但是它也有缺點。

我們在節點向量中存儲了什麼類型的對象是不清楚的。所有的節點類型都不一樣(不同的大小),但向量都是同質的類型。Rust 為這種問題提供了兩種解決方案,但是都不是特別令人滿意。

第一種是枚舉(sum 類型;ADTs; tagged unions)。我們定義一個 Node 類型作為所有可能的節點類型的集合,並將其儲存在節點向量中。這樣,所有的節點就具有相同的類型了。但我們仍然需要將 Node 的方法從封裝的 Node 類型分配到所包含的內部節點。這可以通過模式匹配(聯合類型標籤上的 switch 語句)完成;有 Rust 對模式識別和宏的支持,編寫必要的代碼是輕而易舉的。

但是,這種做法會增加運行時間成本。每次我們使用一個節點,我們需要經過一個 switch 語句來解決內部類型問題。原則上,優化編譯器會將這種代碼編譯成跳轉表(jump tables)。實際上,在我的實驗中為分配代碼生成的程序集僅僅是對所有可能性的線性掃描,強加了與框架支持的具體節點類型數量呈線性關係的分配成本。更糟的是,編譯器不願意內聯 switch 本身和被調用的函數。前者是因為它增加了分支預測的失誤,後者增加了函數調用的開銷。(最近的分值預測攻擊加劇了這個問題: compiler mitigations 可能會導致像這樣的間接指令更加昂貴。)

對節點向量使用 sum 類型的最後一個缺點是它會導致一個封閉的系統(類似於 Scala『s 的 封閉特性):庫的下遊用戶不能添加新的節點類型。

另一種方法是用 Rust 的運行時多態機制(polymorphism mechanism): trait objects。trait objects 是對目標具體類型進行抽象的一種方法:我們將他們隱藏在指向數據的指針和他們方法表的後面,而不是將結構存儲在內聯中。調用方法時,我們跳轉到 vtable,找到函數並執行。通過使用 trait ojbects,我們將這些 fat pointers 放到節點向量中而不是節點自身裡面。

然而,這種解決方案恰恰引入了我們開始時想要避免的那種間接性。此外,它完全否認了編譯器在內聯方面做的努力:被調用的函數直到運行時才知道。

那麼基於圖的設計呢?在這裡,每個節點都在內存中被放置在自己的位置,並且可以通過索引指向其祖先。因為每個節點可以重複使用任意次數,我用 Rust 中的 Rc<T>相當於 C++中的 shared_ptr。

這種方法的一個直接缺點是模糊了圖的所有權結構,使克隆和序列化/反序列化變得困難:因為節點可以被重複利用,單純的克隆/反序列化將導致創建相同節點的多個副本。

第二個缺點是缺少一個容易獲得的拓撲排序:前向和後向傳遞都遞歸地完成,而且必須小心地避免重複計算共享子圖的值。

使用圖形表達的優點是在編譯時已知任何節點的父節點類型。每一個節點在其父節點類型上是(遞歸地)通用的:添加兩個 InputNodes 將會產生一個 AddNode<InputNode, InputNode>。將其添加到另一個輸入節點會產生 AddNode<AddNode<InputNode, InputNode>,InputNode>等等。除了一個在類型系統中表現更好的設計,這給了我分配和內聯的靜態方法。

結果

使用一些非正式的基準,基於圖的方法比基於向量的方法快大約 30%。最後的結果可以在我很普通的雙核筆記本上,20 毫秒內在 Movielens 100K 數據集上完整地運行一個 BPR 學習-排序分解模型。此外,它的性能會隨著處理器內核的增加而線性增長。

除了底層的圖形結構之後,這裡還利用了很多優化。

1. 我用 Rust 的 SIMD 內在函數進行了很多操作,如向量點積和標量加法。

2. 對於大多數操作,我假定 C 為連續矩陣並直接在底層數據上迭代,而不是用 ndarrays 迭代方法。事實證明,這樣做要快得多,大概是因為它允許 LLVM 自動對向量實現向量化。

3. 事實證明,LLVM 足夠智能,能夠自動向量化大部分不涉及縮減步驟(主要是賦值)的數值循環。與(2)結合起來看,這種方法使得很多的數值循環以最小的優化努力獲得更高的效率。

仍然有很多方法可以使計算速度更快。

1. 此時,代碼在正向傳遞中不會緩存任何子圖的結果:如果一個節點在正向傳遞中被用了兩次,所有依賴它的計算將會執行兩次。這可以通過一個簡單的拓撲排序算法很容易的解決,一旦評估了它們的值,就將該節點標記為已評估。

2. 類似地,在後向傳遞中梯度被直接傳遞給參數節點。如果一個節點被多次使用,這意味著在逐步向下傳遞梯度時做了不必要的工作。累積所有的梯度並且只遞歸一次將節省這項工作。

3. 對輸入有一些不必要的複製,在可能的情況下更好的利用索引應該會產生一些小的性能收益。

下一步是什麼

我寫了(並繼續維護)很多的開源 Python ML 包。這些模型是在 Cython 中手工編寫的,儘管它們表現的很好,但是擴展它們是困難的。部分是因為 Cython 的局限性,另一部分的原因在於手動派生更新規則所需的努力。

我希望這個庫(或它的一些變體)可以使這個任務變得簡單一些,並且可以讓我更輕鬆地實現複雜模型以將它們作為獨立的 Python 包發布出去。

相關焦點

  • 谷歌推出開源 Python 庫「Tangent」,支持前向模式自動微分
    據介紹,這個庫與現有的機器學習庫相比,存在諸多優勢,可以大大改善了用戶的使用體驗。雷鋒網 AI科技評論編譯整理如下:Tangent 是一個全新的免費開源 Python 庫,可以用於自動微分。與其他現有的機器學習庫相比,Tangent屬於源到源(source-to-source)系統,可以用 Python f 函數調用新的 Python 函數,計算出 f 的梯度。
  • NumPy、AI基礎設施可微分編程、技術實踐,這是一場開發者的盛會
    在開發者日下午的主單元中,機器之心從開發者最關心的技術話題出發,邀請到了多位大牛做主題演講,內容覆蓋程式語言、開發工具、技術創新與落地實踐等等。賈揚清:構建研究到產品 AI 基礎設施的經驗賈揚清是知名的人工智慧青年學者之一。在加入阿里巴巴之前,他曾任 Facebook AI 架構部門總監,負責前沿 AI 平臺的開發。
  • 可微分的「OpenCV」:這是基於PyTorch的可微計算機視覺庫
    機器之心整理參與:思如何打造一個可微分的 OpenCV?如何將圖像處理嵌入到訓練流程中?你需要 Kornia 這個開源可微的計算機視覺庫。但現在有一個問題,OpenCV 是不可微的,這意味著它更多的是做預處理等工作,而不能嵌入到整個訓練流程中。在這個項目中,開發者提出了一種新型開源可微分計算機視覺庫 Kornia,並且它建立在 PyTorch 之上。Kornia 包含了一組例程和可微分模塊,並致力於解決通用計算機視覺問題。
  • 使用Python從頭開始實現深度學習庫
    為此,可能需要將核心實現單元隱藏在幾個抽象層後面,這使得難以理解深度學習庫所基於的基本底層原理。因此,本文的目的是提供有關深度學習庫構建塊的見解。我們首先了解一些深度學習的背景知識,以了解功能要求,然後使用NumPy在python中使用一個簡單而完整的庫,該庫能夠端到端訓練神經網絡模型(非常簡單的類型)。在此過程中,我們將學習深度學習框架的各個組成部分。該庫不到100行代碼,因此應該很容易遵循。
  • 華為深度學習框架MindSpore正式開源:自動微分不止計算圖
    MindSpore 的整體結構,從後端的硬體支持到前端 API,中間會涉及多種優化與特性。例如不採用計算圖的自動微分、自動並行與優化計算過程等等。MindSpore 最大的特點在於,其採用了業界最新的 Source-to-Source 自動微分,它能利用編譯器及程式語言的底層技術,進一步優化以支持更好的微分表達。
  • 微分方程篇:為你構建微分方程框架
    考研數學中,總分150分,而微分方程大概能佔到10分左右,這是考研大綱中的出現的。一般考研中微分方程要麼是填空,要麼就是大題,所以就沒有了蒙的可能,這也就表示大家要想拿到這部分分就要自己弄懂了。在考研中這部分知識佔分不大,但是高數下半學期期末考試這可是重頭戲,所以大家想要拿到高分的話就要加油了。
  • 知道TF和PyTorch還不夠,快看看怎麼從PyTorch轉向自動微分神器JAX
    它具有正向和反向自動微分功能,非常擅長計算高階導數。這一嶄露頭角的框架究竟有多好用?怎樣用它來展示神經網絡內部複雜的梯度更新和反向傳播?本文是一個教程貼,教你理解 Jax 的底層邏輯,讓你更輕鬆地從 PyTorch 等進行遷移。Jax 是谷歌開發的一個 Python 庫,用於機器學習和數學計算。
  • 計算機圖形也能自動可微:MIT學神的微分太極框架開源
    機器之心機器之心報導參與:一鳴、杜偉去年5月,機器之心報導了 MIT 華人學神胡淵鳴等開源的計算機圖形庫——太極。近日,這位作者聯合其他研究者推出了自動微分版本的太極——微分太極。
  • 計算機圖形自動可微:MIT學神微分太極框架開源,論文被ICLR接收
    機器之心報導參與:一鳴、杜偉去年5月,機器之心報導了 MIT 華人學神胡淵鳴等開源的計算機圖形庫——太極。近日,這位作者聯合其他研究者推出了自動微分版本的太極——微分太極。這一框架可以基於太極實現自動微分,在物理模擬優化方面有很高的性能和靈活性。這意味著太極從計算機圖形學進入了機器學習的領域。
  • 需要知識的後深度學習時代,如何高效自動構建知識圖譜?
    如何自動、高效地構建知識圖譜?前沿的知識圖譜自動構建技術有哪些?這篇文章將逐一解答這些問題。 日常生活中,我們經常遇到以下兩種信息展現方式: 這個過程如何完成?這就要提到我們今天的主題——知識圖譜。 知識圖譜可以做什麼? 知識圖譜的概念於 2012 年由 guge 提出,當時主要被用來提高其搜尋引擎質量,改善用戶搜索體驗。
  • 百分點認知智能實驗室:信息抽取在知識圖譜構建中的實踐與應用
    百分點認知智能實驗室在實踐探索中,通過利用自然語言處理技術獲取結構化的信息抽取能力,探索出了一套行業知識圖譜構建流程方法。尤其是基於深度遷移學習,幫助構建法律百科詞條、公安文本知識圖譜等行業項目中,在實體抽取、關係抽取、事件抽取等方面都取得了理想的實踐效果。本文將從概念辨析、技術路徑、實踐總結,由虛到實、由淺入深引導大家理性看待知識圖譜技術的能與不能,以更好地在實踐中運籌帷幄。
  • 張江:從圖網絡到因果推斷,複雜系統自動建模五部曲
    比如人工股票市場,雖然它構建了一套機制,使得整個系統能夠與真實股票漲落趨勢非常接近,但實際上,這套機制與真實市場中的個體行為是完全無關的,所以不能用來做真實預測。其次,人工模型的構建與否建模者的個人經驗非常相關,它沒有統一的建模規則,非常依賴建模者的能力和啟發性思考。 「如何對複雜系統進行自動建模」,這是一個迫切的需求。
  • PyTorch實現,GitHub4000星:微軟開源的CV庫
    在各種計算機視覺模型和應用層出不窮的當下,如何把握髮展脈絡,跟進領域前沿發展呢?微軟創建了一個庫,提供構建計算機視覺系統的大量示例和最佳實踐指導原則。此外,該庫還展示了如何使用微軟的雲計算平臺 Azure,加快在大型數據集上的訓練速度或將模型部署為 web 服務。2. 圖像相似度該目錄提供了構建圖像相似度系統的示例和最佳實踐,旨在使用戶能夠基於自己的數據集方便快捷地訓練高精度模型。下圖為圖像檢索示例,其中左圖為查詢圖像,右面為與之最相似的 6 幅圖像:3.
  • 「神經常微分方程」提出者之一:如何利用深度微分方程模型處理連續...
    這裡參數化隱藏狀態的導數就類似構建了連續性的層級與參數,而不再是離散的層級。因此參數也是一個連續的空間,我們不需要再分層傳播梯度與更新參數。這篇論文證明了常微分方程可以解決複雜問題,算是對之前相關研究的一次總結。該論文獲獎後獲得了大量關注,而後來的一件事又把它推到了風頭浪尖。
  • 用NumPy寫深度模型,用Julia可微分編程寫函數,這是WAIC開發者日
    但隨著 AlexNet 的提出,錯誤率瞬間就降到了 15%,而且重要的是,這種方法有很大的潛力,之後的研究使 ImageNet 的錯誤率一直在降低,甚至低於「人類」識別錯誤率 5%。深度學習利用強大的擬合能力大大擴展了算法潛力,現在算法問題已經解決了,那麼算力問題呢,我們該怎樣利用計算系統提升訓練速度?
  • 一文解構神經常微分方程
    它描述了某個變量(這就是為什麼是常微分)在某個過程中的變化,這種隨時間的變化用導數來表示為:簡單的常微分方程例子如果存在一些初始條件(變化過程的起始點),並且想要觀察該過程將如何發展到某個最終狀態的話,我們可以探討此微分方程的求解。函數解也稱為積分曲線(因為可以對方程進行積分得到解x(t))。
  • 自動微分、並行加持,一次訓練,可多...
    首先說自動微分,它是指計算機藉助程序自動求出一個函數導數的通用方法。在深度學習中,通常指對網絡模型的自動求導,通過梯度指導對網絡權重的優化。  第三種是基於源碼轉換的通用自動微分,也就是MindSpore採用的技術。  這種方法,源以函數式編程框架為基礎,以即時編譯(JIT)的方式在中間表達上做自動微分變換,支持複雜控制流場景、高階函數和閉包。
  • 高數學習「瓶頸」突破之二:通俗化理解導數與微分概念
    面對這些學科的學習,不少學生感覺「枯燥無味」、「提不起學習興趣」、「不知道如何入手學習」……高等數學更是高度抽象和難於理解的學科,特別是其中的導數、微分、不定積分等概念,不少學生學習之後不知道是什麼意思。先看一下如下的書面定義:
  • 今日Paper|隨機微分方程;流式自動語音識別;圖像分類等
    from=leiphonecolumn_papereview0110推薦理由:作者回顧凸對偶性的基本概念,重點是非常普遍且極為有用的Fenchel-Rockafellar對偶性。 作者總結了如何將這種對偶性應用於各種強化學習(RL)設置,包括策略評估或優化,在線或離線學習以及打折或未打折的獎勵。
  • No.171 原創|布迪厄議題的實踐轉向:背景、邏輯與概念
    而且這種「與結構主義對立」的個體主義情緒甚至是一種明確的學術主張,吉登斯(Anthony Giddens)認為他的向結構中注入個體施動性的結構化理論「當中的一項基礎目的就是主張結構主義所構建的霸權體系的嘗試的破產」[9]。皮耶·布迪厄 Pierre Bourdieu對於施動問題的比較晚近的研究注意到了更為根本的個體行為如何產生的問題。