コンソールへ移動

ML Kit を使用して推論に TensorFlow Lite モデルを使用する(iOS)

ML Kit を使用すると、TensorFlow Lite モデルを使用してデバイス上で推論を実行できます。

ML Kit は、iOS 9 以降を実行するデバイス上でのみ、TensorFlow Lite モデルを使用できます。

この API の使用例については、GitHub の ML Kit クイックスタート サンプルをご覧ください。

準備

  1. まだアプリに Firebase を追加していない場合は、スタートガイドの手順に沿って追加してください。
  2. Podfile に ML Kit ライブラリを含めます。
    pod 'Firebase/Analytics'
    pod 'Firebase/MLModelInterpreter'
    
    プロジェクトのポッドをインストールまたは更新した後に、.xcworkspace を使用して Xcode プロジェクトを開くようにしてください。
  3. アプリに Firebase をインポートします。

    Swift

    import Firebase
    import FirebaseMLCommon

    Objective-C

    @import Firebase;
    @import FirebaseMLCommon;
  4. 使用する TensorFlow モデルを TensorFlow Lite 形式に変換します。TOCO: TensorFlow Lite 最適化コンバータをご覧ください。

モデルをホストまたはバンドルする

アプリの推論に TensorFlow Lite モデルを使用するには、事前に ML Kit でモデルを利用可能にしておく必要があります。ML Kit は、Firebase を使用してリモートでホストされている TensorFlow Lite モデル、アプリのバイナリにバンドルされている TensorFlow Lite モデルのいずれか、またはその両方を使用できます。

Firebase でモデルをホストすると、モデルを更新する際にアプリの新しいバージョンをリリースする必要がなく、Remote Config と A/B テストを使用して異なるモデルを異なるユーザー グループに動的に提供できます。

モデルを提供する方法として Firebase でホストする方法だけを使用し、モデルをアプリにバンドルしない場合は、アプリの初期ダウンロード サイズを小さくすることができます。ただし、モデルがアプリにバンドルされていない場合、モデルに関連する機能は、アプリがモデルを初めてダウンロードするまで使用できません。

モデルをアプリにバンドルすると、Firebase でホストされているモデルを取得できないときにもアプリの ML 機能を引き続き使用できます。

モデルを Firebase でホストする

TensorFlow Lite モデルを Firebase でホストするには:

  1. Firebase コンソールの [ML Kit] セクションで、[カスタム] タブをクリックします。
  2. [カスタムモデルを追加](または [別のモデルを追加])をクリックします。
  3. Firebase プロジェクトでモデルを識別するための名前を指定し、TensorFlow Lite モデルファイル(拡張子は通常 .tflite または .lite)をアップロードします。

Firebase プロジェクトにカスタムモデルを追加した後は、指定した名前を使用してアプリ内でモデルを参照できます。新しい TensorFlow Lite モデルはいつでもアップロードできます。新しいモデルは、次回アプリが起動したときにダウンロードされて使用されます。アプリがモデルを更新するために必要なデバイスの条件を定義できます(以下を参照)。

モデルをアプリにバンドルする

TensorFlow Lite モデルをアプリにバンドルするには、モデルファイル(拡張子は通常 .tflite または .lite)を Xcode プロジェクトに追加します。その際、[Copy bundle resources] を選択してください。モデルファイルはアプリバンドルに含められ、ML Kit から使用できます。

モデルを読み込む

TensorFlow Lite モデルをアプリで使用するには、まずモデルが存在する場所(Firebase を使用してリモートで、ローカル ストレージ、またはその両方)で ML Kit を構成します。モデルとしてローカルとリモートの両方を指定すると、使用可能な場合はリモートモデルが使用され、リモートモデルが使用できない場合はローカルに保存されているモデルにフォールバックします。

Firebase ホストモデルを構成する

Firebase でモデルをホストする場合は、RemoteModel オブジェクトを登録します。その際に、モデルをアップロードしたときに割り当てた名前、ML Kit が最初にモデルをダウンロードする条件、更新が利用可能になった場合にそれをダウンロードする条件を指定します。

Swift

let initialConditions = ModelDownloadConditions(
    allowsCellularAccess: true,
    allowsBackgroundDownloading: true
)
let updateConditions = ModelDownloadConditions(
    allowsCellularAccess: false,
    allowsBackgroundDownloading: true
)
let remoteModel = RemoteModel(
    name: "my_remote_model",
    allowsModelUpdates: true,
    initialConditions: initialConditions,
    updateConditions: updateConditions
)
ModelManager.modelManager().register(remoteModel)

