###好好好####Tensorflow將模型匯出為一個檔案及介面設定
在用PMML實現機器學習模型的跨平臺上線中,我們討論了使用PMML檔案來實現跨平臺模型上線的方法,這個方法當然也適用於tensorflow生成的模型,但是由於tensorflow模型往往較大,使用無法優化的PMML檔案大多數時候很笨拙,因此本文我們專門討論下tensorflow機器學習模型的跨平臺上線的方法。
1. tensorflow模型的跨平臺上線的備選方案
tensorflow模型的跨平臺上線的備選方案一般有三種:即PMML方式,tensorflow serving方式,以及跨語言API方式。
PMML方式的主要思路在上一篇以及講過。這裡唯一的區別是轉化生成PMML檔案需要用一個Java庫
tensorflow serving是tensorflow 官方推薦的模型上線預測方式,它需要一個專門的tensorflow伺服器,用來提供預測的API服務。如果你的模型和對應的應用是比較大規模的,那麼使用tensorflow serving是比較好的使用方式。但是它也有一個缺點,就是比較笨重,如果你要使用tensorflow serving,那麼需要自己搭建serving叢集並維護這個叢集。所以為了一個小的應用去做這個工作,有時候會覺得麻煩。
跨語言API方式是本文要討論的方式,它會用tensorflow自己的Python API生成模型檔案,然後用tensorflow的客戶端庫比如Java或C++庫來做模型的線上預測。下面我們會給一個生成生成模型檔案並用tensorflow Java API來做線上預測的例子。
2. 訓練模型並生成模型檔案
我們這裡給一個簡單的邏輯迴歸並生成邏輯迴歸tensorflow模型檔案的例子。
完整程式碼參見我的github:https://github.com/ljpzzz/machinelearning/blob/master/model-in-product/tensorflow-java
首先,我們生成了一個6特徵,3分類輸出的4000個樣本資料。
import numpy as np import matplotlib.pyplot as plt %matplotlib inline from sklearn.datasets.samples_generator import make_classification import tensorflow as tf X1, y1 = make_classification(n_samples=4000, n_features=6, n_redundant=0, n_clusters_per_class=1, n_classes=3)
接著我們構建tensorflow的資料流圖,這裡要注意裡面的兩個名字,第一個是輸入x的名字input,第二個是輸出prediction_labels的名字output,這裡的這兩個名字可以自己取,但是後面會用到,所以要保持一致。
learning_rate = 0.01 training_epochs = 600 batch_size = 100 x = tf.placeholder(tf.float32, [None, 6],name='input') # 6 features y = tf.placeholder(tf.float32, [None, 3]) # 3 classes W = tf.Variable(tf.zeros([6, 3])) b = tf.Variable(tf.zeros([3])) # softmax迴歸 pred = tf.nn.softmax(tf.matmul(x, W) + b, name="softmax") cost = tf.reduce_mean(-tf.reduce_sum(y*tf.log(pred), reduction_indices=1)) optimizer = tf.train.GradientDescentOptimizer(learning_rate).minimize(cost) prediction_labels = tf.argmax(pred, axis=1, name="output") init = tf.global_variables_initializer()
接著就是訓練模型了,程式碼比較簡單,畢竟只是一個演示:
sess = tf.Session() sess.run(init) y2 = tf.one_hot(y1, 3) y2 = sess.run(y2) for epoch in range(training_epochs): _, c = sess.run([optimizer, cost], feed_dict={x: X1, y: y2}) if (epoch+1) % 10 == 0: print ("Epoch:", '%04d' % (epoch+1), "cost=", "{:.9f}".format(c)) print ("優化完畢!") correct_prediction = tf.equal(tf.argmax(pred, 1), tf.argmax(y2, 1)) accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32)) acc = sess.run(accuracy, feed_dict={x: X1, y: y2}) print (acc)
列印輸出我這裡就不寫了,大家可以自己去試一試。接著就是關鍵的一步,存模型檔案了,注意要用convert_variables_to_constants這個API來儲存模型,否則模型引數不會隨著模型圖一起存下來。
graph = tf.graph_util.convert_variables_to_constants(sess, sess.graph_def, ["output"]) tf.train.write_graph(graph, '.', 'rf.pb', as_text=False)
至此,我們的模型檔案rf.pb已經被儲存下來了,下面就是要跨平臺上線了。
3. 模型檔案在Java平臺上線
這裡我們以Java平臺的模型上線為例,C++的API上線我沒有用過,這裡就不寫了。我們需要引入tensorflow的java庫到我們工程的maven或者gradle檔案。這裡給出maven的依賴如下,版本可以根據實際情況選擇一個較新的版本。
<dependency> <groupId>org.tensorflow</groupId> <artifactId>tensorflow</artifactId> <version>1.7.0</version> </dependency>
接著就是程式碼了,這個程式碼會比JPMML的要簡單,我給出了4個測試樣本的預測例子如下,一定要注意的是裡面的input和output要和訓練模型的時候對應的節點名字一致。
import org.tensorflow.*; import org.tensorflow.Graph; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; /** * Created by 劉建平pinard on 2018/7/1. */ public class TFjavaDemo { public static void main(String args[]){ byte[] graphDef = loadTensorflowModel("D:/rf.pb"); float inputs[][] = new float[4][6]; for(int i = 0; i< 4; i++){ for(int j =0; j< 6;j++){ if(i<2) { inputs[i][j] = 2 * i - 5 * j - 6; } else{ inputs[i][j] = 2 * i + 5 * j - 6; } } } Tensor<Float> input = covertArrayToTensor(inputs); Graph g = new Graph(); g.importGraphDef(graphDef); Session s = new Session(g); Tensor result = s.runner().feed("input", input).fetch("output").run().get(0); long[] rshape = result.shape(); int rs = (int) rshape[0]; long realResult[] = new long[rs]; result.copyTo(realResult); for(long a: realResult ) { System.out.println(a); } } static private byte[] loadTensorflowModel(String path){ try { return Files.readAllBytes(Paths.get(path)); } catch (IOException e) { e.printStackTrace(); } return null; } static private Tensor<Float> covertArrayToTensor(float inputs[][]){ return Tensors.create(inputs); } }
我的預測輸出是1,1,0,0,供大家參考。
4. 一點小結
對於tensorflow來說,模型上線一般選擇tensorflow serving或者client API庫來上線,前者適合於較大的模型和應用場景,後者則適合中小型的模型和應用場景。因此演算法工程師使用在產品之前需要做好選擇和評估。
在上一篇文章中《Tensorflow載入預訓練模型和儲存模型》,我們學習到如何使用預訓練的模型。但注意到,在上一篇文章中使用預訓練模型,必須至少的要4個檔案:
checkpoint
MyModel.meta
MyModel.data-00000-of-00001
MyModel.index
這很不便於我們的使用。有沒有辦法匯出為一個pb檔案,然後直接使用呢?答案是肯定的。在文章《Tensorflow載入預訓練模型和儲存模型》中提到,meta檔案儲存圖結構,weights等引數儲存在data檔案中。也就是說,圖和引數資料時分開儲存的。說的更直白一點,就是meta檔案中沒有weights等資料。但是,值得注意的是,meta檔案會儲存常量。我們只需將data檔案中的引數轉為meta檔案中的常量即可!
1 模型匯出為一個檔案
1.1 有程式碼並且從頭開始訓練
Tensorflow提供了工具函式tf.graph_util.convert_variables_to_constants()用於將變數轉為常量。看看官網的描述:
if you have a trained graph containing Variable ops, it can be convenient to convert them all to Const ops holding the same values. This makes it possible to describe the network fully with a single GraphDef file, and allows the removal of a lot of ops related to loading and saving the variables.
我們繼續通過一個簡單例子開始:
import tensorflow as tf
w1 = tf.Variable(20.0, name="w1")
w2 = tf.Variable(30.0, name="w2")
b1= tf.Variable(2.0,name="bias")
w3 = tf.add(w1,w2)
#記住要定義name,後面需要用到
out = tf.multiply(w3,b1,name="out")
# 轉換Variable為constant,並將網路寫入到檔案
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
# 這裡需要填入輸出tensor的名字
graph = tf.graph_util.convert_variables_to_constants(sess, sess.graph_def, ["out"])
tf.train.write_graph(graph, '.', './checkpoint_dir/graph.pb', as_text=False)
執行可以看到如下日誌:
Converted 3 variables to const ops.
1
可以看到通過tf.graph_util.convert_variables_to_constants()函式將變數轉為了常量,並存儲在graph.pb檔案中,接下來看看如何使用這個模型。
import tensorflow as tf
with tf.Session() as sess:
with open('./checkpoint_dir/graph.pb', 'rb') as graph:
graph_def = tf.GraphDef()
graph_def.ParseFromString(graph.read())
output = tf.import_graph_def(graph_def, return_elements=['out:0'])
print(sess.run(output))
執行結果如下:
[100.0]
回到tf.graph_util.convert_variables_to_constants()函式,可以看到,需要傳入Session物件和圖,這都可以理解。看看第三個引數["out"],它是指定這個模型的輸出Tensor。
1.2 有程式碼和模型,但是不想重新訓練模型
有模型原始碼時,在匯出模型時就可以通過tf.graph_util.convert_variables_to_constants()函式來將變數轉為常量儲存到圖檔案中。但是很多時候,我們拿到的是別人的checkpoint檔案,即meta、index、data等檔案。這種情況下,需要將data檔案裡面變數轉為常量儲存到meta檔案中。思路也很簡單,先將checkpoint檔案載入,再重新儲存一次即可。
假設訓練和儲存模型程式碼如下:
import tensorflow as tf
w1 = tf.Variable(20.0, name="w1")
w2 = tf.Variable(30.0, name="w2")
b1= tf.Variable(2.0,name="bias")
w3 = tf.add(w1,w2)
#記住要定義name,後面需要用到
out = tf.multiply(w3,b1,name="out")
# 轉換Variable為constant,並將網路寫入到檔案
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
saver = tf.train.Saver()
# 這裡需要填入輸出tensor的名字
saver.save(sess, './checkpoint_dir/MyModel', global_step=1000)
此時,模型檔案如下:
checkpoint
MyModel-1000.data-00000-of-00001
MyModel-1000.index
MyModel-1000.meta
如果我們只有以上4個模型檔案,但是可以看到訓練原始碼。那麼,將這4個檔案匯出為一個pb檔案方法如下:
import tensorflow as tf
with tf.Session() as sess:
#初始化變數
sess.run(tf.global_variables_initializer())
#獲取最新的checkpoint,其實就是解析了checkpoint檔案
latest_ckpt = tf.train.latest_checkpoint("./checkpoint_dir")
#載入圖
restore_saver = tf.train.import_meta_graph('./checkpoint_dir/MyModel-1000.meta')
#恢復圖,即將weights等引數加入圖對應位置中
restore_saver.restore(sess, latest_ckpt)
#將圖中的變數轉為常量
output_graph_def = tf.graph_util.convert_variables_to_constants(
sess, sess.graph_def , ["out"])
#將新的圖儲存到"/pretrained/graph.pb"檔案中
tf.train.write_graph(output_graph_def, 'pretrained', "graph.pb", as_text=False)
執行後,會有如下日誌:
Converted 3 variables to const ops.
1
接下來就是使用,使用方法跟前面一致:
import tensorflow as tf
with tf.Session() as sess:
with open('./pretrained/graph.pb', 'rb') as graph:
graph_def = tf.GraphDef()
graph_def.ParseFromString(graph.read())
output = tf.import_graph_def(graph_def, return_elements=['out:0'])
print(sess.run(output))
列印資訊如下:
[100.0]
1
2 模型介面設定
我們注意到,前面只是簡單的獲取一個輸出介面,但是很明顯,我們使用的時候,不可能只有一個輸出,還需要有輸入,接下來我們看看,如何設定輸入和輸出。同樣我們分為有程式碼並且從頭開始訓練,和有程式碼和模型,但是不想重新訓練模型兩種情況。
2.1 有程式碼並且從頭開始訓練
相比1.1中的程式碼略作修改即可,第6行程式碼處做了修改:
import tensorflow as tf
w1 = tf.Variable(20.0, name="w1")
w2 = tf.Variable(30.0, name="w2")
#這裡將b1改為placeholder,讓使用者輸入,而不是寫死
#b1= tf.Variable(2.0,name="bias")
b1= tf.placeholder(tf.float32, name='bias')
w3 = tf.add(w1,w2)
#記住要定義name,後面需要用到
out = tf.multiply(w3,b1,name="out")
# 轉換Variable為constant,並將網路寫入到檔案
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
# 這裡需要填入輸出tensor的名字
graph = tf.graph_util.convert_variables_to_constants(sess, sess.graph_def, ["out"])
tf.train.write_graph(graph, '.', './checkpoint_dir/graph.pb', as_text=False)
日誌如下:
Converted 2 variables to const ops.
1
接下來看看如何使用:
import tensorflow as tf
with tf.Session() as sess:
with open('./checkpoint_dir/graph.pb', 'rb') as f:
graph_def = tf.GraphDef()
graph_def.ParseFromString(f.read())
output = tf.import_graph_def(graph_def, input_map={'bias:0':4.}, return_elements=['out:0'])
print(sess.run(output))
列印資訊如下:
[200.0]
也就是說,在設定輸入時,首先將需要輸入的資料作為placeholdler,然後在匯入圖tf.import_graph_def()時,通過引數input_map={}來指定輸入。輸出通過return_elements=[]直接引用tensor的name即可。
2.2 有程式碼和模型,但是不想重新訓練模型
在有程式碼和模型,但是不想重新訓練模型情況下,意味著我們不能直接修改匯出模型的程式碼。但是我們可以通過graph.get_tensor_by_name()函式取得圖中的某些中間結果,然後再加入一些邏輯。其實這種情況在上一篇文章已經講了。可以參考上一篇檔案解決,相比“有程式碼並且從頭開始訓練”情況侷限比較大,大部分情況只能是獲取模型的一些中間結果,但是也滿足我們大多數情況使用了。