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

Puoi utilizzare ML Kit per eseguire l'inferenza sul dispositivo con un modello TensorFlow Lite .

ML Kit può utilizzare i modelli TensorFlow Lite solo su dispositivi con iOS 9 e versioni successive.

Prima di iniziare

  1. Se non hai già aggiunto Firebase alla tua app, fallo seguendo i passaggi nella guida introduttiva .
  2. Includi le librerie ML Kit nel tuo Podfile:
    pod 'Firebase/MLModelInterpreter', '6.25.0'
    
    Dopo aver installato o aggiornato i Pod del tuo progetto, assicurati di aprire il tuo progetto Xcode utilizzando il relativo .xcworkspace .
  3. Nella tua app, importa Firebase:

    Veloce

    import Firebase

    Obiettivo-C

    @import Firebase;
  4. Converti il ​​modello TensorFlow che desideri utilizzare nel formato TensorFlow Lite. Vedere TOCO: Convertitore di ottimizzazione TensorFlow Lite .

Ospita o raggruppa il tuo modello

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

Ospitando un modello su Firebase, puoi aggiornare il modello senza rilasciare una nuova versione dell'app e puoi utilizzare Remote Config e A/B Testing per servire dinamicamente modelli diversi a diversi gruppi di utenti.

Se scegli di fornire il modello solo ospitandolo con Firebase e non raggruppandolo con la tua app, puoi ridurre le dimensioni di download iniziali della tua app. Tieni presente, tuttavia, che se il modello non è incluso nella tua app, qualsiasi funzionalità relativa al modello non sarà disponibile finché l'app non scaricherà il modello per la prima volta.

Raggruppando il tuo modello con la tua app, puoi assicurarti che le funzionalità ML della tua app continuino 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 sulla scheda Personalizzato .
  2. Fai clic su Aggiungi modello personalizzato (o Aggiungi un altro modello ).
  3. Specifica un nome che verrà utilizzato per identificare il tuo modello nel tuo progetto Firebase, quindi carica il file del modello TensorFlow Lite (di solito termina con .tflite o .lite ).

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

Raggruppa i modelli con un'app

Per raggruppare il tuo modello TensorFlow Lite con la tua app, aggiungi il file del modello (di solito che termina con .tflite o .lite ) al tuo progetto Xcode, avendo cura di selezionare Copia risorse del pacchetto quando lo fai. Il file del modello verrà incluso nel pacchetto dell'app e sarà disponibile per ML Kit.

Carica il modello

Per utilizzare il tuo modello TensorFlow Lite nella tua app, configura innanzitutto ML Kit con le posizioni in cui il tuo modello è disponibile: in remoto utilizzando Firebase, nello spazio di archiviazione locale o entrambi. Se specifichi sia un modello locale che uno remoto, puoi utilizzare il modello remoto se è disponibile e ricorrere al modello 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 che hai assegnato al modello quando lo hai pubblicato:

Veloce

let remoteModel = CustomRemoteModel(
  name: "your_remote_model"  // The name you assigned in the Firebase console.
)

Obiettivo-C

// Initialize using the name you assigned in the Firebase console.
FIRCustomRemoteModel *remoteModel =
    [[FIRCustomRemoteModel alloc] initWithName:@"your_remote_model"];

Avvia quindi l'attività di download del modello, specificando le condizioni alle quali desideri consentire il download. Se il modello non è presente sul dispositivo o se è disponibile una versione più recente del modello, l'attività scaricherà in modo asincrono il modello da Firebase:

Veloce

let downloadConditions = ModelDownloadConditions(
  allowsCellularAccess: true,
  allowsBackgroundDownloading: true
)

let downloadProgress = ModelManager.modelManager().download(
  remoteModel,
  conditions: downloadConditions
)

Obiettivo-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 in qualsiasi momento prima di dover utilizzare il modello.

Configurare un modello locale

Se hai raggruppato il modello con la tua app, crea un oggetto CustomLocalModel , specificando il nome file del modello TensorFlow Lite:

Veloce

guard let modelPath = Bundle.main.path(
  forResource: "your_model",
  ofType: "tflite",
  inDirectory: "your_model_directory"
) else { /* Handle error. */ }
let localModel = CustomLocalModel(modelPath: modelPath)

