ConvNetJS で iris を分類

ニューラルネット用の JavaScript ライブラリ ConvNetJS を使って iris の分類を行ってみました。

ConvNetJS は Web ブラウザ上でも実行できますが、今回は Node.js で実行します。

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

準備

iris データセットの取得

iris データセット (iris.data) は https://archive.ics.uci.edu/ml/datasets/iris からダウンロードできます。

iris.data の 151 行目(改行のみの行)は邪魔なので削除しておきます。

ConvNetJS インストール

今回は npm で ConvNetJS をインストールしました。

ConvNetJS インストール例
> npm install convnetjs --save

npm でインストールできるバージョンは今のところ 0.3.0 ですが、最新ソース と差異があるのでご注意ください。 (input で out_sx 等にデフォルト値が適用されない、adam に対応していない等)

また、最新ソースには bower.json が用意されていることから将来的に bower でインストールするようになるのかもしれません。

その他

今回は以下のライブラリも使いましたのでこれらもインストールしておきます。

  • basic-csv (CSV ファイルのパース)
  • shuffle-array (配列のシャッフル)
  • bluebird (Promise 対応)
> npm install basic-csv shuffle-array bluebird --save

(1) 単純な構成 (入力層 - 出力層)

まず、入力層と出力層だけの単純なニューラルネットの構成を試します。

層の構成

iris データセットの場合は、4つの変数を使って 3つの品種に分類する事になりますので、以下のように設定します。

  • 入力層の out_depth へ変数の数 4 を設定 ※
  • 出力層の type を softmax とし、num_classes で分類の数 3 を設定
 ※ out_sx と out_sy は画像データ等で使用するため
    今回のようなケースでは 1 にします。

    最新のソースでは、
    out_sx と out_sy を省略できるようですが(デフォルト値 1 を適用)
    0.3.0 では省略できないようですのでご注意下さい

convnetjs.NetmakeLayers を使ってニューラルネットを初期構築します。 (重みなどのパラメータをランダム値で初期化)

iris を分類するためのニューラルネット構築例(入力層 + 出力層)
// レイヤー構成の定義
var layer_defs = [
    // 入力層
    { type: 'input', out_sx: 1, out_sy: 1, out_depth: 4 },
    // 出力層
    { type: 'softmax', num_classes: 3 }
];

var model = new convnetjs.Net();

model.makeLayers(layer_defs);

出力層の活性化関数・誤差関数

今回のような分類を行う場合、出力層の活性化関数・誤差関数は一般的に以下を使うようです。

活性化関数 誤差関数
ソフトマックス 交差エントロピー

ConvNetJS では出力層の type を softmax とすればこれらを実施してくれます。(SoftmaxLayer の forward・backward に実装されています)

学習

学習は convnetjs.Trainer を使って実施します。

train メソッドへデータ 1件の入力値 (convnetjs.Vol) と分類結果 (数値で指定可 ※) を与えると算出した誤差を基にパラメータを調整してくれます。

 ※ 3つに分類する場合は 0 ~ 2

Trainer に batch_size (デフォルト 1) を設定すると、その回数毎にしかパラメータの調整を行わなくなり、ミニバッチを実現できます。

convnetjs.Vol の引数へ配列を与えると sx=1, sy=1, depth=配列のサイズ, w=配列の内容 で初期化した Vol オブジェクトを取得できます。

テスト(評価)

テストデータの評価は、forward へテストデータの入力値を与えた後、getPrediction で取得した結果(分類の予測結果)と実際の値を比べる事で実施できます。

サンプル作成

学習係数などのハイパーパラメータにはデフォルト値を使い、iris のデータセットを学習用(105件)とテスト用(45件)に分割して、テスト用データの正解率を出力する処理を実装してみました。

学習を何度も繰り返す事でパラメータを最適化していきますので、以下では 20回繰り返すようにしています。

sample1.js
var Promise = require('bluebird');
var basicCsv = require('basic-csv');
var shuffle = require('shuffle-array');
var convnetjs = require('convnetjs');

var readCSV = Promise.promisify(basicCsv.readCSV);

var epoch = 20; // 学習の繰り返し回数
var trainRate = 0.7; // 学習データとテストデータの分割率

var categoryMap = {
    'Iris-setosa': 0,
    'Iris-versicolor': 1,
    'Iris-virginica': 2
};

// 階層型ニューラルネットの構成
var layer_defs = [
    { type: 'input', out_sx: 1, out_sy: 1, out_depth: 4 },
    { type: 'softmax', num_classes: 3 }
];

