نظرًا لأن مكتبة المشغل المدمجة في TensorFlow Lite لا تدعم سوى عددًا محدودًا من مشغلي TensorFlow ، فليس كل طراز قابل للتحويل. للحصول على التفاصيل ، راجع توافق المشغل .
للسماح بالتحويل ، يمكن للمستخدمين توفير التنفيذ المخصص الخاص بهم لمشغل TensorFlow غير المدعوم في TensorFlow Lite ، والمعروف باسم المشغل المخصص. إذا كنت ترغب بدلاً من ذلك في دمج سلسلة من عوامل تشغيل TensorFlow غير المدعومة (أو المدعومة) في مشغل مخصص محسن مدمج واحد ، فارجع إلى اندماج المشغل .
يتكون استخدام عوامل التشغيل المخصصة من أربع خطوات.
قم بإنشاء نموذج TensorFlow. تأكد من أن النموذج المحفوظ (أو Graph Def) يشير إلى مشغل TensorFlow Lite المسمى بشكل صحيح.
قم بالتحويل إلى نموذج TensorFlow Lite. تأكد من تعيين سمة محول TensorFlow Lite الصحيحة من أجل تحويل النموذج بنجاح.
إنشاء وتسجيل عامل التشغيل. هذا حتى يعرف وقت تشغيل TensorFlow Lite كيفية تعيين المشغل والمعلمات في الرسم البياني الخاص بك إلى كود C / C ++ القابل للتنفيذ.
اختبار وملف تعريف المشغل الخاص بك. إذا كنت ترغب في اختبار المشغل المخصص فقط ، فمن الأفضل إنشاء نموذج باستخدام المشغل المخصص فقط واستخدام برنامج benchmark_model .
دعنا نتصفح مثالاً شاملاً لتشغيل نموذج باستخدام عامل تشغيل مخصص 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)], sin) converter.allow_custom_ops = True tflite_model = converter.convert()
في هذه المرحلة ، إذا قمت بتشغيله باستخدام المترجم الافتراضي ، فستتلقى رسائل الخطأ التالية:
Error:
Didn't find custom operator for name 'Sin'
Registration failed.
إنشاء وتسجيل عامل التشغيل.
يتم تعريف جميع عوامل تشغيل TensorFlow Lite (المخصصة والمضمنة) باستخدام واجهة بسيطة خالصة- C تتكون من أربع وظائف:
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()
.
عندما يتم تغيير حجم موتر الإدخال ، سوف يمر المترجم عبر الرسم البياني لإعلام تطبيقات التغيير. يمنحهم هذا فرصة لتغيير حجم المخزن المؤقت الداخلي الخاص بهم ، والتحقق من صلاحية أشكال وأنواع الإدخال ، وإعادة حساب أشكال الإخراج. يتم كل ذلك من خلال Prepar 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
target) يعتني بتسجيل العناصر المدمجة ، يجب جمع العمليات المخصصة في مكتبات مخصصة منفصلة.
تحديد النواة في وقت تشغيل TensorFlow Lite
كل ما نحتاج إلى القيام به لاستخدام المرجع في TensorFlow Lite هو تحديد وظيفتين ( Prepare
TfLiteRegistration
Eval
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
).
إذا كنت ترغب في تحديد المشغلين المخصصين في Java ، فستحتاج حاليًا إلى إنشاء طبقة JNI المخصصة الخاصة بك وتجميع AAR الخاص بك في كود jni هذا . وبالمثل ، إذا كنت ترغب في تحديد عوامل التشغيل المتوفرة في Python ، يمكنك وضع تسجيلاتك في كود غلاف Python .
لاحظ أنه يمكن اتباع عملية مماثلة على النحو الوارد أعلاه لدعم مجموعة من العمليات بدلاً من مشغل واحد. ما عليك سوى إضافة العديد من عوامل تشغيل AddCustom
حسب حاجتك. بالإضافة إلى ذلك ، يسمح لك BuiltinOpResolver
أيضًا بتجاوز تطبيقات البنايات باستخدام AddBuiltin
.
اختبار وملف تعريف المشغل الخاص بك
لتوصيف عمليتك باستخدام أداة قياس الأداء TensorFlow Lite ، يمكنك استخدام أداة النموذج المعياري لـ TensorFlow Lite. لأغراض الاختبار ، يمكنك جعل التصميم المحلي الخاص بك من TensorFlow Lite على دراية بعملك المخصص عن طريق إضافة مكالمة AddCustom
المناسبة (كما هو موضح أعلاه) للتسجيل .
أفضل الممارسات
قم بتحسين عمليات تخصيص الذاكرة وإلغاء التخصيصات بحذر. يعد تخصيص الذاكرة في
Prepare
أكثر فاعلية منInvoke
، كما أن تخصيص الذاكرة قبل التكرار أفضل من كل تكرار. استخدم بيانات الموترات المؤقتة بدلاً من التلاعب بنفسك (انظر البند 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;
إذا لم يكلفك الكثير من الذاكرة المهدرة ، ففضل استخدام مصفوفة ثابتة الحجم ثابتة (أو
std::vector
مخصصة مسبقًا فيResize
) بدلاً من استخدامstd::vector
مخصص ديناميكيًا لكل تكرار للتنفيذ.تجنب إنشاء مثيل لقوالب حاوية مكتبة قياسية غير موجودة بالفعل ، لأنها تؤثر على الحجم الثنائي. على سبيل المثال ، إذا كنت بحاجة إلى
std::map
في عمليتك غير موجودة في نواة أخرى ، فإن استخدامstd::vector
مع تعيين الفهرسة المباشر يمكن أن يعمل مع الحفاظ على الحجم الثنائي صغيرًا. انظر ما تستخدمه النوى الأخرى لاكتساب نظرة ثاقبة (أو اسأل).تحقق من المؤشر إلى الذاكرة التي أرجعها
malloc
. إذا كان هذا المؤشرnullptr
، فلا يجب إجراء أي عمليات باستخدام هذا المؤشر. إذا قمت بعملmalloc
في إحدى الوظائف وكان لديك مخرج خطأ ، فقم بإلغاء تخصيص الذاكرة قبل الخروج.استخدم
TF_LITE_ENSURE(context, condition)
للتحقق من حالة معينة. يجب ألا تترك التعليمات البرمجية الخاصة بك الذاكرة معلقة عند استخدامTF_LITE_ENSURE
، على سبيل المثال ، يجب استخدام وحدات الماكرو هذه قبل تخصيص أي موارد قد تتسرب.