ML Kit を使用すると、TensorFlow Lite モデルを使用してデバイス上で推論を実行できます。
ML Kit は、iOS 9 以降を実行するデバイス上でのみ、TensorFlow Lite モデルを使用できます。
始める前に
- まだアプリに Firebase を追加していない場合は、スタートガイドの手順に沿って追加してください。
- ML Kit ライブラリを Podfile に含めます:
pod 'Firebase/MLModelInterpreter', '6.25.0'
プロジェクトの Pod をインストールまたは更新した後に、.xcworkspace
を使用して Xcode プロジェクトを開くようにしてください。 - アプリに Firebase をインポートします。
Swift
import Firebase
Objective-C
@import Firebase;
- 使用する TensorFlow モデルを TensorFlow Lite 形式に変換します。TOCO: TensorFlow Lite 最適化コンバータをご覧ください。
モデルをホストまたはバンドルする
アプリの推論に TensorFlow Lite モデルを使用するには、事前に ML Kit でモデルを利用可能にしておく必要があります。ML Kit は、Firebase を使用してリモートでホストされている TensorFlow Lite モデル、アプリのバイナリにバンドルされている TensorFlow Lite モデルのいずれか、またはその両方を使用できます。
Firebase でモデルをホストすることで、新しいアプリ バージョンをリリースすることなくモデルを更新できます。また、Remote Config と A/B Testing を使用して、さまざまなモデルをさまざまなユーザーセットに動的に提供できます。
モデルをアプリにバンドルしないで、Firebase でホストすることによってのみモデルを提供することで、アプリの初期ダウンロード サイズを小さくできます。ただし、モデルがアプリにバンドルされていない場合、モデルに関連する機能は、アプリでモデルを初めてダウンロードするまで使用できません。
モデルをアプリにバンドルすると、Firebase でホストされているモデルを取得できないときにもアプリの ML 機能を引き続き使用できます。
モデルを Firebase でホストする
TensorFlow Lite モデルを Firebase でホストするには:
- Firebase コンソールの [ML Kit] セクションで [カスタム] タブをクリックします。
- [カスタムモデルを追加](または [別のモデルを追加])をクリックします。
- 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 でモデルをホストする場合は、CustomRemoteModel
オブジェクトを作成します。その際に、モデルを公開したときに割り当てた名前を指定します。
Swift
let remoteModel = CustomRemoteModel(
name: "your_remote_model" // The name you assigned in the Firebase console.
)
Objective-C
// Initialize using the name you assigned in the Firebase console.
FIRCustomRemoteModel *remoteModel =
[[FIRCustomRemoteModel alloc] initWithName:@"your_remote_model"];
次に、ダウンロードを許可する条件を指定してモデルのダウンロード タスクを開始します。モデルがデバイスにない場合、または新しいバージョンのモデルが使用可能な場合、このタスクは Firebase から非同期でモデルをダウンロードします。
Swift
let downloadConditions = ModelDownloadConditions(
allowsCellularAccess: true,
allowsBackgroundDownloading: true
)
let downloadProgress = ModelManager.modelManager().download(
remoteModel,
conditions: downloadConditions
)
Objective-C
FIRModelDownloadConditions *downloadConditions =
[[FIRModelDownloadConditions alloc] initWithAllowsCellularAccess:YES
allowsBackgroundDownloading:YES];
NSProgress *downloadProgress =
[[FIRModelManager modelManager] downloadRemoteModel:remoteModel
conditions:downloadConditions];
多くのアプリは、初期化コードでモデルのダウンロード タスクを開始しますが、モデルを使用する前に開始することもできます。
ローカルモデルを構成する
アプリにモデルをバンドルする場合は、TensorFlow Lite モデルのファイル名を指定して CustomLocalModel
オブジェクトを作成します。
Swift
guard let modelPath = Bundle.main.path(
forResource: "your_model",
ofType: "tflite",
inDirectory: "your_model_directory"
) else { /* Handle error. */ }
let localModel = CustomLocalModel(modelPath: modelPath)
Objective-C
NSString *modelPath = [NSBundle.mainBundle pathForResource:@"your_model"
ofType:@"tflite"
inDirectory:@"your_model_directory"];
FIRCustomLocalModel *localModel =
[[FIRCustomLocalModel alloc] initWithModelPath:modelPath];
モデルからインタープリタを作成する
モデルソースを構成したら、そのソースのいずれか 1 つから ModelInterpreter
オブジェクトを作成します。
ローカル バンドルモデルのみがある場合は CustomLocalModel
オブジェクトを modelInterpreter(localModel:)
に渡すだけで済みます。
Swift
let interpreter = ModelInterpreter.modelInterpreter(localModel: localModel)
Objective-C
FIRModelInterpreter *interpreter =
[FIRModelInterpreter modelInterpreterForLocalModel:localModel];
リモートでホストされるモデルがある場合は、そのモデルを実行する前にダウンロード済みであることを確認する必要があります。モデルのダウンロード タスクのステータスは、モデル マネージャーの isModelDownloaded(remoteModel:)
メソッドを使用して確認できます。
ダウンロードのステータスはインタープリタを実行する前に確認するだけ済みますが、リモートでホストされるモデルとローカル バンドルモデルの両方がある場合は、ModelInterpreter
をインスタンス化する、つまりインタープリタを作成する(リモートモデルをダウンロード済みの場合はリモートモデルから、ダウンロードされていない場合はローカルモデルから作成する)ときに確認しても問題ありません。
Swift
var interpreter: ModelInterpreter
if ModelManager.modelManager().isModelDownloaded(remoteModel) {
interpreter = ModelInterpreter.modelInterpreter(remoteModel: remoteModel)
} else {
interpreter = ModelInterpreter.modelInterpreter(localModel: localModel)
}
Objective-C
FIRModelInterpreter *interpreter;
if ([[FIRModelManager modelManager] isModelDownloaded:remoteModel]) {
interpreter = [FIRModelInterpreter modelInterpreterForRemoteModel:remoteModel];
} else {
interpreter = [FIRModelInterpreter modelInterpreterForLocalModel:localModel];
}
リモートでホストされるモデルのみがある場合は、モデルがダウンロード済みであることを確認するまで、モデルに関連する機能(UI の一部をグレー表示または非表示にするなど)を無効にする必要があります。
オブザーバをデフォルトの通知センターに接続して、モデルのダウンロード ステータスを取得できます。ダウンロードに時間がかかり、ダウンロードが終了するまでに元のオブジェクトが解放される可能性があります。このため、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 つ以上の多次元配列を入力として受け取り、出力として生成します。これらの配列には、byte
、int
、long
、float
値のいずれかが含まれます。モデルで使用する配列の数と次元(「シェイプ」)で ML キットを構成する必要があります。
モデルの入出力のシェイプとデータ型がわからない場合は、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:completion:)
メソッドに渡します。
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 形式でローカル ストレージに保存されます。
理論上、これは誰でもモデルをコピーできることを意味します。ただし、実際には、ほとんどのモデルはアプリケーションに固有であり、最適化により難読化されています。このため、リスクは、競合他社がコードを逆アセンブルして再利用する場合と同程度です。そうであっても、アプリでカスタムモデルを使用する前に、このリスクを認識しておく必要があります。