Traiter les données d'entrée et de sortie avec la bibliothèque Support LiteRT

Les développeurs d'applications mobiles interagissent généralement avec des objets typés tels que des bitmaps ou des primitives telles que des entiers. Toutefois, l'API de l'interpréteur LiteRT qui exécute le modèle de machine learning sur l'appareil utilise des Tensors sous la forme de ByteBuffer, qui peuvent être difficiles à déboguer et à manipuler. La bibliothèque d'assistance Android LiteRT est conçue pour aider à traiter les entrées et les sorties des modèles LiteRT, et pour faciliter l'utilisation de l'interpréteur LiteRT.

Premiers pas

Importer la dépendance Gradle et d'autres paramètres

Copiez le fichier de modèle .tflite dans le répertoire des éléments du module Android dans lequel le modèle sera exécuté. Spécifiez que le fichier ne doit pas être compressé et ajoutez la bibliothèque LiteRT au fichier build.gradle du module :

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

Explorez l'AAR de la bibliothèque d'assistance LiteRT hébergée sur MavenCentral pour différentes versions de la bibliothèque d'assistance.

Manipulation et conversion d'images de base

La bibliothèque LiteRT Support dispose d'une suite de méthodes de manipulation d'images de base, telles que le recadrage et le redimensionnement. Pour l'utiliser, créez un ImagePreprocessor et ajoutez les opérations requises. Pour convertir l'image au format Tensor requis par l'interpréteur LiteRT, créez un TensorImage à utiliser comme entrée :

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

DataType d'un Tensor peut être lu à l'aide de la bibliothèque d'extraction de métadonnées, ainsi que d'autres informations sur le modèle.

Traitement de base des données audio

La bibliothèque d'assistance LiteRT définit également une classe TensorAudio qui encapsule certaines méthodes de traitement des données audio de base. Il est principalement utilisé avec AudioRecord et capture des échantillons audio dans un tampon circulaire.

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()

Créer des objets de sortie et exécuter le modèle

Avant d'exécuter le modèle, nous devons créer les objets conteneurs qui stockeront le résultat :

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

Charger le modèle et exécuter l'inférence :

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

Accéder au résultat

Les développeurs peuvent accéder directement au résultat via probabilityBuffer.getFloatArray(). Si le modèle produit une sortie quantifiée, n'oubliez pas de convertir le résultat. Pour le modèle MobileNet quantifié, le développeur doit diviser chaque valeur de sortie par 255 pour obtenir la probabilité comprise entre 0 (moins probable) et 1 (plus probable) pour chaque catégorie.

Facultatif : Mapper les résultats aux libellés

Les développeurs peuvent également mapper les résultats sur des libellés (facultatif). Commencez par copier le fichier texte contenant les libellés dans le répertoire des éléments du module. Ensuite, chargez le fichier de libellés à l'aide du code suivant :

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

L'extrait suivant montre comment associer les probabilités aux libellés de catégorie :

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

Couverture actuelle des cas d'utilisation

La version actuelle de la bibliothèque LiteRT Support couvre les éléments suivants :

  • types de données courants (float, uint8, images, audio et tableau de ces objets) comme entrées et sorties des modèles TFLite.
  • opérations de base sur les images (recadrer, redimensionner et faire pivoter une image).
  • la normalisation et la quantification.
  • utilitaires de fichier

Les futures versions amélioreront la compatibilité avec les applications liées au texte.

Architecture ImageProcessor

La conception de ImageProcessor a permis de définir à l'avance les opérations de manipulation d'image et de les optimiser lors du processus de compilation. ImageProcessor accepte actuellement trois opérations de prétraitement de base, comme décrit dans les trois commentaires de l'extrait de code ci-dessous :

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

Pour en savoir plus sur la normalisation et la quantification, cliquez ici.

L'objectif final de la bibliothèque d'assistance est de prendre en charge toutes les transformations tf.image. Cela signifie que la transformation sera la même que celle de TensorFlow et que l'implémentation sera indépendante du système d'exploitation.

Les développeurs sont également invités à créer des processeurs personnalisés. Dans ces cas, il est important de s'aligner sur le processus d'entraînement, c'est-à-dire que le même prétraitement doit s'appliquer à l'entraînement et à l'inférence pour accroître la reproductibilité.

Quantification

Lorsque vous initialisez des objets d'entrée ou de sortie tels que TensorImage ou TensorBuffer, vous devez spécifier que leurs types sont DataType.UINT8 ou DataType.FLOAT32.

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

TensorProcessor peut être utilisé pour quantifier les Tensor d'entrée ou déquantifier les Tensor de sortie. Par exemple, lors du traitement d'une sortie quantifiée TensorBuffer, le développeur peut utiliser DequantizeOp pour déquantifier le résultat en une probabilité à virgule flottante comprise entre 0 et 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);

Les paramètres de quantification d'un Tensor peuvent être lus à l'aide de la bibliothèque d'extraction de métadonnées.