Cómo procesar datos de entrada y salida con la biblioteca de compatibilidad de LiteRT

Por lo general, los desarrolladores de aplicaciones para dispositivos móviles interactúan con objetos escritos, como mapas de bits, o con tipos primitivos, como números enteros. Sin embargo, la API del intérprete de LiteRT que ejecuta el modelo de aprendizaje automático integrado en el dispositivo usa tensores en forma de ByteBuffer, que pueden ser difíciles de depurar y manipular. La biblioteca de compatibilidad con Android de LiteRT está diseñada para ayudar a procesar la entrada y la salida de los modelos de LiteRT, y facilitar el uso del intérprete de LiteRT.

Comenzar

Importa la dependencia de Gradle y otros parámetros de configuración

Copia el archivo del modelo .tflite en el directorio de recursos del módulo de Android en el que se ejecutará el modelo. Especifica que el archivo no se debe comprimir y agrega la biblioteca de LiteRT al archivo build.gradle del módulo:

android {
    // Other settings

    // Specify tflite file should not be compressed for the app apk
    aaptOptions {
        noCompress "tflite"
    }

}

dependencies {
    // Other dependencies

    // Import tflite dependencies
    implementation 'org.tensorflow:tensorflow-lite:0.0.0-nightly-SNAPSHOT'
    // The GPU delegate library is optional. Depend on it as needed.
    implementation 'com.google.ai.edge.litert:litert-gpu:0.0.0-nightly-SNAPSHOT'
    implementation 'com.google.ai.edge.litert:litert-support:0.0.0-nightly-SNAPSHOT'
}

Explora el AAR de la biblioteca de compatibilidad de LiteRT alojado en MavenCentral para diferentes versiones de la biblioteca de compatibilidad.

Manipulación y conversión básicas de imágenes

La biblioteca de compatibilidad de LiteRT tiene un conjunto de métodos básicos de manipulación de imágenes, como recortar y cambiar el tamaño. Para usarlo, crea un ImagePreprocessor y agrega las operaciones requeridas. Para convertir la imagen al formato de tensor que requiere el intérprete de LiteRT, crea un TensorImage que se usará como entrada:

import org.tensorflow.lite.DataType;
import org.tensorflow.lite.support.image.ImageProcessor;
import org.tensorflow.lite.support.image.TensorImage;
import org.tensorflow.lite.support.image.ops.ResizeOp;

// Initialization code
// Create an ImageProcessor with all ops required. For more ops, please
// refer to the ImageProcessor Architecture section in this README.
ImageProcessor imageProcessor =
    new ImageProcessor.Builder()
        .add(new ResizeOp(224, 224, ResizeOp.ResizeMethod.BILINEAR))
        .build();

// Create a TensorImage object. This creates the tensor of the corresponding
// tensor type (uint8 in this case) that the LiteRT interpreter needs.
TensorImage tensorImage = new TensorImage(DataType.UINT8);

// Analysis code for every frame
// Preprocess the image
tensorImage.load(bitmap);
tensorImage = imageProcessor.process(tensorImage);

El DataType de un tensor se puede leer a través de la biblioteca del extractor de metadatos, así como otra información del modelo.

Procesamiento básico de datos de audio

La biblioteca de compatibilidad de LiteRT también define una clase TensorAudio que encapsula algunos métodos básicos de procesamiento de datos de audio. Se usa principalmente junto con AudioRecord y captura muestras de audio en un búfer de anillo.

import android.media.AudioRecord;
import org.tensorflow.lite.support.audio.TensorAudio;

// Create an `AudioRecord` instance.
AudioRecord record = AudioRecord(...)

// Create a `TensorAudio` object from Android AudioFormat.
TensorAudio tensorAudio = new TensorAudio(record.getFormat(), size)

// Load all audio samples available in the AudioRecord without blocking.
tensorAudio.load(record)

// Get the `TensorBuffer` for inference.
TensorBuffer buffer = tensorAudio.getTensorBuffer()

Crea objetos de salida y ejecuta el modelo

Antes de ejecutar el modelo, debemos crear los objetos de contenedor que almacenarán el resultado:

import org.tensorflow.lite.DataType;
import org.tensorflow.lite.support.tensorbuffer.TensorBuffer;

// Create a container for the result and specify that this is a quantized model.
// Hence, the 'DataType' is defined as UINT8 (8-bit unsigned integer)
TensorBuffer probabilityBuffer =
    TensorBuffer.createFixedSize(new int[]{1, 1001}, DataType.UINT8);

Carga el modelo y ejecuta la inferencia:

import java.nio.MappedByteBuffer;
import org.tensorflow.lite.InterpreterFactory;
import org.tensorflow.lite.InterpreterApi;

// Initialise the model
try{
    MappedByteBuffer tfliteModel
        = FileUtil.loadMappedFile(activity,
            "mobilenet_v1_1.0_224_quant.tflite");
    InterpreterApi tflite = new InterpreterFactory().create(
        tfliteModel, new InterpreterApi.Options());
} catch (IOException e){
    Log.e("tfliteSupport", "Error reading model", e);
}

// Running inference
if(null != tflite) {
    tflite.run(tImage.getBuffer(), probabilityBuffer.getBuffer());
}

Cómo acceder al resultado

