MXNet
MXNetはコンピュータ・サイエンスの名門大学、カーネギーメロン大学が主体となって開発しているディープラーニングのフレームワークです。
注目すべきはその豊富な対応言語で、多くのフレームワークが採用しているPythonはもちろん、データ解析に向いているR言語や、科学技術計算に向いていると注目されるJulia、他にもC++、Perl、Scalaなどにも対応しています。
これらの言語はMXNetを使ってネットワークの構築と学習を行うために使うことができ(もちろん予測器としても使えますが)、その後、jsonなどで保存したモデルとバイナリで保存したパラメータを使って、MatlabやJavascriptなどで予測器とインポートすることが可能なようです。
少しだけMXNetを触ってみて
前々から名前は知っていましたが、KerasがMXNetをバックエンドで用いること(現在はKeras1.2.2で仮の対応中?)が決定し、さらにAWSでもMXNetの利用が可能となりました。
ここで一度しっかりドキュメントを読んでみようと思った次第です。そして、MXNetをちゃんと見てみると、Keras使わなくても十分楽に書ける上、場合によっては低レベルの処理を書くことも可能であることがわかってきました。
とりあえず今回は、ネットワーク構築の部分を抜き出して羅列してみます。TensorFlowのような宣言型の書き方も、Chainerのような命令型の書き方可能で、それらを両立することもできるようです。
まだまだちょっと触ってみただけですが、ガチンコでディープラーニングやるならこれでも良いかもしれないと思える素晴らしいフレームワークだと感じています。
MXNetの特徴
・NdArrayと呼ばれるNumpyライクな独自のデータ構造を利用している。
・NdArrayの処理を使いディープラーニングをスクラッチで行える。
・もちろん数行でニューラルネットを構築するためのAPIも豊富
Chainer
ChainerはCupyやNumpyを基本的な配列計算のためのデータ構造として採用(あるいは開発)し、純粋にPython言語としてCupyやNumpyの処理を呼び出しています。ニューラルネットに特化したPythonのモジュール群のようなものです。
TensorFlow
TensorFlowはTensorを配列計算のためのデータ構造に採用し、低レベルの処理はC++によって実装しています。また、全ての計算を計算グラフで行うため、計算グラフを宣言していく形でプログラミングを行うため、若干クセが有ると感じるでしょう。計算グラフの宣言を(特にニューラルネットについて)サポートするために、PythonによるAPIを提供しているという形になります。
Theano
Theanoはそもそもニューラルネットのフレームワークというよりは、Numpyのように科学技術計算を行うためのPythonライブラリであり、低レベルの計算処理も可能です。Theanoを使うというのはいわば、NumpyやCupyでスクラッチでニューラルネットを書くようなもので、多分いちばん玄人向けだと思われます(それは言いすぎか。もうニューラルネットに特化したクラス・関数がいっぱいある)。
Keras
Kerasは完全に上記のものとは毛色が違い、TheanoやTensorFlowのラッパーです。これ単体でディープラーニングの処理が可能なわけではなく、あくまで、他のフレームワークを呼び出している(裏で使っている)という形になります。
MXNet
MXNetはというと、NdArrayがいわばTheanoやNumpy的なポジションであり、そのNdArrayを簡単に扱えるように、いろいろなAPIをセットで提供しているという形になります。
一方でSymbolAPIなるものも提供されており、こちらが宣言的に書いていく方法になります。
以下で、MXNetのネットワークの定義しの仕方を見ていきましょう(あくまで定義の仕方見るだけで、これを書けば動くわけではありません)。
生TensorFlowのように書く
NdArray(nd)を使ってスクラッチ気味に書くことで、生TensorFlowのような書き方が可能になります。NdArrayには足し算や引き算と同じように、ConvolutionやPoolingの計算がメソッドとして含まれています。こちらは宣言型ではないため、 コードの見た目は生TensorFlowですが、動作自体はまるっきりNumpyのような命令型になっていると思われます。
W1 = nd.random_normal(shape=(20, 1, 3,3)) *.01
b1 = nd.random_normal(shape=20) * .01
W2 = nd.random_normal(shape=(50, 20, 5, 5)) *.01
b2 = nd.random_normal(shape=50) * .01
W3 = nd.random_normal(shape=(800,128)) *.01
b3 = nd.random_normal(shape=128) *.01
W4 = nd.random_normal(shape=(128,10)) *.01
b4 = nd.random_normal(shape=10) *.01
params = [W1, b1, W2, b2, W3, b3, W4, b4]
for param in params:
param.attach_grad()
def softmax(y_linear):
exp = nd.exp(y_linear-nd.max(y_linear))
partition =nd.sum(exp, axis=0, exclude=True).reshape((-1,1))
return exp / partition
def relu(X):
return nd.maximum(X,nd.zeros_like(X))
pool = nd.Pooling(data=conv, pool_type="max", kernel=(2,2), stride=(2,2))
print(pool.shape)
def net(X, debug=False):
h1_conv = nd.Convolution(data=X, weight=W1, bias=b1, kernel=(3,3), num_filter=20)
h1_activation = relu(h1_conv)
h1 = nd.Pooling(data=h1_activation, pool_type="avg", kernel=(2,2), stride=(2,2))
if debug:
print("h1 shape: %s" % (np.array(h1.shape)))
h2_conv = nd.Convolution(data=h1, weight=W2, bias=b2, kernel=(5,5), num_filter=50)
h2_activation = relu(h2_conv)
h2 = nd.Pooling(data=h2_activation, pool_type="avg", kernel=(2,2), stride=(2,2))
if debug:
print("h2 shape: %s" % (np.array(h2.shape)))
########################
# Flattening h2 so that we can feed it into a fully-connected layer
########################
h2 = nd.flatten(h2)
if debug:
print("Flat h2 shape: %s" % (np.array(h2.shape)))
h3_linear = nd.dot(h2, W3) + b3
h3 = relu(h3_linear)
if debug:
print("h3 shape: %s" % (np.array(h3.shape)))
yhat_linear = nd.dot(h3, W4) + b4
yhat = softmax(yhat_linear)
if debug:
print("yhat shape: %s" % (np.array(yhat.shape)))
return yhat
tflearnのように書く
一方、TensorFlowにはtflearnのように、比較的簡単にニューラルネットワークを定義するモジュールが提供されており、これとほとんど同じような手順でネットワークの定義が可能です。
コードの見た目はtflearnのようであり、symbolを使った宣言的な書き方です。つまり、ここでの動作はほぼtensorflowをバックに動くtflearnと同じということになるでしょう。
フレームワークを触ったことが有る人は、以下のコードの説明をしなくともネットワークが想像できるできます。モデルの可視化もできます(とりあえず簡単なネットワークなのでこれだけ回しました)。
net = mx.sym.Variable('data')
net = mx.sym.FullyConnected(net, name='fc1', num_hidden=64)
net = mx.sym.Activation(net, name='relu1', act_type="relu")
net = mx.sym.FullyConnected(net, name='fc2', num_hidden=26)
net = mx.sym.SoftmaxOutput(net, name='softmax')
Chainerのように書く
Chainerのように書くこともできます。
Chainerをやったことがあれば、こちらも処理が完全に想像つくものになっているでしょう。
class Net(gluon.Block):
def __init__(self, **kwargs):
super(Net, self).__init__(**kwargs)
with self.name_scope():
# layers created in name_scope will inherit name space
# from parent layer.
self.conv1 = gluon.nn.Conv2D(20, kernel_size=(5,5))
self.pool1 = gluon.nn.MaxPool2D(pool_size=(2,2), strides = (2,2))
self.conv2 = gluon.nn.Conv2D(50, kernel_size=(5,5))
self.pool2 = gluon.nn.MaxPool2D(pool_size=(2,2), strides = (2,2))
self.fc1 = gluon.nn.Dense(500)
self.fc2 = gluon.nn.Dense(10)
def forward(self, x):
x = self.pool1(nd.tanh(self.conv1(x)))
x = self.pool2(nd.tanh(self.conv2(x)))
# 0 means copy over size from corresponding dimension.
# -1 means infer size from the rest of dimensions.
x = x.reshape((0, -1))
x = nd.tanh(self.fc1(x))
x = nd.tanh(self.fc2(x))
return x
net = Net()
また、MXNetは「mxnet.gloun.HyblidBlock」なるクラスを提供しており、これを用いてネットワークを構築すると、逐一呼び出すことが可能になる模様(もちろん、計算効率は落ちるが)。
class Net(gluon.HybridBlock):
def __init__(self, **kwargs):
super(Net, self).__init__(**kwargs)
with self.name_scope():
# layers created in name_scope will inherit name space
# from parent layer.
self.conv1 = nn.Conv2D(6, kernel_size=5)
self.pool1 = nn.MaxPool2D(pool_size=2)
self.conv2 = nn.Conv2D(16, kernel_size=5)
self.pool2 = nn.MaxPool2D(pool_size=2)
self.fc1 = nn.Dense(120)
self.fc2 = nn.Dense(84)
# You can use a Dense layer for fc3 but we do dot product manually
# here for illustration purposes.
self.fc3_weight = self.params.get('fc3_weight', shape=(10, 84))
def hybrid_forward(self, F, x, fc3_weight):
# Here `F` can be either mx.nd or mx.sym, x is the input data,
# and fc3_weight is either self.fc3_weight.data() or
# self.fc3_weight.var() depending on whether x is Symbol or NDArray
print('input x = ', x)
x = self.pool1(F.relu(self.conv1(x)))
x = self.pool2(F.relu(self.conv2(x)))
# 0 means copy over size from corresponding dimension.
# -1 means infer size from the rest of dimensions.
x = x.reshape((0, -1))
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = F.dot(x, fc3_weight, transpose_b=True)
return x
Kerasのように書く
ほぼ一緒にしか見えないレベルです。
net = gluon.nn.Sequential()
with net.name_scope():
net.add(gluon.Conv2D(channels=20, kernel_size=3, activation=‘relu')
net.add(gluon.Conv2D(channels=50, kernel_size=5, activation=‘relu')
net.add(gluon.nn.Flatten())
net.add(gluon.nn.Dense(128, activation="relu"))
net.add(gluon.nn.Dense(10))
もちろん学習コードもkerasのmodel.fitメソッドに相当するものもありますし、forループで書くこともできるので、むしろ柔軟に対応できるような印象です。
注意点
注意点というか、私がハマっただけなのですが、現状cudnn5.1では動かない模様(多分これが原因でGPUが動かなかった)。TensorFlowが次のアップデートでcudnn6.0に移行するようなので足並み揃えようかなと思っています。
また、LSTMに対する入力shapeが、
Kerasのように(batch_size, seq_len, vector_dim)なのか、
tensorflowのように、(batch_size, vector_dim)の時系列リストなのか、
chainerのnstepLSTMのように、(seq_len, vector_dim)のバッチサイズリストなのか、
いまいちよく分かっていません(それはこれからやるかもしれないし、やらないかもしれない)。
ご存知の方いたらご教授くださいませ(たぶん、どのAPI使うかによるんだと思いますが)。
また、「mxnet.gluon」によってChainer的な書き方やkeras的な書き方が提供されているのですが、これがパスかなんかの問題でインポートできない事例が発生しています(私も発生)。
また、公式にmxnet.gluonは大きく変わる予定だと書かれているので、とりあえず待つという形でいいかなと思います。