MXNet チュートリアル : Mixed Programing

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

 

以上