2022.01.04

GCP Vertex AI + Firebase MLを利用したモバイルApp上での機械学習モデル利用

次世代システム研究室のT.Sです。機械学習を活用したアプリケーションが当たり前になってきた中で、その活用の様々となってきましたね。昔は迷惑メールフィルタリングなどが身近でしたが、最近ではサーバサイドだけでなく身近なモバイルApp上で動作するようにと本当に様々です。そこで今回は機械学習エンジニアがあまり触ることのないモバイルApp(android)上で動作する機械学習モデルの利用方法を、GCPの機械学習最新サービスであるVertex AIを絡ませてお話しようかと思います。

利用するサービス

今回作成する検証モバイルAppはこのようなイメージとなります。


ここでは大きく以下の3つの技術要素・サービスを利用していまうが、まず詳細な技術内容をお話する前に、先立って概要だけご紹介いたします。

Vertex AI

Vertex AIとは、Google Cloud Platformが提供する機械学習に必要な機能が揃った一元的なプラットフォームサービスのことです。「一元的」という言葉が指すとおり、データの準備から訓練、モデル評価、API提供、監視まで一通りそろっています。モバイルAppで機械学習モデルを提供する際には、いろいろな活用パターンがあるのですが、今回はモデルの訓練で利用させてもらいます。

Firebase ML

FirebaseMLとは、Web/モバイルAppのための総合的プラットフォームであるFirebaseに用意された機械学習用サービスのことです。APIとして利用できる機械学習サービス(文字認識など)もあるのですが、今回は独自で学習した機械学習モデルを配布するために利用させてもらいます。

Tensorflow Lite

TensorflowLiteとは、機械学習ライブラリであるTensorflowをモバイル端末やIoT機器で動作させられるように軽量化したものとなります。こちらはモバイル端末上で利用するのですが、Kotlin+FirebaseMLライブラリで動作させる場合は本家tfと違う点があるため、そこを見てもられればと思います。

コーディング内容

学習用コード作成

今回はカスタムモデルを利用するため、まずは機械学習モデルを訓練しなければいけません。そこでVertex AIを使って簡単な訓練を実施してみましょう。今回は簡単のためMNISTを使った0-9の数字認識モデルを作成します。

まずは訓練のためのpythonコードを用意します。今回はMNISTを利用するものであり、非常に基礎的なため詳細は割愛していますがよくあるTensorflowのPython
コードとなっていますので詳細は割愛しています。mnistに限らずよくあるデータ取得→model.fitしてモデル学習するものだというイメージでいただければ相違ないかと思います。
...

mnist = keras.datasets.mnist
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()

...

model.fit(train_generator, epochs=5, validation_data=test_generator)
ちなみにですが、今回はkeras.datasets.mnistを使っているため、train/testセットをそのまま利用していますが、VertexAI datasetを利用する場合以下のような形で取得することも可能です。これをVertex Pipelinesで実行すると、指定した割合でBQ上に分割テーブルが作成され、そのURIを取得してくるため非常に便利に実行できたりもします
# 与えられたURIをbigquery.TableReference.from_stringで取得->df変換する
train_images = os.environ["AIP_TRAINING_DATA_URI"]
test_images = os.environ["AIP_TEST_DATA_URI"]
ここからTensorflow Lite+Firebase ML対応を訓練コードに加えていきます。通常Vertex AIで学習した際にはモデルもVertex AI Modelで取り扱い、これをendpointなどでDeployして利用できる形式にします。アプリで取り扱う際でも外部APIとして提供する場合はこれでいいのですが、今回はアプリに取り込む前提で進めたいと考えています。そうなるとFirebaseに取り込む必要が出てきます。そのため作成したモデルをFirebase Adminを使って取り込んでいきましょう!
# gcloud authは別途実施
import firebase_admin
from firebase_admin import ml
from datetime import datetime
from dateutil import tz

firebase_admin.initialize_app(options={'projectId': PROJECT_ID, 'storageBucket': BUCKET_ID })

