Hari Komunitas ML adalah 9 November! Bergabung dengan kami untuk update dari TensorFlow, JAX, dan lebih Pelajari lebih lanjut

Operator khusus

Karena library operator bawaan TensorFlow Lite hanya mendukung operator TensorFlow dalam jumlah terbatas, tidak semua model dapat dikonversi. Untuk rincian, lihat kompatibilitas Operator .

Untuk mengizinkan konversi, pengguna dapat menyediakan implementasi kustom mereka sendiri dari operator TensorFlow yang tidak didukung di TensorFlow Lite, yang dikenal sebagai operator kustom. Jika sebaliknya, Anda ingin menggabungkan serangkaian tidak didukung (atau didukung) operator TensorFlow ke operator kustom tunggal menyatu dioptimalkan, lihat sekering Operator .

Menggunakan operator kustom terdiri dari empat langkah.

Mari kita berjalan melalui contoh end-to-end menjalankan model dengan operator kustom tf.sin (bernama Sin , lihat #create_a_tensorflow_model) yang didukung dalam TensorFlow, tetapi tidak didukung di TensorFlow Lite.

Contoh: Custom Sin Operator

Mari kita telusuri contoh mendukung operator TensorFlow yang tidak dimiliki TensorFlow Lite. Asumsikan kita menggunakan Sin operator dan bahwa kita sedang membangun sebuah model yang sangat sederhana untuk fungsi y = sin(x + offset) , di mana offset adalah dilatih.

Buat Model TensorFlow

Cuplikan kode berikut melatih model TensorFlow sederhana. Model ini hanya berisi operator kustom bernama Sin , yang merupakan fungsi y = sin(x + offset) , di mana offset adalah dilatih.

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

Pada titik ini, jika Anda mencoba membuat model TensorFlow Lite dengan flag konverter default, Anda akan mendapatkan pesan kesalahan berikut:

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.

Konversikan ke Model TensorFlow Lite

Membuat model TensorFlow Lite dengan operator kustom, dengan menetapkan atribut converter allow_custom_ops seperti yang ditunjukkan di bawah ini:

converter = tf.lite.TFLiteConverter.from_concrete_functions([sin.get_concrete_function(x)], sin)
converter.allow_custom_ops = True
tflite_model = converter.convert()

Pada titik ini, jika Anda menjalankannya dengan penerjemah default, Anda akan mendapatkan pesan kesalahan berikut:

Error:
Didn't find custom operator for name 'Sin'
Registration failed.

Buat dan daftarkan operator.

Semua operator TensorFlow Lite (baik kustom maupun bawaan) ditentukan menggunakan antarmuka pure-C sederhana yang terdiri dari empat fungsi:

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;

Mengacu common.h untuk rincian tentang TfLiteContext dan TfLiteNode . Yang pertama menyediakan fasilitas pelaporan kesalahan dan akses ke objek global, termasuk semua tensor. Yang terakhir memungkinkan implementasi untuk mengakses input dan output mereka.

Ketika beban juru model, itu panggilan init() sekali untuk setiap node dalam grafik. Sebuah diberikan init() akan dipanggil lebih dari sekali jika op tersebut digunakan beberapa kali dalam grafik. Untuk operasi kustom, buffer konfigurasi akan disediakan, berisi flexbuffer yang memetakan nama parameter ke nilainya. Buffer kosong untuk ops bawaan karena interpreter telah mengurai parameter op. Implementasi kernel yang memerlukan status harus menginisialisasinya di sini dan mentransfer kepemilikan ke pemanggil. Untuk setiap init() panggilan, akan ada panggilan sesuai dengan free() , yang memungkinkan implementasi untuk membuang buffer mereka mungkin telah dialokasikan dalam init() .

Setiap kali tensor input diubah ukurannya, interpreter akan melalui grafik yang memberitahukan implementasi perubahan tersebut. Ini memberi mereka kesempatan untuk mengubah ukuran buffer internal mereka, memeriksa validitas bentuk dan jenis input, dan menghitung ulang bentuk output. Ini semua dilakukan melalui prepare() , dan implementasi dapat mengakses negara mereka menggunakan node->user_data .

Akhirnya, setiap kali inferensi berjalan, penafsir melintasi grafik panggilan invoke() , dan di sini juga negara tersedia sebagai node->user_data .

Operasi kustom dapat diimplementasikan dengan cara yang persis sama seperti operasi bawaan, dengan mendefinisikan keempat fungsi tersebut dan fungsi pendaftaran global yang biasanya terlihat seperti ini:

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

Perhatikan bahwa registrasi tidak otomatis dan panggilan eksplisit untuk Register_MY_CUSTOM_OP harus dilakukan. Sementara standar BuiltinOpResolver (tersedia dari :builtin_ops target) mengurus pendaftaran builtin, ops kustom harus dikumpulkan di perpustakaan kustom terpisah.

Mendefinisikan kernel di runtime TensorFlow Lite

Semua yang perlu kita lakukan untuk menggunakan op di TensorFlow Lite adalah mendefinisikan dua fungsi ( Prepare dan Eval ), dan membangun 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;
}

