Swift：CoreML×CreateMLでスタンドアローンな機械学習分類器を実装する

#はじめに
CoreMLに加えてCreateMLが登場したことで，ようやく機械学習モデルの生成から推定まで行う処理をSwiftで実装可能となりました．しかし，CreateMLで機械学習モデルを生成する系の記事のほとんどは`Playground`を使う方法のみで，コードだけで機械学習モデルを生成する方法が載っていませんでした．そこで今回は，データセットの収集，機械学習モデルの生成，分類（推定）までをスタンドアローンで行えるサンプルプログラムを実装したので，そのソースを掻い摘みながら実装方法解説したいと思います．

#サンプルプロジェクト（Github公開）
macOS向けのアプリケーションとして実装しています．
（おそらく同様の方法でiOSでも動作するものが実装できると思います．）
今回は簡単のためSandBoxをオフにしています．

Githubのリンク：https://github.com/Kyome22/ClassificationSample
5×5のマス目の中に描いた○と△を分類します．
![classification_sample.gif](https://qiita-image-store.s3.amazonaws.com/0/67153/975ea9df-7ace-a927-d007-1f380c4a9d7f.gif)
Youtubeリンク：[フルバージョンのデモ動画](https://youtu.be/RpMUobCqyTc)

#1.データセットの用意
CSVファイルを生成してアプリケーションからアクセス可能な場所に保存します．どんなセンサを使ってどんな風にデータを集めてもOKですが，以下のフォーマットに従う必要があります．
- CSVファイルの一行目にはヘッダ（各カラムのタイトル）が必要
- 一つのサンプルデータは一つの目的変数と一つ以上の説明変数で構成される

サンプルが用いるCSVファイルはこんな感じです．

```plaintext:sampling.csv
object,X0Y0,X1Y0,X2Y0,X3Y0,X4Y0,X0Y1,X1Y1,X2Y1,X3Y1,X4Y1,X0Y2,X1Y2,X2Y2,X3Y2,X4Y2,X0Y3,X1Y3,X2Y3,X3Y3,X4Y3,X0Y4,X1Y4,X2Y4,X3Y4,X4Y4
circle,1.0,1.0,1.0,1.0,1.0,1.0,1.0,0.0,0.0,1.0,1.0,0.0,0.0,0.0,1.0,1.0,0.0,0.0,0.0,1.0,1.0,1.0,1.0,1.0,1.0
circle,1.0,1.0,1.0,1.0,0.0,1.0,0.0,0.0,1.0,1.0,1.0,0.0,0.0,0.0,1.0,1.0,0.0,0.0,0.0,1.0,1.0,1.0,1.0,1.0,1.0
circle,1.0,1.0,1.0,1.0,1.0,1.0,0.0,0.0,0.0,1.0,1.0,0.0,0.0,0.0,1.0,1.0,0.0,0.0,0.0,1.0,1.0,1.0,1.0,1.0,1.0
circle,0.0,1.0,1.0,1.0,0.0,1.0,1.0,0.0,1.0,1.0,1.0,0.0,0.0,0.0,1.0,1.0,1.0,0.0,1.0,1.0,0.0,1.0,1.0,1.0,0.0
circle,0.0,1.0,1.0,1.0,1.0,1.0,1.0,0.0,0.0,1.0,1.0,0.0,0.0,0.0,1.0,1.0,1.0,0.0,0.0,1.0,0.0,1.0,1.0,1.0,1.0
triangle,1.0,1.0,1.0,1.0,1.0,1.0,0.0,0.0,1.0,1.0,1.0,1.0,0.0,1.0,0.0,0.0,1.0,1.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0
triangle,1.0,1.0,1.0,1.0,1.0,1.0,0.0,0.0,0.0,1.0,0.0,1.0,0.0,1.0,0.0,0.0,1.0,1.0,1.0,0.0,0.0,1.0,1.0,0.0,0.0
triangle,1.0,1.0,1.0,1.0,1.0,1.0,1.0,0.0,0.0,1.0,0.0,1.0,0.0,1.0,1.0,0.0,1.0,1.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0
triangle,1.0,1.0,1.0,1.0,1.0,1.0,1.0,0.0,1.0,1.0,0.0,1.0,1.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0
triangle,1.0,1.0,1.0,1.0,1.0,1.0,1.0,0.0,0.0,1.0,0.0,1.0,0.0,1.0,0.0,0.0,1.0,1.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0
```

objectが目的変数で，残りが説明変数ですね．

例えば書類フォルダにCSVファイルを保存する場合は以下のようにします．

```swift
func saveCSV(fileName: String, text: String) {
    let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
    let url = dir.appendingPathComponent(fileName)
    if FileManager.default.fileExists(atPath: url.path) { //追記する場合
        if let handle = try? FileHandle(forWritingTo: url) {
            handle.seekToEndOfFile()
            handle.write(text.data(using: String.Encoding.utf8)!)
        }
    } else { //新規作成の場合
        let header: String = "object,X0Y0,X1Y0,X2Y0,...,X4Y4\n" //ヘッダ行
        try? (header + text).write(to: url, atomically: true, encoding: String.Encoding.utf8)
    }
}

// csvDataは目的変数と説明変数のカンマ区切り文字列（行末改行）
saveCSV(fileName: "sampling.csv", text: csvData)
```

#2.機械学習モデルのファイルの生成
生成したCSVファイルを読み込んで機械学習モデルを生成します．最終的に`.mlmodel`ファイルをアプリケーションからアクセス可能な場所に吐き出せればOKです．
先ほどと同様書類フォルダに機械学習モデルファイルを保存する場合を示します．

```swift
func getURL(fileName: String) -> URL? {
    guard let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
        return nil
    }
    let url = dir.appendingPathComponent(fileName)
    if FileManager.default.fileExists(atPath: url.path) {
        return url
    }
    return nil
}

func saveModel(fileName: String, _ classifier: MLClassifier, _ metaData: MLModelMetadata) {
    let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
    let url = dir.appendingPathComponent(fileName)
    try? classifier.write(to: url, metadata: metaData)
}

func learn() {
    guard let url = getURL(fileName: "sampling.csv") else {
        return
    }
    //targetColumnには目的変数のタイトルを指定します． (このためにCSVにヘッダが必要．)
    if let dataTable = try? MLDataTable(contentsOf: url),
        let classifier = try? MLClassifier(trainingData: dataTable, targetColumn: "object") {
        // 機械学習モデルファイルのメタデータ設定
        let metaData = MLModelMetadata(author: "Author Name",                  // 開発者の名前とか
                                       shortDescription: "Object Distinction", // モデルの短い説明
                                       version: "1.0")                         // モデルのバージョン
        saveModel(fileName: "object.mlmodel", classifier, metaData)
    }
}

learn()
```

#3.機械学習モデルのインスタンスの生成
ここが一番鬼門です！`Playground`の手を借りずに機械学習モデルのインスタンスを生成するには一工夫必要です．

1. まず，2の方法で機械学習モデルのファイルを一旦生成してしまいます．
2. 生成した`.mlmodel`ファイルをXcodeのプロジェクトに追加します．
3. その状態でどこでもいいので，`.mlmodel`ファイルの拡張子を除いた部分をソースに書き込みます．
4. 入力した文字列をコマンドキーを押しながらクリックして`Jump to Definition`で飛びます．

![jump.png](https://qiita-image-store.s3.amazonaws.com/0/67153/7c1a2989-7e23-7658-602e-9151dbddc769.png)
5. 機械学習モデル用の自動生成クラスファイルが表示されるので，メニューバーの`File`->`Export`を押してプロジェクトにファイルを追加します．（ファイル名はいい感じにつけてください）

![model.png](https://qiita-image-store.s3.amazonaws.com/0/67153/e6b7f8a6-d0d7-fdf8-5fda-27be2272d285.png)
6. このソースファイルの`〇〇`，`〇〇Input`，`〇〇Output`となっているところをいい感じにつけたファイル名と同じ感じで書き換えてください．（詳しくはGitHubのサンプルコードを参照．）サンプルコードでは`ObjectModel`，`ObjectInput`，`ObjectOutput`としました．
7. また，`init(contentsOf url: URL) throws`以外の`init`は不要なので削除してください．
8. ここまでできたら，Xcodeのプロジェクトに追加した`.mlmodel`ファイルをプロジェクトから除いてください（Move to trash）．
9. あとは以下の例のようにすれば機械学習モデルのインスタンスを生成できます．

```swift
func getModel(fileName: String) -> ObjectModel? {
    let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
    let url = dir.appendingPathComponent(fileName)
    if let compiledUrl = try? MLModel.compileModel(at: url), let model = try? ObjectModel(contentsOf: compiledUrl) {
        return model
    }
    return nil
}

let model: ObjectModel? = getModel(fileName: "object.mlmodel")
```

ちなみに，なぜ自動生成のクラスファイルをそのまま使わないのかというと，複数のユーザにシステムを対応させる場合CSVファイルや`.mlmodel`ファイルが複数になるため，一意のクラスからインスタンスを生成することができなくなるからです．（`.mlmodel`ファイルの数だけその名を冠したクラスファイルが自動生成されるので...）

#4.分類
`prediction`メソッドを使って推定結果を取得します．引数には説明変数をCSVの順番通りに全部渡します．`~~Probability`プロパティから確信度を取得できます．

```swift
// dataは説明変数が入った配列
func predict(_ data: [Double]) {
    if model == nil { return }
        if let output: ObjectOutput = try? model!.prediction(X0Y0: data[0],  X1Y0: data[1],  X2Y0: data[2],  X3Y0: data[3],  X4Y0: data[4],
                                                             X0Y1: data[5],  X1Y1: data[6],  X2Y1: data[7],  X3Y1: data[8],  X4Y1: data[9],
                                                             X0Y2: data[10], X1Y2: data[11], X2Y2: data[12], X3Y2: data[13], X4Y2: data[14],
                                                             X0Y3: data[15], X1Y3: data[16], X2Y3: data[17], X3Y3: data[18], X4Y3: data[19],
                                                             X0Y4: data[20], X1Y4: data[21], X2Y4: data[22], X3Y4: data[23], X4Y4: data[24]) {
            let probability = round(1000 * output.objectProbability[output.object]!) / 10.0
            Swift.print("\(output.object): \(probability)%")
        }
    }
}
```

# 終わりに
少し工夫は必要ですがCreateMLのおかげで機械学習を導入したプログラムをSwiftだけで実装可能になりました．今回はアプリ開発者向けというよりかは研究開発者向けとして記事を書いたのですが，これで「センシング→分類」系の実装に悩んでいる人を少しでも救えたら嬉しいですね．

# 蛇足
今回サンプルコードとして○と△を分類するというのを適当に作ってみたのですが，それぞれ5サンプルずつ収集して学習かけるだけでもかなりの正答率を叩き出してびっくりしました．