深度學習框架太抽象?其實不外乎這五大核心組件
許多初學者覺得深度學習框架抽象,雖然調用了幾個函數/方法,計算了幾個數學難題,但始終不能理解這些框架的全貌。
為了更好地認識深度學習框架,也為了給一些想要自己親手搭建深度學習框架的朋友提供一些基礎性的指導,日前來自蘇黎世聯邦理工學院計算機科學系的碩士研究生Gokula Krishnan Santhanam在博客上撰文,概括了大部分深度學習框架都會包含的五大核心組件,為我們詳細剖析了深度學習框架一般性的內部組織結構。以下由雷鋒網編譯。
Gokula Krishnan Santhanam認為,大部分深度學習框架都包含以下五個核心組件:
1. 張量(Tensor)
張量是所有深度學習框架中最核心的組件,因為後續的所有運算和優化算法都是基於張量進行的。幾何代數中定義的張量是基於向量和矩陣的推廣,通俗一點理解的話,我們可以將標量視為零階張量,矢量視為一階張量,那麼矩陣就是二階張量。
舉例來説,我們可以將任意一張RGB彩色圖片表示成一個三階張量(三個維度分別是圖片的高度、寬度和色彩數據)。如下圖所示是一張普通的水果圖片,按照RGB三原色表示,其可以拆分為三張紅色、綠色和藍色的灰度圖片,如果將這種表示方法用張量的形式寫出來,就是圖中最下方的那張表格。
圖中只顯示了前5行、320列的數據,每個方格代表一個像素點,其中的數據[1.0, 1.0, 1.0]即為顏色。假設用[1.0, 0, 0]表示紅色,[0, 1.0, 0]表示綠色,[0, 0, 1.0]表示藍色,那麼如圖所示,前面5行的數據則全是白色。
將這一定義進行擴展,我們也可以用四階張量表示一個包含多張圖片的數據集,其中的四個維度分別是:圖片在數據集中的編號,圖片高度、寬度,以及色彩數據。
將各種各樣的數據抽象成張量表示,然後再輸入神經網絡模型進行後續處理是一種非常必要且高效的策略。因為如果沒有這一步驟,我們就需要根據各種不同類型的數據組織形式定義各種不同類型的數據操作,這會浪費大量的開發者精力。更關鍵的是,當數據處理完成後,我們還可以方便地將張量再轉換回想要的格式。例如Python NumPy包中numpy.imread和numpy.imsave兩個方法,分別用來將圖片轉換成張量對象(即代碼中的Tensor對象),和將張量再轉換成圖片保存起來。
2. 基於張量的各種操作
有了張量對象之後,下面一步就是一系列針對這一對象的數學運算和處理過程。
其實,整個神經網絡都可以簡單視為為了達到某種目的,針對輸入張量進行的一系列操作過程。而所謂的“學習”就是不斷糾正神經網絡的實際輸出結果和預期結果之間誤差的過程。這裏的一系列操作包含的範圍很寬,可以是簡單的矩陣乘法,也可以是卷積、池化和LSTM等稍複雜的運算。而且各框架支持的張量操作通常也不盡相同,詳細情況可以查看其官方文檔(如下為NumPy、Theano和TensorFlow的説明文檔)。
NumPy:http://www.scipy-lectures.org/intro/numpy/operations.html
Theano:http://deeplearning.net/software/theano/library/tensor/basic.html
TensorFlow:https://www.tensorflow.org/api_docs/python/math_ops/
需要指出的是,大部分的張量操作都是基於類實現的(而且是抽象類),而並不是函數(這一點可能要歸功於大部分的深度學習框架都是用面向對象的編程語言實現的)。這種實現思路一方面允許開發者將各種類似的操作彙總在一起,方便組織管理。另一方面也保證了整個代碼的複用性、擴展性和對外接口的統一。總體上讓整個框架更靈活和易於擴展,為將來的發展預留了空間。
3. 計算圖(Computation Graph)
有了張量和基於張量的各種操作之後,下一步就是將各種操作整合起來,輸出我們需要的結果。
但不幸的是,隨着操作種類和數量的增多,有可能引發各種意想不到的問題,包括多個操作之間應該並行還是順次執行,如何協同各種不同的底層設備,以及如何避免各種類型的宂餘操作等等。這些問題有可能拉低整個深度學習網絡的運行效率或者引入不必要的Bug,而計算圖正是為解決這一問題產生的。
據雷鋒網了解,計算圖首次被引入人工智能領域是在2009年的論文《Learning Deep Architectures for AI》。當時的圖片如下所示,作者用不同的佔位符(*,+,sin)構成操作結點,以字母x、a、b構成變量結點,再以有向線段將這些結點連接起來,組成一個表徵運算邏輯關係的清晰明瞭的“圖”型數據結構,這就是最初的計算圖。
後來隨着技術的不斷演進,加上腳本語言和低級語言各自不同的特點(概括地説,腳本語言建模方便但執行緩慢,低級語言則正好相反),因此業界逐漸形成了這樣的一種開發框架:前端用Python等腳本語言建模,後端用C++等低級語言執行(這裏低級是就應用層而言),以此綜合了兩者的優點。可以看到,這種開發框架大大降低了傳統框架做跨設備計算時的代碼耦合度,也避免了每次後端變動都需要修改前端的維護開銷。而這裏,在前端和後端之間起到關鍵耦合作用的就是計算圖。
將計算圖作為前後端之間的中間表示(Intermediate Representations)可以帶來良好的交互性,開發者可以將Tensor對象作為數據結構,函數/方法作為操作類型,將特定的操作類型應用於特定的數據結構,從而定義出類似MATLAB的強大建模語言。
需要注意的是,通常情況下開發者不會將用於中間表示得到的計算圖直接用於模型構造,因為這樣的計算圖通常包含了大量的宂餘求解目標,也沒有提取共享變量,因而通常都會經過依賴性剪枝、符號融合、內存共享等方法對計算圖進行優化。
目前,各個框架對於計算圖的實現機制和側重點各不相同。例如Theano和MXNet都是以隱式處理的方式在編譯中由表達式向計算圖過渡。而Caffe則比較直接,可以創建一個Graph對象,然後以類似Graph.Operator(xxx)的方式顯示調用。
因為計算圖的引入,開發者得以從宏觀上俯瞰整個神經網絡的內部結構,就好像編譯器可以從整個代碼的角度決定如何分配寄存器那樣,計算圖也可以從宏觀上決定代碼運行時的GPU內存分配,以及分佈式環境中不同底層設備間的相互協作方式。除此之外,現在也有許多深度學習框架將計算圖應用於模型調試,可以實時輸出當前某一操作類型的文本描述。
4. 自動微分(Automatic Differentiation)工具
計算圖帶來的另一個好處是讓模型訓練階段的梯度計算變得模塊化且更為便捷,也就是自動微分法。
正如前面提到的,因為我們可以將神經網絡視為由許多非線性過程組成的一個複雜的函數體,而計算圖則以模塊化的方式完整表徵了這一函數體的內部邏輯關係,因此微分這一複雜函數體,即求取模型梯度的方法就變成了在計算圖中簡單地從輸入到輸出進行一次完整遍歷的過程。與自動微分對應,業內更傳統的做法是符號微分。
符號微分即常見的求導分析。針對一些非線性過程(如修正線性單元ReLU)或者大規模的問題,使用符號微分法的成本往往非常高昂,有時甚至不可行(即不可微)。因此,以上述迭代式的自動微分法求解模型梯度已經被廣泛採用。並且由於自動微分可以成功應對一些符號微分不適用的場景,目前許多計算圖程序包(例如Computation Graph Toolkit)都已經預先實現了自動微分。
另外,由於每個節點處的導數只能相對於其相鄰節點計算,因此實現了自動微分的模塊一般都可以直接加入任意的操作類中,當然也可以被上層的微分大模塊直接調用。
5. BLAS、cuBLAS、cuDNN等拓展包
現在,通過上述所有模塊,我們已經可以搭建一個全功能的深度學習框架:將待處理數據轉換為張量,針對張量施加各種需要的操作,通過自動微分對模型展開訓練,然後得到輸出結果開始測試。這時還缺什麼呢?答案是運算效率。
由於此前的大部分實現都是基於高級語言的(如Java、Python、Lua等),而即使是執行最簡單的操作,高級語言也會比低級語言消耗更多的CPU週期,更何況是結構複雜的深度神經網絡,因此運算緩慢就成了高級語言的一個天然的缺陷。
目前針對這一問題有兩種解決方案。
第一種方法是模擬傳統的編譯器。就好像傳統編譯器會把高級語言編譯成特定平台的彙編語言實現高效運行一樣,這種方法將高級語言轉換為C語言,然後在C語言基礎上編譯、執行。為了實現這種轉換,每一種張量操作的實現代碼都會預先加入C語言的轉換部分,然後由編譯器在編譯階段將這些由C語言實現的張量操作綜合在一起。目前pyCUDA和Cython等編譯器都已經實現了這一功能。
第二種方法就是前文提到的,利用腳本語言實現前端建模,用低級語言如C++實現後端運行,這意味着高級語言和低級語言之間的交互都發生在框架內部,因此每次的後端變動都不需要修改前端,也不需要完整編譯(只需要通過修改編譯參數進行部分編譯),因此整體速度也就更快。
除此之外,由於低級語言的最優化編程難度很高,而且大部分的基礎操作其實也都有公開的最優解決方案,因此另一個顯著的加速手段就是利用現成的擴展包。例如最初用Fortran實現的BLAS(基礎線性代數子程序),就是一個非常優秀的基本矩陣(張量)運算庫,此外還有英特爾的MKL(Math Kernel Library)等,開發者可以根據個人喜好靈活選擇。
值得一提的是,一般的BLAS庫只是針對普通的CPU場景進行了優化,但目前大部分的深度學習模型都已經開始採用並行GPU的運算模式,因此利用諸如NVIDIA推出的針對GPU優化的cuBLAS和cuDNN等更據針對性的庫可能是更好的選擇。
運算速度對於深度學習框架來説至關重要,例如同樣訓練一個神經網絡,不加速需要4天的時間,加速的話可能只要4小時。在快速發展的人工智能領域,特別是對那些成立不久的人工智能初創公司而言,這種差別可能就會決定誰是先驅者,而誰是追隨者。
總結
原文作者在文末指出:為了向開發者提供儘量簡單的接口,大部分深度學習框架通常都會將普通的概念抽象化,這可能是造成許多用户感知不到上述五點核心組件的重要原因。
而這也正是作者寫本文的初衷:他希望開發者能夠通過了解不同框架之間的一些相似特性,更好地認識和使用一個深度學習框架。另一方面,對於那些不僅對學會使用深度學習框架感興趣,還打算親手搭建一個深度框架的朋友,作者認為了解各框架的內部組成和一些共性的特徵也是邁向成功的重要一步。他真誠地相信,一個優秀的工程師不僅應該“知其然”,更應該“知其所以然”。
來源:medium,雷鋒網(公眾號:雷鋒網)編譯
雷鋒網版權文章,未經授權禁止轉載。詳情見轉載須知。
資料來源:雷鋒網
作者/編輯:恆亮
為了更好地認識深度學習框架,也為了給一些想要自己親手搭建深度學習框架的朋友提供一些基礎性的指導,日前來自蘇黎世聯邦理工學院計算機科學系的碩士研究生Gokula Krishnan Santhanam在博客上撰文,概括了大部分深度學習框架都會包含的五大核心組件,為我們詳細剖析了深度學習框架一般性的內部組織結構。以下由雷鋒網編譯。
Gokula Krishnan Santhanam認為,大部分深度學習框架都包含以下五個核心組件:
引用1. 張量(Tensor)
2. 基於張量的各種操作
3. 計算圖(Computation Graph)
4. 自動微分(Automatic Differentiation)工具
5. BLAS、cuBLAS、cuDNN等拓展包
1. 張量(Tensor)
張量是所有深度學習框架中最核心的組件,因為後續的所有運算和優化算法都是基於張量進行的。幾何代數中定義的張量是基於向量和矩陣的推廣,通俗一點理解的話,我們可以將標量視為零階張量,矢量視為一階張量,那麼矩陣就是二階張量。
舉例來説,我們可以將任意一張RGB彩色圖片表示成一個三階張量(三個維度分別是圖片的高度、寬度和色彩數據)。如下圖所示是一張普通的水果圖片,按照RGB三原色表示,其可以拆分為三張紅色、綠色和藍色的灰度圖片,如果將這種表示方法用張量的形式寫出來,就是圖中最下方的那張表格。
圖中只顯示了前5行、320列的數據,每個方格代表一個像素點,其中的數據[1.0, 1.0, 1.0]即為顏色。假設用[1.0, 0, 0]表示紅色,[0, 1.0, 0]表示綠色,[0, 0, 1.0]表示藍色,那麼如圖所示,前面5行的數據則全是白色。
將這一定義進行擴展,我們也可以用四階張量表示一個包含多張圖片的數據集,其中的四個維度分別是:圖片在數據集中的編號,圖片高度、寬度,以及色彩數據。
將各種各樣的數據抽象成張量表示,然後再輸入神經網絡模型進行後續處理是一種非常必要且高效的策略。因為如果沒有這一步驟,我們就需要根據各種不同類型的數據組織形式定義各種不同類型的數據操作,這會浪費大量的開發者精力。更關鍵的是,當數據處理完成後,我們還可以方便地將張量再轉換回想要的格式。例如Python NumPy包中numpy.imread和numpy.imsave兩個方法,分別用來將圖片轉換成張量對象(即代碼中的Tensor對象),和將張量再轉換成圖片保存起來。
2. 基於張量的各種操作
有了張量對象之後,下面一步就是一系列針對這一對象的數學運算和處理過程。
其實,整個神經網絡都可以簡單視為為了達到某種目的,針對輸入張量進行的一系列操作過程。而所謂的“學習”就是不斷糾正神經網絡的實際輸出結果和預期結果之間誤差的過程。這裏的一系列操作包含的範圍很寬,可以是簡單的矩陣乘法,也可以是卷積、池化和LSTM等稍複雜的運算。而且各框架支持的張量操作通常也不盡相同,詳細情況可以查看其官方文檔(如下為NumPy、Theano和TensorFlow的説明文檔)。
NumPy:http://www.scipy-lectures.org/intro/numpy/operations.html
Theano:http://deeplearning.net/software/theano/library/tensor/basic.html
TensorFlow:https://www.tensorflow.org/api_docs/python/math_ops/
需要指出的是,大部分的張量操作都是基於類實現的(而且是抽象類),而並不是函數(這一點可能要歸功於大部分的深度學習框架都是用面向對象的編程語言實現的)。這種實現思路一方面允許開發者將各種類似的操作彙總在一起,方便組織管理。另一方面也保證了整個代碼的複用性、擴展性和對外接口的統一。總體上讓整個框架更靈活和易於擴展,為將來的發展預留了空間。
3. 計算圖(Computation Graph)
有了張量和基於張量的各種操作之後,下一步就是將各種操作整合起來,輸出我們需要的結果。
但不幸的是,隨着操作種類和數量的增多,有可能引發各種意想不到的問題,包括多個操作之間應該並行還是順次執行,如何協同各種不同的底層設備,以及如何避免各種類型的宂餘操作等等。這些問題有可能拉低整個深度學習網絡的運行效率或者引入不必要的Bug,而計算圖正是為解決這一問題產生的。
據雷鋒網了解,計算圖首次被引入人工智能領域是在2009年的論文《Learning Deep Architectures for AI》。當時的圖片如下所示,作者用不同的佔位符(*,+,sin)構成操作結點,以字母x、a、b構成變量結點,再以有向線段將這些結點連接起來,組成一個表徵運算邏輯關係的清晰明瞭的“圖”型數據結構,這就是最初的計算圖。
後來隨着技術的不斷演進,加上腳本語言和低級語言各自不同的特點(概括地説,腳本語言建模方便但執行緩慢,低級語言則正好相反),因此業界逐漸形成了這樣的一種開發框架:前端用Python等腳本語言建模,後端用C++等低級語言執行(這裏低級是就應用層而言),以此綜合了兩者的優點。可以看到,這種開發框架大大降低了傳統框架做跨設備計算時的代碼耦合度,也避免了每次後端變動都需要修改前端的維護開銷。而這裏,在前端和後端之間起到關鍵耦合作用的就是計算圖。
將計算圖作為前後端之間的中間表示(Intermediate Representations)可以帶來良好的交互性,開發者可以將Tensor對象作為數據結構,函數/方法作為操作類型,將特定的操作類型應用於特定的數據結構,從而定義出類似MATLAB的強大建模語言。
需要注意的是,通常情況下開發者不會將用於中間表示得到的計算圖直接用於模型構造,因為這樣的計算圖通常包含了大量的宂餘求解目標,也沒有提取共享變量,因而通常都會經過依賴性剪枝、符號融合、內存共享等方法對計算圖進行優化。
目前,各個框架對於計算圖的實現機制和側重點各不相同。例如Theano和MXNet都是以隱式處理的方式在編譯中由表達式向計算圖過渡。而Caffe則比較直接,可以創建一個Graph對象,然後以類似Graph.Operator(xxx)的方式顯示調用。
因為計算圖的引入,開發者得以從宏觀上俯瞰整個神經網絡的內部結構,就好像編譯器可以從整個代碼的角度決定如何分配寄存器那樣,計算圖也可以從宏觀上決定代碼運行時的GPU內存分配,以及分佈式環境中不同底層設備間的相互協作方式。除此之外,現在也有許多深度學習框架將計算圖應用於模型調試,可以實時輸出當前某一操作類型的文本描述。
4. 自動微分(Automatic Differentiation)工具
計算圖帶來的另一個好處是讓模型訓練階段的梯度計算變得模塊化且更為便捷,也就是自動微分法。
正如前面提到的,因為我們可以將神經網絡視為由許多非線性過程組成的一個複雜的函數體,而計算圖則以模塊化的方式完整表徵了這一函數體的內部邏輯關係,因此微分這一複雜函數體,即求取模型梯度的方法就變成了在計算圖中簡單地從輸入到輸出進行一次完整遍歷的過程。與自動微分對應,業內更傳統的做法是符號微分。
符號微分即常見的求導分析。針對一些非線性過程(如修正線性單元ReLU)或者大規模的問題,使用符號微分法的成本往往非常高昂,有時甚至不可行(即不可微)。因此,以上述迭代式的自動微分法求解模型梯度已經被廣泛採用。並且由於自動微分可以成功應對一些符號微分不適用的場景,目前許多計算圖程序包(例如Computation Graph Toolkit)都已經預先實現了自動微分。
另外,由於每個節點處的導數只能相對於其相鄰節點計算,因此實現了自動微分的模塊一般都可以直接加入任意的操作類中,當然也可以被上層的微分大模塊直接調用。
5. BLAS、cuBLAS、cuDNN等拓展包
現在,通過上述所有模塊,我們已經可以搭建一個全功能的深度學習框架:將待處理數據轉換為張量,針對張量施加各種需要的操作,通過自動微分對模型展開訓練,然後得到輸出結果開始測試。這時還缺什麼呢?答案是運算效率。
由於此前的大部分實現都是基於高級語言的(如Java、Python、Lua等),而即使是執行最簡單的操作,高級語言也會比低級語言消耗更多的CPU週期,更何況是結構複雜的深度神經網絡,因此運算緩慢就成了高級語言的一個天然的缺陷。
目前針對這一問題有兩種解決方案。
第一種方法是模擬傳統的編譯器。就好像傳統編譯器會把高級語言編譯成特定平台的彙編語言實現高效運行一樣,這種方法將高級語言轉換為C語言,然後在C語言基礎上編譯、執行。為了實現這種轉換,每一種張量操作的實現代碼都會預先加入C語言的轉換部分,然後由編譯器在編譯階段將這些由C語言實現的張量操作綜合在一起。目前pyCUDA和Cython等編譯器都已經實現了這一功能。
第二種方法就是前文提到的,利用腳本語言實現前端建模,用低級語言如C++實現後端運行,這意味着高級語言和低級語言之間的交互都發生在框架內部,因此每次的後端變動都不需要修改前端,也不需要完整編譯(只需要通過修改編譯參數進行部分編譯),因此整體速度也就更快。
除此之外,由於低級語言的最優化編程難度很高,而且大部分的基礎操作其實也都有公開的最優解決方案,因此另一個顯著的加速手段就是利用現成的擴展包。例如最初用Fortran實現的BLAS(基礎線性代數子程序),就是一個非常優秀的基本矩陣(張量)運算庫,此外還有英特爾的MKL(Math Kernel Library)等,開發者可以根據個人喜好靈活選擇。
值得一提的是,一般的BLAS庫只是針對普通的CPU場景進行了優化,但目前大部分的深度學習模型都已經開始採用並行GPU的運算模式,因此利用諸如NVIDIA推出的針對GPU優化的cuBLAS和cuDNN等更據針對性的庫可能是更好的選擇。
運算速度對於深度學習框架來説至關重要,例如同樣訓練一個神經網絡,不加速需要4天的時間,加速的話可能只要4小時。在快速發展的人工智能領域,特別是對那些成立不久的人工智能初創公司而言,這種差別可能就會決定誰是先驅者,而誰是追隨者。
總結
原文作者在文末指出:為了向開發者提供儘量簡單的接口,大部分深度學習框架通常都會將普通的概念抽象化,這可能是造成許多用户感知不到上述五點核心組件的重要原因。
而這也正是作者寫本文的初衷:他希望開發者能夠通過了解不同框架之間的一些相似特性,更好地認識和使用一個深度學習框架。另一方面,對於那些不僅對學會使用深度學習框架感興趣,還打算親手搭建一個深度框架的朋友,作者認為了解各框架的內部組成和一些共性的特徵也是邁向成功的重要一步。他真誠地相信,一個優秀的工程師不僅應該“知其然”,更應該“知其所以然”。
來源:medium,雷鋒網(公眾號:雷鋒網)編譯
雷鋒網版權文章,未經授權禁止轉載。詳情見轉載須知。
資料來源:雷鋒網
作者/編輯:恆亮