六本木で働くデータサイエンティストのブログ

元祖「銀座で働くデータサイエンティスト」です / 道玄坂→銀座→東京→六本木

KerasをTensorFlowバックエンドで試してみた:「もっと多くの人に機械学習とDeep Learningを」という時代の幕開け

f:id:TJO:20160608131830p:plain
(左:Keras、右:MXnet)

Kaggle Masterの間ではMXnetよりさらに人気なDeep Learningフレームワークというかラッパーが、@氏の手によるKeras。

結構苦心したのですが、ようやく手元のPython環境で走るようになったので、試してみました。なおKerasの概要と全体像についてはid:aidiaryさんが詳細な解説を書いて下さっているので、そちらの方を是非お読み下さい。


追記

Kerasは人気のフレームワークなので、僕なんぞがこんなブログ記事を書く前から素晴らしい紹介記事・スクリプトが幾つもあります。こちらでは参考までに以下のお二方のものをご紹介させていただきます。


Kerasはレゴブロックを組み合わせるかのようにして、簡単にディープラーニングのモデルを作成できる便利なライブラリです。これを使って楽しく開発できるのではないかと思います。

まことにそう思います。


一応こちらでも簡単に解説しておくと、KerasはバックエンドにTensorFlowもしくはTheanoの好きな方を選べば良い、というDeep Learningフレームワークのラッパーです。開発者の@氏とちょっと個人的にやり取りしたことがあるんですが、彼が語っていたのは「Theanoはもはや煩雑過ぎるし、TensorFlowは使いやすいが計算グラフの理解のところが難しくて万人向けではない」というコメント。「出来るだけ多くの人にDeep Learningに触れてもらいたい」というのがその開発理念だそうです。


ちなみに、彼がTwitterで呟いていたのをチラッとご覧になった方もいるかもしれませんが、Kerasドキュメントの日本語化プロジェクトが目下進行中のようです*1。MXnetが英語以外では中国語ドキュメントのみが整備されていて中国語圏で人気が高いことを鑑みるに、いずれKerasも日本人に最も馴染み深いDeep Learningフレームワークの一つになるかもしれませんね。


インストール


先にTensorFlowを入れておく必要があります。

詳しくはTensorFlowのドキュメントを見てもらいたいのですが、環境によって入れ方が結構異なる点に要注意。また既存のNumPyが原因でコケるケースがあるので、その場合の対処法もチェックしておきましょう。一応、ドキュメントに従うとPython 2系の場合は

# Ubuntu/Linux 64-bit
$ sudo apt-get install python-pip python-dev

# Mac OS X
$ sudo easy_install pip
$ sudo easy_install --upgrade six

の後に

# Ubuntu/Linux 64-bit, CPU only, Python 2.7
$ export TF_BINARY_URL=https://storage.googleapis.com/tensorflow/linux/cpu/tensorflow-0.9.0rc0-cp27-none-linux_x86_64.whl

# Ubuntu/Linux 64-bit, GPU enabled, Python 2.7 
# Requires CUDA toolkit 7.5 and CuDNN v4. For other versions, see "Install from sources" below.
$ export TF_BINARY_URL=https://storage.googleapis.com/tensorflow/linux/gpu/tensorflow-0.9.0rc0-cp27-none-linux_x86_64.whl

# Mac OS X, CPU only, Python 2.7:
$ export TF_BINARY_URL=https://storage.googleapis.com/tensorflow/mac/tensorflow-0.9.0rc0-py2-none-any.whl

として

# Python 2
$ sudo pip install --upgrade $TF_BINARY_URL

で入る、ということになっています。他にもドキュメントにはVirtualEnv, Anaconda, Dockerで入れる方法が書いてありますがここでは割愛します。


また、別の問題として事前にcondaでNumPyを入れてある場合はこれがコケてしまうというケースが最近あるようです*2。この場合は仕方ないので、NumPyだけ別に入れ直す必要がありそうです。

他にもNumPy周りでコケるケースは結構あるようで、僕が実際に見た範囲だとこういうエラーもあるみたいです*3。リンク先の通りにすれば解決する。。。と思いますが、僕の環境にはいくつか制約があってまだ試してませんorz