Objective-C

FIRModelDownloadConditions *initialConditions = [[FIRModelDownloadConditions alloc]
                                                 initWithAllowsCellularAccess:YES
                                                  allowsBackgroundDownloading:YES];
FIRModelDownloadConditions *updateConditions = [[FIRModelDownloadConditions alloc]
                                                initWithAllowsCellularAccess:NO
                                                 allowsBackgroundDownloading:YES];
FIRRemoteModel *remoteModel = [[FIRRemoteModel alloc] initWithName:@"my_remote_model"
                                                allowsModelUpdates:YES
                                                 initialConditions:initialConditions
                                                  updateConditions:updateConditions];
[[FIRModelManager modelManager] registerRemoteModel:remoteModel];

ローカルモデルを構成する

アプリにモデルをバンドルする場合は、TensorFlow Lite モデルのファイル名を指定して LocalModel オブジェクトを登録し、次のステップで使用する名前をモデルに割り当てます。

Swift

guard let modelPath = Bundle.main.path(forResource: "my_model", ofType: "tflite")
    else {
        // Invalid model path
        return
}
let localModel = LocalModel(name: "my_local_model", path: modelPath)
ModelManager.modelManager().register(localModel)

Objective-C

NSString *modelPath = [NSBundle.mainBundle pathForResource:@"my_model"
                                                    ofType:@"tflite"];
FIRLocalModel *localModel = [[FIRLocalModel alloc] initWithName:@"my_local_model"
                                                           path:modelPath];
[[FIRModelManager modelManager] registerLocalModel:localModel];

モデルからインタープリタを作成する

モデルの場所を構成したら、リモートモデル、ローカルモデル、またはその両方を使用して ModelOptions オブジェクトを作成し、それを使用して ModelInterpreter インスタンスを取得します。モデルが 1 つのみである場合は、使用しないモデルタイプに nil を指定します。

Swift

let options = ModelOptions(
    remoteModelName: "my_remote_model",
    localModelName: "my_local_model")
let interpreter = ModelInterpreter.modelInterpreter(options: options)

Objective-C

FIRModelOptions *options = [[FIRModelOptions alloc] initWithRemoteModelName:@"my_remote_model"
                                                             localModelName:@"my_local_model"];
FIRModelInterpreter *interpreter = [FIRModelInterpreter modelInterpreterWithOptions:options];

モデルがデバイスで利用可能なことを確認する

推奨: ローカルにバンドルされたモデルを構成していない場合は、リモートモデルがデバイスにダウンロードされていることを確認してください。

リモートにホストされたモデルを実行するときに、そのモデルがまだデバイスで使用できない場合は、呼び出しが失敗し、モデルはバックグラウンドでデバイスに自動的にダウンロードされます。ダウンロードが完了したら、モデルを正常に実行できます。

モデル ダウンロード タスクをさらに明示的に処理する場合は、モデル ダウンロード タスクを開始し、download(remoteModel:) を呼び出してそのステータスを確認できます。

Swift

let downloadProgress = ModelManager.modelManager().download(remoteModel)

// ...

if downloadProgress.isFinished {
    // The model is available on the device
}

Objective-C

NSProgress *downloadProgress = [[FIRModelManager modelManager] downloadRemoteModel:remoteModel];

// ...

if (downloadProgress.isFinished) {
  // The model is available on the device
}

オブザーバをデフォルトの通知センターに接続して、モデルのダウンロード ステータスを取得することもできます。ダウンロードに時間がかかり、ダウンロードが終了するまでに元のオブジェクトが解放される可能性があります。このため、observer ブロックでは self への弱い参照を使用してください。次に例を示します。

Swift

NotificationCenter.default.addObserver(
    forName: .firebaseMLModelDownloadDidSucceed,
    object: nil,
    queue: nil
) { [weak self] notification in
    guard let strongSelf = self,
        let userInfo = notification.userInfo,
        let model = userInfo[ModelDownloadUserInfoKey.remoteModel.rawValue]
            as? RemoteModel,
        model.name == "your_remote_model"
        else { return }
    // The model was downloaded and is available on the device
}

