ตัวดำเนินการที่กำหนดเอง

จัดทุกอย่างให้เป็นระเบียบอยู่เสมอด้วยคอลเล็กชัน บันทึกและจัดหมวดหมู่เนื้อหาตามค่ากำหนดของคุณ

เนื่องจากไลบรารีโอเปอเรเตอร์ในตัวของ TensorFlow Lite รองรับตัวดำเนินการ TensorFlow ในจำนวนจำกัดเท่านั้น จึงไม่สามารถเปลี่ยนได้ทุกรุ่น สำหรับรายละเอียด โปรดดูที่ ความเข้ากันได้ ของผู้ปฏิบัติงาน

เพื่อให้สามารถแปลงได้ ผู้ใช้สามารถจัดเตรียมการใช้งานที่กำหนดเองของตัวดำเนินการ TensorFlow ที่ไม่สนับสนุนใน TensorFlow Lite หรือที่เรียกว่าตัวดำเนินการแบบกำหนดเอง หากคุณต้องการรวมชุดของโอเปอเรเตอร์ TensorFlow ที่ไม่รองรับ (หรือรองรับ) ไว้ในโอเปอเรเตอร์แบบกำหนดเองที่ปรับให้เหมาะสมที่หลอมรวมแล้วตัวเดียว โปรดดูที่ ตัวดำเนินการ fusing

การใช้ตัวดำเนินการแบบกำหนดเองประกอบด้วยสี่ขั้นตอน

มาดูตัวอย่างแบบ 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

ปฏิบัติที่ดีที่สุด

  1. เพิ่มประสิทธิภาพการจัดสรรหน่วยความจำและยกเลิกการจัดสรรอย่างระมัดระวัง การจัดสรรหน่วยความจำในการ Prepare จะมีประสิทธิภาพมากกว่าใน Invoke และการจัดสรรหน่วยความจำก่อนการวนซ้ำจะดีกว่าในการวนซ้ำทุกครั้ง ใช้ข้อมูลเทนเซอร์ชั่วคราวแทนการหลอกตัวเอง (ดูข้อ 2) ใช้ตัวชี้/ข้อมูลอ้างอิงแทนการคัดลอกให้มากที่สุด

  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;
    
  3. หากไม่เสียค่าใช้จ่ายหน่วยความจำที่สูญเปล่ามากเกินไป ให้เลือกใช้อาร์เรย์ขนาดคงที่แบบคงที่ (หรือ std::vector ที่จัดสรรไว้ล่วงหน้าใน Resize ) แทนที่จะใช้ std::vector ที่จัดสรรแบบไดนามิกทุกครั้งที่ดำเนินการซ้ำ

  4. หลีกเลี่ยงการสร้างอินสแตนซ์ของเทมเพลตคอนเทนเนอร์ไลบรารีมาตรฐานที่ยังไม่มี เนื่องจากจะส่งผลต่อขนาดไบนารี ตัวอย่างเช่น หากคุณต้องการ std::map ในการดำเนินการของคุณที่ไม่มีอยู่ในเคอร์เนลอื่น การใช้ std::vector ที่มีการแมปการทำดัชนีโดยตรงอาจทำงานในขณะที่รักษาขนาดไบนารีให้เล็ก ดูว่าเมล็ดพืชชนิดอื่นใช้อะไรเพื่อให้ได้ข้อมูลเชิงลึก (หรือถาม)

  5. ตรวจสอบตัวชี้ไปยังหน่วยความจำที่ส่งคืนโดย malloc หากตัวชี้นี้เป็น nullptr ไม่ควรดำเนินการใด ๆ โดยใช้ตัวชี้นั้น หากคุณ malloc ในฟังก์ชันและมีข้อผิดพลาดในการออก ให้จัดสรรหน่วยความจำใหม่ก่อนที่คุณจะออก

  6. ใช้ TF_LITE_ENSURE(context, condition) เพื่อตรวจสอบเงื่อนไขเฉพาะ รหัสของคุณต้องไม่ปล่อยให้หน่วยความจำค้างเมื่อใช้ TF_LITE_ENSURE กล่าวคือ ควรใช้มาโครเหล่านี้ก่อนที่จะมีการจัดสรรทรัพยากรที่จะรั่วไหล