その後でKerasを入れます。インストール方法は普通にKerasのドキュメントに書いてある通りで、ソースをgit cloneしてきてから

$ sudo python setup.py install

でも良いですし、普通にpipで

sudo pip install keras

でも入ります。入ったら今度はTensorFlowバックエンドで動かすことを想定しているので、Kerasのドキュメントに従って設定ファイル

~/.keras/keras.json

を以下のように書き換えます(一度起動すると生成されるJSONファイルだが面倒なら最初からエディタで書いてしまっても良い)。

# before
{"epsilon": 1e-07, "floatx": "float32", "backend": "theano"}

# after
{"epsilon": 1e-07, "floatx": "float32", "backend": "tensorflow"}

これで一応Kerasが動くようになるはずです。


KerasのCNNでMNIST短縮版の分類をやってみる


基本的にはKerasのドキュメントに出ているサンプルコードをほぼそのまま当てはめただけです。が、元のコードだと若干パフォーマンスが下がるので、僕の方で適当にチューニングし直しています。MNIST短縮版のCSVファイル2つは以下の僕のGitHubから取ってきてください。

一応、TensorFlowバックエンドかどうかを確認しておきましょう。

import keras
Using TensorFlow backend.
Using TensorFlow backend.


その後は以下のように組めば、基本的には走るはずです。

from __future__ import print_function
from keras.layers.convolutional import Convolution2D
from keras.layers.convolutional import MaxPooling2D
from keras.layers.core import Activation
from keras.layers.core import Dense
from keras.layers.core import Dropout
from keras.layers.core import Flatten
from keras.models import Sequential
from keras.utils import np_utils

import numpy
from pandas import DataFrame
from pandas import read_csv
data_train = read_csv("short_prac_train.csv")
data_test = read_csv("short_prac_test.csv")

batch_size = 100
nb_classes = 10
nb_epoch = 10

# input image dimensions
img_rows, img_cols = 28, 28
# number of convolutional filters to use
nb_filters = 20
# size of pooling area for max pooling
nb_pool = 2
# convolution kernel size
nb_conv = 5

# the data, shuffled and split between tran and test sets
x_train = numpy.asarray([data_train.ix[i][1:] for i in range(len(data_train))])
x_test = numpy.asarray([data_test.ix[i][1:] for i in range(len(data_test))])
y_train = data_train['label']
y_test = data_test['label']

x_train = x_train.reshape(x_train.shape[0], img_rows, img_cols, 1)
x_test = x_test.reshape(x_test.shape[0], img_rows, img_cols, 1)
x_train = x_train.astype("float32")
x_test = x_test.astype("float32")
x_train /= 255
x_test /= 255
print("x_train shape:", x_train.shape)
print(x_train.shape[0], "train samples")
print(x_test.shape[0], "test samples")

# convert class vectors to binary class matrices
y_train = np_utils.to_categorical(y_train, nb_classes)
y_test = np_utils.to_categorical(y_test, nb_classes)

model = Sequential()
model.add(Convolution2D(nb_filters, nb_conv, nb_conv,
                        border_mode="valid",
                        input_shape=(img_rows, img_cols, 1)))
model.add(Activation("relu"))
model.add(Convolution2D(nb_filters, nb_conv, nb_conv))
model.add(Activation("relu"))
model.add(MaxPooling2D(pool_size=(nb_pool, nb_pool)))
model.add(Dropout(0.5))

model.add(Flatten())
model.add(Dense(128))
model.add(Activation("relu"))
model.add(Dropout(0.5))
model.add(Dense(nb_classes))
model.add(Activation("softmax"))

model.compile(loss="categorical_crossentropy", optimizer="adadelta",
              metrics=["accuracy"])

model.fit(x_train, y_train, batch_size=batch_size, nb_epoch=nb_epoch,
          verbose=1, validation_data=(x_test, y_test))
