از آنجایی که کتابخانه اپراتور داخلی TensorFlow Lite تنها از تعداد محدودی از اپراتورهای TensorFlow پشتیبانی می کند، هر مدلی قابل تبدیل نیست. برای جزئیات، به سازگاری اپراتور مراجعه کنید.
برای اجازه دادن به تبدیل، کاربران می توانند پیاده سازی سفارشی خود را از یک اپراتور TensorFlow پشتیبانی نشده در TensorFlow Lite، که به عنوان یک اپراتور سفارشی شناخته می شود، ارائه دهند. در عوض، اگر میخواهید یک سری از عملگرهای پشتیبانینشده (یا پشتیبانیشده) TensorFlow را در یک اپراتور سفارشی بهینهسازی شده واحد ترکیب کنید، به ترکیب عملگر مراجعه کنید.
استفاده از عملگرهای سفارشی شامل چهار مرحله است.
یک مدل TensorFlow ایجاد کنید. مطمئن شوید که Saved Model (یا Graph Def) به عملگر TensorFlow Lite که به درستی نامگذاری شده است اشاره دارد.
تبدیل به یک مدل TensorFlow Lite. مطمئن شوید که ویژگی مبدل TensorFlow Lite را برای تبدیل موفقیت آمیز مدل تنظیم کرده اید.
اپراتور را ایجاد و ثبت کنید. این به این دلیل است که زمان اجرا TensorFlow Lite می داند که چگونه اپراتور و پارامترهای شما را در نمودار به کدهای اجرایی C/C++ نگاشت کند.
اپراتور خود را تست و مشخصات اگر می خواهید فقط اپراتور سفارشی خود را آزمایش کنید، بهتر است یک مدل فقط با اپراتور سفارشی خود ایجاد کنید و از برنامه benchmark_model استفاده کنید.
بیایید از طریق یک مثال سرتاسر اجرای یک مدل با یک عملگر سفارشی tf.atan
(با نام Atan
، به #ایجاد_مدل_تنسورفلو مراجعه کنید) که در 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;
برای جزئیات در مورد TfLiteContext
و TfLiteNode
به common.h
مراجعه کنید. اولی امکانات گزارش خطا و دسترسی به اشیاء جهانی، از جمله تمام تانسورها را فراهم می کند. دومی اجازه می دهد تا پیاده سازی ها به ورودی ها و خروجی های خود دسترسی داشته باشند.
هنگامی که مفسر یک مدل را بارگذاری می کند، برای هر گره در گراف یک بار init()
را فراخوانی می کند. یک init()
بیش از یک بار فراخوانی می شود اگر op چندین بار در نمودار استفاده شود. برای عملیات سفارشی یک بافر پیکربندی ارائه خواهد شد که حاوی یک فلکسبافر است که نام پارامترها را به مقادیر آنها نگاشت می کند. بافر برای عملیات داخلی خالی است زیرا مفسر قبلاً پارامترهای op را تجزیه کرده است. پیاده سازی های هسته ای که نیاز به حالت دارند باید آن را در اینجا مقداردهی اولیه کنند و مالکیت را به تماس گیرنده منتقل کنند. برای هر فراخوانی 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
تنها کاری که برای استفاده از op در 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
به ترتیب برای مقداردهی اولیه متغیرهای مورد استفاده در op و برای آزاد کردن فضا استفاده کنید، آنها به دو آرگومان اول TfLiteRegistration
اضافه میشوند. آن آرگومان ها در این مثال روی nullptr
تنظیم شده اند.
اپراتور را با کتابخانه هسته ثبت کنید
اکنون باید اپراتور را با کتابخانه هسته ثبت کنیم. این کار با 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
موجود است).
اگر می خواهید اپراتورهای سفارشی خود را در جاوا تعریف کنید، در حال حاضر باید لایه JNI سفارشی خود را بسازید و AAR خود را در این کد jni کامپایل کنید. به طور مشابه، اگر میخواهید این عملگرها را در پایتون تعریف کنید، میتوانید ثبتهای خود را در کد پوشش پایتون قرار دهید.
توجه داشته باشید که فرآیند مشابهی مانند بالا را می توان برای پشتیبانی از مجموعه ای از عملیات به جای یک اپراتور منفرد دنبال کرد. فقط هر تعداد اپراتور AddCustom
که نیاز دارید اضافه کنید. علاوه بر این، BuiltinOpResolver
همچنین به شما اجازه می دهد تا با استفاده از AddBuiltin
پیاده سازی های داخلی را لغو کنید.
اپراتور خود را تست و مشخصات
برای نمایه کردن عملیات خود با ابزار بنچمارک TensorFlow Lite، می توانید از ابزار مدل معیار برای TensorFlow Lite استفاده کنید. برای اهداف آزمایشی، می توانید با افزودن فراخوان مناسب AddCustom
(همانطور که در بالا نشان داده شده است) به register.cc، ساخت محلی TensorFlow Lite را از عملیات سفارشی خود آگاه کنید.
بهترین شیوه ها
تخصیص حافظه و عدم تخصیص را با احتیاط بهینه کنید. تخصیص حافظه در
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
، کد شما نباید حافظه را معلق بگذارد، به عنوان مثال، این ماکروها باید قبل از تخصیص هرگونه منبعی که نشت می کند استفاده شود.