使用MXNet的NDArray來處理數據
2018-03-06 14:29 by ☆Ronny丶, 382 閱讀, 0 評論, 收藏, 編輯
NDArray介紹
機器學習處理的對象是數據,數據一般是由外部傳感器(sensors)采集,經過數字化后存儲在計算機中,可能是文本、聲音,圖片、視頻等不同形式。
這些數字化的數據最終會加載到內存進行各種清洗,運算操作。
幾乎所有的機器學習算法都涉及到對數據的各種數學運算,比如:加減、點乘、矩陣乘等。所以我們需要一個易用的、高效的、功能強大的工具來處理這些數據並組支持各種復雜的數學運算。
在C/C++中已經開發出來了很多高效的針對於向量、矩陣的運算庫,比如:OpenBLAS,Altlas,MKL等。
對於Python來說Numpy無疑是一個強大針對數據科學的工具包,它提供了一個強大的高維數據的數組表示,以及支持Broadcasting的運算,並提供了線性代數、傅立葉變換、隨機數等功能強大的函數。
MXNet的NDArray與Numpy中的ndarray極為相似,NDAarray為MXNet中的各種數學計算提供了核心的數據結構,NDArray表示一個多維的、固定大小的數組,並且支持異構計算。那為什么不直接使用Numpy呢?MXNet的NDArray提供額外提供了兩個好處:
- 支持異構計算,數據可以在CPU,GPU,以及多GPU機器的硬件環境下高效的運算
- NDArray支持惰性求值,對於復雜的操作,可以在有多個計算單元的設備上自動的並行運算。
NDArray的重要屬性
每個NDarray都具有以下重要的屬性,我們可以通過相應的api來訪問:
ndarray.shape
:數組的維度。它返回了一個整數的元組,元組的長度等於數組的維數,元組的每個元素對應了數組在該維度上的長度。比如對於一個n行m列的矩陣,那么它的形狀就是(n,m)。ndarray.dtype
:數組中所有元素的類型,它返回的是一個numpy.dtype的類型,它可以是int32/float32/float64
等,默認是'float32'的。ndarray.size
:數組中元素的個數,它等於ndarray.shape
的所有元素的乘積。ndarray.context
:數組的存儲設備,比如:cpu()
或gpu(1)
import mxnet as mx import mxnet.ndarray as nd a = nd.ones(shape=(2,3),dtype='int32',ctx=mx.gpu(1)) print(a.shape, a.dtype, a.size, a.context)
NDArray的創建
一般來常見有2種方法來創建NDarray數組:
- 使用
ndarray.array
直接將一個list或numpy.ndarray轉換為一個NDArray - 使用一些內置的函數
zeros
,ones
以及一些隨機數模塊ndarray.random
創建NDArray,並預填充了一些數據。 - 從一個一維的NDArray進行reshape
import numpy as np l = [[1,2],[3,4]] print(nd.array(l)) # 從List轉到NDArray print(nd.array(np.array(l))) # 從np.array轉到NDArray # 直接利用函數創建指定大小的NDArray print (nd.zeros((3,4), dtype='float32')) print (nd.ones((3,4), ctx=mx.gpu())) # 從一個正態分布的隨機數引擎生成了一個指定大小的NDArray,我們還可以指定分布的參數,比如均值,標准差等 print (nd.random.normal(shape=(3,4))) print (nd.arange(18).reshape(3,2,3))
NDArray的查看
一般情況下,我們可以通過直接使用print來查看NDArray中的內容,我們也可以使用nd.asnumpy()
函數,將一個NDArray轉換為一個numpy.ndarray來查看。
a = nd.random.normal(0, 2, shape=(3,3)) print(a) print(a.asnumpy())
基本的數學運算
NDArray之間可以進行加減乘除等一系列的數學運算,其中大部分的運算都是逐元素進行的。
shape=(3,4) x = nd.ones(shape) y = nd.random_normal(0, 1, shape=shape) x + y # 逐元素相加 x * y # 逐元素相乘 nd.exp(y) # 每個元素取指數 nd.sin(y**2).T # 對y逐元素求平方,然后求sin,最后對整個NDArray轉置 nd.maximum(x,y) # x與y逐元素求最大值
這里需要注意的是*
運算是兩個NDArray之間逐元素的乘法,要進行矩陣乘法,必須使用ndarray.dot
函數進行矩陣乘
nd.dot(x, y.T)
索引與切片
MXNet NDArray提供了各種截取的方法,其用法與Python中list的截取操作以及Numpy.ndarray中的截取操作基本一致。
x = nd.arange(0, 9).reshape((3,3)) x[1:3] # 截取x的axis=0的第1和第2行 x[1:2,1:3] # 截取x的axis=0的第1行,axis=1的第一行和第二行
存儲變化
在對NDArray進行算法運算時,每個操作都會開辟新的內存來存儲運算的結果。例如:如果我們寫y = x + y
,我們會把y
從現在指向的實例轉到新創建的實例上去。我們可以把上面的運算看成兩步:z = x + y; y = z
。
我們可以使用python的內置函數id()
來驗證。id()
返回一個對象的標識符,當這個對象存在時,這個標識符一定是惟一的,在CPython中這個標識符實際上就是對象的地址。
x = nd.ones((3,4)) y = nd.ones((3,4)) before = id(y) y = x + y print(before, id(y))
在很多情況下,我們希望能夠在原地對數組進行運算,那么我們可以使用下面的一些語句:
y += x print(id(y)) nd.elemwise_add(x, y, out=y) print(id(y)) y[:] = x + y print(id(y))
在NDArray中一般的賦值語句像y = x
,y實際上只是x的一個別名而已,x和y是共享一份數據存儲空間的
x = nd.ones((2,2)) y = x print(id(x)) print(id(y))
如果我們想得到一份x的真實拷貝,我們可以使用copy函數
y = x.copy() print(id(y))
Broadcasting
廣播是一種強有力的機制,可以讓不同大小的NDArray在一起進行數學計算。我們常常會有一個小的矩陣和一個大的矩陣,然后我們會需要用小的矩陣對大的矩陣做一些計算。
舉個例子,如果我們想要把一個向量加到矩陣的每一行,我們可以這樣做
# 將v加到x的每一行中,並將結果存儲在y中 x = nd.array([[1,2,3], [4,5,6], [7,8,9], [10, 11, 12]]) v = nd.array([1, 0, 1]) y = nd.zeros_like(x) # Create an empty matrix with the same shape as x for i in range(4): y[i, :] = x[i, :] + v print (y)
這樣是行得通的,但是當x矩陣非常大,利用循環來計算就會變得很慢很慢。我們可以換一種思路:
x = nd.array([[1,2,3], [4,5,6], [7,8,9], [10, 11, 12]]) v = nd.array([1, 0, 1]) vv = nd.tile(v, (4, 1)) # Stack 4 copies of v on top of each other y = x + vv # Add x and vv elementwise print (y) # 也可以通過broadcast_to來實現 vv = v.broadcast_to((4,3)) print(vv)
NDArray的廣播機制使得我們不用像上面那樣先創建vv,可以直接進行運算
x = nd.array([[1,2,3], [4,5,6], [7,8,9], [10, 11, 12]]) v = nd.array([1, 0, 1]) y = x + v print(y)
對兩個數組使用廣播機制要遵守下列規則:
- 如果數組的秩不同,使用1來將秩較小的數組進行擴展,直到兩個數組的尺寸的長度都一樣。
- 如果兩個數組在某個維度上的長度是一樣的,或者其中一個數組在該維度上長度為1,那么我們就說這兩個數組在該維度上是相容的。
- 如果兩個數組在所有維度上都是相容的,他們就能使用廣播。
- 如果兩個輸入數組的尺寸不同,那么注意其中較大的那個尺寸。因為廣播之后,兩個數組的尺寸將和那個較大的尺寸一樣。
- 在任何一個維度上,如果一個數組的長度為1,另一個數組長度大於1,那么在該維度上,就好像是對第一個數組進行了復制。
在GPU上運算
NDArray支持數組在GPU設備上運算,這是MXNet NDArray和Numpy的ndarray最大的不同。默認情況下NDArray的所有操作都是在CPU上執行的,我們可以通過ndarray.context來查詢數組所在設備。在有GPU支持的環境上,我們可以指定NDArray在gpu設備上。
gpu_device = mx.gpu(0) def f(): a = mx.nd.ones((100,100)) b = mx.nd.ones((100,100), ctx=mx.cpu()) c = a + b.as_in_context(a.context) print(c) f() # 在CPU上運算 # 在GPU上運算 with mx.Context(gpu_device): f()
上面語句中使用了with來構造了一個gpu環境的上下文,在上下文中的所有語句,如果沒有顯式的指定context,則會使用wtih語句指定的context。
當前版本的NDArray要求進行相互運算的數組的context必須一致。我們可以使用as_in_context
來進行NDArray context的切換。
NDArray的序列化
有兩種方法可以對NDArray對象進行序列化后保存在磁盤,第一種方法是使用pickle
,就像我們序列化其他python對象一樣。
import pickle a = nd.ones((2,3)) data = pickle.dumps(a) # 將NDArray直接序列化為內存中的bytes b = pickle.loads(data) # 從內存中的bytes反序列化為NDArray pickle.dump(a, open('tmp.pickle', 'wb')) # 將NDArray直接序列化為文件 b = pickle.load(open('tmp.pickle', 'rb')) # 從文件反序列化為NDArray
在NDArray模塊中,提供了更優秀的接口用於數組與磁盤文件(分布式存儲系統)之間進行數據轉換
a = mx.nd.ones((2,3)) b = mx.nd.ones((5,6)) nd.save("temp.ndarray", [a, b]) # 寫入與讀取的路徑支持Amzzon S3以及Hadoop HDFS等。 c = nd.load("temp.ndarray")
惰性求值與自動並行化
MXNet使用了惰性求值來追求最佳的性能。當我們在Python中運行a = b + 1
時,Python線程只是將運算Push到了后端的執行引擎,然后就返回了。這樣做有下面兩個好處:
- 當操作被push到后端后,Python的主線程可以繼續執行下面的語句,這對於Python這樣的解釋性的語言在執行計算型任務時特別有幫助。
- 后端引擎可以對執行的語句進行優化,比如進行自動並行化處理。
后端引擎必須要解決的問題就是數據依賴和合理的調度。但這些操作對於前端的用戶來說是完全透明的。我們可以使用wait_to_read
來等侍后端對於NDArray操作的完成。在NDArray模塊一類將數據拷貝到其他模塊的操作,內部已經使用了wait_to_read,比如asnumpy()
。
import time def do(x, n): """push computation into the backend engine""" return [mx.nd.dot(x,x) for i in range(n)] def wait(x): """wait until all results are available""" for y in x: y.wait_to_read() tic = time.time() a = mx.nd.ones((1000,1000)) b = do(a, 50) print('time for all computations are pushed into the backend engine:\n %f sec' % (time.time() - tic)) wait(b) print('time for all computations are finished:\n %f sec' % (time.time() - tic))
除了分析數據的讀寫依賴外,后端的引擎還能夠將沒有彼此依賴的操作語句進行並行化調度。比如下面的代碼第二行和第三行可以被並行的執行。
a = mx.nd.ones((2,3)) b = a + 1 c = a + 2 d = b * c
下面的代碼演示了在不同設備上並行調度
n = 10 a = mx.nd.ones((1000,1000)) b = mx.nd.ones((6000,6000), gpu_device) tic = time.time() c = do(a, n) wait(c) print('Time to finish the CPU workload: %f sec' % (time.time() - tic)) d = do(b, n) wait(d) print('Time to finish both CPU/GPU workloads: %f sec' % (time.time() - tic))
tic = time.time() c = do(a, n) d = do(b, n) #上面兩條語句可以同時執行,一條在CPU上運算,一條在GPU上運算 wait(c) wait(d) print('Both as finished in: %f sec' % (time.time() - tic))