score = model.evaluate(x_test, y_test, show_accuracy=True, verbose=0)
print("Test score:", score[0])
print("Test accuracy:", score[1])
x_train shape: (5000, 28, 28, 1)
5000 train samples
1000 test samples
Train on 5000 samples, validate on 1000 samples
Epoch 1/10
5000/5000 [==============================] - 5s - loss: 0.9321 - acc: 0.6978 - val_loss: 0.1817 - val_acc: 0.9340
# ... #
Epoch 10/10
5000/5000 [==============================] - 13s - loss: 0.0895 - acc: 0.9710 - val_loss: 0.0468 - val_acc: 0.9890
Test score: 0.0467970174281
Test accuracy: 0.989

ということで、MXnetがほぼ同じパラメータ設定で出したTest accuracy 0.987(後述)とほぼ同じ結果になりました。


MXnetのCNNでMNIST短縮版の分類をやってみる


以前の過去記事で既にお見せした通りですが、一応再掲しておきます。


# Data preparation
> train<-read.csv('https://github.com/ozt-ca/tjo.hatenablog.samples/raw/master/r_samples/public_lib/jp/mnist_reproduced/short_prac_train.csv')
> test<-read.csv('https://github.com/ozt-ca/tjo.hatenablog.samples/raw/master/r_samples/public_lib/jp/mnist_reproduced/short_prac_test.csv')
> train<-data.matrix(train)
> test<-data.matrix(test)
> train.x<-train[,-1]
> train.y<-train[,1]
> train.x<-t(train.x/255)
> test<-test[,-1]
> test<-t(test/255)

# Model
> data <- mx.symbol.Variable("data")
> devices<-mx.cpu()
> # first conv
> conv1 <- mx.symbol.Convolution(data=data, kernel=c(5,5), num_filter=20)
> tanh1 <- mx.symbol.Activation(data=conv1, act_type="relu")
> pool1 <- mx.symbol.Pooling(data=tanh1, pool_type="max",
+                            kernel=c(2,2), stride=c(2,2))
> drop1 <- mx.symbol.Dropout(data=pool1,p=0.5)
> # second conv
> conv2 <- mx.symbol.Convolution(data=drop1, kernel=c(5,5), num_filter=50)
> tanh2 <- mx.symbol.Activation(data=conv2, act_type="relu")
> pool2 <- mx.symbol.Pooling(data=tanh2, pool_type="max",
+                            kernel=c(2,2), stride=c(2,2))
> drop2 <- mx.symbol.Dropout(data=pool2,p=0.5)
> # first fullc
> flatten <- mx.symbol.Flatten(data=drop2)
> fc1 <- mx.symbol.FullyConnected(data=flatten, num_hidden=500)
> tanh4 <- mx.symbol.Activation(data=fc1, act_type="relu")
> drop4 <- mx.symbol.Dropout(data=tanh4,p=0.5)
> # second fullc
> fc2 <- mx.symbol.FullyConnected(data=drop4, num_hidden=10)
> # loss
> lenet <- mx.symbol.SoftmaxOutput(data=fc2)
> train.array <- train.x
> dim(train.array) <- c(28, 28, 1, ncol(train.x))
> test.array <- test
> dim(test.array) <- c(28, 28, 1, ncol(test))
> mx.set.seed(0)
> tic <- proc.time()
> model <- mx.model.FeedForward.create(lenet, X=train.array, y=train.y,
+                                      ctx=devices, num.round=60, array.batch.size=100,
+                                      learning.rate=0.05, momentum=0.9, wd=0.00001,
+                                      eval.metric=mx.metric.accuracy,
+                                      epoch.end.callback=mx.callback.log.train.metric(100))
Start training with 1 devices
[1] Train-accuracy=0.0975510204081633
[2] Train-accuracy=0.0906
[3] Train-accuracy=0.09
# ... #
[59] Train-accuracy=0.978
[60] Train-accuracy=0.9822
> print(proc.time() - tic)
   ユーザ   システム       経過  
   784.666      3.767    677.921 
