在 Windows 下编译和使用 TensorFlow Lite

发布人:腾讯研究员 华超

TensorFlow Lite 官方在移动端提供了官方编译好的库,我们直接拿来用就好。TensorFlow 在 Linux 平台与 Mac 平台下编译也非常轻松,基本不会遇到太多问题。但是在 Windows 10 下编译我们还是花费了一些时间,最后编译成功了。这里记录一下 Windows 10 下 TensorFlow Lite 编译过程,希望能够帮助到需要的人。

前期环境准备

1.1 安装 MSYS2

MSYS2 官方下载地址直接下载安装即可。安装完成后,将安装路径的 usr\bin 添加到环境变量:假设安装目录为 E:\msys64,则要将 E:\msys64\usr\bin 加入到环境变量 %PATH% 中。

打开控制台 cmd.exe,输入如下命令:

pacman -S git patch unzip

用于安装 gitpatch 以及 unzip

1.2 安装 visual studio

visual studio 可以安装 2019 版本或者最新版本即可。

1.3 安装 python

编译过程中,需要用 python 运行一些脚本,去 python 官网下载最新版 python 安装即可,这里不介绍 python 安装。这里我本机已经安装了Anaconda,使用的 python 版本为 3.6.3 :

E:\>python --versionPython 3.6.3 :: Anaconda 4.4.0 (64-bit)

理论上 python 版本直接安装最新的即可。

值得注意的是,安装完 python 后,还需要安装 numpy 库。否则编译期间会报错找不到 numpy。numpy 可以通过 pip install numpy 来安装。

1.4 下载 TensorFlow 源码

前往 github 下载 TensorFow 源码。目前最新版本是 2.4.0,2.4.0 的 c_api 变动比较大,因此选择 2.3.1 版本,等 2.4.x 版本稳定后再考虑用更新的,如下图所示:

image

下载 2.3.2 版本 tensorflow

1.5 安装 bazel

每个版本的 TensorFlow 都有其对应的 bazel 版本,如果版本不一致,可能会在编译期间出现错误,这种错误还很难排查,-_-||。因此,要确认好当前的 TensorFlow 源码应该使用哪个 bazel 版本。

解压缩 TensorFlow-2.3.1.zip 文件后,打开 cmd,进入 TensorFlow-2.3.1。输入命令 cat configure.py | grep -i bazel_ver。如下:

E:\>cd tensorflow-2.3.1
E:\tensorflow-2.3.1>cat configure.py | grep -i bazel_ve
_TF_CURRENT_BAZEL_VERSION = None
_TF_MIN_BAZEL_VERSION = '3.1.0'
_TF_MAX_BAZEL_VERSION = '3.99.0'

def check_bazel_version(min_version, max_version):
      'TF_IGNORE_MAX_BAZEL_VERSION' not in os.environ):
  global _TF_CURRENT_BAZEL_VERSION
    current_bazel_version = check_bazel_version(_TF_MIN_BAZEL_VERSION,
       _TF_MAX_BAZEL_VERSION)
  _TF_CURRENT_BAZEL_VERSION = convert_version_to_int(current_bazel_version)

可以看到,最小的 bazel 版本为 3.1.0。最大版本是 3.99.0,这里看最大的版本意义不大,本意应该是希望在 3.1.0 以上。按照我对 TensorFlow 的了解, 他们每个版本都会使用当前最新的 bazel,换言之,在编译 TensorFlow 2.3.1 期间很有可能最新的 bazel 版本是 3.1.0,也就是说官方用的是 bazel 3.1.0 编译的。为了确保编译不出问题,跟官方保持一致的版本是最明智的选择。

前往 下载 bazel-3.1.0-windows-x86_64.exe,如下所示:

bazel-3.1.0-windows-x86_64.exe

下载后,放入到 E:\bazel 目录中,并把 E:\bazel 加入到环境变量 PATH 中,同时,把 bazel-3.1.0-windows-x86_64.exe 重命名为 bazel.exe。做完这一切后,打开 cmd,输入 bazel --version 确认 bazel 是否安装成功,以及版本是否正确。

E:\tensorflow-2.3.1>bazel --versionbazel 3.1.0

