標籤:

C++運行TensorFlow模型

這裡說的C++運行TensorFlow模型指的是用純C++代碼,實現用訓練好的TensorFlow模型來預測末知數據。對如何讓iOS、Android運行Tensroflow模型的一系列問題中,它最為核心,因為那些系統基本不可能提供python的運行環境。

為更好理解這問題,最好把它放到更大範圍,然後去了解它在整系統中角色。《用Rose構建需要TensorFlow的跨平台app》在更高角度描述了app使用TensorFlow,它具體對應那文章中「移動app中的TensorFlow」節「2)按模型的輸入要求,根據當前問題生成輸入參數,3)基於模型創建會話,運行會話,根據模型對輸出的定義得到當前問題的識別結果」。當然,文章是把TensorFlow內置到了Rose,但這裡說的解決步驟適用其它想在終端app使用TensorFlow的場合。

我把「C++運行TensorFlow模型」分為五個部分:保存模型、C++運行模型、準備輸入張量、處理輸出張量和保存訓練出的模型。

一、保存模型

要使用模型首先要定義它,然後訓練,接著保存成C++可運行格式。要沒意外,這些都是用python實現,然後在高性能PC上運行。

import tensorflow as tf;nfrom tensorflow.python.framework import graph_util;nndef inference(input_sensor):ntbais = tf.Variable(tf.constant(3.0, shape=[1]));ntresult = tf.add(input_sensor, bais, name = result);nninput_sensor = tf.placeholder(tf.float32, [1, 3, 2, 1], name=sensor);ninference(input_sensor);nnwith tf.Session() as sess:nttf.global_variables_initializer().run();ntgraph_def = tf.get_default_graph().as_graph_def();ntoutput = graph_util.convert_variables_to_constants(sess, graph_def, [result]);ntwith tf.gfile.GFile("/mnt/model/combined_model.pb", "wb") as f:nt tf.write(output.SerializeToString());n

app只關心模型的預測部分,即前向傳播演算法。示例中的「def inference」模擬了預測函數,input_sensor是輸入張量,針對識別圖像往往是個四維張量(以上代碼用了[1, 3, 2, 1])。第一維是batch,對app來說,一次只需預測一張,因而用1。第二維是圖像高度,代碼是3。第三維是圖像寬度,代碼中2。第四維是圖像深度,即表示一個像素需要的位元組數,代碼是1,1表示是灰度圖像,RGB三色時是3。inference中的「bais」表示了偏置項,它是模型參數,是變數。實際問題中參數除了偏置項還有權重(weight),它們都是要被訓練的。result是輸出張量,而且會被要求上傳到app,要填寫好它的節點名稱,第一個原因是模型和app之間是靠名稱來識別特定張量,這點接下的「二、C++運行模型」會有補充。第二個原因則和convert_variables_to_constants第三個參數有關

為減少輸入數據在計算圖中佔用的節點數,python代碼會用tf.placehloader機制提供輸入數據。示例正是用該機制提供inference須要的input_sensor。這裡要注意,placeholder參數中的name值,原因依舊是模型和app之間是靠名稱來識別特定張量。

「使用tf.train.Saver會保存運行TensorFlow程序所需要的全部信息,然而有時並不需要某些信息。比如在測試或者離線預測時,只需要知道如何從神經網路的輸入層經過前向傳播計算得到輸出層即可,而不需要類似於變數初始化、模型保存等輔助節點信息。……而且,將變數取值和計算圖結構分成不同的文件存儲有時候也不方便,於是TensorFlow提供了convert_variables_to_constants函數,通過這個函數可以將計算圖中的變數及其取值通過常量的方式保存,這樣整個TensorFlow計算圖就可以統一存放在一個文件中」《TensorFlow:實戰Google深度學習框架》。這個文件就是C++可運行的模型文件,對應的頂層類是GraphDef,而不是MetaGraphDef。

注意convert_variables_to_constants第三個參數,它是須要保存的節點名稱。示例中「result」節點對應「result」,計算該張量等於執行了整個預測過程。

運行上面python代碼就能生成combined_model.pb,然後把這pb放到app運行時可訪問的目錄。

實際使用時參數(權重和偏置)是要被訓練的,不可能是示例中的一次性賦值。後面會說如何用訓練後模型生成pb,但為連慣先講完整個過程。有了pb後,接下就是C++運行該pb。

二、C++運行模型

