ConvNetJS で MNIST を分類1 - 階層型ニューラルネット

Node.js で ConvNetJS を使って MNIST の手書き数字を分類してみます。

今回は階層型ニューラルネット、次回は畳み込みニューラルネットを試す予定です。

ソースは http://github.com/fits/try_samples/tree/master/blog/20160322/

準備

npm で convnetjs 等のモジュールをインストールします。(概ね「ConvNetJS で iris を分類 」と同じ構成です)

package.json
{
  "name": "convnetjs_mnist_sample",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "dependencies": {
    "bluebird": "^3.3.4",
    "convnetjs": "^0.3.0",
    "shuffle-array": "^0.1.2"
  }
}
インストール例
> npm install

共通処理

ConvNetJS には、ニューラルネットの学習モデルを JSON で入出力する機能(fromJSONtoJSON)がありますので、今回はこの機能を使って学習前と後のモデルを JSON ファイルで扱う事にします。

MNIST には学習用のデータセット 6万件と評価用のデータセット 1万件がそれぞれ用意されていますので、今回は 6万件の学習データセットを全て学習に使い、1万件の評価データセットで評価する事にしました。

今回と次回で共通に使う処理として以下のようなスクリプトを作成しました。

(1) MNIST データセットのロード

MNIST の学習・評価データセットをロードする処理です。(処理内容に関しては 前回 を参照)

load_mnist.js
'use strict';

const Promise = require('bluebird');
const convnetjs = require('convnetjs');
const fs = require('fs');

const readFile = Promise.promisify(fs.readFile);
const readToBuffer = file => readFile(file).then(r => new Buffer(r, 'binary'));

const loadImages = file =>
    readToBuffer(file)
        .then(buf => {
            const magicNum = buf.readInt32BE(0);

            const num = buf.readInt32BE(4);
            const rowNum = buf.readInt32BE(8);
            const colNum = buf.readInt32BE(12);

            const dataBuf = buf.slice(16);

            const res = Array(num);

            let offset = 0;

            for (let i = 0; i < num; i++) {
                const data = new convnetjs.Vol(colNum, rowNum, 1, 0);

                for (let y = 0; y < rowNum; y++) {
                    for (let x = 0; x < colNum; x++) {

                        const value = dataBuf.readUInt8(offset++);

                        data.set(x, y, 0, value);
                    }
                }

                res[i] = data;
            }

            return res;
        });

const loadLabels = file =>
    readToBuffer(file)
        .then(buf => {
            const magicNum = buf.readInt32BE(0);

            const num = buf.readInt32BE(4);

            const dataBuf = buf.slice(8);

            const res = Array(num);

            for (let i = 0; i < num; i++) {
                res[i] = dataBuf.readUInt8(i);
            }

            return res;
        });

module.exports.loadMnist = (imgFile, labelFile) => 
    Promise.all([
        loadImages(imgFile),
        loadLabels(labelFile)
    ]).spread( (r1, r2) => 
        r2.map((label, i) => new Object({ values: r1[i], label: label }))
    );

(2) 学習モデルの保存

ニューラルネットの学習モデルを JSON ファイルへ保存する処理です。

save_model.js
'use strict';

const Promise = require('bluebird');
const convnetjs = require('convnetjs');

const writeFile = Promise.promisify(require('fs').writeFile);

module.exports.saveModel = (layers, destFile) => {

    const net = new convnetjs.Net();
    // 内部的なレイヤーの構築
    net.makeLayers(layers);

    // JSON 化してファイルへ保存
    writeFile(destFile, JSON.stringify(net.toJSON()))
        .catch( e => console.error(e) );
};

(3) 学習

指定の学習モデル (JSON) を MNIST の学習データセットで学習する処理です。

処理の進行状況を確認できるように batchSize 毎に誤差の平均と正解率を出力するようにしました。 (ただし、以下の方法では batchSize 次第で Trainer によるパラメータの更新タイミングと合わなくなります)

また、MNIST データセットの配列を直接シャッフルする代わりに、0 ~ 59999 のインデックス値から成る配列を用意して、それをシャッフルするようにしています。

