今天同事在部署xgboost pmml模型时遇到了大坑,线上spark预测和本地python预测结果怎么都不对应,记录一下处理过程。
看了下同事的代码,貌似也没有问题
from sklearn2pmml import PMMLPipeline
from sklearn2pmml import sklearn2pmml
from xgboost import XGBClassifier
weight = train_y.sum() * 1.0/ (len(train_data) - train_y.sum())
xgb_clf = XGBClassifier(learning_rate=0.1,n_estimators=100,max_depth=3,objective='binary:logistic',seed=1,silent=1,reg_alpha=3,reg_lambda=0.9,scale_pos_weight=1/weight,missing=-9999, eval_metric='auc')
pipeline = PMMLPipeline([('classifier',xgb_clf)])
pipeline.fit(train_x,train_y)
sklearn2pmml(pipeline,'data/a_card_1.pmml',with_repr=True)
首先注意到和之前不同点在于这次缺失值不是nan了,这引起了我的警觉,重新训练了下模型,把样本缺失值处理为np.nan,训练时missing设为默认值None,这时和线上对比发现一致了,果然是missing value的问题。
sklearn2pmml对于xgboost并没有暴露missing这个参数,所以对于missing不为None的童鞋可使用https://github.com/jpmml/jpmml-xgboost 转化。
xgb_clf.get_booster().dump_model('/tmp/a_card_model.dump.txt')
xgb_clf.get_booster().save_model('/tmp/xgb.model')
java -jar target/jpmml-xgboost-executable-1.3-SNAPSHOT.jar --model-input /tmp/xgb.model --fmap-input /tmp/xgb.fmap --pmml-output xgboost_miss.pmml --missing-value -9999
fmap可通过以下方式产生
fmap(feature map file):实现feature id和feature name的对应
格式为 featmap.txt: <featureid> <featurename> <q or i or int>\n
Feature id从0开始直到特征的个数为止,从小到大排列。
i表示是二分类特征
q表示数值变量,如年龄,时间等。q可以缺省
int表示特征为整数(when int is hinted, the decision boundary will be integer)
可根据以下语句通过读取pkl文件的feature_name生成,或者根据feature顺序通过别的方式生成
def ceate_feature_map(file_name,features):
outfile = open(file_name, 'w')
for i, feat in enumerate(features):
outfile.write('{0}\t{1}\tq\n'.format(i, feat))
通过对比PMML可以发现不同点就在于DataField增加了missing配置
<DataDictionary>
<DataField name="_target" optype="categorical" dataType="integer">
<Value value="0"/>
<Value value="1"/>
DataField>
<DataField name="pas_age" optype="continuous" dataType="float">
<Value value="-9999" property="missing"/>
DataField>
<DataField name="last_gulf_call_days" optype="continuous" dataType="float">
<Value value="-9999" property="missing"/>
DataField>
....
DataDictionary>
可以手动在之前的PMLL文件中增加即可解决这个问题。
当然我觉得更好的方式就是使用默认值,即np.nan,对应到spark也就是null,非常自然。
不过没怎么看懂PMML是怎么处理缺失值的,贴一段xgboost原生和PMML对比
booster[0]:
0:[last_30_days_invoice_value<2407.28491] yes=1,no=2,missing=2
1:[last_6_month_finish_count_variation_coefficient<0.61500001] yes=3,no=4,missing=4
3:[last_6_month_fast_finish_order_max_actual_cost<86.4949951] yes=7,no=8,missing=8
7:leaf=-0.0717158243
8:leaf=-0.147665188
4:[last_1_year_taxi_finish_order_actual_cost<505.25] yes=9,no=10,missing=9
9:leaf=-0.0261387583
10:leaf=-0.178924426
2:[app_system_tools_wifi_category_number_rate<0.0645833313] yes=5,no=6,missing=5
5:[last_1_year_night_finish_rate<0.0652500018] yes=11,no=12,missing=12
11:leaf=-0.0177322756
12:leaf=0.0268170126
6:[app_stock_sub_category_number_rate<0.0875959098] yes=13,no=14,missing=13
13:leaf=0.06783209
14:leaf=-0.0312540941
<Segment id="1">
<True/>
<TreeModel functionName="regression" missingValueStrategy="none" noTrueChildStrategy="returnLastPrediction" splitCharacteristic="multiSplit" x-mathContext="float">
<MiningSchema>
<MiningField name="last_6_month_fast_finish_order_max_actual_cost"/>
<MiningField name="last_1_year_night_finish_rate"/>
<MiningField name="last_30_days_invoice_value"/>
<MiningField name="app_stock_sub_category_number_rate"/>
<MiningField name="app_system_tools_wifi_category_number_rate"/>
<MiningField name="last_1_year_taxi_finish_order_actual_cost"/>
<MiningField name="last_6_month_finish_count_variation_coefficient"/>
MiningSchema>
<Node score="0.026817013">
<True/>
<Node score="-0.026138758">
<SimplePredicate field="last_30_days_invoice_value" operator="lessThan" value="2407.285"/>
<Node score="-0.14766519">
<SimplePredicate field="last_6_month_finish_count_variation_coefficient" operator="lessThan" value="0.615"/>
<Node score="-0.071715824">
<SimplePredicate field="last_6_month_fast_finish_order_max_actual_cost" operator="lessThan" value="86.494995"/>
Node>
Node>
<Node score="-0.17892443">
<SimplePredicate field="last_1_year_taxi_finish_order_actual_cost" operator="greaterOrEqual" value="505.25"/>
Node>
Node>
<Node score="0.06783209">
<SimplePredicate field="app_system_tools_wifi_category_number_rate" operator="greaterOrEqual" value="0.06458333"/>
<Node score="-0.031254094">
<SimplePredicate field="app_stock_sub_category_number_rate" operator="greaterOrEqual" value="0.08759591"/>
Node>
Node>
<Node score="-0.017732276">
<SimplePredicate field="last_1_year_night_finish_rate" operator="lessThan" value="0.06525"/>
Node>
Node>
TreeModel>
Segment>
xgboost有明确的当遇到缺失值如何处理说明,但PMML貌似并没有,看出的童鞋麻烦告知我一下,非常感谢。
我们实现了配置化在Spark上部署模型,如一模型部署配置如下
sparkConf:
#spark任务名称, 必填
appName: driverCCardPMML
#是否启用hive支持
enableHiveSupport: true
#spark其它配置选项,如内存,shffle partitions数量等
appConf:
#debug开启时,每个节点会做持久化
debug: true
#持久化数量,0代表全量
limit: 10
savePath: /user/fbi/model_deploy/
sourcePath: /user/fbi/model_source/
#一个子节点只有一个父节点,所以树更合适
tree:
#节点描述
desc: C卡PMML
#名称,用于标识一个组件
name: model_pmml
#传递给组件的参数,包括模型超参数,以及配置参数等
parameters:
#pmml文件路径,暂只支持本地文件,spark-submit可通过--files glm1.pmml上传
pmmlPath: zkc_driver_ccard_v1.1.pmml
#是否排除原始列,默认false(保留)
excludeOriginColumn: true
#排除例外,如uid等
excludeExcept: ["uid"]
#子节点合并所有结果,如果children只有一个,可省略joinType,joinKey
#join类型,full(默认), inner, left, right
children:
- desc: 加载C卡原数据
name: data_source
parameters:
#支持hql, hql_file json
type: hql_file
path: datasource.sql
#方便模型校验,可配置saveTable,将负责数据源落库,将会保存到/user/fbi/model_source/year/month/day/model_driver_c_card_source.parquet
saveTable: model_driver_c_card_source
transformer:
- desc: 数据类型转换
name: feature_data_type
parameters:
#原始数据类型 tinyint,smallint, int, bigint, float, double,string, decimal
originalType: ["decimal"]
#目标数据类型
targetType: double
#排除的列名,可省略
#exceptColumn: []
#transformer节点,pipeline模式
transformer:
- desc: 落库
name: data_sink
parameters:
#是否自动建表
auto: true
path: /user/fbi/
db: riskmanage_dm
table: model_driver_c_card_v1
tableName: 模型-司机-C卡-v1
最近也在反思是否有更好的离线部署方式,如DSL,比如通过spark-sql可以完全实现上面的处理流程,当然需要稍微扩展下spark-sql语法,是否值得尝试?
大家对离线模型都是如何部署的,欢迎交流。