ขอขอบคุณที่เข้าร่วม Google I/O ดูเซสชั่นทั้งหมดตามความต้องการ ดูตามความต้องการ

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

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

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

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

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

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

  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 กล่าวคือควรใช้มาโครเหล่านี้ก่อนที่จะจัดสรรทรัพยากรที่จะรั่วไหล