Diese Seite wurde von der Cloud Translation API übersetzt.
Switch to English

Benutzerdefinierte Operatoren

Da die in TensorFlow Lite integrierte Operator-Bibliothek nur eine begrenzte Anzahl von TensorFlow-Operatoren unterstützt, ist nicht jedes Modell konvertierbar. Weitere Informationen finden Sie unter Bedienerkompatibilität .

Um die Konvertierung zu ermöglichen, können Benutzer ihre eigene benutzerdefinierte Implementierung eines nicht unterstützten TensorFlow-Operators in TensorFlow Lite bereitstellen, der als benutzerdefinierter Operator bezeichnet wird. Wenn Sie stattdessen eine Reihe nicht unterstützter (oder unterstützter) TensorFlow-Operatoren zu einem einzigen verschmolzenen, optimierten benutzerdefinierten Operator kombinieren möchten, lesen Sie das Verschmelzen von Operatoren .

Die Verwendung von benutzerdefinierten Operatoren besteht aus vier Schritten.

Lassen Sie uns ein End-to-End-Beispiel für die Ausführung eines Modells mit einem benutzerdefinierten Operator tf.sin (mit dem Namen Sin , siehe #create_a_tensorflow_model) durchgehen, der in TensorFlow unterstützt, in TensorFlow Lite jedoch nicht unterstützt wird.

Beispiel: Benutzerdefinierter Sin Operator

Lassen Sie uns ein Beispiel für die Unterstützung eines TensorFlow-Operators durchgehen, den TensorFlow Lite nicht hat. Angenommen, wir verwenden den Sin Operator und erstellen ein sehr einfaches Modell für eine Funktion y = sin(x + offset) , bei der offset trainierbar ist.

Erstellen Sie ein TensorFlow-Modell

Das folgende Code-Snippet trainiert ein einfaches TensorFlow-Modell. Dieses Modell enthält nur einen benutzerdefinierten Operator namens Sin , der eine Funktion y = sin(x + offset) , wobei der offset trainierbar ist.

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

Wenn Sie zu diesem Zeitpunkt versuchen, ein TensorFlow Lite-Modell mit den Standardkonverter-Flags zu generieren, wird die folgende Fehlermeldung angezeigt:

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.

In ein TensorFlow Lite-Modell konvertieren

Erstellen Sie ein TensorFlow Lite-Modell mit benutzerdefinierten Operatoren, indem Sie das Konverterattribut allow_custom_ops wie allow_custom_ops :

converter = tf.lite.TFLiteConverter.from_concrete_functions([sin.get_concrete_function(x)])
converter.allow_custom_ops = True
tflite_model = converter.convert()

Wenn Sie es zu diesem Zeitpunkt mit dem Standardinterpreter ausführen, werden die folgenden Fehlermeldungen angezeigt:

Error:
Didn't find custom operator for name 'Sin'
Registration failed.

Erstellen und registrieren Sie den Operator.

Alle TensorFlow Lite-Operatoren (sowohl benutzerdefinierte als auch integrierte) werden mithilfe einer einfachen Pure-C-Schnittstelle definiert, die aus vier Funktionen besteht:

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;

common.h Informationen zu TfLiteContext und TfLiteNode TfLiteContext TfLiteNode . Ersteres bietet Funktionen zur Fehlerberichterstattung und Zugriff auf globale Objekte, einschließlich aller Tensoren. Letzteres ermöglicht Implementierungen den Zugriff auf ihre Ein- und Ausgänge.

Wenn der Interpreter ein Modell lädt, ruft er init() einmal für jeden Knoten im Diagramm auf. Ein gegebenes init() wird mehrmals aufgerufen, wenn die Operation im Diagramm mehrmals verwendet wird. Für benutzerdefinierte Operationen wird ein Konfigurationspuffer bereitgestellt, der einen Flexbuffer enthält, der Parameternamen ihren Werten zuordnet. Der Puffer ist für integrierte Operationen leer, da der Interpreter die Operationsparameter bereits analysiert hat. Kernel-Implementierungen, für die ein Status erforderlich ist, sollten diesen hier initialisieren und das Eigentum an den Aufrufer übertragen. Für jeden init() -Aufruf gibt es einen entsprechenden Aufruf von free() , sodass Implementierungen den Puffer entsorgen können, den sie möglicherweise in init() zugewiesen haben.

Immer wenn die Größe der Eingangstensoren geändert wird, durchläuft der Interpreter das Diagramm und benachrichtigt die Implementierungen über die Änderung. Dies gibt ihnen die Möglichkeit, die Größe ihres internen Puffers zu ändern, die Gültigkeit von Eingabeformen und -typen zu überprüfen und Ausgabeformen neu zu berechnen. Dies geschieht alles über prepare() , und Implementierungen können über node->user_data auf ihren node->user_data .

Schließlich durchläuft der Interpreter bei node->user_data Inferenz den Graphen, der invoke() aufruft, und auch hier ist der Status als node->user_data .

Benutzerdefinierte Operationen können genauso implementiert werden wie integrierte Operationen, indem diese vier Funktionen und eine globale Registrierungsfunktion definiert werden, die normalerweise so aussieht:

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

Beachten Sie, dass die Registrierung nicht automatisch erfolgt und ein expliziter Aufruf von Register_MY_CUSTOM_OP erfolgen sollte. Während der Standard- BuiltinOpResolver (verfügbar über das Ziel :builtin_ops ) die Registrierung von Builtins übernimmt, müssen benutzerdefinierte Operationen in separaten benutzerdefinierten Bibliotheken gesammelt werden.

Definieren des Kernels in der TensorFlow Lite-Laufzeit

Alles, was wir tun müssen, um die Operation in TensorFlow Lite zu verwenden, ist, zwei Funktionen ( Prepare und Eval ) zu definieren und eine TfLiteRegistration :

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

OpResolver Sie beim Initialisieren des OpResolver die benutzerdefinierte OpResolver zum Resolver hinzu (ein Beispiel finden Sie unten). Dadurch wird der Bediener bei Tensorflow Lite registriert, damit TensorFlow Lite die neue Implementierung verwenden kann. Beachten Sie, dass die letzten beiden Argumente in TfLiteRegistration den Funktionen SinPrepare und TfLiteRegistration entsprechen, die Sie für die benutzerdefinierte SinEval definiert haben. Wenn Sie die Funktionen SinInit und SinFree verwenden, um die im op verwendeten Variablen zu initialisieren und SinFree , werden sie zu den ersten beiden Argumenten von TfLiteRegistration . Diese Argumente werden in diesem Beispiel auf nullptr .

Registrieren Sie den Operator bei der Kernelbibliothek

Jetzt müssen wir den Operator bei der Kernel-Bibliothek registrieren. Dies geschieht mit einem OpResolver . Hinter den Kulissen lädt der Interpreter eine Bibliothek von Kerneln, die zugewiesen werden, um jeden der Operatoren im Modell auszuführen. Während die Standardbibliothek nur integrierte Kernel enthält, ist es möglich, sie durch benutzerdefinierte Bibliotheksoperatoren zu ersetzen / zu erweitern.

Die OpResolver Klasse, die Operatorcodes und -namen in tatsächlichen Code übersetzt, ist wie folgt definiert:

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

Für die regelmäßige Verwendung müssen Sie den BuiltinOpResolver und BuiltinOpResolver schreiben:

tflite::ops::builtin::BuiltinOpResolver resolver;

Um die oben erstellte benutzerdefinierte AddOp hinzuzufügen, rufen Sie AddOp (bevor Sie den Resolver an InterpreterBuilder ):

resolver.AddCustom("Sin", Register_SIN());

Wenn die Anzahl der integrierten Operationen als zu groß angesehen wird, kann ein neuer OpResolver basierend auf einer bestimmten Teilmenge von Operationen, möglicherweise nur den in einem bestimmten Modell enthaltenen, Code-generiert werden. Dies entspricht der selektiven Registrierung von TensorFlow (und eine einfache Version davon ist im tools Verzeichnis verfügbar).

Wenn Sie Ihre benutzerdefinierten Operatoren in Java definieren möchten, müssen Sie derzeit Ihre eigene benutzerdefinierte JNI-Schicht erstellen und Ihren eigenen AAR in diesem JNI-Code kompilieren . Wenn Sie diese in Python verfügbaren Operatoren definieren möchten, können Sie Ihre Registrierungen auch im Python-Wrapper-Code platzieren .

Beachten Sie, dass ein ähnlicher Prozess wie oben zur Unterstützung einer Reihe von Operationen anstelle eines einzelnen Operators ausgeführt werden kann. AddCustom Sie einfach so viele AddCustom Operatoren hinzu, wie Sie benötigen. Darüber hinaus BuiltinOpResolver können auch mit dem Sie Implementierungen von builtins außer Kraft zu setzen AddBuiltin .

Testen und profilieren Sie Ihren Bediener

Um Ihr Op mit dem TensorFlow Lite-Benchmark-Tool zu profilieren, können Sie das Benchmark-Modell-Tool für TensorFlow Lite verwenden. Zu Testzwecken können Sie Ihren lokalen Build von TensorFlow Lite auf Ihre benutzerdefinierte AddCustom aufmerksam machen, indem Sie den entsprechenden AddCustom Aufruf ( AddCustom oben) zu register.cc hinzufügen

Empfohlene Vorgehensweise

  1. Optimieren Sie die Speicherzuordnungen und -aufhebungen vorsichtig. Das Zuweisen von Speicher in Prepare ist effizienter als in Invoke , und das Zuweisen von Speicher vor einer Schleife ist besser als in jeder Iteration. Verwenden Sie temporäre Tensordaten, anstatt sich selbst zu mallocieren (siehe Punkt 2). Verwenden Sie Zeiger / Referenzen, anstatt so viel wie möglich zu kopieren.

  2. Wenn eine Datenstruktur während des gesamten Vorgangs bestehen bleibt, empfehlen wir, den Speicher mithilfe temporärer Tensoren vorab zuzuweisen. Möglicherweise müssen Sie die OpData-Struktur verwenden, um auf die Tensorindizes in anderen Funktionen zu verweisen. Siehe das Beispiel im Kernel für die Faltung . Ein Beispielcode-Snippet finden Sie unten

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;
  1. Wenn es nicht zu viel verschwendeten Speicher kostet, verwenden Sie lieber ein statisches Array mit fester Größe (oder einen vorab zugewiesenen std::vector in Resize ) als einen dynamisch zugewiesenen std::vector jeder Iteration der Ausführung.

  2. Vermeiden Sie das Instanziieren von Standardvorlagen für Bibliothekscontainer, die noch nicht vorhanden sind, da sie sich auf die Binärgröße auswirken. Wenn Sie beispielsweise eine std::map in Ihrer Operation benötigen, die in anderen Kerneln nicht vorhanden ist, kann die Verwendung eines std::vector mit direkter Indizierungszuordnung funktionieren, während die Binärgröße klein gehalten wird. Sehen Sie, was andere Kernel verwenden, um Einblicke zu gewinnen (oder zu fragen).

  3. Überprüfen Sie den Zeiger auf den von malloc zurückgegebenen malloc . Wenn dieser Zeiger nullptr , sollten mit diesem Zeiger keine Operationen ausgeführt werden. Wenn Sie eine Funktion malloc und einen Fehler beim Beenden haben, geben Sie den Speicher frei, bevor Sie den Vorgang beenden.

  4. Verwenden Sie TF_LITE_ENSURE(context, condition) , um nach einer bestimmten Bedingung zu TF_LITE_ENSURE(context, condition) . Ihr Code darf den Speicher nicht hängen lassen, wenn TF_LITE_ENSURE verwendet wird. Das heißt, diese Makros sollten verwendet werden, bevor Ressourcen zugewiesen werden, die auslaufen.