Aide à protéger la Grande barrière de corail avec tensorflow sur Kaggle Rejoignez Défi

Opérateurs personnalisés

Étant donné que la bibliothèque d'opérateurs intégrée TensorFlow Lite ne prend en charge qu'un nombre limité d'opérateurs TensorFlow, tous les modèles ne sont pas convertibles. Pour plus de détails, reportez - vous à la compatibilité de l' opérateur .

Pour autoriser la conversion, les utilisateurs peuvent fournir leur propre implémentation personnalisée d'un opérateur TensorFlow non pris en charge dans TensorFlow Lite, appelé opérateur personnalisé. Si , au contraire, vous souhaitez combiner une série de non pris en charge (ou pris en charge) des opérateurs de tensorflow en un opérateur personnalisé optimisé unique fusionné, reportez - vous à la fusion de l' opérateur .

L'utilisation d'opérateurs personnalisés comprend quatre étapes.

À pied de laisser passer un exemple de bout en bout de l' exécution d' un modèle avec un opérateur personnalisé tf.sin (nommé Sin , reportez - vous à #create_a_tensorflow_model) qui est pris en charge dans tensorflow, mais non pris en charge dans tensorflow Lite.

Exemple: Custom Sin opérateur

Voyons un exemple de prise en charge d'un opérateur TensorFlow que TensorFlow Lite ne possède pas. Supposons que nous utilisons le Sin opérateur et que nous construisons un modèle très simple pour une fonction y = sin(x + offset) , où offset est trainable.

Créer un modèle TensorFlow

L'extrait de code suivant entraîne un modèle TensorFlow simple. Ce modèle contient juste un opérateur personnalisé nommé Sin , qui est une fonction y = sin(x + offset) , où offset est facile à former.

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

À ce stade, si vous essayez de générer un modèle TensorFlow Lite avec les indicateurs de convertisseur par défaut, vous obtiendrez le message d'erreur suivant :

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.

Convertir en un modèle TensorFlow Lite

Créer un modèle tensorflow Lite avec des opérateurs personnalisés, en définissant les attributs du convertisseur allow_custom_ops comme indiqué ci - dessous:

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

À ce stade, si vous l'exécutez avec l'interpréteur par défaut, vous obtiendrez les messages d'erreur suivants :

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

Créez et enregistrez l'opérateur.

Tous les opérateurs TensorFlow Lite (à la fois personnalisés et intégrés) sont définis à l'aide d'une interface simple en C pur composée de quatre fonctions :

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;

Reportez - vous à common.h pour plus de détails sur TfLiteContext et TfLiteNode . Le premier fournit des fonctions de rapport d'erreurs et un accès aux objets globaux, y compris tous les tenseurs. Ce dernier permet aux implémentations d'accéder à leurs entrées et sorties.

Lorsque les charges d'interprète un modèle, il appelle init() une fois pour chaque noeud dans le graphique. Une donnée init() sera appelé plus d'une fois si l'op est utilisé plusieurs fois dans le graphique. Pour les opérations personnalisées, un tampon de configuration sera fourni, contenant un flexbuffer qui mappe les noms de paramètres à leurs valeurs. Le tampon est vide pour les opérations intégrées car l'interpréteur a déjà analysé les paramètres de l'opération. Les implémentations du noyau qui nécessitent un état doivent l'initialiser ici et transférer la propriété à l'appelant. Pour chaque init() appel, il y aura un appel à correspondant free() , ce qui permet des implémentations de disposer du tampon qu'ils pourraient avoir alloué dans init() .

Chaque fois que les tenseurs d'entrée sont redimensionnés, l'interpréteur parcourt le graphe notifiant les implémentations du changement. Cela leur donne la possibilité de redimensionner leur mémoire tampon interne, de vérifier la validité des formes et des types d'entrée et de recalculer les formes de sortie. Tout cela est fait par prepare() et les mises en œuvre peuvent accéder à leur état à l' aide node->user_data .

Enfin, chaque parcours d'inférence de temps, l'interprète traverse l'appel graphique invoke() , et là aussi l'Etat est disponible en tant que node->user_data .

Les opérations personnalisées peuvent être implémentées exactement de la même manière que les opérations intégrées, en définissant ces quatre fonctions et une fonction d'enregistrement globale qui ressemble généralement à ceci :

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

Notez que l' enregistrement est pas automatique et un appel explicite à Register_MY_CUSTOM_OP devrait être. Bien que la norme BuiltinOpResolver (disponible à partir du :builtin_ops cible) se charge de l'enregistrement des commandes intégrées, ops personnalisées devront être collectées dans les bibliothèques personnalisées séparées.

Définir le noyau dans l'environnement d'exécution TensorFlow Lite

Tout ce que nous devons faire pour utiliser l'op dans tensorflow Lite est de définir deux fonctions ( Prepare et Eval ), et construire un 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;
}