#現在日付を取得してファイル名生成
JST = tz.gettz('Asia/Tokyo')
d = datetime.now(JST).strftime('%Y%m%d%H%M%S')
model_name = f'mnist_{d}.tflite'

# kerasモデルからtensorflow liteに変換し、GCSにアップロードする
fb_source = ml.TFLiteGCSModelSource.from_keras_model(model, model_name)

# TFLiteFormatを表すクラスで定義
fb_format = ml.TFLiteFormat(model_source=fb_source)

# FirebaseML Modelクラスに変換。ここでFirebase上の表示名を加える
fb_model = ml.Model(display_name=model_name, model_format=fb_format)

# 現在のFirebase Projectに該当モデルを作成
fb_model = ml.create_model(fb_model)

# Publishしてアプリが使える状態にする
ml.publish_model(fb_model.model_id)
ちなみにですがfirebaseMLを使わず単にTensorflowLiteのモデルファイルを作成する場合は以下のようなコードで実施します。開発環境で試したい場合なんかはこれで作って取り込んだほうが楽でいいですね。
# kerasモデルをtensorfrolw lite形式に変換
converter = tf.lite.TFLiteConverter.from_keras_model(model)

# 軽量化(量子化)する際の圧縮方法指定。精度やサイズに影響を与える
# 今回は特に指定はないためデフォルト指定
converter.optimizations = [tf.lite.Optimize.DEFAULT]

# Convert実施->ファイルOut
tflite_model = converter.convert()
with open(model_name, "wb") as f:
  f.write(tflite_model)

学習用カスタムコンテナ作成

さあこれでコードができたのでVertex AI上で実行しましょう!!

…とその前に次はこれを実行するためにコンテナを作成します。ローカル実行はこれで十分実行できるのですが、今回Vertex AI Pipelinesを使う都合上Dockerコンテナが必要となります。バックエンドがkubeflowであることもあり、かつ機械学習で必要なライブラリ群+GPU利用可否を設定するのにコンテナ化したほうが何かと都合がよいこともあります。

実行方法は通常のコンテナ作成と大差ありません。詳しくはこちらを見ていただければと思いますが、実行用DockerfileをBuildしてGoogle Container Registryに登録するだけになり、特に変わった作業は必要ありません。

Dockerfile:
FROM gcr.io/deeplearning-platform-release/tf-cpu
WORKDIR /

COPY train.py /train.py

RUN pip install firebase_admin

ENTRYPOINT ["python", "-m", "train"]
command:
export IMAGE_URI=us-central1-docker.pkg.dev/PROJECT_ID/REPO_NAME/IMAGE_NAME:latest
docker build -f Dockerfile -t ${IMAGE_URI} ./

機械学習実行(Vertex AI piepline)

これで事前準備は完了したので、これをVertex AI上で実行してみましょう!今回はこれをVertex AI Pipelinesで動かしてみます。・・・まあ今回のような単純なケースをVertex AI Pipelinesで実施する意味は殆どないのですが、あくまでこうやってやるという例でみていただければ幸いです。なおこれ以降のソースはVertex AI Workbenchで実行すると認証などスキップできて楽なため、こちらをおすすめします。

まずPipelines内の処理を定義します。本番用コードですと、これを複数作成した上でPipelinesをつなげていくのですが今回は必要ないため、単純な一つのみを定義します。
@pipeline(name="mnist_train", pipeline_root=f'{BUCKET_NAME}/pipeline_root')
def mnist_train(container_uri):

    from google_cloud_pipeline_components import aiplatform as gcc_aip

    train_model_op = gcc_aip.CustomContainerTrainingJobRunOp(
        project=PROJECT_ID,
        display_name="pipeline-mnist-firebase-train",
        container_uri=container_uri
    )
今回は上記のような簡素なものなのでVertex AI Pipelinesの恩恵を受けることはできませんが、これに前処理がついて組み替える場合であったり、下記のようにdsl.conditionを使って一定の精度の場合のみfirebaseMLにPublishするなどすれば更に使いやすくなるので、ここは本番で必要とされる要件に合わせて変えると良いかと思います
with dsl.Condition([精度チェック条件式]):
    ...
    ml.publish_model(fb_model.model_id)
