畳み込みニューラルネットワーク(またはConvNet)は、生物学から着想を得た多層パーセプトロン(MLP)の変形です。畳み込みニューラルネットワークには種類の異なる様々な層があり、各層は通常のMLPとは異なる働きをします。ConvNetについて詳しく学びたい方には、CS231n 視覚認識のための畳み込みニューラルネットワークのコースをお勧めします。以下の図は、畳み込みニューラルネットワークのアーキテクチャを表しています。
訳:入力層→隠れ層1→隠れ層2→出力層
標準的なニューラルネットワーク(CS231nのWebサイトより)
畳み込みニューラルネットワークのアーキテクチャ(CS231nのWebサイトより)
この図で分かるように、ConvNetは3次元の量および3次元の量への変換によって機能します。CS231nのチュートリアル全てをここで繰り返すつもりはありませんが、もし興味があれば、この続きを読む前にチュートリアルを一読してください。
Lasagneとnolearn
私が気に入って使っているディープラーニング用のPythonのパッケージの1つがLasagneとnolearnです。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を使っています)。
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に設定して過剰適合を防ぐための正則化項です(下記の画像を参照してください)。
ドロップアウト層の効果(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()
上記のコードは、次のような混同行列をプロットします。
混同行列
ご覧のように、対角項に分類がより密集しており、分類器のパフォーマンスが上々であることを表しています。
フィルタを視覚化する。
次は最初の畳み込み層から32のフィルタを視覚化します。
visualize.plot_conv_weights(net1.layers_['conv2d1'])
上記のコードは、次のようなフィルタを描画します。
最初の層、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_outputとf_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())
上記のコードは次のようにプロットされます。
出力層の活性化
ご覧のように、この手書き数字は、7と認識されました。「ネットワークのどの層を求めるにもtheano関数を作成できる」ということが、とても有効であることが分かります。というのも(以前やったように)、出力層の1つ前の密層の活性化を求めるために関数を作ることができ、その活性化を特徴として活用でき、分類器ではなく、特徴抽出機構としてニューラルネットワークを使用することができるからです。続いて、密層のための256ユニットの活性化をプロットしてみましょう。
pred = f_dense(instance) N = pred.shape[1] plt.bar(range(N), pred.ravel())
上記のコードは、以下のようなプロットを作成します。
密層の活性化
この256の活性化の出力値を、ロジスティック回帰やSVMといった線形分類器で入力する特徴として使用できます。
チュートリアルを楽しんでいただけましたでしょうか?