Utilizzo di un modello TensorFlow Lite per l'inferenza con ML Kit su iOS

Puoi utilizzare ML Kit per eseguire l'inferenza on-device con una Modello TensorFlow Lite.

ML Kit può utilizzare i modelli TensorFlow Lite solo su dispositivi con iOS 9 e più recenti.

Prima di iniziare

  1. Se non hai già aggiunto Firebase alla tua app, puoi farlo seguendo le istruzioni riportate in passaggi nella Guida introduttiva.
  2. Includi le librerie del kit ML nel tuo podfile:
    pod 'Firebase/MLModelInterpreter', '6.25.0'
    
    Dopo aver installato o aggiornato i pod del progetto, assicurati di aprire Xcode utilizzando il suo .xcworkspace.
  3. Nell'app, importa Firebase:

    Swift

    import Firebase

    Objective-C

    @import Firebase;
  4. Converti il modello TensorFlow che vuoi utilizzare nel formato TensorFlow Lite. Consulta TOCO: TensorFlow Lite Optimizing Converter.

Ospita o raggruppa il modello

Prima di poter utilizzare un modello TensorFlow Lite per l'inferenza nella tua app, il modello deve essere disponibile per ML Kit. ML Kit può utilizzare TensorFlow Lite modelli ospitati in remoto utilizzando Firebase, in bundle con il programma binario dell'app o entrambi.

Se ospiti un modello su Firebase, puoi aggiornarlo senza rilasciare un nuova versione dell'app e puoi usare Remote Config e A/B Testing per di pubblicare dinamicamente modelli diversi per insiemi di utenti diversi.

Se scegli di fornire il modello solo ospitandolo con Firebase e non puoi ridurne le dimensioni di download iniziali. Tieni presente, tuttavia, che se il modello non è integrato nella tua app, le funzionalità correlate al modello non saranno disponibili finché l'app non scarica l'app per la prima volta.

Associando il modello alla tua app, puoi garantire che le caratteristiche ML dell'app continueranno a funzionare quando il modello ospitato da Firebase non è disponibile.

Ospita modelli su Firebase

Per ospitare il tuo modello TensorFlow Lite su Firebase:

  1. Nella sezione ML Kit della console Firebase, fai clic su la scheda Personalizzata.
  2. Fai clic su Aggiungi modello personalizzato (o Aggiungi un altro modello).
  3. Specifica un nome che verrà utilizzato per identificare il modello in Firebase progetto, quindi carica il file del modello TensorFlow Lite (che di solito termina con .tflite o .lite).

Dopo aver aggiunto un modello personalizzato al tuo progetto Firebase, puoi fare riferimento alla tuo modello nelle tue app utilizzando il nome specificato. Puoi caricare un nuovo modello TensorFlow Lite in qualsiasi momento e la tua app lo scaricherà e inizierà a utilizzarlo al successivo riavvio. Puoi definire il dispositivo le condizioni necessarie affinché l'app tenti di aggiornare il modello (vedi di seguito).

Raggruppa i modelli con un'app

Per raggruppare il modello TensorFlow Lite con l'app, aggiungi il file del modello (in genere termina con .tflite o .lite) al progetto Xcode, assicurandoti di selezionare Copia risorse del bundle. Il file del modello verrà incluso nel bundle dell'app e sarà disponibile per ML Kit.

Carica il modello

Per utilizzare il modello TensorFlow Lite nella tua app, devi prima configurare ML Kit con le località in cui è disponibile il modello: da remoto tramite Firebase, archiviazione locale o entrambe. Se specifichi sia un modello locale che uno remoto, puoi usare il modello remoto, se disponibile, e tornare archiviato localmente se il modello remoto non è disponibile.

Configura un modello ospitato da Firebase

Se hai ospitato il tuo modello con Firebase, crea un oggetto CustomRemoteModel, specificando il nome assegnato al modello al momento della pubblicazione:

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"];

Poi, avvia l'attività di download del modello, specificando le condizioni in cui vuoi consentire il download. Se il modello non è presente sul dispositivo o se una versione più recente del modello, l'attività scaricherà in modo asincrono modello di 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];

Molte app avviano l'attività di download nel codice di inizializzazione, ma puoi farlo quindi in qualsiasi momento prima di dover usare il modello.

configura un modello locale

Se hai in bundle il modello con la tua app, crea un oggetto CustomLocalModel, che specifica il nome file del modello TensorFlow Lite:

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];

Crea un interprete dal tuo modello

Dopo aver configurato le origini del modello, crea un oggetto ModelInterpreter da una di queste.

