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

昨日の記事の続き。

まずは、Lasagne のハマリポイントを紹介しながらコードの説明。
そのあと、ディープラーニング? ディープニューラルネットワーク? どちらの呼び名が正しいのかビミョウに自信ないが、要は畳み込み& max-pooling &ドロップアウトを織り交ぜたモデルを学習させようとするとハマリポイントが増えるので、そのあたりの注意点とサンプルコード。

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 の方は特筆することはなく、レコード数×次元のシンプルな ndarray でいいのだが。
Lasagne の出力層の非線形関数に softmax を指定した場合、出力層に与える正解ラベル y の型は暗黙に int32 が要求される。
うっかりここに普通の int の ndarray とか渡したりすると、

TypeError: ('Bad input argument to theano function with name "****.py:**" at index 1(0-based)', 'TensorType(int32, vector) cannot store a value of dtype int64 without risking loss of precision. If you do not mind this loss, you can: 1) explicitly cast your data to int32, or 2) set "allow_input_downcast=True" when calling "function".', array([...

と怒られて、どこのことを指しているのか見当つかず悩むハメになる。
このエラーが出たときは、正解ラベル y を numpy.array(*, dtype=numpy.int32) でくるむといい。

#### model
n_classes = 10
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,
)

モデルの定義は入力層 lasagne.layers.InputLayer から始めて、lasagne.layers.* の第一引数に前の層を指定しながらつないでいき、最後に適当な出力層を宣言すると、それがそのままモデルの参照となる。


入力層は shape で入力層の次元を指定するのは自然だが、一度に入力するデータのレコード数もこのモデルを定義する段階で指定する必要がある。これがハマリポイントその……いくつ目だっけ?
おそらく実装上の都合だろうが、適当な batch_size を決めて、入力データを batch_size 件ずつに分けて学習を回すのが Lasagne 流になる。ここで shape の値に、入力データの全データ件数を指定すると、学習のところでデータを分割して回す必要がなくなってコードの一部がすっきりするのだが、(おそらく)最急降下法相当になり、足がめちゃめちゃ遅くなる。
というわけで適当な batch_size を指定する必要がある。あまり小さいと速度が落ちるし、学習が落ち着かずに loss や accuracy が跳ねまわる。大きいと足が遅くなるし、余りのデータが無駄になる。
十分大きいデータなら 500 前後がほどよい印象( mnist.py は batch_size=600 になっている)。このサンプルコードでは、データが 2000件にも満たないので batch_size=100 にしている。


出力層は、ニューラルネットワークで何が作りたいかによって変わるだろうが、いちばん一般的な他クラス分類器なら、サンプルにある通り lasagne.layers.DenseLayer を使って、 num_units にクラス数を、 nonlinearity に lasagne.nonlinearities.softmax を指定すればいい。
クラス数は y.max()+1 とかしてもよかったけど、わかりきってるのでリテラル書いちゃった。

#### 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)

定義したニューラルネットワークモデルから目的関数を生成してくれるところが Lasagne の真骨頂である。
ああ、あとおそらくこの Objective をインスタンス化するまでのどこかのタイミングに、パラメータを格納する SharedVariable も用意してくれている。


loss_train は Theano の expression になっているので、theano.function に食わせれば実行コードにコンパイル済みの関数が得られる。Theano すげー。
あとは、この loss_train を使って、必要な関数や処理を作っていくことになる。

#### 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
)

Lasagne における学習は、Theano の updates の仕組みを使ってパラメータを更新する。
updates に渡す更新関数も、適切な lasagne.updates.* を呼べば Lasagne が作ってくれる。
Lasagne には単純な SGD も用意されているが、lasagne.updates.nesterov_momentum は Nesterov Moment を使った SGD になる。これは、「パラメータを前回動かした方向にしばらく動かす & 勾配を取る点も前回動かした方向に少しずらす」というもの。DNN 向けに SGD を改良したもので、収束が速くなる……のかな? mnist.py がこれを使っていたので、ここのサンプルもそれに倣った。


lasagne.layers.get_all_params は、前回の最後にもチラッと出ていたが、パラメータを格納した SharedVariable のリストを返すものである。
更新関数はもちろん対象となるそのパラメータたちを知らなければならないので必要性はわかるのだが、それをユーザが書かなければならないところには納得いかない(苦笑)。


学習率や Nesterov Moment の moment パラメータが固定で与えられているのは違和感があるかもしれない。
これを T.dscalar にして、theano.function の引数で与えるようにすれば可変にもできることは確認した。が、生 SGD より制御が難しく、結果をうまく改善できなかったので、mnist.py と同じように固定で与えている。


mnist.py では、この train 関数はデータを SharedVariable に置いて givens で渡す形で書かれている。そして引数はそのデータの中でその呼出で対象となる範囲を指すインデックスのみを指定する。特に GPU を用いる場合はデータの転送コストが一番高いわけで、そうすると mnist.py と同じ方式のほうが確実に効率いいだろう。
このサンプルでは簡素なコードを優先したことと、Lasagne を試している環境が GPGPU を利用できないものだったので(笑)、データを引数で渡すシンプルな形にした。

#### 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])

予測のための関数を定義しているところ。
deterministic というパラメータは、おそらく、Dropout などのノイズ層にスルーで渡されて、True のときにはランダムに捨てるのをやめる(学習時のみドロップアウトする)という制御のためだと思われる。
それ以外には特に疑問はないだろう。

#### 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))

ようやく準備が全て終わって推論である。が、batch_size ずつしか訓練に与えることができないので、ここでも自前でコードをちょいと書く必要がある。といっても、読めばわかるレベルなので大丈夫だろう。
せっかく引数でデータを渡すのだから、渡すデータの順序がランダムになるようにした。batch_size に対して余る分はそのイテレーションでは使われないわけだが、ランダムにしておくことで使われないデータの偏りをなくすことも期待している。
テストデータの評価の方は batch_size に分ける必要がないので、一発呼び出しで済んでいる(このときは……)。


以上、これが一番シンプルな Lasagne の使い方。
でも、Lasagne でもっとディープラーニングっぽい画像処理したいという場合にはもう二手間くらい必要になる。
サンプルで使っているデータセット digits は 8x8 の画像なので、これを入力とした畳み込み& max-pooling &ドロップアウトを織り交ぜたモデルのサンプルコードがこちら。

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

# dataset
def digits_dataset(input_width, input_height, test_N = 400):
    import sklearn.datasets
    data = sklearn.datasets.load_digits()
    N = data.data.shape[0]
    X = data.data.reshape(N, 1, input_width, input_height)
    y = numpy.array(data.target, dtype=numpy.int32)

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

n_classes = 10
input_width = input_height = 8
X, y, test_X, test_y = digits_dataset(input_width, input_height)
N = X.shape[0]
test_N = test_X.shape[0]
print(X.shape, test_X.shape)


#### model
batch_size=100

l_in = lasagne.layers.InputLayer(
    shape=(batch_size, 1, input_width, input_height),
)
l_conv1 = lasagne.layers.Conv2DLayer(
    l_in,
    num_filters=8,
    filter_size=(3, 3),
    nonlinearity=lasagne.nonlinearities.rectify,
    W=lasagne.init.GlorotUniform(),
    )
l_pool1 = lasagne.layers.MaxPool2DLayer(l_conv1, pool_size=(2, 2))

l_hidden1 = lasagne.layers.DenseLayer(
    l_pool1,
    num_units=256,
    nonlinearity=lasagne.nonlinearities.rectify,
)
l_hidden1_dropout = lasagne.layers.DropoutLayer(l_hidden1, p=0.2)
l_hidden2 = lasagne.layers.DenseLayer(
    l_hidden1_dropout,
    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.tensor4('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])
    result = []
    for j in xrange(test_N / batch_size):
        j1, j2 = batch_size*j, batch_size*(j+1)
        result.append(test(test_X[j1:j2], test_y[j1:j2]))
    loss, acc = numpy.mean(result, axis=0)
    print("%d: train_loss=%.4f, test_loss=%.4f, test_accuracy=%.4f" % (i+1, train_loss, loss, acc))

元のサンプルコードとよく似ているが、細かいところが結構違うので、そのあたりを中心に説明して Lasagne チュートリアルを終わろう。

  • 2次元データを入力に使うときは、4次元テンソルで渡す必要がある。 digits_dataset() で X を reshape している行を見てもらえば手っ取り早いが、その形も (データ件数, 1, 横次元, 縦次元) と、2次元目がなぜか 1 でないといけない(理由は調べてない。 Theano の制限?)
  • モデルの定義で Conv2DLayer や MaxPool2DLayer や DropoutLayer で畳み込みや max-pooling やドロップアウトを記述できるが、これはコードを見ればわかると思うので説明略。
  • loss function の定義で、入力データを表す変数 X_batch が、1次元のときは T.matrix だったが、2次元では T.tensor4 とする。
  • 1次元のときは updates 関数だけが batch_size のしばりがあったのだが、2次元では loss function そのものにも batch_size のしばりが及ぶようになる。つまり test も一発呼び出しができなくなるので、こちらでも分割してループして結果を平均、といった処理を行う必要がある。テストは訓練と違ってランダムサンプリングするわけにはいかないので、テストデータのサイズは batch_size の整数倍であることが望ましい。