Поскольку встроенная библиотека операторов TensorFlow Lite поддерживает только ограниченное количество операторов TensorFlow, не каждая модель может быть преобразована. Подробнее см. в разделе Совместимость с операторами .
Чтобы разрешить преобразование, пользователи могут предоставить собственную реализацию неподдерживаемого оператора TensorFlow в TensorFlow Lite, известную как пользовательский оператор. Если вместо этого вы хотите объединить ряд неподдерживаемых (или поддерживаемых) операторов TensorFlow в один объединенный оптимизированный пользовательский оператор, обратитесь к разделу объединения операторов .
Использование пользовательских операторов состоит из четырех шагов.
Создайте модель TensorFlow. Убедитесь, что сохраненная модель (или Graph Def) ссылается на правильно названный оператор TensorFlow Lite.
Преобразование в модель TensorFlow Lite. Убедитесь, что вы установили правильный атрибут преобразователя TensorFlow Lite, чтобы успешно преобразовать модель.
Создайте и зарегистрируйте оператора. Это делается для того, чтобы среда выполнения TensorFlow Lite знала, как сопоставить ваш оператор и параметры на вашем графике с исполняемым кодом C/C++.
Протестируйте и профилируйте своего оператора. Если вы хотите протестировать только свой пользовательский оператор, лучше всего создать модель только с вашим настраиваемым оператором и использовать программу Benchmark_Model .
Давайте рассмотрим сквозной пример запуска модели с пользовательским оператором tf.atan
(названным как Atan
, см. #create_a_tensorflow_model), который поддерживается в TensorFlow, но не поддерживается в TensorFlow Lite.
Оператор TensorFlow Text является примером пользовательского оператора. Пример кода см. в учебнике «Преобразование TF Text в 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 с пользовательскими операторами, установив атрибут конвертера 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 (как пользовательские, так и встроенные) определяются с помощью простого интерфейса на чистом 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()
будет вызываться более одного раза, если операция используется в графе несколько раз. Для пользовательских операций будет предоставлен буфер конфигурации, содержащий flexbuffer, который сопоставляет имена параметров с их значениями. Буфер пуст для встроенных операций, потому что интерпретатор уже проанализировал параметры операции. Реализации ядра, которым требуется состояние, должны инициализировать его здесь и передать право собственности вызывающему объекту. Для каждого вызова 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, — это определить две функции ( 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
добавьте пользовательскую операцию в преобразователь (пример см. ниже). Это зарегистрирует оператор в Tensorflow Lite, чтобы TensorFlow Lite мог использовать новую реализацию. Обратите внимание, что последние два аргумента в TfLiteRegistration
соответствуют функциям AtanPrepare
и AtanEval
, которые вы определили для пользовательской операции. Если бы вы использовали функции AtanInit
и AtanFree
для инициализации переменных, используемых в операции, и для освобождения места соответственно, то они были бы добавлены к первым двум аргументам 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("Atan", Register_ATAN());
Если набор встроенных операций считается слишком большим, новый OpResolver
может быть сгенерирован кодом на основе заданного подмножества операций, возможно, только тех, которые содержатся в данной модели. Это эквивалент выборочной регистрации TensorFlow (простая версия доступна в каталоге tools
).
Если вы хотите определить свои пользовательские операторы в Java, вам в настоящее время необходимо создать свой собственный пользовательский слой JNI и скомпилировать свой собственный AAR в этом коде jni . Точно так же, если вы хотите определить эти операторы, доступные в Python, вы можете поместить свои регистрации в код-оболочку Python .
Обратите внимание, что процесс, аналогичный описанному выше, можно использовать для поддержки набора операций вместо одного оператора. Просто добавьте столько операторов AddCustom
, сколько вам нужно. Кроме того, BuiltinOpResolver
также позволяет переопределять реализации встроенных функций с помощью метода AddBuiltin
.
Протестируйте и профилируйте своего оператора
Чтобы профилировать свою операцию с помощью инструмента тестирования TensorFlow Lite, вы можете использовать инструмент эталонной модели для TensorFlow Lite. В целях тестирования вы можете сделать так, чтобы ваша локальная сборка TensorFlow Lite знала о вашей пользовательской операции, добавив соответствующий вызов AddCustom
(как показано выше) в register.cc .
Лучшие практики
Осторожно оптимизируйте выделение и освобождение памяти. Выделение памяти в
Prepare
более эффективно, чем вInvoke
, а выделение памяти перед циклом лучше, чем в каждой итерации. Используйте временные данные тензоров, а не настраивайте их самостоятельно (см. пункт 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;
Если это не требует слишком много потерянной памяти, предпочтите использовать статический массив фиксированного размера (или предварительно выделенный
std::vector
вResize
), а не использовать динамически выделяемыйstd::vector
на каждой итерации выполнения.Избегайте создания экземпляров стандартных шаблонов контейнеров библиотек, которые еще не существуют, поскольку они влияют на размер двоичного файла. Например, если вам нужна
std::map
в вашей операции, которой нет в других ядрах, использованиеstd::vector
с прямым отображением индексации может работать, сохраняя при этом небольшой размер двоичного файла. Посмотрите, что используют другие ядра, чтобы получить представление (или спросить).Проверьте указатель на память, возвращаемый
malloc
. Если этот указатель равенnullptr
, никакие операции не должны выполняться с использованием этого указателя. Если вы используетеmalloc
в функции и имеете выход с ошибкой, освободите память перед выходом.Используйте
TF_LITE_ENSURE(context, condition)
для проверки определенного условия. Ваш код не должен оставлять зависание памяти при использованииTF_LITE_ENSURE
, т. е. эти макросы следует использовать до того, как будут выделены какие-либо ресурсы, которые могут привести к утечке.