Ajuda a proteger a Grande Barreira de Corais com TensorFlow em Kaggle Junte Desafio

Operadores personalizados

Como a biblioteca de operadores integrada do TensorFlow Lite só oferece suporte a um número limitado de operadores do TensorFlow, nem todo modelo é conversível. Para mais detalhes, consulte a compatibilidade operador .

Para permitir a conversão, os usuários podem fornecer sua própria implementação personalizada de um operador TensorFlow sem suporte no TensorFlow Lite, conhecido como operador personalizado. Se em vez disso, pretende combinar uma série de operadores TensorFlow não suportados (ou compatíveis) em um único operador personalizado otimizado fundido, referem-se a fusão operador .

O uso de operadores personalizados consiste em quatro etapas.

Vamos a pé através de um exemplo end-to-end de funcionamento de um modelo com um operador personalizado tf.sin (nomeado como Sin , consulte #create_a_tensorflow_model) que é suportado no TensorFlow, mas sem suporte em TensorFlow Lite.

Exemplo: personalizado Sin operador

Vejamos um exemplo de suporte a um operador do TensorFlow que o TensorFlow Lite não tem. Suponha que estamos usando o Sin do operador e que estamos construindo um modelo muito simples para uma função y = sin(x + offset) , onde offset é treinável.

Crie um modelo do TensorFlow

O snippet de código a seguir treina um modelo simples do TensorFlow. Este modelo contém apenas um operador personalizada chamada Sin , que é uma função y = sin(x + offset) , onde offset é treinável.

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

Neste ponto, se você tentar gerar um modelo do TensorFlow Lite com as sinalizações do conversor padrão, receberá a seguinte mensagem de erro:

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.

Converter para um modelo TensorFlow Lite

Criar um modelo TensorFlow Lite com operadores personalizados, definindo o atributo conversor allow_custom_ops como mostrado abaixo:

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

Neste ponto, se você executá-lo com o interpretador padrão, receberá as seguintes mensagens de erro:

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

Crie e registre o operador.

Todos os operadores TensorFlow Lite (personalizados e integrados) são definidos usando uma interface C puro simples que consiste em quatro funções:

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;

Consulte common.h para obter detalhes sobre TfLiteContext e TfLiteNode . O primeiro fornece recursos de relatório de erros e acesso a objetos globais, incluindo todos os tensores. O último permite que as implementações acessem suas entradas e saídas.

Quando as cargas intérprete um modelo, ele chama init() uma vez para cada nó no gráfico. Um dado init() será chamado mais do que uma vez se o op é utilizado várias vezes no gráfico. Para operações customizadas, um buffer de configuração será fornecido, contendo um flexbuffer que mapeia nomes de parâmetros para seus valores. O buffer está vazio para operações integradas porque o interpretador já analisou os parâmetros de operação. As implementações de kernel que requerem estado devem inicializá-lo aqui e transferir a propriedade para o responsável pela chamada. Para cada init() chamada, haverá uma chamada correspondente à free() , permitindo que as implementações de dispor do tampão que pode ter atribuídas em init() .

Sempre que os tensores de entrada são redimensionados, o interpretador irá percorrer o gráfico notificando as implementações sobre a mudança. Isso lhes dá a chance de redimensionar seu buffer interno, verificar a validade das formas e tipos de entrada e recalcular as formas de saída. Isso tudo é feito através de prepare() , e implementações podem acessar seu estado utilizando node->user_data .

Finalmente, cada vez inferência é executado, o intérprete atravessa o gráfico chamada invoke() , e aqui também o estado está disponível como node->user_data .

As operações personalizadas podem ser implementadas exatamente da mesma maneira que as operações integradas, definindo essas quatro funções e uma função de registro global que geralmente se parece com isto:

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

Note-se que o registo não é automático e uma chamada explícita para Register_MY_CUSTOM_OP deve ser feita. Enquanto o padrão BuiltinOpResolver (disponível no :builtin_ops alvo) cuida do registro de builtins, ops personalizados terão de ser recolhidos em bibliotecas personalizadas separadas.

Definição do kernel no tempo de execução do TensorFlow Lite

Tudo o que precisa fazer para usar a op em TensorFlow Lite é definir duas funções ( Prepare e Eval ), e construir um 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;
}

