ディープラーニング-畳み込みニューラルネットワークとPythonによる特徴抽出

畳み込みニューラルネットワーク(またはConvNet)は、生物学から着想を得た多層パーセプトロン(MLP)の変形です。畳み込みニューラルネットワークには種類の異なる様々な層があり、各層は通常のMLPとは異なる働きをします。ConvNetについて詳しく学びたい方には、CS231n 視覚認識のための畳み込みニューラルネットワークのコースをお勧めします。以下の図は、畳み込みニューラルネットワークのアーキテクチャを表しています。

A regular neural network.
訳:入力層→隠れ層1→隠れ層2→出力層

標準的なニューラルネットワーク(CS231nのWebサイトより)

A ConvNet network achitecture (from CS231n website).

畳み込みニューラルネットワークのアーキテクチャ(CS231nのWebサイトより)

この図で分かるように、ConvNetは3次元の量および3次元の量への変換によって機能します。CS231nのチュートリアル全てをここで繰り返すつもりはありませんが、もし興味があれば、この続きを読む前にチュートリアルを一読してください。

Lasagneとnolearn

私が気に入って使っているディープラーニング用のPythonのパッケージの1つがLasagnenolearnです。LasagneはTheanoをベースにしているので、GPUのスピードアップにより非常に大きな違いが出ます。さらに、ニュートラルネットワークの構築に宣言的アプローチを使えるのでとても便利です。nolearnライブラリは、ニューラルネットワークのパッケージ(Lasagneを含む)周辺のユーティリティを集めたものです。これは、ニューラルネットワークのアーキテクチャを構築する際、層の検査などを行う時にとても役に立ちます。

この投稿では、畳み込み層とプーリング層を用いたシンプルなConvNetアーキテクチャの構築方法をご紹介したいと思います。また、ConvNetを使ってどのように特徴抽出機構をトレーニングし、サポートベクターマシン(SVM)やロジスティック回帰といった異なるモデルに与える前の特徴をどのように抽出するかという方法もご紹介します。多くの場合、事前トレーニングされたConvNetモデルを使い、特徴を抽出するために、ImageNetのデータセットでトレーニングされたConvNetから最終的な出力層を取り除きます。これは普通、転移学習と呼ばれています。というのも、異なる問題用の特徴抽出機構として、他のConvNetの層を使うことができるからです。ConvNetの最初の層のフィルタはエッジ検出器として機能するので、他の問題用の一般的な特徴検出機構として使うことができます。

MNISTデータセットを読み込む

MNISTデータセットとは手書きの数字を分類する最も古典的なデータセットの1つです。ここではPython用にpickle化したバージョンを使いますが、まず、必要なパッケージをインポートしましょう。

import matplotlib
import matplotlib.pyplot as plt
import matplotlib.cm as cm

from urllib import urlretrieve
import cPickle as pickle
import os
import gzip

import numpy as np
import theano

import lasagne
from lasagne import layers
from lasagne.updates import nesterov_momentum

from nolearn.lasagne import NeuralNet
from nolearn.lasagne import visualize

from sklearn.metrics import classification_report
from sklearn.metrics import confusion_matrix

ご覧のとおり、描画のためのmatplotlib、MNISTデータセットをダウンロードするPythonのネイティブモジュール、numpy、theano、lasagne、nolearnと、モデル評価用のscikit-lean関数がインポートされます。

その後、MNISTの読み込み関数を定義します(これはLasagneチュートリアルで使ったものとほぼ同じ関数です)。

def load_dataset():
    url = 'http://deeplearning.net/data/mnist/mnist.pkl.gz'
    filename = 'mnist.pkl.gz'
    if not os.path.exists(filename):
        print("Downloading MNIST dataset...")
        urlretrieve(url, filename)

    with gzip.open(filename, 'rb') as f:
        data = pickle.load(f)

    X_train, y_train = data[0]
    X_val, y_val = data[1]
    X_test, y_test = data[2]

    X_train = X_train.reshape((-1, 1, 28, 28))
    X_val = X_val.reshape((-1, 1, 28, 28))
    X_test = X_test.reshape((-1, 1, 28, 28))

    y_train = y_train.astype(np.uint8)
    y_val = y_val.astype(np.uint8)
    y_test = y_test.astype(np.uint8)

    return X_train, y_train, X_val, y_val, X_test, y_test