Obiettivo-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 uno di essi.

Se disponi solo di un modello raggruppato localmente, passa semplicemente l'oggetto CustomLocalModel a modelInterpreter(localModel:) :

Veloce

let interpreter = ModelInterpreter.modelInterpreter(localModel: localModel)

Obiettivo-C

FIRModelInterpreter *interpreter =
    [FIRModelInterpreter modelInterpreterForLocalModel:localModel];

Se disponi di un modello ospitato in remoto, dovrai verificare che sia stato scaricato prima di eseguirlo. È possibile verificare lo stato dell'attività di download del modello utilizzando il metodo isModelDownloaded(remoteModel:) del gestore modelli.

Sebbene sia necessario confermarlo solo prima di eseguire l'interprete, se si dispone sia di un modello ospitato in remoto che di un modello raggruppato localmente, potrebbe avere senso eseguire questo controllo quando si istanzia ModelInterpreter : creare un interprete dal modello remoto se è stato scaricato e altrimenti dal modello locale.

Veloce

var interpreter: ModelInterpreter
if ModelManager.modelManager().isModelDownloaded(remoteModel) {
  interpreter = ModelInterpreter.modelInterpreter(remoteModel: remoteModel)
} else {
  interpreter = ModelInterpreter.modelInterpreter(localModel: localModel)
}

Obiettivo-C

FIRModelInterpreter *interpreter;
if ([[FIRModelManager modelManager] isModelDownloaded:remoteModel]) {
  interpreter = [FIRModelInterpreter modelInterpreterForRemoteModel:remoteModel];
} else {
  interpreter = [FIRModelInterpreter modelInterpreterForLocalModel:localModel];
}

Se disponi solo di un modello ospitato in remoto, dovresti disabilitare le funzionalità relative al modello, ad esempio disattivare o nascondere parte della tua interfaccia utente, finché non confermi che il modello è stato scaricato.

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

Veloce

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

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

Specificare l'input e l'output del modello

Successivamente, configura i formati di input e output dell'interprete del modello.

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

Se non conosci la forma e il tipo di dati dell'input e dell'output del tuo modello, puoi utilizzare l'interprete Python TensorFlow Lite per ispezionare il tuo modello. Per 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 dell'input e dell'output del modello, configura l'interprete del modello dell'app creando un oggetto ModelInputOutputOptions .

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

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

Veloce

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

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

Eseguire l'inferenza sui dati di input

Infine, per eseguire l'inferenza utilizzando il modello, ottieni i dati di input, esegui eventuali trasformazioni sui dati che potrebbero essere necessarie per il tuo modello e crea un oggetto Data che contenga i dati.

Ad esempio, se il tuo modello elabora immagini e ha dimensioni di input di [BATCH_SIZE, 224, 224, 3] valori a virgola mobile, potresti dover ridimensionare i valori di colore dell'immagine su un intervallo a virgola mobile come nell'esempio seguente :

Veloce

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

Obiettivo-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 è disponibile), passa le opzioni di input e input/output al metodo run(inputs:options:completion:) dell'interprete del modello .

Veloce

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

Obiettivo-C

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

È possibile ottenere l'output chiamando il metodo output(index:) dell'oggetto restituito. Per esempio:

Veloce

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

Obiettivo-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 che stai utilizzando.

Ad esempio, se stai eseguendo la classificazione, come passaggio successivo potresti associare gli indici del risultato alle etichette che rappresentano. Supponiamo di avere un file di testo con stringhe di etichette per ciascuna delle categorie del modello; potresti mappare le stringhe di etichetta alle probabilità di output facendo qualcosa di simile al seguente:

Veloce

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

Obiettivo-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 i tuoi modelli TensorFlow Lite disponibili per ML Kit, ML Kit li archivia nel formato protobuf serializzato standard nell'archivio locale.

In teoria, ciò significa che chiunque può copiare il tuo modello. Tuttavia, in pratica, la maggior parte dei modelli sono così specifici per l'applicazione e offuscati dalle ottimizzazioni che il rischio è simile a quello dei concorrenti che disassemblano e riutilizzano il codice. Tuttavia, dovresti essere consapevole di questo rischio prima di utilizzare un modello personalizzato nella tua app.