var model = new convnetjs.Net();
model.makeLayers(layer_defs);

var trainer = new convnetjs.Trainer(model);

readCSV('iris.data')
    .then( ds => 
        // 入力データの作成
        ds.map(d => 
            new Object({
                // 4つの変数を convnetjs.Vol 化
                features: new convnetjs.Vol([ d[0], d[1], d[2], d[3] ]),
                // 分類の数値化 (0 ~ 2)
                label: categoryMap[d[4]]
            })
        )
    )
    .then( ds => {
        shuffle(ds);

        var trainSize = Math.floor(ds.length * trainRate);
        var testData = ds.splice(trainSize);

        // 学習データとテストデータへ分割
        return {train: ds, test: testData};
    })
    .then( data => {
        for (var i = 0; i < epoch; i++) {
            // 学習
            data.train.forEach( d => trainer.train(d.features, d.label) );

            // テスト(評価)
            var testAccSum = data.test.reduce( (acc, d) => {
                model.forward(d.features);

                return acc + (model.getPrediction() == d.label? 1: 0);
            }, 0);

            // テストデータの正解率を出力
            console.log(testAccSum / data.test.length);
        }
    });

実行結果は以下の通りです。

実行するたびに初期パラメータや学習・テストデータの構成が変わりますので、結果も自ずと変化します。

実行結果1
> node sample1.js

0.6222222222222222
0.7777777777777778
0.6444444444444445
・・・
0.9777777777777777
0.9777777777777777
0.9777777777777777
実行結果2
> node sample1.js

0.8888888888888888
0.8888888888888888
0.9555555555555556
・・・
0.9555555555555556
0.9555555555555556
0.9333333333333333

(2) 誤差と正解率

次は、学習・テストそれぞれの誤差(の平均)と正解率を出力するようにしてみます。

更に、学習係数 learning_rate と学習係数の調節手法 methodコマンドライン引数で指定できるようにします。

method の値

0.3.0 で使える method の値は以下の通りです。(ただし、最新のソースでは adam 等が追加されている)

  • sgd (デフォルト値)
  • adagrad
  • adadelta
  • windowgrad

誤差

誤差というのはニューラルネットで算出された結果と実際の結果(正解値)との差異で、今回は交差エントロピーの結果です。

誤差は以下のようにして取得できます。

  • 学習時は train の戻り値の loss プロパティ
  • テスト時は backward の戻り値 (先に forward を実行しておく必要あり)

誤差を最小化 (0 へ近づける) するようにパラメータが調整されていきますので、誤差の推移が重要です。

サンプル作成

sample1.js をベースにして、学習と評価の誤差と正解率を集計する処理を batchLearn へ集約し、繰り返し回数を 50 に変更しています。

sample2.js
var Promise = require('bluebird');
var basicCsv = require('basic-csv');
var shuffle = require('shuffle-array');
var convnetjs = require('convnetjs');

var readCSV = Promise.promisify(basicCsv.readCSV);

var epoch = 50;
var trainRate = 0.7;
var batchSize = 1;

var learningRate = parseFloat(process.argv[2]);
var updateMethod = process.argv[3];

・・・

// 構成
var layer_defs = [
    { type: 'input', out_sx: 1, out_sy: 1, out_depth: 4 },
    { type: 'softmax', num_classes: 3 }
];

・・・

var trainer = new convnetjs.Trainer(model, {
    batch_size: batchSize,
    learning_rate: learningRate,
    method: updateMethod
});

// 学習・評価の実施
var batchLearn = (data, learnLossFunc) => {
    var res = data.reduce(
        (acc, d) => {
            // 誤差の加算
            acc.loss += learnLossFunc(d);
            // 正解数の加算
            acc.accuracy += (model.getPrediction() == d.label)? 1: 0;

            return acc;
        },
        { loss: 0.0, accuracy: 0 }
    );

    for (var key in res) {
        // 誤差・正解数の平均値を算出
        res[key] /= data.length;
    }

    return res;
};

readCSV('iris.data')
    ・・・
    .then( data => {
        for (var i = 0; i < epoch; i++) {
            shuffle(data.train);

            // 学習
            var trainRes = batchLearn(data.train, d => 
                // 学習と誤差の取得
                trainer.train(d.features, d.label).loss
            );

            // テスト(評価)
            var testRes = batchLearn(data.test, d => {

                model.forward(d.features);
                // 誤差の取得
                return model.backward(d.label);
            });

            console.log([
                trainRes.loss, trainRes.accuracy, 
                testRes.loss, testRes.accuracy
            ].join(','));
        }
    });