ここではMNISTのpickle化されたデータセットをダウンロードし、それを3つの異なるデータセット(train、validationとtest)に分割しています。その後、画像コンテンツをreshapeし、後でLasagne 入力層に入力する準備をします。GPU/theanoにはデータ型に制限があるので、数値配列型をuint8に変換します。

MNISTデータセットを読み込み、inspectする準備ができました。

X_train, y_train, X_val, y_val, X_test, y_test = load_dataset()
plt.imshow(X_train[0][0], cmap=cm.binary)

上記のコードにより下記のイメージが出力されます(IPython Notebookを使っています)。

An example of a MNIST digit (5 in the case).

MNISTの数の例(この場合は5)

ConvNetのアーキテクチャとトレーニング

さあConvNetアーキテクチャを定義し、GPU/CPUを使ってトレーニングしましょう(私のGPUは安物ですが、とても役立っています)。

net1 = NeuralNet(
    layers=[('input', layers.InputLayer),
            ('conv2d1', layers.Conv2DLayer),
            ('maxpool1', layers.MaxPool2DLayer),
            ('conv2d2', layers.Conv2DLayer),
            ('maxpool2', layers.MaxPool2DLayer),
            ('dropout1', layers.DropoutLayer),
            ('dense', layers.DenseLayer),
            ('dropout2', layers.DropoutLayer),
            ('output', layers.DenseLayer),
            ],
    # input layer
    input_shape=(None, 1, 28, 28),
    # layer conv2d1
    conv2d1_num_filters=32,
    conv2d1_filter_size=(5, 5),
    conv2d1_nonlinearity=lasagne.nonlinearities.rectify,
    conv2d1_W=lasagne.init.GlorotUniform(),  
    # layer maxpool1
    maxpool1_pool_size=(2, 2),    
    # layer conv2d2
    conv2d2_num_filters=32,
    conv2d2_filter_size=(5, 5),
    conv2d2_nonlinearity=lasagne.nonlinearities.rectify,
    # layer maxpool2
    maxpool2_pool_size=(2, 2),
    # dropout1
    dropout1_p=0.5,    
    # dense
    dense_num_units=256,
    dense_nonlinearity=lasagne.nonlinearities.rectify,    
    # dropout2
    dropout2_p=0.5,    
    # output
    output_nonlinearity=lasagne.nonlinearities.softmax,
    output_num_units=10,
    # optimization method params
    update=nesterov_momentum,
    update_learning_rate=0.01,
    update_momentum=0.9,
    max_epochs=10,
    verbose=1,
    )

# Train the network
nn = net1.fit(X_train, y_train)

パラメータの中では、層の名称/型でタプルの辞書を定義し、そして、この層のパラメータを定義します。ここでのアーキテクチャは2つの畳み込み層とプーリング、全結合層(密層)、そして出力層を使っています。層の間にはドロップアウトもあります。ドロップアウト層はランダムに入力値を0に設定して過剰適合を防ぐための正則化項です(下記の画像を参照してください)。

Dropout layer effect (from CS231n website).
ドロップアウト層の効果(CS231nのWebサイトより)

トレーニングメソッドを呼び出した後、nolearnパッケージが学習プロセスのステータスを示します。質素なGPUの搭載された私のマシンでは、下記のような結果になりました。

# Neural Network with 160362 learnable parameters

## Layer information

  #  name      size
---  --------  --------
  0  input     1x28x28
  1  conv2d1   32x24x24
  2  maxpool1  32x12x12
  3  conv2d2   32x8x8
  4  maxpool2  32x4x4
  5  dropout1  32x4x4
  6  dense     256
  7  dropout2  256
  8  output    10

