Utilice un modelo de TensorFlow Lite para realizar inferencias con ML Kit en iOS

Puede usar ML Kit para realizar inferencias en el dispositivo con un modelo de TensorFlow Lite .

ML Kit puede usar modelos de TensorFlow Lite solo en dispositivos con iOS 9 y versiones posteriores.

Antes de que empieces

  1. Si aún no has agregado Firebase a tu aplicación, hazlo siguiendo los pasos de la guía de introducción .
  2. Incluya las bibliotecas del kit ML en su Podfile:
    pod 'Firebase/MLModelInterpreter', '6.25.0'
    
    Después de instalar o actualizar los Pods de su proyecto, asegúrese de abrir su proyecto Xcode usando su .xcworkspace .
  3. En tu aplicación, importa Firebase:

    Rápido

    import Firebase

    C objetivo

    @import Firebase;
  4. Convierta el modelo de TensorFlow que desea usar al formato TensorFlow Lite. Consulte TOCO: Convertidor de optimización de TensorFlow Lite .

Aloja o agrupa tu modelo

Antes de poder usar un modelo de TensorFlow Lite para realizar inferencias en su aplicación, debe poner el modelo a disposición del kit de aprendizaje automático. ML Kit puede usar modelos de TensorFlow Lite alojados de forma remota mediante Firebase, incluidos con el binario de la aplicación o ambos.

Al alojar un modelo en Firebase, puedes actualizar el modelo sin lanzar una nueva versión de la aplicación, y puedes usar Remote Config y A/B Testing para servir dinámicamente diferentes modelos a diferentes conjuntos de usuarios.

Si elige proporcionar el modelo únicamente alojándolo en Firebase y no incluirlo con su aplicación, puede reducir el tamaño de descarga inicial de su aplicación. Sin embargo, tenga en cuenta que si el modelo no está incluido con su aplicación, cualquier funcionalidad relacionada con el modelo no estará disponible hasta que su aplicación descargue el modelo por primera vez.

Al combinar tu modelo con tu aplicación, puedes asegurarte de que las funciones de aprendizaje automático de tu aplicación sigan funcionando cuando el modelo alojado en Firebase no esté disponible.

Modelos de host en Firebase

Para alojar su modelo de TensorFlow Lite en Firebase:

  1. En la sección ML Kit de Firebase console , haz clic en la pestaña Personalizado .
  2. Haga clic en Agregar modelo personalizado (o Agregar otro modelo ).
  3. Especifica un nombre que se utilizará para identificar tu modelo en tu proyecto de Firebase, luego carga el archivo del modelo de TensorFlow Lite (que generalmente termina en .tflite o .lite ).

Después de agregar un modelo personalizado a tu proyecto de Firebase, puedes hacer referencia al modelo en tus aplicaciones usando el nombre que especificaste. En cualquier momento, puede cargar un nuevo modelo de TensorFlow Lite y su aplicación descargará el nuevo modelo y comenzará a usarlo la próxima vez que se reinicie. Puede definir las condiciones del dispositivo requeridas para que su aplicación intente actualizar el modelo (ver más abajo).

Combinar modelos con una aplicación

Para agrupar su modelo de TensorFlow Lite con su aplicación, agregue el archivo del modelo (que generalmente termina en .tflite o .lite ) a su proyecto Xcode, teniendo cuidado de seleccionar Copiar recursos del paquete cuando lo haga. El archivo del modelo se incluirá en el paquete de la aplicación y estará disponible para ML Kit.

Cargar el modelo

Para usar tu modelo de TensorFlow Lite en tu aplicación, primero configura ML Kit con las ubicaciones donde tu modelo está disponible: de forma remota usando Firebase, en almacenamiento local o ambos. Si especifica un modelo local y remoto, puede usar el modelo remoto si está disponible y recurrir al modelo almacenado localmente si el modelo remoto no está disponible.

Configurar un modelo alojado en Firebase

Si alojaste tu modelo con Firebase, crea un objeto CustomRemoteModel , especificando el nombre que le asignaste al modelo cuando lo publicaste:

Rápido

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

C objetivo

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

Luego, inicie la tarea de descarga del modelo, especificando las condiciones bajo las cuales desea permitir la descarga. Si el modelo no está en el dispositivo, o si hay una versión más reciente del modelo disponible, la tarea descargará el modelo de forma asincrónica desde Firebase:

Rápido

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

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

C objetivo

FIRModelDownloadConditions *downloadConditions =
    [[FIRModelDownloadConditions alloc] initWithAllowsCellularAccess:YES
                                         allowsBackgroundDownloading:YES];

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

Muchas aplicaciones inician la tarea de descarga en su código de inicialización, pero puede hacerlo en cualquier momento antes de necesitar usar el modelo.

Configurar un modelo local

Si incluiste el modelo con tu aplicación, crea un objeto CustomLocalModel , especificando el nombre de archivo del modelo de TensorFlow Lite:

Rápido

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

C objetivo

NSString *modelPath = [NSBundle.mainBundle pathForResource:@"your_model"
                                                    ofType:@"tflite"
                                               inDirectory:@"your_model_directory"];
