En esta guía, se presenta el proceso de ejecutar un modelo de LiteRT (abreviatura de Lite Runtime) en el dispositivo para realizar predicciones basadas en datos de entrada. Esto se logra con el intérprete de LiteRT, que usa un ordenamiento de gráficos estático y un asignador de memoria personalizado (menos dinámico) para garantizar una latencia mínima de carga, inicialización y ejecución.
Por lo general, la inferencia de LiteRT sigue estos pasos:
Carga de un modelo: Carga el modelo
.tfliteen la memoria, que contiene el gráfico de ejecución del modelo.Transformación de datos: Transforma los datos de entrada en el formato y las dimensiones esperados. Por lo general, los datos de entrada sin procesar para el modelo no coinciden con el formato de datos de entrada que espera el modelo. Por ejemplo, es posible que debas cambiar el tamaño de una imagen o su formato para que sea compatible con el modelo.
Ejecución de la inferencia: Ejecuta el modelo de LiteRT para realizar predicciones. En este paso, se usa la API de LiteRT para ejecutar el modelo. Implica algunos pasos, como compilar el intérprete y asignar tensores.
Interpretación de los resultados: Interpreta los tensores de salida de una manera significativa que sea útil en tu aplicación. Por ejemplo, un modelo podría devolver solo una lista de probabilidades. Depende de ti asignar las probabilidades a las categorías pertinentes y dar formato al resultado.
En esta guía, se describe cómo acceder al intérprete de LiteRT y realizar una inferencia con C++, Java y Python.
Plataformas compatibles
Las APIs de inferencia de TensorFlow se proporcionan para la mayoría de las plataformas móviles y integradas más comunes, como Android, iOS y Linux, en varios lenguajes de programación.
En la mayoría de los casos, el diseño de la API refleja una preferencia por el rendimiento en lugar de la facilidad de uso. LiteRT está diseñado para una inferencia rápida en dispositivos pequeños, por lo que las APIs evitan copias innecesarias a expensas de la comodidad.
En todas las bibliotecas, la API de LiteRT te permite cargar modelos, ingresar datos y recuperar resultados de inferencia.
Plataforma de Android
En Android, la inferencia de LiteRT se puede realizar con las APIs de Java o C++. Las APIs de Java brindan comodidad y se pueden usar directamente en las clases de Activity de Android. Las APIs de C++ ofrecen más flexibilidad y velocidad, pero pueden requerir la escritura de wrappers de JNI para mover datos entre las capas de Java y C++.
Consulta las secciones C++ y Java para obtener más información, o bien sigue la guía de inicio rápido para Android.
Plataforma de iOS
En iOS, LiteRT está disponible en las bibliotecas de iOS de Swift y Objective-C. También puedes usar la API de C directamente en el código de Objective-C.
Consulta las secciones de las APIs de Swift, Objective-C y C, o sigue la guía de inicio rápido para iOS.
Plataforma Linux
En las plataformas de Linux, puedes ejecutar inferencias con las APIs de LiteRT disponibles en C++.
Carga y ejecuta un modelo
Cargar y ejecutar un modelo de LiteRT implica los siguientes pasos:
- Carga el modelo en la memoria.
- Crear un
Interpreterbasado en un modelo existente - Configura los valores del tensor de entrada.
- Invocar inferencias
- Genera valores de tensores.
Android (Java)
La API de Java para ejecutar inferencias con LiteRT está diseñada principalmente para usarse con Android, por lo que está disponible como una dependencia de la biblioteca de Android: com.google.ai.edge.litert.
En Java, usarás la clase Interpreter para cargar un modelo y controlar la inferencia del modelo. En muchos casos, esta puede ser la única API que necesites.
Puedes inicializar un Interpreter con un archivo FlatBuffers (.tflite):
public Interpreter(@NotNull File modelFile);
O con un MappedByteBuffer:
public Interpreter(@NotNull MappedByteBuffer mappedByteBuffer);
En ambos casos, debes proporcionar un modelo de LiteRT válido o la API arrojará IllegalArgumentException. Si usas MappedByteBuffer para inicializar un Interpreter, este debe permanecer sin cambios durante toda la vida útil del Interpreter.
La forma preferida de ejecutar inferencias en un modelo es usar firmas. Esta opción está disponible para los modelos convertidos a 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");
}
El método runSignature toma tres argumentos:
Entradas : Es un mapa de las entradas desde el nombre de entrada en la firma a un objeto de entrada.
Outputs : Es un mapa para la asignación de la salida, desde el nombre de la salida en la firma hasta los datos de salida.
Nombre de la firma (opcional): Nombre de la firma (se puede dejar vacío si el modelo tiene una sola firma).
Otra forma de ejecutar inferencias cuando el modelo no tiene firmas definidas.
Simplemente, llama a Interpreter.run(). Por ejemplo:
try (Interpreter interpreter = new Interpreter(file_of_a_tensorflowlite_model)) {
interpreter.run(input, output);
}
El método run() solo toma una entrada y devuelve solo una salida. Por lo tanto, si tu modelo tiene varias entradas o salidas, usa lo siguiente:
interpreter.runForMultipleInputsOutputs(inputs, map_of_indices_to_outputs);
En este caso, cada entrada en inputs corresponde a un tensor de entrada y map_of_indices_to_outputs asigna índices de tensores de salida a los datos de salida correspondientes.
En ambos casos, los índices del tensor deben corresponder a los valores que le proporcionaste al LiteRT Converter cuando creaste el modelo. Ten en cuenta que el orden de los tensores en input debe coincidir con el orden que se le da al convertidor de LiteRT.
La clase Interpreter también proporciona funciones convenientes para que obtengas el índice de cualquier entrada o salida del modelo con un nombre de operación:
public int getInputIndex(String opName);
public int getOutputIndex(String opName);
Si opName no es una operación válida en el modelo, se arroja un IllegalArgumentException.
También ten en cuenta que Interpreter posee recursos. Para evitar fugas de memoria, los recursos se deben liberar después de usarlos de la siguiente manera:
interpreter.close();
Para ver un proyecto de ejemplo con Java, consulta la app de ejemplo de detección de objetos de Android.
Tipos de datos admitidos
Para usar LiteRT, los tipos de datos de los tensores de entrada y salida deben ser uno de los siguientes tipos primitivos:
floatintlongbyte
También se admiten los tipos String, pero se codifican de manera diferente a los tipos primitivos. En particular, la forma de un tensor de cadenas dicta la cantidad y la disposición de las cadenas en el tensor, y cada elemento es una cadena de longitud variable. En este sentido, el tamaño (en bytes) del tensor no se puede calcular solo a partir de la forma y el tipo, y, en consecuencia, las cadenas no se pueden proporcionar como un solo argumento ByteBuffer plano.
Si se usan otros tipos de datos, incluidos los tipos boxed como Integer y Float, se arrojará un IllegalArgumentException.
Entradas
Cada entrada debe ser un array o un array multidimensional de los tipos primitivos admitidos, o un ByteBuffer sin procesar del tamaño adecuado. Si la entrada es un array o un array multidimensional, el tensor de entrada asociado se redimensionará de forma implícita según las dimensiones del array en el momento de la inferencia. Si la entrada es un ByteBuffer, el llamador primero debe cambiar el tamaño del tensor de entrada asociado de forma manual (a través de Interpreter.resizeInput()) antes de ejecutar la inferencia.
Cuando uses ByteBuffer, prefiere usar búferes de bytes directos, ya que esto permite que Interpreter evite copias innecesarias. Si ByteBuffer es un búfer de bytes directo, su orden debe ser ByteOrder.nativeOrder(). Después de que se usa para la inferencia de un modelo, debe permanecer sin cambios hasta que finalice la inferencia del modelo.
Salidas
Cada salida debe ser un array o un array multidimensional de los tipos primitivos admitidos, o un ByteBuffer del tamaño adecuado. Ten en cuenta que algunos modelos tienen salidas dinámicas, en las que la forma de los tensores de salida puede variar según la entrada. No hay una forma directa de controlar esto con la API de inferencia de Java existente, pero las extensiones planificadas lo harán posible.
iOS (Swift)
La API de Swift está disponible en el Pod TensorFlowLiteSwift de CocoaPods.
Primero, debes importar el módulo 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)
La API de Objective-C está disponible en el pod de LiteRTObjC de CocoaPods.
Primero, debes importar el módulo 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 de C en código Objective-C
La API de Objective-C no admite delegados. Para usar delegados con código de Objective-C, debes llamar directamente a la API de C subyacente.
#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++
La API de C++ para ejecutar la inferencia con LiteRT es compatible con las plataformas Android, iOS y Linux. La API de C++ en iOS solo está disponible cuando se usa bazel.
En C++, el modelo se almacena en la clase FlatBufferModel.
Encapsula un modelo de LiteRT y puedes compilarlo de varias maneras diferentes, según dónde se almacene el modelo:
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);
};
Ahora que tienes el modelo como un objeto FlatBufferModel, puedes ejecutarlo con un Interpreter.
Más de un Interpreter puede usar un solo FlatBufferModel de forma simultánea.
Las partes importantes de la API de Interpreter se muestran en el siguiente fragmento de código. Ten en cuenta lo siguiente:
- Los tensores se representan con números enteros para evitar comparaciones de cadenas (y cualquier dependencia fija de bibliotecas de cadenas).
- No se debe acceder a un intérprete desde subprocesos simultáneos.
- La asignación de memoria para los tensores de entrada y salida se debe activar llamando a
AllocateTensors()inmediatamente después de cambiar el tamaño de los tensores.
El uso más simple de LiteRT con C++ se ve de la siguiente manera:
// 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);
Para obtener más ejemplos de código, consulta minimal.cc y label_image.cc.
Python
La API de Python para ejecutar inferencias usa Interpreter para cargar un modelo y ejecutar inferencias.
Instala el paquete de LiteRT:
$ python3 -m pip install ai-edge-litert
Importa el intérprete de LiteRT
from ai_edge_litert.interpreter import Interpreter
Interpreter = Interpreter(model_path=args.model.file)
En el siguiente ejemplo, se muestra cómo usar el intérprete de Python para cargar un archivo de FlatBuffers (.tflite) y ejecutar la inferencia con datos de entrada aleatorios:
Se recomienda este ejemplo si conviertes un SavedModel con un SignatureDef definido.
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'])
Otro ejemplo si el modelo no tiene definido SignatureDefs.
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)
Como alternativa a cargar el modelo como un archivo .tflite preconvertido, puedes combinar tu código con el compilador de LiteRT, lo que te permite convertir tu modelo de Keras al formato de LiteRT y, luego, ejecutar la inferencia:
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...
Para obtener más muestras de código de Python, consulta label_image.py.
Ejecuta la inferencia con un modelo de forma dinámica
Si deseas ejecutar un modelo con una forma de entrada dinámica, cambia el tamaño de la forma de entrada antes de ejecutar la inferencia. De lo contrario, la forma None en los modelos de TensorFlow se reemplazará por un marcador de posición de 1 en los modelos de LiteRT.
En los siguientes ejemplos, se muestra cómo cambiar el tamaño de la forma de entrada antes de ejecutar la inferencia en diferentes lenguajes. En todos los ejemplos, se supone que la forma de entrada se define como [1/None, 10] y que se debe cambiar el tamaño a [3, 10].
Ejemplo en C++:
// Resize input tensors before allocate tensors
interpreter->ResizeInputTensor(/*tensor_index=*/0, std::vector<int>{3,10});
interpreter->AllocateTensors();
Ejemplo de 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()