NotificationCenter.default.addObserver(
    forName: .firebaseMLModelDownloadDidFail,
    object: nil,
    queue: nil
) { [weak self] notification in
    guard let strongSelf = self,
        let userInfo = notification.userInfo,
        let model = userInfo[ModelDownloadUserInfoKey.remoteModel.rawValue]
            as? RemoteModel
        else { return }
    let error = userInfo[ModelDownloadUserInfoKey.error.rawValue]
    // ...
}

Objective-C

__weak typeof(self) weakSelf = self;

[NSNotificationCenter.defaultCenter
    addObserverForName:FIRModelDownloadDidSucceedNotification
                object:nil
                 queue:nil
            usingBlock:^(NSNotification *_Nonnull note) {
              if (weakSelf == nil | note.userInfo == nil) {
                return;
              }
              __strong typeof(self) strongSelf = weakSelf;

              FIRRemoteModel *model = note.userInfo[FIRModelDownloadUserInfoKeyRemoteModel];
              if ([model.name isEqualToString:@"your_remote_model"]) {
                // The model was downloaded and is available on the device
              }
            }];

[NSNotificationCenter.defaultCenter
    addObserverForName:FIRModelDownloadDidFailNotification
                object:nil
                 queue:nil
            usingBlock:^(NSNotification *_Nonnull note) {
              if (weakSelf == nil | note.userInfo == nil) {
                return;
              }
              __strong typeof(self) strongSelf = weakSelf;

              NSError *error = note.userInfo[FIRModelDownloadUserInfoKeyError];
            }];

モデルの入力と出力を指定する

次に、モデル インタープリタの入出力形式を構成します。

TensorFlow Lite モデルは、1 つ以上の多次元配列を入力として受け取り、出力として生成します。これらの配列には、byteintlongfloat 値のいずれかが含まれます。モデルで使用する配列の数と次元(「シェイプ」)を ML Kit に指定する必要があります。

モデルの入出力のシェイプとデータ型がわからない場合は、TensorFlow Lite Python インタープリタを使用してモデルを検査できます。次に例を示します。

import tensorflow as tf

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

# Print input shape and type
print(interpreter.get_input_details()[0]['shape'])  # Example: [1 224 224 3]
print(interpreter.get_input_details()[0]['dtype'])  # Example: <class 'numpy.float32'>

# Print output shape and type
print(interpreter.get_output_details()[0]['shape'])  # Example: [1 1000]
print(interpreter.get_output_details()[0]['dtype'])  # Example: <class 'numpy.float32'>

モデルの入出力の形式がわかったら、ModelInputOutputOptions オブジェクトを作成してアプリのモデル インタープリタを構成できます。

たとえば、浮動小数点画像分類モデルは、N 個の 224 x 224 x 3 チャネル(RGB)画像のまとまりを表す N x 224 x 224 x 3 の Float 値の配列を入力として受け取り、1,000 個の Float 値のリストを出力として生成します。このリストの値はそれぞれ、対象の画像が、モデルによって予測される 1,000 個のカテゴリのいずれか 1 つのメンバーである確率を表します。

このようなモデルの場合は、モデル インタープリタの入力と出力を次のように構成します。

Swift

let ioOptions = ModelInputOutputOptions()
do {
    try ioOptions.setInputFormat(index: 0, type: .float32, dimensions: [1, 224, 224, 3])
    try ioOptions.setOutputFormat(index: 0, type: .float32, dimensions: [1, 1000])
} catch let error as NSError {
    print("Failed to set input or output format with error: \(error.localizedDescription)")
}

Objective-C

FIRModelInputOutputOptions *ioOptions = [[FIRModelInputOutputOptions alloc] init];
NSError *error;
[ioOptions setInputFormatForIndex:0
                             type:FIRModelElementTypeFloat32
                       dimensions:@[@1, @224, @224, @3]
                            error:&error];
if (error != nil) { return; }
[ioOptions setOutputFormatForIndex:0
                              type:FIRModelElementTypeFloat32
                        dimensions:@[@1, @1000]
                             error:&error];
if (error != nil) { return; }

入力データの推論を行う

最後に、モデルを使用して推論を行うため、入力データを取得してモデルに必要な変換を実行し、データを含む Data オブジェクトを作成します。

たとえば、画像を処理するモデルで、モデルの入力次元が [BATCH_SIZE, 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 }

