사용자 정의 연산자

TensorFlow Lite 내장 연산자 라이브러리는 제한된 수의 TensorFlow 연산자만 지원하므로 모든 모델을 변환할 수 있는 것은 아닙니다. 자세한 내용은 참조 연산자의 호환성 .

변환을 허용하기 위해 사용자는 사용자 지정 연산자라고 하는 TensorFlow Lite에서 지원되지 않는 TensorFlow 연산자의 사용자 지정 구현을 제공할 수 있습니다. 대신, 당신은 하나의 융합 최적화 된 사용자 정의 연산자로 지원되지 않는 (또는 지원) TensorFlow 사업자의 일련의 결합하고자하는 경우, 참조 연산자 융합 .

사용자 지정 연산자 사용은 4단계로 구성됩니다.

하자의 사용자 정의 연산자와 모델 실행의 엔드 - 투 - 엔드 예를 통해 도보 tf.sin (라는 이름으로 Sin TensorFlow에서 지원, #create_a_tensorflow_model 참조)하지만, TensorFlow 라이트에서 지원되지 않는.

예 : 사용자 정의 Sin 연산자

TensorFlow Lite에는 없는 TensorFlow 연산자를 지원하는 예를 살펴보겠습니다. 우리가 사용하는 가정 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 라이트 모델 만들기 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 연산자(맞춤형 및 내장형 모두)는 4가지 기능으로 구성된 간단한 순수 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 에 대한 자세한 내용은 TfLiteContextTfLiteNode . 전자는 오류 보고 기능과 모든 텐서를 포함한 전역 개체에 대한 액세스를 제공합니다. 후자를 통해 구현이 입력 및 출력에 액세스할 수 있습니다.

인터프리터로드 모델, 그것은 호출하면 init() 그래프의 각 노드에 대해 한 번. 소정의 init() 더 연산 그래프에서 여러 번 사용하는 경우 여러 번 호출 될 것이다. 사용자 정의 작업의 경우 매개변수 이름을 해당 값에 매핑하는 플렉스 버퍼를 포함하는 구성 버퍼가 제공됩니다. 인터프리터가 이미 연산 매개변수를 구문 분석했기 때문에 버퍼는 내장 연산에 대해 비어 있습니다. 상태가 필요한 커널 구현은 여기에서 초기화하고 소유권을 호출자에게 이전해야 합니다. 각각에 대해 init() 호출에 해당 호출이있을 것이다 free() 구현은 그들이에 할당 된 수있는 버퍼를 처리 할 수 있도록 init() .

입력 텐서의 크기가 조정될 때마다 인터프리터는 변경 구현을 알리는 그래프를 살펴봅니다. 이를 통해 내부 버퍼의 크기를 조정하고, 입력 모양과 유형의 유효성을 확인하고, 출력 모양을 다시 계산할 수 있습니다. 이것은 모든 통해 이루어진다 prepare() 및 구현하여 자신의 상태를 액세스 할 수있는 node->user_data .

마지막으로, 각 시간 추론 실행은 인터프리터는 그래프 호출 통과 invoke() 하고, 여기에 너무 상태로 사용할 수 있습니다 node->user_data .

사용자 지정 작업은 내장 작업과 정확히 동일한 방식으로 구현될 수 있습니다. 이러한 네 가지 기능과 일반적으로 다음과 같은 전역 등록 기능을 정의하면 됩니다.

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 대상이) 내장 매크로의 등록을 담당, 사용자 정의 작전은 별도의 사용자 지정 라이브러리에서 수집해야합니다.

TensorFlow Lite 런타임에서 커널 정의

우리가 (이 함수를 정의한다 TensorFlow Lite에서 연산을 사용하기 위해 필요한 모든 PrepareEval ) 및 구성 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 에 해당 SinPrepareSinEval 기능을 사용자 정의 연산에 대해 정의. 당신이 사용하는 경우 SinInitSinFree 연산과 공간을 확보하기 위해 사용되는 변수를 초기화하는 기능을 각각 그 때의 처음 두 인수에 추가 될 것입니다 TfLiteRegistration ; 그 인수로 설정되어 nullptr 이 예에.

