フレクトのクラウドblog re:newal

http://blog.flect.co.jp/cloud/からさらに引っ越しています

Einstein Visionで微妙な判定の境界を探る

はじめに

エンジニアの佐藤です。こんにちは。 前回に引き続いて機械学習の話ですが。今回は Heroku Einstein Add-on について書かせていただきます。

ご存知の通り近頃はAIサービス関連のニュースを聞かない日はないほどです。ですが、この流行りに懐疑的な気持ちをお持ちの方も多いのではないでしょうか。実は筆者も、期待はしつつも楽観は危険と考える一人です。

遊びなら、失敗も愛嬌のうち。しかし失敗が「損失」に直結するビジネスでは、できるだけ失敗を避けたいと思うのが普通でしょう。では AIは本当に、十分簡単な課題なら確実に動作してくれるのでしょうか? 今回はこの点を確かめてみたいと思います。

今回紹介する Heroku Einstein Add-on はRESTインターフェイスで操作でき、機械学習の基本的な知識だけでモデルの実装までできます。詳細は他へゆずりますが、とても簡単に使えます。また、今回のような小規模用途であれば、完全無料で試すことができます。 Einstein Add-onの機能は大きく画像認識(Vision)と文書認識(Language)がありますが、今回は画像認識(Vision)を試してみます。

問題設定

画像認識のこの上なく単純な問題として、「boxかbarか」を考えてみます。これは辺の比率によって長方形を2つに分類し、細長い方をbar、他方をboxと判定させるものです。boxとは

f:id:masashi-sato-flect:20180331142540p:plain

で、barとは

f:id:masashi-sato-flect:20180331142605p:plain

です。比率の境界は1:2と決め、これと等しいか、より細長いものはbar、ほかはbox とします。背景は水色、長方形は黒です。回転なし、歪みなしの理想系でやってみましょう。これ以上ない簡単な分類と思いますが、さてEinstein Visionは 「十分簡単だから完璧に」 動作するでしょうか?

トレーニングデータと検証データ

今日流行りのAIは、大規模な機械学習でこれまで不可能だった新機能を実現するものです。機械学習とは前回ブログで書いた通り「データでアルゴリズムを削り出す作業」ですからデータが必要です。

では今回のデータとは?それは様々な比率の長方形以外にはありません。以下のようなプログラムで自動生成します。

from PIL import Image, ImageDraw

for i in range(200):
    img = Image.new('RGB', (400, 200), '#7FFFFF')

    x1 = 50
    y1 = 50
    x2 = 150 + i
    y2 = 150

    draw = ImageDraw.Draw(img)
    draw.rectangle(
        [(x1, y1), (x2, y2)],
        '#000000'
    )

    label = 'box' if i < 100 else 'bar'
    
    if i % 10 == 0:
        fileName = 'eval{:03d}.png'.format(i)
        self.__save_eval(img, label, fileName)
    else:
        fileName = 'train{:03d}.png'.format(i)
        self.__save_train(img, label, fileName)
# end of for (i)

400px x 200px の下地の上に 50px のマージンを確保し、高さは 100px固定、幅は 100pxから 300pxまで 1px刻みで変化させます。 i = 100 で比率が 1:2になりますので、これより前は「box」、これ以降は「bar」とラベル付けします。 また、 10回に 1回、つまり全 200画像の 20個を、テスト用データとしてトレーニングからはよけておきます。

f:id:masashi-sato-flect:20180331142642p:plain

詳しい手順はリファレンスにゆずりますが、このトレーニング画像集を zip形式でアーカイブし、あとは curlコマンドでちょこちょことやれば画像認識の機械学習モデルができあがります。単純極まるデータのためトレーニングも 5分もかからず終了。非常に簡単です。

トレーニング経過とモデルの確認

「非常に簡単」と書きましたが、 油断してはいけません。 これは遊びではなく、ビジネスなのです。およそコンピュータと名のつくもの、何物も疑いの目で見なければいけません。

最初にモデルのメトリクスを確認します。

curl -X GET -H "Authorization: Bearer $EV_TOKEN" -H "Cache-Control: no-cache" https://api.einstein.ai/v2/vision/models/[MODEL_ID] | jq .
{
  "metricsData": {
    "f1": [ 1,  1 ],
    "labels": [ "bar", "box" ],
    "testAccuracy": 1,
    "trainingLoss": 0.5564,
    "confusionMatrix": [
      [ 7, 0 ],
      [ 0, 8 ]
    ],
    "trainingAccuracy": 0.6963
  },
  (一部省略)
}

