TensorflowLite 是Tensorflow 针对移动和嵌入式设备的轻量级解决方案, 支持 Android、iOS 甚至树莓派等多种平台。

  • 轻量:使设备上机器学习模型推断具有小型二进制规模和快速初始化/启动。
  • 跨平台:可以在多个平台运行,包括安卓和iOS。
  • 快速:针对移动设备进行了快速优化,包括模型加载时间显著加快,并支持硬件加速等。
  • 灵活:可以用来创建和运行自定义模型。开发者也可以在模型中添加自定义操作。
  • 兼容:支持通过Tensorflow训练好的模型转换为Tensorflow Lite格式(pd,h5等都可以)

 TensorFlow Lite 转换器可以把模型转换为 TensorFlow Lite(.tflite)文件格式,然后就可以在移动应用程序中使用该转换后的文件。

 

一.编译和接入

以Android平台为例:

方案1.使用Android Studio, 从JCenter仓库直接下载导入arr

在build.gradle(app)中加入 dependencies即可

1

2

3

4

dependencies {

    implementation 'org.tensorflow:tensorflow-lite:0.0.0-nightly'

    implementation 'org.tensorflow:tensorflow-lite-gpu:0.0.0-nightly' //使用gpu

}

方案2.编译本地的arr

如果你需要自定义一部分tensorflow的算子,比如operations selected from TensorFlow,可能需要TensorFlow Lite的本地版本。

在Linux上能够编译,基本上和tensorflow官网的源码编译流程差不多,在configure时注意指定Android ndk和sdk路径,

注意虚拟内存溢出,需要使用swapon增加虚拟内存。

然后执行以下bazel命令:

1

2

3

bazel build --cxxopt='--std=c++11' -c opt             \

  --config=android_arm --config=monolithic          \

  //tensorflow/lite/java:tensorflow-lite-with-select-tf-ops

在Windows上编译失败,我根据建议,安装了MSYS2,但是bazel编译时无法找到ndk路径,在configure文件中也无法指定,直接在android_configure.bzl中修改也没用。

二.数据格式

TF-Lite 使用了 Google 提出的 FlatBuffers 序列化协议。FlatBuffers是一个高效的开源跨平台序列化库,它的结构化数据都以二进制形式保存,不需要数据解析过程,数据也可以方便传递。

FlatBuffers和Protocol buffers很类似,主要区别在FlatBuffers在访问数据之前不需要进行额外的解析。而且,FlatBuffers的代码占用空间比protocol buffers小一个量级(The code footprint of FlatBuffers is an order of magnitude smaller than protocol buffers.)

TF-Lite 模型定义文件可以在 TensorFlow 项目中找到:schema.fbs,通过 schema 文件和 FlatBuffers,可以编译得到解析 TF-Lite 模型文件的头文件,进而对模型进行进一步操作。

 

三.模型转换

 

     tensorflow输出

  • checkpoint(.ckpt):只包含若干 Variables 对象序列化后的数据,不包含图结构
  • GraphDef
    包含了计算图,可以从中得到所有运算符(operators)的细节,也包含张量(tensors)和 Variables 定义,但不包含 Variable 的值,
    因此只能从中恢复计算图,但一些训练的权值仍需要从 checkpoint 中恢复。将 GraphDef 中所有 Variable 节点转换为常量,
    就变为 FrozenGraphDef 格式。代码可以参考 tensorflow/python/tools/freeze_graph.py
  • SavedModel
    使用saved_model接口导出的模型文件,该格式为 GraphDef 和 CheckPoint 的结合体,
    另外还有标记模型输入和输出参数的 SignatureDef。从 SavedModel 中可以提取 GraphDef 和 CheckPoint 对象。
    TensorFlow和Keras模型推荐使用这种模型格式。
  • keras

    HDF5文件

1.使用python API

1.1 从tf.Session转化

1

2

3

4

5

6

7

8

9

import tensorflow as tf

img = tf.placeholder(name="img", dtype=tf.float32, shape=(164643))

var = tf.get_variable("weights", dtype=tf.float32, shape=(164643))

val = img + varout = tf.identity(val, name="out")

