iOS
機械学習
WWDC
Swift
CreateML

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

実施環境

  • macOS 10.14+
  • Xcode 10.0+

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

Image Classification

画像の分類ができる学習モデルの作成方法です。画像に映ったものに対してそれが何なのかラベリングしてくれるモデルです。ドキュメント

データセットの準備

学習させたいクラスの画像データを用意して、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.

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

├── 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からスクリプトなりを書いてまとめて落とせばいいと思います。

トレーニング開始

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

import CreateMLUI

let builder = MLImageClassifierBuilder()
builder.showInLiveView()

fe6997f8-5507-4afd-9d16-d88f4ed8522b.png

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

b5e271a7-9041-463b-8a43-3fdb7395c221.png

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

77ba7e4d-8d95-4c8e-b81b-03429b7f0ae1.png

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

******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

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

実機で試すのであればこのサンプルのMLModelを入れ替えてビルドすればいいです。下の画像はサンプルアプリの画面です。

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

UNADJUSTEDNONRAW_thumb_1217.jpg

上記はMLImageClassifierBuilderでPlayground上で行なっていますが、本格的に導入する場合はBuilderが内部で使用しているMLImageClassifierを使ってmacOSアプリで運用するのがいいのかなと思いました。

Text Classification

自然言語テキストを分類できる学習モデルの作成方法です。文章のようなテキストが何を表しているのかラベリングしてくれるモデルです。ドキュメント

データセットの準備

JSONまたはCSVファイルからMLDataTableを生成します。または配列を扱うようにMLDataTableを直接編集することも可能です。

このMLDataTableMLDataColumnMLDataValueを持っています。ドキュメントを見るとわかるのですが、要は配列の中に辞書型の値を持つデータしか受け付けないようになっています。

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

こんな感じのデータです。少量のデータでは学習できなかったので、東北大学 乾・鈴木研究室 - 日本語評価極性辞書を使用させていただきました。(文章ではなく名詞なのでちょっと違うかもしれませんが細かいことは気にしません)

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

トレーニング開始

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

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を引数に渡してあげるみたいです。(いい感じで終わらせているけど、イテレーション回数とかどこで指定できるのですかね)

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

トレーニング中にはイテレーションの回数と、トレーニング用のデータから少量のバリデーションデータを用意して検証した結果がログ出力されます。更に詳しく見たい場合はMLTextClassifiertrainingMetricsvalidationMetricsがそれぞれトレーニングとバリデーションの詳しい結果であるMLClassifierMetricsを持っているので見るといいです。

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

// 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)
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なので検証結果が見れます。

// 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)
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/です。

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のサンプルコードを元に試してみます。MLModelはプロジェクトに突っ込んでください。

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
})
バチェラーにめっちゃハマってます。: neutral
小銭を投げる人ムカつく。: negative
ところでその手に持っているこんにゃくは何ですか?: neutral

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

Categorization and Quantity Estimation

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

データセットの準備

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

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

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

トレーニング開始

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

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で指定した行名が推定するターゲット(アウトプット)になります。その他の行は全て推定に使う値(インプット)になります。

▿ 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ってデータがあったので削除しました)

--------------------------------------------------------
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        |
+-----------+--------------+--------------------+----------------------+---------------+-----------------+

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

// 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)
Training Accuracy:  66.47823164748654
Validation Accuracy:  58.598195890306016
Evaluation Accuracy:  61.59430382847886

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

MLModelの書き出し

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

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を試すだけです。ターゲット指定した行以外がインプットのパラメータになっているので、適当なpclasssexageを与えてみます。

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)
Young Male:  54.45261909626424
Young Female:  91.35240849573165
Rich Old Male:  23.64654252305627

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

まとめ

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

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