目录
前言
近期使用了Tensorflow的saved_model模块,踩过一些坑,总结分享一下。
参考了很多资料,部分代码也是照搬,事先说明一下,谢谢各位大佬分享的资料。
时间长的同学可以从头看,时间短的同学可以跳转文末看总结。
正文
Saved_model模块用于保存加载模型,一般配合TensorFlow Serving使用。TF Serving是一个将训练好的模型部署至生产环境的系统,主要的优点在于可以保持Server端与API不变的情况下,
部署新的算法或进行试验,同时还有很高的性能。常用的TF模型保存方法还有saver()方法,使用checkpoint文件加载恢复模型;xx.pb文件保存方法,使用tf.graph_util模块保存模型,tf.gfile模块加载模型。
saved_model使用教程很多,这里主要说我在工程化过程中遇到的问题和解决方法。
保存模型的目录问题
with tf.Session() as sess:
builder = tf.saved_model.builder.SavedModelBuilder(pb_file_path+'savemodel')
# 构造模型保存的内容,指定要保存的 session,特定的 tag,
builder.add_meta_graph_and_variables(sess, ['cpu_server_1'])builder.save()
这波操作会先在指定路径下建立savemodel目录,之后在该目录下保存模型。
注意点:
- 如果是拼接的目录,注意pb_file_path后面有‘/’,如'/a' + 'savemodel'不行,'/a/' + 'savemodel'可以。
- 该目录不能已存在,当模型目录已存在,无论是否为空,都会报错。这也意味着不能在同一个目录下保存多次模型。
说到保存模型,总少不了这也的场景:比如,模型训练5000步,我想每500步保存一次模型。之前使用过tf.train.saver(),天真的以为建立一个builder,多次调用builder.save()就行了,
实际上调用很多次builder.save()倒是没报错,但这不意味着达到预期效果(多次保存有多个模型)。
多次调用builder.save(),模型目录下并没有多个模型。
正常情况下,在savemodel目录下会看到:
savemodel/
--variables # 这里不止一个variables.data-xxxxx-of-xxxxn,好多示例代码可能太简单,里面只有variables.data-00000-of-00001
variables.data-00000-of-00002
variables.data-00001-of-00002
variables.index
save_model.pb
接下来介绍本文重点 -- 多次保存产生多个模型。
先说工程化中遇到的情况:模型训练正常,模型保存“正常”(至少结构完整,savemodel目录下有一个save_model.pb文件和一个variables子目录)。模型推理失败。
详细情况:这是一个二分类模型,训练过程模型loss稳步下降,验证集loss正常下降,acc,f1值等逐步提升,推理过程,打分始终徘徊在0.5附近。0.5=1/2,就是随机猜嘛。
分析:模型训练应该没问题,或者模型保存有问题,比如没有保存训练后的计算图和参数;或者模型加载有问题,没把保存的有效计算图和参数加载到session。
模型保存排查
1-1 builder = tf.saved_model.builder.SavedModelBuilder(pb_file_path+'savemodel')
1-2 builder.add_meta_graph_and_variables(sess, ['cpu_server_1'])2 builder.save() # 只执行写入,将builder中的信息(对我们来说就算模型啦)序列化写入磁盘
上述3行代码分开写了,1-1和1-2写在一起,在模型构建过程中,2写在模型保存过程。
这导致只在模型构建最初,将计算图和会话保存到builder里面,后面模型训练,参数更新都没加到builder里面。。。
多次调用 builder.save() 保存了寂寞,只是不断将初始计算图和会话写进去而已
后来想把1-2和2写到一块,都放在模型保存部分,报错。
第一次
builder.add_meta_graph_and_variables(sess, ['cpu_server_1'])
builder.save()
第二次,还特意给sess换个名字
builder.add_meta_graph_and_variables(sess, ['cpu_server_2'])builder.save()
报错:
AssertionError: Graph state including variables and assets has already been saved. Please invoke `add_meta_graph()` instead.
就是builder.add_meta_graph_and_variables()只能在保存模型时调用一次,且头一次调用,就是必须在builder.add_meta_graph()之前。
后面只能使用builder.add_meta_graph()。
于是第二次换成下面这样
builder.add_meta_graph(sess, ['cpu_server_2'])builder.save()
报错:
TypeError: list indices must be integers or slices, not str
具体没弄明白,大意这个session已经有图,后面再加东西需以前图为索引,可以用如下方式。
于是第二次换成下面这样
builder.add_meta_graph(sess, [0])
builder.save()
报错:
AttributeError: 'int' object has no attribute 'inputs'
这个坑无限深,确实没有解决思路,期待知情者在评论区解惑。
换个方式处理
第一次
builder = tf.saved_model.builder.SavedModelBuilder(pb_file_path+'savemodel')
builder.add_meta_graph_and_variables(sess, ['cpu_server_1'])
builder.save()
第二次,还特意给sess换个名字
builder2 = tf.saved_model.builder.SavedModelBuilder(pb_file_path+'savemodel2')
builder2.add_meta_graph_and_variables(sess, ['cpu_server_2'])builder2.save()
建立目录,存放计算图和会话,写入磁盘,这三步作为一个完整的模型保存步骤,每次参数更新后保存模型就执行这三步,走完一套,就没错。
这样最终会保存多个模型,加载时需指明模型存储路径,即savemodel还是savemodel2或是其他路径。
如果一共训练1500steps,每500steps保存一下,假设保存在根目录下的models目录下,会看到
/models/
--savemodel
--savemodel2
--savemodel3
很多情况下我们不想保存这么多模型,只保留一个,比如最好的那个,可以借助一个小技巧,示例如下:
import shutil
import tensorflow as tf
def save(sess, model_name):
model_path = r'./models/' + model_name
# 判断之前是否保存过该模型,保存了就删掉目录
if os.path.exists(model_path):
shutil.rmtree(model_path)
# 重新建立模型目录并保存
builder = tf.saved_model.builder.SavedModelBuilder(model_path)
builder.add_meta_graph_and_variables(self.sess, ['cpu_server'])
builder.save()
接下来继续说工程化遇到的问题,先说现象:模型训练正常,模型保存正常,模型推理失败,依然是模型打分徘徊在0.5附近。
分析:模型训练和保存应该是没问题的,很可能模型载入有问题。
模型载入排查
import tensorflow as tf
with tf.Session(graph=tf.Graph()) as sess:
tf.saved_model.loader.load(self.sess, ['cpu_server'], model_path )
sess.run(tf.global_variables_initializer()) # 问题在这
问题就出在那个初始化,这一下把载入的有效计算图给还原为初始状态了,白训练了。这一个命令可以不用,也可以用在load模型之前,反正load之前也是默认计算图,
初始化前后没有影响,后面还会载入模型的。
在排查问题的过程中还有一个意外发现,这是在练习代码中发现的。基本上开发时我都会在项目外,单独建立练习文件或项目,有些不懂的先在练习部分测试,
弄清楚了再写到工作项目中。结合代码说问题:
import tensorflow as tf
import os
from tensorflow.python.framework import graph_util
from tensorflow.python.saved_model import tag_constants
pb_file_path = os.getcwd() + '/'
print('pb_path', pb_file_path)
with tf.Session(graph=tf.Graph()) as sess:
x = tf.placeholder(tf.int32, name='x')
y = tf.placeholder(tf.int32, name='y')
b = tf.Variable(1, name='b')
xy = tf.multiply(x, y)
# 这里的输出需要加上name属性
op = tf.add(xy, b, name='op_to_store')
sess.run(tf.global_variables_initializer())
graph_org = sess.graph_def
print('graph', type(sess.graph_def))
# 测试 OP
feed_dict = {x: 10, y: 3}
print(sess.run(op, feed_dict))
# INFO:tensorflow:Froze 1 variables.
# Converted 1 variables to const ops.
# 31
builder = tf.saved_model.builder.SavedModelBuilder(pb_file_path+'savemodel')
# 构造模型保存的内容,指定要保存的 session,特定的 tag,
builder.add_meta_graph_and_variables(sess, ['cpu_server_1'])
builder.save() # 保存 PB 模型
b = tf.assign(b, 2)
print('b', sess.run(b)) # 注意这里,不执行b,赋值不会生效
print('op', sess.run(op, feed_dict))
graph2 = sess.graph_def
if graph is graph2:
print('IS')
elif graph == graph2:
print('Equal')
else:
print('Other')
print('graph2', type(sess.graph_def))
# 添加第二个 MetaGraphDef
builder2 = tf.saved_model.builder.SavedModelBuilder(pb_file_path+'savemodel2')
builder2.add_meta_graph_and_variables(sess, ['cpu_server_2'])
builder2.save()
上述代码,如果不执行sess.run(b),b的赋值不会生效,即没有从1变成2.
执行完上述代码可以注意到graph和graph2并不一样,这意味着在计算图中进行的操作,增减改节点,会变更计算图状态。训练模型就是更新参数,图的状态也会变化。
因此,想保存有效计算图还得在反向传播更新模型之后,一般在batch训练之后再保存模型。
载入运行代码如下
# 只需要指定要恢复模型的 session,模型的 tag,模型的保存路径即可,使用起来更加简单
with tf.Graph().as_default():
with tf.Session() as sess: # graph=tf.Graph()
tf.saved_model.loader.load(sess, ['cpu_server_2'], pb_file_path+'savemodel2')
# sess.run(tf.global_variables_initializer())
input_x = sess.graph.get_tensor_by_name('x:0')
input_y = sess.graph.get_tensor_by_name('y:0')
op = sess.graph.get_tensor_by_name('op_to_store:0')
b = sess.graph.get_tensor_by_name('b:0')
print('b', sess.run(b))
ret = sess.run(op, feed_dict={input_x: 10, input_y: 3})
print(ret)
如果没有执行sess.run(b),那么载入savemodel2和载入savemodel的执行结果一样都是31。若保存模型时执行了sess.run(b),载入savemodel2执行结果会变成32。
总结
- 保存模型时,需确保三点:
- 模型保存的直接目录不可存在,无论是否存在,该目录会直接创建。拼接的目录注意有‘/’,'a/' + 'savemodel'或者'a' + r'/savemodel';
- 确保模型保存有效性,在模型参数更新后再保存模型,一般在batch训练后即可;
- 保存模型执行完整的三部曲,建立保存器builder,保存会话和计算图,序列化写入磁盘--builder.save()。
- 模型加载时,需确保两点
- 模型保存路径正确,否则找不到模型;
- 模型加载前执行sess.run(tf.global_variables_initializer()),也可以不执行初始化,绝对不能在模型加载后执行初始化。