with tf.Session() as sess: 

    sess.run(tf.global_variables_initializer()) 

    converter = tf.lite.TFLiteConverter.from_session(sess, [img], [out]) 

    tflite_model =converter.convert() 

    open("converted_model.tflite""wb").write(tflite_model)

1.2 从frozenGraphDef转化

转化的源文件必须为frozen的GraphDef,可以使用freeze_graph.py转化。

1

2

3

4

5

6

7

8

import tensorflow as tf

graph_def_file = "/path/to/Downloads/mobilenet_v1_1.0_224/frozen_graph.pb"

input_arrays = ["input"]

output_arrays = ["MobilenetV1/Predictions/Softmax"]

converter = tf.lite.TFLiteConverter.from_frozen_graph(

  graph_def_file, input_arrays, output_arrays)

tflite_model = converter.convert()

open("converted_model.tflite""wb").write(tflite_model)

1.3 从SavedModel转化

1

2

3

4

import tensorflow as tf

converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir)

tflite_model = converter.convert()

open("converted_model.tflite""wb").write(tflite_model)

1.4 从tf.keras的H5py模型转化

tf.keras文件必须包含模型和权重。

1

2

3

4

import tensorflow as tf

converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir)

tflite_model = converter.convert()

open("converted_model.tflite""wb").write(tflite_model)

2.使用命令行

命令行工具tflite_convert是python库的一部分,可以直接在terminal进行模型转化。

如果想要编译最新的tflite_convert,也可以下载tensorflow源代码,执行bazel命令编译

1

bazel run //tensorflow/lite/python:tflite_convert -- --output_file=...

一个利用tflite_convert将frozenGraph转化为tflite的例子:

1

2

3

4

5

tflite_convert \

  --output_file=/tmp/foo.tflite \

  --graph_def_file=/tmp/mobilenet_v1_0.50_128/frozen_graph.pb \

  --input_arrays=input \

  --output_arrays=MobilenetV1/Predictions/Reshape_1

一些常用的命令行选项:

--output_file 指定输出文件的路径(包含文件名)

--graph_def_file 指定需要转化的frozenGraph的路径(包含文件名)

--saved_model_dir 指定需要转化的saved_model的路径

--keras_model_file 指定需要转化的keras_model的路径(包含文件名)

--output_format  TFLITE或者GRAPHVIZ_DOT,后者是一种特殊的可视化图

--input_arrays 指定输入节点

--output_arrays 指定输出节点

--input_shapes 指定输入节点形状

--allow_custom_ops 是否运行自定义算子。为true时,开发人员需要将自定义算子的解析程序提供给TensorFlow Lite。

--inference_type 指定输出模型中除input_arrays外的数据格式, FLOAT或者QUANTIZED_UINT8 。如果是FLOAT,那么实数数组在输出文件中将是float类型。如果它们在输入文件中被量化,则它们被反量化。

如果是QUANTIZED_UINT8,那么实数数组将在输出文件中量化为uint8。如果它们的输入文件为浮点型,那么它们会被量化。

--inference_input_type 指定输出模型中input_arrays的数据格式, FLOAT或者QUANTIZED_UINT8 。通常应用于指定模型输入为UNIT类型的位图,但是中间推断需要使用FLOAT类型的情况。

在使用QUANTIZED_UINT8的时候也需要配置 --std_dev_values --mean_values等参数。

当对量化输入(--inference_input_type=QUANTIZED_UINT8)执行浮点推断(--inference_type = FLOAT)时,量化输入会根据上述公式进行反量化,再进行推理

当对量化输入( --inference_input_type=QUANTIZED_UINT8) 执行量化推断(--inference_type =  QUANTIZED_UINT8 )时,推断代码不执行反量化。

然而,所有数组的量化参数,包括由mean_value和std_dev_value指定的输入数组的量化参数,决定了推断代码中使用的定点乘数。执行量化推断时,mean_value必须是整数。

3.关于量化(Quantization)和反量化(DeQuantization)

量化深度神经网络使用的技术可以降低权重的表达精度,可视充分利用许多CPU和硬件加速器实现提供SIMD指令功能,减少存储的访问,计算的执行,模型的体积。