> preds <- predict(model, test.array)
> pred.label <- max.col(t(preds)) - 1
> table(test_org[,1],pred.label)
   pred.label
      0   1   2   3   4   5   6   7   8   9
  0  99   0   0   0   0   0   1   0   0   0
  1   0  99   0   0   1   0   0   0   0   0
  2   0   0  98   0   0   0   0   1   1   0
  3   0   0   0  98   0   1   0   0   1   0
  4   0   2   0   0  97   0   1   0   0   0
  5   0   0   0   0   0  99   1   0   0   0
  6   0   0   0   0   0   0 100   0   0   0
  7   0   0   0   0   0   0   0  99   1   0
  8   0   0   0   0   0   0   0   0 100   0
  9   0   0   0   0   2   0   0   0   0  98
> sum(diag(table(test_org[,1],pred.label)))/1000
[1] 0.987

上述のように、MXnetではtest accuracy 0.987ということでKerasの方がMXnetを上回っていますが、その差は僅か0.002。事実上ほぼ同等の性能と見て良いでしょう。そしてKeras / MXnetともまだチューニングの余地が多くあることを鑑みるに、精度面では互角と言って良いかと思います。


一方、スピードの方はMXnetではこのaccuracyを出すのに60 epochsを費やして全体で677秒。Kerasだと10 epochsで200秒前後。MXnetでベストチューニングをすればまた異なるかもしれませんが、今回の例ではKerasの方が優れているという結果になっています。


MXnetとKerasの比較(特にモデル記述部分)


ところで、個人的にKeras / MXnetともに大きなアドバンテージだなと思っているのは、その「直感的なインタフェース」です。基本的には、青本『深層学習』を読めば身につく知識をそのまま以下のようなCNN設計部分に記述すれば、走ってくれます。

MXnetの場合

> data <- mx.symbol.Variable("data")
> # first conv
> conv1 <- mx.symbol.Convolution(data=data, kernel=c(5,5), num_filter=20)
> tanh1 <- mx.symbol.Activation(data=conv1, act_type="relu")
> pool1 <- mx.symbol.Pooling(data=tanh1, pool_type="max",
+                            kernel=c(2,2), stride=c(2,2))
> drop1 <- mx.symbol.Dropout(data=pool1,p=0.5)
> # second conv
> conv2 <- mx.symbol.Convolution(data=drop1, kernel=c(5,5), num_filter=50)
> tanh2 <- mx.symbol.Activation(data=conv2, act_type="relu")
> pool2 <- mx.symbol.Pooling(data=tanh2, pool_type="max",
+                            kernel=c(2,2), stride=c(2,2))
> drop2 <- mx.symbol.Dropout(data=pool2,p=0.5)
> # first fullc
> flatten <- mx.symbol.Flatten(data=drop2)
> fc1 <- mx.symbol.FullyConnected(data=flatten, num_hidden=500)
> tanh4 <- mx.symbol.Activation(data=fc1, act_type="relu")
> drop4 <- mx.symbol.Dropout(data=tanh4,p=0.5)
> # second fullc
> fc2 <- mx.symbol.FullyConnected(data=drop4, num_hidden=10)
> # loss
> lenet <- mx.symbol.SoftmaxOutput(data=fc2)
> train.array <- train.x
> dim(train.array) <- c(28, 28, 1, ncol(train.x))
> test.array <- test
> dim(test.array) <- c(28, 28, 1, ncol(test))
> mx.set.seed(0)
> tic <- proc.time()
> model <- mx.model.FeedForward.create(lenet, X=train.array, y=train.y,
+                                      ctx=devices, num.round=60, array.batch.size=100,
+                                      learning.rate=0.05, momentum=0.9, wd=0.00001,
+                                      eval.metric=mx.metric.accuracy,
+                                      epoch.end.callback=mx.callback.log.train.metric(100))

Kerasの場合

batch_size = 100
nb_classes = 10
nb_epoch = 20

# input image dimensions
img_rows, img_cols = 28, 28
# number of convolutional filters to use
nb_filters = 20
# size of pooling area for max pooling
nb_pool = 2
# convolution kernel size
nb_conv = 5

model = Sequential()
model.add(Convolution2D(nb_filters, nb_conv, nb_conv,
                        border_mode="valid",
                        input_shape=(img_rows, img_cols, 1)))
model.add(Activation("relu"))
model.add(Convolution2D(nb_filters, nb_conv, nb_conv))
model.add(Activation("relu"))
model.add(MaxPooling2D(pool_size=(nb_pool, nb_pool)))
model.add(Dropout(0.5))

