Ponieważ wbudowana biblioteka operatorów TensorFlow Lite obsługuje tylko ograniczoną liczbę operatorów TensorFlow, nie każdy model jest konwertowalny. Aby uzyskać szczegółowe informacje, patrz Kompatybilność operatora .
Aby umożliwić konwersję, użytkownicy mogą zapewnić własną niestandardową implementację nieobsługiwanego operatora TensorFlow w TensorFlow Lite, znanego jako operator niestandardowy. Jeśli zamiast tego chcesz połączyć szereg nieobsługiwanych (lub obsługiwanych) operatorów TensorFlow w jeden połączony zoptymalizowany operator niestandardowy, zapoznaj się z łączeniem operatorów .
Korzystanie z operatorów niestandardowych składa się z czterech kroków.
Utwórz model TensorFlow. Upewnij się, że Saved Model (lub Graph Def) odwołuje się do poprawnie nazwanego operatora TensorFlow Lite.
Konwertuj na model TensorFlow Lite. Upewnij się, że ustawiłeś właściwy atrybut konwertera TensorFlow Lite, aby pomyślnie przekonwertować model.
Utwórz i zarejestruj operatora. Dzieje się tak, aby środowisko wykonawcze TensorFlow Lite wiedziało, jak zmapować operator i parametry na wykresie na wykonywalny kod C/C++.
Przetestuj i sprofiluj swojego operatora. Jeśli chcesz przetestować tylko swój operator niestandardowy, najlepiej utworzyć model za pomocą samego operatora niestandardowego i użyć programu benchmark_model .
Przejdźmy przez kompletny przykład uruchomienia modelu z niestandardowym operatorem tf.sin
(o nazwie Sin
, patrz #create_a_tensorflow_model), który jest obsługiwany w TensorFlow, ale nieobsługiwany w TensorFlow Lite.
Operator tekstowy TensorFlow jest przykładem operatora niestandardowego. Zobacz samouczek Konwertuj tekst TF na TF Lite, aby zapoznać się z przykładem kodu.
Przykład: operator Sin
niestandardowego
Przeanalizujmy przykład obsługi operatora TensorFlow, którego TensorFlow Lite nie posiada. Załóżmy, że używamy operatora Sin
i budujemy bardzo prosty model dla funkcji y = sin(x + offset)
, gdzie offset
można wytrenować.
Utwórz model TensorFlow
Poniższy fragment kodu trenuje prosty model TensorFlow. Ten model zawiera tylko niestandardowy operator o nazwie Sin
, który jest funkcją y = sin(x + offset)
, gdzie offset
można wytrenować.
import tensorflow as tf
# Define training dataset and variables
x = [-8, 0.5, 2, 2.2, 201]
y = [-0.6569866 , 0.99749499, 0.14112001, -0.05837414, 0.80641841]
offset = tf.Variable(0.0)
# Define a simple model which just contains a custom operator named `Sin`
@tf.function
def sin(x):
return tf.sin(x + offset, name="Sin")
# Train model
optimizer = tf.optimizers.Adam(0.01)
def train(x, y):
with tf.GradientTape() as t:
predicted_y = sin(x)
loss = tf.reduce_sum(tf.square(predicted_y - y))
grads = t.gradient(loss, [offset])
optimizer.apply_gradients(zip(grads, [offset]))
for i in range(1000):
train(x, y)
print("The actual offset is: 1.0")
print("The predicted offset is:", offset.numpy())
The actual offset is: 1.0
The predicted offset is: 1.0000001
W tym momencie, jeśli spróbujesz wygenerować model TensorFlow Lite z domyślnymi flagami konwertera, otrzymasz następujący komunikat o błędzie:
Error:
Some of the operators in the model are not supported by the standard TensorFlow
Lite runtime...... Here is
a list of operators for which you will need custom implementations: Sin.
Konwersja do modelu TensorFlow Lite
Utwórz model TensorFlow Lite z operatorami niestandardowymi, ustawiając atrybut konwertera allow_custom_ops
, jak pokazano poniżej:
converter = tf.lite.TFLiteConverter.from_concrete_functions([sin.get_concrete_function(x)], sin) converter.allow_custom_ops = True tflite_model = converter.convert()
W tym momencie, jeśli uruchomisz go z domyślnym interpreterem, otrzymasz następujące komunikaty o błędach:
Error:
Didn't find custom operator for name 'Sin'
Registration failed.
Utwórz i zarejestruj operatora.
Wszystkie operatory TensorFlow Lite (zarówno niestandardowe, jak i wbudowane) są definiowane za pomocą prostego interfejsu w czystym C, który składa się z czterech funkcji:
typedef struct {
void* (*init)(TfLiteContext* context, const char* buffer, size_t length);
void (*free)(TfLiteContext* context, void* buffer);
TfLiteStatus (*prepare)(TfLiteContext* context, TfLiteNode* node);
TfLiteStatus (*invoke)(TfLiteContext* context, TfLiteNode* node);
} TfLiteRegistration;
Zapoznaj się z common.h
, aby uzyskać szczegółowe informacje na temat TfLiteContext
i TfLiteNode
. Pierwsza zapewnia funkcje raportowania błędów i dostęp do obiektów globalnych, w tym wszystkich tensorów. Ta ostatnia umożliwia implementacjom dostęp do swoich wejść i wyjść.
Kiedy interpreter ładuje model, wywołuje init()
raz dla każdego węzła na wykresie. Dana init()
zostanie wywołana więcej niż jeden raz, jeśli op zostanie użyte w grafie wielokrotnie. Dla niestandardowych operacji zostanie dostarczony bufor konfiguracyjny, zawierający flexbuffer, który mapuje nazwy parametrów na ich wartości. Bufor jest pusty dla operacji wbudowanych, ponieważ interpreter już przeanalizował parametry operacji. Implementacje jądra, które wymagają stanu, powinny zainicjować go tutaj i przenieść własność na wywołującego. Każdemu wywołaniu init()
będzie odpowiednie wywołanie free()
, co pozwoli implementacjom na pozbycie się bufora, który mogły przydzielić w init()
.
Za każdym razem, gdy tensory wejściowe są zmieniane, interpreter przejdzie przez graf, powiadamiając implementacje o zmianie. Daje im to możliwość zmiany rozmiaru wewnętrznego bufora, sprawdzenia poprawności kształtów i typów wejściowych oraz ponownego obliczenia kształtów wyjściowych. Wszystko to odbywa się za pomocą metody Prepare prepare()
, a implementacje mogą uzyskać dostęp do swojego stanu za pomocą node->user_data
.
Wreszcie, za każdym razem, gdy uruchamiane jest wnioskowanie, interpreter przechodzi przez graf wywołując invoke()
, a tutaj stan jest dostępny jako node->user_data
.
Własne operacje można zaimplementować w dokładnie taki sam sposób, jak wbudowane operacje, definiując te cztery funkcje i globalną funkcję rejestracji, która zwykle wygląda tak:
namespace tflite {
namespace ops {
namespace custom {
TfLiteRegistration* Register_MY_CUSTOM_OP() {
static TfLiteRegistration r = {my_custom_op::Init,
my_custom_op::Free,
my_custom_op::Prepare,
my_custom_op::Eval};
return &r;
}
} // namespace custom
} // namespace ops
} // namespace tflite
Zauważ, że rejestracja nie jest automatyczna i należy wykonać wyraźne wywołanie Register_MY_CUSTOM_OP
. Podczas gdy standardowy BuiltinOpResolver
(dostępny z celu :builtin_ops
) zajmuje się rejestracją wbudowanych, niestandardowe operacje będą musiały być zebrane w oddzielnych niestandardowych bibliotekach.
Definiowanie jądra w środowisku wykonawczym TensorFlow Lite
Wszystko, co musimy zrobić, aby użyć op w TensorFlow Lite, to zdefiniować dwie funkcje ( TfLiteRegistration
i Eval
) i skonstruować Prepare
:
TfLiteStatus SinPrepare(TfLiteContext* context, TfLiteNode* node) {
using namespace tflite;
TF_LITE_ENSURE_EQ(context, NumInputs(node), 1);
TF_LITE_ENSURE_EQ(context, NumOutputs(node), 1);
const TfLiteTensor* input = GetInput(context, node, 0);
TfLiteTensor* output = GetOutput(context, node, 0);
int num_dims = NumDimensions(input);
TfLiteIntArray* output_size = TfLiteIntArrayCreate(num_dims);
for (int i=0; i<num_dims; ++i) {
output_size->data[i] = input->dims->data[i];
}
return context->ResizeTensor(context, output, output_size);
}
TfLiteStatus SinEval(TfLiteContext* context, TfLiteNode* node) {
using namespace tflite;
const TfLiteTensor* input = GetInput(context, node,0);
TfLiteTensor* output = GetOutput(context, node,0);
float* input_data = input->data.f;
float* output_data = output->data.f;
size_t count = 1;
int num_dims = NumDimensions(input);
for (int i = 0; i < num_dims; ++i) {
count *= input->dims->data[i];
}
for (size_t i=0; i<count; ++i) {
output_data[i] = sin(input_data[i]);
}
return kTfLiteOk;
}
TfLiteRegistration* Register_SIN() {
static TfLiteRegistration r = {nullptr, nullptr, SinPrepare, SinEval};
return &r;
}
Podczas inicjowania OpResolver
, dodaj niestandardową operację do resolvera (patrz przykład poniżej). Spowoduje to zarejestrowanie operatora w Tensorflow Lite, dzięki czemu TensorFlow Lite będzie mógł korzystać z nowej implementacji. Zauważ, że ostatnie dwa argumenty w TfLiteRegistration
odpowiadają SinPrepare
i SinEval
, które zdefiniowałeś dla operacji niestandardowej. Jeśli użyto funkcji SinInit
i SinFree
do inicjalizacji zmiennych używanych w operacji i odpowiednio do zwolnienia miejsca, zostaną one dodane do pierwszych dwóch argumentów TfLiteRegistration
; w tym przykładzie te argumenty są ustawione na nullptr
.
Zarejestruj operatora w bibliotece jądra
Teraz musimy zarejestrować operatora w bibliotece jądra. Odbywa się to za pomocą OpResolver
. Za kulisami interpreter załaduje bibliotekę jąder, które zostaną przypisane do wykonania każdego z operatorów w modelu. Chociaż domyślna biblioteka zawiera tylko wbudowane jądra, możliwe jest zastąpienie/rozszerzenie jej niestandardowymi operatorami operacyjnymi biblioteki.
Klasa OpResolver
, która tłumaczy kody i nazwy operatorów na rzeczywisty kod, jest zdefiniowana w następujący sposób:
class OpResolver {
virtual TfLiteRegistration* FindOp(tflite::BuiltinOperator op) const = 0;
virtual TfLiteRegistration* FindOp(const char* op) const = 0;
virtual void AddBuiltin(tflite::BuiltinOperator op, TfLiteRegistration* registration) = 0;
virtual void AddCustom(const char* op, TfLiteRegistration* registration) = 0;
};
Regularne używanie wymaga użycia BuiltinOpResolver
i pisania:
tflite::ops::builtin::BuiltinOpResolver resolver;
Aby dodać niestandardową operację utworzoną powyżej, wywołujesz AddOp
(przed przekazaniem resolvera do InterpreterBuilder
):
resolver.AddCustom("Sin", Register_SIN());
Jeśli zestaw wbudowanych operacji zostanie uznany za zbyt duży, nowy OpResolver
może zostać wygenerowany na podstawie danego podzbioru operacji, prawdopodobnie tylko tych zawartych w danym modelu. Jest to odpowiednik selektywnej rejestracji TensorFlow (a prosta jej wersja jest dostępna w katalogu tools
).
Jeśli chcesz zdefiniować swoje niestandardowe operatory w Javie, musisz obecnie zbudować własną niestandardową warstwę JNI i skompilować własny AAR w tym kodzie jni . Podobnie, jeśli chcesz zdefiniować te operatory dostępne w Pythonie, możesz umieścić swoje rejestracje w opakowującym kodzie Pythona .
Zauważ, że podobny proces jak powyżej można zastosować do obsługi zestawu operacji zamiast pojedynczego operatora. Po prostu dodaj tyle operatorów AddCustom
, ile potrzebujesz. Ponadto BuiltinOpResolver
umożliwia również nadpisanie implementacji wbudowanych za pomocą AddBuiltin
.
Przetestuj i sprofiluj swojego operatora
Aby sprofilować swoją operację za pomocą narzędzia do testów porównawczych TensorFlow Lite, możesz użyć narzędzia do testów porównawczych dla TensorFlow Lite. Do celów testowych możesz powiadomić swoją lokalną wersję TensorFlow Lite o swojej niestandardowej operacji, dodając odpowiednie wywołanie AddCustom
(jak pokazano powyżej) do register.cc
Najlepsze praktyki
Ostrożnie optymalizuj alokacje pamięci i cofanie alokacji. Alokowanie pamięci w
Prepare
jest bardziej wydajne niż wInvoke
, a przydzielanie pamięci przed pętlą jest lepsze niż w każdej iteracji. Używaj tymczasowych danych tensorowych zamiast nadużywać siebie (patrz punkt 2). Używaj wskaźników/odniesień zamiast kopiowania jak najwięcej.Jeśli struktura danych będzie się utrzymywać podczas całej operacji, zalecamy wstępne przydzielenie pamięci za pomocą tymczasowych tensorów. Może być konieczne użycie struktury OpData, aby odwołać się do indeksów tensorów w innych funkcjach. Zobacz przykład w jądrze dla splotu . Przykładowy fragment kodu znajduje się poniżej
auto* op_data = reinterpret_cast<OpData*>(node->user_data); TfLiteIntArrayFree(node->temporaries); node->temporaries = TfLiteIntArrayCreate(1); node->temporaries->data[0] = op_data->temp_tensor_index; TfLiteTensor* temp_tensor = &context->tensors[op_data->temp_tensor_index]; temp_tensor->type = kTfLiteFloat32; temp_tensor->allocation_type = kTfLiteArenaRw;
Jeśli nie kosztuje to zbyt dużo zmarnowanej pamięci, wolą używać statycznej tablicy o stałym rozmiarze (lub wstępnie przydzielonego
std::vector
wResize
) zamiast używać dynamicznie przydzielanegostd::vector
w każdej iteracji wykonania.Unikaj tworzenia wystąpień szablonów kontenerów biblioteki standardowej, które jeszcze nie istnieją, ponieważ mają one wpływ na rozmiar binarny. Na przykład, jeśli potrzebujesz
std::map
w swojej operacji, która nie istnieje w innych jądrach, użyciestd::vector
z bezpośrednim mapowaniem indeksowania może działać przy zachowaniu małego rozmiaru binarnego. Zobacz, jakich innych jąder używają, aby uzyskać wgląd (lub zapytaj).Sprawdź wskaźnik do pamięci zwróconej przez
malloc
. Jeśli ten wskaźnik jestnullptr
, nie należy wykonywać żadnych operacji przy użyciu tego wskaźnika. Jeślimalloc
w funkcji i masz błąd wyjścia, cofnij alokację pamięci przed zakończeniem.Użyj
TF_LITE_ENSURE(context, condition)
aby sprawdzić określony warunek. Twój kod nie może pozostawiać zawieszonej pamięci, gdy używane jestTF_LITE_ENSURE
, tj. te makra powinny być używane przed przydzieleniem jakichkolwiek zasobów, które wyciekną.