官方评论:建议使用 bazelisk,它自己自动下载需要的 bazel 版本,这样就不用自己每次去更新 bazel。

开始编译

2.1 执行 configure.py

configure.py 文件会预先设置 python 等一些相关可选项,需要先运行这个文件。

E:\tensorflow-2.3.1>python configure.py
You have bazel 3.1.0 installed.
Please specify the location of python. [Default is E:\Anaconda3\python.exe]:

Found possible Python library paths:
  E:\Anaconda3\lib\site-packages
Please input the desired Python library path to use. Default is [E:\Anaconda3\lib\site-packages]

Do you wish to build TensorFlow with ROCm support? [y/N]: n
No ROCm support will be enabled for TensorFlow.

Do you wish to build TensorFlow with CUDA support? [y/N]: n
No CUDA support will be enabled for TensorFlow.

Please specify optimization flags to use during compilation when bazel option "--config=opt" is specified [Default is /arch:AVX]:


Would you like to override eigen strong inline for some C++ compilation to reduce the compilation time? [Y/n]: n
Not overriding eigen strong inline, some compilations could take more than 20 mins.

Would you like to interactively configure ./WORKSPACE for Android builds? [y/N]: n
Not configuring the WORKSPACE for Android builds.

Preconfigured Bazel build configs. You can use any of the below by adding "--config=<>" to your build command. See .bazelrc for more details.
        --config=mkl # Build with MKL support.
        --config=monolithic # Config for mostly static monolithic build.
        --config=ngraph # Build with Intel nGraph support.
        --config=numa # Build with NUMA support.
        --config=dynamic_kernels # (Experimental) Build kernels into separate shared objects.
        --config=v2 # Build TensorFlow 2.x instead of 1.x.
Preconfigured Bazel build configs to DISABLE default on features:
        --config=noaws # Disable AWS S3 filesystem support.
        --config=nogcp # Disable GCP support.
        --config=nohdfs # Disable HDFS support.
        --config=nonccl # Disable NVIDIA NCCL support.

运行期间,会确认 python 路径、是否配置 android 等等,因为我这边是编译精简 CPU 版的 TFLite,且是为了编译 dll。因此 android、cuda、ROCm 等我都选择了N。

2.2 编译

直接执行 bazel 进行编译即可:

bazel build -c opt --config=mkl //tensorflow/lite/c:tensorflowlite_c.dll

稍等片刻,等待自动下载相关库,并完成编译:

····
INFO: From Linking tensorflow/lite/c/tensorflowlite_c.dll:
LINK : warning LNK4044: 无法识别的选项“/s”;已忽略
  正在创建库 bazel-out/x64_windows-opt/bin/tensorflow/lite/c/tensorflowlite_c.dll.if.lib 和对象
bazel-out/x64_windows-opt/bin/tensorflow/lite/c/tensorflowlite_c.dll.if.exp
Target //tensorflow/lite/c:tensorflowlite_c.dll up-to-date:
  bazel-bin/tensorflow/lite/c/tensorflowlite_c.dll
INFO: Elapsed time: 291.250s, Critical Path: 60.46s
INFO: 305 processes: 305 local.
INFO: Build completed successfully, 444 total actions

打开 E:\tensorflow-2.3.1\bazel-out\x64_windows-opt\bin\tensorflow\lite\c 路径即可得看到 tensorflowlite_c.dll 和 tensorflowlite_c.dll.if.lib 两个文件。这里个文件即为我们最终的结果。

2.3 抠出头文件

光有 dll 和 lib 还不够,我们还需要头文件才能在 c++ 代码里面引用。最简单的方法是直接将整个TensorFlow 源码根路径加入到 include 路径中,这样的话整个项目会过于庞大。

最佳做法是将 E:\tensorflow-2.3.1\tensorflow\lite\c 目录下,只需要如下几个头文件:

image

tflite 相关头文件

使用 TFLite c api 运行 TFLite 模型

上文介绍了如何在 Windows 平台下编译 TFLite 为动态链接库 tensorflowlite_c.dll,接下来介绍如何使用 tensorflowlite_c.dll。上一篇文章中我们编译的 TFLite 库为 c 语言接口,即 c_api,在使用过程中,只需下面一条 include 语句即可:

#include "tensorflow/lite/c/c_api.h"

