jpmml / jpmml-transpiler

Java Transpiler (Translator + Compiler) API for PMML
GNU Affero General Public License v3.0
28 stars 2 forks source link

UnsupportedOperationException when following the README #13

Closed thu-zxs closed 3 years ago

thu-zxs commented 3 years ago

When following the README instructions:

InputStream is = this.getClass().getResourceAsStream("/" + pmmlFilePath);
try {
    LoadingModelEvaluatorBuilder evaluatorBuilder = new LoadingModelEvaluatorBuilder().load(is);
    Transpiler transpiler = new InMemoryTranspiler("com.MyCompany.XGBModel");
    evaluatorBuilder = evaluatorBuilder.transform(new TranspilerTransformer(transpiler));
    this.evaluator = evaluatorBuilder.build();
} catch (Exception e) {
    throw new RuntimeException("Failed to Load PMML file from path " + pmmlFilePath, e);
}

Getting this error:

Caused by: java.lang.UnsupportedOperationException
    at org.jpmml.model.collections.AbstractFixedSizeList$1.remove(AbstractFixedSizeList.java:105)
    at java.util.AbstractCollection.clear(AbstractCollection.java:436)
    at org.jpmml.translator.PMMLObjectUtil.createExpression(PMMLObjectUtil.java:371)
    at org.jpmml.translator.PMMLObjectUtil.initializeArray(PMMLObjectUtil.java:579)
    at org.jpmml.translator.PMMLObjectUtil.initializeArray(PMMLObjectUtil.java:570)
    at org.jpmml.translator.PMMLObjectUtil.addValueConstructorParam(PMMLObjectUtil.java:514)
    at org.jpmml.translator.Template.constructObject(Template.java:104)
    at org.jpmml.translator.PMMLObjectUtil.constructObject(PMMLObjectUtil.java:227)
    at org.jpmml.translator.PMMLObjectUtil.createObject(PMMLObjectUtil.java:216)
    at org.jpmml.translator.PMMLObjectUtil.createBuilderMethod(PMMLObjectUtil.java:168)
    at org.jpmml.translator.PMMLObjectUtil.createExpression(PMMLObjectUtil.java:331)
    at org.jpmml.translator.PMMLObjectUtil.addValueConstructorParam(PMMLObjectUtil.java:520)
    at org.jpmml.translator.Template.constructObject(Template.java:104)
    at org.jpmml.translator.PMMLObjectUtil.constructObject(PMMLObjectUtil.java:227)
    at org.jpmml.translator.PMMLObjectUtil.createDefaultConstructor(PMMLObjectUtil.java:140)
    at org.jpmml.transpiler.TranspilerUtil.translate(TranspilerUtil.java:62)
    at org.jpmml.transpiler.Transpiler.translate(Transpiler.java:45)
    at org.jpmml.transpiler.InMemoryTranspiler.transpile(InMemoryTranspiler.java:36)
    at org.jpmml.transpiler.TranspilerTransformer.apply(TranspilerTransformer.java:40)
    at org.jpmml.evaluator.LoadingModelEvaluatorBuilder.transform(LoadingModelEvaluatorBuilder.java:142)
    at com.MyCompany.XgbTranspliedPredictor.initEvaluator(XgbTranspliedPredictor.java:107)
    ... 4 more

Debugging leads me to PMMLObjectUtil.class:

if(field instanceof HasDiscreteDomain){
    HasDiscreteDomain<?> hasDiscreteDomain = (HasDiscreteDomain<?>)field;

    if(hasDiscreteDomain.hasValues()){
        List<Value> pmmlValues = hasDiscreteDomain.getValues();

        for(Value pmmlValue : pmmlValues){
            String displayName = pmmlValue.getDisplayValue();

            if(displayName != null || pmmlValue.hasExtensions()){
                discreteValues.clear();

                break discreteValues;
            }

            discreteValues.put(pmmlValue.getProperty(), pmmlValue.getValue());
        }

        suppressedPmmlValues = new ArrayList<>(pmmlValues);

        pmmlValues.clear();
    }
}

From the line pmmlValues.clear(); I wonder why a java.util.List's clear() method could lead to a AbstractFixedSizeList's unsupported remove method? How to resolve this issue? Thanks for your help!