Se hai solo un modello in bundle locale, è sufficiente trasmettere CustomLocalModel oggetto in modelInterpreter(localModel:):

Swift

let interpreter = ModelInterpreter.modelInterpreter(localModel: localModel)

Objective-C

FIRModelInterpreter *interpreter =
    [FIRModelInterpreter modelInterpreterForLocalModel:localModel];

Se il tuo modello è ospitato in remoto, dovrai verificare che sia stato scaricato prima di eseguirlo. Puoi controllare lo stato del download del modello utilizzando il metodo isModelDownloaded(remoteModel:) del gestore del modello.

Anche se devi solo confermare prima di avviare l'interprete, se sia un modello ospitato in remoto sia uno in bundle locale, di eseguire questo controllo durante la creazione di un'istanza per ModelInterpreter: dall'interprete del modello remoto, se è stato scaricato, e dal servizio modello di machine learning.

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];
}

Se hai solo un modello ospitato in remoto, devi disattivare le funzionalità correlate al modello, ad esempio disattivare o nascondere parte dell'interfaccia utente, finché non confermi che il modello è stato scaricato.

Puoi ottenere lo stato di download del modello collegando gli osservatori all'impostazione predefinita Centro notifiche. Assicurati di utilizzare un riferimento debole a self nel blocco osservatore, poiché i download possono richiedere del tempo e l'oggetto di origine può essere liberato al termine del download. Ad esempio:

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];
            }];

Specifica l'input e l'output del modello

A questo punto, configura i formati di input e di output dell'interprete del modello.

Un modello TensorFlow Lite riceve come input e produce come output uno o più array multidimensionali. Questi array contengono byte, Valori int, long o float. Devi configurare ML Kit con il numero e le dimensioni ("forma") degli array che utilizzati dal modello.

Se non conosci la forma e il tipo di dati dell'input e output del modello, puoi utilizzare l'interprete Python di TensorFlow Lite per ispezionare il tuo modello. Ad esempio:

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'>

Dopo aver determinato il formato di input e output del modello, configura l'interprete del modello di app creando un'immagine ModelInputOutputOptions.

Ad esempio, un modello di classificazione delle immagini a virgola mobile potrebbe prendere come input un array di valori Float di Nx224x224x3, che rappresenta un batch di N immagini a tre canali (RGB) di 224 x 224 e produrre come output un elenco di 1000 valori Float, ciascuno dei quali rappresenta la probabilità che l'immagine appartenga a una delle 1000 categorie previste dal modello.

Per un modello di questo tipo, devi configurare l'input e l'output dell'interprete del modello come mostrato di seguito:

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; }

Esegui l'inferenza sui dati di input

Infine, per eseguire l'inferenza utilizzando il modello, ottenere i dati di input, eseguire le trasformazioni necessarie per il tuo modello e crea un Data oggetto che contiene i dati.

Ad esempio, se il modello elabora immagini e ha dimensioni di input di [BATCH_SIZE, 224, 224, 3] valori in virgola mobile, potresti dover scalare i valori dei colori dell'immagine in un intervallo con rappresentazione in virgola mobile, come nell'esempio seguente:

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; }

Dopo aver preparato l'input del modello (e dopo aver confermato che il modello è disponibili), passa le opzioni di input e input/output run(inputs:options:completion:) dell'interprete di modelli .

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
  // ...
}];

Puoi ottenere l'output chiamando il metodo output(index:) dell'oggetto che viene restituito. Ad esempio:

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];

Il modo in cui utilizzi l'output dipende dal modello utilizzato.

Ad esempio, se esegui la classificazione, come passaggio successivo, mappare gli indici del risultato alle etichette che rappresentano. Supponiamo che tu abbia un file di testo con stringhe di etichetta per ciascuna delle categorie del modello; potresti mappare le stringhe di etichetta alle probabilità di output eseguendo un'operazione simile seguenti:

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);
}

Appendice: Sicurezza del modello

Indipendentemente da come rendi disponibili i tuoi modelli TensorFlow Lite ML Kit, ML Kit li archivia nel formato protobuf serializzato standard in formato archiviazione locale.

In teoria, questo significa che chiunque può copiare il modello. Tuttavia, Nella pratica, la maggior parte dei modelli è così specifica per l'applicazione e offuscata secondo cui il rischio è simile a quello dello smontaggio della concorrenza a riutilizzare il codice. Tuttavia, è necessario essere consapevoli di questo rischio prima di utilizzare un modello personalizzato nella tua app.