上記で実行関数は定義されたので、これをPipelinesにまとめて実行すれば完了です。
TEMPLATE_PATH = "mnist_train.json"

compiler.Compiler().compile(
    pipeline_func=mnist_train, 
    package_path=TEMPLATE_PATH
)

job = aiplatform.PipelineJob(
    display_name="mnist-train-pipeline",
    template_path=TEMPLATE_PATH,
    job_id="mnist-train-pipeline-{0}".format(datetime.now().strftime("%Y%m%d%H%M%S")),
    parameter_values={"container_uri": CONTAINER_URL}
)

pipeline_job.submit()
実行後にはFirebaseのMachine Learningカスタム欄に該当モデルが表示されていることがわかるかと思います。

モバイルApp(android)

Remote Config定義

さてこれでModelがFirebaseに取り込まれました。ただアプリでの実装をするまえにもう一つだけfirebaseでの作業をしておきましょう。それがRemote Configです。これは「クラウド上で定義・変更できる変数」だと思っていただくのが一番直感的かと思います。設定自体は簡単で左ペインで「エンゲージメント」->「Remote Config」を選択し、新しいパラメータを追加するだけになります。設定後は↓な感じになります。


で、これを設定すると何が嬉しいのかというと以下2点になります
  • 利用するモデルファイルをクラウド側で自由に切り替えられる
  • 利用比率を変えることで複数モデルの適用/比較検証ができる
1つ目は必須でありますが、2つ目もかなり嬉しい機能ですね。Remote ConfigではIPや比率などで条件をつけ、Config値を切りかけることができます。右部のペンマークを押下すると条件をつけてConfig値を切り分けることができ、以下のようになります。


このように機械学習モデルを導入するにはまずRemote Configを設定することを考えていただけると良いかと思います。

Android App:MLモデル/Remote Config読み込み

それではようやくAndroid Appを作っていきましょう。まずは先程作成したRemote Configを読み込む部分を定義します。今回はカメラ起動の結果を処理かつモデル予測時に必ず最新モデルを適用させる(&キャッシュ有効期限などの実装の手間を抜く…)ためにonActivityResultにこれを記載してしまいましたが、通常の使い方のようにアプリ起動時に取得など適当な箇所に実装いただければと思います。

内容自体はfetchAndActivateを利用してRemote Configの値をローカルにFetchし、これが成功した場合のみ後続処理を実施するというものになります。おそらくAndroid Appのことを知らない方でもなんとなく意味はわかるものになっているかなと思います。
class MainActivity : AppCompatActivity() {
    val remoteConfig = Firebase.remoteConfig

       override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
           ...
     remoteConfig.fetchAndActivate()
             .addOnCompleteListener { task ->
                    if (task.isSuccessful) {
                                               val modelName = remoteConfig.getString("mnist_model")
                       [後続コード]
                    }
              }
     }
次にtensorflow LiteのモデルファイルをFirebaseからDownloadしていきましょう。ここで気をつける点は2点あるかと。一つはtensorflow Liteの推論のためにInterpreterというクラスを利用し、モデルをロードすること。もう一つはモデルのDL条件を非機能要件にあわせて定義することです。特に2つ目は最新モデルのDL/適用にかかるところでABテストなどとも関わるところなのでデータ分析官とも認識を合わせることをおすすめします。
// tensorflow LiteでPredictを駆動させるために必要なInterpreterを定義する
var interpreter: Interpreter? = null

// FirebaseMLのカスタムモデルをDownloadする際の条件をつける
// 今回は必要であればDLする条件だが、Wifi利用やIdle状態の場合のみ実行するなどの定義もできる
val conditions = CustomModelDownloadConditions.Builder()
                .isChargingRequired() 
                .build()

