在iOS上接入Tensorflow

齊滇大聖發表於2018-06-23

前言

Tensorflow是goole開源的一套機器學習庫,本篇文章並不介紹通過Tensorflow來生成預測模型,而是簡單的介紹一下在iOS上接入通過Tensorflow生成好的預測模型,一般即.pd檔案。

指令碼生成.a靜態庫

首先下載Tensorflow的專案,專案地址。然後在Readme裡找到iOS部分,裡面有說明需要安裝哪些工具和如何生成靜態庫(這裡會下載一些編譯的依賴庫,可能需要翻牆)。

我這裡直接使用tensorflow/contrib/makefile/build_all_ios.sh -a arm64來生成一個只支援真機arm64的庫,這樣快一點節省時間。不然構建全部架構的話起碼需要兩個小時。

如果你之前已經下載過一遍依賴,再次構建的話可以使用tensorflow/contrib/makefile/build_all_ios.sh -a arm64 -T,那樣的話就不會下載那些依賴了,直接進入編譯過程。

構建完成之後我們就能在 tensorflow/contrib/makefile/gen/libtensorflow/contrib/makefile/gen/protobuf_ios/libtensorflow/contrib/makefile/downloads/nsync/builds/lipo.ios.c++11 下找到生成好的靜態庫。

如果你要支援兩個架構那可以單獨生成兩個架構的.a靜態庫後,使用lipo來合併:

lipo libtensorflow-core-armv7s.a libtensorflow-core-arm64.a -create -output libtensorflow-core.a
複製程式碼

引入TensorFlow靜態庫

引入TensorFlow庫有兩種方式,一種是通過cocoaPods來管理,優點就是接入簡單方便,缺點是包有點大,因為它支援了你所有的機型架構(i386sim, x86_64sim, armv7, armv7s and arm64)。

所以另一種就是通過TensorFlow的一個指令碼自己來生成靜態庫,然後自己引入工程裡面,缺點就是稍微繁瑣一點,需要自己生成靜態庫,還有配置Header search pathsOther Linker Flags等。優點就是可以自己選擇適配哪些架構,有效的減小包的大小。

cocoaPods引入.framework靜態庫

建立一個新專案,然後在Podfile裡引入pod 'TensorFlow-experimental',然後在你要使用TensorFlow的類裡引入對應的標頭檔案,如下:

#import "ViewController.h"

#include "tensorflow/core/framework/op_kernel.h"
#include "tensorflow/core/public/session.h"
複製程式碼

然後我們會發現編譯錯誤,因為我們引入的TensorFlow是C++的程式碼,所以你呼叫的類的檔案字尾.m改為.mm

主工程引入.a靜態庫

我們在Tensorflow文件裡的Creating your Own App from your source libraries下可以看到整個連結生成的靜態庫的過程。

  1. 連結靜態庫路徑

Other Linker Flags裡連結入四個庫libtensorflow-core.alibprotobuf-lite.alibprotobuf.ansync.a,分別在目錄 tensorflow/contrib/makefile/gen/lib/tensorflow/contrib/makefile/gen/protobuf_ios/libtensorflow/contrib/makefile/downloads/nsync/builds/lipo.ios.c++11下面。
這裡我把這三個庫都複製到我的Demo裡,所以在Other Linker Flags里加入的路徑是 ${SRCROOT}/DSTensorflow/SDK/libtensorflow-core.a${SRCROOT}/DSTensorflow/SDK/libprotobuf-lite.a${SRCROOT}/DSTensorflow/SDK/libprotobuf.a${SRCROOT}/DSTensorflow/SDK/nsync.a

  1. 設定Header Search paths

它文件裡寫的是加入以下路徑裡的標頭檔案 the root folder of tensorflow, tensorflow/contrib/makefile/downloads/nsync/public tensorflow/contrib/makefile/downloads/protobuf/src tensorflow/contrib/makefile/downloads, tensorflow/contrib/makefile/downloads/eigen, and tensorflow/contrib/makefile/gen/proto.
但是如果把這整個tensorflow資料夾加到版本管理裡,那麼就太大了,整個tensorflow有六七百兆。所以我刪除了一些檔案,只留下一些需要的檔案,所以我工程裡的路徑是:
"${SRCROOT}/DSTensorflow/SDK" "${SRCROOT}/DSTensorflow/SDK/tensorflow/contrib/makefile/downloads" "${SRCROOT}/DSTensorflow/SDK/tensorflow/contrib/makefile/downloads/eigen" "${SRCROOT}/DSTensorflow/SDK/tensorflow/contrib/makefile/downloads/protobuf/src" "${SRCROOT}/DSTensorflow/SDK/tensorflow/contrib/makefile/gen/proto" "${SRCROOT}/DSTensorflow/SDK/tensorflow/contrib/makefile/downloads/nsync/public"

  1. 加入-force_loadOther Linker Flags${SRCROOT}/DSTensorflow/SDK/libtensorflow-core.a路徑前面加入-force_load

  2. 加入Accelerate framework 在Link Binary with Libraries"里加入Accelerate framework

  3. 支援C++ 設定C++ Language Dialect為GNU++11 (or GNU++14),設定C++ Standard Librarylibc++

  4. 設定bitcode為NO

  5. 移除Other Linker Flags裡的-all_load(如果有的話)

私有Pod引入.a靜態庫

我們上面說的是在主工程引入TensorFlow的庫,但是如果你一個私有的Pod倉庫依賴了這個TensorFlow庫,那麼在引入標頭檔案的時候就會報錯。因為你私有Pod的targets對應的build setting並沒有設定相關的Header Search Paths,私有pod在尋找標頭檔案的時候是根據自己target裡的Header Search Paths來索引的。

我們可以看到TensorFlow-experimental.podspec裡的xcconfig是怎麼給主工程設定的:

