Apple プラットフォームでカスタム TensorFlow Lite モデルを使用する

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

前提条件

  • MLModelDownloader ライブラリは Swift でのみ使用できます。
  • TensorFlow Lite は、iOS 9 以降を使用するデバイス上でのみ動作します。

TensorFlow Lite モデル

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

始める前に

TensorFlowLite では Swift Package Manager を使ったインストールが現在サポートされていないため、Firebase で TensorFlowLite を使うには CocoaPods を使用する必要があります。MLModelDownloader のインストール手順については、CocoaPods インストール ガイドをご覧ください。

インストールが完了したら、Firebase と TensorFlowLite を使用できるようインポートします。

Swift

import FirebaseMLModelDownloader
import TensorFlowLite

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

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

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

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

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

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

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

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

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

Swift

let conditions = ModelDownloadConditions(allowsCellularAccess: false)
ModelDownloader.modelDownloader()
    .getModel(name: "your_model",
              downloadType: .localModelUpdateInBackground,
              conditions: conditions) { result in
        switch (result) {
        case .success(let customModel):
            do {
                // 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.
                let interpreter = try Interpreter(modelPath: customModel.path)
            } catch {
                // Error. Bad model file?
            }
        case .failure(let error):
            // Download was unsuccessful. Don't enable ML features.
            print(error)
        }
}

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

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] 個の浮動小数点値である場合は、次の例に示すように、画像のカラー値を浮動小数点範囲にスケーリングする必要があります。

Swift

let image: CGImage = // Your input image
guard let context = CGContext(
  data: nil,
  width: image.width, height: image.height,
  bitsPerComponent: 8, bytesPerRow: image.width * 4,
  space: CGColorSpaceCreateDeviceRGB(),
  bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue
) else {
  return false
}

context.draw(image, in: CGRect(x: 0, y: 0, width: image.width, height: image.height))
guard let imageData = context.data else { return false }

var inputData = Data()
for row in 0 ..&lt; 224 {
  for col in 0 ..&lt; 224 {
    let offset = 4 * (row * context.width + col)
    // (Ignore offset 0, the unused alpha channel)
    let red = imageData.load(fromByteOffset: offset+1, as: UInt8.self)
    let green = imageData.load(fromByteOffset: offset+2, as: UInt8.self)
    let blue = imageData.load(fromByteOffset: offset+3, as: UInt8.self)

    // Normalize channel values to [0.0, 1.0]. This requirement varies
    // by model. For example, some models might require values to be
    // normalized to the range [-1.0, 1.0] instead, and others might
    // require fixed-point values or the original bytes.
    var normalizedRed = Float32(red) / 255.0
    var normalizedGreen = Float32(green) / 255.0
    var normalizedBlue = Float32(blue) / 255.0

    // Append normalized values to Data object in RGB order.
    let elementSize = MemoryLayout.size(ofValue: normalizedRed)
    var bytes = [UInt8](repeating: 0, count: elementSize)
    memcpy(&amp;bytes, &amp;normalizedRed, elementSize)
    inputData.append(&amp;bytes, count: elementSize)
    memcpy(&amp;bytes, &amp;normalizedGreen, elementSize)
    inputData.append(&amp;bytes, count: elementSize)
    memcpy(&ammp;bytes, &amp;normalizedBlue, elementSize)
    inputData.append(&amp;bytes, count: elementSize)
  }
}

入力 NSData をインタープリタにコピーし、実行します。

Swift

try interpreter.allocateTensors()
try interpreter.copy(inputData, toInputAt: 0)
try interpreter.invoke()

モデルの出力を取得するには、インタープリタの output(at:) メソッドを呼び出します。出力をどのように使用するかは、使用しているモデルによって異なります。

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

Swift

let output = try interpreter.output(at: 0)
let probabilities =
        UnsafeMutableBufferPointer<Float32>.allocate(capacity: 1000)
output.data.copyBytes(to: probabilities)

guard let labelPath = Bundle.main.path(forResource: "retrained_labels", ofType: "txt") else { return }
let fileContents = try? String(contentsOfFile: labelPath)
guard let labels = fileContents?.components(separatedBy: "\n") else { return }

for i in labels.indices {
    print("\(labels[i]): \(probabilities[i])")
}

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

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

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