learn_mnist.js
'use strict';

const Promise = require('bluebird');
const fs = require('fs');
const shuffle = require('shuffle-array');
const convnetjs = require('convnetjs');
const readFile = Promise.promisify(fs.readFile);
const writeFile = Promise.promisify(fs.writeFile);

const mnist = require('./load_mnist');

const epoch = parseInt(process.argv[2]);
const batchSize = parseInt(process.argv[3]);
const learningRate = parseFloat(process.argv[4]);
const trainMethod = process.argv[5];

const modelJsonFile = process.argv[6];
const modelJsonDestFile = process.argv[7];

// 0 ~ n - 1 を要素とする配列作成
const range = n => {
    const res = Array(n);

    for (let i = 0; i < n; i++) {
        res[i] = i;
    }

    return res;
};
// 指定サイズ毎に誤差の平均と正解率を出力する処理
const createLogger = (logSize, logFunc) => {
    let list = [];
    let counter = 0;

    return (loss, accuracy) => {
        list.push({loss: loss, accuracy: accuracy});

        const size = list.length;

        if (size >= logSize) {
            const res = list.reduce(
                (acc, d) => {
                    acc.loss += d.loss;
                    acc.accuracy += d.accuracy;

                    return acc;
                },
                { loss: 0.0, accuracy: 0 }
            );
            // 出力処理の実行
            logFunc(
                res.loss / size,
                res.accuracy / size,
                counter++
            );

            list = [];
        }
    };
};


Promise.all([
    readFile(modelJsonFile),
    mnist.loadMnist('train-images.idx3-ubyte', 'train-labels.idx1-ubyte')
]).spread( (json, data) => {
    const net = new convnetjs.Net();
    // JSON から学習モデルを復元
    net.fromJSON(JSON.parse(json));

    const trainer = new convnetjs.Trainer(net, {
        method: trainMethod, 
        batch_size: batchSize, 
        learning_rate: learningRate
    });

    range(epoch).forEach(ep => {
        // ログ出力処理の作成
        const log = createLogger(batchSize, (loss, acc, counter) =>
            console.log( [ep, counter, loss, acc].join(',') )
        );

        // インデックス値の配列を作成しシャッフル
        shuffle(range(data.length)).forEach(i => {
            // 該当するデータを取得
            const d = data[i];
            // 学習
            const stats = trainer.train(d.values, d.label);

            log(
                stats.loss,
                (net.getPrediction() == d.label)? 1: 0
            );
        });
    });

    return net;

}).then( net => 
    // 学習モデルの保存
    writeFile(modelJsonDestFile, JSON.stringify(net.toJSON()))
).catch( e => 
    console.error(e)
);

(4) 評価(テスト)

指定の学習モデル (JSON) で MNIST の評価データセットを処理し、正解率を出力する処理です。

validate_mnist.js
'use strict';

const Promise = require('bluebird');
const convnetjs = require('convnetjs');
const readFile = Promise.promisify(require('fs').readFile);

const mnist = require('./load_mnist');

const modelJsonFile = process.argv[2];

Promise.all([
    readFile(modelJsonFile),
    mnist.loadMnist('t10k-images.idx3-ubyte', 't10k-labels.idx1-ubyte')
]).spread( (json, data) => {

    const net = new convnetjs.Net();
    // JSON から学習モデルを復元
    net.fromJSON(JSON.parse(json));

    const accuCount = data.reduce((acc, d) => {
        net.forward(d.values);
        // 正解数のカウント
        return acc + (d.label == net.getPrediction()? 1: 0);
    }, 0);

    console.log(`data size: ${data.length}`);
    // 正解率の出力
    console.log(`accuracy: ${accuCount / data.length}`);
});

(a) 階層型ニューラルネット

MNIST の画像サイズは 28x28 のため、入力層 (type = input) の out_sxout_sy へそれぞれ 28 を設定し、画素値は 1バイトのため out_depth へ 1 を設定します。

出力層 (type = output)は 0 ~ 9 の分類 (10種類) となるため、typesoftmax にして num_classes へ 10 を設定します。

今回は、隠れ層を 1層にして活性化関数とニューロン数をコマンドライン引数で指定できるようにしました。

