一,概述
深度學習模型在移動端的應用越來越多,tensorflow lite就是專門為tensorflow模型在移動端上線推斷設計的框架。tensorflow 官方提供了不少cv的tflite模型,以及c++調用的例子。我們在這里以一個nlp的例子來從零實現到c++調用,並且以調用so動態庫,用cmake編譯的方式的來實現調用tflite模型進行預測。
項目源代碼:https://github.com/jiangxinyang227/nlp_tflite
二,模型訓練
采用的例子是一個類似語言模型的例子,用上文預測當前詞,詳情見nlp_tflite/char_rnn。
在模型的構造過程中要注意一些問題,因為tensorflow lite為了輕量化(編譯后的so文件只有2.7M大小),所以很多tensorflow中的operation都不支持,所以在編寫模型的過程中需要注意,來說下當前碰到的點,當然肯定還有很多的坑沒有碰到。
1),顯示確定計算圖中所有tensor的維度
我們在搭建模型時在使用tf.placeholder傳入數據的時候,有時候會將維度指定為None,在用python訓練和預測時會根據你傳入的數據去動態的確定這個維度的值,如果你想轉換成tflite模型,就必須確定輸入的tensor的維度。因為在執行轉換到tflite的時候需要明確指定輸入的tensor的維度,否則會將第一個維度定為1(tflite會默認所有輸入的tensor的第一個維度時batch_size,並賦值為1)。
計算圖中的tensor同樣要確定維度,一般來說輸入的維度確定了,中間維度一定會是確定,但也有一些特殊的情況,這種我就碰到過,比如我傳入一個標量sequence_len到模型中,標量的值是動態變化的(如序列的長度),然后用tf.range(sequence_len),這種情況不影響轉換成tflite模型,但是在c++調用時會出問題,因為c++是預先給每個tensor分配一個固定大小的地址,這個地址就是由這個tensor的維度和tensor的數據類型確定的,而tf.range是會根據sequence_len的長度生成一個長度不等的一維tensor,此時c++在分配內存時竟然默認數據長度為1。
因此無論如何都要確定計算圖中所有tensor的維度,這樣在c++編譯時才能合理的預先分配內存。
2),不支持random_uniform
這個operation不支持確實讓有點驚訝,但確實不支持,這里影響到的operation挺多的,如tf.random_unifrom_initializer(), tf.glorot_uniform_initializer(), tf.nn.dropout()等等。只要里面調用了random_uniform就不支持,但是有的初始化可能用random_uniform就是比random_normal好,可以用numpy初始化,然后傳入到模型中。
3),不支持dynamic_rnn
dynamic_rnn是用於rnn類模型解碼的operation,這個暫時不支持,也不支持這里面使用到的tf.while_loop之類的操作,需要更換成static_rnn。
4),不支持tf.boolean_mask
這個ops在rnn中用的也很多,通常用在計算損失的時候mask掉序列中padding的部分,但這個也不支持。
5),定義兩份計算圖
訓練的時候通常使用batch_size=32,64這種形式輸入數據,而預測時通常是一條樣本,這兩種情況有時候使用的operation是不一樣的,所以可以定義兩份計算圖,但是要保證兩份圖中的權重是一樣的,將訓練的圖保存成checkpoint,之后可以加載訓練的圖,並將相應的權重賦給預測的圖,並保存成freeze pb文件。
6),量化
量化可以見tensorflow模型量化實例,在轉換成tflite時候可以量化,但對於模型空間比較小的小模型,采用posted training quanzitation的效果可能會不太好,可采用嘗試quantization aware training。采用cpu推斷時建議量化,cpu在int8的計算效率上要高於float32。
三,編譯tensorflowlite.so
1),安裝bazel,見官方教程 https://docs.bazel.build/versions/master/install.html
2),git clone https://github.com/tensorflow/tensorflow.git
3),進入到tensorflow文件夾下,也就是位於WORKSPACE下面,bazel需要在這個文件的目錄下執行編譯
4),./configure
5),bazel build --cxxopt='--std=c++11' //tensorflow/lite:libtensorflowlite.so 如果要在安卓上使用,需要指定ndk
四,創建c++工程,並將依賴的頭文件和so文件放到工程下
1),將tflite的頭文件放置到include下(cd tensorflow/tensorflow) (find ./lite -name "*.h" | tar -cf headers.tar -T -)
2),將上面的headders.tar壓縮包移動到自己項目下的cpp_tflite/include中並解壓,解壓后可以直接去除
3),將tensorflow/bazel-bin/tensorflow/lite/libtensorflowlite.so和tensorflow/bazel-bin/external/flatbuffers/libflatbuffers.a 放到cpp_tflite/lib文件夾中
4),進入到tensorflow/tensorflow/lite/tools/make下執行download_dependencies.sh,或者直接下載flatbuffers壓縮包,然后取出include的內容放置到cpp_tflite/include中即可。
后序編譯詳情見github項目中的README
五,c++調用tflite
tflite中提供了一種數據結構——flatbuffers,會將tflite模型轉換成flatbuffers模型,之后會將模型中的tensor映射到tflite的解釋器中,之后會使用tflite中的解釋器interpreter進行推斷,在加載模型之后一定要確保flatterbuffers的model指針和interpreter兩個指針都是一直存在的,即在整個模型的工作期間都要一直存在,因為依賴這兩個指針進行tensor的查找並進行推斷。我開始就犯了一個錯誤,以為只需要保持interpreter存在就行,將interpreter寫在成員屬性中,而flatbuffers model只在構造函數中使用,結果就一直報錯。
另外在c++中所有的tensor都是由一個指針和一片連續的內存表示,所以無論多少維的輸入,都需要一個個的循環寫入到c++分配的內存中。
以上就是一些注意的點,基本上就能完成一個簡單的模型轉tflite,並用c++進行推斷。
參考文獻