Android でカスタム TensorFlow Lite モデルを使用する

アプリでカスタム TensorFlow Lite モデルを使用する場合は、Firebase ML を使用してモデルをデプロイできます。Firebase でモデルをデプロイすることで、アプリの初期ダウンロード サイズを減少し、アプリの新しいバージョンをリリースすることなくアプリの ML モデルを更新できます。また、Remote ConfigA/B Testing では、異なるモデルを異なるユーザー グループに動的に提供できます。

TensorFlow Lite モデル

TensorFlow Lite モデルは、モバイル デバイス上での実行に最適化された ML モデルです。TensorFlow Lite モデルを取得するには、次のようにします。

始める前に

  1. まだ Firebase を Android プロジェクトに追加していない場合は追加します。
  2. モジュール(アプリレベル)の Gradle ファイル(通常は <project>/<app-module>/build.gradle.kts または <project>/<app-module>/build.gradle)に、Android 用 Firebase ML モデル ダウンローダー ライブラリの依存関係を追加します。ライブラリのバージョニングの制御には、Firebase Android BoM を使用することをおすすめします。

    また、Firebase ML モデル ダウンローダーの設定の一環として、TensorFlow Lite SDK をアプリに追加する必要があります。

    dependencies {
        // Import the BoM for the Firebase platform
        implementation(platform("com.google.firebase:firebase-bom:33.6.0"))
    
        // Add the dependency for the Firebase ML model downloader library
        // When using the BoM, you don't specify versions in Firebase library dependencies
        implementation("com.google.firebase:firebase-ml-modeldownloader")
    // Also add the dependency for the TensorFlow Lite library and specify its version implementation("org.tensorflow:tensorflow-lite:2.3.0")
    }

    Firebase Android BoM を使用すると、アプリは常に互換性のあるバージョンの Firebase Android ライブラリを使用します。

    (代替方法)BoM を使用せずに Firebase ライブラリの依存関係を追加する

    Firebase BoM を使用しない場合は、依存関係の行でそれぞれの Firebase ライブラリのバージョンを指定する必要があります。

    アプリで複数の Firebase ライブラリを使用する場合は、すべてのバージョンの互換性を確保するため、BoM を使用してライブラリのバージョンを管理することを強くおすすめします。

    dependencies {
        // Add the dependency for the Firebase ML model downloader library
        // When NOT using the BoM, you must specify versions in Firebase library dependencies
        implementation("com.google.firebase:firebase-ml-modeldownloader:25.0.1")
    // Also add the dependency for the TensorFlow Lite library and specify its version implementation("org.tensorflow:tensorflow-lite:2.3.0")
    }
    Kotlin 固有のライブラリ モジュールをお探しの場合、 2023 年 10 月(Firebase BoM 32.5.0)以降、Kotlin と Java のどちらのデベロッパーもメイン ライブラリ モジュールを利用できるようになります(詳しくは、このイニシアチブに関するよくある質問をご覧ください)。
  3. アプリのマニフェストで、INTERNET 権限が必要であることを宣言します。
    <uses-permission android:name="android.permission.INTERNET" />

1. モデルをデプロイする

Firebase コンソールまたは Firebase Admin Python と Node.js SDK を使用して、カスタム TensorFlow モデルをデプロイします。カスタムモデルをデプロイして管理するをご覧ください。

Firebase プロジェクトにカスタムモデルを追加した後は、指定した名前を使用してアプリ内でモデルを参照できます。新しい TensorFlow Lite モデルのデプロイと、ユーザーのデバイスへの新しいモデルのダウンロードは、getModel() を呼び出すことでいつでも行うことができます(下記参照)。

2. モデルをデバイスにダウンロードして TensorFlow Lite インタープリタを初期化する

TensorFlow Lite モデルをアプリで使用するには、まず Firebase ML SDK を使用して最新バージョンのモデルをデバイスにダウンロードします。次に、モデルを使用して TensorFlow Lite インタープリタをインスタンス化します。

モデルのダウンロードを開始するには、モデル ダウンローダーの getModel() メソッドを呼び出し、モデルをアップロードしたときに割り当てた名前、常に最新モデルをダウンロードするかどうか、ダウンロードを許可する条件を指定します。

以下の 3 種類のダウンロードの動作から選択できます。

