机器学习把 Python 的历史地位推向了一个小高峰,我们习惯于用 Python 去编写机器学习的代码,但是在实际工程中,业务方用 Java 的可能性会比较大,那么如何用 Java 调用 Python 写的已经训练好的机器学习模型呢。本文接下来后以代码的形式极简演示,基于 Tensorflow 训练好的分类模型,如何使用 Java 应用调用。
很奇怪,对于这样的问题,网络上很少有正确的例子,或者说很少有基于 Tensorflow 高级分类器 DNNClassifier 导出的模型,用 Java 调用的例子,为了减少麻烦,如果要实验的情况下,请以下面环境为准。
Python: 3.6Tensorflow: 1.10.0Java 工程中的 gradle 依赖: compile 'org.tensorflow:tensorflow:1.10.0' compile 'org.tensorflow:proto:1.10.0'Python 代码如下,以著名的 iris 代码为例,用 DNNClassifier 进行分类
# # Created on: 2018/8/23. # Author: Heng Xiangzhong # # May the code be with you! # import pandas as pd import tensorflow as tf CSV_COLUMN_NAMES = ['SepalLength', 'SepalWidth', 'PetalLength', 'PetalWidth', 'Species'] data_train = pd.read_csv('./data/iris_training.csv', names=CSV_COLUMN_NAMES, header=0) data_test = pd.read_csv('./data/iris_test.csv', names=CSV_COLUMN_NAMES, header=0) #print(data_train.head()) x_train = data_train y_train = data_train.pop('Species') x_test = data_test y_test = data_test.pop('Species') feature_columns = [] for key in x_train.keys(): feature_columns.append(tf.feature_column.numeric_column(key=key)) classifier = tf.estimator.DNNClassifier(feature_columns=feature_columns, hidden_units=[10, 10], n_classes=3) # 用来保存为 saved_model feature_spec = tf.feature_column.make_parse_example_spec(feature_columns) serving_input_receiver_fn = tf.estimator.export.build_parsing_serving_input_receiver_fn(feature_spec) def train_func(x, y): dataset = tf.data.Dataset.from_tensor_slices((dict(x), y)) dataset = dataset.shuffle(1000).repeat().batch(100) return dataset classifier.train(input_fn=lambda: train_func(x_train, y_train), steps=1000) export_dir = classifier.export_savedmodel(".\model", serving_input_receiver_fn) print(export_dir) def eval_input_func(features, labels, batch_size): features = dict(features) if labels is None: inputs = features else: inputs = (features, labels) dataset = tf.data.Dataset.from_tensor_slices(inputs) assert batch_size is not None, "batch_size must not be None" dataset = dataset.batch(batch_size) return dataset predict_arr = [] predictions = classifier.predict(input_fn=lambda: eval_input_func(x_test, labels=y_test, batch_size=100)) for predict in predictions: predict_arr.append(predict['probabilities'].argmax()) result = predict_arr == y_test result1 = [w for w in result if w == True] print("准确率为 %s" % str((len(result1)/len(result)))) # 如果想用 savedmodel 去加载,可以用下面的方法 def savedmodel_predict_demo(): with tf.Session(graph=tf.Graph()) as sess: predictor_func = tf.contrib.predictor.from_saved_model( ".\\model\\1535446427\\") feature = {} feature["SepalLength"] = tf.train.Feature(float_list=tf.train.FloatList(value=[6.4])) feature["SepalWidth"] = tf.train.Feature(float_list=tf.train.FloatList(value=[2.8])) feature["PetalLength"] = tf.train.Feature(float_list=tf.train.FloatList(value=[5.6])) feature["PetalWidth"] = tf.train.Feature(float_list=tf.train.FloatList(value=[2.2])) examples = [] example = tf.train.Example(features=tf.train.Features(feature=feature)) examples.append(example.SerializeToString()) result = predictor_func({"inputs": examples}) print(result)其中比较重要的接口有,classifier.export_savedmodel, 它会保存模型为 pb 格式到相应目录,这种格式方便于在各种语言之间传播,适用于生产环境。代码片段中,savedmodel_predict_demo 函数演示了如何用 python 加载一个通过 savedmodel 保存的模型并作预测。 下文的 Java 代码演示了,用 Java 语言加载该模型并进行预测。
package org.henzox.test; /* * Created on: 2018/8/28. * Author: Heng Xiangzhong * * May the code be with you! */ import org.tensorflow.SavedModelBundle; import org.tensorflow.Session; import org.tensorflow.Tensor; import org.tensorflow.example.*; public class LoadModelDemo { public static void main(String[] args) { Session session = SavedModelBundle.load(".\\model\\1535446427\\", "serve").session(); Features.Builder builder = Features.newBuilder(); builder.putFeature("SepalLength", Feature.newBuilder().setFloatList(FloatList.newBuilder().addValue(6.4f).build()).build()); builder.putFeature("SepalWidth", Feature.newBuilder().setFloatList(FloatList.newBuilder().addValue(2.8f).build()).build()); builder.putFeature("PetalLength", Feature.newBuilder().setFloatList(FloatList.newBuilder().addValue(5.6f).build()).build()); builder.putFeature("PetalWidth", Feature.newBuilder().setFloatList(FloatList.newBuilder().addValue(2.2f).build()).build()); Example.Builder exampleBuilder = Example.newBuilder().setFeatures(builder.build()); byte[] str = exampleBuilder.build().toByteArray(); byte[][] input = new byte[][]{str}; Tensor<?> x = Tensor.create(input); float[][] y = session.runner().feed("input_example_tensor", x).fetch("dnn/head/predictions/probabilities").run().get(0).copyTo(new float[1][3]); } }上面代码片段可对照用 python 方式加载模型的方法,其实是一样的步骤。值得注意的是在建立 Tensor 时,其实是字节数组,且最终给模型中的值都是经过 pb 序列化的。
代码中涉及到一些硬编码,比如 “input_example_tensor” 和 “dnn/head/predictions/probabilities” 这些字符串,它的由来可以通过 *** model\1535446427>saved_model_cli show --dir ./ --all 得到,输出为
MetaGraphDef with tag-set: 'serve' contains the following SignatureDefs: signature_def['classification']: The given SavedModel SignatureDef contains the following input(s): inputs['inputs'] tensor_info: dtype: DT_STRING shape: (-1) name: input_example_tensor:0 The given SavedModel SignatureDef contains the following output(s): outputs['classes'] tensor_info: dtype: DT_STRING shape: (-1, 3) name: dnn/head/Tile:0 outputs['scores'] tensor_info: dtype: DT_FLOAT shape: (-1, 3) name: dnn/head/predictions/probabilities:0 Method name is: tensorflow/serving/classify signature_def['predict']: The given SavedModel SignatureDef contains the following input(s): inputs['examples'] tensor_info: dtype: DT_STRING shape: (-1) name: input_example_tensor:0 The given SavedModel SignatureDef contains the following output(s): outputs['class_ids'] tensor_info: dtype: DT_INT64 shape: (-1, 1) name: dnn/head/predictions/ExpandDims:0 outputs['classes'] tensor_info: dtype: DT_STRING shape: (-1, 1) name: dnn/head/predictions/str_classes:0 outputs['logits'] tensor_info: dtype: DT_FLOAT shape: (-1, 3) name: dnn/logits/BiasAdd:0 outputs['probabilities'] tensor_info: dtype: DT_FLOAT shape: (-1, 3) name: dnn/head/predictions/probabilities:0 Method name is: tensorflow/serving/predict signature_def['serving_default']: The given SavedModel SignatureDef contains the following input(s): inputs['inputs'] tensor_info: dtype: DT_STRING shape: (-1) name: input_example_tensor:0 The given SavedModel SignatureDef contains the following output(s): outputs['classes'] tensor_info: dtype: DT_STRING shape: (-1, 3) name: dnn/head/Tile:0 outputs['scores'] tensor_info: dtype: DT_FLOAT shape: (-1, 3) name: dnn/head/predictions/probabilities:0 Method name is: tensorflow/serving/classify我理解为,模型其实是一些方法声明,包括名称及调用约定,当实验报错时,可以沿着这些思路排查到底哪个地方出现了问题。 仅以此让大家少踩同样的坑。