เนื่องจากไลบรารีโอเปอเรเตอร์ในตัว TensorFlow Lite รองรับเฉพาะโอเปอเรเตอร์ TensorFlow ในจำนวนจำกัด ไม่ใช่ทุกรุ่นที่แปลงได้ สำหรับรายละเอียด โปรดดูที่ ความเข้ากันได้ของโอเปอเรเตอร์
ในการอนุญาตให้มีการแปลง ผู้ใช้สามารถระบุการใช้งานตัวดำเนินการ TensorFlow ที่ไม่รองรับใน TensorFlow Lite หรือที่เรียกว่าตัวดำเนินการแบบกำหนดเอง หากคุณต้องการรวมชุดของตัวดำเนินการ TensorFlow ที่ไม่รองรับ (หรือรองรับ) เข้ากับตัวดำเนินการแบบกำหนดเองที่ปรับแต่งแล้วแบบหลอมรวมเดียว โปรดดูที่ ตัวดำเนินการหลอมรวม
การใช้ตัวดำเนินการแบบกำหนดเองประกอบด้วยสี่ขั้นตอน
สร้างโมเดล TensorFlow ตรวจสอบให้แน่ใจว่าโมเดลที่บันทึกไว้ (หรือ Graph Def) อ้างอิงถึงตัวดำเนินการ TensorFlow Lite ที่มีชื่อถูกต้อง
แปลงเป็นโมเดล TensorFlow Lite ตรวจสอบว่าคุณตั้งค่าแอตทริบิวต์ตัวแปลง TensorFlow Lite ที่ถูกต้องเพื่อให้แปลงโมเดลได้สำเร็จ
สร้างและลงทะเบียนผู้ประกอบการ ทั้งนี้เพื่อให้รันไทม์ TensorFlow Lite รู้วิธีแมปโอเปอเรเตอร์และพารามิเตอร์ในกราฟของคุณกับโค้ด C/C++ ที่เรียกใช้งานได้
ทดสอบและสร้างโปรไฟล์ผู้ให้บริการของคุณ หากคุณต้องการทดสอบเฉพาะโอเปอเรเตอร์ที่กำหนดเอง วิธีที่ดีที่สุดคือสร้างแบบจำลองด้วยโอเปอเรเตอร์ที่กำหนดเองและใช้โปรแกรม benchmark_model
มาดูตัวอย่างแบบ end-to-end ของการรันโมเดลด้วยตัวดำเนินการแบบกำหนดเอง 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 ด้วยตัวดำเนินการแบบกำหนดเอง โดยตั้งค่าแอตทริบิวต์ converter 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 หลายครั้งในกราฟ สำหรับ ops แบบกำหนดเอง จะมีการจัดเตรียมบัฟเฟอร์การกำหนดค่า ซึ่งมี flexbuffer ที่แม็พชื่อพารามิเตอร์กับค่าของพวกมัน บัฟเฟอร์ว่างเปล่าสำหรับ builtin ops เนื่องจากล่ามได้แยกวิเคราะห์พารามิเตอร์ op แล้ว การใช้งานเคอร์เนลที่ต้องการสถานะควรเริ่มต้นที่นี่และโอนความเป็นเจ้าของไปยังผู้โทร สำหรับการเรียก init()
แต่ละครั้ง จะมีการเรียกที่สอดคล้องกันไปยัง free()
ทำให้การใช้งานสามารถกำจัดบัฟเฟอร์ที่พวกเขาอาจจัดสรรไว้ใน init()
เมื่อใดก็ตามที่เทนเซอร์อินพุตถูกปรับขนาด ตัวแปลจะแสดงกราฟเพื่อแจ้งให้ทราบถึงการเปลี่ยนแปลง สิ่งนี้ทำให้พวกเขามีโอกาสปรับขนาดบัฟเฟอร์ภายใน ตรวจสอบความถูกต้องของรูปร่างและประเภทอินพุต และคำนวณรูปร่างเอาต์พุตใหม่ ทั้งหมดนี้ทำผ่าน prepare()
และการนำไปใช้งานสามารถเข้าถึงสถานะได้โดยใช้ node->user_data
สุดท้าย แต่ละครั้งที่การอนุมานทำงาน ตัวแปลจะข้ามผ่านกราฟที่เรียกใช้ invoke()
และที่นี่ก็สามารถใช้สถานะเป็น node->user_data
Custom ops สามารถนำไปใช้ได้ในลักษณะเดียวกับ builtin ops โดยกำหนดสี่ฟังก์ชันเหล่านี้และฟังก์ชันการลงทะเบียนส่วนกลางที่มักจะมีลักษณะดังนี้:
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
เป้าหมาย) ดูแลการลงทะเบียนของ buildins แต่ 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;
}
TfLiteRegistration* Register_ATAN() {
static TfLiteRegistration r = {nullptr, nullptr, AtanPrepare, AtanEval};
return &r;
}
เมื่อเริ่มต้น OpResolver
ให้เพิ่ม op ที่กำหนดเองลงในตัวแก้ไข (ดูตัวอย่างด้านล่าง) สิ่งนี้จะลงทะเบียนตัวดำเนินการกับ Tensorflow Lite เพื่อให้ TensorFlow Lite สามารถใช้การใช้งานใหม่ได้ โปรดทราบว่าสองอาร์กิวเมนต์สุดท้ายใน TfLiteRegistration
สอดคล้องกับฟังก์ชัน AtanPrepare
และ AtanEval
ที่คุณกำหนดไว้สำหรับ op แบบกำหนดเอง หากคุณใช้ฟังก์ชัน AtanInit
และ AtanFree
เพื่อเริ่มต้นตัวแปรที่ใช้ใน op และเพิ่มพื้นที่ว่างตามลำดับ ตัวแปรเหล่านั้นจะถูกเพิ่มในสองอาร์กิวเมนต์แรกของ TfLiteRegistration
อาร์กิวเมนต์เหล่านั้นถูกตั้งค่าเป็น nullptr
ในตัวอย่างนี้
ลงทะเบียนตัวดำเนินการกับไลบรารีเคอร์เนล
ตอนนี้เราต้องลงทะเบียนตัวดำเนินการกับไลบรารีเคอร์เนล สิ่งนี้ทำได้ด้วย OpResolver
เบื้องหลัง ล่ามจะโหลดไลบรารีของเคอร์เนลซึ่งจะถูกกำหนดให้ดำเนินการกับตัวดำเนินการแต่ละตัวในโมเดล แม้ว่าไลบรารีเริ่มต้นจะมีเฉพาะเคอร์เนลในตัว แต่คุณสามารถแทนที่/เพิ่มไลบรารีด้วยตัวดำเนินการ op ของไลบรารีที่กำหนดเองได้
คลาส 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;
หากต้องการเพิ่ม op แบบกำหนดเองที่สร้างขึ้นด้านบน คุณต้องเรียกใช้ AddOp
(ก่อนที่คุณจะส่งตัวแก้ไขไปยัง InterpreterBuilder
):
resolver.AddCustom("Atan", Register_ATAN());
หากถือว่าชุดของ ops ในตัวมีขนาดใหญ่เกินไป OpResolver
ใหม่อาจถูกสร้างรหัสตามชุดย่อยของ ops ที่กำหนด ซึ่งอาจเป็นเพียงชุดที่มีอยู่ในโมเดลที่กำหนดเท่านั้น ซึ่งเทียบเท่ากับการลงทะเบียนแบบเลือกของ TensorFlow (และรุ่นที่เรียบง่ายมีอยู่ในไดเร็กทอรี tools
)
หากคุณต้องการกำหนดโอเปอเรเตอร์แบบกำหนดเองของคุณใน Java คุณจะต้องสร้างเลเยอร์ JNI ของคุณเองและคอมไพล์ AAR ของคุณเอง ในโค้ด jni นี้ ในทำนองเดียวกัน หากคุณต้องการกำหนดโอเปอเรเตอร์เหล่านี้ใน Python คุณสามารถวางการลงทะเบียนของคุณใน Python wrapper code
โปรดทราบว่าสามารถปฏิบัติตามกระบวนการที่คล้ายคลึงกันข้างต้นเพื่อสนับสนุนชุดของการดำเนินการแทนตัวดำเนินการเดียว เพียงเพิ่มตัวดำเนินการ AddCustom
ได้มากเท่าที่คุณต้องการ นอกจากนี้ BuiltinOpResolver
ยังอนุญาตให้คุณแทนที่การใช้งาน buildins โดยใช้ AddBuiltin
ทดสอบและสร้างโปรไฟล์ผู้ให้บริการของคุณ
หากต้องการสร้างโปรไฟล์ op ของคุณด้วยเครื่องมือวัดประสิทธิภาพ TensorFlow Lite คุณสามารถใช้ เครื่องมือแบบจำลองการเปรียบเทียบ สำหรับ TensorFlow Lite เพื่อวัตถุประสงค์ในการทดสอบ คุณสามารถทำให้ TensorFlow Lite รุ่นในเครื่องของคุณทราบถึง op แบบกำหนดเองของคุณได้โดยเพิ่มการเรียก AddCustom
ที่เหมาะสม (ดังที่แสดงด้านบน) ใน register.cc
ปฏิบัติที่ดีที่สุด
ปรับการจัดสรรหน่วยความจำให้เหมาะสมและยกเลิกการจัดสรรอย่างระมัดระวัง การจัดสรรหน่วยความจำใน
Prepare
จะมีประสิทธิภาพมากกว่าในInvoke
และการจัดสรรหน่วยความจำก่อนการวนซ้ำจะดีกว่าในการวนซ้ำทุกครั้ง ใช้ข้อมูลเทนเซอร์ชั่วคราวแทนการทำให้ตัวเองเสียหาย (ดูข้อ 2) ใช้ตัวชี้/การอ้างอิงแทนการคัดลอกให้มากที่สุดหากโครงสร้างข้อมูลยังคงอยู่ในระหว่างการดำเนินการทั้งหมด เราแนะนำให้จัดสรรหน่วยความจำล่วงหน้าโดยใช้เทนเซอร์ชั่วคราว คุณอาจต้องใช้โครงสร้าง OpData เพื่ออ้างอิงดัชนีเทนเซอร์ในฟังก์ชันอื่นๆ ดูตัวอย่างใน เคอร์เนลสำหรับการบิด ข้อมูลโค้ดตัวอย่างอยู่ด้านล่าง
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
กล่าวคือควรใช้มาโครเหล่านี้ก่อนที่จะจัดสรรทรัพยากรที่จะรั่วไหล