ช่วยปกป้อง Great Barrier Reef กับ TensorFlow บน Kaggle เข้าร่วมท้าทาย

ตัวดำเนินการแบบกำหนดเอง

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

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

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

ใช้เวลาเดิน Let 's ผ่านตัวอย่างแบบ end-to-end ของการทำงานรูปแบบที่มีผู้ประกอบการที่กำหนดเอง tf.sin (ชื่อเป็น Sin อ้างถึง #create_a_tensorflow_model) ซึ่งได้รับการสนับสนุนใน TensorFlow แต่ได้รับการสนับสนุนใน TensorFlow 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 กับผู้ประกอบการที่กำหนดเองโดยการตั้งค่าแปลงแอตทริบิวต์ 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() และที่นี่เกินไปรัฐสามารถใช้ได้เป็น 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 เป้าหมาย) ดูแลการลงทะเบียนของ builtins ที่ปฏิบัติการที่กำหนดเองจะต้องมีการเก็บรวบรวมไว้ในห้องสมุดที่กำหนดเองที่แยกต่างหาก

การกำหนดเคอร์เนลในรันไทม์ 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 เพิ่มสหกรณ์ที่กำหนดเองลงจำแนก (ดูด้านล่างสำหรับตัวอย่าง) การดำเนินการนี้จะลงทะเบียนโอเปอเรเตอร์กับ Tensorflow Lite เพื่อให้ TensorFlow Lite สามารถใช้การปรับใช้ใหม่ได้ โปรดทราบว่าทั้งสองมีปากเสียงสุดท้ายใน TfLiteRegistration สอดคล้องกับ SinPrepare และ SinEval ฟังก์ชั่นที่คุณกำหนดไว้สำหรับสหกรณ์ที่กำหนดเอง หากคุณใช้ SinInit และ SinFree ฟังก์ชั่นที่จะเริ่มต้นตัวแปรที่ใช้ในสหกรณ์และเพื่อเพิ่มพื้นที่ว่างตามลำดับแล้วพวกเขาจะถูกเพิ่มไปยังสองอาร์กิวเมนต์แรกของ 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;

หากต้องการเพิ่มสหกรณ์ที่กำหนดเองที่สร้างขึ้นดังกล่าวข้างต้นที่คุณเรียก AddOp (ก่อนที่คุณจะผ่านการจำแนกไปยัง InterpreterBuilder ):

resolver.AddCustom("Sin", Register_SIN());

หากชุดปฏิบัติการในตัวจะถือว่ามีขนาดใหญ่เกินไปใหม่ OpResolver อาจเป็นรหัสที่สร้างอยู่บนพื้นฐานของเซตที่กำหนดของ Ops อาจจะเป็นคนเดียวที่มีอยู่ในรูปแบบที่กำหนด นี้จะเทียบเท่ากับการลงทะเบียนเลือก TensorFlow ของ (และรุ่นที่เรียบง่ายของมันมีอยู่ใน tools directory)

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

โปรดทราบว่าสามารถปฏิบัติตามกระบวนการที่คล้ายคลึงกันข้างต้นเพื่อสนับสนุนชุดของการดำเนินการแทนตัวดำเนินการเพียงตัวเดียว เพียงแค่เพิ่มเป็นจำนวนมาก AddCustom ผู้ประกอบการตามที่คุณต้องการ นอกจากนี้ BuiltinOpResolver ยังช่วยให้คุณสามารถแทนที่การใช้งานของ builtins โดยใช้ AddBuiltin

ทดสอบและกำหนดโปรไฟล์ผู้ปฏิบัติงานของคุณ

ไปที่โปรไฟล์ op ของคุณด้วยเครื่องมือมาตรฐาน TensorFlow Lite คุณสามารถใช้ เครื่องมือรูปแบบมาตรฐาน สำหรับ TensorFlow Lite สำหรับวัตถุประสงค์ในการทดสอบคุณสามารถทำให้การสร้างภายในประเทศของคุณ TensorFlow Lite ตระหนักถึงสหกรณ์กำหนดเองของคุณโดยการเพิ่มที่เหมาะสม 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 ในการทำงานและมีการออกข้อผิดพลาดของหน่วยความจำ deallocate ก่อนที่คุณจะออกจาก

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