يعود مؤتمر Google I / O من 18 إلى 20 مايو! حجز مساحة وبناء الجدول الزمني الخاص بك سجل الآن

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

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

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

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

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

مثال: عامل تشغيل 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 Lite باستخدام عوامل تشغيل مخصصة ، من خلال تعيين سمة المحول allow_custom_ops كما هو موضح أدناه:

converter = tf.lite.TFLiteConverter.from_concrete_functions([sin.get_concrete_function(x)])
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() في init()

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

أخيرًا ، في كل مرة يتم فيها تشغيل الاستدلال ، يمر المترجم invoke() استدعاء الرسم البياني 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 target) يهتم بتسجيل العناصر المدمجة ، يجب جمع العمليات المخصصة في مكتبات مخصصة منفصلة.

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

كل ما نحتاج إلى القيام به لاستخدام المرجع في TensorFlow Lite هو تحديد وظيفتين ( 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 فئة 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 كبيرة جدًا ، OpResolver إنشاء OpResolver الجديد بناءً على مجموعة فرعية معينة من العمليات ، ربما فقط تلك الموجودة في نموذج معين. هذا يعادل التسجيل الانتقائي لـ TensorFlow (وتتوفر نسخة بسيطة منه في دليل tools ).

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

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

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

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

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

  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 ، على TF_LITE_ENSURE ، يجب استخدام وحدات الماكرو هذه قبل تخصيص أي موارد قد تتسرب.