ぱたへね!

はてなダイアリーはrustの色分けができないのでこっちに来た

AutoMLで破産しないように気をつけたいポイント

はじめに

AutoMLを使っていたら一気に26万円の請求が来ました。 他の人が同じ事故にあわないようにやってしまったことを書きます。

やったこと

前回、こんな記事を書きました。

AutoML Vision Edgeが作るニューラルネットワーク

この続きで、物体検出のAutoMLを評価してみようと思いました。VOC2012のデータを用意してAutoMLへかけてみました。トレーニングの前に物体検出の方はtfliteへの変換が選べないことが分かったのですが、トレーニングさえ終わらせておけば何か方法がみつかるかもしれずと思い、精度優先と速度優先の2つのオプションでAutoMLを実行してしまいました。

前回の識別では3時間程度で学習が終わっており、今回も無料期間から足が出ても少しかなとたかをくくってました。

費用の内訳はこうなっています。

  • AutoML Image Object Detection Model Training 14万
  • AutoML Image Object Detection Online Prediction 11万

ここから無料枠分が差し引かれ、消費税が8%乗っています。

AutoML Image Object Detection Model Training

まずはトレーニングにかかった費用です。上に書いたとおり、比較をしたかったので2つの設定でトレーニングを回しています。トレーニングが2回で420時間かかっています。

トレーニングの価格体系はこちらに記載してあります。

https://cloud.google.com/vision/automl/pricing?_ga=2.243008706.-112799687.1556789167

5000枚程度なら5時間とあり、VOC2012が約27000枚なので、24時間見ておけばよいかなと甘く見ていました。

結果は、2回のトレーニングで420時間動いてしまい、合計で14万円の請求になりました。

AutoML Image Object Detection Online Prediction

こちらは識別のAutoML時にはかからない費用だったので、完全に見落としていました。訓練時にデプロイをするかどうかにチェックを入れることができます。訓練した後、絵を入れてみたいのであまり気にせずチェックしてしまいました。これも間違いでした。

上のリンク先に太字で「予測が行われない場合であっても、デプロイしたノード単位での料金が発生します。」と書いてあるように、デプロイするだけでGPUノードを1つキープし続けます。今回2つ訓練をしたので2ノードをキープし続けました。

こちらが11万円です。

よくある言い訳

  • 警告のメールは来ていたが、完全に見落としていた。
  • 請求のルールが識別のAutoMLと同じだと思っていた。
  • 仕事が忙しくて、とりあえず投機的にAutoML回しておこうと考えてしまった。結果の確認が遅れた。

反省点

  • 警告メールはいくつかの閾値で定義し、わざと警告を出させてちゃんとチェック機能が働くか確認した方がよい。
  • 仕事が忙しい時に青天井に費用が出ていくことに挑戦しない方がよい。余裕を持って成果と費用をチェックできるときにやろう。
  • AutoMLは会社のお金で回そう

最後に

今回は悲しい結果になってしまいましたが、AutoMLはリサーチャをかかえられない企業に取っては救世主になると信じています。
費用に関しては改良して欲しいです。「ユーザーがオペレーションをキャンセルした場合は、課金されます。」と書いてあるとおり、いつ終わるか予想ができないのに、途中で止めると費用は発生しているのにモデルが無いという状況になります。費用なり、時間なりでリミットを決められないと怖くて使えないです(今回は200時間だったが、Googleがその気になれば1000時間でも2000時間でも回せる)。 VOC2012のような有名なデータセットを例に、料金や時間の目安は事前に案内があっても良かったと思います。

最後に、物体検出もエッジで動くようにして欲しいです。

AutoML Vision Edgeが作るニューラルネットワーク

AutoML Vision Edgeがどのようなニューラルネットワークを作るのかが気になったので調べてみました。 Kaggleの犬猫分類(画像による2クラス分類)をAutoML Vision EdgeでTensoflow Liteのモデルを作り、ネットワーク構造を読み出して分析しました。まだ、完全に理解したわけではないので間違っている所もありそうでうが、雰囲気がつかめたのでまとめてみます。

準備

データを用意する。

ここから、犬と猫の画像をそれぞれ2000枚選びました。
https://www.kaggle.com/c/dogs-vs-cats

データをAutoMLで扱えるようにする。

