কাস্টম অপারেটর

যেহেতু টেনসরফ্লো লাইট বিল্টইন অপারেটর লাইব্রেরি শুধুমাত্র সীমিত সংখ্যক টেনসরফ্লো অপারেটরকে সমর্থন করে, তাই প্রতিটি মডেল পরিবর্তনযোগ্য নয়। বিস্তারিত জানার জন্য, অপারেটর সামঞ্জস্যতা পড়ুন।

রূপান্তরের অনুমতি দেওয়ার জন্য, ব্যবহারকারীরা TensorFlow Lite-এ একটি অসমর্থিত TensorFlow অপারেটরের নিজস্ব কাস্টম বাস্তবায়ন প্রদান করতে পারে, যা একটি কাস্টম অপারেটর হিসাবে পরিচিত৷ যদি পরিবর্তে, আপনি অসমর্থিত (বা সমর্থিত) টেনসরফ্লো অপারেটরগুলির একটি একক ফিউজড অপ্টিমাইজড কাস্টম অপারেটরে একত্রিত করতে চান, অপারেটর ফিউজিং দেখুন।

কাস্টম অপারেটর ব্যবহার করা চারটি ধাপ নিয়ে গঠিত।

আসুন একটি কাস্টম অপারেটর tf.sin ( Sin নামে পরিচিত, #create_a_tensorflow_model পড়ুন) দিয়ে একটি মডেল চালানোর একটি এন্ড-টু-এন্ড উদাহরণের মধ্য দিয়ে চলুন যা TensorFlow-এ সমর্থিত, কিন্তু TensorFlow Lite-এ অসমর্থিত।

উদাহরণ: কাস্টম Sin অপারেটর

আসুন একটি টেনসরফ্লো অপারেটরকে সমর্থন করার একটি উদাহরণের মাধ্যমে চলুন যা টেনসরফ্লো লাইটে নেই। ধরে নিন আমরা Sin অপারেটর ব্যবহার করছি এবং আমরা একটি ফাংশন y = sin(x + offset) এর জন্য একটি খুব সাধারণ মডেল তৈরি করছি, যেখানে offset প্রশিক্ষণযোগ্য।

একটি টেনসরফ্লো মডেল তৈরি করুন

নিম্নলিখিত কোড স্নিপেট একটি সাধারণ টেনসরফ্লো মডেলকে প্রশিক্ষণ দেয়। এই মডেলটিতে শুধু 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

এই মুহুর্তে, আপনি যদি ডিফল্ট রূপান্তরকারী পতাকাগুলির সাথে একটি টেনসরফ্লো লাইট মডেল তৈরি করার চেষ্টা করেন, আপনি নিম্নলিখিত ত্রুটি বার্তাটি পাবেন:

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.

একটি টেনসরফ্লো লাইট মডেলে রূপান্তর করুন

নীচে দেখানো হিসাবে কনভার্টার অ্যাট্রিবিউট 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.

অপারেটর তৈরি করুন এবং নিবন্ধন করুন।

সমস্ত টেনসরফ্লো লাইট অপারেটর (কাস্টম এবং বিল্টিন উভয়ই) একটি সাধারণ বিশুদ্ধ-সি ইন্টারফেস ব্যবহার করে সংজ্ঞায়িত করা হয়েছে যা চারটি ফাংশন নিয়ে গঠিত:

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() একাধিকবার কল করা হবে যদি গ্রাফে অপটি একাধিকবার ব্যবহার করা হয়। কাস্টম অপ্সের জন্য একটি কনফিগারেশন বাফার প্রদান করা হবে, যেখানে একটি ফ্লেক্সবাফার রয়েছে যা প্যারামিটারের নামগুলি তাদের মানগুলির সাথে মানচিত্র করে। বিল্টইন অপ্সের জন্য বাফারটি খালি কারণ ইন্টারপ্রেটার ইতিমধ্যেই অপ প্যারামিটারগুলি পার্স করেছে৷ যে কার্নেল বাস্তবায়নের জন্য রাষ্ট্রের প্রয়োজন হয় তাদের এটিকে এখানে শুরু করা উচিত এবং মালিকানা কলারের কাছে হস্তান্তর করা উচিত। প্রতিটি 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-এ op ব্যবহার করার জন্য আমাদের যা করতে হবে তা হল দুটি ফাংশন ( 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 ফাংশন ব্যবহার করেন 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("Sin", Register_SIN());

যদি বিল্টইন অপ্সের সেটটি খুব বড় বলে মনে করা হয়, তাহলে একটি নতুন OpResolver কোড-জেনারেট করা যেতে পারে অপসের একটি প্রদত্ত উপসেটের উপর ভিত্তি করে, সম্ভবত শুধুমাত্র একটি প্রদত্ত মডেলে থাকাগুলি। এটি TensorFlow এর সিলেক্টিভ রেজিস্ট্রেশনের সমতুল্য (এবং এর একটি সহজ সংস্করণ tools ডিরেক্টরিতে পাওয়া যায়)।

আপনি যদি জাভাতে আপনার কাস্টম অপারেটরগুলিকে সংজ্ঞায়িত করতে চান তবে আপনাকে বর্তমানে আপনার নিজস্ব কাস্টম JNI স্তর তৈরি করতে হবে এবং এই jni কোডে আপনার নিজস্ব AAR কম্পাইল করতে হবে। একইভাবে, আপনি যদি পাইথনে উপলব্ধ এই অপারেটরগুলিকে সংজ্ঞায়িত করতে চান তাহলে আপনি Python র্যাপার কোডে আপনার নিবন্ধনগুলি স্থাপন করতে পারেন৷

উল্লেখ্য যে উপরের মত একটি অনুরূপ প্রক্রিয়া একটি একক অপারেটরের পরিবর্তে অপারেশনের সেট সমর্থন করার জন্য অনুসরণ করা যেতে পারে। শুধু আপনার প্রয়োজন হিসাবে অনেক AddCustom অপারেটর যোগ করুন. উপরন্তু, BuiltinOpResolver আপনাকে AddBuiltin ব্যবহার করে AddBuiltin বাস্তবায়ন ওভাররাইড করার অনুমতি দেয়।

পরীক্ষা এবং আপনার অপারেটর প্রোফাইল

TensorFlow Lite বেঞ্চমার্ক টুলের সাথে আপনার অপশনটি প্রোফাইল করতে, আপনি TensorFlow Lite-এর জন্য বেঞ্চমার্ক মডেল টুল ব্যবহার করতে পারেন। পরীক্ষার উদ্দেশ্যে, আপনি register.cc এ উপযুক্ত AddCustom কল যোগ করে TensorFlow Lite-এর স্থানীয় বিল্ডকে আপনার কাস্টম অপশন সম্পর্কে সচেতন করতে পারেন।

সেরা অনুশীলন

  1. সতর্কতার সাথে মেমরি বরাদ্দ এবং ডি-অ্যালোকেশন অপ্টিমাইজ করুন। Invoke এর তুলনায় Prepare এ মেমরি বরাদ্দ করা বেশি কার্যকরী, এবং লুপের আগে মেমরি বরাদ্দ করা প্রতিটি পুনরাবৃত্তির চেয়ে ভালো। নিজেকে মেলো করার পরিবর্তে অস্থায়ী টেনসর ডেটা ব্যবহার করুন (আইটেম 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 ব্যবহার করা হয় তখন আপনার কোড মেমরি ঝুলিয়ে রাখা উচিত নয়, অর্থাৎ, লিক হয়ে যাবে এমন কোনও সংস্থান বরাদ্দ করার আগে এই ম্যাক্রোগুলি ব্যবহার করা উচিত।