"xcconfig": {
    "HEADER_SEARCH_PATHS": "'${SRCROOT}/Pods/TensorFlow-experimental/Frameworks/tensorflow_experimental.framework/Headers' '${SRCROOT}/Pods/TensorFlow-experimental/Frameworks/tensorflow_experimental.framework/Headers/third_party/eigen3'",
    "OTHER_LDFLAGS": "-force_load '${SRCROOT}/Pods/TensorFlow-experimental/Frameworks/tensorflow_experimental.framework/tensorflow_experimental' '-L ${SRCROOT}/Pods/TensorFlow-experimental/Frameworks/tensorflow_experimental.framework' -lprotobuf_experimental"
  }
複製程式碼

podspec有三個相關的引數用來配置build setting,分別為xcconfig(設定主工程和當前pod的build setting)、pod_target_xcconfig(修改當前pod的build setting)、user_target_xcconfig(修改主工程的build setting)。

所以你如果要設定你呼叫Tensorflow那個私有pod的build setting,則需要使用pod_target_xcconfig,比如我demo裡面在DSTensorflow.podspec裡設定如下:

s.preserve_paths = 'DFCVinScanner/SDK/**/*'
s.frameworks = 'Accelerate'
s.pod_target_xcconfig = {"HEADER_SEARCH_PATHS" => "$(inherited) '$(PODS_TARGET_SRCROOT)/DSTensorflow/SDK' '$(PODS_TARGET_SRCROOT)/DSTensorflow/SDK/tensorflow/contrib/makefile/downloads' '$(PODS_TARGET_SRCROOT)/DSTensorflow/SDK/tensorflow/contrib/makefile/downloads/eigen' '$(PODS_TARGET_SRCROOT)/DSTensorflow/SDK/tensorflow/contrib/makefile/downloads/protobuf/src' '$(PODS_TARGET_SRCROOT)/DSTensorflow/SDK/tensorflow/contrib/makefile/gen/proto' '$(PODS_TARGET_SRCROOT)/DSTensorflow/SDK/tensorflow/contrib/makefile/downloads/nsync/public'"}
s.user_target_xcconfig = {"OTHER_LDFLAGS" => ['$(inherited)', '$(PODS_ROOT)/DSTensorflow/DSTensorflow/SDK/nsync.a', '-force_load', '$(PODS_ROOT)/DSTensorflow/DSTensorflow/SDK/libtensorflow-core.a', '$(PODS_ROOT)/DSTensorflow/DSTensorflow/SDK/libprotobuf-lite.a', '$(PODS_ROOT)/DSTensorflow/DSTensorflow/SDK/libprotobuf.a']}
複製程式碼

注意:

  1. 我的OTHER_LDFLAGS是使用user_target_xcconfig,因為靜態庫的連結都是主工程來進行連結,然後HEADER_SEARCH_PATHS使用的是pod_target_xcconfig,pod的target在尋找標頭檔案的時候是在自己的build setting裡的HEADER_SEARCH_PATHS裡配置的路徑進行尋找的。

  2. 我們在本地除錯的時候podfile裡寫的是本地podspec的路徑pod 'DSTensorflow', :path => '../',所以在我們的demo裡的Pods資料夾下面並不會有DSTensorflow這個資料夾,而我們的HEADER_SEARCH_PATHSOTHER_LDFLAGS的路徑為Pods/DSTensorflow下的路徑,所以為了可以除錯,我就手動把那些檔案複製進了這個資料夾下面。當然如果你到時候這個私有pod正常釋出的了,使用pod 'DSTensorflow'這樣正常依賴的話,Pods/DSTensorflow下就會有對應的檔案了。

簡單的接入Tensorflow的Demo


Tensorflow編譯靜態庫指令碼解析

tensorflow-r1.8/tensorflow/contrib/makefile/build_all_ios.sh這個就是建立靜態庫的指令碼,它主要有以下3個引數-a -g -T

Usage: build_all_ios.sh [-a:T]
-a [build_arch] build only for specified arch x86_64 [default=all]
-g [graph] optimize and selectively register ops only for this graph
-T only build tensorflow (dont download other deps etc)
複製程式碼

-a我們之前已經說過了主要用來確定生成對應架構的靜態庫,預設為生成所有架構的靜態庫

-T表示是否只build tensorflow的靜態庫,因為它預設需要下載一些依賴庫,工具庫來幫組build,但是如果已經下載過了,第二遍build的時候其實就不用下載了,這時候就可以用這個引數,這樣可以加快速度。

-g表示只選擇註冊某些你這個模型需要的op操作,這個如果不選的話,你在呼叫你的模型的時候,有些模型就會報No OpKernel was registered to support Op xxx的錯誤,表示你這個靜態庫並不支援這個op操作。 我們也可以自己呼叫指令碼來看看你的模型需要哪些op:

執行以下操作下載對應的工具:
$ bazel build --copt="-DUSE_GEMM_FOR_CONV" tensorflow/python/tools/print_selective_registration_header

執行以下操作得到你模型需要的op,並生成ops_to_register.h(tensorflow在建立靜態庫的時候就會用到這個檔案):
$ bazel-bin/tensorflow/python/tools/print_selective_registration_header --graphs=Users/you-path/graph.pb > tensorflow/core/framework/ops_to_register.h
複製程式碼

一個完整的例子如下:

tensorflow/contrib/makefile/build_all_ios.sh -a arm64 -g Users/you-path/graph.pb -T
複製程式碼

問題

No OpKernel was registered to support Op 'Conv2D'


參考

iOS 平臺 TensorFlow 實踐:實際應用教程(附原始碼)(二) TensorFlow Mobile模型壓縮 解決No OpKernel was registered to support Op 'Less' with these attrs問題原始碼

相關文章