この2つを見ればだいたいわかります。

ディレクトリに分けた画像をzipにして4000枚アップロードしたところ、ディレクトリ構造は認識してくれず、結局手で4000枚ラベル付けしました。ラベル付けのUIはあまり使いやすくなかったです。まずは500枚くらいアップロードして、作戦を考えた方が良いでしょう。今回であれば、先に犬の画像を2000枚アップロードし全てにdogのラベルをつけた後、猫の画像を2000枚アップロードして未分類の物を全てcatにすれば楽でした。自力でラベル付けした時点で少なくとも10枚は間違っていたのですが、今回は精度はあまり興味が無いのでこのまま進みました。

モデルをトレーニングする。

トレーニングの方法はここの通りに進めればOKです。
https://qiita.com/shinkoizumi0033/items/4ae75e0bebb2848cbd54

上で準備した同じデータセットに対して、Lowest latency、Best Trade-off、Higher accuracyの設定でそれぞれトレーニングしました。トレーニングは1時間半程度(1時間では終わらなかった)で終わり、メールが届くのでそのリンク先を開くと結果がみれます。

f:id:natsutan:20190504101230p:plain

精度が悪いのは、上に書いたようにデータセットがすでに間違っているからだと思います。

モデルをダウンロードする。

訓練が終わった後、、AutoML Visionの画面からモデルを選択し、PREDICTのタブをクリックします。

f:id:natsutan:20190504101340p:plain

この画面でEXPORTをクリックすると、GCP上にデータがエクスポートされます。gsutilを使ってローカルへ持ってきましょう。

TFLiteのモデルをJSONへ変換する。

TFLiteモデルの中身は、flatbufferです。

github.com

flatbuffersをインストールして、flatc コマンドを使えるようにします。

フォーマットの定義ファイルは、wgetで持ってきましょう。

wget https://raw.githubusercontent.com/tensorflow/tensorflow/master/tensorflow/lite/schema/schema.fbs

flatcコマンドが動けばJsonファイルができるはずです。

flatc --json --defaults-json  --strict-json  schema.fbs  -- XXXX.tflite

JSONファイルを読み込んでdotファイルを作る。

ここは自作ツールでやりました。ONNXで同じようなことをしていれば簡単にできると思います。そんなに難しくないです。需要があれば公開します。

dotファイルができれば、pngやPDFに変換して完了です。

オプションによるモデルの比較

モデルの大きさ比較

Lowest latency Best Trade-off Higher accuracy
TFliteファイルのサイズ(Kb) 548 3,179 5,851
推論速度の見積もり(ms) 22 65 105
計算のサイズ UINT8 UINT8 UINT8
層数 65 64 64

TFliteの中身を見てみましたが特に圧縮されている様子も無く、ファイルサイズがほぼパラメータのデータサイズと思って良さそうです。データの持ち方は全モデルでUINT8でした。意外にもレイヤ数はほぼ同じでした。

可視化したニューラルネットワーク

とりあえず3つ並べてみました。

f:id:natsutan:20190504104628p:plain:w200 f:id:natsutan:20190504104625p:plain:w200 f:id:natsutan:20190504104628p:plain:w200

ニューラルネットワークの基本構造

3つのモデルの構造を見てみました。基本的な構造は3つとも同じのようにみえます。

f:id:natsutan:20190504101835p:plain

ResNetに似た構造が、10~11個シーケンシャルにつなげています。これだけで5層あるので5×10で50層、最初、真ん中、最後に少し違う構造が入っていて、合計で64層のニューラルネットワークを構成しています。
Lowest latency が11個、残りの2つが10層で全体のニューラルネットワークを構成しているので、微妙なところではモデルに違いがありそうです。これが、速度、精度との問題なのか、AutoMLで使われているであろう乱数の結果による違いなのかは分かりません。もう少し、いろんなデータで試す必要がありそうです。

入力層直後を除き、2DConvが全て1x1のみで、5x5等はDepthwiseConv2Dでのみ出てくるところが面白いです。

データ数の比較

3つのモデルについて、先頭と最後のデータ数(重みの数)を比べてみました。Best Trade-offとHigher accuracyは構造が同じで、パラメータの数だけの違いに見えます。Lowest latencyはネットワーク構成が違い、パラメータを大きく減らしています。Best Trade-offの半分以下です。

