autodeployai / pmml4s

PMML scoring library for Scala
https://www.pmml4s.org/
Apache License 2.0
58 stars 9 forks source link

PMML predictions are Nulls and NaNs when model is executed using Java multple threads concurrently #14

Closed wjtdc closed 3 years ago

wjtdc commented 3 years ago

Teradata (https://www.teradata.com/) team is using the following PMML4S library (java) to develop prediction function.

org.pmml4s pmml4s_2.13 0.9.9
The binomial LogisticRegression model is created using python code. The generated model gives correct prediction in python (single thread). However our Java function which we use for prediction is multi-threaded application which takes PMML model as an input and performs prediction. The generated results are not deterministic and reports nulls and NaNs

for probabilities:

` sn prediction json_report


    469 eclectic                  {"predicted_homestyle":"eclectic","probability_eclectic":0.9614659141113676,"probability_classic":0.038534085888
     38 eclectic                  {"predicted_homestyle":"eclectic","probability_eclectic":0.99331616184476,"probability_classic":0.00668383815523
    364 eclectic                  {"predicted_homestyle":"eclectic","probability_eclectic":0.9991498316385669,"probability_classic":8.501683614331
    301 eclectic                  {"predicted_homestyle":"eclectic","probability_eclectic":0.7699433795922794,"probability_classic":0.230056620407
    198 classic                   {"predicted_homestyle":"classic","probability_eclectic":0.02490070485694777,"probability_classic":0.975099295143
    463 classic                   {"predicted_homestyle":"classic","probability_eclectic":0.3820760227574927,"probability_classic":0.6179239772425
    255 null                      {"probability_eclectic":"NaN","probability_classic":"NaN"}
    177 eclectic                  {"predicted_homestyle":"eclectic","probability_eclectic":0.9963200259179247,"probability_classic":0.003679974082
    234 null                      {"probability_eclectic":"NaN","probability_classic":"NaN"}
     13 classic                   {"predicted_homestyle":"classic","probability_eclectic":2.7544847406742705E-4,"probability_classic":0.9997245515
    274 eclectic                  {"predicted_homestyle":"eclectic","probability_eclectic":0.9975884676149918,"probability_classic":0.002411532385
     53 eclectic                  {"predicted_homestyle":"eclectic","probability_eclectic":0.9985451668365372,"probability_classic":0.001454833163
    472 null                      {"probability_eclectic":"NaN","probability_classic":"NaN"}
    440 eclectic                  {"predicted_homestyle":"eclectic","probability_eclectic":0.9971546420765025,"probability_classic":0.002845357923
    306 eclectic                  {"predicted_homestyle":"eclectic","probability_eclectic":0.9895191759633697,"probability_classic":0.010480824036
    459 classic                   {"predicted_homestyle":"classic","probability_eclectic":0.07978961320056205,"probability_classic":0.920210386799
    224 null                      {"probability_eclectic":"NaN","probability_classic":"NaN"}
    251 classic                   {"predicted_homestyle":"classic","probability_eclectic":0.38902512064872086,"probability_classic":0.610974879351
    403 null                      {"probability_eclectic":"NaN","probability_classic":"NaN"}
    249 classic                   {"predicted_homestyle":"classic","probability_eclectic":0.09941208499824894,"probability_classic":0.900587915001
    260 classic                   {"predicted_homestyle":"classic","probability_eclectic":0.03562546026599011,"probability_classic":0.964374539734
    411 eclectic                  {"predicted_homestyle":"eclectic","probability_eclectic":0.9999977879199696,"probability_classic":2.212080030372
    239 classic                   {"predicted_homestyle":"classic","probability_eclectic":5.232953963126947E-4,"probability_classic":0.99947670460
    142 classic                   {"predicted_homestyle":"classic","probability_eclectic":0.034440633214627994,"probability_classic":0.96555936678
    401 eclectic                  {"predicted_homestyle":"eclectic","probability_eclectic":0.9999992647104572,"probability_classic":7.352895428081
    161 eclectic                  {"predicted_homestyle":"eclectic","probability_eclectic":0.9849110142957106,"probability_classic":0.015088985704
    237 classic                   {"predicted_homestyle":"classic","probability_eclectic":0.04198617357481234,"probability_classic":0.958013826425
    140 classic                   {"predicted_homestyle":"classic","probability_eclectic":0.04836046382189843,"probability_classic":0.951639536178
    540 eclectic                  {"predicted_homestyle":"eclectic","probability_eclectic":0.9999904584750603,"probability_classic":9.541524939704
     16 classic                   {"predicted_homestyle":"classic","probability_eclectic":0.018538600968549004,"probability_classic":0.98146139903
    132 classic                   {"predicted_homestyle":"classic","probability_eclectic":0.07643239284598298,"probability_classic":0.923567607154
    117 eclectic                  {"predicted_homestyle":"eclectic","probability_eclectic":0.9999992559263512,"probability_classic":7.440736488062
    254 eclectic                  {"predicted_homestyle":"eclectic","probability_eclectic":0.9821016128448249,"probability_classic":0.017898387155
    443 eclectic                  {"predicted_homestyle":"eclectic","probability_eclectic":0.9851949312333244,"probability_classic":0.014805068766
    111 classic                   {"predicted_homestyle":"classic","probability_eclectic":0.026115725609636067,"probability_classic":0.97388427439
    157 eclectic                  {"predicted_homestyle":"eclectic","probability_eclectic":0.9710977853762294,"probability_classic":0.028902214623
    294 classic                   {"predicted_homestyle":"classic","probability_eclectic":0.20019267408265554,"probability_classic":0.799807325917
    340 classic                   {"predicted_homestyle":"classic","probability_eclectic":0.02490070485694777,"probability_classic":0.975099295143
     25 classic                   {"predicted_homestyle":"classic","probability_eclectic":0.04784248427480358,"probability_classic":0.952157515725
    176 eclectic                  {"predicted_homestyle":"eclectic","probability_eclectic":0.9752179563040265,"probability_classic":0.024782043695
    441 null                      {"probability_eclectic":"NaN","probability_classic":"NaN"}
    195 classic                   {"predicted_homestyle":"classic","probability_eclectic":0.00263697462143674,"probability_classic":0.997363025378
    317 null                      {"probability_eclectic":"NaN","probability_classic":"NaN"}
    355 null                      {"probability_eclectic":"NaN","probability_classic":"NaN"}
    353 null                      {"probability_eclectic":"NaN","probability_classic":"NaN"}
    408 null                      {"probability_eclectic":"NaN","probability_classic":"NaN"}`

   Model XML content:

   `<PMML version="4.4">
2020-11-30 09:32:37.811678
61119.5986238532132120 17482.0752601292660984 4926.0344036697251795 2066.0385888582163716 2.9036697247706420 0.7198912514081920 1.2178899082568808 0.4397154488452761 1.6926605504587156 0.7614817065177369 0.5986238532110092 0.8277846724953176 no 0 yes 1 no 0 yes 1 no 0 yes 1 no 0 yes 1 no 0 yes 1 no 0 yes 1

`

            If we enclose the java "model" object in synchronized block, then the results are correct/deterministic and no NULLs and NaNs are reported:

  Map<String, Object> score = null;
  try {
    synchronized (model) {
      score = model.predict(data);
    }
  } catch (Throwable e) {

    throw new SQLException("PMML Prediction failed:" + e.getMessage());
  }`

 However, "synchronized"   block results in huge performnace hit as each thread needs to wait before other one finishes.

 We would like to have PMML4S supporting thread-safety without us coding synchronized around PMML model.

 Could you please look into the issue and let us know if we are missing anything?
wjtdc commented 3 years ago

[

2020-11-30 09:32:37.811678
61119.5986238532132120 17482.0752601292660984 4926.0344036697251795 2066.0385888582163716 2.9036697247706420 0.7198912514081920 1.2178899082568808 0.4397154488452761 1.6926605504587156 0.7614817065177369 0.5986238532110092 0.8277846724953176 no 0 yes 1 no 0 yes 1 no 0 yes 1 no 0 yes 1 no 0 yes 1 no 0 yes 1

](url)

scorebot commented 3 years ago

@wjtdc Thanks for your findings, what is the type of model? RegressionModel or GeneralRegressionModel? Could you mind sending your model to me for debugging?

soumyava commented 3 years ago

Here is the model. It is LogisticRegression

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<PMML xmlns="http://www.dmg.org/PMML-4_4" xmlns:data="http://jpmml.org/jpmml-model/InlineTable" version="4.4">
    <Header>
        <Application name="JPMML-SkLearn" version="1.6.7"/>
        <Timestamp>2021-04-27T18:21:13Z</Timestamp>
    </Header>
    <MiningBuildTask>
        <Extension>PMMLPipeline(steps=[('mapping', DataFrameMapper(default=False, df_out=False, drop_cols=[],
                features=[(['price'],
                           StandardScaler(copy=True, with_mean=True,
                                          with_std=True)),
                          (['lotsize'],
                           StandardScaler(copy=True, with_mean=True,
                                          with_std=True)),
                          (['bedrooms'],
                           StandardScaler(copy=True, with_mean=True,
                                          with_std=True)),
                          (['bathrms'],
                           StandardScaler(copy=True, with_mean=True,
                                          with_std=True)),
                          (['stories'],
                           StandardScaler(copy=True, with_mean=True,
                                          with_std=True)),
                          (['garagepl'],
                           StandardScaler(copy=True, with_mean=True,
                                          with_std=True)),
                          (['driveway'], LabelEncoder()),
                          (['recroom'], LabelEncoder()),
                          (['fullbase'], LabelEncoder()),
                          (['gashw'], LabelEncoder()),
                          (['airco'], LabelEncoder()),
                          (['prefarea'], LabelEncoder())],
                input_df=False, sparse=False)),
       ('clf', LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
                   intercept_scaling=1, l1_ratio=None, max_iter=1000,
                   multi_class='ovr', n_jobs=None, penalty='l2', random_state=0,
                   solver='liblinear', tol=0.0001, verbose=0, warm_start=False))])</Extension>
    </MiningBuildTask>
    <DataDictionary>
        <DataField name="homestyle" optype="categorical" dataType="string">
            <Value value="classic"/>
            <Value value="eclectic"/>
        </DataField>
        <DataField name="price" optype="continuous" dataType="double"/>
        <DataField name="lotsize" optype="continuous" dataType="double"/>
        <DataField name="bedrooms" optype="continuous" dataType="double"/>
        <DataField name="bathrms" optype="continuous" dataType="double"/>
        <DataField name="stories" optype="continuous" dataType="double"/>
        <DataField name="garagepl" optype="continuous" dataType="double"/>
        <DataField name="driveway" optype="categorical" dataType="string">
            <Value value="no"/>
            <Value value="yes"/>
        </DataField>
        <DataField name="recroom" optype="categorical" dataType="string">
            <Value value="no"/>
            <Value value="yes"/>
        </DataField>
        <DataField name="fullbase" optype="categorical" dataType="string">
            <Value value="no"/>
            <Value value="yes"/>
        </DataField>
        <DataField name="gashw" optype="categorical" dataType="string">
            <Value value="no"/>
            <Value value="yes"/>
        </DataField>
        <DataField name="airco" optype="categorical" dataType="string">
            <Value value="no"/>
            <Value value="yes"/>
        </DataField>
        <DataField name="prefarea" optype="categorical" dataType="string">
            <Value value="no"/>
            <Value value="yes"/>
        </DataField>
    </DataDictionary>
    <TransformationDictionary/>
    <RegressionModel functionName="classification" normalizationMethod="logit">
        <MiningSchema>
            <MiningField name="homestyle" usageType="target"/>
            <MiningField name="price"/>
            <MiningField name="lotsize"/>
            <MiningField name="bedrooms"/>
            <MiningField name="bathrms"/>
            <MiningField name="stories"/>
            <MiningField name="garagepl"/>
            <MiningField name="driveway"/>
            <MiningField name="recroom"/>
            <MiningField name="fullbase"/>
            <MiningField name="gashw"/>
            <MiningField name="airco"/>
            <MiningField name="prefarea"/>
        </MiningSchema>
        <Output>
            <OutputField name="probability(classic)" optype="continuous" dataType="double" feature="probability" value="classic"/>
            <OutputField name="probability(eclectic)" optype="continuous" dataType="double" feature="probability" value="eclectic"/>
        </Output>
        <LocalTransformations>
            <DerivedField name="standardScaler(price)" optype="continuous" dataType="double">
                <Apply function="/">
                    <Apply function="-">
                        <FieldRef field="price"/>
                        <Constant dataType="double">61119.59862385321</Constant>
                    </Apply>
                    <Constant dataType="double">17482.07526012927</Constant>
                </Apply>
            </DerivedField>
            <DerivedField name="standardScaler(lotsize)" optype="continuous" dataType="double">
                <Apply function="/">
                    <Apply function="-">
                        <FieldRef field="lotsize"/>
                        <Constant dataType="double">4926.034403669725</Constant>
                    </Apply>
                    <Constant dataType="double">2066.0385888582164</Constant>
                </Apply>
            </DerivedField>
            <DerivedField name="standardScaler(bedrooms)" optype="continuous" dataType="double">
                <Apply function="/">
                    <Apply function="-">
                        <FieldRef field="bedrooms"/>
                        <Constant dataType="double">2.903669724770642</Constant>
                    </Apply>
                    <Constant dataType="double">0.719891251408192</Constant>
                </Apply>
            </DerivedField>
            <DerivedField name="standardScaler(bathrms)" optype="continuous" dataType="double">
                <Apply function="/">
                    <Apply function="-">
                        <FieldRef field="bathrms"/>
                        <Constant dataType="double">1.2178899082568808</Constant>
                    </Apply>
                    <Constant dataType="double">0.4397154488452761</Constant>
                </Apply>
            </DerivedField>
            <DerivedField name="standardScaler(stories)" optype="continuous" dataType="double">
                <Apply function="/">
                    <Apply function="-">
                        <FieldRef field="stories"/>
                        <Constant dataType="double">1.6926605504587156</Constant>
                    </Apply>
                    <Constant dataType="double">0.7614817065177371</Constant>
                </Apply>
            </DerivedField>
            <DerivedField name="standardScaler(garagepl)" optype="continuous" dataType="double">
                <Apply function="/">
                    <Apply function="-">
                        <FieldRef field="garagepl"/>
                        <Constant dataType="double">0.5986238532110092</Constant>
                    </Apply>
                    <Constant dataType="double">0.8277846724953176</Constant>
                </Apply>
            </DerivedField>
            <DerivedField name="encoder(driveway)" optype="categorical" dataType="integer">
                <MapValues outputColumn="data:output">
                    <FieldColumnPair field="driveway" column="data:input"/>
                    <InlineTable>
                        <row>
                            <data:input>no</data:input>
                            <data:output>0</data:output>
                        </row>
                        <row>
                            <data:input>yes</data:input>
                            <data:output>1</data:output>
                        </row>
                    </InlineTable>
                </MapValues>
            </DerivedField>
            <DerivedField name="encoder(recroom)" optype="categorical" dataType="integer">
                <MapValues outputColumn="data:output">
                    <FieldColumnPair field="recroom" column="data:input"/>
                    <InlineTable>
                        <row>
                            <data:input>no</data:input>
                            <data:output>0</data:output>
                        </row>
                        <row>
                            <data:input>yes</data:input>
                            <data:output>1</data:output>
                        </row>
                    </InlineTable>
                </MapValues>
            </DerivedField>
            <DerivedField name="encoder(fullbase)" optype="categorical" dataType="integer">
                <MapValues outputColumn="data:output">
                    <FieldColumnPair field="fullbase" column="data:input"/>
                    <InlineTable>
                        <row>
                            <data:input>no</data:input>
                            <data:output>0</data:output>
                        </row>
                        <row>
                            <data:input>yes</data:input>
                            <data:output>1</data:output>
                        </row>
                    </InlineTable>
                </MapValues>
            </DerivedField>
            <DerivedField name="encoder(gashw)" optype="categorical" dataType="integer">
                <MapValues outputColumn="data:output">
                    <FieldColumnPair field="gashw" column="data:input"/>
                    <InlineTable>
                        <row>
                            <data:input>no</data:input>
                            <data:output>0</data:output>
                        </row>
                        <row>
                            <data:input>yes</data:input>
                            <data:output>1</data:output>
                        </row>
                    </InlineTable>
                </MapValues>
            </DerivedField>
            <DerivedField name="encoder(airco)" optype="categorical" dataType="integer">
                <MapValues outputColumn="data:output">
                    <FieldColumnPair field="airco" column="data:input"/>
                    <InlineTable>
                        <row>
                            <data:input>no</data:input>
                            <data:output>0</data:output>
                        </row>
                        <row>
                            <data:input>yes</data:input>
                            <data:output>1</data:output>
                        </row>
                    </InlineTable>
                </MapValues>
            </DerivedField>
            <DerivedField name="encoder(prefarea)" optype="categorical" dataType="integer">
                <MapValues outputColumn="data:output">
                    <FieldColumnPair field="prefarea" column="data:input"/>
                    <InlineTable>
                        <row>
                            <data:input>no</data:input>
                            <data:output>0</data:output>
                        </row>
                        <row>
                            <data:input>yes</data:input>
                            <data:output>1</data:output>
                        </row>
                    </InlineTable>
                </MapValues>
            </DerivedField>
            <DerivedField name="continuous(encoder(driveway))" optype="continuous" dataType="integer">
                <FieldRef field="encoder(driveway)"/>
            </DerivedField>
            <DerivedField name="continuous(encoder(recroom))" optype="continuous" dataType="integer">
                <FieldRef field="encoder(recroom)"/>
            </DerivedField>
            <DerivedField name="continuous(encoder(fullbase))" optype="continuous" dataType="integer">
                <FieldRef field="encoder(fullbase)"/>
            </DerivedField>
            <DerivedField name="continuous(encoder(gashw))" optype="continuous" dataType="integer">
                <FieldRef field="encoder(gashw)"/>
            </DerivedField>
            <DerivedField name="continuous(encoder(airco))" optype="continuous" dataType="integer">
                <FieldRef field="encoder(airco)"/>
            </DerivedField>
            <DerivedField name="continuous(encoder(prefarea))" optype="continuous" dataType="integer">
                <FieldRef field="encoder(prefarea)"/>
            </DerivedField>
        </LocalTransformations>
        <RegressionTable intercept="2.4434261415131577" targetCategory="eclectic">
            <NumericPredictor name="standardScaler(price)" coefficient="5.665174186447512"/>
            <NumericPredictor name="standardScaler(lotsize)" coefficient="0.03268595140978323"/>
            <NumericPredictor name="standardScaler(bedrooms)" coefficient="-0.14034552993860838"/>
            <NumericPredictor name="standardScaler(bathrms)" coefficient="0.4693817075890175"/>
            <NumericPredictor name="standardScaler(stories)" coefficient="-0.0751176717450174"/>
            <NumericPredictor name="standardScaler(garagepl)" coefficient="0.01347471331749156"/>
            <NumericPredictor name="continuous(encoder(driveway))" coefficient="0.7588759495896155"/>
            <NumericPredictor name="continuous(encoder(recroom))" coefficient="-0.0781787031939792"/>
            <NumericPredictor name="continuous(encoder(fullbase))" coefficient="0.6193262751903754"/>
            <NumericPredictor name="continuous(encoder(gashw))" coefficient="0.8551064661895064"/>
            <NumericPredictor name="continuous(encoder(airco))" coefficient="0.37834066142531536"/>
            <NumericPredictor name="continuous(encoder(prefarea))" coefficient="-0.046869891091957175"/>
        </RegressionTable>
        <RegressionTable intercept="0.0" targetCategory="classic"/>
    </RegressionModel>
</PMML>
asmirnov-tba commented 3 years ago

Yes, this is very true, I just run the simple test with the model above and it gives me the NaNs sometimes in a mutithreading environment. Here is the source code for my test:

`

public static void main(String[] args) {
    Model m = Model.fromFile("model.pmml");
    Map<String, Object> features = new HashMap<String, Object>();
    features.put("price", 1);
    features.put("lotsize", 1);
    features.put("bedrooms", 1);
    features.put("bathrms", 1);
    features.put("stories", 1);
    features.put("garagepl", 1);
    features.put("driveway", "yes");
    features.put("recroom", "yes");
    features.put("fullbase", "yes");
    features.put("gashw", "yes");
    features.put("airco", "yes");
    features.put("prefarea", "yes");

    for (int i = 0; i < 10; i++) {

        final int threadNum = i;
        new Thread(new Runnable() {

            long l = 0;

            @Override
            public void run() {
                while (true) {

                    Map<String, Object> map = m.predict(features);
                    System.out.println("Thread " + threadNum + "; row num: " + l++ + "; Score claassic: "
                            + map.get("probability(classic)") + "; Score eclectic: "
                            + map.get("probability(eclectic)"));
                }

            }
        }).start();
    }
}`

And here is the sample output:


Thread 6; row num: 138; Score claassic: 0.9999996034045886; Score eclectic: 3.96595411369589E-7
Thread 5; row num: 113; Score claassic: NaN; Score eclectic: NaN
Thread 6; row num: 139; Score claassic: 0.9999996034045886; Score eclectic: 3.96595411369589E-7
asmirnov-tba commented 3 years ago

And to avoid confusion - me, @soumyava and @wjtdc - we are working on a same project

scorebot commented 3 years ago

Thanks for your model and example, we can reproduce the critical thread-safe issue that is caused by the derived field computing, we will fix it as soon as possible.

asmirnov-tba commented 3 years ago

Thank you, @scorebot

wjtdc commented 3 years ago

Thank you for looking into this issue.

Yes, @soumyava and I work on same project.

scorebot commented 3 years ago

We have fixed the thread-safe issue above. Please, clone the latest code of the master branch, build PMML4S by the command: sbt package, then try the new jar. If there is no problem anymore, we will release the next version 0.9.10 to the maven central. Please, let me know if you have any problems.

soumyava commented 3 years ago

Hi @scorebot I have been able to use the jar and do not see the NaNs anymore. Please go ahead and release the next version to maven central and I look forward to picking up 0.9.10 from Maven central. Thank you for fixing the issue in such a short span of time. My colleagues and I really appreciate it !

wjtdc commented 3 years ago

Thanks @scorebot for quick fix.

scorebot commented 3 years ago

@soumyava The latest version 0.9.10 has been pushed to the Maven central, please try it.

scorebot commented 3 years ago

I close this issue now. if you have other problems, please feel free to open a new one.