create_layer_hnn.js
'use strict';

const act = process.argv[2];
const numNeurons = parseInt(process.argv[3]);
const jsonDestFile = process.argv[4];

require('./save_model').saveModel(
    [
        { type: 'input', out_sx: 28, out_sy: 28, out_depth: 1 },
        { type: 'fc', activation: act, num_neurons: numNeurons },
        { type: 'softmax', num_classes: 10 }
    ], 
    jsonDestFile
);

例えば、上記の活性化関数へ relu を指定した場合の makeLayers の結果は下記のようになります。

学習モデルの内部的なレイヤー構成例
input -> fc -> relu -> fc -> softmax

type へ softmax を指定した場合、fc 層が差し込まれるようになっています。

学習と評価

今回は以下の 4種類で学習・評価を試してみました。

  1. 活性化関数 = relu, ニューロン数 = 50
  2. 活性化関数 = relu, ニューロン数 = 300
  3. 活性化関数 = sigmoid, ニューロン数 = 50
  4. 活性化関数 = sigmoid, ニューロン数 = 300

学習回数などのパラメータはとりあえず下記で実行します。

  • 学習回数 = 15
  • バッチサイズ = 100
  • 学習係数 = 0.001
  • 学習係数の決定方法 = adadelta

バッチサイズを 100 とする事で、学習データ 100件毎にパラメータ(重み)の更新が実施されます。

なお、処理時間はニューロン数や学習回数・バッチサイズなどに影響されます。(今回、ニューロン数 50 では 10分程度、300 では 50分程度でした)

1. 活性化関数 = relu, ニューロン数 = 50 (学習回数 = 15, バッチサイズ = 100, 学習係数 = 0.001, adadelta)

> node create_layer_hnn.js relu 50 models/relu_50.json

> node learn_mnist.js 15 100 0.001 adadelta models/relu_50.json results/a-1_relu_50.json > logs/a-1_relu_50.log

> node validate_mnist.js results/a-1_relu_50.json

data size: 10000
accuracy: 0.9455
学習時の誤差と正解率

f:id:fits:20160324201523p:plain

2. 活性化関数 = relu, ニューロン数 = 300 (学習回数 = 15, バッチサイズ = 100, 学習係数 = 0.001, adadelta)

> node create_layer_hnn.js relu 300 models/relu_300.json

> node learn_mnist.js 15 100 0.001 adadelta models/relu_300.json results/a-2_relu_300.json > logs/a-2_relu_300.log

> node validate_mnist.js results/a-2_relu_300.json

data size: 10000
accuracy: 0.965
学習時の誤差と正解率

f:id:fits:20160324201542p:plain

3. 活性化関数 = sigmoid, ニューロン数 = 50 (学習回数 = 15, バッチサイズ = 100, 学習係数 = 0.001, adadelta)

> node create_layer_hnn.js sigmoid 50 models/sigmoid_50.json

> node learn_mnist.js 15 100 0.001 adadelta models/sigmoid_50.json results/a-3_sigmoid_50.json > logs/a-3_sigmoid_50.log

> node validate_mnist.js results/a-3_sigmoid_50.json

data size: 10000
accuracy: 0.9368
学習時の誤差と正解率

f:id:fits:20160324201601p:plain

4. 活性化関数 = sigmoid, ニューロン数 = 300 (学習回数 = 15, バッチサイズ = 100, 学習係数 = 0.001, adadelta)

> node create_layer_hnn.js sigmoid 300 models/sigmoid_300.json

> node learn_mnist.js 15 100 0.001 adadelta models/sigmoid_300.json results/a-4_sigmoid_300.json > logs/a-4_sigmoid_300.log

> node validate_mnist.js results/a-4_sigmoid_300.json

data size: 10000
accuracy: 0.9631
学習時の誤差と正解率

f:id:fits:20160324201617p:plain

結果のまとめ

番号 活性化関数(隠れ層) ニューロン数(隠れ層) 正解率
1 relu 50 0.9455
2 relu 300 0.965
3 sigmoid 50 0.9368
4 sigmoid 300 0.9631