快速熟悉Tensorflow Lite
一.编译和接入方案1.使用Android Studio, 从JCenter仓库直接下载导入arr方案2.编译本地的arr二.数据格式三.模型转换1.使用python API1.1 从tf.Session转化1.2 从frozenGraphDef转化1.3 从SavedModel转化1.4 从tf.keras的H5py模型转化2.使...
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 |
|
方案2.编译本地的arr
如果你需要自定义一部分tensorflow的算子,比如operations selected from TensorFlow,可能需要TensorFlow Lite的本地版本。
在Linux上能够编译,基本上和tensorflow官网的源码编译流程差不多,在configure时注意指定Android ndk和sdk路径,
注意虚拟内存溢出,需要使用swapon增加虚拟内存。
然后执行以下bazel命令:
1 2 3 |
|
在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 |
|
1.2 从frozenGraphDef转化
转化的源文件必须为frozen的GraphDef,可以使用freeze_graph.py转化。
1 2 3 4 5 6 7 8 |
|
1.3 从SavedModel转化
1 2 3 4 |
|
1.4 从tf.keras的H5py模型转化
tf.keras文件必须包含模型和权重。
1 2 3 4 |
|
2.使用命令行
命令行工具tflite_convert是python库的一部分,可以直接在terminal进行模型转化。
如果想要编译最新的tflite_convert,也可以下载tensorflow源代码,执行bazel命令编译
1 |
|
一个利用tflite_convert将frozenGraph转化为tflite的例子:
1 2 3 4 5 |
|
一些常用的命令行选项:
--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:
|
Quantizing weights and activations:
需要使用校准数据集来测量激活和输入的动态范围,创建一个输入数据生成器并将其提供给TFLITE的converter。
|
During-training:
针对仅整数执行的Quantization-aware training量化模型得到了一个延迟更快、大小更小且仅整数加速器兼容的模型。
- 用常规方法训练一个 TensorFlow 浮点模型。
- 用
tf.contrib.quantize
重写网络以插入Fake-Quant 节点并训练 min/max。 - 用 TensorFlow Lite 工具量化网络(该工具读取步骤 2 训练的 min/max)。
- 用 TensorFlow Lite 部署量化的网络。
|
对于全整数模型,输入为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接口
|
支持的数据类型 ,输入和输出tensor的数据类型必须是以下基本类型之一:float、int、long、byte或者是适当大小的原始ByteBuffer。
如果使用其他数据类型(包括类似Integer和Float的封装类型),则会抛出IllegalArgumentException,每个输入输出应该是受支持的基本类型的数组或多维数组。
C++接口
1 2 3 4 5 6 7 8 9 10 |
|
五.自定义算子
一开始参考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文件。
|
当解释器加载模型时,它会为图中的每个节点调用一次init()。如果在图中多次使用op,则会多次调用给定的init()。每次调用init()调用,都会调用对应free()的相应调用。
对于自定义操作,将提供配置缓冲区,其中包含将参数名称映射到其值的flexbuffer。内置操作的缓冲区为空,因为解释器已经解析了op参数。需要状态的内核实现应该在此处初始化并将所有权转移给调用者。注册不是自动的,应该在某处使用BuiltinOpResolver显式调用Register_MY_CUSTOM_OP。
|
载入本地c成功编译的arr成功后:
tensorflow lite自定义算子的注意点:
- 注意内存分配和回收,尽量使用指针和引用减少内存使用,尽量使用临时tensor而不是mallocing,避免在循环中分配内存,在Prepare()中分配内存比在Invoke()中分配内存更有效
-
尽量使用固定大小的数组而不是是std::vector
-
检查指向malloc返回的内存的指针。如果此指针为nullptr,则不应使用该指针执行任何操作。如果函数中有malloc()并且出现错误,请在退出函数之前释放内存。
-
避免实例化尚不存在的标准库容器模板,因为它们会影响二进制文件大小。例如,避免使用std :: map,而是使用带有直接索引映射的std :: vector。
参考资料
更多推荐
所有评论(0)