定义 r 为浮点类型的实际值,定义 q 为整型的量化值,量化的模式可以简述为:

q=rS+Zq=rS+Z

反之,量化值也可以恢复为实际浮点值:

r=(q−Z)⋅Sr=(q-Z)⋅S

这里的 S 和 Z 均为量化参数,前者如字面意思所示,Z 表示浮点数的 0 量化后对应的整型值。由于 0 在神经网络中有着特殊的含义,故必须有精确的整型值对应 0。对于量化后的值 q,通过量化参数 (S, Z)可恢复到所代表的浮点数值。

tensorflow lite支持多种级别的量化:

  • Post-training quantization :训练后量化又可以分为:
    • Weight only quantization(只对权重量化)

      只将权重的精度从浮点型减低为8bit整型,但在计算过程中,会将weight进行dequantized回Float.。由于只有权重进行量化,所以无需验证数据集就可以实现。

      如果只是想为了方便传输和存储而减小模型大小,而不考虑在推断时浮点型计算的性能开销的话,这种量化方法是很有用的。

    • Quantizing weights and activations(量化权重和激活输出)

      我们可以通过计算所有将要被量化的数据的量化参数,来将一个浮点型模型量化为一个8bit精度的整型模型。由于激活输出需要量化,这时我们就得需要Calibration Data了,并且需要计算激活输出的动态范围。

      这种模式,在weight quantization的基础上,对某些支持quantized的Kernel,先进行quantization,再进行activation计算,再de-quant回float32,不支持的话会直接使用Float32进行计算,这相对与直接使用Float32进行计算会快一些.

 

  • Quantization-aware training :在训练时就考虑量化。在训练时精确模拟推断时的算子融合,并对推断的量化效果进行建模,以最小精度下降的方向量化的网络;这仅适用于卷积神经网络架构的子集。

    这种模式,除了会对weight进行quantization,也会在训练过程中,进行模拟量化,求出各个op的max跟min输出,实现不仅仅在训练过程,在测试过程,全程计算过程皆为uint8.不仅仅实现模型的压缩,计算速度也得到提高.

     

Post-training:

Weight only quantization:

converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir)

converter.optimizations = [tf.lite.Optimize.OPTIMIZE_FOR_SIZE]

tflite_quant_model = converter.convert()

Quantizing weights and activations:

需要使用校准数据集来测量激活和输入的动态范围,创建一个输入数据生成器并将其提供给TFLITE的converter。

def representative_dataset_gen():

  for in range(num_calibration_steps):

    # Get sample input data as a numpy array in a method of your choosing.

    yield [input]

 

converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir)

converter.optimizations = [tf.lite.Optimize.DEFAULT]

converter.representative_dataset = representative_dataset_gen //数据集

tflite_quant_model = converter.convert()


During-training:

针对仅整数执行的Quantization-aware training量化模型得到了一个延迟更快、大小更小且仅整数加速器兼容的模型。

  1. 用常规方法训练一个 TensorFlow 浮点模型。
  2. 用 tf.contrib.quantize 重写网络以插入Fake-Quant 节点并训练 min/max
  3. 用 TensorFlow Lite 工具量化网络(该工具读取步骤 2 训练的 min/max)
  4. 用 TensorFlow Lite 部署量化的网络。

converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir)

converter.inference_type = tf.lite.constants.QUANTIZED_UINT8

input_arrays = converter.get_input_arrays()

converter.quantized_input_stats = {input_arrays[0] : (0.1.)}  # std, S

tflite_model = converter.convert()

对于全整数模型,输入为uint8。mean和std_dev指定这些uint8值如何映射到培训模型时使用的浮点输入值。

mean是从0到255的整数值,映射到浮点0.0f。std_dev是255/(float_max-float_min)

 

更多的命令行选项和意义参见Converter command line reference

更多的实例可以参见Converter command line examples

四.模型推理

在安卓平台下,TensorFlow Lite一般使用Java或C ++ API来运行lite模型。

Java API提可以直接在Android Activity类中使用,使用方便。

C ++ API更加灵活和快速,但可能需要编写JNI在Java和C ++层之间传递。

JAVA接口