layer Lowest latency Best Trade-off Higher accuracy
0 Conv2D 3x3 16 Conv2D 3x3 32  Conv2D 3x3 48
1 DConv2D 3x3 16 DConv2D 3x3 32  DConv2D 3x3 48 
2 Conv2D 8 Conv2D 16  Conv2D 24 
3 Conv2D 24 Conv2D 48  Conv2D 72
4 DConv2D 3x3 24 DConv2D 3x3 48  DConv2D 3x3 72
5 Conv2D 8 Conv2D 24  Conv2D 24
6 Conv2D 24 Conv2D 72  Conv2D 72 
7 DConv2D 3x3 24 DConv2D 3x3 72 DConv2D 3x3 72
8 Conv2D 8 Conv2D 24  Conv2D 24
9 Add Add   Add
10 Conv2D 24 Conv2D 72  Conv2D 72
 
50 Add Conv2D 1152  Conv2D 1632
51 Conv2D 384 DConv2D 5x5 1152  DConv2D 5x5 1632
52 DConv2D 5x5 384 Conv2D 192  Conv2D 272
53 Conv2D 64 Add  Add
54 Add Conv2D 1152   Conv2D 1632
55 Conv2D 384 DConv2D 5x5 1152  DConv2D 5x5 1632
56 DConv2D 5x5 384 Conv2D 192  Conv2D 272
57 Conv2D 64 Add   Add
58 Add Conv2D 1152  Conv2D 1632
59 Conv2D 384 DConv2D 3x3 1152   DConv2D 3x3 1632
60 DConv2D 3x3 384 Conv2D 320   Conv2D 448
61 Conv2D 112 Conv2D 1280   Conv2D 1280
62 Conv2D 1280 Reducer   Reducer
63 Reducer FC  FC
64 FC SoftMax   SoftMax
65 SoftMax  

こうやって並べてみると、データ数が8の倍数になっているのは分かります。例えば24個のUINT8(8bit)のデータを22個にしても、速度への影響はあまりないのだと思います。ギリギリで削るというよりは、いくつかの組み合わせを試しているように見えます。おそらく、エッジ動作での速度を確保するために、クラウドで使用するAutoMLよりは制約がきつくなっていると感じます。4x4みたいなカーネルの方が精度が高かったとしても、エッジ側のライブラリが3x3と5x5にしか対応していなければ、速度がでないとかの事情はありそうです。

上には書いていないですが、アクティベーション層が全て計算が少ないReLUになっているのもエッジ動作を想定してのことだと思います。

量子化

ちゃんと調査したわけでは無いのであくまで予想ですが、量子化は2種類のフォーマットを組み合わせています。

ここに詳しく説明されていますが、まだ全部みれてません。

heartbeat.fritz.ai

A typical quantized layer

