从零开始在Windows上构建Android版的Tensorflow Lite

Posted schoene

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从零开始在Windows上构建Android版的Tensorflow Lite相关的知识,希望对你有一定的参考价值。

文章目录


一般情况下,构建android版本的Tensorflow Lite可以参考谷歌的标准文档:

https://tensorflow.google.cn/lite/android/lite_build?hl=cs

其思路是使用Docker构建系统。也可以到网上查询,一般的解决方案是使用Bazel构建系统。这两种构建都需要在Linux系统上进行,可以在一台Linux机器上构建也可以在Windows上安装一个Linux子系统来进行。

本文提供了另一种方案,即在Windows上通过NDK和CMake进行构建。如果熟悉CMake构建系统,整个过程还是比较容易的。

第一步:获取源代码

1. 工具:Git

获取源代码的工具是git,在Windows上,我们从下面网址可以下载Windows git

https://gitforwindows.org/

2. 下载代码

我们在自己计算机的某个工作目录下(<work_dir>) 创建目录: tfbuild,在tfbuild中再创建一层目录tensorflow_src。在tfbuild/tensorflow_src目录下,用以下命令获取代码:

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

如果觉得用这个方法下载比较慢,也可以考虑到gitee上下载:

https://gitee.com/erban-myproject/tensorflow

我觉得后面这个下载速度比较快。直接在网站上查看Release.md,版本是 2.12.0。通过git或者直接下载zip都可以。zip下载的,解压后,把tensorflow所在的哪一级内容全部拷贝到tfbuild/tensorflow_src下。目录结构如下:

第二步:了解代码

我们大致浏览一下代码,发现lite里相关位置有一个说明:

tfbuild/tensorflow_src/tensorflow/lite/tools/cmake/README.md

打开一看,指向了另一个位置,我们打开看一下:

tfbuild/tensorflow_src/tensorflow/lite/g3doc/guide/build_cmake.md

很有意思,就是一个如何用CMake编译Tensorflow Lite库的说明。一上来就说了CMake版本。往下看有六步,最后一步构建benchmark可以不做。第四步定义提到了很多参数,非常重要。另外里面还有一个Specifics of Android cross-compilation的部分也非常重要。在全文最后有一个Build TensorFlow Lite C library,可以让你用CMake在PC上构建tensorflow lite的动态库,也很不错。有兴趣的可以在完成本文构建后自行尝试。

以上内容,基本上也是本文构建Tensorflow Lite的基本思路。

第三步:工具准备

1. Git

第一步已经准备了。因为从github上下载的tensorflow是不包括其依赖库的,在编译过程中依赖库需要被下载,获取的工具就是git。

2. NDK

既然是为Android构建的,我假设看本文的开发者是Android开发者。一般都会安装有Android Studio,通过Android Studio安装NDK即可。我们通过Android Studio安装最新的NDK:25.1.8937393。

3. CMake

按照第二步中的说明,需要3.16或以上版本。我机子上装的是3.19.1。可以直接安装到tfbuild\\cmake下。

4. Python 3

从这里下载最新的:

https://www.python.org/getit/

5. Patch

有部分模块在编译前可能会用到。到网上找一个具有和linux版相同功能的Windows版patch:

https://gnuwin32.sourceforge.net/packages/patch.htm

把这个patch放在tfbuild\\bin下面

(注意:实测构建Tensorflow 2.12.0 的lite不需要这个工具。)

第四步:环境准备

在tfbuild目录下创建一个批处理文件:env.bat
这个批处理文件内容如下:

    set CMAKE_DIR=<work_dir>/tfbuild/cmake/3.19.1
    set PATH=<work_dir>/tfbuild/bin;<your_python_root_dir>/Python3;%CMAKE_DIR%/bin;%PATH%
    set NDK_DIR=<android_sdk_dir>/ndk/21.3.6528147
    start cmd /k echo environment for building tflite is ready

(注意:上面尖括号中的内容根据读者自己计算机上的路径来填写)

上面三行是配置环境,最后一行将启动一个命令窗口。那么在这个打开的命令窗口中运行的命令都会依赖于前面设置的环境。这就很像在Linux上打开一个Console然后source一些环境。在Windows上我们可以用这种批处理文件来达到类似的目的。

我们可以点击这个批处理文件,用where命令测试一下工具的存在性:

where cmake
where python

第五步:补写一个CMakeLists.txt