vruusmann commented 3 years ago

I wonder why a java.util.List's clear() method could lead to a AbstractFixedSizeList's unsupported remove method?

The underlying JPMML-Model library performs java.util.List type optimization, where the type of singleton, doubleton and tripleton lists are changed to org.jpmml.model.collections.SingletonList, o.j.m.c.DoubletonList and o.j.m.c.TripletonList, respectively.

This optimization is implemented as the org.jpmml.model.visitors.ArrayListTransformer Visitor class: https://github.com/jpmml/jpmml-model/blob/1.5.15/pmml-model/src/main/java/org/jpmml/model/visitors/ArrayListTransformer.java

This Visitor class is a member of the org.jpmml.evaluator.visitors.ElementFinalizerBattery Visitor Battery class: https://github.com/jpmml/jpmml-evaluator/blob/1.5.15/pmml-evaluator/src/main/java/org/jpmml/evaluator/visitors/ElementFinalizerBattery.java#L28

TLDR: This List type optimization is applied automatically during org.jpmml.evaluator.LoadingModelEvaluatorBuilder#load method: https://github.com/jpmml/jpmml-evaluator/blob/1.5.15/pmml-evaluator/src/main/java/org/jpmml/evaluator/LoadingModelEvaluatorBuilder.java#L62 https://github.com/jpmml/jpmml-evaluator/blob/1.5.15/pmml-evaluator/src/main/java/org/jpmml/evaluator/LoadingModelEvaluatorBuilder.java#L120-L122

How to resolve this issue?

Simply replace the default Visitor Battery instance with a custom one, which does not include the o.j.m.visitors.ArrayListTransformer Visitor class.

For example:

VisitorBattery customVisitorBattery = new org.jpmml.evaluator.visitors.DefaultModelEvaluatorBattery();
// THIS!
customVisitorBattery.remove(org.jpmml.model.visitors.ArrayListTransformer.class);

Evaluator evaluator = new LoadingModelEvaluatorBuilder()
  .setVisitors(customVisitorBattery)
  .load(is)
  .build();

Alternatively:

Evaluator evaluator = new LoadingModelEvaluatorBuilder()
  .setVisitors(null)
  .load(is)
  .build();
thu-zxs commented 3 years ago

Thanks for your help! I manage to transpile a XGBoost Model by following your instruction.

But I am getting this error when doing evaluation

Caused by: java.lang.IllegalArgumentException
    at org.jpmml.evaluator.regression.RegressionModelUtil.normalizeBinaryLogisticClassificationResult(RegressionModelUtil.java:198)
    at org.jpmml.evaluator.regression.RegressionModelUtil.computeBinomialProbabilities(RegressionModelUtil.java:46)
    at MyCompany.XgbModel$JavaModel$108982313.evaluateRegressionTableList$1250816994(XgbModel.java:12752)
    at MyCompany.XgbModel$JavaModel$108982313.evaluateClassification(XgbModel.java:12770)
    at org.jpmml.evaluator.java.JavaModelEvaluator.evaluateClassification(JavaModelEvaluator.java:59)
    at org.jpmml.evaluator.ModelEvaluator.evaluateInternal(ModelEvaluator.java:449)
    at org.jpmml.evaluator.mining.MiningModelEvaluator.evaluateSegmentation(MiningModelEvaluator.java:539)
    at org.jpmml.evaluator.mining.MiningModelEvaluator.evaluateClassification(MiningModelEvaluator.java:304)
    at org.jpmml.evaluator.ModelEvaluator.evaluateInternal(ModelEvaluator.java:449)
    at org.jpmml.evaluator.mining.MiningModelEvaluator.evaluateInternal(MiningModelEvaluator.java:237)
    at org.jpmml.evaluator.ModelEvaluator.evaluate(ModelEvaluator.java:302)

Which is from this line of code Map<FieldName, ?> results = evaluator.evaluate(arguments);

I am doing just fine for the same evaluation code when using the untranspiled .pmml model. Could you please help?

vruusmann commented 3 years ago

@thu-zxs I've extracted your last comment into a separate issue: https://github.com/jpmml/jpmml-transpiler/issues/14

In brief, you are using a binary classification model, which probably specifies RegressionModel@normalizationMethod="softmax"?