نظرًا لأن مكتبة مشغل TensorFlow Lite المضمنة تدعم فقط عددًا محدودًا من مشغلي TensorFlow، فليس كل طراز قابل للتحويل. للحصول على التفاصيل، راجع توافق المشغل .
للسماح بالتحويل، يمكن للمستخدمين توفير التنفيذ المخصص الخاص بهم لمشغل TensorFlow غير المدعوم في TensorFlow Lite، المعروف باسم عامل التشغيل المخصص. إذا كنت ترغب بدلاً من ذلك في دمج سلسلة من عوامل تشغيل TensorFlow غير المدعومة (أو المدعومة) في عامل تشغيل مخصص محسّن مدمج واحد، فارجع إلى عامل التشغيل fusing .
يتكون استخدام عوامل التشغيل المخصصة من أربع خطوات.
إنشاء نموذج TensorFlow. تأكد من أن النموذج المحفوظ (أو Graph Def) يشير إلى عامل التشغيل TensorFlow Lite المسمى بشكل صحيح.
التحويل إلى نموذج TensorFlow Lite. تأكد من تعيين سمة محول TensorFlow Lite الصحيحة لتحويل النموذج بنجاح.
إنشاء وتسجيل المشغل. وذلك حتى يعرف وقت تشغيل TensorFlow Lite كيفية تعيين المشغل والمعلمات في الرسم البياني الخاص بك إلى كود C/C++ القابل للتنفيذ.
اختبار وتعريف المشغل الخاص بك. إذا كنت ترغب في اختبار عامل التشغيل المخصص الخاص بك فقط، فمن الأفضل إنشاء نموذج باستخدام عامل التشغيل المخصص الخاص بك فقط واستخدام برنامج Benchmark_model .
دعونا نستعرض مثالًا شاملاً لتشغيل نموذج باستخدام عامل تشغيل مخصص tf.atan
(يسمى Atan
، راجع #create_a_tensorflow_model) وهو مدعوم في TensorFlow، ولكنه غير مدعوم في TensorFlow Lite.
يعد عامل تشغيل TensorFlow Text مثالاً على عامل تشغيل مخصص. راجع البرنامج التعليمي لتحويل TF Text إلى TF Lite للحصول على مثال للتعليمات البرمجية.
مثال: مشغل Atan
المخصص
دعونا نستعرض مثالاً لدعم مشغل TensorFlow الذي لا يتوفر لدى TensorFlow Lite. لنفترض أننا نستخدم عامل التشغيل Atan
وأننا نبني نموذجًا بسيطًا جدًا للدالة y = atan(x + offset)
حيث يمكن تدريب offset
.
إنشاء نموذج TensorFlow
يقوم مقتطف الكود التالي بتدريب نموذج TensorFlow البسيط. يحتوي هذا النموذج فقط على عامل تشغيل مخصص اسمه Atan
، وهو عبارة عن دالة y = atan(x + offset)
، حيث يمكن تدريب offset
.
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
في هذه المرحلة، إذا حاولت إنشاء نموذج TensorFlow Lite باستخدام إشارات المحول الافتراضية، فستتلقى رسالة الخطأ التالية:
Error:
error: 'tf.Atan' op is neither a custom op nor a flex op.
التحويل إلى نموذج TensorFlow Lite
أنشئ نموذج TensorFlow Lite باستخدام عوامل تشغيل مخصصة، عن طريق تعيين سمة المحول allow_custom_ops
كما هو موضح أدناه:
converter = tf.lite.TFLiteConverter.from_concrete_functions([atan.get_concrete_function()], atan) converter.allow_custom_ops = True tflite_model = converter.convert()
عند هذه النقطة، إذا قمت بتشغيله باستخدام المترجم الافتراضي باستخدام أوامر مثل ما يلي:
interpreter = tf.lite.Interpreter(model_content=tflite_model)
interpreter.allocate_tensors()
ستظل تحصل على الخطأ:
Encountered unresolved custom op: Atan.
إنشاء وتسجيل المشغل.
يتم تعريف جميع مشغلي 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()
.
كلما تم تغيير حجم موترات الإدخال، سيمر المترجم عبر الرسم البياني لإخطار عمليات التنفيذ بالتغيير. وهذا يمنحهم الفرصة لتغيير حجم المخزن المؤقت الداخلي الخاص بهم، والتحقق من صلاحية أشكال وأنواع الإدخال، وإعادة حساب أشكال الإخراج. يتم كل ذلك من خلال 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
) يعتني بتسجيل العناصر المدمجة، إلا أنه يجب جمع العمليات المخصصة في مكتبات مخصصة منفصلة.
تحديد النواة في وقت تشغيل TensorFlow Lite
كل ما يتعين علينا القيام به لاستخدام العملية في TensorFlow Lite هو تحديد وظيفتين ( Prepare
Eval
)، وإنشاء TfLiteRegistration
:
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
، قم بإضافة العملية المخصصة إلى المحلل (انظر أدناه للحصول على مثال). سيؤدي هذا إلى تسجيل المشغل في Tensorflow Lite حتى يتمكن TensorFlow Lite من استخدام التطبيق الجديد. لاحظ أن الوسيطتين الأخيرتين في TfLiteRegistration
تتوافقان مع وظائف AtanPrepare
و AtanEval
التي قمت بتحديدها للعملية المخصصة. إذا استخدمت دالتي AtanInit
و AtanFree
لتهيئة المتغيرات المستخدمة في العملية ولتحرير مساحة، على التوالي، فستتم إضافتهما إلى الوسيطتين الأوليين لـ TfLiteRegistration
؛ تم تعيين هذه الوسائط على nullptr
في هذا المثال.
قم بتسجيل المشغل في مكتبة النواة
نحتاج الآن إلى تسجيل المشغل في مكتبة 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("Atan", Register_ATAN());
إذا تم اعتبار مجموعة العمليات المضمنة كبيرة جدًا، فيمكن إنشاء OpResolver
جديد بناءً على مجموعة فرعية معينة من العمليات، ربما فقط تلك الموجودة في نموذج معين. وهذا يعادل التسجيل الانتقائي لـ TensorFlow (وتتوفر نسخة بسيطة منه في دليل tools
).
إذا كنت تريد تحديد عوامل التشغيل المخصصة الخاصة بك في Java، فستحتاج حاليًا إلى إنشاء طبقة JNI المخصصة الخاصة بك وتجميع AAR الخاص بك في رمز jni هذا . وبالمثل، إذا كنت ترغب في تحديد عوامل التشغيل هذه المتوفرة في Python، فيمكنك وضع تسجيلاتك في رمز مجمّع Python .
لاحظ أنه يمكن اتباع عملية مشابهة كما هو مذكور أعلاه لدعم مجموعة من العمليات بدلاً من عامل تشغيل واحد. ما عليك سوى إضافة العدد الذي تحتاجه من عوامل AddCustom
. بالإضافة إلى ذلك، يتيح لك BuiltinOpResolver
أيضًا تجاوز عمليات تنفيذ العناصر المدمجة باستخدام AddBuiltin
.
اختبار وتعريف المشغل الخاص بك
لتكوين ملف تعريف للعملية الخاصة بك باستخدام أداة قياس الأداء TensorFlow Lite، يمكنك استخدام أداة النموذج المعياري لـ TensorFlow Lite. لأغراض الاختبار، يمكنك جعل الإصدار المحلي الخاص بك من TensorFlow Lite على دراية بالعملية المخصصة الخاصة بك عن طريق إضافة استدعاء AddCustom
المناسب (كما هو موضح أعلاه) للتسجيل.cc
أفضل الممارسات
تحسين عمليات تخصيص الذاكرة وإلغاء التخصيصات بحذر. يعد تخصيص الذاكرة في
Prepare
أكثر كفاءة منه فيInvoke
، كما أن تخصيص الذاكرة قبل الحلقة أفضل من كل تكرار. استخدم بيانات الموترات المؤقتة بدلاً من تشويش نفسك (انظر البند 2). استخدم المؤشرات/المراجع بدلاً من النسخ قدر الإمكان.إذا استمرت بنية البيانات أثناء العملية بأكملها، فإننا ننصح بالتخصيص المسبق للذاكرة باستخدام موترات مؤقتة. قد تحتاج إلى استخدام بنية OpData للإشارة إلى مؤشرات الموتر في وظائف أخرى. انظر المثال الموجود في النواة للالتفاف . يوجد أدناه نموذج لمقتطف التعليمات البرمجية
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
، أي أنه يجب استخدام وحدات الماكرو هذه قبل تخصيص أي موارد قد تتسرب.