Usar um modelo do TensorFlow Lite para inferência com o Kit de ML no iOS

Você pode usar o Kit de ML para realizar inferências no dispositivo com um modelo do TensorFlow Lite.

O Kit de ML pode usar modelos do TensorFlow Lite somente em dispositivos com o iOS 9 e versões mais recentes.

Antes de começar

  1. Se você ainda não adicionou o Firebase ao seu app, siga as etapas no guia de iniciação.
  2. Inclua as bibliotecas do Kit de ML no seu Podfile:
    pod 'Firebase/MLModelInterpreter', '6.25.0'
    
    Depois de instalar ou atualizar os pods do projeto, abra o projeto do Xcode usando o .xcworkspace.
  3. Importe o Firebase para seu app:

    Swift

    import Firebase

    Objective-C

    @import Firebase;
  4. Converta o modelo do TensorFlow que você quer usar para o formato do TensorFlow Lite. Consulte TOCO: conversor de otimização do TensorFlow Lite.

Hospedar ou agrupar seu modelo

Antes de poder usar um modelo do TensorFlow Lite para inferência no seu app, é preciso disponibilizar o modelo para o Kit de ML. O kit pode usar modelos do TensorFlow Lite hospedados remotamente com o Firebase, armazenados em pacote com o binário do aplicativo, ou ambos.

Com a hospedagem de um modelo no Firebase, você pode atualizar o modelo sem liberar uma nova versão do app e usar Remote Config e A/B Testing para exibir dinamicamente diferentes modelos para diferentes conjuntos de usuários.

Se você optar por fornecer o modelo hospedando-o apenas com o Firebase, sem agrupá-lo com o app, é possível reduzir o tamanho do download inicial do aplicativo. No entanto, se o modelo não estiver agrupado com o app, nenhuma função relacionada a ele vai estar disponível até que o app faça o download do modelo pela primeira vez.

Ao fornecer o modelo com o app, é possível garantir que os recursos de ML do app ainda funcionem quando o modelo hospedado pelo Firebase não estiver disponível.

Hospedar modelos no Firebase

Para hospedar seu modelo do TensorFlow Lite no Firebase:

  1. Na seção Kit de ML do Console do Firebase, clique na guia Personalizar.
  2. Clique em Adicionar modelo personalizado ou Adicionar outro modelo.
  3. Especifique um nome que será usado para identificar seu modelo no seu projeto do Firebase e, em seguida, faça o upload do arquivo do modelo do TensorFlow Lite (normalmente terminado em .tflite ou .lite).

Depois de adicionar um modelo personalizado ao seu projeto do Firebase, você pode referenciá-lo nos seus apps usando o nome especificado. A qualquer momento, você pode fazer o upload de um novo modelo do TensorFlow Lite, e seu aplicativo fará o download do novo modelo e começará a usá-lo quando o aplicativo for reiniciado. Você pode definir as condições do dispositivo necessárias para que seu aplicativo tente atualizar o modelo. Veja como fazer isso abaixo.

Incluir modelos em um aplicativo

Para agrupar seu modelo do TensorFlow Lite com seu aplicativo, adicione o arquivo de modelo (geralmente terminando em .tflite ou .lite) ao projeto Xcode, selecionando Copiar recursos do pacote ao fazer isso. então. O arquivo de modelo será incluído no pacote de apps e estará disponível para o Kit de ML.

Carregar o modelo

Para usar o modelo do TensorFlow Lite no app, primeiro configure o Kit de ML com os locais em que seu modelo está disponível: remotamente usando o Firebase, no armazenamento local ou ambos. Se você especificar um modelo local e remoto, poderá usar o modelo remoto se ele estiver disponível. Caso não esteja, o modelo usado será o local.

Configurar um modelo hospedado pelo Firebase

Se você hospedou seu modelo com o Firebase, crie um objeto CustomRemoteModel especificando o nome atribuído ao modelo quando o publicou:

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

Em seguida, inicie a tarefa de download do modelo, especificando as condições sob as quais você quer permitir o download. Se o modelo não estiver no dispositivo ou se uma versão mais recente do modelo estiver disponível, a tarefa fará o download do modelo de forma assíncrona do 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];

Muitos apps iniciam a tarefa de download no código de inicialização, mas você pode fazer isso a qualquer momento antes de precisar usar o modelo.

Configurar um modelo local

Se você agrupou o modelo com o aplicativo, crie um objeto CustomLocalModel, especificando o nome do arquivo do modelo do 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];