Einstein Visionのトレーニングでは、トレーニング用にアップロードされたデータのランダム選抜された10%がトレーニング結果の評価のために使われます。その結果は「testAccuracy: 1」つまり、全問正解だったとあります。他にもいろいろありますが、ひとまずは良さそうです。

次は学習曲線です。

curl -X GET -H "Authorization: Bearer $EV_TOKEN" -H "Cache-Control: no-cache" https://api.einstein.ai/v2/vision/models/[MODE_ID]/lc | jq -r -f lc.jq

lc.jqは Einstein Vision RESTレスポンスの調整に使用したもので、以下のような内容です。 .data[] | "(.epoch), (.metricsData.trainingAccuracy), (.metricsData.testAccuracy)"

epoch, trainingAccuracy, testAccuracy
1,  0.665,  1
2,  0.6978, 0.9333
3,  0.6909, 1
4,  0.7021, 0.9333
5,  0.6812, 1
6,  0.7095, 0.9333
7,  0.7075, 0.9333
8,  0.7021, 0.9333
9,  0.6963, 1
10, 0.7002, 0.9333
11, 0.7061, 0.9333

Epoch 1からいきなりtestAccuracyが 1(最高値)になり、以後ほとんど変化しません。 要するにトレーニングは「超 楽勝だった」ようです。 本当でしょうか?

本番テスト

では、今度はトレーニングに使用していない検証用データで判定させてみましょう。結果は以下のようになりました。

/eval/box/eval000.png  box:0.83328694 bar:0.16671309 
/eval/box/eval010.png  box:0.8120093  bar:0.18799073 
/eval/box/eval020.png  box:0.79243547 bar:0.20756452 
/eval/box/eval030.png  box:0.7771254  bar:0.22287455 
/eval/box/eval040.png  box:0.74343425 bar:0.25656578 
/eval/box/eval050.png  box:0.7180637  bar:0.28193632 
/eval/box/eval060.png  box:0.66442096 bar:0.33557907 
/eval/box/eval070.png  box:0.64682674 bar:0.35317332 
/eval/box/eval080.png  box:0.5968405  bar:0.4031595 
/eval/box/eval090.png  box:0.5487534  bar:0.45124662 

/eval/bar/eval100.png  bar:0.5115235  box:0.48847654 
/eval/bar/eval110.png  bar:0.55833775 box:0.44166228 
/eval/bar/eval120.png  bar:0.6156298  box:0.38437027 
/eval/bar/eval130.png  bar:0.5786646  box:0.42133546 
/eval/bar/eval140.png  bar:0.8948712  box:0.105128884 
/eval/bar/eval150.png  bar:0.8948712  box:0.105128884 
/eval/bar/eval160.png  bar:0.8948712  box:0.105128884 
/eval/bar/eval170.png  bar:0.8948712  box:0.105128884
/eval/bar/eval180.png  bar:0.8948712  box:0.105128884 
/eval/bar/eval190.png  bar:0.8948712  box:0.105128884 

雑ですいませんが、一番上が正方形(前述の画像作成コードで i = 0 で作成された画像)で、以後下へ行くほど長方形の長辺が伸長し、/eval/bar/eval100.png の時にちょうど 短辺:長辺 = 1:2 になります。

判定結果は、確かに完璧です。 しかも boxと barの境界である 短辺:長辺 = 1:2 に近づくほど、 boxラベルと barラベルの probabilityが拮抗し(つまり判定の確からしさが減少し)、我々人間が感じる感触に近くなっていて親近感がありますね。。。 興味深いのは、「文句なく細長く」なった /eval/bar/eval140.png以降は probabilityの変化がないことです。これはいわば「残りの領域は見る必要がない」という感じでしょうか(筆者の主観ですが)。

それでも疑ってみる

しかし、、、疑い深い筆者は考えてしまいます。これは何かの偶然ではないのかと。そこで もう一度、完全に同じデータでトレーニングとテストをやり直して見ます。

結果は以下のようになりました。

2つ目のモデルのメトリクス

{
  "metricsData": {
    "f1": [ 1, 1 ],
    "labels": [ "bar", "box" ],
    "testAccuracy": 1,
    "trainingLoss": 0.5796,
    "confusionMatrix": [
      [ 7, 0 ],
      [ 0, 8 ]
    ],
    "trainingAccuracy": 0.6826
  },
  (一部省略)
}

