diff --git a/src/Microsoft.ML.StandardTrainers/Standard/LogisticRegression/MulticlassLogisticRegression.cs b/src/Microsoft.ML.StandardTrainers/Standard/LogisticRegression/MulticlassLogisticRegression.cs index e354877578..2c141c7520 100644 --- a/src/Microsoft.ML.StandardTrainers/Standard/LogisticRegression/MulticlassLogisticRegression.cs +++ b/src/Microsoft.ML.StandardTrainers/Standard/LogisticRegression/MulticlassLogisticRegression.cs @@ -978,41 +978,29 @@ private JToken SaveAsPfaCore(BoundPfaContext ctx, JToken input) private bool SaveAsOnnxCore(OnnxContext ctx, string[] outputs, string featureColumn) { Host.CheckValue(ctx, nameof(ctx)); + Host.Assert(outputs[0] == DefaultColumnNames.PredictedLabel); + Host.Assert(outputs[1] == DefaultColumnNames.Score); - string predictedLabelInt64 = null; - string predictedLabelUint32 = null; - // REVIEW: What is the right way to get the name of the predicted column? - for (int i = 0; i < outputs.Length; i++) - { - if (outputs[i] != DefaultColumnNames.PredictedLabel) - continue; - predictedLabelUint32 = DefaultColumnNames.PredictedLabel; - predictedLabelInt64 = ctx.AddIntermediateVariable(NumberDataViewType.Int64, "PredictedLabelInt64", true); - outputs[i] = predictedLabelInt64; - break; - } - - Host.CheckValue(predictedLabelInt64, nameof(predictedLabelInt64)); + string classifierLabelOutput = ctx.AddIntermediateVariable(NumberDataViewType.Int64, "ClassifierLabelOutput", true); string opType = "LinearClassifier"; - var node = ctx.CreateNode(opType, new[] { featureColumn }, outputs, ctx.GetNodeName(opType)); + var node = ctx.CreateNode(opType, new[] { featureColumn }, new[] { classifierLabelOutput, outputs[1] }, ctx.GetNodeName(opType)); node.AddAttribute("post_transform", GetOnnxPostTransform()); node.AddAttribute("multi_class", true); node.AddAttribute("coefficients", Weights.SelectMany(w => w.DenseValues())); node.AddAttribute("intercepts", Biases); node.AddAttribute("classlabels_ints", Enumerable.Range(1, NumberOfClasses).Select(x => (long)x)); + opType = "Unsqueeze"; + var unsqueezeOutput = ctx.AddIntermediateVariable(NumberDataViewType.Int64, "CastNodeOutput"); + var unsqueezeNode = ctx.CreateNode(opType, classifierLabelOutput, unsqueezeOutput, ctx.GetNodeName(opType), ""); + unsqueezeNode.AddAttribute("axes", new long[] { 1 }); + // Onnx outputs an Int64, but ML.NET outputs UInt32. So cast the Onnx output here opType = "Cast"; - var castNodeOutput = ctx.AddIntermediateVariable(NumberDataViewType.UInt32, "CastNodeOutput", true); - var castNode = ctx.CreateNode(opType, predictedLabelInt64, castNodeOutput, ctx.GetNodeName(opType), ""); + var castNode = ctx.CreateNode(opType, unsqueezeOutput, outputs[0], ctx.GetNodeName(opType), ""); var t = InternalDataKindExtensions.ToInternalDataKind(DataKind.UInt32).ToType(); castNode.AddAttribute("to", t); - - opType = "Unsqueeze"; - var unsqueezeNode = ctx.CreateNode(opType, castNodeOutput, predictedLabelUint32, ctx.GetNodeName(opType), ""); - unsqueezeNode.AddAttribute("axes", new long[] { 0 }); - return true; } diff --git a/test/BaselineOutput/Common/Onnx/MultiClassClassification/BreastCancer/MultiClassificationLogisticRegressionSaveModelToOnnxTest.txt b/test/BaselineOutput/Common/Onnx/MultiClassClassification/BreastCancer/MultiClassificationLogisticRegressionSaveModelToOnnxTest.txt index 36d8d01b7b..799ca5125b 100644 --- a/test/BaselineOutput/Common/Onnx/MultiClassClassification/BreastCancer/MultiClassificationLogisticRegressionSaveModelToOnnxTest.txt +++ b/test/BaselineOutput/Common/Onnx/MultiClassClassification/BreastCancer/MultiClassificationLogisticRegressionSaveModelToOnnxTest.txt @@ -12,6 +12,31 @@ "output": [ "Features0" ], + "name": "Imputer", + "opType": "Imputer", + "attribute": [ + { + "name": "replaced_value_float", + "f": "NaN", + "type": "FLOAT" + }, + { + "name": "imputed_value_floats", + "floats": [ + 0 + ], + "type": "FLOATS" + } + ], + "domain": "ai.onnx.ml" + }, + { + "input": [ + "Features0" + ], + "output": [ + "Features1" + ], "name": "Scaler", "opType": "Scaler", "attribute": [ @@ -115,10 +140,10 @@ }, { "input": [ - "Features0" + "Features1" ], "output": [ - "PredictedLabelInt64", + "ClassifierLabelOutput", "Score" ], "name": "LinearClassifier", @@ -137,102 +162,102 @@ { "name": "coefficients", "floats": [ - -0.0871891156, - 0.209310874, - 0.747134566, - 0.364765137, - -0.377612084, - -0.6847462, + -0.03167667, + 0.143471345, + 0.712303758, + 0.352516025, + -0.301202744, + -0.741327, 0, - -0.5566554, - -0.3849638, - -1.29262471, + -0.50539434, + -0.495568782, + -1.18026292, 0, 0, - -0.479907274, - -0.08740093, - -0.5489706, + -0.498504966, + -0.0420997739, + -0.613307, 0, - 0.630316138, + 0.607902765, 0, + 0.141199484, 0, 0, - 0.07319626, - 0.171390951, - 0.6936194, + 0.211812049, + 0.7400253, 0, 0, - -0.6189027, + -0.6818558, 0, - -0.732489467, - -0.71812433, - 0.2614429, - -0.4669126, - -0.250123739, - 1.01838875, - 0.7936676, + -0.551549, + -0.696967065, + 0.160799474, + -0.330957055, + -0.2750144, + 1.0280081, + 0.88234216, 0, 0, - 0.8072781, + 0.6424018, 0, - 0.833407462, + 0.8743826, 0, - -1.67462111, - -1.19559848, - -0.553805768, - -0.5710498, - -0.7325714, - -0.5470721, - -0.7483947, + -1.70732152, + -1.191878, + -0.5418798, + -0.600155354, + -0.740290344, + -0.674283, + -0.7527394, 0, - -0.5655844, - -0.9892823, - -0.237264976, + -0.5712943, + -1.04777157, + -0.226992026, 0, - -0.81984, - -0.0930810943, - -0.4526821, + -0.706178248, + -0.145423546, + -0.465133369, 0, 0, 0, - 0.726712048, + 0.7297528, 0, - 1.12171924, - 0.323810369, - 0.245762676, - 0.07872447, - 0.939905643, - 0.923160553, + 1.16632056, + 0.311890721, + 0.247274086, + 0.102029644, + 0.955604, + 0.9211974, 0, 0, - 1.10209334, - 0.704743862, + 1.15463662, + 0.6863172, 0, - 0.322121173, - 0.5064917, - 1.30212963, + 0.35739857, + 0.51222384, + 1.30172575, 0, - 0.8623323, - 0.0155395176, + 0.8770747, + 0.0631457046, 0, - 0.192209348, - 0.262786478 + 0.184382111, + 0.289009124 ], "type": "FLOATS" }, { "name": "intercepts", "floats": [ - 1.23585367, - 1.68783426, - -0.8096311, - 1.35599542, - -1.59806383, - 2.57355452, - 1.03064489, - -1.67592752, - -1.40655541, - -2.39366078 + 1.24280047, + 1.713405, + -0.833424449, + 1.31703234, + -1.520917, + 2.636808, + 1.016353, + -1.69996321, + -1.4401859, + -2.43189263 ], "type": "FLOATS" }, @@ -257,18 +282,20 @@ }, { "input": [ - "PredictedLabelInt64" + "ClassifierLabelOutput" ], "output": [ "CastNodeOutput" ], - "name": "Cast0", - "opType": "Cast", + "name": "Unsqueeze", + "opType": "Unsqueeze", "attribute": [ { - "name": "to", - "i": "12", - "type": "INT" + "name": "axes", + "ints": [ + "1" + ], + "type": "INTS" } ] }, @@ -279,15 +306,13 @@ "output": [ "PredictedLabel" ], - "name": "Unsqueeze", - "opType": "Unsqueeze", + "name": "Cast0", + "opType": "Cast", "attribute": [ { - "name": "axes", - "ints": [ - "0" - ], - "type": "INTS" + "name": "to", + "i": "12", + "type": "INT" } ] }, @@ -303,7 +328,7 @@ }, { "input": [ - "Features0" + "Features1" ], "output": [ "Features.output" @@ -540,6 +565,24 @@ } } }, + { + "name": "Features1", + "type": { + "tensorType": { + "elemType": 1, + "shape": { + "dim": [ + { + "dimValue": "-1" + }, + { + "dimValue": "8" + } + ] + } + } + } + }, { "name": "Label0", "type": { @@ -612,6 +655,24 @@ } } }, + { + "name": "CastNodeOutput", + "type": { + "tensorType": { + "elemType": 7, + "shape": { + "dim": [ + { + "dimValue": "-1" + }, + { + "dimValue": "1" + } + ] + } + } + } + }, { "name": "Label.output", "type": { diff --git a/test/Microsoft.ML.Tests/OnnxConversionTest.cs b/test/Microsoft.ML.Tests/OnnxConversionTest.cs index e992a730d9..68383b8ffa 100644 --- a/test/Microsoft.ML.Tests/OnnxConversionTest.cs +++ b/test/Microsoft.ML.Tests/OnnxConversionTest.cs @@ -736,7 +736,8 @@ public void MulticlassLogisticRegressionOnnxConversionTest() separatorChar: '\t', hasHeader: true); - var pipeline = mlContext.Transforms.NormalizeMinMax("Features"). + var pipeline = mlContext.Transforms.ReplaceMissingValues("Features"). + Append(mlContext.Transforms.NormalizeMinMax("Features")). Append(mlContext.Transforms.Conversion.MapValueToKey("Label")). Append(mlContext.MulticlassClassification.Trainers.LbfgsMaximumEntropy(new LbfgsMaximumEntropyMulticlassTrainer.Options() { NumberOfThreads = 1 })); @@ -752,6 +753,17 @@ public void MulticlassLogisticRegressionOnnxConversionTest() SaveOnnxModel(onnxModel, onnxFilePath, onnxTextPath); + // Compare results produced by ML.NET and ONNX's runtime. + if (IsOnnxRuntimeSupported()) + { + // Evaluate the saved ONNX model using the data used to train the ML.NET pipeline. + var onnxEstimator = mlContext.Transforms.ApplyOnnxModel(onnxFilePath); + var onnxTransformer = onnxEstimator.Fit(data); + var onnxResult = onnxTransformer.Transform(data); + CompareSelectedColumns("PredictedLabel", "PredictedLabel", transformedData, onnxResult); + CompareSelectedColumns("Score", "Score", transformedData, onnxResult); + } + CheckEquality(subDir, onnxTextName, digitsOfPrecision: 2); Done(); }