From 433ce1cdc35c0a21ecb02d8c95ec1769448f17ca Mon Sep 17 00:00:00 2001 From: Villu Ruusmann Date: Thu, 27 Apr 2023 11:50:00 +0300 Subject: [PATCH] Added 'input_float' model transformation option. Fixes #128 --- .../jpmml/sparkml/xgboost/BoosterUtil.java | 40 ++++++++++++++++++ .../xgboost/HasSparkMLXGBoostOptions.java | 2 + .../sparkml/xgboost/testing/XGBoostTest.java | 4 ++ .../resources/pipeline/XGBoostHousing.zip | Bin 120493 -> 120495 bytes .../test/resources/pipeline/XGBoostIris.zip | Bin 12716 -> 12718 bytes 5 files changed, 46 insertions(+) diff --git a/pmml-sparkml-xgboost/src/main/java/org/jpmml/sparkml/xgboost/BoosterUtil.java b/pmml-sparkml-xgboost/src/main/java/org/jpmml/sparkml/xgboost/BoosterUtil.java index b7149fc4..faff9fba 100644 --- a/pmml-sparkml-xgboost/src/main/java/org/jpmml/sparkml/xgboost/BoosterUtil.java +++ b/pmml-sparkml-xgboost/src/main/java/org/jpmml/sparkml/xgboost/BoosterUtil.java @@ -23,12 +23,17 @@ import java.io.InputStream; import java.util.LinkedHashMap; import java.util.Map; +import java.util.function.Function; import ml.dmlc.xgboost4j.scala.Booster; import ml.dmlc.xgboost4j.scala.spark.params.GeneralParams; import org.apache.spark.ml.Model; import org.apache.spark.ml.param.shared.HasPredictionCol; +import org.dmg.pmml.DataType; +import org.dmg.pmml.Field; import org.dmg.pmml.mining.MiningModel; +import org.jpmml.converter.ContinuousFeature; +import org.jpmml.converter.Feature; import org.jpmml.converter.Schema; import org.jpmml.sparkml.ModelConverter; import org.jpmml.xgboost.HasXGBoostOptions; @@ -60,6 +65,41 @@ public & HasPredictionCol & GeneralParams, C extends ModelCo throw new RuntimeException(ioe); } + Boolean inputFloat = (Boolean)converter.getOption(HasSparkMLXGBoostOptions.OPTION_INPUT_FLOAT, null); + if((Boolean.TRUE).equals(inputFloat)){ + Function function = new Function(){ + + @Override + public Feature apply(Feature feature){ + + if(feature instanceof ContinuousFeature){ + ContinuousFeature continuousFeature = (ContinuousFeature)feature; + + DataType dataType = continuousFeature.getDataType(); + switch(dataType){ + case INTEGER: + case FLOAT: + break; + case DOUBLE: + { + Field field = continuousFeature.getField(); + + field.setDataType(DataType.FLOAT); + + return new ContinuousFeature(continuousFeature.getEncoder(), field); + } + default: + break; + } + } + + return feature; + } + }; + + schema = schema.toTransformedSchema(function); + } + Float missing = model.getMissing(); if(missing.isNaN()){ missing = null; diff --git a/pmml-sparkml-xgboost/src/main/java/org/jpmml/sparkml/xgboost/HasSparkMLXGBoostOptions.java b/pmml-sparkml-xgboost/src/main/java/org/jpmml/sparkml/xgboost/HasSparkMLXGBoostOptions.java index 375e066f..074a866b 100644 --- a/pmml-sparkml-xgboost/src/main/java/org/jpmml/sparkml/xgboost/HasSparkMLXGBoostOptions.java +++ b/pmml-sparkml-xgboost/src/main/java/org/jpmml/sparkml/xgboost/HasSparkMLXGBoostOptions.java @@ -22,4 +22,6 @@ import org.jpmml.xgboost.HasXGBoostOptions; public interface HasSparkMLXGBoostOptions extends HasSparkMLOptions, HasXGBoostOptions { + + String OPTION_INPUT_FLOAT = "input_float"; } \ No newline at end of file diff --git a/pmml-sparkml-xgboost/src/test/java/org/jpmml/sparkml/xgboost/testing/XGBoostTest.java b/pmml-sparkml-xgboost/src/test/java/org/jpmml/sparkml/xgboost/testing/XGBoostTest.java index df70ed2c..8d3ed467 100644 --- a/pmml-sparkml-xgboost/src/test/java/org/jpmml/sparkml/xgboost/testing/XGBoostTest.java +++ b/pmml-sparkml-xgboost/src/test/java/org/jpmml/sparkml/xgboost/testing/XGBoostTest.java @@ -35,6 +35,7 @@ import org.jpmml.model.visitors.AbstractVisitor; import org.jpmml.sparkml.testing.SparkMLEncoderBatch; import org.jpmml.sparkml.testing.SparkMLEncoderBatchTest; +import org.jpmml.sparkml.xgboost.HasSparkMLXGBoostOptions; import org.jpmml.xgboost.HasXGBoostOptions; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -61,7 +62,10 @@ public XGBoostTest getArchiveBatchTest(){ public List> getOptionsMatrix(){ Map options = new LinkedHashMap<>(); + options.put(HasSparkMLXGBoostOptions.OPTION_INPUT_FLOAT, new Boolean[]{false, true}); + options.put(HasXGBoostOptions.OPTION_COMPACT, new Boolean[]{false, true}); + options.put(HasXGBoostOptions.OPTION_PRUNE, false); return OptionsUtil.generateOptionsMatrix(options); } diff --git a/pmml-sparkml-xgboost/src/test/resources/pipeline/XGBoostHousing.zip b/pmml-sparkml-xgboost/src/test/resources/pipeline/XGBoostHousing.zip index 94262f846c37ccdcc45fac2f43c526854a23a927..6326961e317d1d3f5a244615a0381be9a5d0aa77 100644 GIT binary patch delta 3905 zcmb_e2{=@H8+Wd4rif711|ccSj4@-26uKdXESHKg7!_G!vej+ExFthKo#H0jm98w^ zimoO5QpP&c_PHtITC$F&BEEB`zH7$yeBbju-}ir>^PKaX_y2p}|Ns5HTTzj4Ns+Lq zy$!#B6dwYC;QMqqBSlnEgiJO>>l^sseZ9T0U}guPeAlf3!4C;9Dbd(&bqf3l9>J>ud(VE=?7{eODFOL9Tm7qu~pv@7v)vOksV*;|^ zjT!)<8R0wk!`$-ushH(3MwAGjy^S!UzQ9XE8nP+{Jv>(eyC6kWZu2?;WD3cLL?P(} zlR_w3Xm~k0wjGqmh*ynZDfq+wBF!`1o(G+pqU*Kyze`PjniL*4@5-81ky_47x!*i+ z`Ijy86gI00qg`i+CtGiRkXrRz{|csqSS6JdORGQSsoylEcrRk%LZamet$V#=t`C)e zIBxIAo>M1`6bKzU>e9I+HxG9a*Qk#)yo=;g!S;kylz;;>zT$b!h1YF`Rwf>yra3xJ zVtxRP)l9JuEuAf8<&V<>QMO&UZcSvkN`<_C0i91+GxaP!EcV08V~IZ-M4k3zwgInl zis+J=cIGkVuIne`+aIJ@yrJa(QAM?UG)nJUvO9hMaMnd^)R6diuk%9R)?OqsPp^`= zZ+VY@V4^!B#6tf>h0thlt<*$N>6}(Zk}xBtg>WLa;ZvsEvE=nm+a%r-4QK7&j&&)Y zTz}|JqH}f4w&a3mW~N88hZ=$pHorcH>zhE)rG8F!-STRZX1>`fOpE1SZMmpHO`CpU zrtC0MBjM!SaNykdPRa05(&)_o>%t?6>FPF=WL>{sX;ivnol)?S6l^grl_7hBn5a(M z$~v{v=Lrla$GMHLnU9z|^3cs)2|>`|1V+=Gn=wFl+w^-LzeRCn-K2`Za6&f^PfIi)!F;MGScOk z#-qJ`u_PRZ?5zWeJpu5XOfzc$lvl|%wgMU)Svn-!%Y)-2j`m9CXrXc523~kyw3fb? zK2}SQ?5(dw^78W0LL2yyNV>Wt3=WF}JGucGJsWLnUr$d@uWMnGUely0=N47+(cSRb zg%RPV<*{q0uT>+Uyyx#H_%9DoUf2k@15}H=4ppVOYc2Rmm&U%By25mZQ1e(}1V(H} zLE`nsTk)j==Ui&13>GsBBZ>saQ`DB4j?acL&akA1^RoO?&`VDSUn_{;U-CZxTQ+sL zCua#qF%m>9-Dn^hWF6Cbo_zqlSavQlPwt}bG~s}*YN>*6mHM#YeC!{!jF#;W3ID#X9;< zdnyi|MoAs`(LN$&T;i1=8GH!fSZN-wE9DaDNZT??$xN$7Q`r~^qf-W*itLGC%u87d z%~;v#T}`>s(y5u!yUw3!`8_F6Zx2=d8u^g4Bjd`vA1&?HU}oU_m)Qu!yI zlcI_pw@nmpm5=yh zH_TnMP=Dz)C=UGux#RHTs+NF-;t-5$0mMbP$pksA0NM+eX_-wJVi?)m$|r)zvxn0Y z;5{a-fHE)qub!Y1!8r4$5hs>LbsqL?fUJq|v;r7k4B@HVs@G}-0hb7c^=R?NsVVP? z-)X(32~nG9C|daJp{qWHZ{87Js2)r{8*aYIVfrD15#$?`rP+)yMx5DfkW-O-L1nfk z*hPe900_39)A4)eG3+19&F^zo4BmYIha0?_=c5_sM9U{M0(xW5ZmY zw5N|YH9YFS5joXfB`LKoFO*{0`igft$yK#TioLR38)6m4NaNu-dZ8z;wDQbE7#H$vLcc$#s zmg^wz%yBXu_)ZEM|GrJKAh`q3`r9n4#c66Ct3{WskqVGPUIn_Hs}(@CN*eU&1w=Sn zQvN0{?gdatcogOgqu@*@ltbKuCkSPU+Aeqlck9-M4!^P*Yk+?@IY&MPly<`!Eb~Ve z*Fi7@^uR5CWwq7+rD4r-ssjdh0w_gT2@HY?H)rkm_Wt_P1(mx2N#0s()(yyUVBF2x zj|WEQ!2VAIgDS@ucXuK47&8Vp_5hOq`)mM`djKU?zz`ho1!Q@*{dF1cvTw|z{>BA+ Q0i*kXZ9=XcP&wxNC!kH%A^-pY delta 3932 zcmbVO2{e>@8~3IRWuypY--$8S8D_{*vPYC92{Q)SLexZGgVepHy4GI9XHT+a$#y^c zQiDPEDB6qa-iy*5A${*V_1!yzQ{TD&bKY~_bDs78|2@z1d&)`?rKJd@ofYRsK@M(i zZjP|3Z8Djt2C9GR)J5bP~Oe9hgl6@J!F2n*d29t4lQaqz6DTWiHKYWKz9=l!~)h zCXL)bTy`$=%sU|tJ1YeDN4ZD>fXfA%41K?p^ups zw&*2nDNVF2*3=8wEHj196mW`(>Co}0?JAoYDz9(3YC0uH)jNEv!-zsI95~Z|we!xL zSE{#DUYvHX8X`Ax;b+W*_wvC+1!Y5D(p{BI!WY3Ee2Qe91L#VXJI4; zbS9&!k=9YBQ1ke0j>@o#~1WQB&xKi$>NavPF-Q1rj`2O^>a9 zI%^lzPO<=$Y{snGwT-%(6}=x5>Tb&Wcw(X>`16f~cUa5U&8kw6>P;o!vh~ZnXSH*& ziZP@8S$?|K@@85MVs}jJ4=&A*MVqCMa%aA(^CwXD@vLBi^ec-F7;AfveiF_IaUGv< z_FI&8GvA`{YfBL-oq)sJ#eofh}0 zce_*css81BjgzPK$)s{l-3M9*?u|ry&(4I8B6C8j?-yDtwx_<>txa`v%{9b6sfyro z6bCcY6m%_13y&hiSAMmXG((RWE0A~M2)F$Ui)=qmO`kBCr0m`Y1XNz=>D|KhTHi?Tjnj^MvmAe9f69T~+H*-WjZ>QZ*=dWzjm{&PNF~#yEU$BU z`L2~89Eu|+`4Y_XPn>XH0pOy~9XClV-N34YVB;m{v-1D`f z1Hx69jK;JUr}c2%ds|HAu{93t zk=tVvE6-82;T-K5ZQ9AmS`U?XM^exGGuFS+T>hZ_V1DuOh_9EmZzU>i*7`Z4i$A%)u>H(RozG8k1AS72 zojp-g$`Z(i`1yvXdN1*P;bD0^I7+MP(0@`1w@aL4Z|I9N!ir}=+Fm86-XfAC{oSeI zlNp-FwKIb99tDin{Eqv5^j&otailaN=?~|On~jeqsssG+Bgk;~y}s}FTB(2GT|2q{bMZZnoD@82`Vlb8l@bj`F`jrb{u%THk0>;q*o>le8Bm`JNzjIBk!h|3j zk5viaWL8qLf$9t&6o!|L2w>Y%$d}~>tYB-k`6tEF2KZP^EbYS-(#c;P~;`) zVYM)NSRD;424Z}elVQ>RZa@*qD*DFBkSqZy0_xF$t*gXL6qFW&D}|XTL~Xjly*>)i zBB4lObG;r4w)CL$>ii81tv|1*DxqR0j@~+Q`R$;oZQx4Og3BMNbg#BlI`*EfmDAbAvYmtB?{U2k(vK(FG@Om_ zvL3wj_%*d>2-z&L(C}L*wS8*Z$P^)#Jb4UrJS;peH37QH=lGxOO8+-XtkKRdA(P@vHL zpwc#Fe7@o$D(!MfVmy4@R4V+aaUT9jnTBL=wuHV8sn;_l01}2!b21ljXbLmW?!Uu9e_HK7ax!oq*hzt%u#&MKo0F?Qa`KP10~8V3z|~F$ zc?JiAj|RRa!AdA3wj}gBesGgjyZ!&Qp|Ip|2AmiGxWULls083)czF;ImGYr|KPWsq zzm*Ma^A5Hd{E-b@N=(>TNlSZibaBh5>O&SdVpX mim>T%fCi(0B2x*TJgt!M1M~^7DIEgOj{*u@j(yO?=J+oksJoN^ diff --git a/pmml-sparkml-xgboost/src/test/resources/pipeline/XGBoostIris.zip b/pmml-sparkml-xgboost/src/test/resources/pipeline/XGBoostIris.zip index d4177bf41c95c9d451964199704b17d8333f61de..24f2451b10535a85856abd49c8f340c239ba7349 100644 GIT binary patch delta 3290 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 F`V(kIu8sfz delta 3324 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