From b38c79abc9988c4d7eecf1b621f0b87452fe0a6e Mon Sep 17 00:00:00 2001 From: "R. G. Esteves" Date: Mon, 17 Oct 2022 06:00:07 -0700 Subject: [PATCH 1/2] XGBoost integration infrastructure --- Microsoft.ML.sln | 21 +- .../Properties/AssemblyInfo.cs | 1 + .../Properties/AssemblyInfo.cs | 1 + .../Properties/AssemblyInfo.cs | 1 + .../Microsoft.ML.XGBoost.csproj | 34 ++ .../Properties/AssemblyInfo.cs | 13 + .../WrappedXGBoostInterface.cs | 164 +++++++++ src/Microsoft.ML.XGBoost/WrappedXGBooster.cs | 310 ++++++++++++++++++ src/Microsoft.ML.XGBoost/WrappedXGDMatrix.cs | 104 ++++++ src/Microsoft.ML.XGBoost/XGBoostArguments.cs | 307 +++++++++++++++++ src/Microsoft.ML.XGBoost/XGBoostCatalog.cs | 54 +++ src/Microsoft.ML.XGBoost/XGBoostInterface.cs | 52 +++ .../XGBoostRegressionTrainer.cs | 231 +++++++++++++ .../XGBoostTrainerBase.cs | 224 +++++++++++++ 14 files changed, 1516 insertions(+), 1 deletion(-) create mode 100644 src/Microsoft.ML.XGBoost/Microsoft.ML.XGBoost.csproj create mode 100644 src/Microsoft.ML.XGBoost/Properties/AssemblyInfo.cs create mode 100644 src/Microsoft.ML.XGBoost/WrappedXGBoostInterface.cs create mode 100644 src/Microsoft.ML.XGBoost/WrappedXGBooster.cs create mode 100644 src/Microsoft.ML.XGBoost/WrappedXGDMatrix.cs create mode 100644 src/Microsoft.ML.XGBoost/XGBoostArguments.cs create mode 100644 src/Microsoft.ML.XGBoost/XGBoostCatalog.cs create mode 100644 src/Microsoft.ML.XGBoost/XGBoostInterface.cs create mode 100644 src/Microsoft.ML.XGBoost/XGBoostRegressionTrainer.cs create mode 100644 src/Microsoft.ML.XGBoost/XGBoostTrainerBase.cs diff --git a/Microsoft.ML.sln b/Microsoft.ML.sln index f60036dfd1..4e932f32cf 100644 --- a/Microsoft.ML.sln +++ b/Microsoft.ML.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.1.32120.378 MinimumVisualStudioVersion = 10.0.40219.1 @@ -161,6 +161,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.ML.Tokenizers", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.ML.Tokenizers.Tests", "test\Microsoft.ML.Tokenizers.Tests\Microsoft.ML.Tokenizers.Tests.csproj", "{C3D82402-F207-4F19-8C57-5AF0FBAF9682}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.ML.XGBoost", "src\Microsoft.ML.XGBoost\Microsoft.ML.XGBoost.csproj", "{9CF22D6B-3094-4F42-9CBF-1B07087CF1EE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -409,6 +411,14 @@ Global {A7222F41-1CF0-47D9-B80C-B4D77B027A61}.Release|Any CPU.Build.0 = Release|Any CPU {A7222F41-1CF0-47D9-B80C-B4D77B027A61}.Release|x64.ActiveCfg = Release|Any CPU {A7222F41-1CF0-47D9-B80C-B4D77B027A61}.Release|x64.Build.0 = Release|Any CPU + {A7222F94-2AF1-10C9-A21C-C4D22B137A69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A7222F94-2AF1-10C9-A21C-C4D22B137A69}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A7222F94-2AF1-10C9-A21C-C4D22B137A69}.Debug|x64.ActiveCfg = Debug|Any CPU + {A7222F94-2AF1-10C9-A21C-C4D22B137A69}.Debug|x64.Build.0 = Debug|Any CPU + {A7222F94-2AF1-10C9-A21C-C4D22B137A69}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7222F94-2AF1-10C9-A21C-C4D22B137A69}.Release|Any CPU.Build.0 = Release|Any CPU + {A7222F94-2AF1-10C9-A21C-C4D22B137A69}.Release|x64.ActiveCfg = Release|Any CPU + {A7222F94-2AF1-10C9-A21C-C4D22B137A69}.Release|x64.Build.0 = Release|Any CPU {570A0B8A-5463-44D2-8521-54C0CA4CACA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {570A0B8A-5463-44D2-8521-54C0CA4CACA9}.Debug|Any CPU.Build.0 = Debug|Any CPU {570A0B8A-5463-44D2-8521-54C0CA4CACA9}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -757,6 +767,14 @@ Global {C3D82402-F207-4F19-8C57-5AF0FBAF9682}.Release|Any CPU.Build.0 = Release|Any CPU {C3D82402-F207-4F19-8C57-5AF0FBAF9682}.Release|x64.ActiveCfg = Release|Any CPU {C3D82402-F207-4F19-8C57-5AF0FBAF9682}.Release|x64.Build.0 = Release|Any CPU + {9CF22D6B-3094-4F42-9CBF-1B07087CF1EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9CF22D6B-3094-4F42-9CBF-1B07087CF1EE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9CF22D6B-3094-4F42-9CBF-1B07087CF1EE}.Debug|x64.ActiveCfg = Debug|Any CPU + {9CF22D6B-3094-4F42-9CBF-1B07087CF1EE}.Debug|x64.Build.0 = Debug|Any CPU + {9CF22D6B-3094-4F42-9CBF-1B07087CF1EE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9CF22D6B-3094-4F42-9CBF-1B07087CF1EE}.Release|Any CPU.Build.0 = Release|Any CPU + {9CF22D6B-3094-4F42-9CBF-1B07087CF1EE}.Release|x64.ActiveCfg = Release|Any CPU + {9CF22D6B-3094-4F42-9CBF-1B07087CF1EE}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -836,6 +854,7 @@ Global {FF0BD187-4451-4A3B-934B-2AE3454896E2} = {09EADF06-BE25-4228-AB53-95AE3E15B530} {BBC3A950-BD68-45AC-9DBD-A8F4D8847745} = {09EADF06-BE25-4228-AB53-95AE3E15B530} {C3D82402-F207-4F19-8C57-5AF0FBAF9682} = {AED9C836-31E3-4F3F-8ABC-929555D3F3C4} + {9CF22D6B-3094-4F42-9CBF-1B07087CF1EE} = {09EADF06-BE25-4228-AB53-95AE3E15B530} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {41165AF1-35BB-4832-A189-73060F82B01D} diff --git a/src/Microsoft.ML.Core/Properties/AssemblyInfo.cs b/src/Microsoft.ML.Core/Properties/AssemblyInfo.cs index bbfd94eee5..93bc7fd305 100644 --- a/src/Microsoft.ML.Core/Properties/AssemblyInfo.cs +++ b/src/Microsoft.ML.Core/Properties/AssemblyInfo.cs @@ -27,6 +27,7 @@ [assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.Mkl.Components" + PublicKey.Value)] [assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.KMeansClustering" + PublicKey.Value)] [assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.LightGbm" + PublicKey.Value)] +[assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.XGBoost" + PublicKey.Value)] [assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.OnnxConverter" + PublicKey.Value)] [assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.OnnxTransformer" + PublicKey.Value)] [assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.Parquet" + PublicKey.Value)] diff --git a/src/Microsoft.ML.Data/Properties/AssemblyInfo.cs b/src/Microsoft.ML.Data/Properties/AssemblyInfo.cs index d8e6944256..efed04f2af 100644 --- a/src/Microsoft.ML.Data/Properties/AssemblyInfo.cs +++ b/src/Microsoft.ML.Data/Properties/AssemblyInfo.cs @@ -26,6 +26,7 @@ [assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.Mkl.Components" + PublicKey.Value)] [assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.KMeansClustering" + PublicKey.Value)] [assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.LightGbm" + PublicKey.Value)] +[assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.XGBoost" + PublicKey.Value)] [assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.OnnxConverter" + PublicKey.Value)] [assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.OnnxTransformer" + PublicKey.Value)] [assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.Parquet" + PublicKey.Value)] diff --git a/src/Microsoft.ML.FastTree/Properties/AssemblyInfo.cs b/src/Microsoft.ML.FastTree/Properties/AssemblyInfo.cs index 4a1575a5aa..4ee0690c06 100644 --- a/src/Microsoft.ML.FastTree/Properties/AssemblyInfo.cs +++ b/src/Microsoft.ML.FastTree/Properties/AssemblyInfo.cs @@ -9,6 +9,7 @@ [assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.Predictor.Tests" + PublicKey.Value)] [assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.LightGbm" + PublicKey.Value)] +[assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.XGBoost" + PublicKey.Value)] [assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.Sweeper" + PublicKey.Value)] [assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.Internal.FastTree" + InternalPublicKey.Value)] diff --git a/src/Microsoft.ML.XGBoost/Microsoft.ML.XGBoost.csproj b/src/Microsoft.ML.XGBoost/Microsoft.ML.XGBoost.csproj new file mode 100644 index 0000000000..3a955d4ac7 --- /dev/null +++ b/src/Microsoft.ML.XGBoost/Microsoft.ML.XGBoost.csproj @@ -0,0 +1,34 @@ + + + + + netstandard2.0 + Microsoft.ML.XGBoost + true + ML.NET component for XGBoost + + + + + + + + all + + + all + + + all + + + + + + + + + + diff --git a/src/Microsoft.ML.XGBoost/Properties/AssemblyInfo.cs b/src/Microsoft.ML.XGBoost/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..9d1bb36754 --- /dev/null +++ b/src/Microsoft.ML.XGBoost/Properties/AssemblyInfo.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; +using Microsoft.ML; + +[assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.Tests" + PublicKey.TestValue)] +[assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.Predictor.Tests" + PublicKey.Value)] + +[assembly: InternalsVisibleTo(assemblyName: "RunTests" + InternalPublicKey.Value)] + +[assembly: WantsToBeBestFriends] diff --git a/src/Microsoft.ML.XGBoost/WrappedXGBoostInterface.cs b/src/Microsoft.ML.XGBoost/WrappedXGBoostInterface.cs new file mode 100644 index 0000000000..3f885e4f31 --- /dev/null +++ b/src/Microsoft.ML.XGBoost/WrappedXGBoostInterface.cs @@ -0,0 +1,164 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Runtime; +using System.Text; + +namespace Microsoft.ML.Trainers.XGBoost +{ + /// + /// Wrapper of the c interfaces of XGBoost + /// Refer to https://xgboost.readthedocs.io/en/stable/tutorials/c_api_tutorial.html to get the details. + /// + + internal static class WrappedXGBoostInterface + { + + private const string DllName = "xgboost"; + + [DllImport(DllName)] + public static extern void XGBoostVersion(out int major, out int minor, out int patch); + + #region Error API + + [DllImport(DllName)] + public static extern string XGBGetLastError(); + + #endregion + + #region DMatrix API + + [DllImport(DllName)] + public static extern int XGDMatrixCreateFromMat(float[] data, ulong nrow, ulong ncol, + float missing, out IntPtr handle); + + [DllImport(DllName)] + public static extern int XGDMatrixFree(IntPtr handle); + + [DllImport(DllName)] + public static extern int XGDMatrixNumRow(IntPtr handle, out ulong nrows); + + [DllImport(DllName)] + public static extern int XGDMatrixNumCol(IntPtr handle, out ulong ncols); + + [DllImport(DllName)] + public static extern int XGDMatrixGetFloatInfo(IntPtr handle, string field, + out ulong len, out IntPtr result); + + [DllImport(DllName)] + public static extern int XGDMatrixSetFloatInfo(IntPtr handle, string field, + IntPtr array, ulong len); + #endregion + + + #region API Booster + + [DllImport(DllName)] + public static extern int XGBoosterCreate(IntPtr[] dmats, + ulong len, out IntPtr handle); + + [DllImport(DllName)] + public static extern int XGBoosterFree(IntPtr handle); + + [DllImport(DllName)] + public static extern int XGBoosterSetParam(IntPtr handle, string name, string val); + + #endregion + + + #region API train + [DllImport(DllName)] + public static extern int XGBoosterUpdateOneIter(IntPtr bHandle, int iter, + IntPtr dHandle); + + [DllImport(DllName)] + public static extern int XGBoosterEvalOneIter(); + #endregion + + #region API predict + [DllImport(DllName)] + public static extern int XGBoosterPredict(IntPtr bHandle, IntPtr dHandle, + int optionMask, int ntreeLimit, int training, + out ulong predsLen, out IntPtr predsPtr); + #endregion + + #region API serialization + [DllImport(DllName)] + public static extern int XGBoosterDumpModel(IntPtr handle, string fmap, int with_stats, out int out_len, out IntPtr dumpStr); + + [DllImport(DllName)] + public static extern int XGBoosterDumpModelEx(IntPtr handle, string fmap, int with_stats, string format, out int out_len, out IntPtr dumpStr); + #endregion + + } + + internal static class XGBoostInterfaceUtils + { + /// + /// Checks if XGBoost has a pending error message. Raises an exception in that case. + /// + public static void Check(int res) + { + if (res != 0) + { + string mes = WrappedXGBoostInterface.XGBGetLastError(); + throw new Exception($"XGBoost Error, code is {res}, error message is '{mes}'."); + } + } + + public static float[] GetPredictionsArray(IntPtr predsPtr, ulong predsLen) + { + var length = unchecked((int)predsLen); + var preds = new float[length]; + for (var i = 0; i < length; i++) + { + var floatBytes = new byte[4]; + for (var b = 0; b < 4; b++) + { + floatBytes[b] = Marshal.ReadByte(predsPtr, 4 * i + b); + } + preds[i] = BitConverter.ToSingle(floatBytes, 0); + } + return preds; + } + + /// + /// Helper function used for generating the LightGbm argument name. + /// When given a name, this will convert the name to lower-case with underscores. + /// The underscore will be placed when an upper-case letter is encountered. + /// + public static string GetOptionName(string name) + { + // Otherwise convert the name to the light gbm argument + StringBuilder strBuf = new StringBuilder(); + bool first = true; + foreach (char c in name) + { + if (char.IsUpper(c)) + { + if (first) + first = false; + else + strBuf.Append('_'); + strBuf.Append(char.ToLower(c)); + } + else + strBuf.Append(c); + } + return strBuf.ToString(); + } + + /// + /// Convert the pointer of c string to c# string. + /// + public static string GetString(IntPtr src) + { + return Marshal.PtrToStringAnsi(src); + } + } +} diff --git a/src/Microsoft.ML.XGBoost/WrappedXGBooster.cs b/src/Microsoft.ML.XGBoost/WrappedXGBooster.cs new file mode 100644 index 0000000000..5d1cb1a280 --- /dev/null +++ b/src/Microsoft.ML.XGBoost/WrappedXGBooster.cs @@ -0,0 +1,310 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; +using System.Text.Json; +using System.Runtime.InteropServices; +using System.Text; +using Microsoft.ML.Runtime; +//#if false +using Microsoft.ML.Trainers.FastTree; +//#else +using Microsoft.ML; +using Microsoft.ML.CommandLine; +using Microsoft.ML.Data; +using Microsoft.ML.EntryPoints; +using Microsoft.ML.Internal.CpuMath; +using Microsoft.ML.Internal.Utilities; +//using Microsoft.ML.Model.OnnxConverter; +using Microsoft.ML.Numeric; +using Microsoft.ML.Transforms; +//#endif + +namespace Microsoft.ML.Trainers.XGBoost +{ + /// + /// Wrapper of Booster object of XGBoost. + /// + internal class Booster : IDisposable + { + + private bool disposed; + private readonly IntPtr _handle; + private const int normalPrediction = 0; // Value for the optionMask in prediction +#pragma warning disable CS0414 + private int numClass = 1; +#pragma warning restore CS0414 + + public IntPtr Handle => _handle; + + public Booster(Dictionary parameters, DMatrix trainDMatrix) + { + var dmats = new[] { trainDMatrix.Handle }; + var len = unchecked((ulong)dmats.Length); + var errp = WrappedXGBoostInterface.XGBoosterCreate(dmats, len, out _handle); + if (errp == -1) + { + string reason = WrappedXGBoostInterface.XGBGetLastError(); + throw new XGBoostDLLException(reason); + } + SetParameters(parameters); + } + + public Booster(DMatrix trainDMatrix) + { + var dmats = new[] { trainDMatrix.Handle }; + var len = unchecked((ulong)dmats.Length); + var errp = WrappedXGBoostInterface.XGBoosterCreate(dmats, len, out _handle); + if (errp == -1) + { + string reason = WrappedXGBoostInterface.XGBGetLastError(); + throw new XGBoostDLLException(reason); + } + } + + public void Update(DMatrix train, int iter) + { + var errp = WrappedXGBoostInterface.XGBoosterUpdateOneIter(_handle, iter, train.Handle); + if (errp == -1) + { + string reason = WrappedXGBoostInterface.XGBGetLastError(); + throw new XGBoostDLLException(reason); + } + } + + public float[] Predict(DMatrix test) + { + ulong predsLen; + IntPtr predsPtr; + /* + allowed values of optionmask: + 0:normal prediction + 1:output margin instead of transformed value + 2:output leaf index of trees instead of leaf value, note leaf index is unique per tree + 4:output feature contributions to individual predictions + + // using `0` for ntreeLimit means use all the trees + */ + + var errp = WrappedXGBoostInterface.XGBoosterPredict(_handle, test.Handle, 0, 0, 0, out predsLen, out predsPtr); + if (errp == -1) + { + string reason = WrappedXGBoostInterface.XGBGetLastError(); + throw new XGBoostDLLException(reason); + } + return XGBoostInterfaceUtils.GetPredictionsArray(predsPtr, predsLen); + } + +#if false + // Should enable XGBoosterSaveModelToBuffer + [BestFriend] + internal unsafe string GetModelString() + { + int bufLen = 2 << 15; + byte[] buffer = new byte[bufLen]; + int size = 0; + fixed (byte* ptr = buffer) + LightGbmInterfaceUtils.Check(WrappedLightGbmInterface.BoosterSaveModelToString(Handle, 0, BestIteration, bufLen, ref size, ptr)); + // If buffer size is not enough, reallocate buffer and get again. + if (size > bufLen) + { + bufLen = size; + buffer = new byte[bufLen]; + fixed (byte* ptr = buffer) + LightGbmInterfaceUtils.Check(WrappedLightGbmInterface.BoosterSaveModelToString(Handle, 0, BestIteration, bufLen, ref size, ptr)); + } + byte[] content = new byte[size]; + Array.Copy(buffer, content, size); + fixed (byte* ptr = content) + return LightGbmInterfaceUtils.GetString((IntPtr)ptr); + } +#endif + + public void SetParameters(Dictionary parameters) + { + // support internationalisation i.e. support floats with commas (e.g. 0,5F) + var nfi = new NumberFormatInfo { NumberDecimalSeparator = "." }; + + SetParameter("max_depth", ((int)parameters["max_depth"]).ToString()); + SetParameter("learning_rate", ((float)parameters["learning_rate"]).ToString(nfi)); + SetParameter("n_estimators", ((int)parameters["n_estimators"]).ToString()); + SetParameter("silent", ((bool)parameters["silent"]).ToString()); + SetParameter("objective", (string)parameters["objective"]); + SetParameter("booster", (string)parameters["booster"]); + SetParameter("tree_method", (string)parameters["tree_method"]); + + SetParameter("nthread", ((int)parameters["nthread"]).ToString()); + SetParameter("gamma", ((float)parameters["gamma"]).ToString(nfi)); + SetParameter("min_child_weight", ((int)parameters["min_child_weight"]).ToString()); + SetParameter("max_delta_step", ((int)parameters["max_delta_step"]).ToString()); + SetParameter("subsample", ((float)parameters["subsample"]).ToString(nfi)); + SetParameter("colsample_bytree", ((float)parameters["colsample_bytree"]).ToString(nfi)); + SetParameter("colsample_bylevel", ((float)parameters["colsample_bylevel"]).ToString(nfi)); + SetParameter("reg_alpha", ((float)parameters["reg_alpha"]).ToString(nfi)); + SetParameter("reg_lambda", ((float)parameters["reg_lambda"]).ToString(nfi)); + SetParameter("scale_pos_weight", ((float)parameters["scale_pos_weight"]).ToString(nfi)); + + SetParameter("base_score", ((float)parameters["base_score"]).ToString(nfi)); + SetParameter("seed", ((int)parameters["seed"]).ToString()); + SetParameter("missing", ((float)parameters["missing"]).ToString(nfi)); + + SetParameter("sample_type", (string)parameters["sample_type"]); + SetParameter("normalize_type ", (string)parameters["normalize_type"]); + SetParameter("rate_drop", ((float)parameters["rate_drop"]).ToString(nfi)); + SetParameter("one_drop", ((int)parameters["one_drop"]).ToString()); + SetParameter("skip_drop", ((float)parameters["skip_drop"]).ToString(nfi)); + + if (parameters.TryGetValue("num_class", out var value)) + { + numClass = (int)value; + SetParameter("num_class", numClass.ToString()); + } + + } + + public void SetParameter(string name, string val) + { + var errp = WrappedXGBoostInterface.XGBoosterSetParam(_handle, name, val); + + if (errp == -1) + { + string reason = WrappedXGBoostInterface.XGBGetLastError(); + throw new XGBoostDLLException(reason); + } + } + + #region Create Models + + public string[] DumpModelEx(string fmap, int with_stats, string format) + { + int length; + IntPtr treePtr; + var intptrSize = IntPtr.Size; + + WrappedXGBoostInterface.XGBoosterDumpModelEx(_handle, fmap, with_stats, format, out length, out treePtr); + + var trees = new string[length]; + int readSize = 0; + var handle2 = GCHandle.Alloc(treePtr, GCHandleType.Pinned); + + //iterate through the length of the tree ensemble and pull the strings out from the returned pointer's array of pointers. prepend python's api convention of adding booster[i] to the beginning of the tree + for (var i = 0; i < length; i++) + { + var ipt1 = Marshal.ReadIntPtr(Marshal.ReadIntPtr(handle2.AddrOfPinnedObject()), intptrSize * i); + string s = Marshal.PtrToStringAnsi(ipt1); + trees[i] = string.Format("booster[{0}]\n{1}", i, s); + var bytesToRead = (s.Length * 2) + IntPtr.Size; + readSize += bytesToRead; + } + handle2.Free(); + return trees; + } + + public void GetModel() + { + var ss = DumpModelEx("", with_stats: 0, format: "json"); + var boosterPattern = @"^booster\[\d+\]"; +#if false + List ensemble = new List(); // should probably return this +#endif + + for (int i = 0; i < ss.Length; i++) + { + var m = Regex.Matches(ss[i], boosterPattern, RegexOptions.IgnoreCase); + if ((m.Count >= 1) && (m[0].Groups.Count >= 1)) + { + // every booster representation should match + var structString = ss[i].Substring(m[0].Groups[0].Value.Length); + var doc = JsonDocument.Parse(structString); +#if false + TreeNode t = TreeNode.Create(doc.RootElement); + ensemble.Add(t); +#else + //var table = new TablePopulator(doc); +#endif + } + } + } + + private class TablePopulator + { + public Dictionary> dict = new(); + public Dictionary nodes = new(); + public Dictionary lte = new(); + public Dictionary gt = new(); + + public TablePopulator(JsonElement elm) + { + PopulateTable(elm, 0); + } + + public void PopulateTable(JsonElement elm, int level) + { + string nodeId = ""; + if (elm.TryGetProperty("nodeid", out JsonElement nodeidElm)) + { + // If this test fails, should probably bail, as the syntax of the booster is incorrect + nodeId = nodeidElm.ToString(); + nodes.Add(nodeId, level); + } + + if (!dict.ContainsKey(level)) + { + dict.Add(level, new List()); + } + + if (elm.TryGetProperty("leaf", out JsonElement leafJsonNode)) + { + dict[level].Add(elm); + } + else if (elm.TryGetProperty("children", out JsonElement internalJsonNode)) + { + dict[level].Add(elm); + if (elm.TryGetProperty("yes", out JsonElement yesNodeId)) + { + lte.Add(nodeId, yesNodeId.ToString()); + } + + if (elm.TryGetProperty("no", out JsonElement noNodeId)) + { + gt.Add(nodeId, noNodeId.ToString()); + } + + foreach (var e in internalJsonNode.EnumerateArray()) + { + PopulateTable(e, level + 1); + } + } + else + { + throw new Exception("Invalid booster content"); + } + } + } + #endregion + + #region IDisposable Support + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposed) + { + return; + } + WrappedXGBoostInterface.XGBoosterFree(_handle); + disposed = true; + } + #endregion + } +} + diff --git a/src/Microsoft.ML.XGBoost/WrappedXGDMatrix.cs b/src/Microsoft.ML.XGBoost/WrappedXGDMatrix.cs new file mode 100644 index 0000000000..ea81043646 --- /dev/null +++ b/src/Microsoft.ML.XGBoost/WrappedXGDMatrix.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.ML.Trainers.XGBoost +{ + /// + /// Wrapper of DMatrix object of XGBoost + /// + internal sealed class DMatrix : IDisposable + { + private bool disposed = false; + private IntPtr _handle; + public IntPtr Handle => _handle; + private const float Missing = 0f; + + /// + /// Create a for storing training and prediction data under XGBoost framework. + /// + public unsafe DMatrix(float[] data, uint nrows, uint ncols, float[]? labels = null) + { + int errp = WrappedXGBoostInterface.XGDMatrixCreateFromMat(data, nrows, ncols, Missing, out _handle); + if (errp == -1) + { + string reason = WrappedXGBoostInterface.XGBGetLastError(); + throw new XGBoostDLLException(reason); + } + + if (labels != null) + { + SetLabel(labels); + } + + } + + public ulong GetNumRows() + { + ulong numRows; + int errp = WrappedXGBoostInterface.XGDMatrixNumRow(_handle, out numRows); + if (errp == -1) + { + string reason = WrappedXGBoostInterface.XGBGetLastError(); + throw new XGBoostDLLException(reason); + } + return numRows; + } + + public ulong GetNumCols() + { + ulong numCols; + int errp = WrappedXGBoostInterface.XGDMatrixNumCol(_handle, out numCols); + if (errp == -1) + { + string reason = WrappedXGBoostInterface.XGBGetLastError(); + throw new XGBoostDLLException(reason); + } + return numCols; + } + + public unsafe void SetLabel(float[] labels) + { +#if false + Contracts.AssertValue(labels); + Contracts.Assert(labels.Length == GetNumRows()); +#endif + fixed (float* ptr = labels) + { + int errp = WrappedXGBoostInterface.XGDMatrixSetFloatInfo(_handle, "label", (IntPtr)ptr, (ulong)labels.Length); + if (errp == -1) + { + string reason = WrappedXGBoostInterface.XGBGetLastError(); + throw new XGBoostDLLException(reason); + } + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + void Dispose(bool disposing) + { + if (disposed) + { + return; + } + + int errp = WrappedXGBoostInterface.XGDMatrixFree(_handle); + if (errp == -1) + { + string reason = WrappedXGBoostInterface.XGBGetLastError(); + throw new XGBoostDLLException(reason); + } + disposed = true; + + } + } +} + diff --git a/src/Microsoft.ML.XGBoost/XGBoostArguments.cs b/src/Microsoft.ML.XGBoost/XGBoostArguments.cs new file mode 100644 index 0000000000..8f39c2cc58 --- /dev/null +++ b/src/Microsoft.ML.XGBoost/XGBoostArguments.cs @@ -0,0 +1,307 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Reflection; +using Microsoft.ML; +using Microsoft.ML.CommandLine; +using Microsoft.ML.EntryPoints; +using Microsoft.ML.Internal.Internallearn; +using Microsoft.ML.Runtime; +using Microsoft.ML.Trainers.XGBoost; + +[assembly: LoadableClass(typeof(GradientBooster), typeof(GradientBooster.Options), + typeof(SignatureXGBoostBooster), GradientBooster.FriendlyName, GradientBooster.Name)] +[assembly: LoadableClass(typeof(DartBooster), typeof(DartBooster.Options), + typeof(SignatureXGBoostBooster), DartBooster.FriendlyName, DartBooster.Name)] + +[assembly: EntryPointModule(typeof(GradientBooster.Options))] +[assembly: EntryPointModule(typeof(DartBooster.Options))] + +namespace Microsoft.ML.Trainers.XGBoost +{ +#if true + internal delegate void SignatureXGBoostBooster(); + + [TlcModule.ComponentKind("BoosterParameterFunction")] + internal interface IBoosterParameterFactory : IComponentFactory + { + new BoosterParameterBase CreateComponent(IHostEnvironment env); + } +#endif + + public abstract class BoosterParameterBase + { + + private protected static Dictionary NameMapping = new Dictionary() + { + {nameof(OptionsBase.MinimumSplitGain), "min_split_gain" }, +#if false + {nameof(OptionsBase.MaximumTreeDepth), "max_depth"}, + {nameof(OptionsBase.MinimumChildWeight), "min_child_weight"}, + {nameof(OptionsBase.SubsampleFraction), "subsample"}, + {nameof(OptionsBase.SubsampleFrequency), "subsample_freq"}, + {nameof(OptionsBase.L1Regularization), "reg_alpha"}, + {nameof(OptionsBase.L2Regularization), "reg_lambda"}, +#endif + }; + + + public BoosterParameterBase(OptionsBase options) + { + Contracts.CheckUserArg(options.MinimumSplitGain >= 0, nameof(OptionsBase.MinimumSplitGain), "must be >= 0."); +#if false + Contracts.CheckUserArg(options.MinimumChildWeight >= 0, nameof(OptionsBase.MinimumChildWeight), "must be >= 0."); + Contracts.CheckUserArg(options.SubsampleFraction > 0 && options.SubsampleFraction <= 1, nameof(OptionsBase.SubsampleFraction), "must be in (0,1]."); + Contracts.CheckUserArg(options.FeatureFraction > 0 && options.FeatureFraction <= 1, nameof(OptionsBase.FeatureFraction), "must be in (0,1]."); + Contracts.CheckUserArg(options.L2Regularization >= 0, nameof(OptionsBase.L2Regularization), "must be >= 0."); + Contracts.CheckUserArg(options.L1Regularization >= 0, nameof(OptionsBase.L1Regularization), "must be >= 0."); +#endif + BoosterOptions = options; + } + + public abstract class OptionsBase : IBoosterParameterFactory + { + internal BoosterParameterBase GetBooster() { return null; } + /// + /// The minimum loss reduction required to make a further partition on a leaf node of the tree. + /// + /// + /// Larger values make the algorithm more conservative. + /// + [Argument(ArgumentType.AtMostOnce, + HelpText = "Minimum loss reduction required to make a further partition on a leaf node of the tree. the larger, " + + "the more conservative the algorithm will be.")] + [TlcModule.Range(Min = 0.0)] + public double MinimumSplitGain = 0; + +#if false + /// + /// The maximum depth of a tree. + /// + /// + /// 0 means no limit. + /// + [Argument(ArgumentType.AtMostOnce, + HelpText = "Maximum depth of a tree. 0 means no limit. However, tree still grows by best-first.")] + [TlcModule.Range(Min = 0, Max = int.MaxValue)] + public int MaximumTreeDepth = 0; + + /// + /// The minimum sum of instance weight needed to form a new node. + /// + /// + /// If the tree partition step results in a leaf node with the sum of instance weight less than , + /// the building process will give up further partitioning. In linear regression mode, this simply corresponds to minimum number + /// of instances needed to be in each node. The larger, the more conservative the algorithm will be. + /// + [Argument(ArgumentType.AtMostOnce, + HelpText = "Minimum sum of instance weight(hessian) needed in a child. If the tree partition step results in a leaf " + + "node with the sum of instance weight less than min_child_weight, then the building process will give up further partitioning. In linear regression mode, " + + "this simply corresponds to minimum number of instances needed to be in each node. The larger, the more conservative the algorithm will be.")] + [TlcModule.Range(Min = 0.0)] + public double MinimumChildWeight = 0.1; + + /// + /// The frequency of performing subsampling (bagging). + /// + /// + /// 0 means disable bagging. N means perform bagging at every N iterations. + /// To enable bagging, should also be set to a value less than 1.0. + /// + [Argument(ArgumentType.AtMostOnce, + HelpText = "Subsample frequency for bagging. 0 means no subsample. " + + "Specifies the frequency at which the bagging occurs, where if this is set to N, the subsampling will happen at every N iterations." + + "This must be set with Subsample as this specifies the amount to subsample.")] + [TlcModule.Range(Min = 0, Max = int.MaxValue)] + public int SubsampleFrequency = 0; + + /// + /// The fraction of training data used for creating trees. + /// + /// + /// Setting it to 0.5 means that LightGBM randomly picks half of the data points to grow trees. + /// This can be used to speed up training and to reduce over-fitting. Valid range is (0,1]. + /// + [Argument(ArgumentType.AtMostOnce, + HelpText = "Subsample ratio of the training instance. Setting it to 0.5 means that LightGBM randomly collected " + + "half of the data instances to grow trees and this will prevent overfitting. Range: (0,1].")] + [TlcModule.Range(Inf = 0.0, Max = 1.0)] + public double SubsampleFraction = 1; + + /// + /// The fraction of features used when creating trees. + /// + /// + /// If is smaller than 1.0, LightGBM will randomly select fraction of features to train each tree. + /// For example, if you set it to 0.8, LightGBM will select 80% of features before training each tree. + /// This can be used to speed up training and to reduce over-fitting. Valid range is (0,1]. + /// + [Argument(ArgumentType.AtMostOnce, + HelpText = "Subsample ratio of columns when constructing each tree. Range: (0,1].", + ShortName = "ff")] + [TlcModule.Range(Inf = 0.0, Max = 1.0)] + public double FeatureFraction = 1; + + /// + /// The L2 regularization term on weights. + /// + /// + /// Increasing this value could help reduce over-fitting. + /// + [Argument(ArgumentType.AtMostOnce, + HelpText = "L2 regularization term on weights, increasing this value will make model more conservative.", + ShortName = "l2")] + [TlcModule.Range(Min = 0.0)] + [TGUI(Label = "Lambda(L2)", SuggestedSweeps = "0,0.5,1")] + [TlcModule.SweepableDiscreteParam("RegLambda", new object[] { 0f, 0.5f, 1f })] + public double L2Regularization = 0.01; + + /// + /// The L1 regularization term on weights. + /// + /// + /// Increasing this value could help reduce over-fitting. + /// + [Argument(ArgumentType.AtMostOnce, + HelpText = "L1 regularization term on weights, increase this value will make model more conservative.", + ShortName = "l1")] + [TlcModule.Range(Min = 0.0)] + [TGUI(Label = "Alpha(L1)", SuggestedSweeps = "0,0.5,1")] + [TlcModule.SweepableDiscreteParam("RegAlpha", new object[] { 0f, 0.5f, 1f })] + public double L1Regularization = 0; +#endif + + BoosterParameterBase IComponentFactory.CreateComponent(IHostEnvironment env) + => BuildOptions(); + + BoosterParameterBase IBoosterParameterFactory.CreateComponent(IHostEnvironment env) + => BuildOptions(); + + internal abstract BoosterParameterBase BuildOptions(); + } + + internal void UpdateParameters(Dictionary res) + { + FieldInfo[] fields = BoosterOptions.GetType().GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + foreach (var field in fields) + { + var attribute = field.GetCustomAttribute(false); + + if (attribute == null) + continue; + + var name = NameMapping.ContainsKey(field.Name) ? NameMapping[field.Name] : XGBoostInterfaceUtils.GetOptionName(field.Name); + res[name] = field.GetValue(BoosterOptions); + } + } + + /// + /// Create for supporting legacy infra built upon . + /// + internal abstract IBoosterParameterFactory BuildFactory(); + internal abstract string BoosterName { get; } + + private protected OptionsBase BoosterOptions; + } + + /// + /// Gradient boosting decision tree. + /// + /// + /// For details, please see gradient tree boosting. + /// + public sealed class GradientBooster : BoosterParameterBase + { + internal const string Name = "gbdt"; + internal const string FriendlyName = "Tree Booster"; + + /// + /// The options for , used for setting . + /// + [TlcModule.Component(Name = Name, FriendlyName = FriendlyName, Desc = "Traditional Gradient Boosting Decision Tree.")] + public sealed class Options : OptionsBase + { + internal override BoosterParameterBase BuildOptions() => new GradientBooster(this); + } + + internal GradientBooster(Options options) + : base(options) + { + } + + internal override IBoosterParameterFactory BuildFactory() => BoosterOptions; + + internal override string BoosterName => Name; + + } + + + /// + /// DART booster (Dropouts meet Multiple Additive Regression Trees) + /// + /// + /// For details, please see here. + /// + public sealed class DartBooster : BoosterParameterBase + { + internal const string Name = "dart"; + internal const string FriendlyName = "Tree Dropout Tree Booster"; + + + /// + /// The options for , used for setting . + /// + [TlcModule.Component(Name = Name, FriendlyName = FriendlyName, Desc = "Dropouts meet Multiple Additive Regresion Trees. See https://arxiv.org/abs/1505.01866")] + public sealed class Options : OptionsBase + { + static Options() + { + // Add additional name mappings + NameMapping.Add(nameof(TreeDropFraction), "rate_drop"); + NameMapping.Add(nameof(SkipDropFraction), "skip_drop"); + } + + /// + /// The dropout rate, i.e. the fraction of previous trees to drop during the dropout. + /// + /// + /// Valid range is [0,1]. + /// + [Argument(ArgumentType.AtMostOnce, HelpText = "The drop ratio for trees. Range:(0,1).")] + [TlcModule.Range(Inf = 0.0, Max = 1.0)] + public double TreeDropFraction = 0.1; + + /// + /// The probability of skipping the dropout procedure during a boosting iteration. + /// + [Argument(ArgumentType.AtMostOnce, HelpText = "Probability for not dropping in a boosting round.")] + [TlcModule.Range(Inf = 0.0, Max = 1.0)] + public double SkipDropFraction = 0.5; + +#if false + /// + /// Whether to enable uniform drop. + /// Allowed values: "uniform" or "weighted" + /// + [Argument(ArgumentType.AtMostOnce, HelpText = "True will enable uniform drop.")] + public bool UniformDrop = false; +#endif + + internal override BoosterParameterBase BuildOptions() => new DartBooster(this); + } + + internal DartBooster(Options options) + : base(options) + { + Contracts.CheckUserArg(options.TreeDropFraction > 0 && options.TreeDropFraction < 1, nameof(options.TreeDropFraction), "must be in (0,1)."); + Contracts.CheckUserArg(options.SkipDropFraction >= 0 && options.SkipDropFraction < 1, nameof(options.SkipDropFraction), "must be in [0,1)."); + BoosterOptions = options; + } + + internal override IBoosterParameterFactory BuildFactory() => BoosterOptions; + internal override string BoosterName => Name; + } + +} diff --git a/src/Microsoft.ML.XGBoost/XGBoostCatalog.cs b/src/Microsoft.ML.XGBoost/XGBoostCatalog.cs new file mode 100644 index 0000000000..45e5e3b3e7 --- /dev/null +++ b/src/Microsoft.ML.XGBoost/XGBoostCatalog.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Text; + +using Microsoft.ML.Data; +using Microsoft.ML.Runtime; +using Microsoft.ML.Trainers.XGBoost; + +namespace Microsoft.ML +{ + /// + /// Collection of extension methods for the and + /// catalogs. + /// + public static class XGBoostExtensions + { + /// + /// Create , which predicts a target using a gradient boosting decision tree regression model. + /// + /// The . + /// The name of the label column. The column data must be . + /// The name of the feature column. The column data must be a known-sized vector of . + /// The name of the example weight column (optional). + /// The maximum number of leaves in one tree. + /// The minimal number of data points required to form a new tree leaf. + /// The learning rate. + /// The number of boosting iterations. A new tree is created in each iteration, so this is equivalent to the number of trees. + public static XGBoostRegressionTrainer LightGbm(this RegressionCatalog.RegressionTrainers catalog, + string labelColumnName = DefaultColumnNames.Label, + string featureColumnName = DefaultColumnNames.Features, + string exampleWeightColumnName = null, + int? numberOfLeaves = null, + int? minimumExampleCountPerLeaf = null, + double? learningRate = null, + int numberOfIterations = Defaults.NumberOfIterations) + { + Contracts.CheckValue(catalog, nameof(catalog)); + var env = CatalogUtils.GetEnvironment(catalog); + return new XGBoostRegressionTrainer(env, labelColumnName, featureColumnName, exampleWeightColumnName, numberOfLeaves, minimumExampleCountPerLeaf, learningRate, numberOfIterations); + } + + public static XGBoostRegressionTrainer XGBoost(this RegressionCatalog.RegressionTrainers catalog, + XGBoostRegressionTrainer.Options options) + { + Contracts.CheckValue(catalog, nameof(catalog)); + var env = CatalogUtils.GetEnvironment(catalog); + return new XGBoostRegressionTrainer(env, options); + } + } +} diff --git a/src/Microsoft.ML.XGBoost/XGBoostInterface.cs b/src/Microsoft.ML.XGBoost/XGBoostInterface.cs new file mode 100644 index 0000000000..9c99f40a1a --- /dev/null +++ b/src/Microsoft.ML.XGBoost/XGBoostInterface.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Microsoft.ML.Runtime; + +namespace Microsoft.ML.Trainers.XGBoost +{ + +#pragma warning disable MSML_GeneralName + public class XGBoostDLLException : Exception +#pragma warning restore MSML_GeneralName + { + public XGBoostDLLException() + { + /* empty */ + } + + public XGBoostDLLException(string message) + : base(message) + { + /* empty */ + } + + } + + public static class XGBoost + { + public struct XGBoostVersion + { + public int Major; + public int Minor; + public int Patch; + } + + public static XGBoostVersion Version() + { + int major, minor, patch; + WrappedXGBoostInterface.XGBoostVersion(out major, out minor, out patch); + return new XGBoostVersion + { + Major = major, + Minor = minor, + Patch = patch + }; + } + } +} diff --git a/src/Microsoft.ML.XGBoost/XGBoostRegressionTrainer.cs b/src/Microsoft.ML.XGBoost/XGBoostRegressionTrainer.cs new file mode 100644 index 0000000000..785efd9a7e --- /dev/null +++ b/src/Microsoft.ML.XGBoost/XGBoostRegressionTrainer.cs @@ -0,0 +1,231 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.ML; +using Microsoft.ML.CommandLine; +using Microsoft.ML.Data; +using Microsoft.ML.EntryPoints; +using Microsoft.ML.Runtime; +using Microsoft.ML.Trainers.FastTree; +using Microsoft.ML.Trainers.XGBoost; + +[assembly: LoadableClass(XGBoostRegressionTrainer.Summary, typeof(XGBoostRegressionTrainer), typeof(XGBoostRegressionTrainer.Options), + new[] { typeof(SignatureRegressorTrainer), typeof(SignatureTrainer), typeof(SignatureTreeEnsembleTrainer) }, + XGBoostRegressionTrainer.UserNameValue, XGBoostRegressionTrainer.LoadNameValue, XGBoostRegressionTrainer.ShortName)] + +[assembly: LoadableClass(typeof(XGBoostRegressionModelParameters), null, typeof(SignatureLoadModel), + "XGBoost Regression Executor", + XGBoostRegressionModelParameters.LoaderSignature)] +namespace Microsoft.ML.Trainers.XGBoost +{ + /// + /// Model parameters for . + /// + public sealed class XGBoostRegressionModelParameters : TreeEnsembleModelParametersBasedOnRegressionTree + { + internal const string LoaderSignature = "XGBoostRegressionExec"; + internal const string RegistrationName = "XGBoostRegressionPredictor"; + + private static VersionInfo GetVersionInfo() + { + // REVIEW: can we decouple the version from FastTree predictor version ? + return new VersionInfo( + modelSignature: "XGBSIREG", + verWrittenCur: 0x00010001, // Initial (not sure if "Categorical splits") are supported + // verWrittenCur: 0x00010002, // _numFeatures serialized + // verWrittenCur: 0x00010003, // Ini content out of predictor + // verWrittenCur: 0x00010004, // Add _defaultValueForMissing + verReadableCur: 0x00010004, + verWeCanReadBack: 0x00010001, + loaderSignature: LoaderSignature, + loaderAssemblyName: typeof(XGBoostRegressionModelParameters).Assembly.FullName); + } + private protected override uint VerNumFeaturesSerialized => 0x00010002; + private protected override uint VerDefaultValueSerialized => 0x00010004; + private protected override uint VerCategoricalSplitSerialized => 0x00010005; + private protected override PredictionKind PredictionKind => PredictionKind.Regression; + + internal XGBoostRegressionModelParameters(IHostEnvironment env, InternalTreeEnsemble trainedEnsemble, int featureCount, string innerArgs) + : base(env, RegistrationName, trainedEnsemble, featureCount, innerArgs) + { + } + + private XGBoostRegressionModelParameters(IHostEnvironment env, ModelLoadContext ctx) + : base(env, RegistrationName, ctx, GetVersionInfo()) + { + } + + private protected override void SaveCore(ModelSaveContext ctx) + { + base.SaveCore(ctx); + ctx.SetVersionInfo(GetVersionInfo()); + } + + internal static XGBoostRegressionModelParameters Create(IHostEnvironment env, ModelLoadContext ctx) + { + Contracts.CheckValue(env, nameof(env)); + env.CheckValue(ctx, nameof(ctx)); + ctx.CheckAtModel(GetVersionInfo()); + return new XGBoostRegressionModelParameters(env, ctx); + } + } + + /// + /// The for training a boosted decision tree regression model using XGBoost. + /// + public sealed class XGBoostRegressionTrainer : + XGBoostTrainerBase, XGBoostRegressionModelParameters> + { + internal new const string Summary = "XGBoost Regression"; + internal new const string LoadNameValue = "XGBoostRegression"; + internal const string ShortName = "XGBoostR"; + internal new const string UserNameValue = "XGBoost Regressor"; + + public XGBoostRegressionTrainer(IHost host, SchemaShape.Column feature, SchemaShape.Column label, SchemaShape.Column weight = default, SchemaShape.Column groupId = default) : base(host, feature, label, weight, groupId) + { + } + + public override TrainerInfo Info => throw new System.NotImplementedException(); + + private protected override PredictionKind PredictionKind => PredictionKind.Regression; + + private protected override SchemaShape.Column[] GetOutputColumnsCore(SchemaShape inputSchema) + { + return new[] + { + new SchemaShape.Column(DefaultColumnNames.Score, SchemaShape.Column.VectorKind.Scalar, NumberDataViewType.Single, false, new SchemaShape(AnnotationUtils.GetTrainerOutputAnnotation())) + }; + } + + private protected override RegressionPredictionTransformer MakeTransformer(XGBoostRegressionModelParameters model, DataViewSchema trainSchema) + => new RegressionPredictionTransformer(Host, model, trainSchema, FeatureColumn.Name); + + /// + /// Options for the as used in + /// [XGBoost(Options)](xref:Microsoft.ML.XGBoostExtensions.XGBoost(Microsoft.ML.RegressionCatalog.RegressionTrainers,Microsoft.ML.Trainers.XGBoost.XGBoostRegressionTrainer.Options)). + /// + public sealed class Options : OptionsBase + { + public enum EvaluateMetricType + { + None, + Default, + MeanAbsoluteError, + RootMeanSquaredError, + MeanSquaredError + }; + + /// + /// Determines what evaluation metric to use. + /// + [Argument(ArgumentType.AtMostOnce, + HelpText = "Evaluation metrics.", + ShortName = "em")] + public EvaluateMetricType EvaluationMetric = EvaluateMetricType.RootMeanSquaredError; + +#if false + static Options() + { + NameMapping.Add(nameof(EvaluateMetricType), "metric"); + NameMapping.Add(nameof(EvaluateMetricType.None), "None"); + NameMapping.Add(nameof(EvaluateMetricType.Default), ""); + NameMapping.Add(nameof(EvaluateMetricType.MeanAbsoluteError), "mae"); + NameMapping.Add(nameof(EvaluateMetricType.RootMeanSquaredError), "rmse"); + NameMapping.Add(nameof(EvaluateMetricType.MeanSquaredError), "mse"); + } + + internal override Dictionary ToDictionary(IHost host) + { + var res = base.ToDictionary(host); + res[GetOptionName(nameof(EvaluateMetricType))] = GetOptionName(EvaluationMetric.ToString()); + + return res; + } +#endif + } + + /// + /// Initializes a new instance of + /// + /// The private instance of . + /// The name of the label column. + /// The name of the feature column. + /// The name of the example weight column (optional). + /// The number of leaves to use. + /// The minimal number of data points allowed in a leaf of the tree, out of the subsampled data. + /// The learning rate. + /// Number of iterations. + internal XGBoostRegressionTrainer(IHostEnvironment env, + string labelColumnName = DefaultColumnNames.Label, + string featureColumnName = DefaultColumnNames.Features, + string exampleWeightColumnName = null, + int? numberOfLeaves = null, + int? minimumExampleCountPerLeaf = null, + double? learningRate = null, + int numberOfIterations = Defaults.NumberOfIterations + ) + : this(env, new Options() + { + LabelColumnName = labelColumnName, + FeatureColumnName = featureColumnName, + ExampleWeightColumnName = exampleWeightColumnName, +#if false + NumberOfLeaves = numberOfLeaves, + MinimumExampleCountPerLeaf = minimumExampleCountPerLeaf, + LearningRate = learningRate, + NumberOfIterations = numberOfIterations +#endif + }) + { + } + + internal XGBoostRegressionTrainer(IHostEnvironment env, Options options) + : base(env, LoadNameValue, options, TrainerUtils.MakeR4ScalarColumn(options.LabelColumnName)) + { + } + + private protected override XGBoostRegressionModelParameters CreatePredictor() + { + Host.Check(TrainedEnsemble != null, + "The predictor cannot be created before training is complete"); +#if true + return null; +#else + var innerArgs = LightGbmInterfaceUtils.JoinParameters(GbmOptions); + return new LightGbmRegressionModelParameters(Host, TrainedEnsemble, FeatureCount, innerArgs); +#endif + } + +#if false + private protected override void CheckDataValid(IChannel ch, RoleMappedData data) + { + Host.AssertValue(ch); + base.CheckDataValid(ch, data); + var labelType = data.Schema.Label.Value.Type; + if (!(labelType is BooleanDataViewType || labelType is KeyDataViewType || labelType == NumberDataViewType.Single)) + { + throw ch.ExceptParam(nameof(data), + $"Label column '{data.Schema.Label.Value.Name}' is of type '{labelType.RawType}', but must be an unsigned int, boolean or float."); + } + } + + private protected override void CheckAndUpdateParametersBeforeTraining(IChannel ch, RoleMappedData data, float[] labels, int[] groups) + { + GbmOptions["objective"] = "regression"; + } + + private protected override SchemaShape.Column[] GetOutputColumnsCore(SchemaShape inputSchema) + { + return new[] + { + new SchemaShape.Column(DefaultColumnNames.Score, SchemaShape.Column.VectorKind.Scalar, NumberDataViewType.Single, false, new SchemaShape(AnnotationUtils.GetTrainerOutputAnnotation())) + }; + } + + private protected override RegressionPredictionTransformer MakeTransformer(LightGbmRegressionModelParameters model, DataViewSchema trainSchema) + => new RegressionPredictionTransformer(Host, model, trainSchema, FeatureColumn.Name); +#endif + } +} diff --git a/src/Microsoft.ML.XGBoost/XGBoostTrainerBase.cs b/src/Microsoft.ML.XGBoost/XGBoostTrainerBase.cs new file mode 100644 index 0000000000..80d49e9c7c --- /dev/null +++ b/src/Microsoft.ML.XGBoost/XGBoostTrainerBase.cs @@ -0,0 +1,224 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.ML.CommandLine; +using Microsoft.ML.Data; +using Microsoft.ML.EntryPoints; +using Microsoft.ML.Internal.Internallearn; +using Microsoft.ML.Internal.Utilities; +using Microsoft.ML.Runtime; +using Microsoft.ML.Trainers; +using Microsoft.ML.Trainers.FastTree; + +namespace Microsoft.ML.Trainers.XGBoost +{ + [BestFriend] + internal static class Defaults + { + public const int NumberOfIterations = 100; + } + + public abstract class XGBoostTrainerBase : TrainerEstimatorBaseWithGroupId + where TTransformer : ISingleFeaturePredictionTransformer + where TModel : class // IPredictorProducing + where TOptions : XGBoostTrainerBase.OptionsBase, new() + { + internal const string LoadNameValue = "XGBoostPredictor"; + internal const string UserNameValue = "XGBoost Predictor"; + internal const string Summary = "The base logic for all XGBoost-based trainers."; + + private protected int FeatureCount; + private protected InternalTreeEnsemble TrainedEnsemble; + +#if false + /// + /// The shrinkage rate for trees, used to prevent over-fitting. + /// Also aliased to "eta" + /// + /// + /// Valid range is (0,1]. + /// + [Argument(ArgumentType.AtMostOnce, + HelpText = "Shrinkage rate for trees, used to prevent over-fitting. Range: (0,1].", + SortOrder = 2, ShortName = "lr", NullName = "")] + public double? LearningRate; + + /// + /// The maximum number of leaves in one tree. + /// + [Argument(ArgumentType.AtMostOnce, HelpText = "Maximum leaves for trees.", + SortOrder = 2, ShortName = "nl", NullName = "")] + public int? NumberOfLeaves; + + /// + /// Minimum loss reduction required to make a further partition on a leaf node of + /// the tree. The larger gamma is, the more conservative the algorithm will be. + /// aka: gamma + /// range: [0,\infnty] + /// + public int? MinSplitLoss; +#endif + + /// + /// Maximum depth of a tree. Increasing this value will make the model more complex and + /// more likely to overfit. 0 indicates no limit on depth. Beware that XGBoost aggressively + /// consumes memory when training a deep tree. exact tree method requires non-zero value. + /// range: [0,\infnty], default=6 + /// + public int? MaxDepth; + + /// + /// Minimum sum of instance weight (hessian) needed in a child. If the tree partition step + /// results in a leaf node with the sum of instance weight less than min_child_weight, then + /// the building process will give up further partitioning. In linear regression task, this + /// simply corresponds to minimum number of instances needed to be in each node. The larger + /// MinChildWeight is, the more conservative the algorithm will be. + /// range: [0,\infnty] + /// + public float? MinChildWeight; + + private protected XGBoostTrainerBase(IHost host, + SchemaShape.Column feature, + SchemaShape.Column label, SchemaShape.Column weight = default, SchemaShape.Column groupId = default) : base(host, feature, label, weight, groupId) + { + } + +#if false + /// + /// L2 regularization term on weights. Increasing this value will make model more conservative + /// + public float? L2Regularization; + + /// + /// L1 regularization term on weights. Increasing this value will make model more conservative. + /// + public float? L1Regularization; +#endif + + public class OptionsBase : TrainerInputBaseWithGroupId + { + + // Static override name map that maps friendly names to XGBMArguments arguments. + // If an argument is not here, then its name is identical to a lightGBM argument + // and does not require a mapping, for example, Subsample. + // For a complete list, see https://xgboost.readthedocs.io/en/latest/parameter.html + private protected static Dictionary NameMapping = new Dictionary() + { +#if false + {nameof(MinSplitLoss), "min_split_loss"}, + {nameof(NumberOfLeaves), "num_leaves"}, +#endif + {nameof(MaxDepth), "max_depth" }, + {nameof(MinChildWeight), "min_child_weight" }, +#if false + {nameof(L2Regularization), "lambda" }, + {nameof(L1Regularization), "alpha" } +#endif + }; + + private BoosterParameterBase.OptionsBase _boosterParameter; + +#if true + /// + /// Determines which booster to use. + /// + /// + /// Available boosters are , and . + /// + [Argument(ArgumentType.Multiple, + HelpText = "Which booster to use, can be gbtree, gblinear or dart. gbtree and dart use tree based model while gblinear uses linear function.", + Name = "Booster", + SortOrder = 3)] + internal IBoosterParameterFactory BoosterFactory = new GradientBooster.Options(); +#endif + + /// + /// Booster parameter to use + /// + public BoosterParameterBase.OptionsBase Booster + { + get => _boosterParameter; + + set + { + _boosterParameter = value; + BoosterFactory = _boosterParameter; + } + + } + + private protected string GetOptionName(string name) + { + if (NameMapping.ContainsKey(name)) + return NameMapping[name]; + return XGBoostInterfaceUtils.GetOptionName(name); + } + } + + private protected override TModel TrainModelCore(TrainContext context) + { +#if true + return null; +#else + InitializeBeforeTraining(); + + Host.CheckValue(context, nameof(context)); + + Dataset dtrain = null; + Dataset dvalid = null; + CategoricalMetaData catMetaData; + try + { + using (var ch = Host.Start("Loading data for XGBoost")) + { + using (var pch = Host.StartProgressChannel("Loading data for XGBoost")) + { + dtrain = LoadTrainingData(ch, context.TrainingSet, out catMetaData); + if (context.ValidationSet != null) + dvalid = LoadValidationData(ch, dtrain, context.ValidationSet, catMetaData); + } + } + using (var ch = Host.Start("Training with XGBoost")) + { + using (var pch = Host.StartProgressChannel("Training with XGBoost")) + TrainCore(ch, pch, dtrain, catMetaData, dvalid); + } + } + finally + { + dtrain?.Dispose(); + dvalid?.Dispose(); + DisposeParallelTraining(); + } + return CreatePredictor(); +#endif + } + + private protected XGBoostTrainerBase(IHostEnvironment env, string name, TOptions options, SchemaShape.Column label) + : base(Contracts.CheckRef(env, nameof(env)).Register(name), TrainerUtils.MakeR4VecFeature(options.FeatureColumnName), label, + TrainerUtils.MakeR4ScalarWeightColumn(options.ExampleWeightColumnName), TrainerUtils.MakeU4ScalarColumn(options.RowGroupColumnName)) + { + Host.CheckValue(options, nameof(options)); +#if false + Contracts.CheckUserArg(options.NumberOfIterations >= 0, nameof(options.NumberOfIterations), "must be >= 0."); + Contracts.CheckUserArg(options.MaximumBinCountPerFeature > 0, nameof(options.MaximumBinCountPerFeature), "must be > 0."); + Contracts.CheckUserArg(options.MinimumExampleCountPerGroup > 0, nameof(options.MinimumExampleCountPerGroup), "must be > 0."); + Contracts.CheckUserArg(options.MaximumCategoricalSplitPointCount > 0, nameof(options.MaximumCategoricalSplitPointCount), "must be > 0."); + Contracts.CheckUserArg(options.CategoricalSmoothing >= 0, nameof(options.CategoricalSmoothing), "must be >= 0."); + Contracts.CheckUserArg(options.L2CategoricalRegularization >= 0.0, nameof(options.L2CategoricalRegularization), "must be >= 0."); +#endif + +#if false + XGBoostTrainerOptions = options; + GbmOptions = XGBoostTrainerOption.ToDictionary(Host); +#endif + } + + private protected abstract TModel CreatePredictor(); + } +} From 3a0aea55940145524b071fdcf539db1a0c22073a Mon Sep 17 00:00:00 2001 From: "R. G. Esteves" Date: Wed, 16 Nov 2022 07:29:32 -0800 Subject: [PATCH 2/2] Address some naming issues (via disabling the syntax check) --- src/Microsoft.ML.XGBoost/WrappedXGBoostInterface.cs | 2 ++ src/Microsoft.ML.XGBoost/WrappedXGBooster.cs | 10 ++++++---- src/Microsoft.ML.XGBoost/WrappedXGDMatrix.cs | 6 ++++++ src/Microsoft.ML.XGBoost/XGBoostInterface.cs | 4 +++- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.ML.XGBoost/WrappedXGBoostInterface.cs b/src/Microsoft.ML.XGBoost/WrappedXGBoostInterface.cs index 3f885e4f31..141e0ec60c 100644 --- a/src/Microsoft.ML.XGBoost/WrappedXGBoostInterface.cs +++ b/src/Microsoft.ML.XGBoost/WrappedXGBoostInterface.cs @@ -88,11 +88,13 @@ public static extern int XGBoosterPredict(IntPtr bHandle, IntPtr dHandle, #endregion #region API serialization +#pragma warning disable MSML_ParameterLocalVarName [DllImport(DllName)] public static extern int XGBoosterDumpModel(IntPtr handle, string fmap, int with_stats, out int out_len, out IntPtr dumpStr); [DllImport(DllName)] public static extern int XGBoosterDumpModelEx(IntPtr handle, string fmap, int with_stats, string format, out int out_len, out IntPtr dumpStr); +#pragma warning restore MSML_ParameterLocalVarName #endregion } diff --git a/src/Microsoft.ML.XGBoost/WrappedXGBooster.cs b/src/Microsoft.ML.XGBoost/WrappedXGBooster.cs index 5d1cb1a280..8bc45aba95 100644 --- a/src/Microsoft.ML.XGBoost/WrappedXGBooster.cs +++ b/src/Microsoft.ML.XGBoost/WrappedXGBooster.cs @@ -32,13 +32,12 @@ namespace Microsoft.ML.Trainers.XGBoost /// internal class Booster : IDisposable { - +#pragma warning disable MSML_PrivateFieldName // Private field name not in: _camelCase format private bool disposed; private readonly IntPtr _handle; private const int normalPrediction = 0; // Value for the optionMask in prediction -#pragma warning disable CS0414 private int numClass = 1; -#pragma warning restore CS0414 +#pragma warning restore MSML_PrivateFieldName public IntPtr Handle => _handle; @@ -179,8 +178,9 @@ public void SetParameter(string name, string val) } #region Create Models - +#pragma warning disable MSML_ParameterLocalVarName public string[] DumpModelEx(string fmap, int with_stats, string format) +#pragma warning restore MSML_ParameterLocalVarName { int length; IntPtr treePtr; @@ -233,10 +233,12 @@ public void GetModel() private class TablePopulator { +#pragma warning disable MSML_GeneralName public Dictionary> dict = new(); public Dictionary nodes = new(); public Dictionary lte = new(); public Dictionary gt = new(); +#pragma warning restore MSML_GeneralName public TablePopulator(JsonElement elm) { diff --git a/src/Microsoft.ML.XGBoost/WrappedXGDMatrix.cs b/src/Microsoft.ML.XGBoost/WrappedXGDMatrix.cs index ea81043646..cd2d797525 100644 --- a/src/Microsoft.ML.XGBoost/WrappedXGDMatrix.cs +++ b/src/Microsoft.ML.XGBoost/WrappedXGDMatrix.cs @@ -12,14 +12,19 @@ namespace Microsoft.ML.Trainers.XGBoost /// internal sealed class DMatrix : IDisposable { +#pragma warning disable MSML_PrivateFieldName private bool disposed = false; +#pragma warning restore MSML_PrivateFieldName +#pragma warning disable IDE0044 private IntPtr _handle; +#pragma warning restore IDE0044 public IntPtr Handle => _handle; private const float Missing = 0f; /// /// Create a for storing training and prediction data under XGBoost framework. /// +#nullable enable public unsafe DMatrix(float[] data, uint nrows, uint ncols, float[]? labels = null) { int errp = WrappedXGBoostInterface.XGDMatrixCreateFromMat(data, nrows, ncols, Missing, out _handle); @@ -35,6 +40,7 @@ public unsafe DMatrix(float[] data, uint nrows, uint ncols, float[]? labels = nu } } +#nullable disable public ulong GetNumRows() { diff --git a/src/Microsoft.ML.XGBoost/XGBoostInterface.cs b/src/Microsoft.ML.XGBoost/XGBoostInterface.cs index 9c99f40a1a..56d8cd3ad5 100644 --- a/src/Microsoft.ML.XGBoost/XGBoostInterface.cs +++ b/src/Microsoft.ML.XGBoost/XGBoostInterface.cs @@ -39,7 +39,9 @@ public struct XGBoostVersion public static XGBoostVersion Version() { - int major, minor, patch; + int major; + int minor; + int patch; WrappedXGBoostInterface.XGBoostVersion(out major, out minor, out patch); return new XGBoostVersion {