Los desarrolladores pueden acceder al resultado directamente a través de probabilityBuffer.getFloatArray(). Si el modelo produce un resultado cuantificado, recuerda convertirlo. En el caso del modelo cuantificado de MobileNet, el desarrollador debe dividir cada valor de salida por 255 para obtener la probabilidad que varía de 0 (menos probable) a 1 (más probable) para cada categoría.

Opcional: Asigna resultados a etiquetas

Los desarrolladores también pueden, de manera opcional, asignar los resultados a etiquetas. Primero, copia el archivo de texto que contiene las etiquetas en el directorio de recursos del módulo. A continuación, carga el archivo de etiquetas con el siguiente código:

import org.tensorflow.lite.support.common.FileUtil;

final String ASSOCIATED_AXIS_LABELS = "labels.txt";
List<String> associatedAxisLabels = null;

try {
    associatedAxisLabels = FileUtil.loadLabels(this, ASSOCIATED_AXIS_LABELS);
} catch (IOException e) {
    Log.e("tfliteSupport", "Error reading label file", e);
}

En el siguiente fragmento, se muestra cómo asociar las probabilidades con las etiquetas de categoría:

import java.util.Map;
import org.tensorflow.lite.support.common.TensorProcessor;
import org.tensorflow.lite.support.common.ops.NormalizeOp;
import org.tensorflow.lite.support.label.TensorLabel;

// Post-processor which dequantize the result
TensorProcessor probabilityProcessor =
    new TensorProcessor.Builder().add(new NormalizeOp(0, 255)).build();

if (null != associatedAxisLabels) {
    // Map of labels and their corresponding probability
    TensorLabel labels = new TensorLabel(associatedAxisLabels,
        probabilityProcessor.process(probabilityBuffer));

    // Create a map to access the result based on label
    Map<String, Float> floatMap = labels.getMapWithFloatValue();
}

Cobertura actual de los casos de uso

La versión actual de la biblioteca de compatibilidad de LiteRT abarca lo siguiente:

  • tipos de datos comunes (float, uint8, imágenes, audio y array de estos objetos) como entradas y salidas de los modelos de TFLite.
  • Operaciones básicas con imágenes (recortar, cambiar el tamaño y rotar imágenes)
  • Normalización y cuantización
  • utilidades de archivos

Las versiones futuras mejorarán la compatibilidad con las aplicaciones relacionadas con el texto.

Arquitectura de ImageProcessor

El diseño de ImageProcessor permitió que las operaciones de manipulación de imágenes se definieran por adelantado y se optimizaran durante el proceso de compilación. Actualmente, ImageProcessor admite tres operaciones básicas de preprocesamiento, como se describe en los tres comentarios del siguiente fragmento de código:

import org.tensorflow.lite.support.common.ops.NormalizeOp;
import org.tensorflow.lite.support.common.ops.QuantizeOp;
import org.tensorflow.lite.support.image.ops.ResizeOp;
import org.tensorflow.lite.support.image.ops.ResizeWithCropOrPadOp;
import org.tensorflow.lite.support.image.ops.Rot90Op;

int width = bitmap.getWidth();
int height = bitmap.getHeight();

int size = height > width ? width : height;

ImageProcessor imageProcessor =
    new ImageProcessor.Builder()
        // Center crop the image to the largest square possible
        .add(new ResizeWithCropOrPadOp(size, size))
        // Resize using Bilinear or Nearest neighbour
        .add(new ResizeOp(224, 224, ResizeOp.ResizeMethod.BILINEAR));
        // Rotation counter-clockwise in 90 degree increments
        .add(new Rot90Op(rotateDegrees / 90))
        .add(new NormalizeOp(127.5, 127.5))
        .add(new QuantizeOp(128.0, 1/128.0))
        .build();

Obtén más detalles sobre la normalización y la cuantificación aquí.

El objetivo final de la biblioteca de compatibilidad es admitir todas las transformaciones de tf.image. Esto significa que la transformación será la misma que la de TensorFlow y la implementación será independiente del sistema operativo.

Los desarrolladores también pueden crear procesadores personalizados. En estos casos, es importante que se alinee con el proceso de entrenamiento, es decir, que se aplique el mismo preprocesamiento tanto al entrenamiento como a la inferencia para aumentar la reproducibilidad.

Cuantización

Cuando inicializas objetos de entrada o salida, como TensorImage o TensorBuffer, debes especificar que sus tipos sean DataType.UINT8 o DataType.FLOAT32.

TensorImage tensorImage = new TensorImage(DataType.UINT8);
TensorBuffer probabilityBuffer =
    TensorBuffer.createFixedSize(new int[]{1, 1001}, DataType.UINT8);

El TensorProcessor se puede usar para cuantificar tensores de entrada o para decuantificar tensores de salida. Por ejemplo, cuando se procesa un resultado cuantificado TensorBuffer, el desarrollador puede usar DequantizeOp para descuantificar el resultado en una probabilidad de punto flotante entre 0 y 1:

import org.tensorflow.lite.support.common.TensorProcessor;

// Post-processor which dequantize the result
TensorProcessor probabilityProcessor =
    new TensorProcessor.Builder().add(new DequantizeOp(0, 1/255.0)).build();
TensorBuffer dequantizedBuffer = probabilityProcessor.process(probabilityBuffer);

Los parámetros de cuantización de un tensor se pueden leer a través de la biblioteca del extractor de metadatos.