מפעילים מותאמים אישית

מאחר שספריית המפעילים המובנית של TensorFlow Lite תומכת רק במספר מוגבל של מפעילי TensorFlow, לא כל דגם ניתן להמרה. לפרטים, עיין בתאימות למפעיל .

כדי לאפשר המרה, משתמשים יכולים לספק יישום מותאם אישית משלהם של אופרטור TensorFlow שאינו נתמך ב-TensorFlow Lite, המכונה אופרטור מותאם אישית. אם במקום זאת, ברצונך לשלב סדרה של אופרטורים של TensorFlow שאינם נתמכים (או נתמכים) לאופרטור מותאם מותאם יחיד מותאם, עיין ב- Fuing Operator .

שימוש באופרטורים מותאמים אישית מורכב מארבעה שלבים.

בואו נעבור על דוגמה מקצה לקצה של הפעלת מודל עם אופרטור מותאם אישית tf.atan (ששמו Atan , עיין ב-#create_a_tensorflow_model) שנתמך ב-TensorFlow, אך אינו נתמך ב-TensorFlow Lite.

האופרטור TensorFlow Text הוא דוגמה לאופרטור מותאם אישית. עיין במדריך המרת טקסט TF ל-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 (הן מותאמים אישית והן מובנים) מוגדרים באמצעות ממשק pure-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() נתון ייקרא יותר מפעם אחת אם נעשה שימוש ב-op מספר פעמים בגרף. עבור פעולות מותאמות אישית יסופק מאגר תצורה, המכיל flexbuffer הממפה שמות פרמטרים לערכים שלהם. המאגר ריק עבור פעולות מובנות מכיוון שהמתורגמן כבר פרש את פרמטרי ההפעלה. יישומי ליבה הדורשים מצב צריכים לאתחל אותו כאן ולהעביר את הבעלות למתקשר. עבור כל קריאה init() תהיה קריאה מתאימה ל- free() , המאפשרת למימושים להיפטר מהמאגר שאולי הקצו ב- init() .

בכל פעם שמשנים את גודל טנסור הקלט, המתורגמן יעבור על הגרף ויודיע על יישומים של השינוי. זה נותן להם את ההזדמנות לשנות את גודל המאגר הפנימי שלהם, לבדוק תקפות של צורות וסוגי קלט ולחשב מחדש צורות פלט. כל זה נעשה באמצעות prepare() , ומימושים יכולים לגשת למצב שלהם באמצעות node->user_data .

לבסוף, בכל פעם שהמסק רץ, המתורגמן חוצה את הגרף הקורא invoke() , וגם כאן המצב זמין כ- node->user_data .

ניתן ליישם פעולות מותאמות אישית בדיוק באותו אופן כמו פעולות מובנות, על ידי הגדרת ארבע הפונקציות הללו ופונקציית רישום גלובלית שנראית בדרך כלל כך:

namespace my_namespace {
  const TfLiteRegistration* Register_MY_CUSTOM_OP() {
    static const TfLiteRegistration r = {my_custom_op::Init,
                                         my_custom_op::Free,
                                         my_custom_op::Prepare,
                                         my_custom_op::Eval};
    return &r;
  }
}  // namespace my_namespace

שימו לב שההרשמה אינה אוטומטית ויש לבצע קריאה מפורשת ל- 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;
}

const TfLiteRegistration* Register_ATAN() {
  static const TfLiteRegistration r = {nullptr, nullptr, AtanPrepare, AtanEval};
  return &r;
}

בעת אתחול ה- OpResolver , הוסף את ה-op המותאם אישית לפותר (ראה למטה לדוגמא). זה ירשום את המפעיל עם Tensorflow Lite כך ש-TensorFlow Lite יוכל להשתמש ביישום החדש. שים לב ששני הארגומנטים האחרונים ב- TfLiteRegistration תואמים לפונקציות AtanPrepare ו- AtanEval שהגדרת עבור ה-Op Custom. אם השתמשת בפונקציות AtanInit ו- AtanFree כדי לאתחל משתנים המשמשים ב-op וכדי לפנות מקום, בהתאמה, אז הם יתווספו לשני הארגומנטים הראשונים של TfLiteRegistration ; ארגומנטים אלה מוגדרים ל- nullptr בדוגמה זו.