ダウンロードの種類 説明
LOCAL_MODEL デバイスからローカルモデルを取得します。使用できるローカルモデルがない場合は、LATEST_MODEL と同じ動作を行います。このダウンロードの種類は、モデルの更新の確認を行わなくてもいい場合に使用します。たとえば、Remote Config を使用してモデル名を取得し、常に新しい名前でモデルをアップロードする場合です(推奨)。
LOCAL_MODEL_UPDATE_IN_BACKGROUND デバイスからローカルモデルを取得し、バックグラウンドでモデルの更新を開始します。使用できるローカルモデルがない場合は、LATEST_MODEL と同じ動作を行います。
LATEST_MODEL 最新のモデルを取得します。ローカルモデルが最新バージョンの場合は、ローカルモデルを返します。それ以外の場合は最新のモデルをダウンロードします。その際に、最新バージョンがダウンロードされるまで処理がブロックされます(非推奨)。この動作は、明らかに最新バージョンが必要な場合にのみ使用してください。

モデルがダウンロード済みであることを確認するまで、モデルに関連する機能を無効にする必要があります(UI の一部をグレー表示または非表示にするなど)。

Kotlin+KTX

val conditions = CustomModelDownloadConditions.Builder()
        .requireWifi()  // Also possible: .requireCharging() and .requireDeviceIdle()
        .build()
FirebaseModelDownloader.getInstance()
        .getModel("your_model", DownloadType.LOCAL_MODEL_UPDATE_IN_BACKGROUND,
            conditions)
        .addOnSuccessListener { model: CustomModel? ->
            // Download complete. Depending on your app, you could enable the ML
            // feature, or switch from the local model to the remote model, etc.

            // The CustomModel object contains the local path of the model file,
            // which you can use to instantiate a TensorFlow Lite interpreter.
            val modelFile = model?.file
            if (modelFile != null) {
                interpreter = Interpreter(modelFile)
            }
        }

Java

CustomModelDownloadConditions conditions = new CustomModelDownloadConditions.Builder()
    .requireWifi()  // Also possible: .requireCharging() and .requireDeviceIdle()
    .build();
FirebaseModelDownloader.getInstance()
    .getModel("your_model", DownloadType.LOCAL_MODEL_UPDATE_IN_BACKGROUND, conditions)
    .addOnSuccessListener(new OnSuccessListener<CustomModel>() {
      @Override
      public void onSuccess(CustomModel model) {
        // Download complete. Depending on your app, you could enable the ML
        // feature, or switch from the local model to the remote model, etc.

        // The CustomModel object contains the local path of the model file,
        // which you can use to instantiate a TensorFlow Lite interpreter.
        File modelFile = model.getFile();
        if (modelFile != null) {
            interpreter = new Interpreter(modelFile);
        }
      }
    });

多くのアプリは、初期化コードでモデルのダウンロード タスクを開始しますが、モデルを使用する前に開始することもできます。

3. 入力データの推論を行う

モデルの入出力シェイプを取得する

TensorFlow Lite モデル インタープリタは、1 つ以上の多次元配列を入力として受け取り、出力として生成します。これらの配列には、byteintlongfloat 値のいずれかが含まれます。データをモデルに渡したり、結果を使用したりする前に、モデルで使用する配列の数と次元(「シェイプ」)を把握しておく必要があります。

自分でモデルを作成した場合や、モデルの入出力形式が文書化されている場合は、すでにこの情報があります。モデルの入出力のシェイプとデータ型がわからない場合は、TensorFlow Lite インタープリタを使用してモデルを検査できます。次に例を示します。

Python

import tensorflow as tf

interpreter = tf.lite.Interpreter(model_path="your_model.tflite")
interpreter.allocate_tensors()

# Print input shape and type
inputs = interpreter.get_input_details()
print('{} input(s):'.format(len(inputs)))
for i in range(0, len(inputs)):
    print('{} {}'.format(inputs[i]['shape'], inputs[i]['dtype']))

# Print output shape and type
outputs = interpreter.get_output_details()
print('\n{} output(s):'.format(len(outputs)))
for i in range(0, len(outputs)):
    print('{} {}'.format(outputs[i]['shape'], outputs[i]['dtype']))

出力例:

1 input(s):
[  1 224 224   3] <class 'numpy.float32'>

1 output(s):
[1 1000] <class 'numpy.float32'>

インタープリタを実行する

モデルの入出力の形式を決定したら、入力データを取得し、モデルに適したシェイプの入力を取得するために必要なデータを変換します。

たとえば、使用する画像分類モデルの入力シェイプが [1 224 224 3] 個の浮動小数点値である場合は、次の例に示すように、Bitmap オブジェクトから入力 ByteBuffer を生成できます。

Kotlin+KTX

