Create MLで簡単に機械学習を体験する

## 実施環境

* macOS 10.14+
* Xcode 10.0+

まだ正式リリースされていない情報なので変更の可能性があります。

## Image Classification

画像の分類ができる学習モデルの作成方法です。画像に映ったものに対してそれが何なのかラベリングしてくれるモデルです。[ドキュメント](https://developer.apple.com/documentation/create_ml/creating_an_image_classifier_model)

### データセットの準備

学習させたいクラスの画像データを用意して、9:1とかでトレーニング用とテスト用に分けます。データは少なくとも10枚、それぞれのクラスで同じ枚数分用意してください。(クロスバリデーションとか機械学習ど素人の私は気にしないのでスルーです)

> Use at least 10 images per label for the training set, but more is always better. Also, balance the number of images for each label.

また、画像は小さくても299x299pxのもので、実際のユースケースに近い様々な場面の画像にした方がいいです。(回転やノイズを入れるのはできるのでそこまで意識しなくて良さそうだけど)
> The images don’t have to be the same size as each other, nor do they have to be any particular size, although it’s best to use images that are at least 299x299 pixels.

画像の名前は何でもよくて、画像を入れているディレクトリ名をクラス名にしておけば大丈夫です。

```console
├── dataset
│   ├── test
│   │   ├── apple
│   │   │   ├ ...
│   │   │   └── 53089209_7eaa21ab1c.jpg
│   │   ├── banana
│   │   │   ├ ...
│   │   │   └── 67485060_673dc47824.jpg
│   │   └── orange
│   │   │   ├ ...
│   │       └── 81014064_ff7a1ca6c0.jpg
│   └── train
│       ├── apple
│       │   ├ ...
│       │   └── 455149905_9cec4da2e5.jpg
│       ├── banana
│       │   ├ ...
│       │   └── 925720160_7005c05f7d.jpg
│       └── orange
│           ├ ...
│           └── 543810773_4ca350eeb5.jpg
```

こんな感じで配置します。画像集めるのがダルい人は、個人利用であれば[ImageNet](http://www.image-net.org/)からスクリプトなりを書いてまとめて落とせばいいと思います。

### トレーニング開始
macOSターゲットのPlaygroundファイルを作成、下記のコードをペッて貼ってExecute Playgroundで実行、Assistant Editorを開きます。

```swift
import CreateMLUI

let builder = MLImageClassifierBuilder()
builder.showInLiveView()
```

![fe6997f8-5507-4afd-9d16-d88f4ed8522b.png](https://qiita-image-store.s3.amazonaws.com/0/83361/e04addc2-57e4-eee3-5608-5b43cb4fede9.png)

こんな画面が出るので、イテレートの回数(各クラスのデータセットを何回処理するか)や、Flip, Rotate, Expose, Shearなどで画像に変化を与えるパラメータの設定ができます。[ここ](https://developer.apple.com/documentation/create_ml/improving_your_model_s_accuracy)に軽く説明が書いてありました。

![b5e271a7-9041-463b-8a43-3fdb7395c221.png](https://qiita-image-store.s3.amazonaws.com/0/83361/0c21b01a-845a-44e9-6970-357d42234b96.png)

あとは先ほど用意したトレーニングデータとテストデータのディレクトリを選んでTrainボタンを押すだけです。画像がピョコピョコ変わっていくのを見ていれば終わります。

![77ba7e4d-8d95-4c8e-b81b-03429b7f0ae1.png](https://qiita-image-store.s3.amazonaws.com/0/83361/c6bf54dd-8f22-8d8b-6e2b-ee35f2b0ac73.png)

```console
Extracting image features from full data set.
Analyzing and extracting image features.
+------------------+--------------+------------------+
| Images Processed | Elapsed Time | Percent Complete |
+------------------+--------------+------------------+
| 1                | 1.57s        | 2%               |
| 2                | 1.87s        | 4%               |
| 3                | 2.05s        | 6.25%            |
| 4                | 2.23s        | 8.25%            |
| 5                | 2.39s        | 10.25%           |
| 10               | 3.18s        | 20.75%           |
| 25               | 5.80s        | 52%              |
| 48               | 9.48s        | 100%             |
+------------------+--------------+------------------+
```

めっちゃPCが重くなりますが、デフォルトの設定で3クラスの画像20枚を学習させたら数十秒で学習が終わりました。すごい早いです。サイズも33KBという小ささです。

> Create ML leverages the machine learning infrastructure built in to Apple products like Photos and Siri. This means your image classification and natural language models are smaller and take much less time to train.

SiriとかPhotosの学習基盤を使用しているからだとか。ブラックボックス過ぎてよくわからないけどすごい。転移学習させてるってことなんですかね。

```console
******CONFUSION MATRIX******
----------------------------------
True¥Pred apple   banana  orange  
apple     4       0       0       
banana    0       4       0       
orange    0       0       4       

******PRECISION RECALL******
----------------------------------
Class  Precision(%)   Recall(%)      
apple  100.00         100.00         
banana 100.00         100.00         
orange 100.00         100.00    
```

結構異なる場面のテスト画像を与えてみたのですが、100%の正答率です。本当にしゅごい。

### MLModelの書き出し

![4eaa95da-421f-4812-8938-2bada720444e.png](https://qiita-image-store.s3.amazonaws.com/0/83361/6fac9858-7e69-90e0-4b54-44423cfa4a2b.png)

この画面が出ればMLModeファイルの出力先や名前などを指定して保存ができます。または既に`/var/folders/mh/foo/bar/baz.mlmodel`に出力したってログに出ているのでコピってくれば良いです。

実機で試すのであればこの[サンプル](https://developer.apple.com/documentation/vision/classifying_images_with_vision_and_core_ml)のMLModelを入れ替えてビルドすればいいです。下の画像はサンプルアプリの画面です。

```diff
-            let model = try VNCoreMLModel(for: MobileNet().model)
+            let model = try VNCoreMLModel(for: Fruits().model)
```

![UNADJUSTEDNONRAW_thumb_1217.jpg](https://qiita-image-store.s3.amazonaws.com/0/83361/d5e3428e-f282-73de-544a-dfc8d0a70d83.jpeg)

上記は`MLImageClassifierBuilder`でPlayground上で行なっていますが、本格的に導入する場合はBuilderが内部で使用している[`MLImageClassifier`](https://developer.apple.com/documentation/create_ml/mlimageclassifier)を使ってmacOSアプリで運用するのがいいのかなと思いました。

## Text Classification

自然言語テキストを分類できる学習モデルの作成方法です。文章のようなテキストが何を表しているのかラベリングしてくれるモデルです。[ドキュメント](https://developer.apple.com/documentation/create_ml/creating_a_text_classifier_model)

### データセットの準備

JSONまたはCSVファイルから[`MLDataTable`](https://developer.apple.com/documentation/create_ml/mldatatable)を生成します。または配列を扱うように`MLDataTable`を直接編集することも可能です。

この`MLDataTable`は[`MLDataColumn`](https://developer.apple.com/documentation/create_ml/mldatacolumn)と[`MLDataValue`](https://developer.apple.com/documentation/create_ml/mldatavalue)を持っています。ドキュメントを見るとわかるのですが、要は配列の中に辞書型の値を持つデータしか受け付けないようになっています。

```json
[
    {
        "text": "この映画はとても面白いね！",
        "label": "positive"
    }, {
        "text": "とても退屈だ。眠くなるよ。",
        "label": "negative"
    }, {
        "text": "いいんじゃない。",
        "label": "neutral"
    ...
```

こんな感じのデータです。少量のデータでは学習できなかったので、[東北大学 乾・鈴木研究室 - 日本語評価極性辞書](http://www.cl.ecei.tohoku.ac.jp/index.php?Open%20Resources%2FJapanese%20Sentiment%20Polarity%20Dictionary)を使用させていただきました。(文章ではなく名詞なのでちょっと違うかもしれませんが細かいことは気にしません)

macOSターゲットのPlaygroundファイルを作成して、Resourcesフォルダに先ほどのJSONまたはCSVファイルをぶち込みます。(そうするとメインバンドルからファイルが取得できます)

### トレーニング開始

あとは下記のコードをペッて貼ってExecute Playgroundを実行するだけで学習が開始します。やっていること前回同様トレーニング用とテスト用に分けてトレーニング用のデータを渡しているだけです。

```swift
let url = Bundle.main.url(forResource: "data", withExtension: "csv")!
let data = try MLDataTable(contentsOf: url)
let (train, test) = data.randomSplit(by: 0.8, seed: 5)

// Start classification training
let classifier = try MLTextClassifier(
    trainingData: train, textColumn: "text", labelColumn: "label")
```

もしバリデーションデータの指定、言語やアルゴリズムの設定を変えたい場合は[MLTextClassifier.ModelParameters](https://developer.apple.com/documentation/create_ml/mltextclassifier/modelparameters)を引数に渡してあげるみたいです。(いい感じで終わらせているけど、イテレーション回数とかどこで指定できるのですかね)

```console
Starting MaxEnt training with 10149 samples
Iteration 1 training accuracy 0.371268
Iteration 2 training accuracy 0.931422
Iteration 3 training accuracy 0.947581
Iteration 4 training accuracy 0.966598
Iteration 5 training accuracy 0.977042
Iteration 6 training accuracy 0.984629
Iteration 7 training accuracy 0.986304
Iteration 8 training accuracy 0.986994
Iteration 9 training accuracy 0.987388
Iteration 10 training accuracy 0.987881
Iteration 11 training accuracy 0.988472
Finished MaxEnt training in 0.17 seconds
```

トレーニング中にはイテレーションの回数と、トレーニング用のデータから少量のバリデーションデータを用意して検証した結果がログ出力されます。更に詳しく見たい場合は`MLTextClassifier`の`trainingMetrics`と`validationMetrics`がそれぞれトレーニングとバリデーションの詳しい結果である[`MLClassifierMetrics`](https://developer.apple.com/documentation/create_ml/mlclassifiermetrics)を持っているので見るといいです。

ド素人の私には必要ないのですが、一応バリデーションのAccuracy(正解率)、Recall(再現率)とConfusion(何と何を間違えたか)を出力してみます。下記のコードをペッて貼れば良いです。

```swift
// Validation accuracy as a percentage
let validAccuracy = (1.0 - classifier.validationMetrics.classificationError) * 100.0
print("Validation Accuracy: ", validAccuracy)
print("Recall: ", classifier.validationMetrics.precisionRecall)
print("Confusion: ", classifier.validationMetrics.confusion)
```

```console
Validation Accuracy:  51.127819548872175

Recall:  
Columns:
    actual_count	integer
    class	string
    missed_predicting_this	integer
    precision	float
    predicted_correctly	integer
    predicted_this_incorrectly	integer
    recall	float
Rows: 3
Data:
+----------------+----------------+------------------------+----------------+---------------------+
| actual_count   | class          | missed_predicting_this | precision      | predicted_correctly |
+----------------+----------------+------------------------+----------------+---------------------+
| 211            | negative       | 132                    | 0.731481       | 79                  |
| 199            | neutral        | 40                     | 0.438017       | 159                 |
| 122            | positive       | 88                     | 0.557377       | 34                  |
+----------------+----------------+------------------------+----------------+---------------------+
+----------------------------+----------------+
| predicted_this_incorrectly | recall         |
+----------------------------+----------------+
| 29                         | 0.374408       |
| 204                        | 0.798995       |
| 27                         | 0.278689       |
+----------------------------+----------------+
[3 rows x 7 columns]

Confusion:  
Columns:
    True Label	string
    Predicted	string
    Count	integer
Rows: 9
Data:
+----------------+----------------+----------------+
| True Label     | Predicted      | Count          |
+----------------+----------------+----------------+
| negative       | negative       | 79             |
| negative       | neutral        | 124            |
| negative       | positive       | 8              |
| neutral        | negative       | 21             |
| neutral        | neutral        | 159            |
| neutral        | positive       | 19             |
| positive       | negative       | 8              |
| positive       | neutral        | 80             |
| positive       | positive       | 34             |
+----------------+----------------+----------------+
[9 rows x 3 columns]
```

なるほど、よくわからないけど精度が低そうなことはわかった。次にテスト用のデータで精度を確かめてみます。先ほどのトレーニング時と同様にテストの`evaluation(on:)`メソッドで返ってくるのは`MLClassifierMetrics`なので検証結果が見れます。

```swift
// Start classification testing
let evaluation = classifier.evaluation(on: test)

// Evaluation accuracy as a percentage
let evalAccuracy = (1.0 - evaluation.classificationError) * 100.0
print("Evaluation Accuracy: ", evalAccuracy)
print("Confusion: ", evaluation.confusion)
print("Recall: ", evaluation.precisionRecall)
```

```console
Evaluation Accuracy:  52.98139004937333

Recall:  
Columns:
    actual_count	integer
    class	string
    missed_predicting_this	integer
    precision	float
    predicted_correctly	integer
    predicted_this_incorrectly	integer
    recall	float
Rows: 3
Data:
+----------------+----------------+------------------------+----------------+---------------------+
| actual_count   | class          | missed_predicting_this | precision      | predicted_correctly |
+----------------+----------------+------------------------+----------------+---------------------+
| 959            | negative       | 599                    | 0.72           | 360                 |
| 1006           | neutral        | 181                    | 0.456558       | 825                 |
| 668            | positive       | 458                    | 0.644172       | 210                 |
+----------------+----------------+------------------------+----------------+---------------------+
+----------------------------+----------------+
| predicted_this_incorrectly | recall         |
+----------------------------+----------------+
| 140                        | 0.375391       |
| 982                        | 0.82008        |
| 116                        | 0.314371       |
+----------------------------+----------------+
[3 rows x 7 columns]

Confusion:  
Columns:
    True Label	string
    Predicted	string
    Count	integer
Rows: 9
Data:
+----------------+----------------+----------------+
| True Label     | Predicted      | Count          |
+----------------+----------------+----------------+
| negative       | negative       | 360            |
| negative       | neutral        | 574            |
| negative       | positive       | 25             |
| neutral        | negative       | 90             |
| neutral        | neutral        | 825            |
| neutral        | positive       | 91             |
| positive       | negative       | 50             |
| positive       | neutral        | 408            |
| positive       | positive       | 210            |
+----------------+----------------+----------------+
[9 rows x 3 columns]
```

ほとんどneutralになってますね。ハッキリしないモデルが出来上がってしまいました。

### MLModelの書き出し

結果はどうであれ、とりあえず`MLModel`に書き出します。下記のコードをペッて貼ります。ちなみに`playgroundSharedDataDirectory`は`/Users/foo/Documents/Shared\ Playground\ Data/`です。

```swift
import PlaygroundSupport

let metadata = MLModelMetadata(
    author: "Hanawa Takuro",
    shortDescription: "A model trained to classify emotion",
    version: "1.0")

let directory = playgroundSharedDataDirectory.appendingPathComponent("Emotion.mlmodel")
try classifier.write(to: directory, metadata: metadata)
```

Text ClassifierタイプのMLModelが出力されるので、[Natural Language](https://developer.apple.com/documentation/naturallanguage)のサンプルコードを元に試してみます。MLModelはプロジェクトに突っ込んでください。

```swift
let text = """
バチェラーにめっちゃハマってます。
小銭を投げる人ムカつく。
ところでその手に持っているこんにゃくは何ですか？
"""
let bundle = Bundle(for: emotion.self)
let assetPath = bundle.url(forResource: "emotion", withExtension:"mlmodelc")!
let model = try! NLModel(contentsOf: assetPath)
let tokenizer = NLTokenizer(unit: .sentence)
let range = text.startIndex..<text.endIndex
tokenizer.string = text
tokenizer.enumerateTokens(in: range, using: { tokenRange, attributes in
    let word = String(text[tokenRange])
    let label = model.predictedLabel(for: word)!
    print(word, ":", label)
    return true
})
```

```console
バチェラーにめっちゃハマってます。: neutral
小銭を投げる人ムカつく。: negative
ところでその手に持っているこんにゃくは何ですか？: neutral
```

テンション低い人ですね。今回は名詞のみのデータセットだったので精度が低くなったのですかね。とてつもなく暇なときにでもTwitterなどのSNSから文章を拾って試してみたいですね。

## Categorization and Quantity Estimation

データの分類や量の推定ができる学習モデルの作成方法です。上記のふたつは画像と自然言語に特化してますが、それ以外のゲームの勝ち負けや価格の推定などを行う汎用的なモデルです。

### データセットの準備

これもText Classificationと同様に`MLDataTable`を生成します。日本のデータを探したのですがいい感じのが見つからなかったので、今回は有名なタイタニック号の生存データからある条件下の生存率を推定してみます。使用したのは欠損値を置き換えてくれている[これ](https://github.com/Geoyi/Cleaning-Titanic-Data)を使用しました。

```console
pclass,survived,sex,age
1,1,female,29
1,1,male,1
1,0,female,2
...
```

こんな感じのデータセットで、その中でも推定に使用する`pclass`(1-3: チケットのクラス)、`sex`、`age`、そして推定したい`survived`(0: 死亡、1: 生存)のデータのみに削ったものです。

### トレーニング開始

Text Classificationとほぼ同じ手順でResourceからファイルを読み込み、トレーニング用とテスト用に分けて`MLRegressor`クラスにトレーニング用のデータを渡すだけで始まります。ちなみに分類系の課題の場合は`MLClassifier`を使ってください。

```swift
let url = Bundle.main.url(forResource: "titanic", withExtension: "csv")!
let data = try MLDataTable(contentsOf: url)
let (train, test) = data.randomSplit(by: 0.8, seed: 5)

// Start regressor training
let regressor = try MLRegressor(trainingData: train, targetColumn: "survived")
```

Text Classificationの時に書き忘れましたが、`targetColumn`で指定した行名が推定するターゲット(アウトプット)になります。その他の行は全て推定に使う値(インプット)になります。

```console
▿ MLCreateError
  ▿ generic : 1 element
    - reason : "Error: Unable to parse line \"NA,NA,,29.8811345124283\"\nSet error_bad_lines=False to skip bad lines”a
```

おろろ、そのまま実行すると下記のようなエラーが出ました。これは`MLDataValue`の値としておかしいものが入っていた場合に`MLDataTable`が投げるエラーなので、手直しします。(最後にPclassとかがNAってデータがあったので削除しました)

```console
--------------------------------------------------------
Number of examples          : 984
Number of features          : 3
Number of unpacked features : 3
+-----------+--------------+--------------------+----------------------+---------------+-----------------+
| Iteration | Elapsed Time | Training-max_error | Validation-max_error | Training-rmse | Validation-rmse |
+-----------+--------------+--------------------+----------------------+---------------+-----------------+
| 1         | 0.003303     | 0.638710           | 0.643750             | 0.433433      | 0.447691        |
| 2         | 0.005831     | 0.736254           | 0.746172             | 0.395679      | 0.421459        |
| 3         | 0.008548     | 0.803576           | 0.819147             | 0.373063      | 0.413870        |
| 4         | 0.011107     | 0.846008           | 0.842051             | 0.359576      | 0.412926        |
| 5         | 0.013996     | 0.880159           | 0.860660             | 0.350874      | 0.409760        |
| 10        | 0.028493     | 0.911848           | 0.944801             | 0.335218      | 0.414018        |
+-----------+--------------+--------------------+----------------------+---------------+-----------------+
```

毎回のごとくトレーニング、バリデーション、テストの正答率を見てみましょう。平均のエラー率があったのでそれを使います。ペッて貼ってください。

```swift
// Training accuracy as a percentage
let trainAccuracy = (1.0 - regressor.trainingMetrics.rootMeanSquaredError) * 100.0
print("Training Accuracy: ", trainAccuracy)

// Validation accuracy as a percentage
let validAccuracy = (1.0 - regressor.validationMetrics.rootMeanSquaredError) * 100.0
print("Validation Accuracy: ", validAccuracy)

let evaluation = regressor.evaluation(on: test)
let evalAcculacy = (1.0 - evaluation.rootMeanSquaredError) * 100.0
print("Evaluation Accuracy: ", evalAcculacy)
```

```console
Training Accuracy:  66.47823164748654
Validation Accuracy:  58.598195890306016
Evaluation Accuracy:  61.59430382847886
```

うーん、正直微妙そうです。推定に使用する値が少なかったのですかね。でもデータの整形とか面倒だし、細かいことは気にしないのでこれで良いです。

### MLModelの書き出し

書き出し方法もText Classifierの時と同じです。`MLRegressor`または`MLClassifier`クラスが`write(to:metadata:)`メソッドを持っているのでそれを使います。

```swift
import PlaygroundSupport

let metadata = MLModelMetadata(
    author: "Hanawa Takueo",
    shortDescription: "A model trained to titanic surviver regressor",
    version: "1.0")

let directory = playgroundSharedDataDirectory.appendingPathComponent("Titanic.mlmodel")
try regressor.write(to: directory, metadata: metadata)
```

あとはこの`MLModel`を試すだけです。ターゲット指定した行以外がインプットのパラメータになっているので、適当な`pclass`、`sex`、`age`を与えてみます。


```swift
let model = Titanic()
let youngMale = try model.prediction(pclass: 2.0, sex: "male", age: 20.0)
let youngFemale = try model.prediction(pclass: 2.0, sex: "female", age: 20.0)
let richOldMale = try model.prediction(pclass: 3.0, sex: "male", age: 80.0)
print("Young Male: ", youngMale.survived * 100.0)
print("Young Female: ", youngFemale.survived * 100.0)
print("Rich Old Male: ", richOldMale.survived * 100.0)
```

```console
Young Male:  54.45261909626424
Young Female:  91.35240849573165
Rich Old Male:  23.64654252305627
```

若い女の人のが生存率高いのはなんとなくわかる。金持ち(一番良いチケット)のおじいちゃんが低い感じなのもわかる。だいたい当たってるんじゃないですかね。(なげやり)

## まとめ

画像の分類、自然言語テキストの分類、汎用的なデータの推定、分類のみですがかなり簡単に学習モデルの生成ができます。iOSエンジニアからするとそれがXcode上で完結するのは魅力的ですね。

またモデルのサイズも今回全てが10〜100KB程だったので、アプリサイズを圧迫しないで済みそうです。ご自身のプロダクトに有効なデータがある場合は試してみてもいいんじゃないですかね。チャオ。