为什么还要补写一个CMake编译脚本?我们来分析一下。你可以用上面提到的步骤编tensorflow lite,但是它得到的并不是Android可以用.so,而是一个tensorflow-lite.a。

如果我们在Android中来使用,那一定是以JNI方式来使用的。也就是说有一批Java类,通过这些类调用底层JNI接口。如果只在tensorflow/lite里编译,那么那些jni接口在哪里?

我们重新再找一下代码,看到在tensorflow/lite/java/src/main下有java和native目录。java目录是提供Java类的,native是提供底层接口的,也就是说这个native中的代码也要编译进去。

另外nnapi也是需要的。tensorflow/lite/delegates/nnapi/java/src/main里面也是一个java和一个native目录。那说明和上面处理是相同的。

从tensorflow/lite/java/src/main/java/org/tensorflow/lite/TensorFlowLite.java来看这个.so名字叫libtensorflowlite_jni.so。我们在tensorflow_src/tensorflow下创建一个CMakeLists.txt:

    cmake_minimum_required(VERSION 3.16)
    project(tensorflowlite_jni)

    add_library(
             tensorflowlite_jni

             SHARED

			 lite/java/src/main/native/interpreter_factory_impl_jni.cc
			 lite/java/src/main/native/jni_utils.cc
			 lite/java/src/main/native/nativeinterpreterwrapper_jni.cc
			 lite/java/src/main/native/nativesignaturerunner_jni.cc
			 lite/java/src/main/native/op_resolver_lazy_delegate_proxy.cc
			 lite/java/src/main/native/tensor_jni.cc
			 lite/java/src/main/native/tensorflow_lite_jni.cc
			 lite/delegates/nnapi/java/src/main/native/nnapi_delegate_impl_jni.cc
			 lite/core/shims/jni/jni_utils.cc
             )

    include_directories($PROJECT_SOURCE_DIR)
    add_subdirectory(lite)

    find_library(
              log-lib
              log )
              
    target_link_libraries(
                       tensorflowlite_jni
					   tensorflow-lite
                       $log-lib )

第六步:CMake项目配置

我们以构建Release版本的arm64-v8a为例子。在tfbuild下创建一个目录:tflite_build。双击env.bat,键入如下命令:

    cd tflite_build	
    cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=%NDK_DIR%/build/cmake/android.toolchain.cmake -DANDROID_TOOLCHAIN=clang -DANDROID_ABI=arm64-v8a -DTFLITE_ENABLE_GPU=ON ../tensorflow_src/tensorflow

可以看到CMake的工具链配置来自于NDK。

这一步将消耗比较多的时间,因为系统会根据依赖性将所有依赖库的源代码全部下载下来,这时候就看你的网速了。这一步主要是等待和反复执行命令。你会看到原本tflite_build目录是空的,随着时间的推移会有一个个目录被创建出来,一个个测试模块被编译和运行。命令行窗口上会显示各种测试结果。

配置常见错误1

Cloning into 'farmhash'...
fatal: unable to access 'https://github.com/google/farmhash/': OpenSSL SSL_read: Connection was reset, errno 10054
Cloning into 'farmhash'...
fatal: unable to access 'https://github.com/google/farmhash/': Failed to connect to github.com port 443: Timed out

遇到这种错误是网络连接的问题,再次执行上面开始构建的命令即可。一般当你看到出错库的对应目录创建出来了,就说明已经连上了,正在下载了。

配置常见错误2

CMake Error at <work_dir>/tfbuild/tflite_build/cpuinfo/CMakeLists.txt:262 (ADD_SUBDIRECTORY):
ADD_SUBDIRECTORY not given a binary directory but the given source
    directory "<work_dir>/tfbuild/tflite_build/clog-source" is
    not a subdirectory of
    "<work_dir>/tfbuild/tflite_build/cpuinfo".  When specifying
    an out-of-tree source a binary directory must be explicitly specified.

这个错误非常奇怪,通过观察,发现clog和cpuinfo的代码是完全一样的,甚至clog的CMakeLists.txt里项目名写的是cpuinfo。另外在cpuinfo/deps下发现一个clog模块,看样子外面下载的那个clog是搞错了,应该使用里面这个clog模块。

