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

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

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

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

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

דוגמה: אופרטור Custom Sin

בואו נעבור על דוגמה לתמיכה באופרטור TensorFlow שאין ל-TensorFlow Lite. נניח שאנו משתמשים באופרטור Sin ושאנו בונים מודל פשוט מאוד לפונקציה y = sin(x + offset) , כאשר offset ניתן לאימון.

צור מודל TensorFlow

קטע הקוד הבא מאמן מודל TensorFlow פשוט. המודל הזה רק מכיל אופרטור מותאם אישית בשם 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

בשלב זה, אם תנסה ליצור מודל TensorFlow Lite עם דגלי ברירת המחדל של הממיר, תקבל את הודעת השגיאה הבאה:

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.

המר לדגם TensorFlow Lite

צור מודל TensorFlow Lite עם אופרטורים מותאמים אישית, על ידי הגדרת תכונת הממיר 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.

צור ורשום את המפעיל.

כל אופרטורי 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 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 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 , הוסף את ה-op המותאם אישית לפותר (ראה למטה לדוגמא). זה ירשום את המפעיל עם Tensorflow Lite כך ש-TensorFlow Lite יוכל להשתמש ביישום החדש. שים לב ששני הארגומנטים האחרונים ב- TfLiteRegistration תואמים SinPrepare ו- SinEval שהגדרת עבור ה-Op Custom. אם השתמשת 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 ).

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

שים לב שניתן לבצע תהליך דומה לעיל לתמיכה בסט פעולות במקום אופרטור יחיד. פשוט הוסף כמה אופרטורים AddCustom שאתה צריך. בנוסף, BuiltinOpResolver גם מאפשר לך לעקוף יישומים של מובנים על ידי שימוש ב- 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 , כלומר, יש להשתמש בפקודות מאקרו אלו לפני שהוקצו משאבים כלשהם שידלפו.