// FirebaseModelDownloaderを使ってFirebaseから実際に使う機械学習モデルをDLする
// LOCAL_MODEL_UPDATE_IN_BACKGROUNDというDownloadTypeは、ローカルモデルを使いつつ、必要であれば最新モデルをDLするという定義となる
// もし必ず最新モデルを使わせたい場合はLATEST_MODELという定義としてください
FirebaseModelDownloader.getInstance()
                              .getModel(modelName, DownloadType.LOCAL_MODEL_UPDATE_IN_BACKGROUND, conditions)
                              .addOnSuccessListener { model: CustomModel? ->
                                    val modelFile = model?.file
                                    if (modelFile != null) {
                                        interpreter = Interpreter(modelFile)
                                   [後続コード]
                                                        }
                                               }

モデルを使った推論

さあようやくお膳は整ったので、このモデルを使った推論を実施していきましょう!ただここからがtensorflow Lite特有の書き方が有り若干厄介です。。たかがMNISTなのですがそれでも初見は???となるかなと。

まずはInputデータを入れるための箱を作ります。それが一番最後のByteBufferの部分になっています。おそらく一番わかりにくいのは[28 * 28 * 1 * 4]の部分だと思いますがこれはそれぞれ以下のような意味となります。
  • Input画像の縦サイズ(28px)
  • Input画像の横サイズ(28px)
  • Input画像のカラー(今回は白黒なので明暗だけ1つ。カラーの場合はRGBで3つ)
  • 格納する値のサイズ。今回はFloatなので4byteの4
// カメラデバイスから撮影した手書きの数字を取得
val bitmap = data?.getParcelableExtra<Bitmap>("data")

// MNISTにあわせるため28x28に変更
val scaled_bitmap = Bitmap.createScaledBitmap(bitmap!!, 28, 28, false)

val input = ByteBuffer.allocateDirect(28 * 28 * 1 * 4).order(ByteOrder.nativeOrder())
これで箱が出来たので、撮影した写真をこの箱の中に入れていきましょう。先程作った箱に、Pixel1つ1つを格納していく作業となります。なお本来はgetPixelsを使えばもうちょっと簡単に書けるのでしょうがわかりやすくforで処理しています。
// 28x28の画像をPixel毎に処理/追加する
for (y in 0 until 28) {
    for (x in 0 until 28) {
        val px = bitmap.getPixel(x, y)

        // 撮影画像はカラーのためRGBそれぞれの値を取得する
        // (正規化のため255で割っている)
        val r = Color.red(px) / 255f
        val g = Color.green(px) / 255f
        val b = Color.blue(px) / 255f

                // カラーをモノクロに変換
        val mono = (0.2125f * r) + (0.7154f * g) + (0.0721f * b);

                // 値を箱に格納
        input.putFloat(mono)
    }
}
上記でInputが作成できたので後は推論するのみです。推論についても同じように箱を作ります。MNISTは0-9の数字であるため、このそれぞれの数字の確率を格納できるようにした上で実行してください。
// Outputを格納する箱を作成する
val prob = Array(1) { FloatArray(10) }

// tensorflow Lite実行のためのドライバークラスに与え実行する
interpreter?.run(input, prob)
これで実行が完了しました!あとは格納された値を取り出して自由に表示させれば良いわけです。もし全ての数字を見たければ↓とかでもいいですね。
for (index in 0.until(10)) {
        //該当数字
    Log.i("predict", index.toString())
        //確率
    Log.i("predict", modelOutput[0][index].toString())
}
以上いかがでしたでしょうか?
機械学習をモバイルApp上で動かすというのは今後かなり必要になるでしょうが、MLエンジニアとモバイルAppエンジニアはなかなか距離も遠く、双方を習得するのは難しいかもしれません。ただ今回のように各種サービスが整備されていく中ではその敷居もどんどん下がって行っているはずでしょうし、ぜひチャレンジしてみてはどうでしょうか。

次世代システム研究室では、機械学習プラットフォームの設計・開発を行うアーキテクトとデータサイエンティストを募集しています。興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧からご応募をお願いします。
一緒に勉強しながら楽しく働きたい方のご応募をお待ちしております。

Pocket

関連記事