歡迎光臨
每天分享高質量文章

在GPU上執行,效能是NumPy的11倍,這個Python庫你值得擁有

導讀:NumPy是資料計算的基礎,更是深度學習框架的基石。但如果直接使用NumPy計算大資料,其效能已成為一個瓶頸。

隨著資料爆炸式增長,尤其是影象資料、音訊資料等資料的快速增長,迫切需要突破NumPy效能上的瓶頸。需求就是強大動力!透過大家的不懈努力,在很多方面取得可喜進展,如硬體有GPU,軟體有Theano、Keras、TensorFlow,演演算法有摺積神經網路、迴圈神經網路等。

Theano是Python的一個庫,為開源專案,在2008年,由Yoshua Bengio領導的加拿大蒙特利爾理工學院LISA實驗室開發。對於解決大量資料的問題,使用Theano可能獲得與手工用C實現差不多的效能。另外透過利用GPU,它能獲得比CPU上快很多數量級的效能。

至於Theano是如何實現效能方面的跨越,如何用“符號計算圖”來運算等內容,本文都將有所涉獵,但限於篇幅無法深入分析,只做一些基礎性的介紹。涵蓋的主要內容:

  • 如何安裝Theano。

  • 符號變數是什麼。

  • 如何設計符號計算圖。

  • 函式的功能。

  • 共享變數的妙用。

 

作者:吳茂貴,王冬,李濤,楊本法

如需轉載請聯絡大資料(ID:hzdashuju)

 

Theano開發者在2010年公佈的測試報告中指出:在CPU上執行程式時,Theano程式效能是NumPy的1.8倍,而在GPU上是NumPy的11倍。這還是2010年的測試結果,近些年無論是Theano還是GPU,效能都有顯著提高。

 

這裡我們把Theano作為基礎來講,除了效能方面的跨越外,它還是“符合計算圖”的開創者,當前很多優秀的開源工具,如TensorFlow、Keras等,都派生於或借鑒了Theano的底層設計。所以瞭解Theano的使用,將有助於我們更好地學習TensorFlow、Keras等其他開源工具。

 

 

 

01 安裝

 

這裡主要介紹Linux+Anaconda+theano環境的安裝說明,在CentOS或Ubuntu環境下,建議使用Python的Anaconda發行版,後續版本升級或新增新模組可用Conda工具。當然也可用pip進行安裝。但最好使用工具來安裝,這樣可以避免很多程式依賴的麻煩,而且日後的軟體升級維護也很方便。

 

Theano支援CPU、GPU,如果使用GPU還需要安裝其驅動程式如CUDA等,限於篇幅,這裡只介紹CPU的,有關GPU的安裝,大家可參考:

http://www.deeplearning.net/software/theano/install.html

 

以下為主要安裝步驟:

 

1. 安裝anaconda

 

從anaconda官網下載Linux環境最新的軟體包,Python版本建議選擇3系列的,2系列後續將不再維護。

anaconda官網:

https://www.anaconda.com/download/

下載檔案為一個sh程式包,如Anaconda3-4.3.1-Linux-x86_64.sh,然後在下載目錄下執行如下命令:

 

bash Anaconda3-4.3.1-Linux-x86_64.sh

 

安裝過程中按enter或y即可,安裝完成後,程式提示是否把anaconda的binary加入到.bashrc配置檔案中,加入後執行python、ipython時將自動使用新安裝的Python環境。

 

安裝完成後,你可用conda list命令檢視已安裝的庫:

 

conda list

 

安裝成功的話,應該能看到numpy、scipy、matplotlib、conda等庫。

 

2. 安裝theano

 

利用conda 來安裝或更新程式:

 

conda install theano

 

3. 測試

 

先啟動Python,然後匯入theano模組,如果不報錯,說明安裝成功。

 