学習曲線

epoch, trainingAccuracy, testAccuracy
1,  0.6367, 0.8667
2,  0.6934, 1
3,  0.6826, 1
4,  0.6904, 0.9333
5,  0.6753, 0.9333
6,  0.6982, 0.9333
7,  0.6992, 0.9333
8,  0.6904, 0.8667
9,  0.6851, 0.8667
10, 0.6963, 0.9333
11, 0.7148, 0.9333

テスト結果

/eval/box/eval000.png  box:0.7758191  bar:0.22418085 
/eval/box/eval010.png  box:0.7284226  bar:0.27157742 
/eval/box/eval020.png  box:0.6806543  bar:0.31934574 
/eval/box/eval030.png  box:0.6620312  bar:0.33796886 
/eval/box/eval040.png  box:0.6430078  bar:0.35699224 
/eval/box/eval050.png  box:0.61368895 bar:0.38631108 
/eval/box/eval060.png  box:0.57791686 bar:0.4220831 
/eval/box/eval070.png  box:0.5619922  bar:0.43800783 
/eval/box/eval080.png  box:0.5478586  bar:0.4521414 
/eval/box/eval090.png  box:0.5113313  bar:0.48866868 

/eval/bar/eval100.png  bar:0.51804864 box:0.4819514 
/eval/bar/eval110.png  bar:0.5393514  box:0.4606486 
/eval/bar/eval120.png  bar:0.55827296 box:0.44172707 
/eval/bar/eval130.png  bar:0.56668216 box:0.43331784 
/eval/bar/eval140.png  bar:0.8327772  box:0.16722284 
/eval/bar/eval150.png  bar:0.8327772  box:0.16722284 
/eval/bar/eval160.png  bar:0.8327772  box:0.16722284 
/eval/bar/eval170.png  bar:0.8327772  box:0.16722284
/eval/bar/eval180.png  bar:0.8327772  box:0.16722284 
/eval/bar/eval190.png  bar:0.8327772  box:0.16722284 

比べてみると、全体的傾向は同じですが、個々の数字は微妙に違います。

数値をExcelでプロットして見ましょう。

学習曲線については、差は小さいですが、

f:id:masashi-sato-flect:20180331142724p:plain

意外と違っていたのがテスト時の probability値です。

f:id:masashi-sato-flect:20180331142738p:plain

どちらも boxと barの判定は期待通りですが、プロットしてみると 2つのラベルで probabilityが反転している点が異なり、 bar1 / box1の交点は bar2 / box2の交点より x値が大きいところにあります。

なぜこのような違いが生じるのでしょうか?筆者は主に以下の2つの要因があると考えています。

トレーニングのランダムの結果が異なるから

一般的にディープラーニングのトレーニングでは乱数を利用するステップがいくつかあり、乱数の結果によってトレーニングの進行が異なります。もちろん、 トレーニングデータが十分にあればこれらの結果による影響は少なく、トレーニング結果は一定の範囲に収束するはずです。

トレーニング結果の評価のためのデータの選抜結果が異なるから

トレーニングに再しては、アップロードされたデータの10%をトレーニング評価用に使いますが、この選抜方法はランダムであり、指定することはできず、選抜結果を取得することもできません(本稿執筆時点)。したがって、今回180画像アップロードした画像のどれがトレーニング評価用に選択されたかで、 barと boxの probabilityを反転させる最適ポイントが変わります。 したがって、今回の場合 テストデータの 10番と 11番の周辺に、トレーニング評価用のデータが 1つもなかった場合は、テストデータの 10番か 11番のどちらかの判定に失敗していた可能性があると思います。 しかしこういうケースの発生確率は小さく、 たいていは 大丈夫なのです。

結論として、「boxかbarか」という本課題については、 Einstein Visionは十分確実な仕事をしてくれると言えそうです。 ですが、現実の画像判定ソリューションの場合、トレーニングデータもテストデータももっと複雑であり、その内容と評価基準について今回のように考察することはほとんど不可能でしょう。結局のところ、本番で高確率で成功したければ、「実戦に近く、かつ、豊富な経験」しかないという、言われるまでもない自明の真理に戻ってきたような気がします。AIと言えど、マジックはないようです。

最後までお読みいただき、ありがとうございました。