爱笑的小姐姐 · 2021年06月23日

Tensorflow Lite 使用与优化

本文转自:知乎
作者:djh

1、简介

Tensorflow Lite 的基本框架如上。数据存储的结构是Flatbuffer。执行上次结构支持Keras 和Estimator和Legacy等等。而下层支持NN API、GPU等等。

2、模型转换

要先将ckpt转成pb,在让pb转tflite格式。

ckpt转pb

import tensorflow as tf
from tensorflow.python.framework import graph_util

output_node_names = "heats_map_regression/pred_keypoints/BiasAdd"
input_checkpoint = "modelfilter.ckpt"
saver = tf.train.import_meta_graph(input_checkpoint + '.meta', clear_devices=True)
graph = tf.get_default_graph() 
input_graph_def = graph.as_graph_def() 

output_graph="frozen_modeld.pb"
with tf.Session() as sess:
    saver.restore(sess, input_checkpoint) #恢复图并得到数据
    output_graph_def = graph_util.convert_variables_to_constants(  # 模型持久化,将变量值固定
        sess=sess,
        input_graph_def=input_graph_def,# 等于:sess.graph_def
        output_node_names=output_node_names.split(","))# 如果有多个输出节点,以逗号隔开

    with tf.gfile.GFile(output_graph, "wb") as f: #保存模型
        f.write(output_graph_def.SerializeToString()) #序列化输出

pb转tflite

                 graph_def_file = "outquantizemodel/frozen_graph.pb" input_arrays = ["MobilenetV2/input"] output_arrays = ["heats_map_regression/pred_keypoints/BiasAdd"] converter = tf.contrib.lite.TFLiteConverter.from_frozen_graph(graph_def_file, \     input_arrays, output_arrays,input_shapes={"MobilenetV2/input":[1,600,800,3]}) converter.inference_type = tf.contrib.lite.constants.QUANTIZED_UINT8 converter.quantized_input_stats = {input_arrays[0]: (127.5, 127.5)}    # mean, std_dev,需要自己从训练集(增强后,输入网络之前的)统计出来 tflite_model = converter.convert() open("outquantizemodel/eval_graph_filequantize.tflite", "wb").write(tflite_model)                

3、模型执行C++ Android

当然你可以使用Python执行,python执行的好处是可以对比pb模型运行的结果。

也可以使用Android java执行。

而使用C++执行代码,可以更加好的掌控,模型的运行过程和效率。方面我们后续优化。

https://github.com/tensorflow/tensorflow.git​github.com

在github 中下载源码。接下来我们使用bazel 编译出C++版本的Tensorflow Lite 。

首先1:建立tensorflow/test目录。在目录下书写自己要运行的代码,和build配置文件。

目录结构大概是这样子的。

C文件大概是这样子的

test.cc
bool modelInit(const char* model_file, int numthreads) {

  model = tflite::FlatBufferModel::BuildFromFile(model_file);
  tflite::InterpreterBuilder(*model, resolver)(&interpreter);
  if (!interpreter) {
      LOGI("Failed to construct interpreter");
  } else {
    LOGI("construct interpreter is ok !!");
  }
  if (interpreter->AllocateTensors() != kTfLiteOk) {
    LOGI("Failed to allocate tensors!");
  }
  // input_tensor = interpreter->tensor(interpreter->inputs()[0]);
  interpreter->SetNumThreads(numthreads);
  // load model ..., init interpreter

  djh = &a;
  return true;
}

bazel的配置文件大概是如下:

package(default_visibility = ["//visibility:public"])
load("//tensorflow/lite:build_def.bzl", "tflite_linkopts", "tflite_copts")
cc_binary(
  name = "libtest.so",
  srcs = [
    "test.cc",
    "test.h"
  ],
  linkopts=[
    "-shared",
    "-Wl,-soname,libtest.so",# for gun compile (ex: ubuntu, android)
    # "-Wl,-install_name,libtest.dylib", # for clang (ex: mac)
  ],
  linkshared = 1,
  copts = tflite_copts(),
  visibility = ["//visibility:public"],
  deps = [
    "//tensorflow/lite:framework",
    "//tensorflow/lite/kernels:builtin_ops",
  ]
)

在tensorflow 文件夹下使用bazel命令进行编译。

bazel build --crosstool_top=//external:android/crosstool --cpu=arm64-v8a --host_crosstool_top=@bazel_tools//tools/cpp:toolchain --cxxopt="-std=c++11" //test:libtest.so

编译成功后可以在Android很方便的使用so。

4、模型大小优化

我们看到这个so编译出来是很大的,有20M左右。所以我们要对这个so进行裁剪。如何裁剪,裁剪掉不需要的算子保留自己的算子就行了。

最简单的方式就是在register中不用注册这么多我们用不到的算子。只保留我们要用到的几个,至于你的模型用到了哪些,你可以在pb中解析出来。

找到register.cc文件在register.cc中我们保留我们自己的,然后重新编译。

BuiltinOpResolver::BuiltinOpResolver() {
  AddBuiltin(BuiltinOperator_ADD, Register_ADD(),
             /* min_version */ 1,
             /* max_version */ 2);
  AddBuiltin(BuiltinOperator_CONV_2D, Register_CONV_2D(),
             /* min_version */ 1,
             /* max_version */ 3);
  AddBuiltin(BuiltinOperator_DEPTHWISE_CONV_2D, Register_DEPTHWISE_CONV_2D(),
             /* min_version */ 1,
             /* max_version */ 3);
  AddBuiltin(BuiltinOperator_SUB, Register_SUB(),
             /* min_version */ 1,
             /* max_version */ 2);

  }