$ Python
Python 3.6.0 |Anaconda custom (64-bit)| (default, Dec 23 2016, 12:22:00) 
[GCC 4.4.7 20120313 (Red Hat 4.4.7-1)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import theano
>>>

 

 

02 符號變數

 

儲存資料需要用到各種變數,那Theano是如何使用變數的呢?Theano用符號變數TensorVariable來表示變數,又稱為張量(Tensor)。

張量是Theano的核心元素(也是TensorFlow的核心元素),是Theano運算式和運算操作的基本單位。張量可以是標量(scalar)、向量(vector)、矩陣(matrix)等的統稱。

具體來說,標量就是我們通常看到的0階的張量,如12,a等,而向量和矩陣分別為1階張量和2階的張量。

 

如果透過這些概念,你還不很清楚,沒有關係,可以結合以下實體來直觀感受一下。

 

首先定義三個標量:一個代表輸入x、一個代表權重w、一個代表偏移量b,然後計算這些標量運算結果z=x*w+b,Theano程式碼實現如下:

 

#匯入需要的庫或模組
import theano
from theano import tensor as T

#初始化張量
x=T.scalar(name='input',dtype='float32')
w=T.scalar(name='weight',dtype='float32')
b=T.scalar(name='bias',dtype='float32')
z=w*x+b

#編譯程式
net_input=theano.function(inputs=[w,x,b],outputs=z)
#執行程式
print('net_input: %2f'% net_input(2.0,3.0,0.5))

 

列印結果:

 

net_input: 6.500000

 

透過以上實體我們不難看出,Theano本身是一個通用的符號計算框架,與非符號架構的框架不同,它先使用tensor variable初始化變數,然後將複雜的符號運算式編譯成函式模型,最後執行時傳入實際資料進行計算。

整個過程涉及三個步驟:定義符號變數,編譯程式碼,執行程式碼。這節主要介紹第一步如何定義符號變數,其他步驟將在後續小節介紹。

 

如何定義符號變數?或定義符號變數有哪些方式?在Theano中定義符號變數的方式有三種:使用內建的變數型別、自定義變數型別、轉換其他的變數型別。具體如下:

 

1. 使用內建的變數型別建立

 

目前Theano支援7種內建的變數型別,分別是標量(scalar)、向量(vector)、行(row)、列(col)、矩陣(matrix)、tensor3、tensor4等。其中標量是0階張量,向量為1階張量,矩陣為2階張量等,以下為建立內建變數的實體:

 

import theano
from theano import tensor as T
x=T.scalar(name='input',dtype='float32')
data=T.vector(name='data',dtype='float64')

其中,name指定變數名字,dtype指變數的資料型別。

 

2. 自定義變數型別

 

內建的變數型別只能處理4維及以下的變數,如果需要處理更高維的資料時,可以使用Theano的自定義變數型別,具體透過TensorType方法來實現:

 

import theano
from theano import tensor as T

mytype=T.TensorType('float64',broadcastable=(),name=None,sparse_grad=False)

 

其中broadcastable是True或False的布林型別元組,元組的大小等於變數的維度,如果為True,表示變數在對應維度上的資料可以進行廣播,否則資料不能廣播。

 

廣播機制(broadcast)是一種重要機制,有了這種機制,就可以方便地對不同維的張量進行運算,否則,就要手工把低維資料變成高維,利用廣播機制系統自動複製等方法把低維資料補齊(MumPy也有這種機制)。以下我們透過圖2-1所示的一個實體來說明廣播機制原理。

 

▲圖2-1 廣播機制

 

圖2-1中矩陣與向量相加的具體程式碼如下:

 

import theano
import numpy as np
import theano.tensor as T
r = T.row()
r.broadcastable
# (True, False)

mtr = T.matrix()
mtr.broadcastable
# (False, False)

f_row = theano.function([r, mtr], [r + mtr])
R = np.arange(1,3).reshape(1,2)
print(R)
#array([[1, 2]])

M = np.arange(1,7).reshape(32)
print(M)
#array([[1, 2],
#       [3, 4],
#       [5, 6]])

f_row(R, M)
#[array([[ 2.,  4.],
#        [ 4.,  6.],
#        [ 6.,  8.]])]

 

3. 將Python型別變數或者NumPy型別變數轉化為Theano共享變數

 

共享變數是Theano實現變數更新的重要機制,後面我們會詳細講解。要建立一個共享變數,只要把一個Python物件或NumPy物件傳遞給shared函式即可,如下所示:

 

import theano
import numpy as np
import theano.tensor as T

data=np.array([[1,2],[3,4]])
shared_data=theano.shared(data)
type(shared_data)

 

 

03 符號計算圖模型

 

符號變數定義後,需要說明這些變數間的運算關係,那如何描述變數間的運算關係呢?Theano實際採用符號計算圖模型來實現。首先建立運算式所需的變數,然後透過運運算元(op)把這些變數結合在一起,如前文圖2-1所示。

 

Theano處理符號運算式時是透過把符號運算式轉換為一個計算圖(graph)來處理(TensorFlow也使用了這種方法,等到我們介紹TensorFlow時,大家可對比一下),符號計算圖的節點有:variable、type、apply和op。

 

  • variable節點:即符號的變數節點,符號變數是符號運算式存放資訊的資料結構,可以分為輸入符號和輸出符號。

  • type節點:當定義了一種具體的變數型別以及變數的資料型別時,Theano為其指定資料儲存的限制條件。

  • apply節點:把某一種型別的符號運運算元應用到具體的符號變數中,與variable不同,apply節點無須由使用者指定,一個apply節點包括3個欄位:op、inputs、outputs。

  • op節點:即運運算元節點,定義了一種符號變數間的運算,如+、-、sum()、tanh()等。

 

Theano是將符號運算式的計算表示成計算圖。這些計算圖是由Apply 和 Variable將節點連線而組成,它們分別與函式的應用和資料相連線。操作由op 實體表示,而資料型別由type 實體表示。

下麵這段程式碼和圖2-2說明瞭這些程式碼所構建的結構。藉助這個圖或許有助於你進一步理解如何將這些內容擬合在一起:

 

import theano
import numpy as np
import theano.tensor as T

x = T.dmatrix('x')  
y = T.dmatrix('y')  
z = x + y  

▲圖2-2 符號計算圖

 

圖2-2中箭頭表示指向Python物件的取用。中間大的長方形是一個 Apply 節點,3個圓角矩形(如X)是 Variable 節點,帶+號的圓圈是ops,3個圓角小長方形(如matrix)是Types。

 

在建立 Variables 之後,應用 Apply ops得到更多的變數,這些變數僅僅是一個佔位符,在function中作為輸入。變數指向 Apply 節點的過程是用來表示函式透過owner 域來生成它們 。這些Apply節點是透過它們的inputs和outputs域來得到它們的輸入和輸出變數。

 

x和y的owner域的指向都是None,這是因為它們不是另一個計算的結果。如果它們中的一個變數是另一個計算的結果,那麼owner域將會指向另一個藍色盒。

 

 

04 函式

 

上節我們介紹瞭如何把一個符號運算式轉化為符號計算圖,這節我們介紹函式的功能,函式是Theano的一個核心設計模組,它提供一個介面,把函式計算圖編譯為可呼叫的函式物件。前面介紹瞭如何定義自變數x(不需要賦值),這節介紹如何編寫函式方程。

 

1. 函式定義的格式

 

先來看一下函式格式示例:

 

theano.function(inputs, outputs, mode=None, updates=None, givens=None, no_default_updates=False, accept_inplace=False, name=None,rebuild_strict=True, allow_input_downcast=None, profile=None, on_unused_input='raise')

 

這裡引數看起來很多,但一般只用到三個:inputs表示自變數;outputs表示函式的因變數(也就是函式的傳回值);還有一個比較常用的是updates引數,它一般用於神經網路共享變數引數更新,通常以字典或元組串列的形式指定。

此外,givens是一個字典或元組串列,記為[(var1,var2)],表示在每一次函式呼叫時,在符號計算圖中,把符號變數var1節點替換為var2節點,該引數常用來指定訓練資料集的batch大小。

下麵我們看一個有多個自變數,同時又有多個因變數的函式定義例子:

 

import theano  
x, y =theano.tensor.fscalars('x''y')  
z1= x + y  
z2=x*y  
#定義x、y為自變數,z1、z2為函式傳回值(因變數)
f =theano.function([x,y],[z1,z2])  

#傳回當x=2,y=3的時候,函式f的因變數z1,z2的值
print(f(2,3))

 

列印結果:

 

[array(5.0, dtype=float32), array(6.0, dtype=float32)]

 

在執行theano.function()時,Theano進行了編譯最佳化,得到一個end-to-end的函式,傳入資料呼叫f(2,3)時,執行的是最佳化後儲存在圖結構中的模型,而不是我們寫的那行z=x+y,儘管二者結果一樣。

這樣的好處是Theano可以對函式f進行最佳化,提升速度;壞處是不方便開發和除錯,由於實際執行的程式碼不是我們寫的程式碼,所以無法設定斷點進行除錯,也無法直接觀察執行時中間變數的值。

 

2. 自動求導

 

有了符號計算,自動計算導數就很容易了。tensor.grad()唯一需要做的就是從outputs逆向遍歷到輸入節點。對於每個op,它都定義了怎麼根據輸入計算出偏導數。使用鏈式法則就可以計算出梯度了。利用Theano求導時非常方便,可以直接利用函式theano.grad(),比如求s函式的導數:

 

 

以下程式碼實現當x=3的時候,求s函式的導數:

import theano  
x =theano.tensor.fscalar('x')#定義一個float型別的變數x  
y= 1 / (1 + theano.tensor.exp(-x))#定義變數y  
dx=theano.grad(y,x)#偏導數函式  
f= theano.function([x],dx)#定義函式f,輸入為x,輸出為s函式的偏導數  
print(f(3))#計算當x=3的時候,函式y的偏導數

列印結果:

 

0.045176658779382706

 

3. 更新共享變數引數

 

在深度學習中通常需要迭代多次,每次迭代都需要更新引數。Theano如何更新引數呢?

在theano.function函式中,有一個非常重要的引數updates。updates是一個包含兩個元素的串列或tuple,一般示例為updates=[old_w,new_w],當函式被呼叫的時候,會用new_w替換old_w,具體看下麵這個例子。

 

import theano
w= theano.shared(1)#定義一個共享變數w,其初始值為1  
x=theano.tensor.iscalar('x')  
f=theano.function([x], w, updates=[[w, w+x]])#定義函式自變數為x,因變數為w,當函式執行完畢後,更新引數w=w+x  
print(f(3))#函式輸出為w  
print(w.get_value())#這個時候可以看到w=w+x為4

 

列印結果:

 

1、4

 

在求梯度下降的時候,經常用到updates這個引數。比如updates=[w,w-α*(dT/dw)],其中dT/dw就是梯度下降時,代價函式對引數w的偏導數,α是學習速率。為便於大家更全面地瞭解Theano函式的使用方法,下麵我們透過一個邏輯回歸的完整實體來說明:

 

import numpy  as np
import theano  
import theano.tensor as T  
rng = np.random  

# 我們為了測試,自己生成10個樣本,每個樣本是3維的向量,然後用於訓練 
N = 10
feats = 3
D = (rng.randn(N, feats).astype(np.float32), rng.randint(size=N, low=0, high=2).astype(np.float32))

# 宣告自變數x、以及每個樣本對應的標簽y(訓練標簽)  
x = T.matrix("x")
y = T.vector("y")

#隨機初始化引數w、b=0,為共享變數  
w = theano.shared(rng.randn(feats), name="w")  
b = theano.shared(0., name="b")

#構造代價函式
p_1 = 1 / (1 + T.exp(-T.dot(x, w) - b))   # s啟用函式  
xent = -y * T.log(p_1) - (1-y) * T.log(1-p_1) # 交叉商代價函式
cost = xent.mean() + 0.01 * (w ** 2).sum()# 代價函式的平均值+L2正則項以防過擬合,其中權重衰減繫數為0.01  
gw, gb = T.grad(cost, [w, b])             #對總代價函式求引數的偏導數  

prediction = p_1 > 0.5                    # 大於0.5預測值為1,否則為0.

train = theano.function(inputs=[x,y],outputs=[prediction, xent],updates=((w, w - 0.1 * gw), (b, b - 0.1 * gb)))#訓練所需函式
predict = theano.function(inputs=[x], outputs=prediction)#測試階段函式  

#訓練  
training_steps = 1000  
for i in range(training_steps):  
    pred, err = train(D[0], D[1])  
    print (err.mean())#檢視代價函式下降變化過程  

 

05 條件與迴圈

 

編寫函式需要經常用到條件陳述句或迴圈陳述句,這節我們就簡單介紹Theano如何實現條件判斷或邏輯迴圈。

 

1. 條件判斷

 

Theano是一種符號語言,條件判斷不能直接使用Python的if陳述句。在Theano可以用ifelse和switch來表示判定陳述句。這兩個判定陳述句有何區別呢?

 

switch對每個輸出變數進行操作,ifelse只對一個滿足條件的變數操作。比如對陳述句:

switch(cond, ift, iff) 

如果滿足條件,則switch既執行ift也執行iff。而對陳述句:

if cond then ift else iff

ifelse只執行ift或者只執行iff。

下麵透過一個示例進一步說明:

from theano import tensor as T  
from theano.ifelse import ifelse  
import theano,time,numpy  

a,b=T.scalars('a','b')  
x,y=T.matrices('x','y')  
z_switch=T.switch(T.lt(a,b),T.mean(x),T.mean(y))#lt:a 
z_lazy=ifelse(T.lt(a,b),T.mean(x),T.mean(y))  

#optimizer:optimizer的型別結構(可以簡化計算,增加計算的穩定性)  
#linker:決定使用哪種方式進行編譯(C/Python) 
f_switch = theano.function([a, b, x, y], z_switch,mode=theano.Mode(linker='vm'))  
f_lazyifelse = theano.function([a, b, x, y], z_lazy,mode=theano.Mode(linker='vm'))  

val1 = 0.  
val2 = 1.  
big_mat1 = numpy.ones((1000100))  
big_mat2 = numpy.ones((1000100))  

n_times = 10  

tic = time.clock()  
for i in range(n_times):  
    f_switch(val1, val2, big_mat1, big_mat2)  
print('time spent evaluating both values %f sec' % (time.clock() - tic))  

tic = time.clock()  
for i in range(n_times):  
    f_lazyifelse(val1, val2, big_mat1, big_mat2)  
print('time spent evaluating one value %f sec' % (time.clock() - tic))  

 

列印結果:

 

time spent evaluating both values 0.005268 sec
time spent evaluating one value 0.007501 sec 

2. 迴圈陳述句

 

scan是Theano中構建迴圈Graph的方法,scan是個靈活複雜的函式,任何用迴圈、遞迴或者跟序列有關的計算,都可以用scan完成。其格式如下:

 

theano.scan(fn, sequences=None, outputs_info=None, non_sequences=None, n_steps=None, truncate_gradient=-1, go_backwards=False, mode=None, name=None, profile=False, allow_gc=None, strict=False)

 

引數說明:

 

  • fn:一個lambda或者def函式,描述了scan中的一個步驟。除了outputs_info,fn可以傳回sequences變數的更新updates。fn的輸入變數的順序為sequences中的變數、outputs_info的變數、non_sequences中的變數。如果使用了taps,則按照taps給fn喂變數。taps的詳細介紹會在後面的例子中給出。

  • sequences:scan進行迭代的變數,scan會在T.arange()生成的list上遍歷,例如下麵的polynomial 例子。

  • outputs_info:初始化fn的輸出變數,和輸出的shape一致。如果初始化值設為None,表示這個變數不需要初始值。

  • non_sequences:fn函式用到的其他變數,迭代過程中不可改變(unchange)。

  • n_steps:fn的迭代次數。

 

下麵透過一個例子解釋scan函式的具體使用方法。

 

程式碼實現思路是:先定義函式one_step,即scan裡的fn,其任務就是計算多項式的一項,scan函式傳回的result裡會儲存多項式每一項的值,然後我們對result求和,就得到了多項式的值。

 

import theano
import theano.tensor as T
import numpy as np

# 定義單步的函式,實現a*x^n
# 輸入引數的順序要與下麵scan的輸入引數對應
def one_step(coef, power, x):
    return coef * x ** power

coefs = T.ivector()  # 每步變化的值,系陣列成的向量
powers = T.ivector() # 每步變化的值,指陣列成的向量
x = T.iscalar()      # 每步不變的值,自變數

# seq,out_info,non_seq與one_step函式的引數順序一一對應
# 傳回的result是每一項的符號運算式組成的list
result, updates = theano.scan(fn = one_step,
                       sequences = [coefs, powers],
                       outputs_info = None,
                       non_sequences = x)

# 每一項的值與輸入的函式關係
f_poly = theano.function([x, coefs, powers], result, allow_input_downcast=True)

coef_val = np.array([2,3,4,6,5])
power_val = np.array([0,1,2,3,4])
x_val = 10

print("多項式各項的值: ",f_poly(x_val, coef_val, power_val))
#scan傳回的result是每一項的值,並沒有求和,如果我們只想要多項式的值,可以把f_poly寫成這樣:
# 多項式每一項的和與輸入的函式關係
f_poly = theano.function([x, coefs, powers], result.sum(), allow_input_downcast=True)

print("多項式和的值:",f_poly(x_val, coef_val, power_val))

 

列印結果:

 

多項式各項的值:  [ 2   30   400  6000 50000]
多項式和的值: 56432

 

 

06 共享變數

 

共享變數(shared variable)是實現機器學習演演算法引數更新的重要機制。shared函式會傳回共享變數。這種變數的值在多個函式可直接共享。可以用符號變數的地方都可以用共享變數。

但不同的是,共享變數有一個內部狀態的值,這個值可以被多個函式共享。它可以儲存在視訊記憶體中,利用GPU提高效能。我們可以使用get_value和set_value方法來讀取或者修改共享變數的值,使用共享變數實現累加操作。

 

import theano
import theano.tensor as T
from theano import shared
import numpy as np

#定義一個共享變數,並初始化為0
state = shared(0)
inc = T.iscalar('inc')
accumulator = theano.function([inc], state, updates=[(state, state+inc)])
# 列印state的初始值
print(state.get_value())
accumulator(1# 進行一次函式呼叫
# 函式傳回後,state的值發生了變化
print(state.get_value()) 

 

這裡state是一個共享變數,初始化為0,每次呼叫accumulator(),state都會加上inc。共享變數可以像普通張量一樣用於符號運算式,另外,它還有自己的值,可以直接用.get_value()和.set_value()方法來訪問和修改。

 

上述程式碼引入了函式中的updates引數。updates引數是一個list,其中每個元素是一個元組(tuple),這個tuple的第一個元素是一個共享變數,第二個元素是一個新的運算式。updatas中的共享變數會在函式傳回後更新自己的值。

updates的作用在於執行效率,updates多數時候可以用原地(in-place)演演算法快速實現,在GPU上,Theano可以更好地控制何時何地給共享變數分配空間,帶來效能提升。最常見的神經網路權值更新,一般會用update實現。

 

 

07 小結

 

Theano基於NumPy,但效能方面又高於NumPy。因Theano採用了張量(Tensor)這個核心元素,在計算方面採用符號計算模型,而且採用共享變數、自動求導、利用GPU等適合於大資料、深度學習的方法,其他很多開發專案也深受這些技術和框架影響。

 

關於作者:吳茂貴,BI和大資料專家,就職於中國外匯交易中心,在BI、資料挖掘與分析、資料倉庫、機器學習等領域有超過20年的工作經驗,在Spark機器學習、TensorFlow深度學習領域大量的實踐經驗。

王冬,任職於博世(中國)投資有限公司,負責Bosch企業BI及工業4.0相關大資料和資料挖掘專案。對機器學習、人工智慧有多年實踐經驗。

李濤,參與過多個人工智慧專案,如研究開發服務機器人、無人售後店等專案。熟悉python、caffe、TensorFlow等,對深度學習、尤其對計算機視覺方面有較深理解。

楊本法,高階演演算法工程師,在機器學習、文字挖掘、視覺化等領域有多年實踐經驗。熟悉Hadoop、Spark生態圈的相關技術,對Python有豐富的實戰經驗。

本文摘編自《Python深度學習:基於TensorFlow》,經出版方授權釋出。

延伸閱讀《Python深度學習:基於TensorFlow

點選上圖瞭解及購買

轉載請聯絡微信:DoctorData

推薦語:從Python和數學,到機器學習和TensorFlow,再到深度學習的應用和擴充套件,為深度學習提供全棧解決方案。 

贊(0)

分享創造快樂