المشغلين المخصصين

نظرًا لأن مكتبة المشغل المدمجة في TensorFlow Lite تدعم فقط عددًا محدودًا من مشغلي TensorFlow ، فليس كل طراز قابل للتحويل. لمزيد من التفاصيل، يرجى الرجوع إلى التوافق المشغل .

للسماح بالتحويل ، يمكن للمستخدمين توفير التنفيذ المخصص الخاص بهم لمشغل TensorFlow غير المدعوم في TensorFlow Lite ، والمعروف باسم المشغل المخصص. إذا بدلا من ذلك، كنت ترغب في الجمع بين سلسلة من غير معتمد (أو دعم) مشغلي TensorFlow إلى مشغل مخصصة الأمثل تنصهر واحد، إشارة إلى التفجير المشغل .

يتكون استخدام عوامل التشغيل المخصصة من أربع خطوات.

دعونا المشي من خلال مثال نهاية إلى نهاية تشغيل نموذج مع مشغل مخصص tf.sin (واسمه Sin ، راجع #create_a_tensorflow_model) الذي تدعمه في TensorFlow، ولكن غير معتمد في TensorFlow لايت.

مثال: عرف Sin المشغل

دعنا نتعرف على مثال لدعم مشغل TensorFlow لا يمتلكه TensorFlow Lite. نفترض أننا تستخدم Sin المشغل وأن نقوم ببناء نموذج بسيط جدا لوظيفة y = sin(x + offset) ، حيث offset هو للتدريب.

قم بإنشاء نموذج TensorFlow

يقوم مقتطف الكود التالي بتدريب نموذج TensorFlow بسيط. هذا النموذج يحتوي مجرد عامل مخصص اسمه Sin ، التي هي وظيفة y = sin(x + offset) ، حيث offset غير قابلة للتدريب.

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

في هذه المرحلة ، إذا حاولت إنشاء نموذج TensorFlow Lite باستخدام أعلام المحول الافتراضي ، فستتلقى رسالة الخطأ التالية:

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.

قم بالتحويل إلى نموذج TensorFlow Lite

إنشاء نموذج TensorFlow لايت مع مشغلي مخصصة، من خلال تحديد تحويل سمة allow_custom_ops كما هو مبين أدناه:

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

في هذه المرحلة ، إذا قمت بتشغيله باستخدام المترجم الافتراضي ، فستتلقى رسائل الخطأ التالية:

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

إنشاء وتسجيل عامل التشغيل.

يتم تعريف جميع عوامل تشغيل TensorFlow Lite (المخصصة والمضمنة) باستخدام واجهة نقية بسيطة تتكون من أربع وظائف:

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 حصول على تفاصيل حول TfLiteContext و TfLiteNode . الأول يوفر تسهيلات للإبلاغ عن الأخطاء والوصول إلى الكائنات العالمية ، بما في ذلك جميع الموترات. هذا الأخير يسمح للتطبيقات بالوصول إلى مدخلاتها ومخرجاتها.

عند تحميل مترجم نموذجا، فإنه يدعو init() مرة واحدة لكل عقدة في الرسم البياني. ونظرا init() وسوف يطلق أكثر من مرة إذا تم استخدام المرجع عدة مرات في الرسم البياني. بالنسبة إلى العمليات المخصصة ، سيتم توفير مخزن مؤقت للتكوين ، يحتوي على مخزن مرن يقوم بتعيين أسماء المعلمات إلى قيمها. المخزن المؤقت فارغ للعمليات المضمنة لأن المترجم قام بالفعل بتحليل معلمات المرجع. يجب أن تقوم تطبيقات Kernel التي تتطلب الحالة بتهيئتها هنا ونقل الملكية إلى المتصل. لكل init() المكالمة، سوف يكون هناك مكالمة الموافق free() ، مما يسمح للتطبيقات للتخلص من العازلة أنها قد خصصت في init() .

عندما يتم تغيير حجم موتر الإدخال ، سوف يمر المترجم عبر الرسم البياني لإعلام تطبيقات التغيير. يمنحهم هذا فرصة لتغيير حجم المخزن المؤقت الداخلي الخاص بهم ، والتحقق من صلاحية أشكال وأنواع الإدخال ، وإعادة حساب أشكال الإخراج. ويتم كل ذلك من خلال prepare() ، ويمكن استخدام تطبيقات الوصول دولتهم باستخدام node->user_data .

وأخيرا، كل يعمل الاستدلال الوقت، مترجم يخترق الرسم البياني الدعوة invoke() ، وهنا أيضا الدولة هو متاح في node->user_data .

يمكن تنفيذ العمليات المخصصة بالطريقة نفسها تمامًا مثل العمليات المضمنة ، من خلال تحديد هذه الوظائف الأربع ووظيفة التسجيل العالمية التي تبدو عادةً على النحو التالي:

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

ملاحظة أن التسجيل ليس التلقائي ودعوة صريحة إلى Register_MY_CUSTOM_OP ينبغي. في حين أن معيار BuiltinOpResolver (متوفر من :builtin_ops الهدف) يعتني تسجيل builtins، ومكتب خدمات المشاريع المخصصة لها التي سيتم جمعها في مكتبات مخصصة منفصلة.

تحديد النواة في وقت تشغيل TensorFlow Lite

كل ما عليك القيام به لاستخدام المرجع في TensorFlow ايت وتحديد وظيفتين ( Prepare و Eval )، وبناء 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 ، إضافة المرجع العرف في محلل (انظر أدناه للحصول على سبيل المثال). سيؤدي هذا إلى تسجيل المشغل مع Tensorflow Lite بحيث يمكن لـ TensorFlow Lite استخدام التطبيق الجديد. علما بأن الحجج الماضيين في TfLiteRegistration تتوافق مع SinPrepare و SinEval الوظائف التي حددت للمرجع المخصصة. إذا كنت تستخدم SinInit و SinFree ظائف تهيئة المتغيرات المستخدمة في المرجع ولتحرير مساحة على التوالي، ثم أنها ستضاف إلى حجج الأولين من TfLiteRegistration . يتم تعيين هذه الحجج ل nullptr في هذا المثال.

سجل المشغل في مكتبة kernel

نحتاج الآن إلى تسجيل عامل التشغيل في مكتبة kernel. ويتم ذلك مع OpResolver . خلف الكواليس ، سيقوم المترجم بتحميل مكتبة من النوى التي سيتم تخصيصها لتنفيذ كل من المشغلين في النموذج. بينما تحتوي المكتبة الافتراضية فقط على نواة مضمنة ، فمن الممكن استبدالها / زيادتها بمشغلي مكتبة مخصصين.

و OpResolver يتم تعريف الطبقة، وهو ما يترجم رموز المشغل وأسماء إلى رمز الفعلي، مثل هذا:

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

يتطلب استخدام العادي أن استخدام BuiltinOpResolver والكتابة:

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

لإضافة المرجع مخصصة تم إنشاؤها أعلاه، يمكنك استدعاء AddOp (قبل تمرير محلل لل InterpreterBuilder ):

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

إذا اعتبرت مجموعة من التقاط مدمج لتكون كبيرة جدا، جديدة OpResolver يمكن أن يكون على أساس مجموعة فرعية معينة من مكتب خدمات المشاريع إنشاء التعليمات البرمجية، ربما فقط تلك الواردة في نموذج معين. هذا هو ما يعادل تسجيل انتقائية TensorFlow في (ونسخة بسيطة منه متاح في tools الدليل).

إذا كنت ترغب في تحديد مشغلي المخصصة في جاوة، وكنت بحاجة حاليا لبناء الخاصة طبقة JNI عهدك وتجميع AAR الخاصة بك في هذا الرمز JNI . وبالمثل، إذا كنت ترغب في تحديد هذه العوامل متوفرة في بيثون يمكنك وضع التسجيلات الخاصة بك في كود المجمع بيثون .

لاحظ أنه يمكن اتباع عملية مماثلة على النحو الوارد أعلاه لدعم مجموعة من العمليات بدلاً من مشغل واحد. فقط إضافة إلى العديد من AddCustom مشغلي ما تحتاج إليه. وبالإضافة إلى ذلك، BuiltinOpResolver كما يسمح لك لتجاوز تطبيقات builtins باستخدام AddBuiltin .

اختبار وملف تعريف المشغل الخاص بك

لمحة المرجع الخاص بك مع أداة المؤشر TensorFlow لايت، يمكنك استخدام أداة النموذج القياسي لTensorFlow لايت. لأغراض الاختبار، يمكنك جعل بناء المحلي من TensorFlow لايت علم المرجع مخصصة عن طريق إضافة المناسبة AddCustom دعوة (كما تبين أعلاه) ل register.cc

أفضل الممارسات

  1. قم بتحسين عمليات تخصيص الذاكرة وإلغاء التخصيصات بحذر. تخصيص الذاكرة في Prepare أكثر كفاءة مما كان عليه في Invoke ، وتخصيص الذاكرة قبل حلقة أفضل من في كل التكرار. استخدم بيانات الموترات المؤقتة بدلاً من التلاعب بنفسك (انظر البند 2). استخدم المؤشرات / المراجع بدلاً من النسخ قدر الإمكان.

  2. إذا استمرت بنية البيانات أثناء العملية بأكملها ، فإننا ننصح بالتخصيص المسبق للذاكرة باستخدام موترات مؤقتة. قد تحتاج إلى استخدام OpData Struct للإشارة إلى فهارس الموتر في وظائف أخرى. راجع المثال في نواة للالتفاف . يوجد أدناه نموذج مقتطف الشفرة

    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. إذا كان لا يكلف الذاكرة هدر الكثير، ويفضل استخدام ثابت ثابت حجم مجموعة (أو-المخصصة مسبقا std::vector في Resize ) بدلا من استخدام المخصصة بشكل حيوي std::vector كل التكرار التنفيذ.

  4. تجنب إنشاء مثيل لقوالب حاوية مكتبة قياسية غير موجودة بالفعل ، لأنها تؤثر على الحجم الثنائي. على سبيل المثال، إذا كنت في حاجة الى std::map في العملية الخاصة بك التي لا وجود لها في حبات أخرى، وذلك باستخدام std::vector مع تعيين الفهرسة المباشر يمكن أن تعمل مع الحفاظ على حجم صغير ثنائي. انظر ما تستخدمه النوى الأخرى لاكتساب نظرة ثاقبة (أو اسأل).

  5. تحقق المؤشر إلى الذاكرة التي تم إرجاعها بواسطة malloc . إذا كان هذا المؤشر nullptr ، يجب أن يتم تنفيذ أي عمليات باستخدام هذا المؤشر. إذا كنت malloc في وظيفة ولها مخرج الخطأ، ذاكرة إلغاء تخصيص قبل الخروج.

  6. استخدام TF_LITE_ENSURE(context, condition) للتحقق من حالة محددة. التعليمات البرمجية يجب ألا تترك معلقة الذاكرة عند TF_LITE_ENSURE يستخدم، أي ينبغي أن تستخدم وحدات الماكرو هذه قبل أن يتم تخصيص أي موارد من شأنها أن تسرب.