注意,如果不想亲自动手编译,可以直接到本文最后下载作者已编译好的库即可。

准备 TFLite 模型

TFLite 模型可以使用 TensorFlow 官方提供的相关模型转换接口得到,这里为了演示,使用 mobilenet v3 提供的 TFLite 模型。前往 找到如下 mobilenet v3 模型下载:

image

下载 mobilenet v3 tflite

如果无法打开链接或者是无法下载,请到附件中下载。

加载模型

封装函数 initModel,传入 TFLite 模型路径,代码如下:

void initModel(string path ) {
        TfLiteModel* model = TfLiteModelCreateFromFile(path.c_str());
        TfLiteInterpreterOptions* options = TfLiteInterpreterOptionsCreate();
        interpreter = TfLiteInterpreterCreate(model, options);
        if (interpreter == nullptr) {
                 printf("Failed to create interpreter");
                 cout << (path) << endl;
                 return ;
        }
        // Allocate tensor buffers.
        if (TfLiteInterpreterAllocateTensors(interpreter) != kTfLiteOk) {
                 printf("Failed to allocate tensors!");
                 return ;
        }
        input_tf = getInputTensorByName(interpreter, "input");
        output_tf = getOutputTensorByName(interpreter, "MobilenetV3/Predictions/Softmax");
}

上述代码中,主要使用了如下几个接口:

TfLiteModelCreateFromFile: 创建 TfLiteModel 对象 TfLiteInterpreterOptionsCreate: 设置一些选项,这里暂时没有设置更多的参数。TfLiteInterpreterCreate:创建 TfLiteInterpreter 对象,PS: 这个对象有点 Session 的感觉。TfLiteInterpreterAllocateTensors: 为所有的 Tensor 分配空间,用于向系统请求分配空间。getOutputTensorByName 和 getInputTensorByName 这两个函数是我这边单独封装。getOutputTensorByName 和 getInputTensorByName代码如下:

TfLiteTensor * getOutputTensorByName(TfLiteInterpreter * interpreter, const char * name)
{
       int count = TfLiteInterpreterGetOutputTensorCount(interpreter);
       for (int i = 0; i < count; ++i) {
            TfLiteTensor* ts = (TfLiteTensor*)TfLiteInterpreterGetOutputTensor(interpreter, i);
            if (!strcmp(ts->name, name)) {
                     return ts;
            }
       }
       return nullptr;
}
TfLiteTensor * getInputTensorByName(TfLiteInterpreter * interpreter, const char * name)
{
       int count = TfLiteInterpreterGetInputTensorCount(interpreter);
       for (int i = 0; i < count; ++i) {
                TfLiteTensor* ts = TfLiteInterpreterGetInputTensor(interpreter, i);
                if (!strcmp(ts->name, name)) {
                         return ts;
                }
       }
       return nullptr;
}

前向推理

前向推理主要包括 3 步:

向输入 Tensor 拷贝输入数据 执行推理 从输出 Tensor 将运算结果拷贝出来

示例代码如下:

void forward(float* data, int len) {
       TfLiteTensorCopyFromBuffer(input_tf, data, len*sizeof(float));
       TfLiteInterpreterInvoke(interpreter);
       float logits[1001];
       TfLiteTensorCopyToBuffer(output_tf, logits, 1001*sizeof(float));
       float maxV = -1;
       int maxIdx = -1;
       for (int i = 0; i < 1001; ++i) {
                if (logits[i] > maxV) {
                         maxV = logits[i];
                         maxIdx = i;
                }
                //printf("%d->%f\n", i, logits[i]);
       }
       cout << "类别:" << maxIdx << ",概率:" << maxV << endl;
}

上面代码写的比较粗糙,用起来不灵活,但是足够作为一个示例来使用了。

完整代码

接下来看完整代码,如下:

#include "pch.h"
#include <map>
#include <iostream>
#include <sstream>
#include <fstream>
#include <string>
#include "tensorflow/lite/c/c_api.h"
#pragma comment( lib, "tensorflowlite_c.dll.if.lib" )
using namespace std;
TfLiteTensor* input_tf;
TfLiteTensor* output_tf;
TfLiteInterpreter* interpreter;
TfLiteTensor * getOutputTensorByName(TfLiteInterpreter * interpreter, const char * name)
{
     int count = TfLiteInterpreterGetOutputTensorCount(interpreter);
     for (int i = 0; i < count; ++i) {
              TfLiteTensor* ts = (TfLiteTensor*)TfLiteInterpreterGetOutputTensor(interpreter, i);
              if (!strcmp(ts->name, name)) {
                       return ts;
              }
     }
     return nullptr;
}
TfLiteTensor * getInputTensorByName(TfLiteInterpreter * interpreter, const char * name)
{
        int count = TfLiteInterpreterGetInputTensorCount(interpreter);
        for (int i = 0; i < count; ++i) {
                TfLiteTensor* ts = TfLiteInterpreterGetInputTensor(interpreter, i);
                if (!strcmp(ts->name, name)) {
                         return ts;
}
}
return nullptr;
}
void initModel(string path ) {
        TfLiteModel* model = TfLiteModelCreateFromFile(path.c_str());
        TfLiteInterpreterOptions* options = TfLiteInterpreterOptionsCreate();
        interpreter = TfLiteInterpreterCreate(model, options);
        if (interpreter == nullptr) {
                 printf("Failed to create interpreter");
                 cout << (path) << endl;
                 return ;
        }
        // Allocate tensor buffers.
        if (TfLiteInterpreterAllocateTensors(interpreter) != kTfLiteOk) {
                 printf("Failed to allocate tensors!");
                 return ;
        }
        input_tf = getInputTensorByName(interpreter, "input");
        output_tf = getOutputTensorByName(interpreter, "MobilenetV3/Predictions/Softmax");
}
void forward(float* data, int len) {
        TfLiteTensorCopyFromBuffer(input_tf, data, len*sizeof(float));
        TfLiteInterpreterInvoke(interpreter);
        float logits[1001];
        TfLiteTensorCopyToBuffer(output_tf, logits, 1001*sizeof(float));
        float maxV = -1;
        int maxIdx = -1;
        for (int i = 0; i < 1001; ++i) {
                if (logits[i] > maxV) {
                         maxV = logits[i];
                         maxIdx = i;
                }
                //printf("%d->%f\n", i, logits[i]);
        }
        cout << "类别:" << maxIdx << ",概率:" << maxV << endl;
}
long getSize(string path) {
        ifstream file(path, ios::in | ios::binary);
        long l, m;
        l = file.tellg();
        file.seekg(0, ios::end);
        m = file.tellg();
        file.close();
        return m - l;
}
float* readBmp(string path, int& len) {
        len = getSize(path);
        unsigned char* buff = (unsigned char*)calloc(len, sizeof(unsigned char*));
        ifstream fin(path, std::ifstream::binary);
        fin.read(reinterpret_cast<char*>(buff), len *sizeof(unsigned char*));
        fin.close();
        float* data = (float*)calloc(len, sizeof(float));
        for (int i = 0; i < len; ++i) {
                 data[i] = (buff[i]/255.0-0.5)*2;
        }
        free(buff);
        return data;
}
int main()
{
        initModel("v3-small_224_0.75_float.tflite" );
        int size=0;
        float* bmp = readBmp("input.bin", size);
        forward(bmp, size );
}

附件下载

中文:TensorFlow 公众号

1 Like

上面编译出来的是64位的dll,请问如何编译32位dll

input.bin文件有其他下载方式么?附件下载地址都失效了。

原作者的网站好像挂掉了,我们也没有下载备份,我去联系一下看吧。

@xiaowei 资源下载的网站已经恢复访问,请再来尝试一下。
远程谢谢我的前同事 Lily :wink: 也谢谢 askonline.tech 的站长帮助恢复网站。

我们也上传了一份作为备份下载:

祝大家有一个愉快的学习经历,谢谢!

十分感谢能帮忙联系原作者,以及恢复和备份下载!
我这边刚已经使用完整的包成功运行了这个例子。 :smiling_face:

再次感谢!

1 Like

别客气,有任何问题欢迎继续提问和探讨,也同时期待你的经验分享。 :wink:

1 Like

