Erste Schritte mit LiteRT

In dieser Anleitung wird beschrieben, wie Sie ein LiteRT-Modell (Lite Runtime) auf dem Gerät ausführen, um Vorhersagen auf Grundlage von Eingabedaten zu treffen. Dies wird durch den LiteRT-Interpreter erreicht, der eine statische Grafikanordnung und einen benutzerdefinierten (weniger dynamischen) Speicher-Allocator verwendet, um minimale Latenz bei Laden, Initialisierung und Ausführung zu gewährleisten.

Die LiteRT-Inferenz erfolgt in der Regel in den folgenden Schritten:

  1. Modell laden: Das .tflite-Modell wird in den Arbeitsspeicher geladen. Es enthält den Ausführungsgraphen des Modells.

  2. Daten transformieren: Eingabedaten in das erwartete Format und die erwarteten Dimensionen transformieren. Die Rohdaten für das Modell entsprechen in der Regel nicht dem vom Modell erwarteten Eingabedatenformat. Möglicherweise müssen Sie beispielsweise die Größe eines Bildes anpassen oder das Bildformat ändern, damit es mit dem Modell kompatibel ist.

  3. Inferenz ausführen: Führen Sie das LiteRT-Modell aus, um Vorhersagen zu treffen. In diesem Schritt wird die LiteRT API verwendet, um das Modell auszuführen. Dazu sind einige Schritte erforderlich, z. B. das Erstellen des Interpreters und das Zuweisen von Tensoren.

  4. Ausgabe interpretieren: Interpretieren Sie die Ausgabetensoren auf eine sinnvolle Weise, die für Ihre Anwendung nützlich ist. Ein Modell gibt beispielsweise möglicherweise nur eine Liste von Wahrscheinlichkeiten zurück. Es liegt an Ihnen, die Wahrscheinlichkeiten relevanten Kategorien zuzuordnen und die Ausgabe zu formatieren.

In diesem Leitfaden wird beschrieben, wie Sie auf den LiteRT-Interpreter zugreifen und eine Inferenz mit C++, Java und Python ausführen.

Unterstützte Plattformen

TensorFlow-Inferenz-APIs sind für die meisten gängigen mobilen und eingebetteten Plattformen wie Android, iOS und Linux in mehreren Programmiersprachen verfügbar.

In den meisten Fällen wird bei der API-Entwicklung die Leistung gegenüber der Benutzerfreundlichkeit bevorzugt. LiteRT ist für schnelle Inferenz auf kleinen Geräten konzipiert. Die APIs vermeiden daher unnötige Kopien, was auf Kosten der Benutzerfreundlichkeit geht.

Mit der LiteRT API können Sie in allen Bibliotheken Modelle laden, Eingaben bereitstellen und Inferenz-Ausgaben abrufen.

Android-Plattform

Unter Android kann die LiteRT-Inferenz entweder mit Java- oder C++-APIs ausgeführt werden. Die Java-APIs sind praktisch und können direkt in Ihren Android-Activity-Klassen verwendet werden. Die C++-APIs bieten mehr Flexibilität und Geschwindigkeit, erfordern aber möglicherweise das Schreiben von JNI-Wrappern, um Daten zwischen Java- und C++-Ebenen zu übertragen.

Weitere Informationen finden Sie in den Abschnitten C++ und Java oder im Android-Schnellstart.

iOS-Plattform

Unter iOS ist LiteRT in den Swift- und Objective-C-iOS-Bibliotheken verfügbar. Sie können die C-API auch direkt in Objective-C-Code verwenden.

Weitere Informationen finden Sie in den Abschnitten Swift, Objective-C und C API oder in der iOS-Kurzanleitung.

Linux-Plattform

Auf Linux-Plattformen können Sie Inferenzvorgänge mit LiteRT-APIs ausführen, die in C++ verfügbar sind.

Modell laden und ausführen

Das Laden und Ausführen eines LiteRT-Modells umfasst die folgenden Schritte:

  1. Das Modell wird in den Arbeitsspeicher geladen.
  2. Interpreter auf Grundlage eines vorhandenen Modells erstellen
  3. Eingabetensorwerte festlegen.
  4. Schlussfolgerungen aufrufen
  5. Ausgabe von Tensorwerten

Android (Java)

Die Java API zum Ausführen von Inferenz mit LiteRT ist in erster Linie für die Verwendung mit Android konzipiert und daher als Android-Bibliotheksabhängigkeit verfügbar: com.google.ai.edge.litert.

In Java verwenden Sie die Klasse Interpreter, um ein Modell zu laden und die Modellinferenz zu steuern. In vielen Fällen ist dies die einzige API, die Sie benötigen.

