Ten przewodnik wprowadza w proces uruchamiania na urządzeniu modelu LiteRT (skrót od Lite Runtime) w celu prognozowania na podstawie danych wejściowych. Jest to możliwe dzięki interpreterowi LiteRT, który używa statycznego porządkowania wykresów i niestandardowego (mniej dynamicznego) alokatora pamięci, aby zapewnić minimalne opóźnienie wczytywania, inicjowania i wykonywania.
Wnioskowanie LiteRT zwykle przebiega w tych krokach:
Wczytywanie modelu: wczytaj do pamięci model
.tflite, który zawiera graf wykonania modelu.Przekształcanie danych: przekształcanie danych wejściowych w oczekiwany format i wymiary. Surowe dane wejściowe modelu zwykle nie są zgodne z formatem danych wejściowych oczekiwanym przez model. Może być na przykład konieczne zmiana rozmiaru obrazu lub formatu, aby był zgodny z modelem.
Wnioskowanie: wykonaj model LiteRT, aby tworzyć prognozy. Ten krok obejmuje użycie interfejsu LiteRT API do wykonania modelu. Obejmuje to kilka kroków, takich jak utworzenie interpretera i przydzielenie tensorów.
Interpretacja danych wyjściowych: interpretuj tensory wyjściowe w sposób, który ma znaczenie i jest przydatny w Twojej aplikacji. Na przykład model może zwracać tylko listę prawdopodobieństw. To Ty musisz przypisać prawdopodobieństwa do odpowiednich kategorii i sformatować dane wyjściowe.
Z tego przewodnika dowiesz się, jak uzyskać dostęp do interpretera LiteRT i przeprowadzić wnioskowanie za pomocą języków C++, Java i Python.
Obsługiwane platformy
Interfejsy API wnioskowania TensorFlow są dostępne na najpopularniejszych platformach mobilnych i wbudowanych, takich jak Android, iOS i Linux, w wielu językach programowania.
W większości przypadków projekt interfejsu API odzwierciedla preferencję wydajności nad łatwością użycia. LiteRT został zaprojektowany z myślą o szybkiej inferencji na małych urządzeniach, więc interfejsy API unikają niepotrzebnych kopii kosztem wygody.
Interfejs LiteRT API umożliwia wczytywanie modeli, przekazywanie danych wejściowych i pobieranie wyników wnioskowania we wszystkich bibliotekach.
Platforma
Na Androidzie wnioskowanie LiteRT można przeprowadzać za pomocą interfejsów Java API lub C++ API. Interfejsy API w języku Java są wygodne i można ich używać bezpośrednio w klasach aktywności Androida. Interfejsy API C++ zapewniają większą elastyczność i szybkość, ale mogą wymagać napisania otoczek JNI do przenoszenia danych między warstwami Java i C++.
Więcej informacji znajdziesz w sekcjach C++ i Java lub skorzystaj z przewodnika szybkiego startu na Androida.
Platforma iOS
W systemie iOS biblioteka LiteRT jest dostępna w językach Swift i Objective-C. Możesz też używać C API bezpośrednio w kodzie Objective-C.
Zapoznaj się z sekcjami Swift, Objective-C i C API lub przeczytaj krótkie wprowadzenie do iOS.
Platforma Linux
Na platformach Linux możesz uruchamiać wnioskowanie za pomocą interfejsów LiteRT API dostępnych w C++.
Wczytywanie i uruchamianie modelu
Wczytywanie i uruchamianie modelu LiteRT obejmuje te kroki:
- wczytanie modelu do pamięci,
- Tworzenie
Interpreterna podstawie istniejącego modelu. - Ustawianie wartości tensora wejściowego.
- Wywoływanie wniosków.
- wypisywanie wartości tensora,
Android (Java)
Interfejs Java API do przeprowadzania wnioskowania za pomocą LiteRT jest przeznaczony głównie do użytku na Androidzie, dlatego jest dostępny jako zależność biblioteki Androida:com.google.ai.edge.litert.
W Javie do wczytywania modelu i przeprowadzania wnioskowania na jego podstawie użyjesz klasy Interpreter. W wielu przypadkach może to być jedyny interfejs API, którego potrzebujesz.
Możesz zainicjować Interpreter za pomocą pliku FlatBuffers (.tflite):
public Interpreter(@NotNull File modelFile);
lub za pomocą MappedByteBuffer:
public Interpreter(@NotNull MappedByteBuffer mappedByteBuffer);
W obu przypadkach musisz podać prawidłowy model LiteRT. W przeciwnym razie interfejs API zgłosi błąd IllegalArgumentException. Jeśli używasz MappedByteBuffer do inicjowania
Interpreter, musi on pozostać niezmieniony przez cały okres życia
Interpreter.
Preferowanym sposobem przeprowadzania wnioskowania na podstawie modelu jest używanie sygnatur – dostępne w przypadku modeli przekonwertowanych od 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");
}
Metoda runSignature przyjmuje 3 argumenty:
Dane wejściowe : mapowanie danych wejściowych z nazwy danych wejściowych w sygnaturze na obiekt danych wejściowych.
Outputs : mapowanie danych wyjściowych z nazwy wyjściowej w sygnaturze na dane wyjściowe.
Nazwa podpisu (opcjonalnie): nazwa podpisu (można pozostawić puste, jeśli model ma jeden podpis).
Inny sposób przeprowadzania wnioskowania, gdy model nie ma zdefiniowanych sygnatur.
Wystarczy zadzwonić pod numer Interpreter.run(). Na przykład:
try (Interpreter interpreter = new Interpreter(file_of_a_tensorflowlite_model)) {
interpreter.run(input, output);
}
Metoda run() przyjmuje tylko 1 dane wejściowe i zwraca tylko 1 dane wyjściowe. Jeśli Twój model ma wiele danych wejściowych lub wyjściowych, użyj:
interpreter.runForMultipleInputsOutputs(inputs, map_of_indices_to_outputs);
W tym przypadku każdy wpis w inputs odpowiada tensorowi wejściowemu, a map_of_indices_to_outputs mapuje indeksy tensorów wyjściowych na odpowiednie dane wyjściowe.
W obu przypadkach indeksy tensora powinny odpowiadać wartościom podanym w konwerterze LiteRT podczas tworzenia modelu. Pamiętaj, że kolejność tensorów w input musi być zgodna z kolejnością podaną w konwerterze LiteRT.
Klasa Interpreter udostępnia też wygodne funkcje, które umożliwiają uzyskanie indeksu dowolnego wejścia lub wyjścia modelu za pomocą nazwy operacji:
public int getInputIndex(String opName);
public int getOutputIndex(String opName);
Jeśli opName nie jest prawidłową operacją w modelu, zwraca błąd IllegalArgumentException.
Pamiętaj też, że Interpreter jest właścicielem zasobów. Aby uniknąć wycieku pamięci, po użyciu zasoby należy zwolnić, wykonując te czynności:
interpreter.close();
Przykładowy projekt w Javie znajdziesz w aplikacji z przykładem wykrywania obiektów na Androidzie.
Obsługiwane typy danych
Aby używać LiteRT, typy danych tensorów wejściowych i wyjściowych muszą być jednym z tych typów prostych:
floatintlongbyte
String są również obsługiwane, ale są kodowane inaczej niż typy proste. W szczególności kształt tensora ciągu znaków określa liczbę i układ ciągów znaków w tensorze, przy czym każdy element jest ciągiem znaków o zmiennej długości. W tym sensie rozmiaru tensora (w bajtach) nie można obliczyć tylko na podstawie kształtu i typu, a w konsekwencji ciągów znaków nie można podać jako pojedynczego, płaskiego argumentu ByteBuffer.
Jeśli używane są inne typy danych, w tym typy w ramkach, takie jak Integer i Float, zostanie zgłoszony błąd IllegalArgumentException.
Wejścia
Każde dane wejściowe powinny być tablicą lub tablicą wielowymiarową obsługiwanych typów prostych albo surowym obiektem ByteBuffer o odpowiednim rozmiarze. Jeśli dane wejściowe są tablicą lub tablicą wielowymiarową, powiązany tensor wejściowy zostanie niejawnie zmieniony w czasie wnioskowania do rozmiarów tablicy. Jeśli dane wejściowe to ByteBuffer, przed uruchomieniem wnioskowania wywołujący powinien najpierw ręcznie zmienić rozmiar powiązanego tensora wejściowego (za pomocą Interpreter.resizeInput()).
Podczas korzystania z ByteBuffer lepiej używać bezpośrednich buforów bajtów, ponieważ pozwala to Interpreter uniknąć niepotrzebnych kopii. Jeśli ByteBuffer jest bezpośrednim buforem bajtów, jego kolejność musi być ByteOrder.nativeOrder(). Po użyciu do wnioskowania modelu musi pozostać niezmieniony do czasu zakończenia wnioskowania.
Wyniki
Każde wyjście powinno być tablicą lub tablicą wielowymiarową obsługiwanych typów prostych albo obiektem ByteBuffer o odpowiednim rozmiarze. Pamiętaj, że niektóre modele mają dynamiczne dane wyjściowe, w przypadku których kształt tensorów wyjściowych może się różnić w zależności od danych wejściowych. Obecnie nie ma prostego sposobu na obsługę tej sytuacji za pomocą istniejącego interfejsu Java Inference API, ale planowane rozszerzenia to umożliwią.
iOS (Swift)
Interfejs API Swift jest dostępny w TensorFlowLiteSwift Pod z CocoaPods.
Najpierw musisz zaimportować moduł 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)
Interfejs API Objective-C jest dostępny w LiteRTObjC Pod z Cocoapods.
Najpierw musisz zaimportować moduł 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... */ }
Interfejs C API w kodzie Objective-C
Interfejs Objective-C API nie obsługuje delegatów. Aby używać delegatów w kodzie Objective-C, musisz bezpośrednio wywoływać bazowy interfejs API w języku C.
#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++
Interfejs C++ API do przeprowadzania wnioskowania za pomocą LiteRT jest zgodny z platformami Android, iOS i Linux. Interfejs C++ API na iOS jest dostępny tylko w przypadku korzystania z bazela.
W C++ model jest przechowywany w klasie FlatBufferModel.
Zawiera model LiteRT i można go utworzyć na kilka różnych sposobów w zależności od tego, gdzie jest przechowywany model:
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);
};
Teraz, gdy masz model jako obiekt FlatBufferModel, możesz go wykonać za pomocą Interpreter.
Z jednego FlatBufferModel może korzystać jednocześnie więcej niż jeden Interpreter.
Ważne części interfejsu Interpreter API są pokazane w poniższym fragmencie kodu. Pamiętaj, że:
- Tensory są reprezentowane przez liczby całkowite, aby uniknąć porównań ciągów znaków (i wszelkich stałych zależności od bibliotek ciągów znaków).
- Do interpretera nie można uzyskiwać dostępu z wątków współbieżnych.
- Przydzielanie pamięci dla tensorów wejściowych i wyjściowych musi być wywoływane przez wywołanie funkcji
AllocateTensors()bezpośrednio po zmianie rozmiaru tensorów.
Najprostsze użycie LiteRT w C++ wygląda tak:
// 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);
Więcej przykładowego kodu znajdziesz w sekcjach
minimal.cc
i
label_image.cc.
Python
Interfejs Python API do przeprowadzania wnioskowania używa funkcji Interpreter do wczytywania modelu i przeprowadzania wnioskowania.
Zainstaluj pakiet LiteRT:
$ python3 -m pip install ai-edge-litert
Importowanie interpretera LiteRT
from ai_edge_litert.interpreter import Interpreter
Interpreter = Interpreter(model_path=args.model.file)
Poniższy przykład pokazuje, jak użyć interpretera Pythona do wczytania pliku FlatBuffers (.tflite) i przeprowadzenia wnioskowania z użyciem losowych danych wejściowych:
Ten przykład jest zalecany, jeśli konwertujesz model SavedModel ze zdefiniowanym elementem SignatureDef.
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'])
Inny przykład, jeśli model nie ma zdefiniowanego parametru 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)
Zamiast wczytywać model jako wstępnie przekonwertowany plik .tflite, możesz połączyć kod z kompilatorem LiteRT, co umożliwi Ci przekonwertowanie modelu Keras do formatu LiteRT, a następnie uruchomienie wnioskowania:
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...
Więcej przykładowych fragmentów kodu w Pythonie znajdziesz label_image.py.
Przeprowadzanie wnioskowania za pomocą modelu o dynamicznym kształcie
Jeśli chcesz uruchomić model z dynamicznym kształtem danych wejściowych, zmień rozmiar kształtu danych wejściowych przed uruchomieniem wnioskowania. W przeciwnym razie kształt None w modelach TensorFlow zostanie zastąpiony obiektem zastępczym 1 w modelach LiteRT.
Z przykładów poniżej dowiesz się, jak zmienić rozmiar kształtu wejściowego przed uruchomieniem wnioskowania w różnych językach. We wszystkich przykładach zakłada się, że kształt wejściowy jest zdefiniowany jako [1/None, 10] i musi zostać zmieniony na [3, 10].
Przykład w C++:
// Resize input tensors before allocate tensors
interpreter->ResizeInputTensor(/*tensor_index=*/0, std::vector<int>{3,10});
interpreter->AllocateTensors();
Przykład w Pythonie:
# 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()