使用MXNet的NDArray來處理數據

NDArray.ipynbhtml

NDArray介紹

機器學習處理的對象是數據,數據通常是由外部傳感器(sensors)採集,通過數字化後存儲在計算機中,多是文本、聲音,圖片、視頻等不一樣形式。
這些數字化的數據最終會加載到內存進行各類清洗,運算操做。
幾乎全部的機器學習算法都涉及到對數據的各類數學運算,好比:加減、點乘、矩陣乘等。因此咱們須要一個易用的、高效的、功能強大的工具來處理這些數據並組支持各類複雜的數學運算。前端

在C/C++中已經開發出來了不少高效的針對於向量、矩陣的運算庫,好比:OpenBLAS,Altlas,MKL等。python

對於Python來講Numpy無疑是一個強大針對數據科學的工具包,它提供了一個強大的高維數據的數組表示,以及支持Broadcasting的運算,並提供了線性代數、傅立葉變換、隨機數等功能強大的函數。git

MXNet的NDArray與Numpy中的ndarray極爲類似,NDAarray爲MXNet中的各類數學計算提供了核心的數據結構,NDArray表示一個多維的、固定大小的數組,而且支持異構計算。那爲何不直接使用Numpy呢?MXNet的NDArray提供額外提供了兩個好處:github

  • 支持異構計算,數據能夠在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數組:apache

  1. 使用ndarray.array直接將一個list或numpy.ndarray轉換爲一個NDArray
  2. 使用一些內置的函數zeros,ones以及一些隨機數模塊ndarray.random建立NDArray,並預填充了一些數據。
  3. 從一個一維的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之間能夠進行加減乘除等一系列的數學運算,其中大部分的運算都是逐元素進行的。api

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來將秩較小的數組進行擴展,直到兩個數組的尺寸的長度都同樣。
  2. 若是兩個數組在某個維度上的長度是同樣的,或者其中一個數組在該維度上長度爲1,那麼咱們就說這兩個數組在該維度上是相容的。
  3. 若是兩個數組在全部維度上都是相容的,他們就能使用廣播。
  4. 若是兩個輸入數組的尺寸不一樣,那麼注意其中較大的那個尺寸。由於廣播以後,兩個數組的尺寸將和那個較大的尺寸同樣。
  5. 在任何一個維度上,若是一個數組的長度爲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到了後端的執行引擎,而後就返回了。這樣作有下面兩個好處:

  1. 當操做被push到後端後,Python的主線程能夠繼續執行下面的語句,這對於Python這樣的解釋性的語言在執行計算型任務時特別有幫助。
  2. 後端引擎能夠對執行的語句進行優化,好比進行自動並行化處理。

後端引擎必需要解決的問題就是數據依賴和合理的調度。但這些操做對於前端的用戶來講是徹底透明的。咱們可使用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))

參考資源

相關文章
相關標籤/搜索