用PMML实现机器学习模型的跨平台上线
在机器学习用于产品的时候,我们经常会遇到跨平台的问题。比如我们用Python基于一系列的机器学习库训练了一个模型,但是有时候其他的产品和项目想把这个模型集成进去,但是这些产品很多只支持某些特定的生产环境比如Java,为了上一个机器学习模型去大动干戈修改环境配置很不划算,此时我们就可以考虑用预测模型标记语言(Predictive Model Markup Language,以下简称PMML)来实现跨平台的机器学习模型部署了。
1. PMML概述
PMML是数据挖掘的一种通用的规范,它用统一的XML格式来描述我们生成的机器学习模型。这样无论你的模型是sklearn,R还是Spark MLlib生成的,我们都可以将其转化为标准的XML格式来存储。当我们需要将这个PMML的模型用于部署的时候,可以使用目标环境的解析PMML模型的库来加载模型,并做预测。
可以看出,要使用PMML,需要两步的工作,第一块是将离线训练得到的模型转化为PMML模型文件,第二块是将PMML模型文件载入在线预测环境,进行预测。这两块都需要相关的库支持。
2. PMML模型的生成和加载相关类库
PMML模型的生成相关的库需要看我们使用的离线训练库。如果我们使用的是sklearn,那么可以使用sklearn2pmml这个python库来做模型文件的生成,这个库安装很简单,使用"pip install sklearn2pmml"即可,相关的使用我们后面会有一个demo。如果使用的是Spark MLlib, 这个库有一些模型已经自带了保存PMML模型的方法,可惜并不全。如果是R,则需要安装包"XML"和“PMML”。此外,JAVA库JPMML可以用来生成R,SparkMLlib,xgBoost,Sklearn的模型对应的PMML文件。github地址是:https://github.com/jpmml/jpmml。
加载PMML模型需要目标环境支持PMML加载的库,如果是JAVA,则可以用JPMML来加载PMML模型文件。相关的使用我们后面会有一个demo。
3. PMML模型生成和加载示例
下面我们给一个示例,使用sklearn生成一个决策树模型,用sklearn2pmml生成模型文件,用JPMML加载模型文件,并做预测。
完整代码参见我的github:https://github.com/ljpzzz/machinelearning/blob/master/model-in-product/sklearn-jpmml
首先是用用sklearn生成一个决策树模型,由于我们是需要保存PMML文件,所以最好把模型先放到一个Pipeline数组里面。这个数组里面除了我们的决策树模型以外,还可以有归一化,降维等预处理操作,这里作为一个示例,我们Pipeline数组里面只有决策树模型。代码如下:
import numpy as np import matplotlib.pyplot as plt %matplotlib inline import pandas as pd from sklearn import tree from sklearn2pmml.pipeline import PMMLPipeline from sklearn2pmml import sklearn2pmml import os os.environ["PATH"] += os.pathsep + 'C:/Program Files/Java/jdk1.8.0_171/bin' X=[[1,2,3,1],[2,4,1,5],[7,8,3,6],[4,8,4,7],[2,5,6,9]] y=[0,1,0,2,1] pipeline = PMMLPipeline([("classifier", tree.DecisionTreeClassifier(random_state=9))]); pipeline.fit(X,y) sklearn2pmml(pipeline, ".\demo.pmml", with_repr = True)
上面这段代码做了一个非常简单的决策树分类模型,只有5个训练样本,特征有4个,输出类别有3个。实际应用时,我们需要将模型调参完毕后才将其放入PMMLPipeline进行保存。运行代码后,我们在当前目录会得到一个PMML的XML文件,可以直接打开看,内容大概如下:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <PMML xmlns="http://www.dmg.org/PMML-4_3" version="4.3"> <Header> <Application name="JPMML-SkLearn" version="1.5.3"/> <Timestamp>2018-06-24T05:47:17Z</Timestamp> </Header> <MiningBuildTask> <Extension>PMMLPipeline(steps=[('classifier', DecisionTreeClassifier(class_weight=None, criterion='gini', max_depth=None, max_features=None, max_leaf_nodes=None, min_impurity_decrease=0.0, min_impurity_split=None, min_samples_leaf=1, min_samples_split=2, min_weight_fraction_leaf=0.0, presort=False, random_state=9, splitter='best'))])</Extension> </MiningBuildTask> <DataDictionary> <DataField name="y" optype="categorical" dataType="integer"> <Value value="0"/> <Value value="1"/> <Value value="2"/> </DataField> <DataField name="x3" optype="continuous" dataType="float"/> <DataField name="x4" optype="continuous" dataType="float"/> </DataDictionary> <TransformationDictionary> <DerivedField name="double(x3)" optype="continuous" dataType="double"> <FieldRef field="x3"/> </DerivedField> <DerivedField name="double(x4)" optype="continuous" dataType="double"> <FieldRef field="x4"/> </DerivedField> </TransformationDictionary> <TreeModel functionName="classification" missingValueStrategy="nullPrediction" splitCharacteristic="multiSplit"> <MiningSchema> <MiningField name="y" usageType="target"/> <MiningField name="x3"/> <MiningField name="x4"/> </MiningSchema> <Output> <OutputField name="probability(0)" optype="continuous" dataType="double" feature="probability" value="0"/> <OutputField name="probability(1)" optype="continuous" dataType="double" feature="probability" value="1"/> <OutputField name="probability(2)" optype="continuous" dataType="double" feature="probability" value="2"/> </Output> <Node> <True/> <Node> <SimplePredicate field="double(x3)" operator="lessOrEqual" value="3.5"/> <Node score="1" recordCount="1.0"> <SimplePredicate field="double(x3)" operator="lessOrEqual" value="2.0"/> <ScoreDistribution value="0" recordCount="0.0"/> <ScoreDistribution value="1" recordCount="1.0"/> <ScoreDistribution value="2" recordCount="0.0"/> </Node> <Node score="0" recordCount="2.0"> <True/> <ScoreDistribution value="0" recordCount="2.0"/> <ScoreDistribution value="1" recordCount="0.0"/> <ScoreDistribution value="2" recordCount="0.0"/> </Node> </Node> <Node score="2" recordCount="1.0"> <SimplePredicate field="double(x4)" operator="lessOrEqual" value="8.0"/> <ScoreDistribution value="0" recordCount="0.0"/> <ScoreDistribution value="1" recordCount="0.0"/> <ScoreDistribution value="2" recordCount="1.0"/> </Node> <Node score="1" recordCount="1.0"> <True/> <ScoreDistribution value="0" recordCount="0.0"/> <ScoreDistribution value="1" recordCount="1.0"/> <ScoreDistribution value="2" recordCount="0.0"/> </Node> </Node> </TreeModel> </PMML>
可以看到里面就是决策树模型的树结构节点的各个参数,以及输入值。我们的输入被定义为x1-x4,输出定义为y。
有了PMML模型文件,我们就可以写JAVA代码来读取加载这个模型并做预测了。
我们创建一个Maven或者gradle工程,加入JPMML的依赖,这里给出maven在pom.xml的依赖,gradle的结构是类似的。
<dependency> <groupId>org.jpmml</groupId> <artifactId>pmml-evaluator</artifactId> <version>1.4.1</version> </dependency> <dependency> <groupId>org.jpmml</groupId> <artifactId>pmml-evaluator-extension</artifactId> <version>1.4.1</version> </dependency>
接着就是读取模型文件并预测的代码了,具体代码如下:
import org.dmg.pmml.FieldName; import org.dmg.pmml.PMML; import org.jpmml.evaluator.*; import org.xml.sax.SAXException; import javax.xml.bind.JAXBException; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * Created by 刘建平Pinard on 2018/6/24. */ public class PMMLDemo { private Evaluator loadPmml(){ PMML pmml = new PMML(); InputStream inputStream = null; try { inputStream = new FileInputStream("D:/demo.pmml"); } catch (IOException e) { e.printStackTrace(); } if(inputStream == null){ return null; } InputStream is = inputStream; try { pmml = org.jpmml.model.PMMLUtil.unmarshal(is); } catch (SAXException e1) { e1.printStackTrace(); } catch (JAXBException e1) { e1.printStackTrace(); }finally { //关闭输入流 try { is.close(); } catch (IOException e) { e.printStackTrace(); } } ModelEvaluatorFactory modelEvaluatorFactory = ModelEvaluatorFactory.newInstance(); Evaluator evaluator = modelEvaluatorFactory.newModelEvaluator(pmml); pmml = null; return evaluator; } private int predict(Evaluator evaluator,int a, int b, int c, int d) { Map<String, Integer> data = new HashMap<String, Integer>(); data.put("x1", a); data.put("x2", b); data.put("x3", c); data.put("x4", d); List<InputField> inputFields = evaluator.getInputFields(); //过模型的原始特征,从画像中获取数据,作为模型输入 Map<FieldName, FieldValue> arguments = new LinkedHashMap<FieldName, FieldValue>(); for (InputField inputField : inputFields) { FieldName inputFieldName = inputField.getName(); Object rawValue = data.get(inputFieldName.getValue()); FieldValue inputFieldValue = inputField.prepare(rawValue); arguments.put(inputFieldName, inputFieldValue); } Map<FieldName, ?> results = evaluator.evaluate(arguments); List<TargetField> targetFields = evaluator.getTargetFields(); TargetField targetField = targetFields.get(0); FieldName targetFieldName = targetField.getName(); Object targetFieldValue = results.get(targetFieldName); System.out.println("target: " + targetFieldName.getValue() + " value: " + targetFieldValue); int primitiveValue = -1; if (targetFieldValue instanceof Computable) { Computable computable = (Computable) targetFieldValue; primitiveValue = (Integer)computable.getResult(); } System.out.println(a + " " + b + " " + c + " " + d + ":" + primitiveValue); return primitiveValue; } public static void main(String args[]){ PMMLDemo demo = new PMMLDemo(); Evaluator model = demo.loadPmml(); demo.predict(model,1,8,99,1); demo.predict(model,111,89,9,11); } }
代码里有两个函数,第一个loadPmml是加载模型的,第二个predict是读取预测样本并返回预测值的。我的代码运行结果如下:
target: y value: {result=2, probability_entries=[0=0.0, 1=0.0, 2=1.0], entityId=5, confidence_entries=[]}
1 8 99 1:2
target: y value: {result=1, probability_entries=[0=0.0, 1=1.0, 2=0.0], entityId=6, confidence_entries=[]}
111 89 9 11:1
也就是样本(1,8,99,1)被预测为类别2,而(111,89,9,11)被预测为类别1。
以上就是PMML生成和加载的一个示例,使用起来其实门槛并不高,也很简单。
4. PMML总结与思考
PMML的确是跨平台的利器,但是是不是就没有缺点呢?肯定是有的!
第一个就是PMML为了满足跨平台,牺牲了很多平台独有的优化,所以很多时候我们用算法库自己的保存模型的API得到的模型文件,要比生成的PMML模型文件小很多。同时PMML文件加载速度也比算法库自己独有格式的模型文件加载慢很多。
第二个就是PMML加载得到的模型和算法库自己独有的模型相比,预测会有一点点的偏差,当然这个偏差并不大。比如某一个样本,用sklearn的决策树模型预测为类别1,但是如果我们把这个决策树落盘为一个PMML文件,并用JAVA加载后,继续预测刚才这个样本,有较小的概率出现预测的结果不为类别1.
第三个就是对于超大模型,比如大规模的集成学习模型,比如xgboost, 随机森林,或者tensorflow,生成的PMML文件很容易得到几个G,甚至上T,这时使用PMML文件加载预测速度会非常慢,此时推荐为模型建立一个专有的环境,就没有必要去考虑跨平台了。
此外,对于TensorFlow,不推荐使用PMML的方式来跨平台。可能的方法一是TensorFlow serving,自己搭建预测服务,但是会稍有些复杂。另一个方法就是将模型保存为TensorFlow的模型文件,并用TensorFlow独有的JAVA库加载来做预测。
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库来上线,前者适合于较大的模型和应用场景,后者则适合中小型的模型和应用场景。因此算法工程师使用在产品之前需要做好选择和评估。