感谢作者提供的信息。下面对于原作者的内容进行一下补充,从而可以让其他开发者更好的使用TensorFlow lite C。其很多内容在目前最新的2.8.0版本依旧是可以用的。并且经过自己验证,原作者的C接口在2.8中依旧可以用,并能得到正确的结果。

  1. 添加模型转换例子。 onnx.zip (4.7 MB)
  2. 对原作者的例子进行了整理,并添加了自己的例子,并用cmake进行构建。cmake-tensorflow-lite.zip (11.4 MB)
1 Like

模型转换例子:

我的环境:
Python 3.9.7
Tensorflow 2.8
onnx 1.10.2
onnxruntime 1.10.0
onnx-tf 1.9.0

使用onnx的squeezenet里面的onnx模型来进行尝试,这里我用的1.1,opset 7的,也就是 onnx.zip包里面的 squeezenet1.1-7.onnx。

https://github.com/onnx/models/tree/main/vision/classification/squeezenet

先可以尝试在自己的环境中运行onnx-test.py。

注意:需要安装一些python包,脚本的头部也写了。这里直接装最新版本的就可以了,不用指定版本,若最先版本的结果与下面的结果不同,可以尝试和我环境中的版本对齐试试。

pip install onnx mxnet numpy onnxruntime

在我的环境中运行出来的结果如下:

mx_result Top 10 Result:
probability=0.41672978;  class=n02124075 Egyptian cat
probability=0.20263974;  class=n02123159 tiger cat
probability=0.18957904;  class=n02123045 tabby, tabby cat
probability=0.09721760;  class=n02127052 lynx, catamount
probability=0.09200621;  class=n02125311 cougar, puma, catamount, mountain lion, painter, panther, Felis concolor
probability=0.00056486;  class=n02129604 tiger, Panthera tigris
probability=0.00039477;  class=n02120505 grey fox, gray fox, Urocyon cinereoargenteus
probability=0.00031570;  class=n02128757 snow leopard, ounce, Panthera uncia
probability=0.00030183;  class=n02128385 leopard, Panthera pardus
probability=0.00008712;  class=n02119789 kit fox, Vulpes macrotis


onnxruntime_result Top 10 Result:
probability=0.41672498;  class=n02124075 Egyptian cat
probability=0.20264357;  class=n02123159 tiger cat
probability=0.18957756;  class=n02123045 tabby, tabby cat
probability=0.09721833;  class=n02127052 lynx, catamount
probability=0.09200724;  class=n02125311 cougar, puma, catamount, mountain lion, painter, panther, Felis concolor
probability=0.00056487;  class=n02129604 tiger, Panthera tigris
probability=0.00039477;  class=n02120505 grey fox, gray fox, Urocyon cinereoargenteus
probability=0.00031570;  class=n02128757 snow leopard, ounce, Panthera uncia
probability=0.00030183;  class=n02128385 leopard, Panthera pardus
probability=0.00008712;  class=n02119789 kit fox, Vulpes macrotis

如果运行的结果和我相同,那么证明目前的onnx模型是没有问题的。

接下来将onnx转换成tflite。

查阅了一些资料,发现onnx无法直接转换成tflite,所以需要先转换成tf的模型,也就是onnx->tf->tflite这样的过程。这里需要先对onnx-tf进行安装。

这里我参考了这个库:

省略从pytorch->onnx的过程,直接从onnx开始。

这里我直接将转换过程和转换后的tflite模型测试放在一起了,也就是 onnx.zip中的onnx2tf-tflite.py。

这里有一个需要注意的,在脚本的第21行

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

注释部分是参考库中的写法,这个写法对于tflite的Python接口没有问题,但使用C接口时就会报如下错误:

Unsupported value: java.lang.IllegalArgumentException: Internal error: 

Cannot create interpreter: Didn't find op for builtin opcode 'CONV_2D' version '5'

Registration failed.

再参考了这里的回答 android - Firebase Local Model throws "Didn't find op for builtin opcode 'CONV_2D' version '2'" - Stack Overflow 后,将上面的代码改成converter.optimizations = []这样的形式,C接口在加载模型的时候就不会上面的错误了,这样对于Python接口的结果也没有影响,就是转换出来的模型会大一些。

运行之后可以在终端的末端得到inference的结果:

Estimated count of arithmetic ops: 704.034 M  ops, equivalently 352.017 M  MACs
WARNING:absl:Buffer deduplication procedure will be skipped when flatbuffer library is not properly loaded
INFO: Created TensorFlow Lite XNNPACK delegate for CPU.
Tensorflow Lite Top 10 Result:
probability=0.41672441;  class=n02124075 Egyptian cat
probability=0.20264174;  class=n02123159 tiger cat
probability=0.18958022;  class=n02123045 tabby, tabby cat
probability=0.09721856;  class=n02127052 lynx, catamount
probability=0.09200677;  class=n02125311 cougar, puma, catamount, mountain lion, painter, panther, Felis concolor
probability=0.00056487;  class=n02129604 tiger, Panthera tigris
probability=0.00039477;  class=n02120505 grey fox, gray fox, Urocyon cinereoargenteus
probability=0.00031570;  class=n02128757 snow leopard, ounce, Panthera uncia
probability=0.00030183;  class=n02128385 leopard, Panthera pardus
probability=0.00008712;  class=n02119789 kit fox, Vulpes macrotis

然后,可以看到转换成的tf模型和tflite模型。tflite模型在我的环境中的名字为squeezenet1.1-7-2.8.0.tflite。

这样,从onnx到tflite模型的转换就已经完成了。

C接口使用例子:

我的环境:
Visual studio 2019
Tensorflow lite c 2.8 和 2.3.4 (源码编译)

接下来,就是使用C接口进行预测了。

如果没有在windows下安装cmake,可以去 Download CMake 下载cmake的最新版本进行安装。我的例子中需要cmake最低的版本为3.20,只要cmake版本比3.20高,就无需修改直接运行我的例子了。

cmake-tensorflow-lite.zip中我放入了我编译好的tensorflow lite c库。

我这边使用的是VS2019,估计2017和2022也是可以的。如若不行,可以根据原作者的教程使用源码编译对应VS版本的tensorflow lite库,或者安装VS2019先进行尝试。

解压cmake-tensorflow-lite.zip后,可以在有CMakeLists.txt的目录下使用如下命令:

cmake -S. -Bbuild-windows -DTENSORFLOW_LITE_VERSION=2.8.0

就可以生成相应的工程了,这里TENSORFLOW_LITE_VERSION使用的是2.8.0,压缩包里还有2.3.4版本的,如果想用2.3.4版本的可以将cmake命令中的2.8.0改成2.3.4。

接下来可以使用VS打开工程进行编译并运行,或使用下面cmake命令进行编译:

cmake --build build-windows --config Release

要是想要编译Debug版本的,可以将上面命令中的Release改成Debug。

我这边的运行结果如下所示:

mobilenet_v3
类别:896,军用飞机; 概率:0.923549

squeezenet1_1
probability=0.41672564|  class=285 | n02124075 Egyptian cat
probability=0.20264469|  class=282 | n02123159 tiger cat
probability=0.18957788|  class=281 | n02123045 tabby, tabby cat
probability=0.09721737|  class=287 | n02127052 lynx, catamount
probability=0.09200670|  class=286 | n02125311 cougar, puma, catamount, mountain lion, painter, panther, Felis concolor
probability=0.00056487|  class=292 | n02129604 tiger, Panthera tigris
probability=0.00039477|  class=280 | n02120505 grey fox, gray fox, Urocyon cinereoargenteus
probability=0.00031570|  class=289 | n02128757 snow leopard, ounce, Panthera uncia
probability=0.00030183|  class=288 | n02128385 leopard, Panthera pardus
probability=0.00008712|  class=278 | n02119789 kit fox, Vulpes macrotis

可以看到结果tflite C接口和Python接口的结果一致,那么就证明没有问题了。

这里要说明一下:

  1. 转换出来的tflite模型,用2.3.4和2.8.0都可以运行,也就是原作者提供的模型和我转换出来的模型在两个版本的tflite C中都可以运行。
  2. 2.8.0的头文件也还是tensorflow\lite\c下,不过如果要抠出来需要对其进行修改。可以对比我zip包中抠出来的头文件,和原始2.8.0里的头文件差别即可。
2 Likes

cmake 的链接失效了,需要把几个头文件写进去吗?

看看回贴里是否有你需要的链接?
具体是哪个链接失效啦?