เนื่องจากไลบรารีโอเปอเรเตอร์ในตัวของ TensorFlow Lite รองรับตัวดำเนินการ TensorFlow ในจำนวนจำกัดเท่านั้น จึงไม่สามารถเปลี่ยนได้ทุกรุ่น สำหรับรายละเอียด โปรดดูที่ ความเข้ากันได้ ของผู้ปฏิบัติงาน
เพื่อให้สามารถแปลงได้ ผู้ใช้สามารถจัดเตรียมการใช้งานที่กำหนดเองของตัวดำเนินการ TensorFlow ที่ไม่สนับสนุนใน TensorFlow Lite หรือที่เรียกว่าตัวดำเนินการแบบกำหนดเอง หากคุณต้องการรวมชุดของโอเปอเรเตอร์ TensorFlow ที่ไม่รองรับ (หรือรองรับ) ไว้ในโอเปอเรเตอร์แบบกำหนดเองที่ปรับให้เหมาะสมที่หลอมรวมแล้วตัวเดียว โปรดดูที่ ตัวดำเนินการ fusing
การใช้ตัวดำเนินการแบบกำหนดเองประกอบด้วยสี่ขั้นตอน
สร้างโมเดล TensorFlow ตรวจสอบให้แน่ใจว่าโมเดลที่บันทึกไว้ (หรือ Graph Def) อ้างถึงตัวดำเนินการ TensorFlow Lite ที่มีชื่อถูกต้อง
แปลงเป็นโมเดล TensorFlow Lite ตรวจสอบให้แน่ใจว่าคุณได้ตั้งค่าแอตทริบิวต์ตัวแปลง TensorFlow Lite ที่ถูกต้องเพื่อที่จะแปลงโมเดลได้สำเร็จ
สร้างและลงทะเบียนโอเปอเรเตอร์ ทั้งนี้เพื่อให้รันไทม์ TensorFlow Lite รู้วิธีแมปโอเปอเรเตอร์และพารามิเตอร์ในกราฟของคุณกับโค้ด C/C++ ที่เรียกใช้งานได้
ทดสอบและกำหนดโปรไฟล์ผู้ปฏิบัติงานของคุณ หากคุณต้องการทดสอบเฉพาะโอเปอเรเตอร์แบบกำหนดเอง วิธีที่ดีที่สุดคือสร้างโมเดลด้วยโอเปอเรเตอร์แบบกำหนดเองของคุณและใช้โปรแกรม เบนช์ มาร์ก_โมเดล
มาดูตัวอย่างแบบ end-to-end ของการรันโมเดลด้วยโอเปอเรเตอร์ที่กำหนดเอง tf.sin
(ชื่อ Sin
อ้างถึง #create_a_tensorflow_model) ซึ่งรองรับใน TensorFlow แต่ไม่รองรับใน TensorFlow Lite
ตัวดำเนินการข้อความ TensorFlow เป็นตัวอย่างของตัวดำเนินการแบบกำหนดเอง ดูบทช่วยสอนการ แปลงข้อความ TF เป็น TF Lite สำหรับตัวอย่างโค้ด
ตัวอย่าง: ตัวดำเนินการ 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 ด้วยโอเปอเรเตอร์แบบกำหนดเอง โดยการตั้งค่าแอททริบิวต์ converter 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 ถูกใช้หลายครั้งในกราฟ สำหรับ ops แบบกำหนดเอง จะมีการจัดเตรียมบัฟเฟอร์คอนฟิกูเรชัน ซึ่งประกอบด้วย flexbuffer ที่แมปชื่อพารามิเตอร์กับค่า บัฟเฟอร์ว่างเปล่าสำหรับ builtin ops เนื่องจากล่ามได้แยกวิเคราะห์พารามิเตอร์ op แล้ว การใช้งานเคอร์เนลที่ต้องการสถานะควรเริ่มต้นที่นี่และโอนความเป็นเจ้าของไปยังผู้โทร สำหรับการเรียก init()
แต่ละครั้ง จะมีการเรียกใช้ free()
ที่สอดคล้องกัน ทำให้การใช้งานสามารถกำจัดบัฟเฟอร์ที่พวกเขาอาจจัดสรรไว้ใน init()
เมื่อใดก็ตามที่เมตริกซ์อินพุตถูกปรับขนาด ล่ามจะอ่านกราฟเพื่อแจ้งการใช้งานการเปลี่ยนแปลง สิ่งนี้ทำให้พวกเขามีโอกาสปรับขนาดบัฟเฟอร์ภายใน ตรวจสอบความถูกต้องของรูปร่างและประเภทของอินพุต และคำนวณรูปร่างเอาต์พุตใหม่ ทั้งหมดนี้ทำได้ผ่าน prepare()
และการใช้งานสามารถเข้าถึงสถานะได้โดยใช้ node->user_data
สุดท้าย ทุกครั้งที่มีการอนุมาน ล่ามจะข้ามผ่านกราฟที่เรียกใช้ invoke 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
เป้าหมาย) จะดูแลการลงทะเบียนของบิวด์อิน แต่ 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 ที่กำหนดเอง หากคุณใช้ SinInit
และ SinFree
เพื่อเริ่มต้นตัวแปรที่ใช้ใน 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("Sin", Register_SIN());
หากชุดของ ops ในตัวมีขนาดใหญ่เกินไป OpResolver
ใหม่สามารถสร้างโค้ดตามชุดย่อยของ ops ที่กำหนด อาจเป็นเฉพาะชุดที่อยู่ในโมเดลที่กำหนดเท่านั้น ซึ่งเทียบเท่ากับการลงทะเบียนแบบเลือกของ TensorFlow (และมีเวอร์ชันง่าย ๆ อยู่ในไดเรกทอรี tools
)
หากคุณต้องการกำหนดโอเปอเรเตอร์ที่กำหนดเองใน Java คุณจะต้องสร้างเลเยอร์ JNI ที่กำหนดเองและคอมไพล์ AAR ของคุณเอง ในโค้ด jni นี้ ในทำนองเดียวกัน หากคุณต้องการกำหนดโอเปอเรเตอร์เหล่านี้ที่มีอยู่ใน Python คุณสามารถลงทะเบียนของคุณใน Python wrapper code
โปรดทราบว่าสามารถปฏิบัติตามกระบวนการที่คล้ายคลึงกันข้างต้นเพื่อสนับสนุนชุดของการดำเนินการแทนตัวดำเนินการเพียงตัวเดียว เพียงเพิ่มตัวดำเนินการ AddCustom
มากเท่าที่คุณต้องการ นอกจากนี้ BuiltinOpResolver
ยังอนุญาตให้คุณแทนที่การใช้งานบิวด์อินโดยใช้ 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
กล่าวคือ ควรใช้มาโครเหล่านี้ก่อนที่จะมีการจัดสรรทรัพยากรที่จะรั่วไหล