Criar um interpretador usando o modelo

Depois de configurar as origens do modelo, crie um objeto ModelInterpreter a partir de uma delas.

Se você tiver apenas um modelo agrupado localmente, basta transmitir o objeto CustomLocalModel para modelInterpreter(localModel:):

Swift

let interpreter = ModelInterpreter.modelInterpreter(localModel: localModel)

Objective-C

FIRModelInterpreter *interpreter =
    [FIRModelInterpreter modelInterpreterForLocalModel:localModel];

Se você tiver um modelo hospedado remotamente, será necessário verificar se foi feito o download dele antes de executá-lo. É possível verificar o status da tarefa de download do modelo usando o método isModelDownloaded(remoteModel:) do gerenciador de modelos.

Embora você só precise confirmar isso antes de executar o interpretador, se você tiver um modelo hospedado remotamente e um modelo agrupado localmente, talvez faça sentido realizar essa verificação ao instanciar o ModelInterpreter: crie um interpretador a partir do remoto modelo se ele tiver sido baixado e a partir do modelo local, caso contrário.

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 você tiver apenas um modelo hospedado remotamente, desative o recurso relacionado ao modelo (por exemplo, ocultando ou esmaecendo parte da IU) até confirmar que o download do modelo foi concluído.

Também é possível receber o status de download do modelo. Para fazer isso, basta enviar observadores à Central de Notificações padrão: Use uma referência fraca a self no bloco de observadores, já que os downloads podem demorar um pouco, e o objeto de origem vai ser liberado quando o download for concluído. Exemplo:

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

Especificar a entrada e saída do modelo

O próximo passo é configurar os formatos de entrada e saída do interpretador do modelo.

Um modelo do TensorFlow Lite utiliza como entrada e produz como saída uma ou mais matrizes multidimensionais. Essas matrizes contêm valores byte, int, long ou float. É preciso configurar o kit de ML com o número e as dimensões ("forma") das matrizes usadas pelo modelo.

Se você não sabe qual é a forma e o tipo de dados da entrada e da saída do seu modelo, pode usar o interpretador do TensorFlow Lite em Python para inspecionar seu modelo. Exemplo:

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

Depois de determinar o formato de entrada e saída do modelo, configure o interpretador de modelos do app criando um objeto ModelInputOutputOptions.

Por exemplo, um modelo de classificação de imagem de ponto flutuante pode usar como entrada uma matriz N x224x224x3 de valores Float, representando um lote de N imagens de três canais (RGB) de 224x224 e produzir como saída uma lista de 1.000 valores Float, cada um representando a probabilidade de a imagem fazer parte de uma das 1.000 categorias previstas pelo modelo.

Para um modelo assim, você configuraria a entrada e saída do interpretador de modelo conforme abaixo:

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

Realizar inferência em dados de entrada

Por fim, para realizar a inferência usando o modelo, receba os dados de entrada, faça as transformações necessárias nos dados e crie um objeto Data que contenha os dados.

Por exemplo, se o modelo processar imagens e seu modelo tiver dimensões de entrada de valores de ponto flutuante [BATCH_SIZE, 224, 224, 3], talvez seja necessário dimensionar os valores de cor da imagem para um intervalo de ponto flutuante, como no exemplo a seguir:

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

Depois de preparar a entrada do modelo (e depois de confirmar que o modelo está disponível), transmita as opções de entrada e entrada/saída para o método run(inputs:options:completion:) do interpretador do modelo.

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

É possível receber a saída chamando método output(index:) do objeto retornado. Por exemplo:

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

Como você usa a saída depende do modelo que está usando.

Por exemplo, se você estiver realizando uma classificação, como próxima etapa, será possível mapear os índices do resultado para os rótulos representados. Suponha que você tenha um arquivo de texto com strings de rótulo para cada uma das categorias de seu modelo; você poderia mapear as strings de rótulo para as probabilidades de saída, fazendo algo assim:

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

Apêndice: segurança do modelo

Independentemente de como você disponibiliza seus modelos do TensorFlow Lite para o Kit de ML, o kit os armazena localmente no formato padrão protobuf serializado.

Teoricamente, isso significa que qualquer pessoa pode copiar seu modelo. No entanto, na prática, a maioria dos modelos é tão específica de cada aplicativo e ofuscada por otimizações que o risco é comparável ao de concorrentes desmontando e reutilizando seu código. Apesar disso, você deve estar ciente desse risco antes de usar um modelo personalizado no seu app.