TensorFlow Lite淺度解析

TensorFlow Lite是TensorFlow針對移動和嵌入式設備的輕量級解決方案。它支持設備內機器學習推理,具有低延遲和小二進位大小。TensorFlow Lite還支持Android神經網路API的硬體加速。TensorFlow Lite使用許多技術來實現低延遲,例如優化移動應用程序的內核,預融合激活以及允許更小和更快(定點數學)模型的量化內核。

1.TensorFlow Lite包含什麼?

TensorFlow Lite支持一系列量化和浮點的核心運算符,這些核心運算符已針對移動平台進行了優化。它們結合預融合激活和偏置來進一步提高性能和量化精度。此外,TensorFlow Lite還支持在模型中使用自定義運算。

TensorFlow Lite基於flatbuffer定義了一種新的模型文件格式。 FlatBuffers是一個開源、高效的跨平台序列化庫。它與協議緩衝區類似,但主要區別在於FlatBuffers在訪問數據之前不需要解析/解包步驟到二級表示,通常與每個對象的內存分配相結合。此外,FlatBuffers的代碼佔用空間比比協議緩衝區小一個數量級。

TensorFlow Lite有一個新的基於移動設備優化的解釋器,其主要目標是保持應用程序的精簡和快速。解釋器使用靜態圖形排序和自定義(動態性較小)內存分配器來確保最小的負載,初始化和執行延遲。

TensorFlow Lite提供了一個利用硬體加速的介面 (如果在設備上可用)。它通過Android神經??網路API實現,可在Android 8.1(API級別27)及更高版本上使用。

2.TensorFlow Lite架構

下圖顯示了TensorFlow Lite的架構設計:

從磁碟上經過訓練的TensorFlow模型開始,您將使用TensorFlow Lite轉換器將該模型轉換為TensorFlow Lite文件格式(.tflite)。 然後,您可以在移動應用程序中使用該轉換後的文件。

部署TensorFlow Lite模型文件使用:

Java API:圍繞Android上C++ API的便捷包裝。

C++ API:載入TensorFlow Lite模型文件並調用解釋器。 Android和iOS都提供相同的庫。

解釋器:使用一組內核來執行模型。解釋器支持選擇性內核載入;沒有內核,只有100KB,載入了所有內核的300KB。這比TensorFlow Mobile要求的1.5M的顯著減少。

在選定的Android設備上,解釋器將使用Android神經??網路API進行硬體加速,如果沒有可用的,則默認為CPU執行。

您還可以使用可由Interpreter使用的C ++ API實現自定義內核。

3.模型相關的文件

正是由於 TensorFlow Lite 運行在客戶端本地,開發者必須要在桌面設備上提前訓練好一個模型。並且為了實現模型的導入,還需要認識一些其他類型的文件,比如:Graph Definition, Checkpoints 以及 Frozen Graph。各種類型的數據都需要使用 Protocol Buffers(簡稱 ProtoBuff)來定義數據結構,有了這些 ProtoBuff 代碼,你就可以使用工具來生成對應的 C 和 Python 或者其它語言的代碼,方便裝載、保存和使用數據。

Graph Def

關於 Graph Def(Graph Definition)文件,有兩種格式。拓展名為 .pb 的是二進位 binary 文件;而 .pbtxt 格式的則是更具可讀性的文本文件。但是,實際使用中,二進位文件有著相當高的執行效率和內存優勢。

Graph Def 是你訓練的模型的核心,它定義了 node 的關係結構,方便由其他的進程來讀取。比如下面這個 Graph Def 就定義了「矩陣 A 與矩陣 B 相乘得到矩陣 C」的描述。

node {
name: "a"
op: "matmul"
}
node {
name: "b"
op: "matmul"
input: "a:0"
}
node {
name: "c"
op: "matmul"
input: "a:0"
output: "b:0"
}

Checkpoint

Checkpoint 文件是來自 TensorFlow 圖的序列化變數。這個文件當中沒有圖的結構,所以不會被解釋。在訓練學習的過程中,Checkpoint 文件記錄了不同的 Iteration 中變數的取值。

Frozen Graph

