Premiers pas avec LiteRT

Ce guide vous présente le processus d'exécution d'un modèle LiteRT (Lite Runtime) sur l'appareil pour effectuer des prédictions à partir des données d'entrée. Pour ce faire, nous utilisons l'interpréteur LiteRT, qui utilise un ordre de graphique statique et un allocateur de mémoire personnalisé (moins dynamique) pour garantir une latence de chargement, d'initialisation et d'exécution minimale.

L'inférence LiteRT suit généralement les étapes suivantes :

  1. Charger un modèle : chargez le modèle .tflite en mémoire, qui contient le graphique d'exécution du modèle.

  2. Transformation des données : transformez les données d'entrée au format et dans les dimensions attendus. Les données d'entrée brutes du modèle ne correspondent généralement pas au format de données d'entrée attendu par le modèle. Par exemple, vous devrez peut-être redimensionner une image ou modifier son format pour qu'elle soit compatible avec le modèle.

  3. Exécuter l'inférence : exécutez le modèle LiteRT pour faire des prédictions. Cette étape consiste à utiliser l'API LiteRT pour exécuter le modèle. Il comporte plusieurs étapes, comme la création de l'interpréteur et l'allocation des Tensors.

  4. Interpréter la sortie : interprétez les Tensors de sortie de manière pertinente et utile dans votre application. Par exemple, un modèle peut ne renvoyer qu'une liste de probabilités. Il vous appartient de mapper les probabilités sur les catégories pertinentes et de mettre en forme le résultat.

Ce guide explique comment accéder à l'interpréteur LiteRT et effectuer une inférence à l'aide de C++, Java et Python.

Plates-formes compatibles

Les API d'inférence TensorFlow sont fournies pour la plupart des plates-formes mobiles et embarquées courantes, telles qu'Android, iOS et Linux, dans plusieurs langages de programmation.

Dans la plupart des cas, la conception de l'API privilégie les performances à la facilité d'utilisation. LiteRT est conçu pour une inférence rapide sur les petits appareils. Les API évitent donc les copies inutiles au détriment de la commodité.

Dans toutes les bibliothèques, l'API LiteRT vous permet de charger des modèles, d'alimenter des entrées et de récupérer des sorties d'inférence.

Plate-forme Android

Sur Android, l'inférence LiteRT peut être effectuée à l'aide des API Java ou C++. Les API Java sont pratiques et peuvent être utilisées directement dans vos classes d'activité Android. Les API C++ offrent plus de flexibilité et de rapidité, mais peuvent nécessiter l'écriture de wrappers JNI pour déplacer les données entre les couches Java et C++.

Pour en savoir plus, consultez les sections C++ et Java, ou suivez le guide de démarrage rapide Android.

Plate-forme iOS

Sur iOS, LiteRT est disponible dans les bibliothèques iOS Swift et Objective-C. Vous pouvez également utiliser l'API C directement dans le code Objective-C.

Consultez les sections Swift, Objective-C et API C, ou suivez le guide de démarrage rapide pour iOS.

Plate-forme Linux

Sur les plates-formes Linux, vous pouvez exécuter des inférences à l'aide des API LiteRT disponibles en C++.

Charger et exécuter un modèle

Le chargement et l'exécution d'un modèle LiteRT impliquent les étapes suivantes :

  1. Chargement du modèle en mémoire.
  2. Créer un Interpreter à partir d'un modèle existant
  3. Définir les valeurs du Tensor d'entrée.
  4. Appel des inférences.
  5. Affichage des valeurs Tensor.

Android (Java)

L'API Java pour exécuter des inférences avec LiteRT est principalement conçue pour être utilisée avec Android. Elle est donc disponible en tant que dépendance de bibliothèque Android : com.google.ai.edge.litert.

En Java, vous utiliserez la classe Interpreter pour charger un modèle et générer une inférence de modèle. Dans de nombreux cas, il s'agit de la seule API dont vous avez besoin.

Vous pouvez initialiser un Interpreter à l'aide d'un fichier FlatBuffers (.tflite) :

public Interpreter(@NotNull File modelFile);

Ou avec un MappedByteBuffer :

public Interpreter(@NotNull MappedByteBuffer mappedByteBuffer);

