Özel operatörler

TensorFlow Lite yerleşik operatör kitaplığı yalnızca sınırlı sayıda TensorFlow operatörünü desteklediğinden, her model dönüştürülebilir değildir. Ayrıntılar için operatör uyumluluğuna bakın.

Dönüşüme izin vermek için kullanıcılar, özel operatör olarak bilinen, TensorFlow Lite'ta desteklenmeyen bir TensorFlow operatörünün kendi özel uygulamasını sağlayabilirler. Bunun yerine, bir dizi desteklenmeyen (veya desteklenen) TensorFlow operatörünü tek bir birleştirilmiş optimize edilmiş özel operatörde birleştirmek istiyorsanız, operatör birleştirme bölümüne bakın.

Özel işleçlerin kullanılması dört adımdan oluşur.

TensorFlow'da desteklenen ancak TensorFlow Lite'ta desteklenmeyen özel bir operatör olan tf.atan ( Atan olarak adlandırılır, #create_a_tensorflow_model'e bakın) ile bir model çalıştırmanın uçtan uca örneğini inceleyelim.

TensorFlow Text operatörü özel operatöre bir örnektir. Bir kod örneği için TF Metnini TF Lite'a Dönüştürme eğitimine bakın.

Örnek: Özel Atan operatörü

TensorFlow Lite'ın sahip olmadığı bir TensorFlow operatörünü destekleme örneğini inceleyelim. Atan operatörünü kullandığımızı ve offset eğitilebilir olduğu y = atan(x + offset) fonksiyonu için çok basit bir model oluşturduğumuzu varsayalım.

TensorFlow Modeli Oluşturun

Aşağıdaki kod parçacığı basit bir TensorFlow modelini eğitiyor. Bu model yalnızca, offset eğitilebilir olduğu y = atan(x + offset) işlevi olan Atan adında özel bir operatör içerir.

import tensorflow as tf

# Define training dataset and variables
x = [-8, 0.5, 2, 2.2, 201]
y = [-1.4288993, 0.98279375, 1.2490457, 1.2679114, 1.5658458]
offset = tf.Variable(0.0)

# Define a simple model which just contains a custom operator named `Atan`
@tf.function(input_signature=[tf.TensorSpec.from_tensor(tf.constant(x))])
def atan(x):
  return tf.atan(x + offset, name="Atan")

# Train model
optimizer = tf.optimizers.Adam(0.01)
def train(x, y):
    with tf.GradientTape() as t:
      predicted_y = atan(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: 0.99999905

Bu noktada varsayılan dönüştürücü bayraklarıyla TensorFlow Lite modeli oluşturmaya çalışırsanız aşağıdaki hata mesajını alırsınız:

Error:
error: 'tf.Atan' op is neither a custom op nor a flex op.

TensorFlow Lite Modeline Dönüştürme

allow_custom_ops dönüştürücü özelliğini aşağıda gösterildiği gibi ayarlayarak özel operatörlerle bir TensorFlow Lite modeli oluşturun:

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

Bu noktada aşağıdaki gibi komutları kullanarak varsayılan yorumlayıcıyla çalıştırırsanız:

interpreter = tf.lite.Interpreter(model_content=tflite_model)
interpreter.allocate_tensors()

Hala hatayı alacaksınız:

Encountered unresolved custom op: Atan.

Operatörü oluşturun ve kaydedin.

Tüm TensorFlow Lite operatörleri (hem özel hem de yerleşik), dört işlevden oluşan basit bir saf C arayüzü kullanılarak tanımlanır:

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;

TfLiteContext ve TfLiteNode ile ilgili ayrıntılar için common.h bakın. İlki, hata raporlama olanakları ve tüm tensörler dahil olmak üzere küresel nesnelere erişim sağlar. İkincisi, uygulamaların girdi ve çıktılarına erişmesine izin verir.

Yorumlayıcı bir model yüklediğinde grafikteki her düğüm için init() işlevini bir kez çağırır. Belirli bir init() eğer op grafikte birden çok kez kullanılırsa birden çok kez çağrılacaktır. Özel operasyonlar için, parametre adlarını değerleriyle eşleyen bir esnek arabellek içeren bir yapılandırma arabelleği sağlanacaktır. Yorumlayıcı zaten op parametrelerini ayrıştırdığından yerleşik op'lar için arabellek boştur. Durum gerektiren çekirdek uygulamaları, onu burada başlatmalı ve sahipliği arayana aktarmalıdır. Her init() çağrısı için, free() ye karşılık gelen bir çağrı olacak ve bu, uygulamaların init() içinde tahsis etmiş olabilecekleri tamponu atmasına olanak tanıyacaktır.

Giriş tensörleri yeniden boyutlandırıldığında, yorumlayıcı, değişikliğin uygulamalarını bildiren grafiği inceleyecektir. Bu onlara dahili arabelleklerini yeniden boyutlandırma, giriş şekillerinin ve türlerinin geçerliliğini kontrol etme ve çıktı şekillerini yeniden hesaplama şansı verir. Bunların hepsi prepare() aracılığıyla yapılır ve uygulamalar node->user_data kullanarak durumlarına erişebilir.

Son olarak, her çıkarım çalıştırıldığında, yorumlayıcı invoke() öğesini çağırarak grafiğin üzerinden geçer ve burada da durum node->user_data olarak mevcuttur.

Özel işlemler, yerleşik işlemlerle tamamen aynı şekilde, bu dört işlevi ve genellikle şuna benzeyen bir genel kayıt işlevini tanımlayarak uygulanabilir:

namespace my_namespace {
  const TfLiteRegistration* Register_MY_CUSTOM_OP() {
    static const TfLiteRegistration r = {my_custom_op::Init,
                                         my_custom_op::Free,
                                         my_custom_op::Prepare,
                                         my_custom_op::Eval};
    return &r;
  }
}  // namespace my_namespace

Kaydın otomatik olmadığını ve Register_MY_CUSTOM_OP açık bir çağrı yapılması gerektiğini unutmayın. Standart BuiltinOpResolver ( :builtin_ops hedefinden edinilebilir) yerleşiklerin kaydıyla ilgilenirken, özel operasyonların ayrı özel kitaplıklarda toplanması gerekecektir.

TensorFlow Lite çalışma zamanında çekirdeği tanımlama

TensorFlow Lite'ta op'u kullanmak için yapmamız gereken tek şey iki işlevi tanımlamak ( Prepare ve Eval ) ve bir TfLiteRegistration oluşturmaktır:

TfLiteStatus AtanPrepare(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 AtanEval(TfLiteContext* context, TfLiteNode* node) {
  using namespace tflite;
  const TfLiteTensor* input = GetInput(context, node, 0);
  TfLiteTensor* output = GetOutput(context, node, 0);

  float* input_data = GetTensorData<float>(input);
  float* output_data = GetTensorData<float>(output);

  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] = atan(input_data[i]);
  }
  return kTfLiteOk;
}

const TfLiteRegistration* Register_ATAN() {
  static const TfLiteRegistration r = {nullptr, nullptr, AtanPrepare, AtanEval};
  return &r;
}

OpResolver başlatırken özel işlemi çözümleyiciye ekleyin (örnek için aşağıya bakın). Bu, TensorFlow Lite'ın yeni uygulamayı kullanabilmesi için operatörü Tensorflow Lite'a kaydedecektir. TfLiteRegistration son iki bağımsız değişkenin, özel işlem için tanımladığınız AtanPrepare ve AtanEval işlevlerine karşılık geldiğini unutmayın. Sırasıyla op'ta kullanılan değişkenleri başlatmak ve yer açmak için AtanInit ve AtanFree işlevlerini kullandıysanız, bunlar TfLiteRegistration ilk iki bağımsız değişkenine eklenir; bu argümanlar bu örnekte nullptr olarak ayarlanmıştır.

Operatörü çekirdek kütüphanesine kaydedin

Şimdi operatörü çekirdek kütüphanesine kaydetmemiz gerekiyor. Bu bir OpResolver ile yapılır. Perde arkasında yorumlayıcı, modeldeki operatörlerin her birini yürütmek için atanacak olan çekirdek kitaplığını yükleyecektir. Varsayılan kitaplık yalnızca yerleşik çekirdekler içerse de, onu özel bir kitaplık op operatörleriyle değiştirmek/arttırmak mümkündür.

Operatör kodlarını ve adlarını gerçek koda çeviren OpResolver sınıfı şu şekilde tanımlanır:

class OpResolver {
 public:
  virtual TfLiteRegistration* FindOp(tflite::BuiltinOperator op) const = 0;
  virtual TfLiteRegistration* FindOp(const char* op) const = 0;
  ...
};

MutableOpResolver ve BuiltinOpResolver sınıfları OpResolver türetilmiştir:

class MutableOpResolver : public OpResolver {
 public:
  MutableOpResolver();  // Constructs an initially empty op resolver.
  void AddBuiltin(tflite::BuiltinOperator op, const TfLiteRegistration* registration) = 0;
  void AddCustom(const char* op, const TfLiteRegistration* registration) = 0;
  void AddAll(const MutableOpResolver& other);
  ...
};

class BuiltinOpResolver : public MutableOpResolver {
 public:
  BuiltinOpResolver();  // Constructs an op resolver with all the builtin ops.
};

Düzenli kullanım, BuiltinOpResolver kullanmanızı ve şunları yazmanızı gerektirir:

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

Yukarıda oluşturulan özel operasyonu eklemek için bunun yerine MutableOpResolver kullanabilir ve AddCustom çağırabilirsiniz (çözümleyiciyi InterpreterBuilder iletmeden önce):

tflite::ops::builtin::MutableOpResolver resolver;
resolver.AddAll(tflite::ops::builtin::BuiltinOpResolver());
resolver.AddCustom("Atan", Register_ATAN());

Yerleşik ops kümesinin çok büyük olduğu kabul edilirse, yeni bir OpResolver belirli bir ops alt kümesine, muhtemelen yalnızca belirli bir modelde yer alanlara dayalı olarak kodla oluşturulabilir. Bu, TensorFlow'un seçici kaydına eşdeğerdir (ve bunun basit bir sürümü, tools dizininde mevcuttur).

Özel operatörlerinizi Java'da tanımlamak istiyorsanız, şu anda kendi özel JNI katmanınızı oluşturmanız ve kendi AAR'ınızı bu jni kodunda derlemeniz gerekir. Benzer şekilde, Python'da bulunan bu operatörleri tanımlamak isterseniz kayıtlarınızı Python sarmalayıcı koduna yerleştirebilirsiniz.

Tek bir operatör yerine bir dizi işlemi desteklemek için yukarıdakine benzer bir sürecin takip edilebileceğini unutmayın. İhtiyaç duyduğunuz kadar AddCustom operatörü eklemeniz yeterli. Ayrıca MutableOpResolver , AddBuiltin kullanarak yerleşiklerin uygulamalarını geçersiz kılmanıza da olanak tanır.

Operatörünüzü test edin ve profilini çıkarın

Operasyonunuzun profilini TensorFlow Lite kıyaslama aracıyla çıkarmak için TensorFlow Lite için kıyaslama modeli aracını kullanabilirsiniz. Test amacıyla, uygun AddCustom çağrısını (yukarıda gösterildiği gibi) Register.cc'ye ekleyerek yerel TensorFlow Lite yapınızın özel operasyonunuzdan haberdar olmasını sağlayabilirsiniz.

En iyi uygulamalar

  1. Bellek ayırmalarını ve ayırmaları kaldırma işlemlerini dikkatli bir şekilde optimize edin. Prepare bellek ayırmak Invoke daha verimlidir ve bir döngüden önce bellek ayırmak her yinelemeden daha iyidir. Kendinizi mallokasyona tabi tutmak yerine geçici tensör verilerini kullanın (bkz. madde 2). Mümkün olduğunca kopyalamak yerine işaretçileri/referansları kullanın.

  2. Tüm işlem boyunca bir veri yapısı devam edecekse, geçici tensörler kullanılarak belleğin önceden tahsis edilmesini öneririz. Diğer işlevlerdeki tensör endekslerine başvurmak için OpData yapısını kullanmanız gerekebilir. Evrişim için çekirdekteki örneğe bakın. Örnek kod pasajı aşağıdadır

    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. Çok fazla boşa harcanan belleğe mal olmazsa, her yürütme yinelemesinde dinamik olarak tahsis edilmiş bir std::vector std::vector kullanmak yerine statik sabit boyutlu bir dizi (veya Resize içinde önceden tahsis edilmiş bir std::vector) kullanmayı tercih edin.

  4. İkili boyutu etkilediğinden, halihazırda mevcut olmayan standart kitaplık kapsayıcı şablonlarını başlatmaktan kaçının. Örneğin, işleminizde diğer çekirdeklerde bulunmayan bir std::map ihtiyacınız varsa, doğrudan indeksleme eşlemeli bir std::vector kullanmak, ikili boyutu küçük tutarken işe yarayabilir. Diğer çekirdeklerin fikir edinmek (veya sormak) için neler kullandığını görün.

  5. malloc tarafından döndürülen belleğin işaretçisini kontrol edin. Bu işaretçi nullptr ise, bu işaretçiyi kullanarak hiçbir işlem yapılmamalıdır. Bir işlevde malloc ve çıkış hatasıyla karşılaşırsanız, çıkmadan önce belleğin yerini ayırın.

  6. Belirli bir koşulu kontrol etmek için TF_LITE_ENSURE(context, condition) işlevini kullanın. TF_LITE_ENSURE kullanıldığında kodunuz hafızayı askıda bırakmamalı, yani sızıntı yapacak herhangi bir kaynak tahsis edilmeden önce bu makrolar kullanılmalıdır.