《用Rose構建需要TensorFlow的跨平台app》有說如何載入模型到內存,操作適用於任何模型,調用上也直觀,這裡不再補充。載入後是運行,運行調用Run函數。

Status Run(const std::vector<std::pair<string, Tensor> >& inputs,n const std::vector<string>& output_tensor_names, n const std::vector<string>& target_node_names, n std::vector<Tensor>* outputs);n

針對示例,調用是以下語句。「三、準備輸入張量」會說如何生成image_tensor,「四、處理輸出張量」會說如何處理outputs。

std::vector<tensorflow::Tensor> outputs;nrun_status = session->Run({{"sensor", image_tensor}}, {"result"}, {}, &outputs);n

函數功能是用inputs提供的張量執行預測(前向傳播演算法),並把表示預測結果的張量存放在outputs。參數中inputs、output_tensor_names、target_node_names都是輸入,只有outputs是輸出。

  • inputs[INPUT]。它是一個vector,每個單元是(名稱、張量)對,對應GraphDef中的某個輸入張量,注意,名稱不是張量名稱,是節點名稱,即如果是「add:0」的話,它只是前面的「add」部分。換個更容易方法,用的是tf.placeholder指定張量時,名稱就是name參數的值。
  • output_tensor_names[INPUT]。雖然有tensor字眼,但它不是張量名稱而是節點名稱,指定的名稱次序決定了outputs中的張量次序。一旦返回OK,output_tensor_names尺寸一定等於outputs尺寸,否則outputs內容是未定義。對如何填這個名稱,較多使用的方法就是對應convert_variables_to_constants第三個參數中的某個要保存的節點名稱。
  • target_node_names[INPUPT]。tensorflow文檔對它的解釋是「Runs to but does not return Tensors in target_node_names(存儲著運行但不返回的節點)」,我還沒理解它的意義,但好像傳「{}」就行了。
  • outputs[OUT]。輸出張量,往往是convert_variables_to_constants第三個參數對應的一個或多個張量。更多見上面對「output_tensor_names」的注釋。

Run執行完,C++和模型交互的過程其實已經結束了。但考慮到當中一種全新的數據結構:Tensor,有必要說下如何根據C++常見的一維數組生成輸入張量,以及如何把輸出張量轉成一維數組。

三、準備輸入張量

tensorflow::Tensor image_tensor(tensorflow::DT_FLOAT, tensorflow::TensorShape({1, 3, 2, 1}));nauto image_tensor_mapped = image_tensor.tensor<float, 4>();nfloat* out = image_tensor_mapped.data();n

首先是構建一個類型是DT_FLOAT、四個維度[1, 3, 2, 1]的張量對象。為處理方便,張量可能會把內中數據分塊存放,但此時外部向它填充像素數據時以一維數組格式更方便,image_tensor.tensor<float, 4>()於是就把內部數據「映射」成float數組。「映射」後,image_tensor_mapped.data()指向這個數組的起始地址。接下C++就可以認為out就是個通常的float數組,該數組長度是1x3x2x1。

注意out要求的數據格式,對圖像來說,第一維往往是1,後面依次是高度、寬度、深度,那一維數組中位元組是以下格式。

R00 G00 B00 R01 G01 B01.....R10 G10 B10 R11 G11 B11.....n

R00表示是第0行第0列的R分量,B12表示第1行第2列的B分量。這個排序次序就是常見的以「Z」掃描圖像存儲出的位元組格式。

四、處理輸出張量