let inputs = ModelInputs()
var inputData = Data()
do {
  for row in 0 ..< 224 {
    for col in 0 ..< 224 {
      let offset = 4 * (col * context.width + row)
      // (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(&bytes, &normalizedRed, elementSize)
      inputData.append(&bytes, count: elementSize)
      memcpy(&bytes, &normalizedGreen, elementSize)
      inputData.append(&bytes, count: elementSize)
      memcpy(&ammp;bytes, &normalizedBlue, elementSize)
      inputData.append(&bytes, count: elementSize)
    }
  }
  try inputs.addInput(inputData)
} catch let error {
  print("Failed to add input: \(error)")
}

Objective-C

CGImageRef image = // Your input image
long imageWidth = CGImageGetWidth(image);
long imageHeight = CGImageGetHeight(image);
CGContextRef context = CGBitmapContextCreate(nil,
                                             imageWidth, imageHeight,
                                             8,
                                             imageWidth * 4,
                                             CGColorSpaceCreateDeviceRGB(),
                                             kCGImageAlphaNoneSkipFirst);
CGContextDrawImage(context, CGRectMake(0, 0, imageWidth, imageHeight), image);
UInt8 *imageData = CGBitmapContextGetData(context);

FIRModelInputs *inputs = [[FIRModelInputs alloc] init];
NSMutableData *inputData = [[NSMutableData alloc] initWithCapacity:0];

for (int row = 0; row < 224; row++) {
  for (int col = 0; col < 224; col++) {
    long offset = 4 * (col * imageWidth + row);
    // 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.
    // (Ignore offset 0, the unused alpha channel)
    Float32 red = imageData[offset+1] / 255.0f;
    Float32 green = imageData[offset+2] / 255.0f;
    Float32 blue = imageData[offset+3] / 255.0f;

    [inputData appendBytes:&red length:sizeof(red)];
    [inputData appendBytes:&green length:sizeof(green)];
    [inputData appendBytes:&blue length:sizeof(blue)];
  }
}

[inputs addInput:inputData error:&error];
if (error != nil) { return nil; }

モデルへの入力データが準備できたら、入力オプションと入出力オプションをモデル インタープリタrun(inputs:options:) メソッドに渡します。

Swift

interpreter.run(inputs: inputs, options: ioOptions) { outputs, error in
    guard error == nil, let outputs = outputs else { return }
    // Process outputs
    // ...
}

Objective-C

[interpreter runWithInputs:inputs
                   options:ioOptions
                completion:^(FIRModelOutputs * _Nullable outputs,
                             NSError * _Nullable error) {
  if (error != nil || outputs == nil) {
    return;
  }
  // Process outputs
  // ...
}];

返されたオブジェクトの output(index:) メソッドを呼び出すことで、出力を取得できます。次に例を示します。

Swift

// Get first and only output of inference with a batch size of 1
let output = try? outputs.output(index: 0) as? [[NSNumber]]
let probabilities = output??[0]

Objective-C

// Get first and only output of inference with a batch size of 1
NSError *outputError;
NSArray *probabilites = [outputs outputAtIndex:0 error:&outputError][0];

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

たとえば、分類を行う場合は、次のステップとして、結果のインデックスをそれぞれが表すラベルにマッピングできます。モデルの各カテゴリに対応するラベル文字列を含むテキスト ファイルがある場合、これらのラベル文字列と出力確率のマッピングは次のようなコードで行うことができます。

Swift

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 0 ..< labels.count {
  if let probability = probabilities?[i] {
    print("\(labels[i]): \(probability)")
  }
}

Objective-C

NSError *labelReadError = nil;
NSString *labelPath = [NSBundle.mainBundle pathForResource:@"retrained_labels"
                                                    ofType:@"txt"];
NSString *fileContents = [NSString stringWithContentsOfFile:labelPath
                                                   encoding:NSUTF8StringEncoding
                                                      error:&labelReadError];
if (labelReadError != nil || fileContents == NULL) { return; }
NSArray<NSString *> *labels = [fileContents componentsSeparatedByString:@"\n"];
for (int i = 0; i < labels.count; i++) {
    NSString *label = labels[i];
    NSNumber *probability = probabilites[i];
    NSLog(@"%@: %f", label, probability.floatValue);
}

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

TensorFlow Lite モデルをどの方法で ML Kit に提供するかにかかわらず、これらのモデルは標準のシリアル化された protobuf 形式でローカル ストレージに保存されます。

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