커널 라이브러리에 연산자 등록

이제 커널 라이브러리에 연산자를 등록해야 합니다. 이은으로 이루어집니다 OpResolver . 배후에서 인터프리터는 모델의 각 연산자를 실행하기 위해 할당될 커널 라이브러리를 로드합니다. 기본 라이브러리에는 내장 커널만 포함되어 있지만 사용자 지정 라이브러리 연산 연산자로 교체/증가할 수 있습니다.

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 코드 생성 작전의 주어진 집합을 기반으로, 아마도 유일한 사람이 주어진 모델에 포함 될 수 있습니다. 이 TensorFlow의 선택적 등록하는 것과 동일합니다 (그리고 그것의 간단한 버전에서 사용할 tools 디렉토리).

당신이 자바 사용자 정의 연산자를 정의하려면, 당신은 현재 자신 만의 JNI 층을 구축하고 자신의 AAR을 컴파일해야 이 JNI 코드 . 파이썬에서 사용할 수있는 이러한 연산자를 정의 할 경우 마찬가지로, 당신은 당신의 등록을 배치 할 수 있습니다 파이썬 래퍼 코드 .

단일 연산자 대신 일련의 작업을 지원하기 위해 위와 유사한 프로세스를 따를 수 있습니다. 그냥 많은로 추가 AddCustom 당신이 필요로하는 사업자. 또한, BuiltinOpResolver 또한 당신이 사용하여 내장 매크로의 구현을 대체 할 수 있습니다 AddBuiltin .

운영자 테스트 및 프로파일링

TensorFlow 라이트 벤치 마크 도구를 사용하여 연산을 프로파일 링하려면 사용할 수있는 벤치 마크 모델 도구 TensorFlow Lite에 대한합니다. 테스트 목적을 위해 적절한 추가하여 사용자 정의 연산의 인식 TensorFlow 라이트의 지역 빌드를 만들 수 있습니다 AddCustom 에 (위의 표시로) 호출을 register.cc

모범 사례

  1. 메모리 할당 및 할당 해제를 신중하게 최적화하십시오. 메모리를 할당 Prepare 보다 더 효율적입니다 Invoke 및 루프 전에 메모리를 할당하는 것은 모든 반복에보다 낫다. 자신을 mallocing하는 대신 임시 텐서 데이터를 사용합니다(항목 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::vectorResize 하지 않고 동적으로 할당 된 사용하는 것보다) std::vector 실행의 모든 반복.

  4. 바이너리 크기에 영향을 미치므로 아직 존재하지 않는 표준 라이브러리 컨테이너 템플릿을 인스턴스화하지 마십시오. 예를 들어, 당신이 필요로하는 경우 std::map 사용하여 다른 커널에 존재하지 않는 작업의 std::vector 진 크기의 작은을 유지하면서 일할 수있는 직접 인덱싱 매핑합니다. 다른 커널이 통찰력을 얻기 위해(또는 질문하기 위해) 무엇을 사용하는지 확인하십시오.

  5. 에 의해 반환 된 메모리에 대한 포인터를 확인 malloc . 이 포인터 인 경우 nullptr , 어떤 작업이 포인터를 사용하여 수행 될 수 없습니다. 당신이 경우 malloc 함수와 오류 출구가, 할당 해제 메모리 당신이 종료하기 전에.

  6. 사용 TF_LITE_ENSURE(context, condition) 특정 조건에 대해 확인합니다. 때 코드는 메모리 매달려 떠나지해야한다 TF_LITE_ENSURE 즉, 사용되는 모든 리소스가 누출이 할당되기 전에이 매크로를 사용해야합니다.