Sie können ein Interpreter mit einer FlatBuffers-Datei (.tflite) initialisieren:

public Interpreter(@NotNull File modelFile);

Oder mit einem MappedByteBuffer:

public Interpreter(@NotNull MappedByteBuffer mappedByteBuffer);

In beiden Fällen müssen Sie ein gültiges LiteRT-Modell angeben. Andernfalls wird von der API IllegalArgumentException zurückgegeben. Wenn Sie MappedByteBuffer verwenden, um ein Interpreter zu initialisieren, muss es während der gesamten Lebensdauer des Interpreter unverändert bleiben.

Die bevorzugte Methode zum Ausführen von Inferenz für ein Modell ist die Verwendung von Signaturen. Diese sind für Modelle verfügbar, die ab Tensorflow 2.5 konvertiert wurden.

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

Die Methode runSignature verwendet drei Argumente:

  • Eingaben : Zuordnung von Eingaben vom Eingabenamen in der Signatur zu einem Eingabeobjekt.

  • Ausgaben : Zuordnung für die Ausgabezuordnung vom Ausgabenamen in der Signatur zu Ausgabedaten.

  • Signaturname (optional): Signaturname. Kann leer gelassen werden, wenn das Modell eine einzelne Signatur hat.

Eine weitere Möglichkeit, Inferenzen auszuführen, wenn für das Modell keine Signaturen definiert sind. Rufen Sie einfach Interpreter.run() an. Beispiel:

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

Die run()-Methode akzeptiert nur eine Eingabe und gibt nur eine Ausgabe zurück. Wenn Ihr Modell mehrere Eingaben oder Ausgaben hat, verwenden Sie stattdessen:

interpreter.runForMultipleInputsOutputs(inputs, map_of_indices_to_outputs);

In diesem Fall entspricht jeder Eintrag in inputs einem Eingabetensor und map_of_indices_to_outputs ordnet die Indexe der Ausgabetensoren den entsprechenden Ausgabedaten zu.

In beiden Fällen sollten die Tensor-Indexe den Werten entsprechen, die Sie dem LiteRT Converter beim Erstellen des Modells gegeben haben. Die Reihenfolge der Tensoren in input muss mit der Reihenfolge übereinstimmen, die dem LiteRT-Konverter übergeben wurde.

Die Klasse Interpreter bietet auch praktische Funktionen, mit denen Sie den Index einer beliebigen Modelleingabe oder -ausgabe anhand eines Vorgangsnamens abrufen können:

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

Wenn opName kein gültiger Vorgang im Modell ist, wird eine IllegalArgumentException ausgelöst.

Interpreter ist auch Eigentümer von Ressourcen. Um Speicherlecks zu vermeiden, müssen die Ressourcen nach der Verwendung freigegeben werden:

interpreter.close();

Ein Beispielprojekt mit Java finden Sie in der Android-Beispiel-App zur Objekterkennung.

Unterstützte Datentypen

Damit LiteRT verwendet werden kann, müssen die Datentypen der Eingabe- und Ausgabetensoren einer der folgenden primitiven Typen sein:

  • float
  • int
  • long
  • byte

String-Typen werden ebenfalls unterstützt, sie werden jedoch anders codiert als die primitiven Typen. Insbesondere bestimmt die Form eines String-Tensors die Anzahl und Anordnung der Strings im Tensor, wobei jedes Element selbst ein String variabler Länge ist. In diesem Sinne kann die (Byte-)Größe des Tensors nicht allein aus der Form und dem Typ berechnet werden. Daher können Strings nicht als einzelnes, flaches ByteBuffer-Argument angegeben werden.

Wenn andere Datentypen verwendet werden, einschließlich der eingeschlossenen Typen wie Integer und Float, wird eine IllegalArgumentException ausgelöst.

Eingaben

Jede Eingabe muss ein Array oder mehrdimensionales Array der unterstützten primitiven Typen oder ein Roh-ByteBuffer der entsprechenden Größe sein. Wenn die Eingabe ein Array oder ein mehrdimensionales Array ist, wird der zugehörige Eingabetensor zur Laufzeit implizit an die Dimensionen des Arrays angepasst. Wenn die Eingabe ein ByteBuffer ist, sollte der Aufrufer den zugehörigen Eingabetensor vor der Ausführung der Inferenz zuerst manuell anpassen (über Interpreter.resizeInput()).