将<work_dir>/tfbuild/tflite_build/cpuinfo/CMakeLists.txt中有关clog的部分进行修改, 即注释掉256和258行:

    #IF(NOT DEFINED CLOG_SOURCE_DIR)
    SET(CLOG_SOURCE_DIR "$PROJECT_SOURCE_DIR/deps/clog")
    #ENDIF()

也就是无条件设置CLOG_SOURCE_DIR为cpuinfo下的clog

(注意:在修改完CMakeLists.txt后,最好删除tfbuild\\tflite_build\\CMakeCache.txt,然后重新运行本步骤配置CMake项目。)

第七步:构建

终于进入构建步骤了。其命令是:

cmake --build . -j

构建过程也是比较长的,取决于你计算机的编译能力。有4500多个文件要进行编译。在编译过程中也会遇到一些问题。

构建常见错误1

找不到flatc

Traceback (most recent call last):
File "<work_dir>\\tflite_build\\flatbuffers\\scripts\\generate_code.py", line 148, in <module>
    flatc(
File "<work_dir>\\tflite_build\\flatbuffers\\scripts\\generate_code.py", line 82, in flatc
     result = subprocess.run(cmd, cwd=str(cwd), check=True)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "D:\\Python3\\Lib\\subprocess.py", line 548, in run
    with Popen(*popenargs, **kwargs) as process:
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "D:\\Python3\\Lib\\subprocess.py", line 1024, in __init__
    self._execute_child(args, executable, preexec_fn, close_fds,
File "D:\\Python3\\Lib\\subprocess.py", line 1493, in _execute_child
    hp, ht, pid, tid = _winapi.CreateProcess(executable, args,
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
OSError: [WinError 193] %1 不是有效的 Win32 应用程序。

这个错误是由于flatc不是Windows可执行文件导致的。仔细查找generate_code.py在flatbuffers的CMakeLists.txt里的使用。
我们看到它在函数compile_flatbuffers_schema_to_binary里,再查找compile_flatbuffers_schema_to_binary的使用点:

    if(FLATBUFFERS_BUILD_TESTS)
      file(COPY "$CMAKE_CURRENT_SOURCE_DIR/tests" DESTINATION "$CMAKE_CURRENT_BINARY_DIR")
      file(COPY "$CMAKE_CURRENT_SOURCE_DIR/samples" DESTINATION "$CMAKE_CURRENT_BINARY_DIR")

      # TODO Add (monster_test.fbs monsterdata_test.json)->monsterdata_test.mon
      compile_flatbuffers_schema_to_cpp(tests/monster_test.fbs)
      compile_flatbuffers_schema_to_binary(tests/monster_test.fbs)
      compile_flatbuffers_schema_to_cpp_opt(tests/namespace_test/namespace_test1.fbs "--no-includes;--gen-compare;--gen-name-strings")
      compile_flatbuffers_schema_to_cpp_opt(tests/namespace_test/namespace_test2.fbs "--no-includes;--gen-compare;--gen-name-strings")
      compile_flatbuffers_schema_to_cpp_opt(tests/union_vector/union_vector.fbs "--no-includes;--gen-compare;--gen-name-strings")
      ...

从这里往上看,有条件FLATBUFFERS_BUILD_TESTS,可以知道如果开启了构建flatbuffers测试用例,那么最终就会调用flatc。

既然我们现在的目标是编译tensorflow lite,我们可以跳过这些测试用例的构建。

我们查找FLATBUFFERS_BUILD_TESTS,可以看到:

    option(FLATBUFFERS_BUILD_TESTS "Enable the build of tests and samples." ON)
    option(FLATBUFFERS_INSTALL "Enable the installation of targets." ON)
    option(FLATBUFFERS_BUILD_FLATC "Enable the build of the flatbuffers compiler"
       ON)
    option(FLATBUFFERS_STATIC_FLATC "Build flatbuffers compiler with -static flag"
       ON)

我们可以将这些项的ON改为OFF

(注意,在修改完CMakeLists.txt后,删除tfbuild\\tflite_build\\CMakeCache.txt,然后重新运行第六步配置CMake项目。完成后,再启动本步骤的构建。)

这里,我们也可以考虑把flatc编出来的方案,那就要使用Visual Studio。网络已经有文章可以参考:

https://blog.csdn.net/huangjiazhi_/article/details/103262814

问题是编出flatc.exe如何在本构建中来使用。经测试,本文提供一个方法:

  1. 将Visual Studio在Windows上编出flatc.exe,放到tfbuild\\tflite_build_deps\\flatbuffers-build下。
  2. 修改tfbuild\\tflite_build\\flatbuffers\\scripts\\generate_code.py的内容:
# Get the location of the flatc executable, reading from the first command line
# argument or defaulting to default names.
flatc_exe = Path(
    ("flatc" if not platform.system() == "Windows" else "flatc.exe")
    if not args.flatc
    else args.flatc
)

# append .exe on Windows
flatc_exe_str = str(flatc_exe)
if platform.system() == "Windows":
    flatc_exe_str = '%s.exe' % flatc_exe_str
flatc_exe = Path(flatc_exe_str)

# Find and assert flatc compiler is present.
if root_path in flatc_exe.parents:
    flatc_exe = flatc_exe.relative_to(root_path)
flatc_path = Path(root_path, flatc_exe)
assert flatc_path.exists(), "Cannot find the flatc compiler " + str(flatc_path)

# append .exe on Windows 那一段是需要加的内容。就是运行测试前,在要用的可执行文件名后面加上.exe

感觉关闭开关的方案较方便

构建常见错误2

JNI源文件缺失导致链接错误。

error: undefined symbol: TfLiteCheckInitializedOrThrow
>>> referenced by jni_utils.cc:50 (E:/2023/CSDN/BuildTFLite/tfbuild/tensorflow_src/tensorflow/lite/java/src/main/native/jni_utils.cc:50)
>>>               CMakeFiles/tensorflowlite_jni.dir/lite/java/src/main/native/jni_utils.cc.o:(tflite::jni::CheckJniInitializedOrThrow(_JNIEnv*))
clang++: error: linker command failed with exit code 1 (use -v to see invocation)

(注意,这是笔者曾经遇到过的错误。如果你按前面的tensorflow_src/tensorflow/CMakeLists.txt来构建,是不会遇到这个错误的。这里只是举一个例子。)

这个错误是没有TfLiteCheckInitializedOrThrow函数导致的,我们在tfbuild/tensorflow_src/tensorflow/lite里查找所有的文件,在tfbuild\\tensorflow_src\\tensorflow\\lite\\core\\shims\\jni\\jni_utils.cc中找到了这个函数,把它加到tensorflow_src/tensorflow/CMakeLists.txt里即可。

第八步:符号剥离

如果上述问题都解决你将看到正确的编译和链接:

我们在tfbuild\\tflite_build下得到了Release版本的libtensorflowlite_jni.so

祝贺你按照本文获得了你要的.so文件。但是这.so文件好像有点大,达到70多M。

这是由于.so文件中含有大量的符号信息,如果要最终在应用中使用应该把这些符号信息去掉。
运行如下命令:

%NDK_DIR%\\toolchains\\llvm\\prebuilt\\windows-x86_64\\bin\\llvm-strip.exe --strip-all --strip-unneeded libtensorflowlite_jni.so

(注意,不同版本的NDK,strip的位置略有不同。)

再看libtensorflowlite_jni.so,就变成了5.5M,竟然有60多M的符号信息!

构建其他版本

如果打算编译如armv7-a, x86, x86_64, 那在第六步中的参数-DANDROID_ABI=arm64-v8a,可以改为:
-DANDROID_ABI=armv7-a, -DANDROID_ABI=x86, -DANDROID_ABI=x86_64即可。
编译Debug版本使用-DCMAKE_BUILD_TYPE=Debug
(注意,如果改变参数,最好将tfbuild\\tflite_build\\CMakeCache.txt删除后,再按第六步命令来配置。)

总结

本文整理了在Windows上编译Android版本的Tensorflow Lite的步骤。整个过程会遇到一些坑,需要开发人员具有一定的CMake构建项目的经验。开发者需要通过仔细观察CMakeLists.txt文件的内容,错误输出内容来分析和解决问题。

以上是关于从零开始在Windows上构建Android版的Tensorflow Lite的主要内容,如果未能解决你的问题,请参考以下文章

Android从零开始-Gradle详解

从零开始写一个迷你版的Tomcat

教你从零开始写一个迷你版的Tomcat!

《逐梦旅程 WINDOWS游戏编程之从零开始》笔记10——三维天空的构建&三维粒子的实现&多游戏模型的载入

《逐梦旅程 WINDOWS游戏编程之从零开始》笔记9——游戏摄像机&三维地形的构建

从零开始 Flutter