Dans les deux cas, vous devez fournir un modèle LiteRT valide, sinon l'API génère IllegalArgumentException. Si vous utilisez MappedByteBuffer pour initialiser un Interpreter, il doit rester inchangé pendant toute la durée de vie du Interpreter.

La méthode recommandée pour exécuter l'inférence sur un modèle consiste à utiliser des signatures. Cette méthode est disponible pour les modèles convertis à partir de TensorFlow 2.5.

try (Interpreter interpreter = new Interpreter(file_of_tensorflowlite_model)) {
  Map<String, Object> inputs = new HashMap<>();
  inputs.put("input_1", input1);
  inputs.put("input_2", input2);
  Map<String, Object> outputs = new HashMap<>();
  outputs.put("output_1", output1);
  interpreter.runSignature(inputs, outputs, "mySignature");
}

La méthode runSignature comporte trois arguments :

  • Entrées : mappage des entrées à partir du nom d'entrée dans la signature vers un objet d'entrée.

  • Sorties : mappage de la sortie (nom de la sortie dans la signature) aux données de sortie.

  • Nom de la signature (facultatif) : nom de la signature (peut être laissé vide si le modèle ne comporte qu'une seule signature).

Autre façon d'exécuter des inférences lorsque le modèle ne possède pas de signatures définies. Il vous suffit d'appeler le Interpreter.run(). Exemple :

try (Interpreter interpreter = new Interpreter(file_of_a_tensorflowlite_model)) {
  interpreter.run(input, output);
}

La méthode run() n'accepte qu'une seule entrée et ne renvoie qu'une seule sortie. Par conséquent, si votre modèle comporte plusieurs entrées ou sorties, utilisez plutôt :

interpreter.runForMultipleInputsOutputs(inputs, map_of_indices_to_outputs);

Dans ce cas, chaque entrée de inputs correspond à un Tensor d'entrée et map_of_indices_to_outputs mappe les index des Tensors de sortie aux données de sortie correspondantes.

Dans les deux cas, les index de Tensor doivent correspondre aux valeurs que vous avez attribuées à LiteRT Converter lorsque vous avez créé le modèle. Notez que l'ordre des Tensors dans input doit correspondre à l'ordre donné au convertisseur LiteRT.

La classe Interpreter fournit également des fonctions pratiques pour obtenir l'index de n'importe quelle entrée ou sortie de modèle à l'aide d'un nom d'opération :

public int getInputIndex(String opName);
public int getOutputIndex(String opName);

Si opName n'est pas une opération valide dans le modèle, une IllegalArgumentException est générée.

Notez également que Interpreter possède des ressources. Pour éviter les fuites de mémoire, les ressources doivent être libérées après utilisation par :

interpreter.close();

Pour obtenir un exemple de projet avec Java, consultez l'exemple d'application Android de détection d'objets.

Types de données acceptés

Pour utiliser LiteRT, les types de données des Tensors d'entrée et de sortie doivent être l'un des types primitifs suivants :

  • float
  • int
  • long
  • byte

Les types String sont également acceptés, mais ils sont encodés différemment des types primitifs. En particulier, la forme d'un Tensor de chaîne dicte le nombre et l'arrangement des chaînes dans le Tensor, chaque élément étant lui-même une chaîne de longueur variable. En ce sens, la taille (en octets) du Tensor ne peut pas être calculée à partir de la forme et du type seuls. Par conséquent, les chaînes ne peuvent pas être fournies en tant qu'argument ByteBuffer unique et plat.

Si d'autres types de données sont utilisés, y compris des types boxed tels que Integer et Float, une IllegalArgumentException sera générée.

Entrées

Chaque entrée doit être un tableau ou un tableau multidimensionnel des types primitifs compatibles, ou un ByteBuffer brut de la taille appropriée. Si l'entrée est un tableau ou un tableau multidimensionnel, le Tensor d'entrée associé sera redimensionné de manière implicite aux dimensions du tableau au moment de l'inférence. Si l'entrée est un ByteBuffer, l'appelant doit d'abord redimensionner manuellement le Tensor d'entrée associé (via Interpreter.resizeInput()) avant d'exécuter l'inférence.

Lorsque vous utilisez ByteBuffer, préférez les tampons d'octets directs, car cela permet à Interpreter d'éviter les copies inutiles. Si ByteBuffer est un tampon d'octets direct, son ordre doit être ByteOrder.nativeOrder(). Une fois qu'il est utilisé pour l'inférence d'un modèle, il doit rester inchangé jusqu'à la fin de l'inférence du modèle.

Sorties

Chaque sortie doit être un tableau ou un tableau multidimensionnel des types primitifs compatibles, ou un ByteBuffer de la taille appropriée. Notez que certains modèles ont des sorties dynamiques, où la forme des Tensors de sortie peut varier en fonction de l'entrée. Il n'existe pas de moyen simple de gérer cela avec l'API d'inférence Java existante, mais les extensions prévues le permettront.

iOS (Swift)

L'API Swift est disponible dans le pod TensorFlowLiteSwift de CocoaPods.

Tout d'abord, vous devez importer le module TensorFlowLite.

import TensorFlowLite
// Getting model path
guard
  let modelPath = Bundle.main.path(forResource: "model", ofType: "tflite")
else {
  // Error handling...
}

do {
  // Initialize an interpreter with the model.
  let interpreter = try Interpreter(modelPath: modelPath)

  // Allocate memory for the model's input `Tensor`s.
  try interpreter.allocateTensors()

  let inputData: Data  // Should be initialized

  // input data preparation...

  // Copy the input data to the input `Tensor`.
  try self.interpreter.copy(inputData, toInputAt: 0)

  // Run inference by invoking the `Interpreter`.
  try self.interpreter.invoke()

  // Get the output `Tensor`
  let outputTensor = try self.interpreter.output(at: 0)

  // Copy output to `Data` to process the inference results.
  let outputSize = outputTensor.shape.dimensions.reduce(1, {x, y in x * y})
  let outputData =
        UnsafeMutableBufferPointer<Float32>.allocate(capacity: outputSize)
  outputTensor.data.copyBytes(to: outputData)

  if (error != nil) { /* Error handling... */ }
} catch error {
  // Error handling...
}

iOS (Objective-C)

L'API Objective-C est disponible dans le pod LiteRTObjC de Cocoapods.

Tout d'abord, vous devez importer le module TensorFlowLiteObjC.

@import TensorFlowLite;
NSString *modelPath = [[NSBundle mainBundle] pathForResource:@"model"
                                                      ofType:@"tflite"];
NSError *error;

// Initialize an interpreter with the model.
TFLInterpreter *interpreter = [[TFLInterpreter alloc] initWithModelPath:modelPath
                                                                  error:&error];
if (error != nil) { /* Error handling... */ }

// Allocate memory for the model's input `TFLTensor`s.
[interpreter allocateTensorsWithError:&error];
if (error != nil) { /* Error handling... */ }

NSMutableData *inputData;  // Should be initialized
// input data preparation...

// Get the input `TFLTensor`
TFLTensor *inputTensor = [interpreter inputTensorAtIndex:0 error:&error];
if (error != nil) { /* Error handling... */ }

// Copy the input data to the input `TFLTensor`.
[inputTensor copyData:inputData error:&error];
if (error != nil) { /* Error handling... */ }

// Run inference by invoking the `TFLInterpreter`.
[interpreter invokeWithError:&error];
if (error != nil) { /* Error handling... */ }

// Get the output `TFLTensor`
TFLTensor *outputTensor = [interpreter outputTensorAtIndex:0 error:&error];
if (error != nil) { /* Error handling... */ }

// Copy output to `NSData` to process the inference results.
NSData *outputData = [outputTensor dataWithError:&error];
if (error != nil) { /* Error handling... */ }

API C dans le code Objective-C

L'API Objective-C n'est pas compatible avec les délégués. Pour utiliser des délégués avec du code Objective-C, vous devez appeler directement l'API C sous-jacente.

#include "tensorflow/lite/c/c_api.h"
TfLiteModel* model = TfLiteModelCreateFromFile([modelPath UTF8String]);
TfLiteInterpreterOptions* options = TfLiteInterpreterOptionsCreate();

// Create the interpreter.
TfLiteInterpreter* interpreter = TfLiteInterpreterCreate(model, options);

// Allocate tensors and populate the input tensor data.
TfLiteInterpreterAllocateTensors(interpreter);
TfLiteTensor* input_tensor =
    TfLiteInterpreterGetInputTensor(interpreter, 0);
TfLiteTensorCopyFromBuffer(input_tensor, input.data(),
                           input.size() * sizeof(float));

// Execute inference.
TfLiteInterpreterInvoke(interpreter);

// Extract the output tensor data.
const TfLiteTensor* output_tensor =
    TfLiteInterpreterGetOutputTensor(interpreter, 0);
TfLiteTensorCopyToBuffer(output_tensor, output.data(),
                         output.size() * sizeof(float));

// Dispose of the model and interpreter objects.
TfLiteInterpreterDelete(interpreter);
TfLiteInterpreterOptionsDelete(options);
TfLiteModelDelete(model);

C++

L'API C++ pour exécuter l'inférence avec LiteRT est compatible avec les plates-formes Android, iOS et Linux. L'API C++ sur iOS n'est disponible que lorsque vous utilisez bazel.

En C++, le modèle est stocké dans la classe FlatBufferModel. Il encapsule un modèle LiteRT et peut être créé de différentes manières, selon l'emplacement où le modèle est stocké :

class FlatBufferModel {
  // Build a model based on a file. Return a nullptr in case of failure.
  static std::unique_ptr<FlatBufferModel> BuildFromFile(
      const char* filename,
      ErrorReporter* error_reporter);

  // Build a model based on a pre-loaded flatbuffer. The caller retains
  // ownership of the buffer and should keep it alive until the returned object
  // is destroyed. Return a nullptr in case of failure.
  static std::unique_ptr<FlatBufferModel> BuildFromBuffer(
      const char* buffer,
      size_t buffer_size,
      ErrorReporter* error_reporter);
};

Maintenant que vous avez le modèle en tant qu'objet FlatBufferModel, vous pouvez l'exécuter avec un Interpreter. Un même FlatBufferModel peut être utilisé simultanément par plusieurs Interpreter.

Les parties importantes de l'API Interpreter sont présentées dans l'extrait de code ci-dessous. À noter :

  • Les Tensors sont représentés par des entiers afin d'éviter les comparaisons de chaînes (et toute dépendance fixe aux bibliothèques de chaînes).
  • Un interprète ne doit pas être accessible à partir de threads simultanés.
  • L'allocation de mémoire pour les Tensors d'entrée et de sortie doit être déclenchée en appelant AllocateTensors() juste après le redimensionnement des Tensors.

L'utilisation la plus simple de LiteRT avec C++ ressemble à ceci :

// Load the model
std::unique_ptr<tflite::FlatBufferModel> model =
    tflite::FlatBufferModel::BuildFromFile(filename);

// Build the interpreter
tflite::ops::builtin::BuiltinOpResolver resolver;
std::unique_ptr<tflite::Interpreter> interpreter;
tflite::InterpreterBuilder(*model, resolver)(&interpreter);

// Resize input tensors, if needed.
interpreter->AllocateTensors();

float* input = interpreter->typed_input_tensor<float>(0);
// Fill `input`.

interpreter->Invoke();

float* output = interpreter->typed_output_tensor<float>(0);

Pour obtenir d'autres exemples de code, consultez minimal.cc et label_image.cc.

Python

L'API Python pour exécuter des inférences utilise Interpreter pour charger un modèle et exécuter des inférences.

Installez le package LiteRT :

$ python3 -m pip install ai-edge-litert

Importer l'interpréteur LiteRT

from ai_edge_litert.interpreter import Interpreter
Interpreter = Interpreter(model_path=args.model.file)

L'exemple suivant montre comment utiliser l'interpréteur Python pour charger un fichier FlatBuffers (.tflite) et exécuter l'inférence avec des données d'entrée aléatoires :

Cet exemple est recommandé si vous effectuez une conversion à partir de SavedModel avec un SignatureDef défini.

class TestModel(tf.Module):
  def __init__(self):
    super(TestModel, self).__init__()

  @tf.function(input_signature=[tf.TensorSpec(shape=[1, 10], dtype=tf.float32)])
  def add(self, x):
    '''
    Simple method that accepts single input 'x' and returns 'x' + 4.
    '''
    # Name the output 'result' for convenience.
    return {'result' : x + 4}

SAVED_MODEL_PATH = 'content/saved_models/test_variable'
TFLITE_FILE_PATH = 'content/test_variable.tflite'

# Save the model
module = TestModel()
# You can omit the signatures argument and a default signature name will be
# created with name 'serving_default'.
tf.saved_model.save(
    module, SAVED_MODEL_PATH,
    signatures={'my_signature':module.add.get_concrete_function()})

# Convert the model using TFLiteConverter
converter = tf.lite.TFLiteConverter.from_saved_model(SAVED_MODEL_PATH)
tflite_model = converter.convert()
with open(TFLITE_FILE_PATH, 'wb') as f:
  f.write(tflite_model)

# Load the LiteRT model in LiteRT Interpreter
from ai_edge_litert.interpreter import Interpreter
interpreter = Interpreter(TFLITE_FILE_PATH)

# There is only 1 signature defined in the model,
# so it will return it by default.
# If there are multiple signatures then we can pass the name.
my_signature = interpreter.get_signature_runner()

# my_signature is callable with input as arguments.
output = my_signature(x=tf.constant([1.0], shape=(1,10), dtype=tf.float32))
# 'output' is dictionary with all outputs from the inference.
# In this case we have single output 'result'.
print(output['result'])

Autre exemple si le modèle n'a pas de SignatureDefs défini.

import numpy as np
import tensorflow as tf

# Load the LiteRT model and allocate tensors.
from ai_edge_litert.interpreter import Interpreter
interpreter = Interpreter(TFLITE_FILE_PATH)
interpreter.allocate_tensors()

# Get input and output tensors.
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()

# Test the model on random input data.
input_shape = input_details[0]['shape']
input_data = np.array(np.random.random_sample(input_shape), dtype=np.float32)
interpreter.set_tensor(input_details[0]['index'], input_data)

interpreter.invoke()

# The function `get_tensor()` returns a copy of the tensor data.
# Use `tensor()` in order to get a pointer to the tensor.
output_data = interpreter.get_tensor(output_details[0]['index'])
print(output_data)

Au lieu de charger le modèle en tant que fichier .tflite préconverti, vous pouvez combiner votre code avec le compilateur LiteRT. Cela vous permet de convertir votre modèle Keras au format LiteRT, puis d'exécuter l'inférence :

import numpy as np
import tensorflow as tf

img = tf.keras.Input(shape=(64, 64, 3), name="img")
const = tf.constant([1., 2., 3.]) + tf.constant([1., 4., 4.])
val = img + const
out = tf.identity(val, name="out")

# Convert to LiteRT format
converter = tf.lite.TFLiteConverter.from_keras_model(tf.keras.models.Model(inputs=[img], outputs=[out]))
tflite_model = converter.convert()

# Load the LiteRT model and allocate tensors.
from ai_edge_litert.interpreter import Interpreter
interpreter = Interpreter(model_content=tflite_model)
interpreter.allocate_tensors()

# Continue to get tensors and so forth, as shown above...

Pour obtenir d'autres exemples de code Python, consultez label_image.py.

Exécuter l'inférence avec un modèle à forme dynamique

Si vous souhaitez exécuter un modèle avec une forme d'entrée dynamique, redimensionnez la forme d'entrée avant d'exécuter l'inférence. Sinon, la forme None dans les modèles TensorFlow sera remplacée par un espace réservé 1 dans les modèles LiteRT.

Les exemples suivants montrent comment redimensionner la forme d'entrée avant d'exécuter l'inférence dans différentes langues. Tous les exemples supposent que la forme d'entrée est définie sur [1/None, 10] et doit être redimensionnée sur [3, 10].

Exemple en C++ :

// Resize input tensors before allocate tensors
interpreter->ResizeInputTensor(/*tensor_index=*/0, std::vector<int>{3,10});
interpreter->AllocateTensors();

Exemple Python :

# Load the LiteRT model in LiteRT Interpreter
from ai_edge_litert.interpreter import Interpreter
interpreter = Interpreter(model_path=TFLITE_FILE_PATH)

# Resize input shape for dynamic shape model and allocate tensor
interpreter.resize_tensor_input(interpreter.get_input_details()[0]['index'], [3, 10])
interpreter.allocate_tensors()

# Get input and output tensors.
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()