MXNet チュートリアル : NDArray

MXNet チュートリアル : NDArray (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
日時 : 02/16/2017

* 本ページは、MXNet 本家サイトの NDArray Tutorial を翻訳した上で適宜、補足説明したものです:
    http://mxnet.io/tutorials/python/ndarray.html

CPU/GPU 配列操作 – テンソル計算のための mxnet.ndarray(numpy array に似ていますが GPU をサポート)をどのように使うか。また MXNet のパワフルな自動並列化の特徴についても説明します。

* サンプルコードの動作確認はしておりますが、適宜、追加改変しています。

 

MXNet の主要オブジェクトの一つは、パッケージ $mxnet.ndarray$、あるいはより短く $mxnet.nd$ により提供される多次元配列です。もし貴方が科学計算 python パッケージ Numpy に慣れ親しんでいるならば、$mxnet.ndarray$ は多くの面で $numpy.ndarray$ に類似しています。

 

基本

多次元配列は同じ型の数字のテーブルです。例えば、3D 空間の点の座標は 1-次元配列でその次元で長さ 3 を持ちます。次の例は 2-次元配列を示します。最初の次元の長さは 2 で、2番目の次元は長さ 3 を持ちます。

[[0, 1, 2]
 [3, 4, 5]]

配列クラスは $NDArray$ と呼ばれます。$NDArray$ オブジェクトの幾つかの重要な属性は :

  • ndarray.shape 配列の次元。整数のタプルで、各次元の配列の長さを示します。$n$ 行 $m$ 列の行列のためには、shape は $(n, m)$ になるでしょう。
  • ndarray.dtype 要素の型を表す numpy オブジェクト。
  • ndarray.size 配列内の数字の総数で、shape の要素の積に等しいです。
  • ndarray.context この配列が保持されるデバイスです。デバイスは CPU または i-th GPU です。

配列作成

配列は複数の方法で作成できます。例えば、array 関数を使って通常の Python リストまたはタプルから 配列を作成できます。

import mxnet as mx
# create a 1-dimensional array with a python list
a = mx.nd.array([1,2,3])
# create a 2-dimensional array with a nested python list 
b = mx.nd.array([[1,2,3], [2,3,4]])
{'a.shape':a.shape, 'b.shape':b.shape}
{'a.shape': (3L,), 'b.shape': (2L, 3L)}

あるいは $numpy.ndarray$ オブジェクトから

import numpy as np
import math
c = np.arange(15).reshape(3,5)
# create a 2-dimensional array from a numpy.ndarray object
a = mx.nd.array(c)
{'a.shape':a.shape}
{'a.shape': (3L, 5L)}

オプション $dtype$ で要素型を指定できます、これは numpy 型を受け入れます。デフォルトでは、$float32$ が使用されます。

# float32 is used in deafult
a = mx.nd.array([1,2,3])
# create an int32 array
b = mx.nd.array([1,2,3], dtype=np.int32)
# create a 16-bit float array
c = mx.nd.array([1.2, 2.3], dtype=np.float16)
(a.dtype, b.dtype, c.dtype)
(numpy.float32, numpy.int32, numpy.float16)

サイズのみが分かっていて要素値が分からない場合には、初期のプレースホルダーの内容で配列を作成するための幾つかの関数があります。

# create a 2-dimensional array full of zeros with shape (2,3) 
a = mx.nd.zeros((2,3))
# create a same shape array full of ones
b = mx.nd.ones((2,3))
# create a same shape array with all elements set to 7
c = mx.nd.full((2,3), 7)
# create a same shape whose initial content is random and 
# depends on the state of the memory
d = mx.nd.empty((2,3))

配列をプリントする

プリントするために関数 $asnumpy$ でしばしば最初に $NDArray$ を $numpy.ndarray$ に変換します :

  • 最後の軸は左から右にプリントされ、
  • 2番目から最後(の軸)は上から下にプリントされ、
  • 残りもまた、各スライスが空行で次から分離されて上から下にプリントされます。
b = mx.nd.ones((2,3))
b.asnumpy()
array([[ 1.,  1.,  1.],
       [ 1.,  1.,  1.]], dtype=float32)

基本操作

配列上の算術演算は element-wise に適用されます。新しい配列が作成され結果で満たされます。

a = mx.nd.ones((2,3))
b = mx.nd.ones((2,3))
# elementwise plus
c = a + b
# elementwise minus
d = - c 
# elementwise pow and sin, and then transpose
e = mx.nd.sin(c**2).T
# elementwise max
f = mx.nd.maximum(a, c)  
f.asnumpy()
array([[ 2.,  2.,  2.],
       [ 2.,  2.,  2.]], dtype=float32)

$NumPy$ と同様に、* は element-wise な乗算に使われます、一方で行列-行列乗算は dot も残しています。

a = mx.nd.ones((2,2))
b = a * a
c = mx.nd.dot(a,a)
{'b':b.asnumpy(), 'c':c.asnumpy()}
{'b': array([[ 1.,  1.],
        [ 1.,  1.]], dtype=float32), 'c': array([[ 2.,  2.],
        [ 2.,  2.]], dtype=float32)}

+= と *= のような代入演算子 (assignment operators) は新しいものを作成するよりも既存の配列を変更するように動作します。

a = mx.nd.ones((2,2))
b = mx.nd.ones(a.shape)
b += a
b.asnumpy()
array([[ 2.,  2.],
       [ 2.,  2.]], dtype=float32)

Indexing と Slicing

slice 演算子 [] は軸 0 上で適用されます。

a = mx.nd.array(np.arange(6).reshape(3,2))
a[1:2] = 1
a[:].asnumpy()
array([[ 0.,  1.],
       [ 1.,  1.],
       [ 4.,  5.]], dtype=float32)

メソッド slice_axis で特定の軸をスライスすることもできます。

d = mx.nd.slice_axis(a, axis=1, begin=1, end=2)
d.asnumpy()
array([[ 1.],
       [ 1.],
       [ 5.]], dtype=float32)

Shape 操作

サイズが同じである限り、配列の shape は変更できます。

a = mx.nd.array(np.arange(24))
b = a.reshape((2,3,4))
b.asnumpy()
array([[[  0.,   1.,   2.,   3.],
        [  4.,   5.,   6.,   7.],
        [  8.,   9.,  10.,  11.]],

       [[ 12.,  13.,  14.,  15.],
        [ 16.,  17.,  18.,  19.],
        [ 20.,  21.,  22.,  23.]]], dtype=float32)

メソッド concatenate は複数の配列を最初の次元に沿って stack します (= 積み重ねます)(それらの shape は同じでなければなりません)。

a = mx.nd.ones((2,3))
b = mx.nd.ones((2,3))*2
c = mx.nd.concatenate([a,b])
c.asnumpy()
array([[ 1.,  1.,  1.],
       [ 1.,  1.,  1.],
       [ 2.,  2.,  2.],
       [ 2.,  2.,  2.]], dtype=float32)

Reduce

配列をスカラに reduce 可能です。

a = mx.nd.ones((2,3))
b = mx.nd.sum(a)
b.asnumpy()
array([ 6.], dtype=float32)

または特定の軸に沿って

c = mx.nd.sum_axis(a, axis=1)
c.asnumpy()
array([ 3.,  3.], dtype=float32)

Broadcast

複製する (duplicating) ことで配列を broadcast することもできます。次のコードは軸 1 に沿って broadcast します。

a = mx.nd.array(np.arange(6).reshape(6,1))
b = a.broadcast_to((6,2))  # 
b.asnumpy()
array([[ 0.,  0.],
       [ 1.,  1.],
       [ 2.,  2.],
       [ 3.,  3.],
       [ 4.,  4.],
       [ 5.,  5.]], dtype=float32)

または軸 1 と 2 に沿って boadcast します。

c = a.reshape((2,1,1,3))
d = c.broadcast_to((2,2,2,3))
d.asnumpy()
array([[[[ 0.,  1.,  2.],
         [ 0.,  1.,  2.]],

        [[ 0.,  1.,  2.],
         [ 0.,  1.,  2.]]],


       [[[ 3.,  4.,  5.],
         [ 3.,  4.,  5.]],

        [[ 3.,  4.,  5.],
         [ 3.,  4.,  5.]]]], dtype=float32)

broadcast は * と + のような演算子にも適用されます。

a = mx.nd.ones((3,2))
b = mx.nd.ones((1,2))
c = a + b
c.asnumpy()
array([[ 2.,  2.],
       [ 2.,  2.],
       [ 2.,  2.]], dtype=float32

Copies

データは通常の代入ではコピーされません。

a = mx.nd.ones((2,2))
b = a  
b is a
True

関数引数渡しについても同様です。

def f(x):  
    return x
a is f(a)
True

$copy$ メソッドは配列とそのデータを deep copy します。

b = a.copy()
b is a
False

上のコードは新しい NDArray を割り当ててそして b に割り当てます。追加のメモリ割り当てを避けるために $copyto$ メソッドか slice 演算子 [] を使用できます。

b = mx.nd.ones(a.shape)
c = b
c[:] = a
d = b
a.copyto(d)
(c is b, d is b)
(True, True)

 

上級

$mxnet.ndarray$ には幾つかの進んだ特徴があります。これは mxnet を他のライブラリと異なるものにしています。

GPU サポート

デフォルトでは演算は CPU 上で実行されます。利用可能であれば、GPU のような他の計算リソースにスイッチすることは簡単です。デバイス情報は ndarray.context で保持されます。MXNet が flag USE_CUDA=1 でコンパイルされてそして少なくとも一つ Nvidia GPU カードがある時には、コンテキスト mx.gpu(0)、あるいは単純に mx.gpu() を使用して全ての計算を GPU 0 上で実行できます。2つ以上の GPU があるならば、2nd GPU は mx.gpu(1) で表されます。

def f():
    a = mx.nd.ones((100,100))
    b = mx.nd.ones((100,100))
    c = a + b
    print(c)
# in default mx.cpu() is used
f()  
# change the default context to the first GPU
with mx.Context(mx.gpu()):  
    f()
<NDArray 100x100 @cpu(0)>
<'NDArray 100x100 @gpu(0)>

配列を作成する時にコンテキストを明示的に指定することもできます。

a = mx.nd.ones((100, 100), mx.gpu(0))
a
<NDArray 100x100 @gpu(0)>

現時点では MXNet は2つの配列が計算のために同じデバイスに在ることが要求されます。デバイス間でデータをコピーするためには幾つかのメソッドがあります。

a = mx.nd.ones((100,100), mx.cpu())
b = mx.nd.ones((100,100), mx.gpu())
c = mx.nd.ones((100,100), mx.gpu())
a.copyto(c)  # copy from CPU to GPU
d = b + c
e = b.as_in_context(c.context) + c  # same to above
{'d':d, 'e':e}
{'d': <NDArray 100x100 @gpu(0)>, 'e': <NDArray 100x100 @gpu(0)>

(分散)ファイルシステムから/へ Serialize する

データを簡単にディスクに保存(からロード)する2つの方法があります。最初の方法は pickle を使用します。NDArray は pickle 互換です。

import pickle as pkl
a = mx.nd.ones((2, 3))
# pack and then dump into disk
data = pkl.dumps(a)
pkl.dump(data, open('tmp.pickle', 'wb'))
# load from disk and then unpack 
data = pkl.load(open('tmp.pickle', 'rb'))
b = pkl.loads(data)
b.asnumpy()
array([[ 1.,  1.,  1.],
       [ 1.,  1.,  1.]], dtype=float32)

2つ目の方法はメソッド save と load によりバイナリ・フォーマットでディスクに直接ダンプします。単一の NDArray 以外にも、リストもロード/セーブできます。

a = mx.nd.ones((2,3))
b = mx.nd.ones((5,6))               
mx.nd.save("temp.ndarray", [a,b])
c = mx.nd.load("temp.ndarray")
c
[<NDArray 2x3 @cpu(0)>, <NDArray 5x6 @cpu(0)>]

あるいは辞書

d = {'a':a, 'b':b}
mx.nd.save("temp.ndarray", d)
c = mx.nd.load("temp.ndarray")
c
{'a': <NDArray 2x3 @cpu(0)>, 'b': <NDArray 5x6 @cpu(0)>}

load/save は2つの面で pickle よりも良いです。

  1. Python インターフェイスで保存されたデータは他の言語バインディングでも使用できます。例えば、python でデータをセーブします :
a = mx.nd.ones((2, 3))
mx.save("temp.ndarray", [a,])

そして R にロードできます :

a <- mx.nd.load("temp.ndarray")
as.array(a[[1]])
##      [,1] [,2] [,3]
## [1,]    1    1    1
## [2,]    1    1    1
  1. もし Amazon S3 や Hadoop HDFS のような分散ファイルシステムがセットアップされているならば、直接それにセーブしたりロードしたりできます。
mx.nd.save('s3://mybucket/mydata.ndarray', [a,])  # if compiled with USE_S3=1
mx.nd.save('hdfs///users/myname/mydata.bin', [a,])  # if compiled with USE_HDFS=1

遅延評価 (Lazy Evaluation) と自動並列化 (Auto Parallelization) *

MXNet はより良いパフォーマンスのために遅延評価を使用します。python で a=b+1 を実行する時、python スレッドはその演算をバックエンド・エンジンに push して戻ります。そのような最適化には2つの恩恵があります :

  1. 前の一つが一度プッシュされればメイン python スレッドは他の計算を実行し続けることができます。重いオーバーヘッドを持つフロントエンド言語に有用です。
  2. バックエンド・エンジンにとって短く議論される自動並列化のような更なる最適化を explore することがより簡単になります。

バックエンド・エンジンはデータ依存性を解決して計算を正しくスケジュールすることができます。それはフロントエンド・ユーザには透過です。計算が終わるのを待つために結果の配列上でメソッド $wait\_to\_read$ を明示的に呼び出すことができます。asnumpy のような配列から他のパッケージにデータをコピーする操作は暗黙的に wait_to_read を呼び出します。

# @@@ AUTOTEST_OUTPUT_IGNORED_CELL
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))
# on my own gpu
time for all computations are pushed into the backend engine:
 0.002174 sec
time for all computations are finished:
 5.361299 sec
# original
time for all computations are pushed into the backend engine:
 0.001089 sec
time for all computations are finished:
 5.398588 sec

データ read と write 依存を解析した上で、バックエンド・エンジンは依存性なしで並列に計算をスケジュールすることができます。例えば、次のコードにおいて :

a = mx.nd.ones((2,3))
b = a + 1
c = a + 2
d = b * c

2つ目と3つ目の分は並列に実行可能です。次の例は最初に CPU 上で次に GPU 上で動作します。

# @@@ AUTOTEST_OUTPUT_IGNORED_CELL
n = 10
a = mx.nd.ones((1000,1000))
b = mx.nd.ones((6000,6000), mx.gpu())
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))
# on my own gpu
Time to finish the CPU workload: 1.066894 sec
Time to finish both CPU/GPU workloads: 2.622280 sec
# original
Time to finish the CPU workload: 1.089354 sec
Time to finish both CPU/GPU workloads: 2.663608 sec

さて全てのワークロードを同時に発行します。バックエンド・エンジンは CPU と GPU 計算を並列化しようと試みます。

# @@@ AUTOTEST_OUTPUT_IGNORED_CELL
tic = time.time()
c = do(a, n)
d = do(b, n)
wait(c)
wait(d)
print('Both as finished in: %f sec' % (time.time() - tic))
# on my own gpu
Both as finished in: 1.625559 sec
# original
Both as finished in: 1.543902 sec

Current Status

NDArray API を numpy のものと同じように保持するためにベストをつくしています。しかしまだ numpy と完全互換ではありません。ここに幾つかの主要な違いを要約します、これは短期間にfix されることを望んでいます。We are also welcome to any contribution.

  • Slice と Index.
    • NDArray は各回で一つの次元のみをスライスできます、つまり両者の次元をスライスするために x[:, 1] を使用することはできません。
    • continues index のみがサポートされ、x[1:2:3] とはできません。
    • x[y==1] のような、boolean index はサポートされません。
  • max, min.. のような reduce 関数の欠如

Futher Readings

  • NDArray API Documents for all NDArray methods.
  • MinPy on-going project, fully numpy compatible with GPU and auto differentiation supports

Next Steps

 

以上