val bitmap = Bitmap.createScaledBitmap(yourInputImage, 224, 224, true)
val input = ByteBuffer.allocateDirect(224*224*3*4).order(ByteOrder.nativeOrder())
for (y in 0 until 224) {
    for (x in 0 until 224) {
        val px = bitmap.getPixel(x, y)

        // Get channel values from the pixel value.
        val r = Color.red(px)
        val g = Color.green(px)
        val b = Color.blue(px)

        // Normalize channel values to [-1.0, 1.0]. This requirement depends on the model.
        // For example, some models might require values to be normalized to the range
        // [0.0, 1.0] instead.
        val rf = (r - 127) / 255f
        val gf = (g - 127) / 255f
        val bf = (b - 127) / 255f

        input.putFloat(rf)
        input.putFloat(gf)
        input.putFloat(bf)
    }
}

Java

Bitmap bitmap = Bitmap.createScaledBitmap(yourInputImage, 224, 224, true);
ByteBuffer input = ByteBuffer.allocateDirect(224 * 224 * 3 * 4).order(ByteOrder.nativeOrder());
for (int y = 0; y < 224; y++) {
    for (int x = 0; x < 224; x++) {
        int px = bitmap.getPixel(x, y);

        // Get channel values from the pixel value.
        int r = Color.red(px);
        int g = Color.green(px);
        int b = Color.blue(px);

        // Normalize channel values to [-1.0, 1.0]. This requirement depends
        // on the model. For example, some models might require values to be
        // normalized to the range [0.0, 1.0] instead.
        float rf = (r - 127) / 255.0f;
        float gf = (g - 127) / 255.0f;
        float bf = (b - 127) / 255.0f;

        input.putFloat(rf);
        input.putFloat(gf);
        input.putFloat(bf);
    }
}

次に、モデルの出力を格納するのに十分な大きさの ByteBuffer を割り当て、入力バッファと出力バッファを TensorFlow Lite インタープリタの run() メソッドに渡します。たとえば、出力シェイプが [1 1000] の浮動小数点値の場合:

Kotlin+KTX

val bufferSize = 1000 * java.lang.Float.SIZE / java.lang.Byte.SIZE
val modelOutput = ByteBuffer.allocateDirect(bufferSize).order(ByteOrder.nativeOrder())
interpreter?.run(input, modelOutput)

Java

int bufferSize = 1000 * java.lang.Float.SIZE / java.lang.Byte.SIZE;
ByteBuffer modelOutput = ByteBuffer.allocateDirect(bufferSize).order(ByteOrder.nativeOrder());
interpreter.run(input, modelOutput);

出力をどのように使用するかは、使用しているモデルによって異なります。

たとえば、分類を行う場合は、次のステップとして、結果のインデックスをそれぞれが表すラベルにマッピングできます。

Kotlin+KTX

modelOutput.rewind()
val probabilities = modelOutput.asFloatBuffer()
try {
    val reader = BufferedReader(
            InputStreamReader(assets.open("custom_labels.txt")))
    for (i in probabilities.capacity()) {
        val label: String = reader.readLine()
        val probability = probabilities.get(i)
        println("$label: $probability")
    }
} catch (e: IOException) {
    // File not found?
}

Java

modelOutput.rewind();
FloatBuffer probabilities = modelOutput.asFloatBuffer();
try {
    BufferedReader reader = new BufferedReader(
            new InputStreamReader(getAssets().open("custom_labels.txt")));
    for (int i = 0; i < probabilities.capacity(); i++) {
        String label = reader.readLine();
        float probability = probabilities.get(i);
        Log.i(TAG, String.format("%s: %1.4f", label, probability));
    }
} catch (IOException e) {
    // File not found?
}

付録: モデルのセキュリティ

TensorFlow Lite モデルを Firebase ML で利用可能にした方法に関係なく、Firebase ML は標準のシリアル化された protobuf 形式でモデルをローカル ストレージに格納します。

理論上、これは誰でもモデルをコピーできることを意味します。ただし、実際には、ほとんどのモデルはアプリケーションに固有であり、最適化により難読化されています。このため、リスクは、競合他社がコードを逆アセンブルして再利用する場合と同程度です。そうであっても、アプリでカスタムモデルを使用する前に、このリスクを認識しておく必要があります。

Android API レベル 21(Lollipop)以降では、モデルは自動バックアップから除外されるディレクトリにダウンロードされます。

Android API レベル 20 以前では、モデルはアプリ専用の内部ストレージ内の com.google.firebase.ml.custom.models というディレクトリにダウンロードされます。BackupAgent を使用したファイルのバックアップを有効にした場合は、このディレクトリを除外できます。