از اینکه با Google I/O تنظیم کردید متشکریم. مشاهده همه جلسات در صورت تقاضا تماشا کنید

اپراتورهای سفارشی

از آنجایی که کتابخانه اپراتور داخلی TensorFlow Lite تنها از تعداد محدودی از اپراتورهای TensorFlow پشتیبانی می کند، هر مدلی قابل تبدیل نیست. برای جزئیات، به سازگاری اپراتور مراجعه کنید.

برای اجازه دادن به تبدیل، کاربران می توانند پیاده سازی سفارشی خود را از یک اپراتور TensorFlow پشتیبانی نشده در TensorFlow Lite، که به عنوان یک اپراتور سفارشی شناخته می شود، ارائه دهند. در عوض، اگر می‌خواهید یک سری از عملگرهای پشتیبانی‌نشده (یا پشتیبانی‌شده) TensorFlow را در یک اپراتور سفارشی بهینه‌سازی شده واحد ترکیب کنید، به ترکیب عملگر مراجعه کنید.

استفاده از عملگرهای سفارشی شامل چهار مرحله است.

بیایید از طریق یک مثال سرتاسر اجرای یک مدل با یک عملگر سفارشی 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 را از عملیات سفارشی خود آگاه کنید.

بهترین شیوه ها

  1. تخصیص حافظه و عدم تخصیص را با احتیاط بهینه کنید. تخصیص حافظه در Prepare کارآمدتر از Invoke است و تخصیص حافظه قبل از حلقه بهتر از هر تکرار است. از داده‌های تانسور موقت به‌جای اینکه خودتان را مخفی کنید، استفاده کنید (به مورد 2 مراجعه کنید). تا حد امکان به جای کپی کردن از اشاره گرها / مراجع استفاده کنید.

  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;
    
  3. اگر هزینه زیادی برای حافظه تلف نمی شود، ترجیح دهید از یک آرایه با اندازه ثابت ثابت (یا یک std::vector از پیش تخصیص داده شده در Resize ) به جای استفاده از std::vector اختصاص داده شده به صورت پویا در هر تکرار اجرا استفاده کنید.

  4. از نمونه‌سازی الگوهای استاندارد کانتینر کتابخانه‌ای که قبلاً وجود ندارند، خودداری کنید، زیرا بر اندازه باینری تأثیر می‌گذارند. به عنوان مثال، اگر در عملیات خود به یک std::map نیاز دارید که در هسته های دیگر وجود ندارد، استفاده از یک std::vector با نگاشت نمایه سازی مستقیم می تواند در عین کوچک نگه داشتن اندازه باینری کار کند. ببینید هسته های دیگر از چه چیزهایی برای به دست آوردن بینش استفاده می کنند (یا بپرسید).

  5. نشانگر حافظه بازگشتی توسط malloc را بررسی کنید. اگر این اشاره گر nullptr باشد، هیچ عملیاتی نباید با استفاده از آن نشانگر انجام شود. اگر در یک تابع malloc دارید و خطای خروجی دارید، قبل از خروج، حافظه را اختصاص دهید.

  6. از TF_LITE_ENSURE(context, condition) برای بررسی یک شرایط خاص استفاده کنید. هنگام استفاده از TF_LITE_ENSURE ، کد شما نباید حافظه را معلق بگذارد، به عنوان مثال، این ماکروها باید قبل از تخصیص هرگونه منبعی که نشت می کند استفاده شود.