מאחר שספריית המפעילים המובנית של TensorFlow Lite תומכת רק במספר מוגבל של אופרטורים של TensorFlow, לא כל דגם ניתן להמרה. לפרטים, עיין בתאימות למפעיל .
כדי לאפשר המרה, משתמשים יכולים לספק יישום מותאם אישית משלהם של אופרטור TensorFlow שאינו נתמך ב-TensorFlow Lite, המכונה אופרטור מותאם אישית. אם במקום זאת, ברצונך לשלב סדרה של אופרטורים של TensorFlow שאינם נתמכים (או נתמכים) לאופרטור מותאם מותאם יחיד מותאם, עיין ב- fusing מפעיל .
השימוש באופרטורים מותאמים אישית מורכב מארבעה שלבים.
צור מודל TensorFlow. ודא שהמודל השמור (או Graph Def) מתייחס לאופרטור TensorFlow Lite בעל השם הנכון.
המר לדגם TensorFlow Lite. ודא שאתה מגדיר את תכונת הממיר TensorFlow Lite הנכונה כדי להמיר את המודל בהצלחה.
צור ורשום את המפעיל. זאת כדי שזמן הריצה של TensorFlow Lite ידע למפות את האופרטור והפרמטרים שלך בגרף שלך לקוד C/C++ בר הפעלה.
בדוק ופרופיל את המפעיל שלך. אם אתה רוצה לבדוק רק את האופרטור המותאם אישית שלך, עדיף ליצור מודל רק עם האופרטור המותאם אישית שלך ולהשתמש בתוכנית benchmark_model .
בואו נעבור על דוגמה מקצה לקצה של הפעלת מודל עם אופרטור מותאם אישית 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
שיטות עבודה מומלצות
בצע אופטימיזציה של הקצאות זיכרון וביטול הקצאות בזהירות. הקצאת זיכרון ב-
Prepare
יעילה יותר מאשר ב-Invoke
, והקצאת זיכרון לפני לולאה טובה יותר מאשר בכל איטרציה. השתמש בנתוני טנסורים זמניים במקום להזיז את עצמך (ראה פריט 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;
אם זה לא עולה יותר מדי זיכרון מבוזבז, העדיפו להשתמש במערך סטטי בגודל קבוע (או ב-
std::vector
שהוקצה מראש ב-Resize
) במקום להשתמש ב-std::vector
שהוקצה באופן דינמי בכל איטרציה של ביצוע.הימנע מיצירת תבניות מיכל סטנדרטיות של ספרייה שעדיין לא קיימות, מכיוון שהן משפיעות על הגודל הבינארי. לדוגמה, אם אתה זקוק
std::map
בפעולה שלך שאינה קיימת בקרנלים אחרים, שימוש ב-std::vector
עם מיפוי אינדקס ישיר יכול לעבוד תוך שמירה על הגודל הבינארי קטן. ראה באילו גרעינים אחרים משתמשים כדי לקבל תובנה (או לשאול).בדוק את המצביע לזיכרון שהוחזר על ידי
malloc
. אם מצביע זה הואnullptr
, אין לבצע פעולות באמצעות מצביע זה. אם אתה ממוקםmalloc
ויש לך שגיאה ביציאה, הקצאת זיכרון לפני שאתה יוצא.השתמש
TF_LITE_ENSURE(context, condition)
כדי לבדוק אם יש תנאי ספציפי. אסור שהקוד שלך ישאיר את הזיכרון תלוי כאשר נעשה שימושTF_LITE_ENSURE
, כלומר, יש להשתמש בפקודות מאקרו אלו לפני שהוקצו משאבים כלשהם שידלפו.