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 でインストールするようになるのかもしれません。
その他
今回は以下のライブラリも使いましたのでこれらもインストールしておきます。
> 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.Net
の makeLayers
を使ってニューラルネットを初期構築します。 (重みなどのパラメータをランダム値で初期化)
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)
learning_rate = 0.01 (sample2-0.01-sgd.csv.svg)
learning_rate = 0.001 (sample2-0.001-sgd.csv.svg)
(b) method = adagrad
learning_rate = 0.05 (sample2-0.05-adagrad.csv.svg)
learning_rate = 0.01 (sample2-0.01-adagrad.csv.svg)
learning_rate = 0.001 (sample2-0.001-adagrad.csv.svg)
誤差が 1以下にならなかったので、誤差のグラフには何も描画されていません。
(c) method = adadelta
learning_rate = 0.05 (sample2-0.05-adadelta.csv.svg)
learning_rate = 0.01 (sample2-0.01-adadelta.csv.svg)
learning_rate = 0.001 (sample2-0.001-adadelta.csv.svg)
(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