MXNet チュートリアル : Mixed Programing (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
日時 : 02/19/2017
* 本ページは、MXNet 本家サイトの Mixed Programing Tutorial を翻訳した上で適宜、補足説明したものです:
http://mxnet.io/tutorials/python/mixed.html
* 画像は基本的には自作していますが、一部 github から引用しています。
Mixing Array and Graphs (Advanced) – 命令型と記号型プログラミングを mix するための MXNet の signature サポート のショーケース。これらの機能まわりをラップすることにより Module は既に高位 I/F を提供していることに注意してください。従ってこのチュートリアルは主としてスクラッチからビルドすることを望むユーザのためです。
序
このチュートリアルではニューラルネットワークをスクラッチからトレーニングするために NDArray と Symbol をどのように一緒に結合するかを示します。この mixed programming フレーバーは MXNet を他のフレームワークとは違うものとするユニークな特徴の一つです。MXNet の用語 MX はまた “mixed” をしばしば意味します。
Note that mx.module provides all functions will be implemented.(= 原文まま) 従ってこのチュートリアルは主としてスクラッチからビルドすることを望むユーザのためです。
多層パーセプトロンをトレーニングする
考え方を示すためにサンプルとして2層パーセプトロンを用います。コードは、深層畳込みニューラルネットワークのような他の目的関数にも適用されることに注意してください。最初にネットワークを定義します :
import mxnet as mx num_classes = 10 net = mx.sym.Variable('data') net = mx.sym.FullyConnected(data=net, name='fc1', num_hidden=128) net = mx.sym.Activation(data=net, name='relu1', act_type="relu") net = mx.sym.FullyConnected(data=net, name='fc2', num_hidden=num_classes) net = mx.sym.SoftmaxOutput(data=net, name='out') mx.viz.plot_network(net)
自由変数は、各完全結合層 ($f1$ と $fc2$) からの重みとバイアス、変数 $data$ のためのサンプル、そして softmax 出力 $out$ のためのラベルを含みます。これらの変数名を list_argument でリストします :
print(net.list_arguments())
['data', 'fc1_weight', 'fc1_bias', 'fc2_weight', 'fc2_bias', 'out_label']
forward と backward を実行するためには、最初にデータを全ての自由変数にバインドする必要があります。Symbol チュートリアル でやったように、 全ての NDArray を作成してそれからそれらをバインドできます。この手続きを単純化する $simple_bind$ という名前の関数もまたあります。この関数は、提供されたデータ shape を使用して最初に全ての自由変数の shape を推論し、それから割り当ててデータをバインドします。これには返された executor の属性 $arg\_arrays$ でアクセス可能です。
num_features = 100 batch_size = 100 ex = net.simple_bind(ctx=mx.cpu(), data=(batch_size, num_features)) args = dict(zip(net.list_arguments(), ex.arg_arrays)) for name in args: print(name, args[name].shape)
('fc2_weight', (10L, 128L)) ('fc1_weight', (128L, 100L)) ('out_label', (100L,)) ('fc2_bias', (10L,)) ('data', (100L, 100L)) ('fc1_bias', (128L,))
$ctx$ を GPU に変更すれば、配列を GPU 上に割り当てることができます :
ex = net.simple_bind(ctx=mx.gpu(), data=(batch_size, num_features)) args = dict(zip(net.list_arguments(), ex.arg_arrays)) for name in args: print(name, args[name].shape, args[name].context)
('fc2_weight', (10L, 128L), gpu(0)) ('fc1_weight', (128L, 100L), gpu(0)) ('out_label', (100L,), gpu(0)) ('fc2_bias', (10L,), gpu(0)) ('data', (100L, 100L), gpu(0)) ('fc1_bias', (128L,), gpu(0))
そして重みをランダム値で初期化します。
for name in args: data = args[name] if 'weight' in name: data[:] = mx.random.uniform(-0.1, 0.1, data.shape) if 'bias' in name: data[:] = 0
トレーニングする前に、人工的なデータセットを生成します。
import numpy as np import matplotlib.pyplot as plt class ToyData: def __init__(self, num_classes, num_features): self.num_classes = num_classes self.num_features = num_features self.mu = np.random.rand(num_classes, num_features) self.sigma = np.ones((num_classes, num_features)) * 0.1 def get(self, num_samples): num_cls_samples = num_samples / self.num_classes x = np.zeros((num_samples, self.num_features)) y = np.zeros((num_samples, )) for i in range(self.num_classes): cls_samples = np.random.normal(self.mu[i,:], self.sigma[i,:], (num_cls_samples, self.num_features)) x[i*num_cls_samples:(i+1)*num_cls_samples] = cls_samples y[i*num_cls_samples:(i+1)*num_cls_samples] = i return x, y def plot(self, x, y): colors = ['r', 'b', 'g', 'c', 'y'] for i in range(self.num_classes): cls_x = x[y == i] plt.scatter(cls_x[:,0], cls_x[:,1], color=colors[i%5], s=1) plt.show() toy_data = ToyData(num_classes, num_features) x, y = toy_data.get(1000) toy_data.plot(x,y)
最後にトレーニングを開始します。ここでは、固定された学習率で平易なミニバッチ確率的勾配降下を使用します。10 iterations 毎に精度をプロットします。
For every 10 iterations we plot the accuracy.
# @@@ AUTOTEST_OUTPUT_IGNORED_CELL learning_rate = 0.1 final_acc = 0 for i in range(100): x, y = toy_data.get(batch_size) args['data'][:] = x args['out_label'][:] = y ex.forward(is_train=True) ex.backward() for weight, grad in zip(ex.arg_arrays, ex.grad_arrays): weight[:] -= learning_rate * (grad / batch_size) if i % 10 == 0: acc = (mx.nd.argmax_channel(ex.outputs[0]).asnumpy() == y).sum() final_acc = acc print('iteration %d, accuracy %f' % (i, float(acc)/y.shape[0])) assert final_acc > 0.95, "Low training accuracy."
iteration 0, accuracy 0.080000 iteration 10, accuracy 0.920000 iteration 20, accuracy 1.000000 iteration 30, accuracy 1.000000 iteration 40, accuracy 1.000000 iteration 50, accuracy 1.000000 iteration 60, accuracy 1.000000 iteration 70, accuracy 1.000000 iteration 80, accuracy 1.000000 iteration 90, accuracy 1.000000
このセクションでは完全なトレーニング・アルゴリズムを実装するために命令型 NDArray と記号型 Symbol を一緒にどのように使うかを示します。前者はしばしば以下のために使用されます :
- データ・コンテナ
- 更新ルールの実装と最適化メソッドの進捗のモニタリングのような柔軟性を必要とするプログラム
- Symbol 演算子の実装
- プリンティングと step-by-step 実行のようなデバッグ
一方で後者は目的関数の定義のために使用でき、それは Symbol と自動微分上に置かれる重たい最適化の利益を得ます。
マルチ-デバイスでデータ並列処理
NDArray チュートリアル でバックエンド・システムは計算を自動的に並列化する能力があることを述べました。この特徴は MXNet では並列プログラムの開発をシリアル・プログラムを書くの同じ程度に簡単にします。
ここで GPUs と CPUs のような複数のデバイスを使用してデータ並列処理でどのようにトレーニング・プログラムを開発するかを示します。MXNet では、デバイスはそれ自身がメモリを持つ計算リソースを意味します。それは GPU チップまたは全ての CPU チップです :
- GPU チップは計算ユニットとメモリの両者を含む GPU ユニットです。Nvidia GPU については、全てのユニットをリストするために nvidia-smi が使用できます。通常は物理的な GPU カードは単一の GPU チップのみを含みますが、幾つかのカードは一つのユニット以上を持つかもしれません。例えば、各 Tesla K80 は2つの GK210 チップを含みます。
- 全ての CPU。一つの物理的な CPU チップ以上があったとしても、MXNet では $mx.cpu()$ として参照可能な単一のデバイスとして全ての CPU を単純に扱います。その理由はこれらの CPU は同じメインメモリを共有するからです。
各 iteration で size $n$ でミニバッチをトレーニングすると仮定します。データ並列処理では、このバッチを全ての利用可能なデバイスにそれらの計算パワーに従って分割します。各デバイスはバッチの一部での勾配を計算し、そしてこれの勾配はマージされます。
さて上記のトレーニング・プログラムを複数のデバイスに拡張します、新しい関数はネットワーク、データ iterator、デバイスのリストそしてそれらの計算パワーを受け取ります。
def train(network, data_shape, data, devs, devs_power): # partition the batch into each device batch_size = float(data_shape[0]) workloads = [int(round(batch_size/sum(devs_power)*p)) for p in devs_power] print('workload partition: ', zip(devs, workloads)) # create an executor for each device exs = [network.simple_bind(ctx=d, data=tuple([p]+data_shape[1:])) for d, p in zip(devs, workloads)] args = [dict(zip(network.list_arguments(), ex.arg_arrays)) for ex in exs] # initialize weight on dev 0 for name in args[0]: arr = args[0][name] if 'weight' in name: arr[:] = mx.random.uniform(-0.1, 0.1, arr.shape) if 'bias' in name: arr[:] = 0 # run 50 iterations learning_rate = 0.1 acc = 0 for i in range(50): # broadcast weight from dev 0 to all devices for j in range(1, len(devs)): for name, src, dst in zip(network.list_arguments(), exs[0].arg_arrays, exs[j].arg_arrays): if 'weight' in name or 'bias' in name: src.copyto(dst) # get data x, y = data() for j in range(len(devs)): # partition and assign data idx = range(sum(workloads[:j]), sum(workloads[:j+1])) args[j]['data'][:] = x[idx,:].reshape(args[j]['data'].shape) args[j]['out_label'][:] = y[idx].reshape(args[j]['out_label'].shape) # forward and backward exs[j].forward(is_train=True) exs[j].backward() # sum over gradient on dev 0 if j > 0: for name, src, dst in zip(network.list_arguments(), exs[j].grad_arrays, exs[0].grad_arrays): if 'weight' in name or 'bias' in name: dst += src.as_in_context(dst.context) # update weight on dev 0 for weight, grad in zip(exs[0].arg_arrays, exs[0].grad_arrays): weight[:] -= learning_rate * (grad / batch_size) # monitor if i % 10 == 0: pred = np.concatenate([mx.nd.argmax_channel(ex.outputs[0]).asnumpy() for ex in exs]) acc = (pred == y).sum() / batch_size print('iteration %d, accuracy %f' % (i, acc)) return acc
今 cpu と gpu の両者を使用して前のネットワークをトレーニングできます。それは cpu だけを使用した時と同様の結果を与えるでしょう。
It should give similar results as using cpu only.
# @@@ AUTOTEST_OUTPUT_IGNORED_CELL batch_size = 100 acc = train(net, [batch_size, num_features], lambda : toy_data.get(batch_size), [mx.cpu(), mx.gpu()], [1, 5]) assert acc > 0.95, "Low training accuracy."
('workload partition: ', [(cpu(0), 17), (gpu(0), 83)]) iteration 0, accuracy 0.170000 iteration 10, accuracy 1.000000 iteration 20, accuracy 1.000000 iteration 30, accuracy 1.000000 iteration 40, accuracy 1.000000
前のネットワークは複数のデバイスへ移行するパフォーマンスの恩恵を見るには小さすぎる点に注意してください。そこで少しより複雑なネットワークを使うことを考えます : 手書き数字認識のための LeNet-5 です。最初にネットワークを定義します。
def lenet(): data = mx.sym.Variable('data') # first conv conv1 = mx.sym.Convolution(data=data, kernel=(5,5), num_filter=20) tanh1 = mx.sym.Activation(data=conv1, act_type="tanh") pool1 = mx.sym.Pooling(data=tanh1, pool_type="max", kernel=(2,2), stride=(2,2)) # second conv conv2 = mx.sym.Convolution(data=pool1, kernel=(5,5), num_filter=50) tanh2 = mx.sym.Activation(data=conv2, act_type="tanh") pool2 = mx.sym.Pooling(data=tanh2, pool_type="max", kernel=(2,2), stride=(2,2)) # first fullc flatten = mx.sym.Flatten(data=pool2) fc1 = mx.sym.FullyConnected(data=flatten, num_hidden=500) tanh3 = mx.sym.Activation(data=fc1, act_type="tanh") # second fullc fc2 = mx.sym.FullyConnected(data=tanh3, num_hidden=10) # loss lenet = mx.sym.SoftmaxOutput(data=fc2, name='out') return lenet mx.viz.plot_network(lenet(), shape={'data':(128,1,28,28)})

次に mnist データセットを準備します。
from sklearn.datasets import fetch_mldata import numpy as np import matplotlib.pyplot as plt class MNIST: def __init__(self): mnist = fetch_mldata('MNIST original') p = np.random.permutation(mnist.data.shape[0]) self.X = mnist.data[p] self.Y = mnist.target[p] self.pos = 0 def get(self, batch_size): p = self.pos self.pos += batch_size return self.X[p:p+batch_size,:], self.Y[p:p+batch_size] def reset(self): self.pos = 0 def plot(self): for i in range(10): plt.subplot(1,10,i+1) plt.imshow(self.X[i].reshape((28,28)), cmap='Greys_r') plt.axis('off') plt.show() mnist = MNIST() mnist.plot()
最初に単一の GPU 上で lenet をトレーニングします。
# @@@ AUTOTEST_OUTPUT_IGNORED_CELL import time batch_size = 1024 shape = [batch_size, 1, 28, 28] mnist.reset() tic = time.time() acc = train(lenet(), shape, lambda:mnist.get(batch_size), [mx.gpu(),], [1,]) assert acc > 0.8, "Low training accuracy." print('time for train lenent on cpu %f sec' % (time.time() - tic))
('workload partition: ', [(gpu(0), 1024)]) iteration 0, accuracy 0.071289 iteration 10, accuracy 0.815430 iteration 20, accuracy 0.896484 iteration 30, accuracy 0.912109 iteration 40, accuracy 0.932617 time for train lenent on cpu 2.708110 sec
それから複数の GPUs にトライします。次のコードは 4 GPUs を必要とします。
# @@@ AUTOTEST_OUTPUT_IGNORED_CELL for ndev in (2, 4): mnist.reset() tic = time.time() acc = train(lenet(), shape, lambda:mnist.get(batch_size), [mx.gpu(i) for i in range(ndev)], [1]*ndev) assert acc > 0.9, "Low training accuracy." print('time for train lenent on %d GPU %f sec' % ( ndev, time.time() - tic))
('workload partition: ', [(gpu(0), 512), (gpu(1), 512)]) iteration 0, accuracy 0.104492 iteration 10, accuracy 0.741211 iteration 20, accuracy 0.876953 iteration 30, accuracy 0.914062 iteration 40, accuracy 0.924805 time for train lenent on 2 GPU 1.623732 sec ('workload partition: ', [(gpu(0), 256), (gpu(1), 256), (gpu(2), 256), (gpu(3), 256)]) iteration 0, accuracy 0.092773 iteration 10, accuracy 0.777344 iteration 20, accuracy 0.887695 iteration 30, accuracy 0.908203 iteration 40, accuracy 0.916992 time for train lenent on 4 GPU 1.086430 sec
見て分かるように、更なる GPU の利用は速度を加速します。スピードアップは完全ではありません、何故ならばネットワークが依然として単純で、計算と通信のパイプラインによる複数 GPU に渡る通信コストを完全には隠せていないからです。最先端のネットワークの使用により更に良い結果を観察しました。次の図は 8 Nvidia Tesla M40 の使用による imagenet 勝利者のスピードアップを示します。
Next Steps
以上