diff --git a/src/Microsoft.ML.Data/Transforms/ColumnConcatenatingTransformer.cs b/src/Microsoft.ML.Data/Transforms/ColumnConcatenatingTransformer.cs index de4950cfd9..64284a92c1 100644 --- a/src/Microsoft.ML.Data/Transforms/ColumnConcatenatingTransformer.cs +++ b/src/Microsoft.ML.Data/Transforms/ColumnConcatenatingTransformer.cs @@ -896,7 +896,6 @@ public void SaveAsOnnx(OnnxContext ctx) Host.CheckValue(ctx, nameof(ctx)); Contracts.Assert(CanSaveOnnx(ctx)); - string opType = "Concat"; for (int iinfo = 0; iinfo < _columns.Length; ++iinfo) { var colInfo = _parent._columns[iinfo]; @@ -904,7 +903,7 @@ public void SaveAsOnnx(OnnxContext ctx) string outName = colInfo.Name; var outColType = boundCol.OutputType; - if (!outColType.IsKnownSize) + if ((!outColType.IsKnownSize) || (!(outColType.GetItemType() is NumberDataViewType))) { ctx.RemoveColumn(outName, false); continue; @@ -925,10 +924,19 @@ public void SaveAsOnnx(OnnxContext ctx) InputSchema[srcIndex].Type.GetValueCount())); } + string opType = "FeatureVectorizer"; + int outVectorSize = (int)inputList.Sum(x => x.Value); + var vectorizerOutputType = new VectorDataViewType(NumberDataViewType.Single, outVectorSize); + var vectorizerOutputName = ctx.AddIntermediateVariable(vectorizerOutputType, "VectorFeaturizerOutput"); var node = ctx.CreateNode(opType, inputList.Select(t => t.Key), - new[] { ctx.AddIntermediateVariable(outColType, outName) }, ctx.GetNodeName(opType), ""); - - node.AddAttribute("axis", 1); + new[] { vectorizerOutputName }, ctx.GetNodeName(opType)); + node.AddAttribute("inputdimensions", inputList.Select(x => x.Value)); + + opType = "Cast"; + var dstVectorType = new VectorDataViewType(outColType.GetItemType() as PrimitiveDataViewType, outVectorSize); + var dstVariableName = ctx.AddIntermediateVariable(dstVectorType, outName); + var castNode = ctx.CreateNode(opType, vectorizerOutputName, dstVariableName, ctx.GetNodeName(opType), ""); + castNode.AddAttribute("to", outColType.ItemType.RawType); } } } diff --git a/test/BaselineOutput/Common/Onnx/BinaryClassification/BreastCancer/ExcludeVariablesInOnnxConversion.txt b/test/BaselineOutput/Common/Onnx/BinaryClassification/BreastCancer/ExcludeVariablesInOnnxConversion.txt index 6c0545bed8..4e8133d917 100644 --- a/test/BaselineOutput/Common/Onnx/BinaryClassification/BreastCancer/ExcludeVariablesInOnnxConversion.txt +++ b/test/BaselineOutput/Common/Onnx/BinaryClassification/BreastCancer/ExcludeVariablesInOnnxConversion.txt @@ -176,14 +176,35 @@ "F1", "F22" ], + "output": [ + "VectorFeaturizerOutput" + ], + "name": "FeatureVectorizer", + "opType": "FeatureVectorizer", + "attribute": [ + { + "name": "inputdimensions", + "ints": [ + "1", + "10" + ], + "type": "INTS" + } + ], + "domain": "ai.onnx.ml" + }, + { + "input": [ + "VectorFeaturizerOutput" + ], "output": [ "Features" ], - "name": "Concat", - "opType": "Concat", + "name": "Cast1", + "opType": "Cast", "attribute": [ { - "name": "axis", + "name": "to", "i": "1", "type": "INT" } @@ -431,7 +452,7 @@ "output": [ "PredictedLabel" ], - "name": "Cast1", + "name": "Cast2", "opType": "Cast", "attribute": [ { @@ -638,6 +659,24 @@ } } }, + { + "name": "VectorFeaturizerOutput", + "type": { + "tensorType": { + "elemType": 1, + "shape": { + "dim": [ + { + "dimValue": "-1" + }, + { + "dimValue": "11" + } + ] + } + } + } + }, { "name": "Features", "type": { diff --git a/test/BaselineOutput/Common/Onnx/BinaryClassification/BreastCancer/ModelWithLessIO.txt b/test/BaselineOutput/Common/Onnx/BinaryClassification/BreastCancer/ModelWithLessIO.txt index 4bce784b91..1d72cf3487 100644 --- a/test/BaselineOutput/Common/Onnx/BinaryClassification/BreastCancer/ModelWithLessIO.txt +++ b/test/BaselineOutput/Common/Onnx/BinaryClassification/BreastCancer/ModelWithLessIO.txt @@ -125,14 +125,35 @@ "F1", "F21" ], + "output": [ + "VectorFeaturizerOutput" + ], + "name": "FeatureVectorizer", + "opType": "FeatureVectorizer", + "attribute": [ + { + "name": "inputdimensions", + "ints": [ + "8", + "9" + ], + "type": "INTS" + } + ], + "domain": "ai.onnx.ml" + }, + { + "input": [ + "VectorFeaturizerOutput" + ], "output": [ "Features" ], - "name": "Concat", - "opType": "Concat", + "name": "Cast1", + "opType": "Cast", "attribute": [ { - "name": "axis", + "name": "to", "i": "1", "type": "INT" } @@ -757,7 +778,7 @@ "output": [ "PredictedLabel" ], - "name": "Cast1", + "name": "Cast2", "opType": "Cast", "attribute": [ { @@ -946,6 +967,24 @@ } } }, + { + "name": "VectorFeaturizerOutput", + "type": { + "tensorType": { + "elemType": 1, + "shape": { + "dim": [ + { + "dimValue": "-1" + }, + { + "dimValue": "17" + } + ] + } + } + } + }, { "name": "Features", "type": { diff --git a/test/BaselineOutput/Common/Onnx/BinaryClassification/BreastCancer/OneHotBagPipeline.txt b/test/BaselineOutput/Common/Onnx/BinaryClassification/BreastCancer/OneHotBagPipeline.txt index 48945bf310..4158d89aa4 100644 --- a/test/BaselineOutput/Common/Onnx/BinaryClassification/BreastCancer/OneHotBagPipeline.txt +++ b/test/BaselineOutput/Common/Onnx/BinaryClassification/BreastCancer/OneHotBagPipeline.txt @@ -176,14 +176,35 @@ "F1", "F22" ], + "output": [ + "VectorFeaturizerOutput" + ], + "name": "FeatureVectorizer", + "opType": "FeatureVectorizer", + "attribute": [ + { + "name": "inputdimensions", + "ints": [ + "1", + "10" + ], + "type": "INTS" + } + ], + "domain": "ai.onnx.ml" + }, + { + "input": [ + "VectorFeaturizerOutput" + ], "output": [ "Features" ], - "name": "Concat", - "opType": "Concat", + "name": "Cast1", + "opType": "Cast", "attribute": [ { - "name": "axis", + "name": "to", "i": "1", "type": "INT" } @@ -384,7 +405,7 @@ "output": [ "PredictedLabel" ], - "name": "Cast1", + "name": "Cast2", "opType": "Cast", "attribute": [ { @@ -871,6 +892,24 @@ } } }, + { + "name": "VectorFeaturizerOutput", + "type": { + "tensorType": { + "elemType": 1, + "shape": { + "dim": [ + { + "dimValue": "-1" + }, + { + "dimValue": "11" + } + ] + } + } + } + }, { "name": "Features", "type": { diff --git a/test/Microsoft.ML.Tests/OnnxConversionTest.cs b/test/Microsoft.ML.Tests/OnnxConversionTest.cs index ad2bbd0a20..195f8d4634 100644 --- a/test/Microsoft.ML.Tests/OnnxConversionTest.cs +++ b/test/Microsoft.ML.Tests/OnnxConversionTest.cs @@ -878,6 +878,36 @@ public void LoadingPredictorModelAndOnnxConversionTest() Done(); } + [Fact] + public void ConcatenateOnnxConversionTest() + { + var mlContext = new MLContext(seed: 1); + string dataPath = GetDataPath("breast-cancer.txt"); + + var data = ML.Data.LoadFromTextFile(dataPath, new[] { + new TextLoader.Column("VectorDouble2", DataKind.Double, 1), + new TextLoader.Column("VectorDouble1", DataKind.Double, 4, 8), + new TextLoader.Column("Label", DataKind.Boolean, 0) + }); + var pipeline = mlContext.Transforms.Concatenate("Features", "VectorDouble1", "VectorDouble2"); + var model = pipeline.Fit(data); + var transformedData = model.Transform(data); + var onnxModel = mlContext.Model.ConvertToOnnxProtobuf(model, data); + + // Compare results produced by ML.NET and ONNX's runtime. + if (IsOnnxRuntimeSupported()) + { + var onnxModelName = "Concatenate.onnx"; + var onnxModelPath = GetOutputPath(onnxModelName); + SaveOnnxModel(onnxModel, onnxModelPath, null); + // Evaluate the saved ONNX model using the data used to train the ML.NET pipeline. + var onnxEstimator = mlContext.Transforms.ApplyOnnxModel(onnxModelPath); + var onnxTransformer = onnxEstimator.Fit(data); + var onnxResult = onnxTransformer.Transform(data); + CompareSelectedColumns("Features", "Features", transformedData, onnxResult); + } + Done(); + } [Fact] public void RemoveVariablesInPipelineTest()