Google I/O में ट्यूनिंग के लिए धन्यवाद। मांग पर सभी सत्र देखें मांग पर देखें

कस्टम ऑपरेटर

चूंकि TensorFlow Lite निर्मित ऑपरेटर लाइब्रेरी केवल सीमित संख्या में TensorFlow ऑपरेटरों का समर्थन करती है, प्रत्येक मॉडल परिवर्तनीय नहीं है। विवरण के लिए, ऑपरेटर संगतता देखें।

रूपांतरण की अनुमति देने के लिए, उपयोगकर्ता TensorFlow Lite में एक असमर्थित TensorFlow ऑपरेटर का अपना कस्टम कार्यान्वयन प्रदान कर सकते हैं, जिसे कस्टम ऑपरेटर के रूप में जाना जाता है। यदि इसके बजाय, आप असमर्थित (या समर्थित) TensorFlow ऑपरेटरों की एक श्रृंखला को एक फ़्यूज्ड अनुकूलित कस्टम ऑपरेटर में संयोजित करना चाहते हैं, तो ऑपरेटर फ़्यूज़िंग देखें।

कस्टम ऑपरेटरों का उपयोग करने में चार चरण होते हैं।

आइए एक कस्टम ऑपरेटर tf.atan (जिसका नाम Atan है, #create_a_tensorflow_model देखें) के साथ एक मॉडल चलाने के शुरू से अंत तक के उदाहरण पर चलते हैं, जो TensorFlow में समर्थित है, लेकिन TensorFlow Lite में असमर्थित है।

TensorFlow टेक्स्ट ऑपरेटर कस्टम ऑपरेटर का एक उदाहरण है। कोड उदाहरण के लिए TF टेक्स्ट को TF लाइट में बदलें ट्यूटोरियल देखें।

उदाहरण: कस्टम Atan ऑपरेटर

आइए एक TensorFlow ऑपरेटर का समर्थन करने का एक उदाहरण देखें जो TensorFlow Lite के पास नहीं है। मान लें कि हम Atan ऑपरेटर का उपयोग कर रहे हैं और हम फ़ंक्शन y = atan(x + offset) के लिए एक बहुत ही सरल मॉडल बना रहे हैं, जहां offset ट्रेन करने योग्य है।

एक टेंसरफ्लो मॉडल बनाएं

निम्नलिखित कोड स्निपेट एक साधारण 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 ऑपरेटर्स (कस्टम और बिल्टिन दोनों) को एक साधारण प्योर-सी इंटरफ़ेस का उपयोग करके परिभाषित किया गया है जिसमें चार फ़ंक्शन होते हैं:

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 में ऑप का उपयोग करने के लिए हमें केवल दो कार्यों ( 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 पर सेट हैं।

ऑपरेटर को कर्नेल लाइब्रेरी के साथ पंजीकृत करें

अब हमें ऑपरेटर को कर्नेल लाइब्रेरी के साथ पंजीकृत करने की आवश्यकता है। यह एक 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 निर्देशिका में उपलब्ध है)।

यदि आप जावा में अपने कस्टम ऑपरेटरों को परिभाषित करना चाहते हैं, तो आपको वर्तमान में अपनी खुद की कस्टम जेएनआई परत बनाने और इस जेनी कोड में अपना एएआर संकलित करने की आवश्यकता होगी। इसी तरह, यदि आप पायथन में उपलब्ध इन ऑपरेटरों को परिभाषित करना चाहते हैं, तो आप अपने पंजीकरण को पायथन रैपर कोड में रख सकते हैं।

ध्यान दें कि एक ही ऑपरेटर के बजाय संचालन के एक सेट का समर्थन करने के लिए उपरोक्त के समान प्रक्रिया का पालन किया जा सकता है। बस जितने की जरूरत हो उतने AddCustom ऑपरेटर जोड़ें। इसके अलावा, BuiltinOpResolver आपको AddBuiltin का उपयोग करके बिलिन के कार्यान्वयन को ओवरराइड करने की भी अनुमति देता है।

अपने ऑपरेटर का परीक्षण और प्रोफाइल करें

TensorFlow Lite बेंचमार्क टूल के साथ अपने ऑप को प्रोफाइल करने के लिए, आप TensorFlow Lite के लिए बेंचमार्क मॉडल टूल का उपयोग कर सकते हैं। परीक्षण उद्देश्यों के लिए, आप TensorFlow Lite के अपने स्थानीय बिल्ड को register.cc में उपयुक्त AddCustom कॉल (जैसा कि ऊपर दिखाया गया है) जोड़कर अपने कस्टम ऑप से अवगत करा सकते हैं।

सर्वोत्तम प्रथाएं

  1. स्मृति आवंटन और डी-आवंटन को सावधानी से अनुकूलित करें। Invoke की तुलना में Prepare में मेमोरी आवंटित करना अधिक कुशल है, और प्रत्येक पुनरावृत्ति की तुलना में लूप से पहले मेमोरी आवंटित करना बेहतर है। अपने आप को मैलो करने के बजाय अस्थायी टेंसर डेटा का उपयोग करें (आइटम 2 देखें)। जितना संभव हो कॉपी करने के बजाय पॉइंटर्स/संदर्भों का प्रयोग करें।

  2. यदि कोई डेटा संरचना पूरे ऑपरेशन के दौरान बनी रहती है, तो हम अस्थायी टेन्सर का उपयोग करके मेमोरी को पूर्व-आवंटित करने की सलाह देते हैं। अन्य कार्यों में टेंसर इंडेक्स को संदर्भित करने के लिए आपको ओपडाटा स्ट्रक्चर का उपयोग करने की आवश्यकता हो सकती है। कनवल्शन के लिए कर्नेल में उदाहरण देखें। एक नमूना कोड स्निपेट नीचे है

    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 std::vector निष्पादन के प्रत्येक पुनरावृत्ति का उपयोग करने के बजाय एक स्थिर निश्चित आकार सरणी (या पूर्व-आवंटित std::vector in Resize ) का उपयोग करना पसंद करें।

  4. मानक लाइब्रेरी कंटेनर टेम्प्लेट को तत्काल करने से बचें जो पहले से मौजूद नहीं हैं, क्योंकि वे बाइनरी आकार को प्रभावित करते हैं। उदाहरण के लिए, यदि आपको अपने ऑपरेशन में एक std::map आवश्यकता है जो अन्य गुठली में मौजूद नहीं है, तो सीधे इंडेक्सिंग मैपिंग के साथ एक std::vector का उपयोग करना बाइनरी आकार को छोटा रखते हुए काम कर सकता है। देखें कि अंतर्दृष्टि प्राप्त करने के लिए अन्य कर्नेल क्या उपयोग करते हैं (या पूछें)।

  5. पॉइंटर को malloc द्वारा लौटाई गई मेमोरी की जाँच करें। यदि यह पॉइंटर nullptr है, तो उस पॉइंटर का उपयोग करके कोई ऑपरेशन नहीं किया जाना चाहिए। यदि आप किसी फ़ंक्शन में malloc और बाहर निकलने में त्रुटि होती है, तो बाहर निकलने से पहले मेमोरी को हटा दें।

  6. किसी विशिष्ट स्थिति की जांच के लिए TF_LITE_ENSURE(context, condition) का उपयोग करें। जब TF_LITE_ENSURE उपयोग किया जाता है, तो आपका कोड मेमोरी को हैंग नहीं होने देना चाहिए, यानी, इन मैक्रोज़ का उपयोग किसी भी संसाधन को आवंटित करने से पहले किया जाना चाहिए जो लीक हो जाएगा।