public Interpreter(@NotNull File modelFile);

Interpreter.run()

try (Interpreter interpreter = new Interpreter(file_of_a_tensorflowlite_model)) {

  interpreter.run(input, output);

}

 

支持的数据类型 ,输入和输出tensor的数据类型必须是以下基本类型之一:float、int、long、byte或者是适当大小的原始ByteBuffer。

如果使用其他数据类型(包括类似Integer和Float的封装类型),则会抛出IllegalArgumentException,每个输入输出应该是受支持的基本类型的数组或多维数组。

C++接口

1

2

3

4

5

6

7

8

9

10

tflite::FlatBufferModel model = tflite::FlatBufferModel.BuildfromFile(path_to_model);

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

std::unique_ptr<tflite::Interpreter> interpreter;

tflite::InterpreterBuilder(*model, resolver)(&interpreter);

// Resize input tensors, if desired.

interpreter->AllocateTensors();

float* input = interpreter->typed_input_tensor<float>(0);

// Fill ∈put.

interpreter->Invoke();

float* output = interpreter->typed_output_tensor<float>(0);

五.自定义算子

一开始参考TFlite的官方教程,但是在实验时发现sin算子已经被TFLITE的官方库实现了(AddBuiltin/AddCustom)。于是尝试以下方式:

1.在tensorflow中加入自定义算子(CPU)

参考添加新操作

将自定义算子的cpp文件放入路径 TFsource_PATH/tensorflow/core/user_ops 

需要编译自定义算子库:

方法(1). 使用 bazel 编译操作(TensorFlow 源代码安装)

方法(2). 使用系统编译器编译操作(TensorFlow 二进制文件安装)

方法(1)是从源代码开始编译,需要花费较长的实际. 使用方法(2)编译时,需要注意

anaconda内下载的tensorflow二进制库是支持C++11 ABI的,如果使用的系统gcc版本在5.0以下,编译出来的自定义算子库会无法被tensorflow load。

可以使用anaconda下载gxx_linux-64,然后用它来编译自定义算子。

利用自定义的算子库构建最简单的网络结构,利用convert工具转化为tflite模型,用Netron可以看到:

 

2.在tensorflow lite中解析自定义算子

在Android上搭建工程,载入经过转化的tflite模型,如果使用的是官方的arr会出现:

因为在tensorflow中使用的ZeroOut是我们自定义的,所以需要重新编译arr。

参照教程,可以在tensorflow/lite/java/src/main/native中添加c++解析方式,在builtin_ops_jni.cc中注册自定义算子,注意需要修改同路径的BUILD文件。

 

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;

当解释器加载模型时,它会为图中的每个节点调用一次init()。如果在图中多次使用op,则会多次调用给定的init()。每次调用init()调用,都会调用对应free()的相应调用。

对于自定义操作,将提供配置缓冲区,其中包含将参数名称映射到其值的flexbuffer。内置操作的缓冲区为空,因为解释器已经解析了op参数。需要状态的内核实现应该在此处初始化并将所有权转移给调用者。注册不是自动的,应该在某处使用BuiltinOpResolver显式调用Register_MY_CUSTOM_OP。

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

resolver.AddCustom("MY_CUSTOM_OP", Register_MY_CUSTOM_OP());


载入本地c成功编译的arr成功后:

tensorflow lite自定义算子的注意点:

  • 注意内存分配和回收,尽量使用指针和引用减少内存使用,尽量使用临时tensor而不是mallocing,避免在循环中分配内存,在Prepare()中分配内存比在Invoke()中分配内存更有效
  • 尽量使用固定大小的数组而不是是std::vector

  • 检查指向malloc返回的内存的指针。如果此指针为nullptr,则不应使用该指针执行任何操作。如果函数中有malloc()并且出现错误,请在退出函数之前释放内存。

  • 避免实例化尚不存在的标准库容器模板,因为它们会影响二进制文件大小。例如,避免使用std :: map,而是使用带有直接索引映射的std :: vector。

参考资料

TensorFlow Lite guide

Compiling a TensorFlow Lite Build with Custom Operations

TFLite基础知识

TensorflowLite量化原理

用 TensorFlow 压缩神经网络

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