一般的なTF Liteで使われる量子化フォーマット

          "quantization": {
            "min": [
              -22.376556
            ],
            "max": [
              23.63975
            ],
            "scale": [
              0.180456
            ],
            "zero_point": [
              124
            ],

Fake Quantization

おそらく、単純にかけ算をするだけのフォーマット。

          "quantization": {
            "scale": [
              0.011161
            ],
            "zero_point": [
              0
            ],

他に気になったこと

定番のMaxpoolingやBatchNormalizationが入っていないのも気になりました。Maxpoolingに関してはResNetの構造を上手く使っているので必要なさそうですが、BatchNormalizationはあまり効果がないのかConvの計算に組み込まれて(Fuse)いるのかは良く分かりません。

まとめ

  • AutoML Vision Edgeで作ったモデルは、JSONに変換して構成を見ることができる。
  • 今回のケースでは、ResNetっぽいNNを生成している。いくつかの組み合わせから選んでいる様子。
  • エッジでの計算早くなりそうなレイヤーを組み合わせている。
  • Lowest latencyは層数こそ変わらないが、かなり重みの数を減らしている。

次は物体検出が、どのようなネットワークで構成されているかをやってみたいです。

Rustでnomを使ってみた

nomはrust用のパーサコンビネータです。 https://github.com/Geal/nom/

Qiitaの記事を見て使ってみましたが、小一時間ハマったので結果をメモ

元ネタ https://qiita.com/k5n/items/e95842744fc5db931d03

このサンプルが最新のnomでは動きませんでした。

Cargo.toml

[dependencies]
nom = "^4.2"

とりあえずchain!が影も形もなくて、do_parse!で置き換え、結果の受け取り方も少し変わっています。

#[macro_use]
extern crate nom;

use nom::{space, alpha};

named!(name_parser<&str>,
    do_parse!(
        tag!("Hello,") >>
        space >>
        name: map_res!(alpha, std::str::from_utf8) >>
        (name)
    )
);

fn main() {
    match name_parser("Hello, world!".as_bytes()) {
        Ok(name)  => println!("name = {:?}", name),
        _ => println!("Error")
    }
}

これを実行すると、ちゃんと動いているようです。

name = ([33], "world")

機械学習の説明

今日来たPiqcyから Attention is not Explanation の紹介に反応。 https://arxiv.org/abs/1902.10186

「自然言語処理においてAttentionはよくモデルの判断根拠として用いられるが、本当に説明になっているのかを検証した研究。結果として、GradientベースのスコアとAttentionは乖離があり、またAttentionの分布が異なるよう変更しても予測結果を維持することができることを確認」

意味づけとか解釈の話は、それで一つの研究ジャンルって所まで分かってきた。GRAD-CAMはいろんなアルゴリズムの中でも、意味づけするための仕組みが不要、計算量が少ない事がメリットで、正確さ(これもいろいろ方向がある)はそれなりのアルゴリズムっぽい。

この本が分かりやすく勉強中。 https://christophm.github.io/interpretable-ml-book/

TPUがサポートしていないOPをTensorboardで調べる

TPUを使うと良く謎のエラーに悩まされます。

RuntimeError: Compilation failed: Compilation failure: Detected unsupported operations when trying to compile graph cluster_666512355060360340[] on XLA_TPU_JIT: Placeholder

どのoperationをTPUがサポートしていて、そうでないのかが良く分からず悩んでました。 Tensorboardを使うと、どのoperationがTPUをサポートしているのかを教えてくれます。

やり方は簡単で、tensorboardのgraphを表示させた状態で、TPU compatibilityをクリックするだけです。

f:id:natsutan:20190119100635p:plain

この例ですと6のoperationがサポート対象外です。サポート対象外であってもCPUで動かせる時もあるので全部TPU compatibilityにする必要は無いのですが、エラーの解決やパフォーマンス向上には役に立ちますね。

GCP, TPU, Tensorflow, Tensorboard

ローカルで動いたTensorflow環境をTPUに持って行くとすんなり動かない。動いた物を集めながら、差分を少しずつでも調べていかないと前に進まない感じがする。少しずつ環境をつくっています。やりたいことがいっぱいあるがもどかしい。

TPUを使う

Kerasを使わずにTPUを使ったサンプル。これはあっさりと動きました。

import os
import tensorflow as tf
from tensorflow.contrib import tpu
from tensorflow.examples.tutorials.mnist import input_data

MAX_EPOCH = 50

mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)
num_input = 28 * 28 * 1
num_classes = 10

x_ = tf.placeholder("float", shape=[None, num_input], name='X')
y_ = tf.placeholder("float", shape=[None, num_classes], name='Y')

is_training = tf.placeholder(tf.bool)
x_image = tf.reshape(x_, [-1, 28, 28, 1])

conv1 = tf.layers.conv2d(inputs=x_image, filters=32, kernel_size=(5, 5), padding='same', activation=tf.nn.relu)
pool1 = tf.layers.max_pooling2d(inputs=conv1, pool_size=(2, 2), strides=2)
conv2 = tf.layers.conv2d(inputs=pool1, filters=64, kernel_size=(5, 5), padding='same', activation=tf.nn.relu)
pool2 = tf.layers.max_pooling2d(inputs=conv2, pool_size=(2, 2), strides=2)

pool2_flat = tf.reshape(pool2, shape=[-1, 7 * 7 * 64])
dense = tf.layers.dense(inputs=pool2_flat, units=1024, activation=tf.nn.relu)
dropout = tf.layers.dropout(inputs=dense, rate=0.4, training=is_training)
logits = tf.layers.dense(inputs=dropout, units=10)

# 誤差関数
cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(logits=logits, labels=y_))
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)