Saat inisialisasi OpResolver , tambahkan op kustom ke resolver (lihat di bawah untuk contoh). Ini akan mendaftarkan operator dengan Tensorflow Lite sehingga TensorFlow Lite dapat menggunakan implementasi baru. Perhatikan bahwa dua argumen terakhir di TfLiteRegistration berkorespondensi dengan SinPrepare dan SinEval fungsi Anda ditetapkan untuk op kustom. Jika Anda menggunakan SinInit dan SinFree fungsi untuk menginisialisasi variabel yang digunakan dalam op dan untuk membebaskan ruang, masing-masing, maka mereka akan ditambahkan ke dua argumen pertama TfLiteRegistration ; argumen-argumen ditetapkan untuk nullptr dalam contoh ini.

Daftarkan operator dengan perpustakaan kernel

Sekarang kita perlu mendaftarkan operator dengan perpustakaan kernel. Hal ini dilakukan dengan OpResolver . Di belakang layar, interpreter akan memuat pustaka kernel yang akan ditugaskan untuk mengeksekusi setiap operator dalam model. Meskipun pustaka default hanya berisi kernel bawaan, dimungkinkan untuk mengganti/menambahnya dengan operator op pustaka khusus.

The OpResolver kelas, yang diterjemahkan kode operator dan nama ke dalam kode yang sebenarnya, didefinisikan seperti ini:

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;
};

Penggunaan reguler mengharuskan Anda menggunakan BuiltinOpResolver dan menulis:

tflite::ops::builtin::BuiltinOpResolver resolver;

Untuk menambahkan op kustom dibuat di atas, Anda menelepon AddOp (sebelum Anda lulus penyelesai ke InterpreterBuilder ):

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

Jika set ops builtin dianggap terlalu besar, baru OpResolver bisa kode yang dihasilkan didasarkan pada subset tertentu ops, mungkin hanya orang-orang yang terdapat dalam sebuah model yang diberikan. Ini adalah setara pendaftaran selektif TensorFlow (dan versi sederhana dari itu tersedia di tools direktori).

Jika Anda ingin mendefinisikan operator kustom Anda di Jawa, Anda akan saat ini perlu untuk membangun lapisan JNI kustom Anda sendiri dan mengkompilasi AAR Anda sendiri dalam kode JNI ini . Demikian pula, jika Anda ingin menentukan operator ini tersedia dalam Python Anda dapat menempatkan pendaftaran Anda di kode wrapper Python .

Perhatikan bahwa proses serupa seperti di atas dapat diikuti untuk mendukung serangkaian operasi alih-alih satu operator. Hanya menambahkan sebanyak AddCustom operator yang Anda butuhkan. Selain itu, BuiltinOpResolver juga memungkinkan Anda untuk menimpa implementasi dari builtin dengan menggunakan AddBuiltin .

Uji dan profilkan operator Anda

Untuk profil op Anda dengan alat benchmark TensorFlow Lite, Anda dapat menggunakan alat Model patokan untuk TensorFlow Lite. Untuk tujuan pengujian, Anda dapat membuat membangun lokal Anda dari TensorFlow Lite menyadari op kustom Anda dengan menambahkan sesuai AddCustom panggilan (sebagai menunjukkan di atas) untuk register.cc

Praktik terbaik

  1. Optimalkan alokasi memori dan de-alokasi dengan hati-hati. Mengalokasikan memori di Prepare lebih efisien daripada di Invoke , dan mengalokasikan memori sebelum loop adalah lebih baik daripada di setiap iterasi. Gunakan data tensor sementara daripada membuat mallocing sendiri (lihat item 2). Gunakan pointer/referensi daripada menyalin sebanyak mungkin.

  2. Jika struktur data akan tetap ada selama seluruh operasi, sebaiknya alokasikan memori terlebih dahulu menggunakan tensor sementara. Anda mungkin perlu menggunakan struct OpData untuk mereferensikan indeks tensor di fungsi lain. Lihat contoh di kernel untuk konvolusi . Cuplikan kode contoh ada di bawah

    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. Jika tidak biaya memori terlalu banyak terbuang, lebih suka menggunakan statis tetap ukuran array (atau pra-dialokasikan std::vector di Resize ) daripada menggunakan dialokasikan secara dinamis std::vector setiap iterasi eksekusi.

  4. Hindari membuat instance template container library standar yang belum ada, karena mempengaruhi ukuran biner. Misalnya, jika Anda memerlukan std::map dalam operasi Anda yang tidak ada di kernel lainnya, menggunakan std::vector dengan pemetaan pengindeksan langsung bisa bekerja sambil menjaga biner ukuran kecil. Lihat apa yang digunakan kernel lain untuk mendapatkan wawasan (atau tanyakan).

  5. Periksa pointer ke memori dikembalikan oleh malloc . Jika pointer ini nullptr , tidak ada operasi harus dilakukan dengan menggunakan pointer itu. Jika Anda malloc dalam fungsi dan memiliki keluar error, memori deallocate sebelum Anda keluar.

  6. Gunakan TF_LITE_ENSURE(context, condition) untuk memeriksa kondisi tertentu. Kode Anda tidak harus meninggalkan gantung memori ketika TF_LITE_ENSURE digunakan, yaitu, macro ini harus digunakan sebelum sumber daya yang dialokasikan itu akan bocor.