###好好好####Tensorflow将模型导出为一个文件及接口设置

    在用PMML实现机器学习模型的跨平台上线中,我们讨论了使用PMML文件来实现跨平台模型上线的方法,这个方法当然也适用于tensorflow生成的模型,但是由于tensorflow模型往往较大,使用无法优化的PMML文件大多数时候很笨拙,因此本文我们专门讨论下tensorflow机器学习模型的跨平台上线的方法。

1. tensorflow模型的跨平台上线的备选方案

    tensorflow模型的跨平台上线的备选方案一般有三种:即PMML方式,tensorflow serving方式,以及跨语言API方式。

    PMML方式的主要思路在上一篇以及讲过。这里唯一的区别是转化生成PMML文件需要用一个Java库jpmml-tensorflow来完成,生成PMML文件后,跨语言加载模型和其他PMML模型文件基本类似。

    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()函数取得图中的某些中间结果,然后再加入一些逻辑。其实这种情况在上一篇文章已经讲了。可以参考上一篇文件解决,相比“有代码并且从头开始训练”情况局限比较大,大部分情况只能是获取模型的一些中间结果,但是也满足我们大多数情况使用了。

阅读更多

没有更多推荐了,返回首页