epoch   train loss    valid loss    train/val    valid acc  dur
------- ------------  ------------  -----------  ---------  ---
      1     0.85204   0.16707      5.09977      0.95174  33.71s
      2     0.27571   0.10732      2.56896      0.96825  33.34s
      3     0.20262   0.08567      2.36524      0.97488  33.51s
      4     0.16551   0.07695      2.15081      0.97705  33.50s
      5     0.14173   0.06803      2.08322      0.98061  34.38s
      6     0.12519   0.06067      2.06352      0.98239  34.02s
      7     0.11077   0.05532      2.00254      0.98427  33.78s
      8     0.10497   0.05771      1.81898      0.98248  34.17s
      9     0.09881   0.05159      1.91509      0.98407  33.80s
     10     0.09264   0.04958      1.86864      0.98526  33.40s

最終的な正確性は0.98526でした。10エポックのトレーニングにしてはかなりいいパフォーマンスです。

予測値と混同行列

これでデータセットの全テスト結果を予測するために、このモデルを使用することが可能となりました。

preds = net1.predict(X_test)

そこから、またニュートラルネットワーク分類のパフォーマンスをチェックするために、混同行列をプロットすることができます。

cm = confusion_matrix(y_test, preds)
plt.matshow(cm)
plt.title('Confusion matrix')
plt.colorbar()
plt.ylabel('True label')
plt.xlabel('Predicted label')
plt.show()

上記のコードは、次のような混同行列をプロットします。

Confusion Matrix
混同行列

ご覧のように、対角項に分類がより密集しており、分類器のパフォーマンスが上々であることを表しています。

フィルタを視覚化する。

次は最初の畳み込み層から32のフィルタを視覚化します。

visualize.plot_conv_weights(net1.layers_['conv2d1'])

上記のコードは、次のようなフィルタを描画します。

The first layer 5x5x32 filters.
最初の層、5x5x32フィルタ

ご覧のように、nolearnの plot_conv_weightsが指定した層に存在する全てのフィルタを生成します。

Theanoによるレイヤ関数と特徴抽出

今度はいよいよtheanoでコンパイルした関数を作成し、あなたが意図する層の所までアーキテクチャに入力値をフィードフォワードします。出力層を求めるための関数と出力層の前にある密層を求めるための関数を取得します。

dense_layer = layers.get_output(net1.layers_['dense'], deterministic=True)
output_layer = layers.get_output(net1.layers_['output'], deterministic=True)
input_var = net1.layers_['input'].input_var

f_output = theano.function([input_var], output_layer)
f_dense = theano.function([input_var], dense_layer)

ご覧のように、(出力層と密層を求めるための)f_outputf_denseという2つのtheano関数を得ました。注意してほしいのは、ここで層を得るために、“deterministic”という追加パラメータを使用しているということです。これはフィードフォワードパスに影響するドロップアウト層を避けるためです。

ここでサンプルのinstanceを入力フォーマットに変え、出力層を求めるためのtheano関数にフィードします。

instance = X_test[0][None, :, :]
%timeit -n 500 f_output(instance)

500 loops, best of 3: 858 µs per loop

ご覧のように、f_output関数は平均858マイクロ秒かかりました。instanceの出力層の活性化を描画することができます。

pred = f_output(instance)
N = pred.shape[1]
plt.bar(range(N), pred.ravel())

上記のコードは次のようにプロットされます。

Output layer activations.
出力層の活性化

ご覧のように、この手書き数字は、7と認識されました。「ネットワークのどの層を求めるにもtheano関数を作成できる」ということが、とても有効であることが分かります。というのも(以前やったように)、出力層の1つ前の密層の活性化を求めるために関数を作ることができ、その活性化を特徴として活用でき、分類器ではなく、特徴抽出機構としてニューラルネットワークを使用することができるからです。続いて、密層のための256ユニットの活性化をプロットしてみましょう。

pred = f_dense(instance)
N = pred.shape[1]
plt.bar(range(N), pred.ravel())

上記のコードは、以下のようなプロットを作成します。

Dense layer activations.
密層の活性化

この256の活性化の出力値を、ロジスティック回帰やSVMといった線形分類器で入力する特徴として使用できます。

チュートリアルを楽しんでいただけましたでしょうか?