誰でもできちゃうSVMによるパターン認識

#はじめに
最近、機械学習に興味を持ち、少しずつですが勉強しています。
ただ、モチベーションを維持するのって結構難しいですよね...
そこで、モチベーションを維持するために、実際に動くものを作って遊んでみました。

この記事では、Node.jsでSVM(サポートベクターマシン)を簡単に実装できる手順を公開します。

#対象者
- Node.jsを触ったことがある人
- npmの知識が多少ある人
- **機械学習に興味のあるけど、モチベーションが維持できない人**
- (SVMの説明はこの記事では省略します)

#今回作るもの
学習データからモデルを生成し、そのモデルからテストデータの分類を行うプログラムを作ります。
使用するモジュールとして、node-svmを使います。

#バージョン
```terminal
$ node -v
v10.16.0
$ npm -v
6.9.0
```
Node.jsのバージョンによって、node-svmをインストールできないので注意が必要です。
私は、v12.6.0でインストールできませんでした。

#手順

###準備
- 作業するディレクトリを作成
- `npm init`で初期化

```terminal
$ mkdir test_svm
$ cd test_svm
$ npm init
```

###モジュールのインストール
node-svm以外に、以下のモジュールをインストールします。
- 学習データやテストデータを読み込むために`fs`
- 読み込んだデータをパースするために`csv-parse`

```terminal
$ npm install --save fs
$ npm install --save csv-parse
$ npm install --save node-svm
```

###実装
```main.js
// モジュールの読み込み
const fs = require("fs");
const csvSync = require("csv-parse/lib/sync");
const svm = require("node-svm");

// 学習データ、テストデータのパス
const learningDataPath = "learning_data.csv";
const testDataPath = "test_data.csv";

// 生成したモデルを格納するための変数
var newModel = null;

// メイン処理
if (require.main === module) {
    var arrLD = readFile(learningDataPath);
    var normArrLD = normalizeArr(arrLD);
    var arrTD = readFile(testDataPath);
    initModel(normArrLD).then(() => {   // 学習データからモデルの生成
        estimate(arrTD);                // モデル生成完了後、テスト
    })
}

/**
 * ファイルを読み込み、配列として返す関数
 * @param {string} filePath 読み込むファイルのパス
 * @return {Array.<Array.<number>>} 学習データの二次元配列
 */
function readFile(filePath) {
    var file = fs.readFileSync(filePath);
    var arr = csvSync(file);
    // 以下の処理で、配列内の値が文字列のため数値にパース
    var columnNum = arr.length;
    var rowNum = arr[0].length;
    for(var i=0; i<columnNum; i++) {
        for(var j=0; j<rowNum; j++) {
            arr[i][j] = parseFloat(arr[i][j]);
        }
    }
    return arr;
}

/**
 * 二次元配列を入力データと正解データに分割し、正規化する関数
 * @param {Array.<Array.<number>>} arr 正規化前の配列
 * @return {Array.<Array.<number>, number>} 正規化後の配列
 */
function normalizeArr(arr) {
    var columnNum = arr.length;
    var rowNum = arr[0].length;
    var normArr = Array(columnNum);     // 長さががデータ数の配列を用意
    for (var i=0; i<columnNum; i++) {
        normArr[i] = Array(2);          // 学習データと正解データを分割するための配列を用意 normArr[i]=[学習データ,正解データ]
        normArr[i][0] = Array(rowNum-1);// 長さが学習データの次元数の配列を用意 normArr[i]=[[学習データ[0],...,学習データ[rowNum-1]],正解データ]
        // 以下の処理で、正規化
        for (var j=0; j<rowNum-1; j++) {
            normArr[i][0][j] = arr[i][j];
        }
        normArr[i][1] = arr[i][rowNum-1];
    }
    return normArr;
}

/**
 * 正規化された配列からモデルを生成する関数
 * @param {Array.<Array.<number>, number>} arr 正規化された配列
 * @return {Promise} Promiseのインスタンス
 */
function initModel(arr) {
    return new Promise(function (resolve, reject) {
        var clf = new svm.CSVC();         // 分類器の生成(今回の設定はデフォルト)
        clf.train(arr).spread(function (model, report) {
            newModel = svm.restore(model);// モデルの生成
            resolve("モデル生成完了");      // モデル生成完了後、thenメソッドが呼ばれる
        });
    });
}

/**
 * モデルからテストデータの分類を行う関数
 * @param {Array.<Array.<number>>} テストデータ
 */
function estimate(arr) {
    if(newModel) {  // モデルのnullチェック
        var result = Array(arr.length);
        for(var i=0; i<arr.length; i++) {
            result[i] = newModel.predictSync(arr[i]);   // モデルからテストデータを分類
            console.log(result[i]);                     // 結果の表示
        }
    }
}
```
モデルの生成が非同期で行われるため、Promiseを使って「モデル生成 -> テストデータの分類」の順で処理します。

###学習データの準備
今回は、RGBの値から色を分類するための学習データを作ります。
左からR値、G値、B値、正解データとなっています。
正解データは、1が赤、2が緑、3が青とラベル付けしてあります。

```learning_data.csv
255, 49, 44, 1
235, 114, 0, 1
185, 0, 0, 1
180, 9, 72, 1
92, 194, 24, 2
0, 144, 66, 2
69, 193, 112, 2
0, 228, 69, 2
0, 9 ,72, 3
65, 164, 255, 3
107, 180, 231, 3
108, 81, 194, 3
```

###テストデータの準備
```test_data.csv
228,79,107
```
![test_red.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108396/169b383c-b696-9df9-f78d-9f23966d5f36.png)
上の色をテストデータとしてセットします。
赤ぽいので、1(赤)と分類されると嬉しいですね。

学習データとテストデータは`main.js`と同じ階層に置きます。
ここまでで、ファイル構成が以下のようになると思います。

>
test_svm/
 ├ main.js
 ├ node_modules/
 ├ package.json
 ├ learning_data.csv
 └ test_data.csv

###実行
```terminal
$ node main.js 
1
```
今回生成したモデルは、[228,79,107]を1(赤)と分類しました。
期待通りの結果ですね！

#最後に
私はSVMについてあまり知識がありませんが、モジュールを使うことで簡単に実装できました。
やっぱり実際に動いているものを見ると、中で何が行われているか気になりますよね〜
本記事では、色の分類をするモデルを生成しましたが、他のものを分類することもできます。
よかったら遊んでください。