玩转jpmml之tpot+sklearn2pmml自动化机器学习集成模型部署

前言

应该是首发原创,网上没搜到。
总体的逻辑是:数据导入----tpot自动化机器学习挑选最适合的模型和数据预处理思路----转换为sklearn代码----通过sklearn2pmml库转换为pmml模型----通过jpmml库调用pmml模型实现在java中部署。
好像看起来很简单,但是实际处理中问题不少,且在外网上甚至搜不到相关答案,可以说完全自己摸索出来的路径,在这里给后来人借鉴宝贵经验。
注:本章中的模型为stacking三层模型,因此和一般的机器学习模型转换存在相当大的差异。

实验

sklearn2pmml,tpot库都用最新的。
jpmml库版本为1.5.15,官网现在最新为1.6,输入方式略有不同,主要体现为去掉了fieldname和fieldvalue这种调用方式,使用时需注意,怎么安装不说了看以前的文章,主要是pom.xml文件里写相关依赖,然后通过maven仓库一键拉下来。

tpot部分

from sklearn import preprocessing
from sklearn.model_selection import train_test_split
from tpot import TPOTClassifier
le = preprocessing.LabelEncoder() #标签列转换工具
x = df.iloc[:,1:17] #特征列
y = le.fit_transform(df.iloc[:,17]) #转换标签 YES-->0,NO-->1
X_train, X_test, y_train, y_test = train_test_split(x, y,
                                                    train_size=0.75, test_size=0.25, random_state=1)
                                                    
tpot = TPOTClassifier(generations=100, population_size=100, verbosity=2,n_jobs=2,random_state=42,warm_start=True)
tpot.fit(X_train,y_train)#拟合训练集
print(tpot.score(X_test,y_test))#给测试集打分
print(tpot.score(x,y))#给整个数据集打分

tpot的用法和sklearn大同小异,这里着重讲一下TPOTClassifier的API重点。
population_size + generations × offspring_size等于总共寻找的pipeline个数(默认情况下offspring_size的默认值等于population_size),这里等于训练10100个不同的pipeline,在我的办公电脑上大概要跑一个小时,基本可以找到最优解。
verbosity是用来输出信息的,训练过程中可以看见一个进度条,如果调成0,那啥都不会输出,如果调成3,会输出中间的一切信息,一般用2就足够了。
n_jobs可以简单理解为你要用电脑cpu的几个核心进行并行计算,我这里写2是因为这办公电脑只有两个核心,如果算力足够往上加就完事了,或者写成-1,可以尽量多的占用你电脑核心的算力
random_state不用说了吧,为了固定结果,随机状态固定是必要的,特别是跑一万个pipeline。
warm_start是重点,如果你中途想暂停(指中断一下kernel的计算)看结果然后再继续跑,warm_start=True是必要的,不然又会从头开始训练,得不偿失。

模型转换

比较令人遗憾的是tpot的优化用的是cv验证,而不是用指定的测试集去验证,当然一般来说cv准确率上去了,测试集准确率不会差的。那么在经过一代代的优化之后,我们得到了最终的模型(一般会比较复杂),那么是否可以直接转化为pmml模型呢?

from sklearn2pmml import sklearn2pmml, make_pmml_pipeline
pmml_pipeline = make_pmml_pipeline(classifier.fitted_pipeline_, active_fields = iris.feature_names, target_fields = ["species"])

sklearn2pmml(pmml_pipeline, "TPOTIris.pmml", with_repr = True)

这是官方给出的转换tpot模型的代码,你只需要把classifier改成tpot(你训练的tpot模型的名字),并指定好active_fields(指特征列名字)和target_fields(指标签列名字)即可(或者你也可以不搞这两个参数,默认会识别为x1—xn和y)。

很遗憾,一般来说会转换错误。

我们先来看看tpot给出的到底是什么模型:

tpot.export('tpot_digits_pipeline.py')

在当前目录下找到并打开py文件,并摘出核心内容:

exported_pipeline = make_pipeline(
    StackingEstimator(estimator=XGBClassifier(learning_rate=0.5, max_depth=9, min_child_weight=1, n_estimators=100, n_jobs=1, subsample=0.7500000000000001, verbosity=0)),
    StackingEstimator(estimator=DecisionTreeClassifier(criterion="gini", max_depth=10, min_samples_leaf=19, min_samples_split=12)),
    KNeighborsClassifier(n_neighbors=3, p=2, weights="distance")
)

make_pipeline是sklearn库中的方法,StackingEstimator怎么tpot.builtins 里面的方法?(夹带私货)
先不说这个,这个pmml模型竟然不支持KNeighborsClassifier里面的distance参数!ok,我们把distance改成uniform之后准确率成功下降了0.03%,并再次尝试导出,会发现还是报错。

Name(s) [probability(stack(x1, x2, .., x14, x15), 0)] do not match any fields

问题就在于这个StackingEstimator,官方说是支持,实际做的兼容性可真烂,我尝试了各种构建pipeline的方法,一开始还以为是嵌套的问题,结果就是这个tpot的类兼容太差了!

解决方法:使用sklearn自己的StackingClassifier取代tpot的StackingEstimator

像上面的模型转换之后应该是下面这样:

from sklearn.ensemble import StackingClassifier
estimators = [
    ('GB', GradientBoostingClassifier(learning_rate=1.0, max_depth=8, max_features=0.35000000000000003, min_samples_leaf=9, min_samples_split=6, n_estimators=100, subsample=0.6500000000000001,random_state=42)),
    ('svr', DecisionTreeClassifier(criterion="gini", max_depth=9, min_samples_leaf=1, min_samples_split=2,random_state=42))
]
clf = StackingClassifier(
    estimators=estimators, final_estimator=KNeighborsClassifier(n_neighbors=3, p=2, weights="uniform")
)

再进行sklearn2pmml的pipeline构建和导出:

pipeline = PMMLPipeline([
  ("classifier", clf)
])
pipeline.fit(X_train,y_train) #千万不要忘记训练过程
sklearn2pmml(pipeline, "xxx.pmml",debug=True,with_repr = True)

就完成了!

JPMML部分
先讲重点,因为是集成模型,stack堆叠有三层,所以特征名转换会混乱,不能用我们上次文章里面使用的map方式传参,你会发现结果完全不对!这个问题困扰了我两天,各种调试都没找到原因,最后采用数组形式传参才发现根本问题

直接上代码:

import org.dmg.pmml.FieldName;
import org.dmg.pmml.PMML;
import org.jpmml.evaluator.*;
import java.io.*;
import java.util.*;

import static org.jpmml.evaluator.example.Example.newInstance;


public class TestPmml {
    private static String modelEvaluatorFactoryClazz = ModelEvaluatorFactory.class.getName();
    private static String valueFactoryFactoryClazz = ValueFactoryFactory.class.getName();
    public static void main(String args[]) throws Exception {
        TestPmml obj = new TestPmml();
        PMML pmml;
        File file = new File("xxx.pmml");
        InputStream inputStream = new FileInputStream(file);
        pmml = org.jpmml.model.PMMLUtil.unmarshal(inputStream);
        ModelEvaluatorBuilder modelEvaluatorBuilder = new ModelEvaluatorBuilder(pmml).setOutputFilter(OutputFilters.KEEP_ALL)
                .setModelEvaluatorFactory((ModelEvaluatorFactory)newInstance(modelEvaluatorFactoryClazz))
                .setValueFactoryFactory((ValueFactoryFactory)newInstance(valueFactoryFactoryClazz));
        Evaluator evaluator = modelEvaluatorBuilder.build();
        Map<FieldName, Object> arguments = new HashMap<>();
        List<InputField> inputFields = evaluator.getInputFields();
        System.out.println("Input fields: " + inputFields);
        double aa[]={81.48,78.38,26.63,18.53,48.47,4.6,0.58,22.37,37.38,21.12,76.93,43.02,34.28,26.84,31.08};
        int i = 0;
        for(InputField inputField : inputFields){
            FieldName inputName = inputField.getName();

            FieldValue inputValue = inputField.prepare(aa[i]);

            arguments.put(inputName, inputValue);
            i++;
        }
        evaluator.verify();
        Map<FieldName, ?> results = evaluator.evaluate(arguments);
        System.out.println("result fields: " + results);
    }
}

和上次文章相比会看起来臃肿很多,但其实抓住evaluator这个重点就行了,我们是用它进行预测,变化的无非是evaluator的初始化方式,在调试过程中,我试了各种新的老的evaluator的初始化方式都发现没有任何变化。

重点在于double开始的数组传入部分,先说一下inputfields,你可以像我一样把它打印出来看看模型的输入是什么,而它的顺序也就是数组输入的正确顺序,传参数时要按照inputfields里面的特征名一个个顺序传入,特别是在集成模型中,再次强调不能采用map形式。将这些参数传入arguments后,我们就可以使用evaluator对arguments进行evaluate(评估,打分),也就能得到我们要的结果,在这个示例中,结果存在results里,还有其他的方式,新的老的都在官网里写着,大家可以试试。

以上只是示例,是对单个进行打分预测,至于批量打分预测,你可以写一个读csv的类,一行行读,或者你要验证结果,可以采用官网提供的命令行调用jar预测

java -cp target/pmml-evaluator-example-executable-1.6-SNAPSHOT.jar org.jpmml.evaluator.example.TestingExample --model model.pmml --input input.csv --expected-output expected-output.csv

玩转jpmml之tpot+sklearn2pmml自动化机器学习集成模型部署
注意文件名,model.pmml改成你的pmml文件名,input和output的csv文件也是,如果这个打分结果和你的python预测不一致,那是jpmml库计算错误,你可以去github上提issue。如果你只是代码写出来有问题,用命令行调用jar和你的python预测是一致的,那是你的代码问题。

总结

经过这几天的调试总算是对jpmml库有了比较深的了解,很多网上查不到的疑难杂症都要自己解决,可以说差点把我整抑郁了。之前也在找其他模型部署的方式,结果一轮看下来都不是特别满意,我需要一个真正傻瓜式的一键部署的懒人包,而不是*去学习服务器网络知识和我不熟悉的java知识。希望国内能有这样的开源框架吧。

上一篇:pipeline的优点


下一篇:一文搞懂 Netty 的整体流程,还有谁不会?