用於TensorFlow Serving部署生產環境的saved_model 模組
saved_model模組主要用於TensorFlow Serving。TF Serving是一個將訓練好的模型部署至生產環境的系統,主要的優點在於可以保持Server端與API不變的情況下,部署新的演算法或進行試驗,同時還有很高的效能。
保持Server端與API不變有什麼好處呢?有很多好處,我只從我體會的一個方面舉例子說明一下,比如我們需要部署一個文字分類模型,那麼輸入和輸出是可以確定的,輸入文字,輸出各類別的概率或類別標籤。為了得到較好的效果,我們可能想嘗試很多不同的模型,CNN,RNN,RCNN等,這些模型訓練好儲存下來以後,在inference階段需要重新載入這些模型,我們希望的是inference的程式碼有一份就好,也就是使用新模型的時候不需要針對新模型來修改inference的程式碼。這應該如何實現呢?
在TensorFlow 模型儲存/載入的兩種方法中總結過。
1. 僅用Saver來儲存/載入變數。這個方法顯然不行,僅儲存變數就必須在inference的時候重新定義Graph(定義模型),這樣不同的模型程式碼肯定要修改。即使同一種模型,引數變化了,也需要在程式碼中有所體現,至少需要一個配置檔案來同步,這樣就很繁瑣了。
2. 使用tf.train.import_meta_graph
匯入graph資訊並建立Saver, 再使用Saver restore變數。相比第一種,不需要重新定義模型,但是為了從graph中找到輸入輸出的tensor,還是得用graph.get_tensor_by_name()
經過上面的分析發現,要實現inference的程式碼統一,使用原來的方法也是可以的,只不過TensorFlow官方提供了更好的方法,並且這個方法不僅僅是解決這個問題,所以還是得學習使用saved_model這個模組。
saved_model 儲存/載入模型
先列出會用到的API
class tf.saved_model.builder.SavedModelBuilder
# 初始化方法
__init__(export_dir)
# 匯入graph與變數資訊
add_meta_graph_and_variables(
sess,
tags,
signature_def_map=None,
assets_collection=None,
legacy_init_op=None,
clear_devices=False,
main_op=None
)
# 載入儲存好的模型
tf.saved_model.loader.load(
sess,
tags,
export_dir,
**saver_kwargs
)
(1) 最簡單的場景,只是儲存/載入模型
儲存
要儲存一個已經訓練好的模型,使用下面三行程式碼就可以了。
builder = tf.saved_model.builder.SavedModelBuilder(saved_model_dir)
builder.add_meta_graph_and_variables(sess, ['tag_string'])
builder.save()
首先構造SavedModelBuilder物件,初始化方法只需要傳入用於儲存模型的目錄名,目錄不用預先建立。
add_meta_graph_and_variables
方法匯入graph的資訊以及變數,這個方法假設變數都已經初始化好了,對於每個SavedModelBuilder這個方法一定要執行一次用於匯入第一個meta graph。
第一個引數傳入當前的session,包含了graph的結構與所有變數。
第二個引數是給當前需要儲存的meta graph一個標籤,標籤名可以自定義,在之後載入模型的時候,需要根據這個標籤名去查詢對應的MetaGraphDef,找不到就會報如RuntimeError: MetaGraphDef associated with tags 'foo' could not be found in SavedModel
這樣的錯。標籤也可以選用系統定義好的引數,如tf.saved_model.tag_constants.SERVING
與tf.saved_model.tag_constants.TRAINING
。
save方法就是將模型序列化到指定目錄底下。
儲存好以後到saved_model_dir目錄下,會有一個saved_model.pb
檔案以及variables
資料夾。顧名思義,variables
儲存所有變數,saved_model.pb
用於儲存模型結構等資訊。
載入
使用tf.saved_model.loader.load
方法就可以載入模型。如
meta_graph_def = tf.saved_model.loader.load(sess, ['tag_string'], saved_model_dir)
第一個引數就是當前的session,第二個引數是在儲存的時候定義的meta graph的標籤,標籤一致才能找到對應的meta graph。第三個引數就是模型儲存的目錄。
load完以後,也是從sess對應的graph中獲取需要的tensor來inference。如
x = sess.graph.get_tensor_by_name('input_x:0')
y = sess.graph.get_tensor_by_name('predict_y:0')
# 實際的待inference的樣本
_x = ...
sess.run(y, feed_dict={x: _x})
這樣和之前的第二種方法一樣,也是要知道tensor的name。那麼如何可以在不知道tensor name的情況下使用呢? 那就需要給add_meta_graph_and_variables
方法傳入第三個引數,signature_def_map
。
(2) 使用SignatureDef
相關API
# 構建signature
tf.saved_model.signature_def_utils.build_signature_def(
inputs=None,
outputs=None,
method_name=None
)
# 構建tensor info
tf.saved_model.utils.build_tensor_info(tensor)
SignatureDef,將輸入輸出tensor的資訊都進行了封裝,並且給他們一個自定義的別名,所以在構建模型的階段,可以隨便給tensor命名,只要在儲存訓練好的模型的時候,在SignatureDef中給出統一的別名即可。
TensorFlow的關於這部分的例子中用到了不少signature_constants,這些constants的用處主要是提供了一個方便統一的命名。在我們自己理解SignatureDef的作用的時候,可以先不用管這些,遇到需要命名的時候,想怎麼寫怎麼寫。
儲存
假設定義模型輸入的別名為“input_x”,輸出的別名為“output” ,使用SignatureDef的程式碼如下
builder = tf.saved_model.builder.SavedModelBuilder(saved_model_dir)
# x 為輸入tensor, keep_prob為dropout的prob tensor
inputs = {'input_x': tf.saved_model.utils.build_tensor_info(x),
'keep_prob': tf.saved_model.utils.build_tensor_info(keep_prob)}
# y 為最終需要的輸出結果tensor
outputs = {'output' : tf.saved_model.utils.build_tensor_info(y)}
signature = tf.saved_model.signature_def_utils.build_signature_def(inputs, outputs, 'test_sig_name')
builder.add_meta_graph_and_variables(sess, ['test_saved_model'], {'test_signature':signature})
builder.save()
上述inputs增加一個keep_prob是為了說明inputs可以有多個, build_tensor_info
方法將tensor相關的資訊序列化為TensorInfo protocol buffer。
inputs,outputs都是dict,key是我們約定的輸入輸出別名,value就是對具體tensor包裝得到的TensorInfo。
然後使用build_signature_def
方法構建SignatureDef,第三個引數method_name暫時先隨便給一個。
建立好的SignatureDef是用在add_meta_graph_and_variables
的第三個引數signature_def_map
中,但不是直接傳入SignatureDef物件。事實上signature_def_map
接收的是一個dict,key是我們自己命名的signature名稱,value是SignatureDef物件。
載入與使用的程式碼如下
## 略去構建sess的程式碼
signature_key = 'test_signature'
input_key = 'input_x'
output_key = 'output'
meta_graph_def = tf.saved_model.loader.load(sess, ['test_saved_model'], saved_model_dir)
# 從meta_graph_def中取出SignatureDef物件
signature = meta_graph_def.signature_def
# 從signature中找出具體輸入輸出的tensor name
x_tensor_name = signature[signature_key].inputs[input_key].name
y_tensor_name = signature[signature_key].outputs[output_key].name
# 獲取tensor 並inference
x = sess.graph.get_tensor_by_name(x_tensor_name)
y = sess.graph.get_tensor_by_name(y_tensor_name)
# _x 實際輸入待inference的data
sess.run(y, feed_dict={x:_x})
從上面兩段程式碼可以知道,我們只需要約定好輸入輸出的別名,在儲存模型的時候使用這些別名建立signature,輸入輸出tensor的具體名稱已經完全隱藏,這就實現建立模型與使用模型的解耦。
最近在學習tensorflow serving,但是就這樣平淡看程式碼可能覺得不能真正思考,就想著寫個文章看看,自己寫給自己的,就像自己對著鏡子演講一樣,寫個文章也像自己給自己講課,這樣思考的比較深,學到的也比較多,有錯歡迎揪出,
minist_saved_model.py 是tensorflow的第一個例子,裡面有很多serving的知識,還不瞭解,現在看。下面是它的入口函式,然後直接跳轉到main。
if __name__ == '__main__':
tf.app.run()
在main函式裡:
首先,是對一些引數取值等的合理性校驗:
def main(_):
if len(sys.argv) < 2 or sys.argv[-1].startswith('-'):
print('Usage: mnist_export.py [--training_iteration=x] '
'[--model_version=y] export_dir')
sys.exit(-1)
if FLAGS.training_iteration <= 0:
print 'Please specify a positive value for training iteration.'
sys.exit(-1)
if FLAGS.model_version <= 0:
print 'Please specify a positive value for version number.'
sys.exit(-1)
然後,就開始train model,既然是程式碼解讀加上自己能力還比較弱,簡單的我得解讀呀,牛人繞道。。。
# Train model
print 'Training model...'
#輸入minist資料,這個常見的,裡面的原始碼就是檢視有沒有資料,沒有就在網上
下載下來,然後封裝成一個個batch
mnist = mnist_input_data.read_data_sets(FLAGS.work_dir, one_hot=True)
#這是建立一個session,Session是Graph和執行者之間的媒介,Session.run()實際
上將graph、fetches、feed_dict序列化到位元組陣列中進行計算
sess = tf.InteractiveSession()
#定義一個佔位符,為以後資料等輸入留好介面
serialized_tf_example = tf.placeholder(tf.string, name='tf_example')
#feature_configs 顧名思義,是特徵配置,從形式上看這是一個字典,字典中
初始化key為‘x’,value 是 tf.FixedLenFeature(shape=[784], dtype=tf.float32)的返
回值,而該函式的作用是解析定長的輸入特徵feature相關配置
feature_configs = {'x': tf.FixedLenFeature(shape=[784], dtype=tf.float32),}
#parse_example 常用於稀疏輸入資料
tf_example = tf.parse_example(serialized_tf_example, feature_configs)
#
x = tf.identity(tf_example['x'], name='x') # use tf.identity() to assign name
#因為輸出是10類,所以y_設定成None×10
y_ = tf.placeholder('float', shape=[None, 10])
#定義權重變數
w = tf.Variable(tf.zeros([784, 10]))
#定義偏置變數
b = tf.Variable(tf.zeros([10]))
#對定義的變數進行引數初始化
sess.run(tf.global_variables_initializer())
#對輸入的x和權重w,偏置b進行處理
y = tf.nn.softmax(tf.matmul(x, w) + b, name='y')
#計算交叉熵
cross_entropy = -tf.reduce_sum(y_ * tf.log(y))
#配置優化函式
train_step = tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy)
#這個函式的作用是返回 input 中每行最大的 k 個數,並且返回它們所在位置的索引
values, indices = tf.nn.top_k(y, 10)
#這函式返回一個將索引的Tensor對映到字串的查詢表
table = tf.contrib.lookup.index_to_string_table_from_tensor(
tf.constant([str(i) for i in xrange(10)]))
#在tabel中查詢索引
prediction_classes = table.lookup(tf.to_int64(indices))
#然後開始訓練迭代啦
for _ in range(FLAGS.training_iteration):
#獲取一個batch資料
batch = mnist.train.next_batch(50)
#計算train_step運算,train_step是優化函式的,這個執行帶來的作用就是
根據學習率,最小化cross_entropy,執行一次,就調整引數權重w一次
train_step.run(feed_dict={x: batch[0], y_: batch[1]})
#將得到的y和y_進行對比
correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))
#對比結果計算準確率
accuracy = tf.reduce_mean(tf.cast(correct_prediction, 'float'))
#執行sess,並使用更新後的最終權重,去做預測,並返回預測結果
print 'training accuracy %g' % sess.run(
accuracy, feed_dict={x: mnist.test.images,
y_: mnist.test.labels})
print 'Done training!'
上面就是訓練的過程,就和普通情況下train模型是一樣的道理,現在,我們看後面的model export
# Export model
# WARNING(break-tutorial-inline-code): The following code snippet is
# in-lined in tutorials, please update tutorial documents accordingly
# whenever code changes.
#export_path_base基本路徑代表你要將model export到哪一個路徑下面,
#它的值的獲取是傳入引數的最後一個,訓練命令為:
bazel-bin/tensorflow_serving/example/mnist_saved_model /tmp/mnist_model
那輸出的路徑就是/tmp/mnist_model
export_path_base = sys.argv[-1]
#export_path 真正輸出的路徑是在基本路徑的基礎上加上版本號,預設是version=1
export_path = os.path.join(
tf.compat.as_bytes(export_path_base),
tf.compat.as_bytes(str(FLAGS.model_version)))
print 'Exporting trained model to', export_path
#官網解釋:Builds the SavedModel protocol buffer and saves variables and assets.
builder = tf.saved_model.builder.SavedModelBuilder(export_path)
# Build the signature_def_map.
# serialized_tf_example是上面提到的佔位的輸入,
#其當時定義為tf.placeholder(tf.string, name='tf_example')
#tf.saved_model.utils.build_tensor_info 的作用是構建一個TensorInfo proto
#輸入引數是張量的名稱,型別,大小,這裡是string,想應該是名稱吧,畢竟
#程式碼還沒全部看完,先暫時這麼猜測。輸出是,基於提供引數的a tensor protocol
# buffer
classification_inputs = tf.saved_model.utils.build_tensor_info(
serialized_tf_example)
#函式功能介紹同上,這裡不同的是輸入引數是prediction_classes,
#其定義,prediction_classes = table.lookup(tf.to_int64(indices)),是一個查詢表
#為查詢表構建a tensor protocol buffer
classification_outputs_classes = tf.saved_model.utils.build_tensor_info(
prediction_classes)
#函式功能介紹同上,這裡不同的是輸入引數是values,
#其定義,values, indices = tf.nn.top_k(y, 10),是返回的預測值
#為預測值構建a tensor protocol buffer
classification_outputs_scores = tf.saved_model.utils.build_tensor_info(values)
#然後,繼續看,下面那麼多行都是一個語句,一個個結構慢慢解析
#下面可以直觀地看到有三個引數,分別是inputs ,ouputs和method_name
#inputs ,是一個字典,其key是tensorflow serving 固定定義的介面,
#為: tf.saved_model.signature_constants.CLASSIFY_INPUTS,value的話
#就是之前build的a tensor protocol buffer 之 classification_inputs
#同樣的,output 和method_name 也是一個意思,好吧,這部分就
#瞭解完啦。
classification_signature = (
tf.saved_model.signature_def_utils.build_signature_def(
inputs={
tf.saved_model.signature_constants.CLASSIFY_INPUTS:
classification_inputs
},
outputs={
tf.saved_model.signature_constants.CLASSIFY_OUTPUT_CLASSES:
classification_outputs_classes,
tf.saved_model.signature_constants.CLASSIFY_OUTPUT_SCORES:
classification_outputs_scores
},
method_name=tf.saved_model.signature_constants.CLASSIFY_METHOD_NAME))
#這兩句話都和上面一樣,都是構建a tensor protocol buffer
tensor_info_x = tf.saved_model.utils.build_tensor_info(x)
tensor_info_y = tf.saved_model.utils.build_tensor_info(y)
這個和上面很多行的classification_signature,一樣的
prediction_signature = (
tf.saved_model.signature_def_utils.build_signature_def(
inputs={'images': tensor_info_x},
outputs={'scores': tensor_info_y},
method_name=tf.saved_model.signature_constants.PREDICT_METHOD_NAME))
#這個不一樣了,tf.group的官網解釋挺簡潔的
#Create an op that groups multiple operations.
#When this op finishes, all ops in input have finished. This op has no output.
#Returns:An Operation that executes all its inputs.
#我們看下另一個tf.tables_initializer():
#Returns:An Op that initializes all tables. Note that if there are not tables the returned Op is a NoOp
legacy_init_op = tf.group(tf.tables_initializer(), name='legacy_init_op')
#下面是重點啦,怎麼看出來的?因為上面都是定義什麼的,下面是最後的操作啦
#就一個函式:builder.add_meta_graph_and_variables,
builder.add_meta_graph_and_variables(
sess, [tf.saved_model.tag_constants.SERVING],
signature_def_map={
'predict_images':
prediction_signature,
tf.saved_model.signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY:
classification_signature,
},
legacy_init_op=legacy_init_op)
builder.save()
print 'Done exporting!'