רשום את המפעיל עם ספריית הליבה

כעת עלינו לרשום את האופרטור עם ספריית הליבה. זה נעשה עם OpResolver . מאחורי הקלעים, המתורגמן יטען ספריית גרעינים אשר תוקצה לביצוע כל אחד מהאופרטורים במודל. בעוד שספריית ברירת המחדל מכילה רק גרעינים מובנים, אפשר להחליף/להגדיל אותה עם אופרטורים של ספרייה מותאמת אישית.

מחלקת OpResolver , המתרגמת קודי אופרטור ושמות לקוד בפועל, מוגדרת כך:

class OpResolver {
 public:
  virtual TfLiteRegistration* FindOp(tflite::BuiltinOperator op) const = 0;
  virtual TfLiteRegistration* FindOp(const char* op) const = 0;
  ...
};

המחלקות MutableOpResolver ו- BuiltinOpResolver נגזרים מ- OpResolver :

class MutableOpResolver : public OpResolver {
 public:
  MutableOpResolver();  // Constructs an initially empty op resolver.
  void AddBuiltin(tflite::BuiltinOperator op, const TfLiteRegistration* registration) = 0;
  void AddCustom(const char* op, const TfLiteRegistration* registration) = 0;
  void AddAll(const MutableOpResolver& other);
  ...
};

class BuiltinOpResolver : public MutableOpResolver {
 public:
  BuiltinOpResolver();  // Constructs an op resolver with all the builtin ops.
};

שימוש רגיל מחייב שימוש ב- BuiltinOpResolver ולכתוב:

tflite::ops::builtin::BuiltinOpResolver resolver;

כדי להוסיף את האופציה המותאמת אישית שנוצרה למעלה, אתה יכול במקום זאת להשתמש ב- MutableOpResolver , ולהתקשר AddCustom (לפני שתעביר את הפותר ל- InterpreterBuilder ):

tflite::ops::builtin::MutableOpResolver resolver;
resolver.AddAll(tflite::ops::builtin::BuiltinOpResolver());
resolver.AddCustom("Atan", Register_ATAN());

אם קבוצת המבצעים המובנים נחשבת גדולה מדי, ניתן ליצור OpResolver חדש על סמך תת-קבוצה נתונה של פעולות, אולי רק אלה הכלולות במודל נתון. זו המקבילה לרישום הסלקטיבי של TensorFlow (וגרסה פשוטה שלו זמינה בספריית tools ).

אם ברצונך להגדיר את האופרטורים המותאמים אישית שלך ב-Java, כעת תצטרך לבנות שכבת JNI מותאמת אישית משלך ולהרכיב AAR משלך בקוד jni זה . באופן דומה, אם ברצונך להגדיר את האופרטורים הללו הזמינים ב-Python, תוכל למקם את הרישומים שלך בקוד המעטפת של Python .

שימו לב שניתן לבצע תהליך דומה לעיל לתמיכה בסט פעולות במקום אופרטור בודד. פשוט הוסף כמה אופרטורים AddCustom שאתה צריך. בנוסף, MutableOpResolver גם מאפשר לך לעקוף יישומים של מובנים באמצעות AddBuiltin .

בדוק ופרופיל את המפעיל שלך

כדי ליצור פרופיל פעולה עם כלי ההשוואה של TensorFlow Lite, אתה יכול להשתמש בכלי מודל הבנצ'מרק עבור TensorFlow Lite. למטרות בדיקה, אתה יכול להפוך את המבנה המקומי שלך של TensorFlow Lite מודע לאופציה המותאמת אישית שלך על ידי הוספת הקריאה AddCustom המתאימה (כפי שמוצג לעיל) ל- register.cc

שיטות עבודה מומלצות

  1. בצע אופטימיזציה של הקצאות זיכרון וביטול הקצאות בזהירות. הקצאת זיכרון ב- Prepare יעילה יותר מאשר ב- Invoke , והקצאת זיכרון לפני לולאה טובה יותר מאשר בכל איטרציה. השתמש בנתוני טנסורים זמניים במקום להזיז את עצמך (ראה פריט 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 , כלומר, יש להשתמש בפקודות מאקרו אלו לפני שהוקצו משאבים כלשהם שידלפו.