FIRCustomLocalModel *localModel =
    [[FIRCustomLocalModel alloc] initWithModelPath:modelPath];

Crea un intérprete a partir de tu modelo.

Después de configurar las fuentes de su modelo, cree un objeto ModelInterpreter a partir de una de ellas.

Si solo tiene un modelo empaquetado localmente, simplemente pase el objeto CustomLocalModel a modelInterpreter(localModel:) :

Rápido

let interpreter = ModelInterpreter.modelInterpreter(localModel: localModel)

C objetivo

FIRModelInterpreter *interpreter =
    [FIRModelInterpreter modelInterpreterForLocalModel:localModel];

Si tiene un modelo alojado de forma remota, deberá comprobar que se haya descargado antes de ejecutarlo. Puede verificar el estado de la tarea de descarga del modelo utilizando el método isModelDownloaded(remoteModel:) del administrador de modelos.

Aunque solo tiene que confirmar esto antes de ejecutar el intérprete, si tiene un modelo alojado de forma remota y un modelo empaquetado localmente, podría tener sentido realizar esta verificación al crear una instancia de ModelInterpreter : cree un intérprete a partir del modelo remoto si es descargado, y del modelo local en caso contrario.

Rápido

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

C objetivo

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

Si solo tiene un modelo alojado de forma remota, debe desactivar la funcionalidad relacionada con el modelo (por ejemplo, atenuar u ocultar parte de su interfaz de usuario) hasta que confirme que el modelo se ha descargado.

Puede obtener el estado de descarga del modelo adjuntando observadores al Centro de notificaciones predeterminado. Asegúrese de utilizar una referencia débil a self en el bloque de observador, ya que las descargas pueden tardar algún tiempo y el objeto de origen puede liberarse cuando finaliza la descarga. Por ejemplo:

Rápido

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

C objetivo

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

Especificar la entrada y salida del modelo.

A continuación, configure los formatos de entrada y salida del intérprete de modelos.

Un modelo de TensorFlow Lite toma como entrada y produce como salida una o más matrices multidimensionales. Estas matrices contienen valores byte , int , long o float . Debes configurar ML Kit con el número y las dimensiones ("forma") de las matrices que utiliza tu modelo.

Si no conoce la forma y el tipo de datos de la entrada y salida de su modelo, puede usar el intérprete de Python de TensorFlow Lite para inspeccionar su modelo. Por ejemplo:

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

Después de determinar el formato de entrada y salida de su modelo, configure el intérprete de modelo de su aplicación creando un objeto ModelInputOutputOptions .

Por ejemplo, un modelo de clasificación de imágenes de punto flotante podría tomar como entrada una matriz N x224x224x3 de valores Float , que representa un lote de N imágenes de tres canales (RGB) de 224x224, y producir como salida una lista de 1000 valores Float , cada uno de los cuales representa el probabilidad de que la imagen sea miembro de una de las 1000 categorías que predice el modelo.

Para dicho modelo, configuraría la entrada y salida del intérprete del modelo como se muestra a continuación:

Rápido

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

C objetivo

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

Realizar inferencias sobre datos de entrada.

Finalmente, para realizar inferencias usando el modelo, obtenga sus datos de entrada, realice cualquier transformación en los datos que pueda ser necesaria para su modelo y cree un objeto Data que contenga los datos.

Por ejemplo, si su modelo procesa imágenes y su modelo tiene dimensiones de entrada de [BATCH_SIZE, 224, 224, 3] valores de punto flotante, es posible que deba escalar los valores de color de la imagen a un rango de punto flotante como en el siguiente ejemplo. :

Rápido

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

C objetivo

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

Después de preparar la entrada de su modelo (y después de confirmar que el modelo está disponible), pase las opciones de entrada y entrada/salida al método run(inputs:options:completion:) de su intérprete de modelo .

Rápido

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

C objetivo

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

Puede obtener el resultado llamando al método output(index:) del objeto que se devuelve. Por ejemplo:

Rápido

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

C objetivo

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

La forma de utilizar la salida depende del modelo que esté utilizando.

Por ejemplo, si está realizando una clasificación, como siguiente paso, puede asignar los índices del resultado a las etiquetas que representan. Suponga que tiene un archivo de texto con cadenas de etiquetas para cada una de las categorías de su modelo; podría asignar las cadenas de etiquetas a las probabilidades de salida haciendo algo como lo siguiente:

Rápido

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

C objetivo

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

Apéndice: Seguridad del modelo

Independientemente de cómo haga que sus modelos de TensorFlow Lite estén disponibles para ML Kit, ML Kit los almacena en el formato protobuf serializado estándar en el almacenamiento local.

En teoría, esto significa que cualquiera puede copiar su modelo. Sin embargo, en la práctica, la mayoría de los modelos son tan específicos de la aplicación y están tan confusos por las optimizaciones que el riesgo es similar al de que los competidores desensamblen y reutilicen su código. Sin embargo, debes ser consciente de este riesgo antes de utilizar un modelo personalizado en tu aplicación.