実行結果

learning_rate を 3パターン (0.05, 0.01, 0.001) と method を 3パターン (sgd, adagrad, adadelta) の組み合わせ (計 9パターン) の誤差と正解率を折れ線グラフで表示してみます。

  • 青線が学習結果
  • 赤線がテスト(評価)結果

グラフ作成には D3.js を使い、SVG として出力するようにしています。(line_chart.js 参照)

実行例
> node sample2.js 0.01 sgd > results/sample2-0.01-sgd.csv

> node line_chart.js results/sample2-0.01-sgd.csv > results/sample2-0.01-sgd.csv.svg

実行の度に結果が変わる点にご注意下さい。

(a) method = sgd

learning_rate = 0.05 (sample2-0.05-sgd.csv.svg

f:id:fits:20160215002402p:plain

learning_rate = 0.01 (sample2-0.01-sgd.csv.svg

f:id:fits:20160215002629p:plain

learning_rate = 0.001 (sample2-0.001-sgd.csv.svg

f:id:fits:20160215002645p:plain

(b) method = adagrad

learning_rate = 0.05 (sample2-0.05-adagrad.csv.svg

f:id:fits:20160215002711p:plain

learning_rate = 0.01 (sample2-0.01-adagrad.csv.svg

f:id:fits:20160215002734p:plain

learning_rate = 0.001 (sample2-0.001-adagrad.csv.svg

f:id:fits:20160215002746p:plain

誤差が 1以下にならなかったので、誤差のグラフには何も描画されていません。

(c) method = adadelta

learning_rate = 0.05 (sample2-0.05-adadelta.csv.svg

f:id:fits:20160215002811p:plain

learning_rate = 0.01 (sample2-0.01-adadelta.csv.svg

f:id:fits:20160215002826p:plain

learning_rate = 0.001 (sample2-0.001-adadelta.csv.svg

f:id:fits:20160215002844p:plain

(3) 隠れ層を追加 (入力層 - 隠れ層 - 出力層)

最後に隠れ層を追加してみます。

サンプル作成

入力層と出力層の間に type: 'fc' の層を追加すれば隠れ層を追加できます。

sample3.js
・・・

var fcNeuNum = parseInt(process.argv[4]); // 隠れ層のニューロン数
var fcAct = process.argv[5]; // 隠れ層の活性化関数

・・・

// 構成
var layer_defs = [
    { type: 'input', out_sx: 1, out_sy: 1, out_depth: 4 },
    { type: 'fc', num_neurons: fcNeuNum, activation: fcAct },
    { type: 'softmax', num_classes: 3 }
];

・・・

実行結果

(2) と同様に誤差と正解率をグラフ化してみます。

隠れ層のニューロン数は 8、隠れ層の活性化関数は relu で実施しました。

実行例
> node sample3.js 0.01 sgd 8 relu > results/sample3-0.01-sgd-8-relu.csv

> node line_chart.js results/sample3-0.01-sgd-8-relu.csv > results/sample3-0.01-sgd-8-relu.csv.svg

(a) method = sgd, num_neurons = 8, activation = relu

learning_rate = 0.05 (sample3-0.05-sgd-8-relu.csv.svg

f:id:fits:20160215003106p:plain

learning_rate = 0.01 (sample3-0.01-sgd-8-relu.csv.svg

f:id:fits:20160215003119p:plain

learning_rate = 0.001 (sample3-0.001-sgd-8-relu.csv.svg

f:id:fits:20160215003131p:plain

(b) method = adagrad, num_neurons = 8, activation = relu

learning_rate = 0.05 (sample3-0.05-adagrad-8-relu.csv.svg

f:id:fits:20160215003149p:plain

learning_rate = 0.01 (sample3-0.01-adagrad-8-relu.csv.svg

f:id:fits:20160215003200p:plain

learning_rate = 0.001 (sample3-0.001-adagrad-8-relu.csv.svg

f:id:fits:20160215003219p:plain

(c) method = adadelta, num_neurons = 8, activation = relu

learning_rate = 0.05 (sample3-0.05-adadelta-8-relu.csv.svg

f:id:fits:20160215003235p:plain

learning_rate = 0.01 (sample3-0.01-adadelta-8-relu.csv.svg

f:id:fits:20160215003253p:plain

learning_rate = 0.001 (sample3-0.001-adadelta-8-relu.csv.svg

f:id:fits:20160215003311p:plain