Pylearn2を使って手書き文字認識を行う

Pylearn2を使って手書き文字認識を行います。
Pylearn2のインストール方法については割愛します。
画像の表示を行うために環境変数 PYLEARN2_VIEWER_COMMAND の設定をしておいてください。

今回使ったソースコードはGithubにアップロードしてあります。
https://github.com/dsanno/pylearn2_mnist

# データをダウンロードする

データは[MNIST database](http://yann.lecun.com/exdb/mnist/)を使います。
以下のデータセットとなっています。

* 1個のデータは28 x 28 ピクセルの白黒画像で、0から9までの数字のうち1つが描かれている
* 学習用データ60000個
* テストデータ10000個

pylearn2には、いくつかのデータセットをダウンロードしたり加工したりするスクリプトが含まれています。
MNIST databaseをダウンロードするにはpylearn2に含まれる以下のファイルを実行します。
ダウンロードしたデータは $PYLEARN2_DATA_PATH/mnist に置かれます。
pylearn2/scripts/datasets/download_mnist.py

# データを確認する

どのようなデータが入っているのか確認してみましょう。

まずデータセットを定義したyamlファイルを作ります。

```dataset.yaml
!obj:pylearn2.datasets.mnist.MNIST {
    which_set: 'train'
}
```

次にpylearn2の show_examples.py を利用してデータのサンプルを表示します。
以下のファイルを作成して実行します。

```show_samples.py
import pylearn2.scripts.show_examples as show_examples

show_examples.show_examples('dataset.yaml', 20, 20)
```

もしくは以下のコマンドでも表示することができます。
pylearn2/scripts/show_examples.py dataset.yaml

以下のような画像が表示されます。

![dataset_examples.png](https://qiita-image-store.s3.amazonaws.com/0/58026/d8b024d8-e010-246f-7fc4-b2bdbaac8557.png)

# モデルを定義する

学習するためのモデルを定義します。
チュートリアルの stacked_autoencoders ディレクトリにMNISTデータを学習するためのモデルがあるので、これを修正して使います。
pylearn2/scripts/tutorals/stacked_autoencoders/

今回は以下のモデルを定義します。
* 入力は28 x 28 = 784ユニット
* 出力は10ユニット
* 隠れ層3層
* 隠れ層はそれぞれ100ユニット

1層目:
28 x 28 = 784個の入力値をとります。

```dae_l1.yaml
!obj:pylearn2.train.Train {
    dataset: &train !obj:pylearn2.datasets.mnist.MNIST {
        which_set: 'train',
        start: 0,
        stop: %(train_stop)i
    },
    model: !obj:pylearn2.models.autoencoder.DenoisingAutoencoder {
        nvis : 784,
        nhid : %(nhid)i,
        irange : 0.05,
        corruptor: !obj:pylearn2.corruption.BinomialCorruptor {
            corruption_level: .2,
        },
        act_enc: "tanh",
        act_dec: null,    # Linear activation on the decoder side.
    },
    algorithm: !obj:pylearn2.training_algorithms.sgd.SGD {
        learning_rate : 1e-3,
        batch_size : %(batch_size)i,
        monitoring_batches : %(monitoring_batches)i,
        monitoring_dataset : *train,
        cost : !obj:pylearn2.costs.autoencoder.MeanSquaredReconstructionError {},
        termination_criterion : !obj:pylearn2.termination_criteria.EpochCounter {
            max_epochs: %(max_epochs)i,
        },
    },
    save_path: "%(save_path)s/dae_l1.pkl",
    save_freq: 1
}
```

2層目:
datasetには、学習データを1層目によって変換したものを使います。

```dae_l2.yaml
!obj:pylearn2.train.Train {
    dataset: &train !obj:pylearn2.datasets.transformer_dataset.TransformerDataset {
        raw: !obj:pylearn2.datasets.mnist.MNIST {
            which_set: 'train',
            start: 0,
            stop: %(train_stop)i
        },
        transformer: !pkl: "%(save_path)s/dae_l1.pkl"
    },
    model: !obj:pylearn2.models.autoencoder.DenoisingAutoencoder {
        nvis : %(nvis)i,
        nhid : %(nhid)i,
        irange : 0.05,
        corruptor: !obj:pylearn2.corruption.BinomialCorruptor {
            corruption_level: .3,
        },
        act_enc: "tanh",
        act_dec: null,    # Linear activation on the decoder side.
    },
    algorithm: !obj:pylearn2.training_algorithms.sgd.SGD {
        learning_rate : 1e-3,
        batch_size : %(batch_size)i,
        monitoring_batches : %(monitoring_batches)i,
        monitoring_dataset : *train,
        cost : !obj:pylearn2.costs.autoencoder.MeanSquaredReconstructionError {},
        termination_criterion : !obj:pylearn2.termination_criteria.EpochCounter {
            max_epochs: %(max_epochs)i,
        },
    },
    save_path: "%(save_path)s/dae_l2.pkl",
    save_freq: 1
}
```

3層目:
datasetには、学習データを1層目、2層目によって変換したものを使います。

```
!obj:pylearn2.train.Train {
    dataset: &train !obj:pylearn2.datasets.transformer_dataset.TransformerDataset {
        raw: !obj:pylearn2.datasets.mnist.MNIST {
            which_set: 'train',
            start: 0,
            stop: %(train_stop)i
        },
        transformer: !obj:pylearn2.blocks.StackedBlocks {
            layers: [!pkl: "dae_l1.pkl", !pkl: "dae_l2.pkl"]
        }
    },
    model: !obj:pylearn2.models.autoencoder.DenoisingAutoencoder {
        nvis : %(nvis)i,
        nhid : %(nhid)i,
        irange : 0.05,
        corruptor: !obj:pylearn2.corruption.BinomialCorruptor {
            corruption_level: .3,
        },
        act_enc: "tanh",
        act_dec: null,    # Linear activation on the decoder side.
    },
    algorithm: !obj:pylearn2.training_algorithms.sgd.SGD {
        learning_rate : 1e-3,
        batch_size : %(batch_size)i,
        monitoring_batches : %(monitoring_batches)i,
        monitoring_dataset : *train,
        cost : !obj:pylearn2.costs.autoencoder.MeanSquaredReconstructionError {},
        termination_criterion : !obj:pylearn2.termination_criteria.EpochCounter {
            max_epochs: %(max_epochs)i,
        },
    },
    save_path: "%(save_path)s/dae_l3.pkl",
    save_freq: 1
}
```

最後に、ファインチューニング用に各層を連結したモデルを定義します。
出力層は10ユニットあり、それぞれの値は0から9までの文字のうちどれであるかの確率とみなすことができます。

```dae_mlp.yaml
!obj:pylearn2.train.Train {
    dataset: &train !obj:pylearn2.datasets.mnist.MNIST {
        which_set: 'train',
        start: 0,
        stop: %(train_stop)i
    },
    model: !obj:pylearn2.models.mlp.MLP {
        batch_size: %(batch_size)i,
        layers: [
                 !obj:pylearn2.models.mlp.PretrainedLayer {
                     layer_name: 'h1',
                     layer_content: !pkl: "%(save_path)s/dae_l1.pkl"
                 },
                 !obj:pylearn2.models.mlp.PretrainedLayer {
                     layer_name: 'h2',
                     layer_content: !pkl: "%(save_path)s/dae_l2.pkl"
                 },
                 !obj:pylearn2.models.mlp.PretrainedLayer {
                     layer_name: 'h3',
                     layer_content: !pkl: "%(save_path)s/dae_l3.pkl"
                 },
                 !obj:pylearn2.models.mlp.Softmax {
                     max_col_norm: 1.9365,
                     layer_name: 'y',
                     n_classes: 10,
                     irange: .005
                 }
                ],
        nvis: 784
    },
    algorithm: !obj:pylearn2.training_algorithms.sgd.SGD {
        learning_rate: .05,
        learning_rule: !obj:pylearn2.training_algorithms.learning_rule.Momentum {
            init_momentum: .5,
        },
        monitoring_dataset:
            {
                'valid' : !obj:pylearn2.datasets.mnist.MNIST {
                              which_set: 'train',
                              start: 0,
                              stop: %(valid_stop)i
                          },
            },
        cost: !obj:pylearn2.costs.mlp.Default {},
        termination_criterion: !obj:pylearn2.termination_criteria.And {
            criteria: [
                !obj:pylearn2.termination_criteria.MonitorBased {
                    channel_name: "valid_y_misclass",
                    prop_decrease: 0.,
                    N: 100
                },
                !obj:pylearn2.termination_criteria.EpochCounter {
                    max_epochs: %(max_epochs)i
                }
            ]
        },
        update_callbacks: !obj:pylearn2.training_algorithms.sgd.ExponentialDecay {
            decay_factor: 1.00004,
            min_lr: .000001
        }
    },
    extensions: [
        !obj:pylearn2.training_algorithms.learning_rule.MomentumAdjustor {
            start: 1,
            saturate: 250,
            final_momentum: .7
        }
    ],
    save_path: "%(save_path)s/dae_mlp.pkl",
    save_freq: 1
}
```

# 学習を行う

stacked_autoencoders チュートリアルに含まれている学習スクリプトを修正して使います。
以下のような修正を加えています。

* チュートリアルでは隠れ層2層だったのを3層に変更
* 学習時間を短くするためか、学習データ数(train_stop)や学習の最大回数(max_epochs)が小さいのを修正。
学習データは60000個全部使い、学習回数は最大100回としました。

実行するとdae_l1.pkl, dae_l2.pkl, dae_l3.pkl, dae_mlp.pklという各モデルに対応したファイルを出力します。
実行時間ですが、私の環境(Core i7-3770)では20分ほどかかりました。

```test_dae.py
import os

from pylearn2.testing import skip
from pylearn2.testing import no_debug_mode
from pylearn2.config import yaml_parse


@no_debug_mode
def train_yaml(yaml_file):

    train = yaml_parse.load(yaml_file)
    train.main_loop()


def train_layer1(yaml_file_path, save_path):

    yaml = open("{0}/dae_l1.yaml".format(yaml_file_path), 'r').read()
    hyper_params = {'train_stop': 60000,
                    'batch_size': 100,
                    'monitoring_batches': 1,
                    'nhid': 100,
                    'max_epochs': 100,
                    'save_path': save_path}
    yaml = yaml % (hyper_params)
    train_yaml(yaml)


def train_layer2(yaml_file_path, save_path):

    yaml = open("{0}/dae_l2.yaml".format(yaml_file_path), 'r').read()
    hyper_params = {'train_stop': 60000,
                    'batch_size': 100,
                    'monitoring_batches': 1,
                    'nvis': 100,
                    'nhid': 100,
                    'max_epochs': 100,
                    'save_path': save_path}
    yaml = yaml % (hyper_params)
    train_yaml(yaml)


def train_layer3(yaml_file_path, save_path):

    yaml = open("{0}/dae_l3.yaml".format(yaml_file_path), 'r').read()
    hyper_params = {'train_stop': 60000,
                    'batch_size': 100,
                    'monitoring_batches': 1,
                    'nvis': 100,
                    'nhid': 100,
                    'max_epochs': 100,
                    'save_path': save_path}
    yaml = yaml % (hyper_params)
    train_yaml(yaml)


def train_mlp(yaml_file_path, save_path):

    yaml = open("{0}/dae_mlp.yaml".format(yaml_file_path), 'r').read()
    hyper_params = {'train_stop': 60000,
                    'valid_stop': 60000,
                    'batch_size': 100,
                    'max_epochs': 100,
                    'save_path': save_path}
    yaml = yaml % (hyper_params)
    train_yaml(yaml)


def test_sda():

    skip.skip_if_no_data()

    yaml_file_path = '.';
    save_path = '.'

    train_layer1(yaml_file_path, save_path)
    train_layer2(yaml_file_path, save_path)
    train_layer3(yaml_file_path, save_path)
    train_mlp(yaml_file_path, save_path)

if __name__ == '__main__':
    test_sda()
```

# テストデータを使って文字認識を行う

テストデータを使って文字認識を行い、認識率を取得します。
テストデータを pylearn2.datasets.mnist.MNIST(which_set='test') で取得し、モデルのfpropを使って出力層の値を求めています。
一番値の大きい出力ユニットに対応する文字を予測値としています。
私の環境では 10000 個中 9814 個正解でした。

```test_result.py
import numpy as np
import pickle
import theano
import pylearn2.datasets.mnist as mnist


def simulate(inputs, model):
    return model.fprop(theano.shared(inputs)).eval()

def countCorrectResults(outputs, labels):
    correct = 0;
    for output, label in zip(outputs, labels):
        if np.argmax(output) == label:
            correct += 1
    return correct
 
def score(dataset, model):
    outputs = simulate(dataset.X, model)
    correct = countCorrectResults(outputs, dataset.y)

    return {
        'correct': correct,
        'total': len(dataset.X)
    }

model = pickle.load(open('dae_mlp.pkl'))
test_data = mnist.MNIST(which_set='test')
print '%(correct)d / %(total)d' % score(test_data, model)
```


# 参考文献

以下のサイトを参考にしました。
http://tanopy.blog79.fc2.com/blog-entry-118.html
http://www.slideshare.net/yurieoka37/ss-28152060