Python Lasagne でニューラルネットするチュートリアル その 1

@nishio さんに教えてもらったのだが、Lasagne というニューラルネットワークPython ライブラリが Kaggle でけっこうよく使われているらしい。
イタリア語読みすると「ラザーニェ」、Lasagna(ラザニア) の複数形なので、まあ日本人が呼ぶ分には「ラザニア」でいい気がする。


2015年6月現在でバージョンが 0.1.dev と、今手を出すのは人柱感満載。
実際、自分の思ったとおりのモデルを動かすのはなかなかに大変だったので、そのメモを残しておく。


インストールは別に難しいところはない。
ただ Theano 前提なので、Python 2.7 でないと動かないし、Windows で動かすのは茨の道だろう。


また、ドキュメントには "Install from PyPI" とあるくせに、pip ではインストールできない(ワナその1)。
ぐぐると、

Lasagne が PyPI からインストールできないんですけど
git clone でインストールできるよ
そりゃそうだろうけど、ドキュメントには "Install from PyPI" って書いてあるよ?
そんなこと言ってもできないもんはできないんだから、ガタガタぬかさず git から入れとけ

みたいなやりとりが引っかかって、ウケる。
というわけで、おとなしく git clone & python setup.py しよう。


インストール後、git clone した場所に examples というディレクトリがあって、かの MNIST を使ったサンプルコードが置いてある。
GPGPU が叩けない環境でも mnist.py と mnist_conv.py というサンプルは問題なく動くので、まずはそれで遊んでみるといい。
標準のサンプルなのにいきなり

The uniform initializer no longer uses Glorot et al.'s approach to determine the bounds, but defaults to the range (-0.01, 0.01) instead. Please use the new GlorotUniform initializer to get the old behavior. GlorotUniform is now the default for all layers.

みたいなワーニングが出るのだが、多分気にしたら負け。


mnist.py は 512 個ずつのユニットを持つ2段の隠れ層からなる古き良きニューラルネットワークで、環境にもよるだろうが2時間半くらい学習して 98.5% くらいの精度が出る。
mnist_conv.py は 5x5 の畳込みと 2x2 の max-pooling を2回重ねたあと、256 ユニットの隠れ層、そしてドロップアウトという今風のディープなニューラルネット。さすがに重く、それでも 27時間ほどで学習を終えて、99.4% の精度を叩き出す。
これが Python のコードをちょちょっと書くだけで動く(ウソ)んだから、楽しそうでしょう?


mnist.py のコードを見るとモデルを定義するのは簡単そうなので、簡単に使えるのかと思って、mnist.py を改造して自前のデータを僕の考えた最強のモデルに食わせようとしたら、図ったように動かない。
まず mnist.py のコードが無駄に複雑で、汎用化しているつもりなんだろうけど、明示していない仕様があれこれあるようで、謎の型エラーがバンバン出る。


よし、改造はあきらめて一からコードを書こう。ドキュメントにはちゃんと TUTORIAL の文字がある(ワナその2)。
開くと、

Understand the MNIST example
TODO:

良かった、紙のマニュアルだったら壁に叩きつけているところだった。電子化バンザイ。


しかたない、MNIST サンプルコードを理解してやろうじゃないか。
と、勢い込んで読み始めるが、学習や予測のためのコードが 100行以上あって、わずか数行で機械学習できる scikit-learn(ぬるま湯) に慣らされた ゆとり には大層ツライ。


ともあれ、そうして一応理解したつもりで、必要最小限にしぼった Lasagne のスモールサンプルコードがこちら。

import numpy
import lasagne
import theano
import theano.tensor as T

#### dataset
def digits_dataset(test_N = 400):
    import sklearn.datasets
    data = sklearn.datasets.load_digits()

    numpy.random.seed(0)
    z = numpy.arange(data.data.shape[0])
    numpy.random.shuffle(z)
    X = data.data[z>=test_N, :]
    y = numpy.array(data.target[z>=test_N], dtype=numpy.int32)
    test_X = data.data[z<test_N, :]
    test_y = numpy.array(data.target[z<test_N], dtype=numpy.int32)
    return X, y, test_X, test_y

X, y, test_X, test_y = digits_dataset()
N, input_dim = X.shape
n_classes = 10
print(X.shape, test_X.shape)


#### model
batch_size=100

l_in = lasagne.layers.InputLayer(
    shape=(batch_size, input_dim),
)
l_hidden1 = lasagne.layers.DenseLayer(
    l_in,
    num_units=512,
    nonlinearity=lasagne.nonlinearities.rectify,
)
l_hidden2 = lasagne.layers.DenseLayer(
    l_hidden1,
    num_units=64,
    nonlinearity=lasagne.nonlinearities.rectify,
)
model = lasagne.layers.DenseLayer(
    l_hidden2,
    num_units=n_classes,
    nonlinearity=lasagne.nonlinearities.softmax,
)

#### loss function
objective = lasagne.objectives.Objective(model,
    loss_function=lasagne.objectives.categorical_crossentropy)

X_batch = T.matrix('x')
y_batch = T.ivector('y')
loss_train = objective.get_loss(X_batch, target=y_batch)

#### update function
learning_rate = 0.01
momentum = 0.9
all_params = lasagne.layers.get_all_params(model)
updates = lasagne.updates.nesterov_momentum(
    loss_train, all_params, learning_rate, momentum)

#### training
train = theano.function(
    [X_batch, y_batch], loss_train,
    updates=updates
)

#### prediction
loss_eval = objective.get_loss(X_batch, target=y_batch,
                               deterministic=True)
pred = T.argmax(
    lasagne.layers.get_output(model, X_batch, deterministic=True),
    axis=1)
accuracy = T.mean(T.eq(pred, y_batch), dtype=theano.config.floatX)
test = theano.function([X_batch, y_batch], [loss_eval, accuracy])


#### inference
numpy.random.seed()
nlist = numpy.arange(N)
for i in xrange(100):
    numpy.random.shuffle(nlist)
    for j in xrange(N / batch_size):
        ns = nlist[batch_size*j:batch_size*(j+1)]
        train_loss = train(X[ns], y[ns])
    loss, acc = test(test_X, test_y)
    print("%d: train_loss=%.4f, test_loss=%.4f, test_accuracy=%.4f" % (i+1, train_loss, loss, acc))


このコードは何をやっているか。

  • データは scikit-learn の datasets に含まれる digits 。0 から 9 までの数字画像(16階調 8x8 ピクセル)が 1797 件。今回 scikit-learn はこのためだけw*1
  • 400 件をテストデータに、残り 1397 件を訓練データに回している。テストデータを切りの良い数字にしているのは次回への振り
  • モデルは隠れ層2層(1層目 512ユニット、2層目 64ユニット)。100周の学習で 97% くらいの精度になる。


細かい解説は次回に回すが、とりあえず Lasagne の守備範囲は、内部 DSL 的に記述されたモデルから、目的関数を生成するところだけということを念頭に置けば、このコードは特に苦もなく読めると思う。
学習におけるパラメータ更新とか、テストデータの評価とかはほぼ Theano 頼みで、現状はそこのつなぎを利用者が書く必要がある(だから書かないといけないコードが多い)。まあ 0.1.dev なんで。


またこのコードでは学習後のモデルを保存していないが(このサンプルデータの規模なら保存する必要もないだろうし)、まじめにやるなら当然その要望は出てくるだろう。
そのときは lasagne.layers.get_all_params(model) がパラメータを格納した Theano の SharedVariable のリストを返すので、こいつらを何らかの方法で永続化するといい。

続き。

*1:なので、わざわざ def してスコープを分けたところで sklearn を import している