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 | TensorFlowancientcc:TensorFlow Lite(2/3):tflite文件和AI Smart
推薦閱讀:
TAG:TensorFlow | 深度學習(DeepLearning) |