tensorflow::Tensor* output = &outputs[0];nconst Eigen::TensorMap<Eigen::Tensor<float, 1, Eigen::RowMajor>, Eigen::Aligned>& prediction = output->flat<float>();nconst long count = prediction.size();nfor (int i = 0; i < count; ++i) {ntconst float value = prediction(i);nt// value是該張量以一維數組表示時在索引i處的值。n}n

output->flat<float>()把可能多維的張量轉成一維數組。prediction.size()是數組長度,prediction(i)則是i索引處的單元值。

五、保存訓練出的模型

saver.save(sess, "/mnt/model-10000");n

假設訓練過程是調用上面語句,然後mnt目錄會生成以下三個文件。

model-10000.data-00000-of-00001nmodel-10000.indexnmodel-10000.metan

接下任務就是讓從這三個文件生成pb模型。

import tensorflow as tf;nfrom tensorflow.python.framework import graph_util;nnsaver = tf.train.import_meta_graph("/mnt/model-10000.meta");nnwith tf.Session() as sess:ntsaver.restore(sess, "/mnt/model-10000");nntsess.run(tf.assign(tf.get_default_graph().get_tensor_by_name("layer1/weights:0"), tf.get_default_graph().get_tensor_by_name("layer1/weights/ExponentialMovingAverage:0")));ntsess.run(tf.assign(tf.get_default_graph().get_tensor_by_name("layer1/biases:0"), tf.get_default_graph().get_tensor_by_name("layer1/biases/ExponentialMovingAverage:0")));ntsess.run(tf.assign(tf.get_default_graph().get_tensor_by_name("layer2/weights:0"), tf.get_default_graph().get_tensor_by_name("layer2/weights/ExponentialMovingAverage:0")));ntsess.run(tf.assign(tf.get_default_graph().get_tensor_by_name("layer2/biases:0"), tf.get_default_graph().get_tensor_by_name("layer2/biases/ExponentialMovingAverage:0")));tntsess.run(tf.assign(tf.get_default_graph().get_tensor_by_name("batch-size:0"), [1.0]));nntgraph_def = tf.get_default_graph().as_graph_def();ntoutput = graph_util.convert_variables_to_constants(sess, graph_def, [layer2/add]);ntwith tf.gfile.GFile("/mnt/combined_model-10000.pb", "wb") as f:nt tf.write(output.SerializeToString());n

model-10000.meta存儲著計算圖結構信息,import_meta_graph執行載入這個圖。這時變數未初始化,restore把保存model-10000時的變數值賦給相應變數(相應地完成了初始化)。後面四個變數賦值語句涉及到滑動平均。在TensorFlow,每個變數的滑動平均值是通過影子變數維護的,所以要獲取變數的平均值實際上就是獲取這個影子變數的取值。四個賦值語句作用就是把最後的滑動平均值賦給變數。當中是什麼變數名、以及有多少個變數,要由具體模型決定。示例分了兩層,分別命名為「layer1」、「layer2」,每一層各有兩個變數,表示權重的「weight」和表示偏置項的「biases」。

在訓練時,batch尺寸一般不會是1,修改batch-size變數是為了把batch長度改到app預測需要的1。通常情況下,訓練和app預測會用同樣的inference邏輯,有些模型在寫inference時不得不「顯示」使用batch,這時為兼顧訓練和app預測,就不能把這batch值設為常量,於是改為使用一個叫batch-size的變數。

pool_shape = pool2.get_shape().as_list();nnodes = pool_shape[1] * pool_shape[2] * pool_shape[3];nreshaped = tf.reshape(pool2, [batch_size, nodes]);n

以上代碼使用場景是CNN的卷積層到全連接層。pool2是卷積層的輸出張量,維度[100, 7, 7, 64],全連接層須要個二維向量[100, 3136],即須要把[7, 7, 64]拉直成[3136]。tf.reshape執行這個維度轉換,在轉換時不得不「顯示」使用batch_size。註:此時pool_shape[0]存儲著batch尺寸。以下是定義batch_size變數的代碼,BATCH_SIZE是訓練時的batch尺寸。

batch_size = tf.Variable(tf.constant([BATCH_SIZE * 1.0]), name=batch-size, trainable=False);n

有了計算圖,並設置好變數,調用convert_variables_to_constants生成需要的模型。

示例把滑動平均的影子變數賦給變數用的是「顯示」指定,隨著層數變多,這種寫法很容易導致遺漏哪變數,怎麼能優化它?TensorFlow提供了載入時重命名變數機制,這種機制的一個用途就是處理滑動平均。

variable_averages = tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY);nvariables_to_restore = variable_averages.variables_to_restore();nsaver = tf.train.Saver(variables_to_restore);n

上面代碼用變數重命名機制處理滑動平均,variables_to_restore存儲著和滑動平圴相關、tf.train.Saver類所需的變數重命名字典。它要求用tf.train.Save生成saver,這就和保存模型中用載入計算圖方式生成saver的方法相衝突,目前我沒找到解決這衝突辦法。


推薦閱讀:

編譯器能否對如下場景優化,以及如何檢查不同編譯器對此是否做了優化?
Linux內核應該怎麼去學習?
有沒有使用「==」判斷浮點數相等與否出現錯誤的例子?
學習完 C++ Primer 能做什麼項目練手或者看什麼好的開源項目源碼?
有講C/C++代碼優化和編譯器優化的書或文章嗎?

TAG:TensorFlow | CC |