correct_prediction = tf.equal(tf.argmax(logits, 1), tf.argmax(y_, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

# TPU
tpu_grpc_url = tf.contrib.cluster_resolver.TPUClusterResolver(tpu=[os.environ['TPU_NAME']])

strategy = tf.contrib.tpu.TPUDistributionStrategy(tpu_grpc_url)

init = tf.global_variables_initializer()

tf.summary.scalar('softmax_cross_entropy', cross_entropy)
tf.summary.scalar('accuracy', accuracy)

merged = tf.summary.merge_all()

with tf.Session() as sess:
    with tf.name_scope('summary'):
        writer = tf.summary.FileWriter('./logs_tpu', sess.graph)

        sess.run(init)

        for i in range(MAX_EPOCH):
            batch = mnist.train.next_batch(50)

            train_accuracy = accuracy.eval(session=sess, feed_dict={x_: batch[0], y_: batch[1], is_training: True})
            print("step = %d, training accuracy = %g" % (i, train_accuracy))

            s = sess.run(merged,  feed_dict={x_: batch[0], y_: batch[1], is_training: False})
            writer.add_summary(s, i)

            train_step.run(session=sess, feed_dict={x_: batch[0], y_: batch[1], is_training: False})
            print("Test Accuracy:", sess.run(accuracy, feed_dict={x_: mnist.test.images, y_: mnist.test.labels, is_training: False}))


tpu.shutdown_system()

Tensorboardを使う

ここを参考に6006のfirewall ruleを追加することで、GCPでの学習結果をtensorboardで確認できます。 https://qiita.com/tk_01/items/307716a680460f8dbe17

f:id:natsutan:20190118205846p:plain f:id:natsutan:20190118205849p:plain

どうやら上手く学習できているようです。

PythonとKerasによるディープラーニング

PythonとKerasによるディープラーニング

PythonとKerasによるディープラーニングを読みました。Kerasの作者が書いた本だけあって、非常に分かりやすく書かれています。Kerasの楽できる関数群をフルに使って、短い記述で定番のニューラルネットワークを動かすことができます。単にニューラルネットワークの説明だけでなく、データの内容を確認したり、学習結果をグラフにプロットしたり、もう半歩先まではコード付きで解説があります。

本のレベルとしては、最初の一歩の次に進める本です。CNNを使った識別や、LSTMを使った未来予測など、やりたいことがはっきりしているのであれば、ショートカットとしてとても役に立ちます。Pythonこれからという人には、ちょっと厳しいと思います。

この本を読んで手を動かしていけば、例えばニューラルネットワークがどこを見て猫と判断しているのかをわかりやすくするヒートマップも、Kerasを使って簡単にできるようになります。

f:id:natsutan:20190105225346j:plain
heatmap

こんな感じですね。識別の部分も含めて、これくらいのコードで実装できます。

model = VGG16(weights='imagenet')

preds = model.predict(x)
print('Predicted:', decode_predictions(preds, top=3))

last_conv_layer = model.get_layer('block5_conv3')
cat_output = model.output[:,386]
grads = K.gradients(cat_output, last_conv_layer.output)[0]

pooled_grads = K.mean(grads, axis=(0,1,2))
grads = K.function([model.input], [pooled_grads, last_conv_layer.output[0]])

iterate = K.function([model.input], [pooled_grads, last_conv_layer.output[0]])

pooled_grads_value, conv_layer_value = iterate([x])

for i in range(512):
    conv_layer_value[:, :, 1] *= pooled_grads_value[i]

heatmap = np.mean(conv_layer_value, axis=-1)
heetmap = np.maximum(heatmap, 0)
heatmap /= np.max(heatmap)

一方、Kerasの弱点として、型から外れると使いにくいというのもあり、ちょっと難しいことをするとこの一冊だけではきついです。

ちょうど昨年末にQiitaに闇のkerasに対する防衛術という記事が上がってました。Kerasが想定していることと違う事をしようとすると、まだまだ情報が足りなかったり、見つけた情報も更新が速すぎて古かったりします。何か新しいことをするというレベルではなく、まず手持ちのデータで識別から始めたいという人におすすめします。