用 Graph Def 和 Checkpoint 生成 Frozen Graph 的過程叫做「凍結」。為什麼稱之為凍結呢?我們知道,生成 Frozen Graph 所需要的量都是從 Checkpoint 當中得到的,那麼這個變數轉為常量的過程就被形象地稱之為「凍結」了。

TensorFlow Lite 模型

TensorFlow Lite 所用的模型是使用 TOCO 工具從 TensorFlow 模型轉化而來的,來源就是經過生成的 Frozen Graph。假如你已經得到了一個「夠用」的模型了,而且你也沒有源代碼或者數據來重新進行訓練,那麼就使用當前的模型吧,沒有任何問題。但如果你有源代碼和數據,直接使用 TOCO 工具進行模型轉化將會是最好的選擇。示例代碼如下:

with tf.Session() as sess:
tflite_model = tf.contrib.lite.toco_convert(sess.graph_def, [img], [out])
open("converted_model.tflite","wb").write(tflite_model)

4.tflite文件格式

tflite存儲格式是flatbuffer,它是google開源的一種二進位序列化格式,同功能的像protobuf。對flatbuffer可小結為三點。

內容分為vtable區和數據區,vtable區保存著變數的偏移值,數據區保存著變數值。

要解析變數a,是在vtable區組合一層層的offset偏移量計算出總偏移,然後以總偏移到數據區中定位從而獲取變數a的值。

一個叫schema的文本文件定義了要進行序列化和反序列化的數據結構。

我們要理解tflite格式,首先要找到這個schema,以及它的頂層數據結構。

Model 結構體:模型的主結構

table Model {
version: uint;
operator_codes: [OperatorCode];
subgraphs: [SubGraph];

description: string;
buffers: [Buffer]
}

在上面的 Model 結構體定義中,operator_codes 定義了整個模型的所有運算元,subgraphs 定義了所有的子圖。子圖當中,第一個元素是主圖。buffers 屬性則是數據存儲區域,主要存儲的是模型的權重信息。

SubGraph 結構體:Model 中最重要的部分

table SubGraph {
tensors: [Tensor];
inputs: [int];
outputs: [int];
operators: [Operator];
name: string;
}

類似的,tensors 屬性定義了子圖的張量列表,而 inputs 和 outputs 都是int數組,每個int值代表張量列表中的索引。剩下的operators 定義了子圖當中的運算元。

Tensor 結構體:包含維度、數據類型、Buffer 位置等信息

table Tensor {
shape: [int];
type: TensorType;
buffer: uint;

name: string;
}

shape表示張量維度,type表示張量類型,分為FLOAT和UINT8,buffer 以索引的形式,給出了這個 Tensor 需要用到子圖的哪一個buffer。

[1, 224, 224, 3]是張量維度,第一維是batch,一次只需預測一張,因而用1。第二維是圖像高度,第三維是圖像寬度,第四維表示圖像深度是3,即一個像素同時有RGB分量。UINT8是張量類型。「data at buffer#48"指示初始化該張量的數據存放在buffers[48]

Operator 結構體:SubGraph 中最重要的結構體

Operator 結構體定義了子圖的結構:

table Operator {
opcode_index: uint;
inputs: [int];
outputs: [int];
}

opcode_index 用索引方式指明該 Operator 對應了哪個運算元。 inputs 和 outputs 則是 Tensor 的索引值,指明該 Operator 的輸入輸出信息。

5.運行tflite

運行分四個步驟,1)載入tflite文件。2)根據當前問題填充輸入張量。3)調用Invoke進行預測。4)解析輸出張量得到識別結果。

1)載入tflite文件。

載入的第一步是從*.tflite得到一個FlatBufferModel對象。後者用於管理tflite模型文件,字面中的flatbuffer指示了模型文件的存儲格式。構造FlatBufferModel有三種,它們都是static成員。

從文件構造:

std::unique_ptr<FlatBufferModel> BuildFromFile(const char* filename, ErrorReporter* error_reporter = DefaultErrorReporter());

從內存數據構造:

std::unique_ptr<FlatBufferModel> BuildFromBuffer(const char* buffer, size_t buffer_size, ErrorReporter* error_reporter = DefaultErrorReporter());

