読者です 読者をやめる 読者になる 読者になる

Chainerを使って特徴抽出してみた

こんにちは、@mktozkです。

ディープラーニングの新しいフレームワークであるChainerが先日リリースされました。 少し遊んでみると、うたい文句通りモデル定義がとても簡単にできると感じました。 ネットワークの学習にはかなり重点が置かれているようです。

が、他がちょっと弱いような...

ということで、Chainerのサンプルコードをちょこっと変更して、特徴抽出が簡単にできるようにしてみました。 元にしたコードはMNISTの学習サンプルです。


モデル定義

構造自体はサンプルと同じです。これをsmall_model.pyという名前で保存します。

from collections import OrderedDict
from chainer import Variable, FunctionSet
import chainer.functions as F

class SmallModel(FunctionSet):
    def __init__(self, n_units=1000):
        super(SmallModel, self).__init__(
            fc1 = F.Linear(784, n_units),
            fc2 = F.Linear(n_units, n_units),
            fc3 = F.Linear(n_units, 10),
        )

    def _get_all_functions(self, train):
        functions = [
            ("fc1",   lambda x: self.fc1(x)),
            ("relu1", lambda x: F.relu(x)),
            ("drop1", lambda x: F.dropout(x, train=train)),
            ("fc2",   lambda x: self.fc2(x)),
            ("relu2", lambda x: F.relu(x)),
            ("drop2", lambda x: F.dropout(x, train=train)),
            ("fc3",   lambda x: self.fc3(x))
        ]
        return OrderedDict(functions)

    def forward(self, inputs, start=None, end=None, train=True):
        functions = self._get_all_functions(train=train)
        layer_names = functions.keys()

        if start is not None:
            start_ind = layer_names.index(start)
        else:
            start_ind = 0

        if end is not None:
            end_ind = layer_names.index(end) + 1
        else:
            end_ind = len(layer_names)

        inputs.volatile = not train
        seq = [inputs] + functions.values()[start_ind:end_ind]
        return reduce(lambda arg1, arg2: arg2(arg1), seq)

    def evaluate_model(self, x_data, y_data, train=True):
        x = Variable(x_data, volatile=not train)
        t = Variable(y_data, volatile=not train)
        y  = self.forward(x, train=train)
        return F.softmax_cross_entropy(y, t), F.accuracy(y, t)

大きな変更点は_get_all_functionsの追加です。 元々のサンプルコードではネットワーク構造はコードからしか読み取れませんでした。 今回は新たな関数を用意することでネットワークを保持しています。 *1

また、forwardはCaffeっぽい感じにしました。

特徴抽出

次に、train_mnist.pyを修正します。

#!/usr/bin/env python
"""Chainer example: train a multi-layer perceptron on MNIST

This is a minimal example to write a feed-forward net. It requires scikit-learn
to load MNIST dataset.

"""
import argparse
import numpy as np
from sklearn.datasets import fetch_mldata
from chainer import cuda, Variable, FunctionSet, optimizers
import chainer.functions  as F
import small_model

parser = argparse.ArgumentParser(description='Chainer example: MNIST')
parser.add_argument('--gpu', '-g', default=-1, type=int,
                    help='GPU ID (negative value indicates CPU)')
args = parser.parse_args()

batchsize = 100
n_epoch   = 20
n_units   = 1000

# Prepare dataset
print 'fetch MNIST dataset'
mnist = fetch_mldata('MNIST original')
mnist.data   = mnist.data.astype(np.float32)
mnist.data  /= 255
mnist.target = mnist.target.astype(np.int32)

N = 60000
x_train, x_test = np.split(mnist.data,   [N])
y_train, y_test = np.split(mnist.target, [N])
N_test = y_test.size

# Prepare multi-layer perceptron model
model = small_model.SmallModel(n_units)
if args.gpu >= 0:
    cuda.init(args.gpu)
    model.to_gpu()

# Setup optimizer
optimizer = optimizers.Adam()
optimizer.setup(model.collect_parameters())

# Learning loop
for epoch in xrange(1, n_epoch+1):
    print 'epoch', epoch

    # training
    perm = np.random.permutation(N)
    sum_accuracy = 0
    sum_loss = 0
    for i in xrange(0, N, batchsize):
        x_batch = x_train[perm[i:i+batchsize]]
        y_batch = y_train[perm[i:i+batchsize]]
        if args.gpu >= 0:
            x_batch = cuda.to_gpu(x_batch)
            y_batch = cuda.to_gpu(y_batch)

        optimizer.zero_grads()
        loss, acc = model.evaluate_model(x_batch, y_batch)
        loss.backward()
        optimizer.update()

        sum_loss     += float(cuda.to_cpu(loss.data)) * batchsize
        sum_accuracy += float(cuda.to_cpu(acc.data)) * batchsize

    print 'train mean loss={}, accuracy={}'.format(
        sum_loss / N, sum_accuracy / N)

    # evaluation
    sum_accuracy = 0
    sum_loss     = 0
    for i in xrange(0, N_test, batchsize):
        x_batch = x_test[i:i+batchsize]
        y_batch = y_test[i:i+batchsize]
        if args.gpu >= 0:
            x_batch = cuda.to_gpu(x_batch)
            y_batch = cuda.to_gpu(y_batch)

        loss, acc = model.evaluate_model(x_batch, y_batch, train=False)

        sum_loss     += float(cuda.to_cpu(loss.data)) * batchsize
        sum_accuracy += float(cuda.to_cpu(acc.data)) * batchsize

    print 'test  mean loss={}, accuracy={}'.format(
        sum_loss / N_test, sum_accuracy / N_test)

# ==================================================
# Extract features
n_dims = model.fc2.b.size
features = np.zeros((N_test, n_dims))
for i in xrange(0, N_test, batchsize):
    x_batch = x_test[i:i+batchsize]
    if args.gpu >= 0:
        x_batch = cuda.to_gpu(x_batch)

    x = Variable(x_batch, volatile=True)
    forward = model.forward(x, end="drop2", train=False)
    features[i:i+batchsize] = cuda.to_cpu(forward.data)

後半に特徴抽出のコードを追加した以外はほとんど元コードのままです。 ここではMNISTのテストデータについて、fc3層の直前のdrop2層の特徴を取り出しています。

サンプル通りにモデルを定義すると、特徴抽出時にもう一度ネットワークの構造を書く必要があるので、かなり面倒です。 あらかじめ特徴抽出がやりやすいようにモデルを定義したことで、学習済みネットワークから簡単に特徴量を取り出すことができました。


まとめ

Deep Learningの新しいフレームワークであるChainerで特徴抽出をしてみました。 モデル定義の仕方を少し変更するだけで簡単にあらゆる層から特徴を取り出せるようになりました。 行数は増えてしまいましたが、ちょっと便利になったのかなと思います。

おわりに

Chainerはまだ生まれたばかりです。これからもどんどん成長していくことを期待しています。

深層学習 (機械学習プロフェッショナルシリーズ)

深層学習 (機械学習プロフェッショナルシリーズ)

*1:self.fc1などはラムダ式で書く必要はないのですが、他と統一するためにこうしました