5、模型汇编优化

汇编优化是为了提升速度。当然,在Tensorflow Lite中以及有很多NEON的汇编优化了。但是还是有些算子没有优化到,我觉得有可能google不屑于优化吧。但是对于如果我们的系统芯片不是很好的情况下,我觉得还是要优化的。比如SUB算子。只是一个普通的减法,但是对于图片来说,每个像素都要做减法,加起来就是几万次的减法。例如600*800的图片做一次均值。

def _preprocess_subtract_imagenet_mean(inputs):
  """Subtract Imagenet mean RGB value."""
  mean_rgb = tf.reshape(_MEAN_RGB, [1, 1, 1, 3])
  return inputs - mean_rgb

800×600×3 = 1440000 就做了这么多长的减法。

Tensorflow Lite 没有优化之前是怎么做的。如下就是通道for循环,这样的代码是很损耗性能的。

 for (int b = 0; b < extended_output_shape.Dims(0); ++b) {
     for (int y = 0; y < extended_output_shape.Dims(1); ++y) {
       for (int x = 0; x < extended_output_shape.Dims(2); ++x) {
         for (int c = 0; c < extended_output_shape.Dims(3); ++c) {
           float in1 = input1_data[y * desc1.strides[1] + x * desc1.strides[2] + c * desc1.strides[3]];//max 1439999
           float in2 = input2_data[c * desc2.strides[3]];
           int offset = ( y * dims_data[2] + x) * dims_data[3] + c;//max 1439999

         }
       }
     }
   }

这个代码在我的Android 高通660上运行了17ms左右。我将它改成汇编。

inline void BroadcastSub4DSlowDjh(const ArithmeticParams& params,
                               const RuntimeShape& input1_shape,
                               const float* input1_data,
                               const RuntimeShape& input2_shape,
                               const float* input2_data,
                               const RuntimeShape& output_shape,
                               float* output_data) {
  float in2bufz1[4] = {123.15f, 115.90f, 103.06f, 123.15f};
  float in2bufz2[4] = {115.90f, 103.06f, 123.15f, 115.90f};
  float in2bufz3[4] = {103.06f, 123.15f, 115.90f, 103.06f};
  
  for (int b = 0; b < 1440000; b+=12) {
    // _MEAN_RGB = [123.15, 115.90, 103.06]
    float32x4_t in1_vecz1 = vld1q_f32(&input1_data[b]);
    float32x4_t in2_vecz1 = vld1q_f32(in2bufz1);
    float32x4_t in1_vecz2 = vld1q_f32(&input1_data[b+4]);
    float32x4_t in2_vecz2 = vld1q_f32(in2bufz2);
    float32x4_t in1_vecz3 = vld1q_f32(&input1_data[b+8]);
    float32x4_t in2_vecz3 = vld1q_f32(in2bufz3);
    float32x4_t ret_vecz1 = vsubq_f32(in1_vecz1, in2_vecz1);
    float32x4_t ret_vecz2 = vsubq_f32(in1_vecz2, in2_vecz2);
    float32x4_t ret_vecz3 = vsubq_f32(in1_vecz3, in2_vecz3);
    float out1 = vgetq_lane_f32(ret_vecz1, 0);
    float out2 = vgetq_lane_f32(ret_vecz1, 1);
    float out3 = vgetq_lane_f32(ret_vecz1, 2);
    float out4 = vgetq_lane_f32(ret_vecz1, 3);
    float out5 = vgetq_lane_f32(ret_vecz2, 0);
    float out6 = vgetq_lane_f32(ret_vecz2, 1);
    float out7 = vgetq_lane_f32(ret_vecz2, 2);
    float out8 = vgetq_lane_f32(ret_vecz2, 3);
    float out9 = vgetq_lane_f32(ret_vecz3, 0);
    float out10 = vgetq_lane_f32(ret_vecz3, 1);
    float out11 = vgetq_lane_f32(ret_vecz3, 2);
    float out12 = vgetq_lane_f32(ret_vecz3, 3);
    output_data[b] = out1;
    output_data[b + 1] = out2;
    output_data[b + 2] = out3;
    output_data[b + 3] = out4;
    output_data[b + 4] = out5;
    output_data[b + 5] = out6;
    output_data[b + 6] = out7;
    output_data[b + 7] = out8;
    output_data[b + 8] = out9;
    output_data[b + 9] = out10;
    output_data[b + 10] = out11;
    output_data[b + 11] = out12;
  }

速度变成了5ms左右

6、总结

总的来说,Tensorflow Lite基本上是不错的,在Arm架构运行上效率较高。当然也还有其他很多地方可以优化的,比如内存,其他API接口的优化,等等。

说几个Tensorflow 不好的地方,1、转换太麻烦了 2、GPU的版本对应NN api要支持,如果没有支持就不能用了。

其他

关注我不迷路,目前只是一些入门级的小文章,后面会有AI系列文章推送。

https://github.com/yazone/ai_learning_path
更多嵌入式AI技术相关内容请关注嵌入式AI专栏。
推荐阅读
关注数
16535
内容数
1230
嵌入式端AI,包括AI算法在推理框架Tengine,MNN,NCNN,PaddlePaddle及相关芯片上的实现。欢迎加入微信交流群,微信号:aijishu20(备注:嵌入式)
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息