從另一個model構造:

std::unique_ptr<FlatBufferModel> BuildFromModel(const tflite::Model* model_spec,ErrorReporter* error_reporter = DefaultErrorReporter());

當從文件構造時,或使用一次讀入或使用內存映射(mmap)。

if (mmap_file) {
if (use_nnapi && NNAPIExists())
allocation.reset(new NNAPIAllocation(filename, error_reporter));
else
allocation.reset(new MMAPAllocation(filename, error_reporter));
} else {
allocation.reset(new FileCopyAllocation(filename, error_reporter));
}
session.model = tflite::FlatBufferModel::BuildFromBuffer((const char*)fp.data, fsize);

FlatBufferModel不從任何類派生,那它是怎麼和flatbuffer關聯的呢?內部有個tflite::Model類型成員mode_,Mode是從flatbuffers::Table派生的類。FlatBufferModel的構造函數從tflite內容構造出這個mode_。除了mode_這個和flatbuffer有關的變數,FlatBufferModel還有一個成員是allocation_,它存儲著讀取文件的方法。對於一次性傳入內存塊來說,它對應的是MemoryAllocation,allocation_的類型Allocation是這些類的基類。

生成FlatBufferModel後,接著是用它構造模型解釋器:tflite::Interpreter。

tflite::ops::builtin::BuiltinOpResolver resolver;
tflite::InterpreterBuilder(*session.model, resolver)(&session.interpreter);

一旦構造出Interpreter,後續的工作只需要和它打交道。如圖1顯示,Interpreter有兩個重要成員,context_.tensors和nodes_and_registration_。

TfLiteTensor* content_.tensors。它存儲著此次預測要用到的全部張量。一些運算在執行時會要求分配臨時張量,存儲著中間操作結果。像conv_2d,它須要兩個臨時張量。分配這些張量的時機一般在運算的Init常式(調用它的是InterpreterBuilder::ParseNodes),分配方法則是Interpreter::AddTensors。

std::vector<std::pair<TfLiteNode, TfLiteRegistration> > nodes_and_registration_。它存儲接下用Invoke進行預測時要依次執行的運算。TfLiteNode存儲著該運算要用的輸入、輸出、臨時張量,TfLiteRegistration則是運算的各種常式函數指針,像預測時要調用的invoke。

載入過程還有個操作是調用AllocateTensors。

session.interpreter->AllocateTensors();

AllocateTensors作用是給那些沒有內存塊的張量分配內存。舉個例子,上面說過的索引88處的輸入張量,雖然是用了buffer#48,但這buffer其實是空,這時就要給它分配內存塊。

2)根據當前問題填充輸入張量。

int input = interpreter->inputs()[0];
uint8_t* out = interpreter->typed_tensor<uint8_t>(input);

interpreter->inputs()[0]得到輸入張量數組中的第一個張量,也就是classifier中唯一的那個輸入張量。input是個整型值,語義是張量列表中的引索。第二條語句有兩個作用,1)以input為索引,在TfLiteTensor* content_.tensors這個張量表得到具體的張量。2)返回該張量的data.raw,它指示張量正關聯著的內存塊。有了out,就可以把要預測的圖像數據填向它了。

3)調用Invoke進行預測

interpreter.Invoke();

預測就這一條語句,它依次執行nodes_and_registration_中的哪些運算,具體是調用註冊著的invoke方法。

4)解析輸出張量得到識別結果

std::vector<std::pair<float, int> >& top_results
uint8_t* output = interpreter.typed_output_tensor<uint8_t>(0);
GetTopN(output, output_size, N, kThreshold, &top_results);

第一條語句的作用類似第二步的輸入張量,作用是得到輸出張量關聯的內存塊。output存放的數據已是一維數組,之後就可用它得到識別結果了。GetTopN用於計算output數組中最大的N個值(first),以及它們的位置(second)。

參考文獻:

TensorFlow Lite | TensorFlow?

tensorflow.google.cn圖標ancientcc:TensorFlow Lite(2/3):tflite文件和AI Smart?

zhuanlan.zhihu.com圖標
推薦閱讀:

TAG:TensorFlow | 深度學習(DeepLearning) |