Verwenden Sie bei der Verwendung von ByteBuffer vorzugsweise direkte Byte-Puffer, da Interpreter so unnötige Kopien vermeiden kann. Wenn ByteBuffer ein direkter Bytepuffer ist, muss seine Reihenfolge ByteOrder.nativeOrder() sein. Nachdem sie für eine Modellinferenz verwendet wurde, muss sie unverändert bleiben, bis die Modellinferenz abgeschlossen ist.

Ausgaben

Jede Ausgabe muss ein Array oder mehrdimensionales Array der unterstützten primitiven Typen oder ein ByteBuffer der entsprechenden Größe sein. Bei einigen Modellen sind die Ausgaben dynamisch. Die Form der Ausgabetensoren kann je nach Eingabe variieren. Mit der vorhandenen Java Inference API ist das nicht ohne Weiteres möglich, aber geplante Erweiterungen werden dies ermöglichen.

iOS (Swift)

Die Swift-API ist im TensorFlowLiteSwift-Pod von CocoaPods verfügbar.

Zuerst müssen Sie das Modul TensorFlowLite importieren.

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)

Die Objective-C-API ist im LiteRTObjC-Pod von Cocoapods verfügbar.

Zuerst müssen Sie das Modul TensorFlowLiteObjC importieren.

@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... */ }

C-API in Objective-C-Code

Die Objective-C-API unterstützt keine Delegates. Wenn Sie Delegaten mit Objective-C-Code verwenden möchten, müssen Sie die zugrunde liegende C-API direkt aufrufen.

#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++

Die C++ API für die Ausführung von Inferenzen mit LiteRT ist mit Android-, iOS- und Linux-Plattformen kompatibel. Die C++ API für iOS ist nur verfügbar, wenn Bazel verwendet wird.

In C++ wird das Modell in der Klasse FlatBufferModel gespeichert. Es enthält ein LiteRT-Modell und kann auf verschiedene Arten erstellt werden, je nachdem, wo das Modell gespeichert ist:

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

Nachdem Sie das Modell als FlatBufferModel-Objekt haben, können Sie es mit einem Interpreter ausführen. Ein einzelnes FlatBufferModel kann gleichzeitig von mehreren Interpreter verwendet werden.

Die wichtigen Teile der Interpreter API sind im folgenden Code-Snippet zu sehen. Beachten Sie Folgendes:

  • Tensoren werden durch Ganzzahlen dargestellt, um Stringvergleiche und jegliche feste Abhängigkeit von Stringbibliotheken zu vermeiden.
  • Auf einen Interpreter darf nicht über gleichzeitige Threads zugegriffen werden.
  • Die Speicherzuweisung für Eingabe- und Ausgabetensoren muss durch Aufrufen von AllocateTensors() direkt nach dem Ändern der Größe von Tensoren ausgelöst werden.

Die einfachste Verwendung von LiteRT mit C++ sieht so aus:

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

Weitere Beispielcodes finden Sie unter minimal.cc und label_image.cc.

Python

Die Python API zum Ausführen von Inferenzen verwendet Interpreter, um ein Modell zu laden und Inferenzen auszuführen.

Installieren Sie das LiteRT-Paket:

$ python3 -m pip install ai-edge-litert

LiteRT-Interpreter importieren

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

Im folgenden Beispiel wird gezeigt, wie Sie den Python-Interpreter verwenden, um eine FlatBuffers-Datei (.tflite) zu laden und die Inferenz mit zufälligen Eingabedaten auszuführen:

Dieses Beispiel wird empfohlen, wenn Sie ein SavedModel mit einer definierten SignatureDef konvertieren.

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

Ein weiteres Beispiel, wenn das Modell keine SignatureDefs-Definition hat.

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)

Alternativ zum Laden des Modells als vorkonvertierte .tflite-Datei können Sie Ihren Code mit dem LiteRT-Compiler kombinieren. So können Sie Ihr Keras-Modell in das LiteRT-Format konvertieren und dann die Inferenz ausführen:

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

Weitere Python-Beispielcode finden Sie unter label_image.py.

Inferenz mit einem Modell mit dynamischer Form ausführen

Wenn Sie ein Modell mit dynamischer Eingabeform ausführen möchten, passen Sie die Größe der Eingabeform an, bevor Sie die Inferenz ausführen. Andernfalls wird die None-Form in Tensorflow-Modellen durch einen Platzhalter mit 1 in LiteRT-Modellen ersetzt.

Die folgenden Beispiele zeigen, wie die Eingabeform vor dem Ausführen der Inferenz in verschiedenen Sprachen angepasst wird. In allen Beispielen wird davon ausgegangen, dass die Eingabeform als [1/None, 10] definiert ist und auf [3, 10] skaliert werden muss.

C++-Beispiel:

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

Python-Beispiel:

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