Ö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 uygulamalarını sağlayabilir. Bunun yerine, bir dizi desteklenmeyen (veya desteklenen) TensorFlow işlecini tek bir kaynaştırılmış optimize edilmiş özel işleçte birleştirmek istiyorsanız, bkz . işleç birleştirme .

Özel operatörlerin kullanılması dört adımdan oluşur.

TensorFlow'da desteklenen ancak TensorFlow Lite'ta desteklenmeyen özel bir operatör 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 bir örneğini inceleyelim.

TensorFlow Metin işleci, özel bir işleç örneğidir. Bir kod örneği için TF Metnini TF Lite'a Dönüştür öğreticisine 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) işlevi için çok basit bir model oluşturduğumuzu varsayalım.

Bir TensorFlow Modeli Oluşturun

Aşağıdaki kod parçacığı, basit bir TensorFlow modelini eğitiyor. Bu model yalnızca Atan adında özel bir işleç içerir; bu y = atan(x + offset) işlevidir ve burada offset eğitilebilir.

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 bir 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.

Bir TensorFlow Lite Modeline Dönüştürün

Aşağıda gösterildiği gibi allow_custom_ops dönüştürücü özniteliğini ayarlayarak özel işleçlerle 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ı ile ç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 arabirimi 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 girdilerine ve çıktılarına erişmesine izin verir.

Yorumlayıcı bir model yüklediğinde, grafikteki her düğüm için bir kez init() i çağırır. İşlem grafikte birden çok kez kullanılıyorsa, belirli bir init() birden çok kez çağrılacaktır. Özel işlemler için, parametre adlarını değerlerine eşleyen bir flexbuffer içeren bir yapılandırma arabelleği sağlanacaktır. Yerleşik işlemler için arabellek boş çünkü yorumlayıcı işlem parametrelerini zaten ayrıştırdı. Durum gerektiren çekirdek uygulamaları onu burada başlatmalı ve sahipliği arayana aktarmalıdır. Her init() çağrısı için, uygulamaların init() içinde tahsis etmiş olabilecekleri arabelleği atmasına izin veren karşılık gelen bir free() çağrısı olacaktır.

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

Son olarak, çıkarım her çalıştırıldığında, yorumlayıcı invoke() öğesini çağırarak grafiği çaprazlar ve burada da durum node->user_data olarak kullanılabilir.

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

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

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ştur:

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

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

OpResolver başlatırken, özel işlemi çözümleyiciye ekleyin (bir ö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 işlemde kullanılan değişkenleri başlatmak ve yer açmak için AtanInit ve AtanFree işlevlerini kullandıysanız, bunlar TfLiteRegistration öğesinin ilk iki argümanına eklenir; bu bağımsız değişkenler bu örnekte nullptr olarak ayarlanmıştır.

Operatörü çekirdek kitaplığına kaydedin

Şimdi operatörü çekirdek kütüphanesine kaydetmemiz gerekiyor. Bu bir OpResolver ile yapılır. Perde arkasında yorumlayıcı, modeldeki her bir işleci yürütmek için atanacak bir çekirdek kitaplığı yükleyecektir. Varsayılan kitaplık yalnızca yerleşik çekirdekler içerirken, onu özel bir kitaplık işlem işleçleriyle değiştirmek/büyütmek 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 {
  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;
};

Normal kullanım, BuiltinOpResolver kullanmanızı ve şunu yazmanızı gerektirir:

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

Yukarıda oluşturulan özel işlemi eklemek için AddOp çağırırsınız (çözümleyiciyi InterpreterBuilder aktarmadan önce):

resolver.AddCustom("Atan", Register_ATAN());

Yerleşik işlemler kümesinin çok büyük olduğu düşünülürse, yeni bir OpResolver belirli bir işlem alt kümesine, muhtemelen yalnızca belirli bir modelde bulunanlara dayalı olarak kod üretilebilir. Bu, TensorFlow'un seçici kaydına eşdeğerdir (ve bunun basit bir sürümü, tools dizininde mevcuttur).

Java'da özel işleçlerinizi tanımlamak istiyorsanız, şu anda kendi özel JNI katmanınızı oluşturmanız ve bu jni kodunda kendi AAR'ınızı derlemeniz gerekir. Benzer şekilde, Python'da bulunan bu işleçleri 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üreç izlenebileceğini unutmayın. İhtiyaç duyduğunuz kadar AddCustom işleci eklemeniz yeterlidir. Ek olarak, BuiltinOpResolver 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

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

En iyi uygulamalar

  1. Bellek ayırmalarını ve ayırmaları 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 dağıtmak yerine geçici tensör verilerini kullanın (2. maddeye bakın). Mümkün olduğu kadar kopyalamak yerine işaretçileri/referansları kullanın.

  2. Tüm işlem boyunca bir veri yapısı devam edecekse, geçici tensörler kullanarak belleği önceden ayırmanızı öneririz. Diğer işlevlerdeki tensör indekslerine başvurmak için OpData yapısını kullanmanız gerekebilir. Evrişim için çekirdekteki örneğe bakın. Örnek bir kod parçacığı 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 ayrılmış bir std::vector std::vector kullanmak yerine statik sabit boyutlu bir dizi (veya Resize içinde önceden ayrılmış bir std::vector) kullanmayı tercih edin.

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

  5. malloc tarafından döndürülen belleğe işaretçiyi kontrol edin. Bu işaretçi nullptr ise, bu işaretçi kullanılarak hiçbir işlem yapılmamalıdır. Bir işlevde malloc yaparsanız ve çıkışta hatayla karşılaşırsanız, çıkmadan önce belleği serbest bırakın.

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