Lors de l' initialisation du OpResolver , ajoutez l'op personnalisé dans le résolveur (voir exemple ci - dessous). Cela enregistrera l'opérateur auprès de Tensorflow Lite afin que TensorFlow Lite puisse utiliser la nouvelle implémentation. Notez que les deux derniers arguments TfLiteRegistration correspondent au SinPrepare et SinEval fonctions que vous avez définies pour l'op personnalisé. Si vous avez utilisé SinInit et SinFree fonctions pour initialiser les variables utilisées dans l'op et de libérer de l' espace, respectivement, ils seraient ajoutés aux deux premiers arguments de TfLiteRegistration ; ces arguments sont mis à nullptr dans cet exemple.

Enregistrez l'opérateur avec la bibliothèque du noyau

Nous devons maintenant enregistrer l'opérateur avec la bibliothèque du noyau. Cela se fait avec un OpResolver . Dans les coulisses, l'interpréteur chargera une bibliothèque de noyaux qui seront affectés à l'exécution de chacun des opérateurs du modèle. Bien que la bibliothèque par défaut ne contienne que des noyaux intégrés, il est possible de la remplacer/de l'augmenter avec des opérateurs op de bibliothèque personnalisés.

La OpResolver classe, ce qui se traduit par des codes d'opérateur et les noms dans le code réel, est défini comme suit:

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

L' utilisation régulière exige que vous utilisez le BuiltinOpResolver et écrire:

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

Pour ajouter l'op personnalisé créé, vous appelez au- dessus AddOp (avant de passer le résolveur au InterpreterBuilder ):

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

Si l'ensemble des opérations de BUILTIN est jugée trop grande, une nouvelle OpResolver pourrait être le code généré basé sur un sous - ensemble donné d'opérations, peut - être que ceux contenus dans un modèle donné. Ceci est l'équivalent d'enregistrement sélectif tensorflow (et une version simple , il est disponible dans les tools répertoire).

Si vous souhaitez définir vos opérateurs personnalisés en Java, vous actuellement besoin de construire votre propre couche JNI personnalisée et compiler votre propre AAR dans ce code JNI . De même, si vous souhaitez définir ces opérateurs disponibles en Python vous pouvez placer vos enregistrements dans le code wrapper Python .

Notez qu'un processus similaire à celui ci-dessus peut être suivi pour prendre en charge un ensemble d'opérations au lieu d'un seul opérateur. Il suffit d' ajouter autant AddCustom opérateurs que vous avez besoin. En outre, BuiltinOpResolver vous permet également de remplacer les implémentations de builtins en utilisant le AddBuiltin .

Testez et profilez votre opérateur

Pour votre profil op avec l'outil de référence tensorflow Lite, vous pouvez utiliser l' outil de modèle de référence pour tensorflow Lite. Pour fins de test, vous pouvez faire votre construction locale de tensorflow Lite conscient de votre op personnalisé en ajoutant le approprié AddCustom appel (comme indiqué ci - dessus) register.cc

Les meilleures pratiques

  1. Optimisez les allocations et les désallocations de mémoire avec prudence. Allouez de la mémoire dans Prepare est plus efficace que dans Invoke , et l' allocation de mémoire avant une boucle est meilleure que dans chaque itération. Utilisez des données de tenseurs temporaires plutôt que de vous mallocaliser (voir élément 2). Utilisez des pointeurs/références au lieu de copier autant que possible.

  2. Si une structure de données persiste pendant toute l'opération, nous conseillons de pré-allouer la mémoire à l'aide de tenseurs temporaires. Vous devrez peut-être utiliser la structure OpData pour référencer les indices de tenseur dans d'autres fonctions. Voir l'exemple dans le noyau pour convolution . Un exemple d'extrait de code est ci-dessous

    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;
    
  3. Si cela ne coûte pas trop de mémoire perdu, préfèrent utiliser un tableau de taille fixe statique (ou une pré-alloué std::vector dans Resize ) plutôt que d' utiliser un allouée dynamiquement std::vector chaque itération d'exécution.

  4. Évitez d'instancier des modèles de conteneur de bibliothèque standard qui n'existent pas déjà, car ils affectent la taille binaire. Par exemple, si vous avez besoin d' un std::map dans votre entreprise qui n'existe pas dans d' autres noyaux, en utilisant un std::vector avec la cartographie de l' indexation directe pourrait travailler tout en gardant la taille binaire petite. Voyez ce que les autres noyaux utilisent pour avoir un aperçu (ou demander).

  5. Vérifiez le pointeur sur la mémoire renvoyée par malloc . Si ce pointeur est nullptr , aucune opération doit être effectuée en utilisant ce pointeur. Si vous malloc dans une fonction et avoir une sortie d'erreur, la mémoire deallocate avant de quitter.

  6. Utilisez TF_LITE_ENSURE(context, condition) pour vérifier une condition spécifique. Votre code ne doit pas laisser pendre la mémoire lorsque TF_LITE_ENSURE est utilisé, par exemple, ces macros doivent être utilisées avant que les ressources sont allouées qui fuira.