Ao inicializar o OpResolver , adicione a op personalizado para o resolver (veja abaixo para um exemplo). Isso registrará o operador com o Tensorflow Lite para que o TensorFlow Lite possa usar a nova implementação. Note-se que os dois últimos argumentos em TfLiteRegistration correspondem aos SinPrepare e SinEval funções definido para o op personalizado. Se você usou SinInit e SinFree funções para inicializar variáveis utilizadas na op e para liberar espaço, respectivamente, em seguida, eles seriam acrescentados aos dois primeiros argumentos de TfLiteRegistration ; esses argumentos estão definidos para nullptr neste exemplo.

Registre o operador com a biblioteca do kernel

Agora precisamos registrar o operador na biblioteca do kernel. Isso é feito com um OpResolver . Nos bastidores, o interpretador carregará uma biblioteca de kernels que será designada para executar cada um dos operadores no modelo. Embora a biblioteca padrão contenha apenas kernels integrados, é possível substituí-la / aumentá-la com operadores op de biblioteca personalizados.

O OpResolver classe, o que se traduz códigos de operador e nomes em código real, é definido assim:

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

O uso regular requer que você use o BuiltinOpResolver e escreve:

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

Para adicionar o op personalizado criado acima, você chamar AddOp (antes de passar o resolvedor ao InterpreterBuilder ):

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

Se o conjunto de ops embutidas é considerada muito grande, um novo OpResolver poderia ser baseado em um determinado subconjunto de ops gerado pelo código, possivelmente apenas aquelas contidas em um determinado modelo. Isso é o equivalente de registro seletivo de TensorFlow (e uma versão simples do que está disponível no tools diretório).

Se você quiser definir seus operadores personalizados em Java, você atualmente precisa para construir sua própria camada JNI personalizada e compilar seu próprio AAR neste código JNI . Da mesma forma, se você deseja definir esses operadores disponíveis em Python você pode colocar suas inscrições no código invólucro Python .

Observe que um processo semelhante ao acima pode ser seguido para dar suporte a um conjunto de operações em vez de um único operador. Basta adicionar como muitos AddCustom operadores como você precisa. Além disso, BuiltinOpResolver também permite substituir implementações de builtins usando a AddBuiltin .

Teste e crie o perfil de sua operadora

Para o perfil de seu op com a ferramenta de benchmark TensorFlow Lite, você pode usar a ferramenta de modelo de referência para TensorFlow Lite. Para fins de teste, você pode fazer a sua construção local do TensorFlow Lite ciente de sua op personalizado, adicionando o apropriado AddCustom chamada (como a mostra acima) para register.cc

Melhores Práticas

  1. Otimize as alocações e desalocações de memória com cuidado. Alocação de memória no Prepare é mais eficiente do que em Invoke , e alocação de memória antes de um loop é melhor do que em cada iteração. Use dados de tensores temporários em vez de se maltratar (consulte o item 2). Use ponteiros / referências em vez de copiar o máximo possível.

  2. Se uma estrutura de dados persistir durante toda a operação, recomendamos pré-alocar a memória usando tensores temporários. Você pode precisar usar a estrutura OpData para fazer referência aos índices de tensor em outras funções. Veja o exemplo no kernel para convolução . Um exemplo de snippet de código está abaixo

    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. Se não custa memória muito desperdiçado, preferem usar uma matriz de tamanho fixo estática (ou um pré-alocada std::vector em Resize ) ao invés de usar um alocada dinamicamente std::vector cada iteração de execução.

  4. Evite instanciar modelos de contêiner de biblioteca padrão que ainda não existem, porque eles afetam o tamanho do binário. Por exemplo, se você precisa de um std::map em sua operação que não existem em outros kernels, usando um std::vector com o mapeamento de indexação direta poderia trabalhar, mantendo o pequeno tamanho do binário. Veja o que outros kernels usam para obter uma visão (ou pergunte).

  5. Verifique o ponteiro para a memória retornado por malloc . Se esse ponteiro é nullptr , nenhuma operação deve ser realizada usando esse ponteiro. Se você malloc em uma função e tem uma saída de erro, desalocar memória antes de sair.

  6. Use TF_LITE_ENSURE(context, condition) para verificar se há uma condição específica. Seu código não deve deixar pendurado memória quando TF_LITE_ENSURE é utilizado, ou seja, estas macros deve ser usado antes de quaisquer recursos são alocados que vai vazar.