model.add(Flatten())
model.add(Dense(128))
model.add(Activation("relu"))
model.add(Dropout(0.5))
model.add(Dense(nb_classes))
model.add(Activation("softmax"))

model.compile(loss="categorical_crossentropy", optimizer="adadelta",
              metrics=["accuracy"])

model.fit(x_train, y_train, batch_size=batch_size, nb_epoch=nb_epoch,
          verbose=1, validation_data=(x_test, y_test))


構成としてはKerasもMXnetもよく似ています。モデル記述部分は各層の中身(例えばユニット数・カーネルサイズ・プーリングサイズ・活性化関数・Dropout率など)をただベタッと書いてただひたすらメソッドを順番に書き足していけば良いだけで、後で全体のモデルを計算するメソッドを走らせるだけです。OOPで書かれている分、Kerasの方がスッキリしたスクリプトになっている印象がありますね。一方でMXnetの方がもう少し細かく調整できる感じもありまして、ここはお互いに一長一短と言ったところでしょうか。


他にも、例えばチューニングに拘りたいユーザーなら気になる最適化手法の選択のところも、Kerasであればcompileメソッドのoptimizer引数で、MXnetであればmx.model.FeedForward.createメソッドのoptimizer引数で、お好みのものを選ぶことができます。Theanoで書くと面倒臭いことこの上ないらしい*4ので、そういう点でもあらゆる点で組み上げるのが非常に楽だなと感じています。


Deep Learningのみならず機械学習のdemocratization(民主化)へ


最近、業界で時々耳にするキーワードが機械学習のdemocratization(民主化)」。言い換えると「一握りのエキスパートだけでなく、多くの新規参入者が気軽にDeep Learningのような先進的な機械学習手法を実践できるようになる」ということです。


実際問題、ちょっと前までの機械学習業界*5はどちらかというとゴリゴリ何もかもスクラッチから組めるコーディング力と機械学習の学識の双方に長けた人材でないと手が出せない世界でしたし、その後例えばscikit-learnのようなパッケージ・ライブラリが広く普及するようになった後も、Deep Learningを初めとした先進的なモデル・アルゴリズムの実践には依然として高い壁が立ちはだかっているという印象がありました。


特にDeep Learningはしばらくの間GPUで動かすのがほぼ前提とされていた時期もあり、またTheanoで組むとなるとスクラッチから組むよりは遥かにマシなもののそれでも結構煩雑だというのもあって、正直手を出しづらいなぁと個人的には感じていました。以下のリンク先にあるのはid:aidiaryさんの手によるTheanoを用いたCNN実装ですが、上記のKeras / MXnetによる実装と比べるとやはりかなりの労作になるというのがお分かりになるかと思います。


これらに比べると、Keras / MXnetのシンプルさとスクリプトの分かりやすさ、そしてスクリプトが分かりやすいにもかかわらずチューニングが容易にできるという便利さが特に際立って見て取れると思います。


そしてKerasの場合はバックエンドにTensorFlowを利用できるということで、そのネームバリューとシステムセットアップの簡便さから今後広く普及することが確実視されるTensorFlowがインストールされている環境であれば気軽に使える、というのもKerasの利点と言えるでしょう。言い換えると、TensorFlowが持つCPU / GPU切り替えの容易さ、分散環境への拡張の容易さなどなどにそのまま乗っかることができるので、さらなる発展も望めます。


もしかしたらKeras / MXnetよりもさらにさらに容易にDeep Learningを組めるシンプルなフレームワークが登場するかもしれませんが*6、今後これらのフレームワークによるDeep Learningそして機械学習そのもののdemocratizationが進んでいくものと期待しております。

*1:@氏はそもそも日本での留学経験があるということで、日本でも是非Kerasを広めたいというお話のようです

*2:自腹EC2インスタンスに入れる時はこれでハマったorz

*3:MBPに入れる時はこれでハマったorz

*4:自分では組んだことないもので伝聞です笑

*5:学術系ではなく実務系ですら

*6:いや必ずや何かしらそういうものが登場するだろうと予想していますが