diff --git a/README/ReleaseNotes/v642/index.md b/README/ReleaseNotes/v642/index.md index 88736926acd8a..4261292ca1193 100644 --- a/README/ReleaseNotes/v642/index.md +++ b/README/ReleaseNotes/v642/index.md @@ -50,6 +50,7 @@ The following people have contributed to this new version: * The inclusion by external projects of Makefile templates contained within ROOT is deprecated in 6.42, a warning will be raised if you use them. These files will be removed in ROOT 7. * The conversion from Python set to **RooArgSet** is deprecated and won't work anymore in ROOT 6.44. The problem is that Python sets are unordered while RooArgSets are ordered, and this mismatch can lead to subtle problems later on. Prefer conversion from Python lists or tuples, which are ordered too. * The ROOT IO capability for the `TMVA::Experimental::SOFIE::RModel` has been removed. Users should not be encouraged to serialize models in experimental classes. For the serialization of ONNX models one can already use ONNX directly, and even serialize the ONNX bytes to a ROOT file if required. +* The ROOT IO capability for the `TMVA::Experimental::RBDT` class has been removed, along with the `TMVA.Experimental.SaveXGBoost` Python function. Experimental classes should not be persistified since their on-disk layout is not guaranteed to be stable. An `RBDT` is now built directly from an XGBoost model in its native JSON serialization with the new `TMVA::Experimental::RBDT::LoadXGBoost(jsonPath)`, which works both from C++ and Python. To convert a trained model, save it first with XGBoost's `Booster.save_model("model.json")` and then load it with `LoadXGBoost`. * The **JsMVA** feature for interactive TMVA training in Jupyter notebooks is now removed. It was not functional for years and was therefore already excluded from ROOT 6.38. This also removes the `TMVA::IPythonInteractive` class and the related interactive-training interfaces from the TMVA method and fitter classes, such as `MethodBase::ExitFromTraining()` or `FitterBase::SetIPythonInteractive()`. * The **RooStats::DebuggingSampler** and **RooStats::DebuggingTestStat** classes are removed. They were mock implementations of the `TestStatSampler` and `TestStatistic` interfaces that returned uniform random numbers independent of the data, only meant for debugging the RooStats framework itself during its initial development. diff --git a/bindings/pyroot/pythonizations/python/CMakeLists.txt b/bindings/pyroot/pythonizations/python/CMakeLists.txt index c7a4de7c4fab4..331a5cca5718f 100644 --- a/bindings/pyroot/pythonizations/python/CMakeLists.txt +++ b/bindings/pyroot/pythonizations/python/CMakeLists.txt @@ -58,7 +58,6 @@ if(tmva) ROOT/_pythonization/_tmva/__init__.py ROOT/_pythonization/_tmva/_rbdt.py ROOT/_pythonization/_tmva/_rtensor.py - ROOT/_pythonization/_tmva/_tree_inference.py ROOT/_pythonization/_tmva/_utils.py ROOT/_pythonization/_tmva/_gnn.py ROOT/_pythonization/_tmva/_sofie/_parser/_keras/__init__.py diff --git a/bindings/pyroot/pythonizations/python/ROOT/_facade.py b/bindings/pyroot/pythonizations/python/ROOT/_facade.py index b845b77dc852f..7aa486ab5efba 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_facade.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_facade.py @@ -567,13 +567,11 @@ def TMVA(self): from ._pythonization._tmva._rtensor import _AsRTensor from ._pythonization._tmva._sofie._parser._keras.parser import PyKeras from ._pythonization._tmva._sofie._parser._pytorch.parser import PyTorch - from ._pythonization._tmva._tree_inference import SaveXGBoost setattr(ns.Experimental.SOFIE, "PyKeras", PyKeras) setattr(ns.Experimental.SOFIE, "PyTorch", PyTorch) ns.Experimental.AsRTensor = _AsRTensor - ns.Experimental.SaveXGBoost = SaveXGBoost except ImportError: # _tmva submodule not available (expected for tmva=OFF) pass diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_tree_inference.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_tree_inference.py deleted file mode 100644 index 7a8bb4b4fbd91..0000000000000 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_tree_inference.py +++ /dev/null @@ -1,94 +0,0 @@ -# Author: Stefan Wunsch CERN 09/2019 - -################################################################################ -# Copyright (C) 1995-2019, Rene Brun and Fons Rademakers. # -# All rights reserved. # -# # -# For the licensing terms see $ROOTSYS/LICENSE. # -# For the list of contributors see $ROOTSYS/README/CREDITS. # -################################################################################ - -import json - - -def get_basescore(model): - """Get base score from an XGBoost sklearn estimator. - - Copy-pasted from XGBoost unit test code. - - See also: - * https://github.com/dmlc/xgboost/blob/2463938/python-package/xgboost/testing/updater.py#L43 - * https://github.com/dmlc/xgboost/issues/9347 - * https://discuss.xgboost.ai/t/how-to-get-base-score-from-trained-booster/3192 - """ - jintercept = json.loads(model.get_booster().save_config())["learner"]["learner_model_param"]["base_score"] - out = json.loads(jintercept) - if isinstance(out, float): - return out - # For XGBoost 3.1.0 and after, the value is itself a list. - # However, we don't support multiple base scores yet. - if len(out) > 1: - raise ValueError( - f"Model contains multiple base scores ({out}). " - "This typically occurs with XGBoost ≥ 3.1.0, which supports multi-target base scores. " - "This function only supports a single base score. " - ) - return out[0] - - -def SaveXGBoost(xgb_model, key_name, output_path, num_inputs): - """ - Saves the XGBoost model to a ROOT file as a TMVA::Experimental::RBDT object. - - Args: - xgb_model: The trained XGBoost model. - key_name (str): The name to use for storing the RBDT in the output file. - output_path (str): The path to save the output file. - num_inputs (int): The number of input features used in the model. - - Raises: - Exception: If the XGBoost model has an unsupported objective. - """ - import ROOT - - # Extract objective - objective_map = { - "multi:softprob": "softmax", # Naming the objective softmax is more common today - "binary:logistic": "logistic", - "reg:linear": "identity", - "reg:squarederror": "identity", - } - model_objective = xgb_model.objective - if model_objective not in objective_map: - raise Exception( - 'XGBoost model has unsupported objective "{}". Supported objectives are {}.'.format( - model_objective, objective_map.keys() - ) - ) - objective = ROOT.std.string(objective_map[model_objective]) - - # Determine number of outputs - num_outputs = xgb_model.n_classes_ if "multi:" in model_objective else 1 - - # Dump XGB model as json file - xgb_model.get_booster().dump_model(output_path, dump_format="json") - - # Dump XGB model as txt file - xgb_model.get_booster().dump_model(output_path) - - if xgb_model.get_booster().feature_names is None: - features = ROOT.std.vector["std::string"]([f"f{i}" for i in range(num_inputs)]) - else: - features = ROOT.std.vector["std::string"](xgb_model.get_booster().feature_names) - bs = get_basescore(xgb_model) - logistic = objective == "logistic" - bdt = ROOT.TMVA.Experimental.RBDT.LoadText( - output_path, - features, - num_outputs, - logistic, - ROOT.std.log(bs / (1.0 - bs)) if logistic else bs, - ) - - with ROOT.TFile.Open(output_path, "RECREATE") as tFile: - tFile.WriteObject(bdt, key_name) diff --git a/tmva/tmva/CMakeLists.txt b/tmva/tmva/CMakeLists.txt index 1adae4b59430f..d36b33341a15e 100644 --- a/tmva/tmva/CMakeLists.txt +++ b/tmva/tmva/CMakeLists.txt @@ -455,6 +455,8 @@ ROOT_STANDARD_LIBRARY_PACKAGE(TMVAUtils ${EXTRA_DICT_OPTS} ) +# RBDT::LoadXGBoost parses the XGBoost native JSON serialization. +target_link_libraries(TMVAUtils PRIVATE nlohmann_json::nlohmann_json) endif() ROOT_ADD_TEST_SUBDIRECTORY(test) diff --git a/tmva/tmva/inc/LinkDefUtils.h b/tmva/tmva/inc/LinkDefUtils.h index 82709731c20f2..68de99aeacd8e 100644 --- a/tmva/tmva/inc/LinkDefUtils.h +++ b/tmva/tmva/inc/LinkDefUtils.h @@ -9,11 +9,6 @@ #pragma link C++ nestedclass; -#ifdef R__HAS_DATAFRAME -// BDT inference -#pragma link C++ class TMVA::Experimental::RBDT+; -#endif - // RTensor will have its own streamer function #pragma link C++ class TMVA::Experimental::RTensor>-; diff --git a/tmva/tmva/inc/TMVA/RBDT.hxx b/tmva/tmva/inc/TMVA/RBDT.hxx index 3bd428f732a4b..40cda2e352724 100644 --- a/tmva/tmva/inc/TMVA/RBDT.hxx +++ b/tmva/tmva/inc/TMVA/RBDT.hxx @@ -20,7 +20,6 @@ #ifndef TMVA_RBDT #define TMVA_RBDT -#include #include #include @@ -38,12 +37,6 @@ class RBDT final { public: typedef float Value_t; - /// IO constructor (both for ROOT IO and LoadText()). - RBDT() = default; - - /// Construct backends from model in ROOT file. - RBDT(const std::string &key, const std::string &filename); - /// Compute model prediction on a single event. /// /// The method is intended to be used with std::vectors-like containers, @@ -62,10 +55,12 @@ public: RTensor Compute(RTensor const &x) const; - static RBDT LoadText(std::string const &txtpath, std::vector &features, int nClasses, bool logistic, - Value_t baseScore); + static RBDT LoadXGBoost(std::string const &jsonPath); private: + /// Private default constructor, used by the public LoadXGBoost() factory. + RBDT() = default; + /// Map from XGBoost to RBDT indices. using IndexMap = std::unordered_map; @@ -75,8 +70,6 @@ private: static void correctIndices(std::span indices, IndexMap const &nodeIndices, IndexMap const &leafIndices); static void terminateTree(TMVA::Experimental::RBDT &ff, int &nPreviousNodes, int &nPreviousLeaves, IndexMap &nodeIndices, IndexMap &leafIndices, int &treesSkipped); - static RBDT - LoadText(std::istream &is, std::vector &features, int nClasses, bool logistic, Value_t baseScore); std::vector fRootIndices; std::vector fCutIndices; @@ -88,8 +81,6 @@ private: std::vector fBaseResponses; Value_t fBaseScore = 0.0; bool fLogistic = false; - - ClassDefNV(RBDT, 1); }; } // namespace Experimental diff --git a/tmva/tmva/src/RBDT.cxx b/tmva/tmva/src/RBDT.cxx index ca2858e15e201..b06183fcfbf30 100644 --- a/tmva/tmva/src/RBDT.cxx +++ b/tmva/tmva/src/RBDT.cxx @@ -20,9 +20,10 @@ #include -#include #include +#include + #include #include #include @@ -54,17 +55,6 @@ void softmaxTransformInplace(Value_t *out, int nOut) namespace util { -inline bool isInteger(const std::string &s) -{ - if (s.empty() || ((!isdigit(s[0])) && (s[0] != '-') && (s[0] != '+'))) - return false; - - char *p; - strtol(s.c_str(), &p, 10); - - return (*p == 0); -} - template struct NumericAfterSubstrOutput { explicit NumericAfterSubstrOutput() @@ -231,138 +221,129 @@ void TMVA::Experimental::RBDT::terminateTree(TMVA::Experimental::RBDT &ff, int & nPreviousLeaves = ff.fResponses.size(); } -TMVA::Experimental::RBDT TMVA::Experimental::RBDT::LoadText(std::string const &txtpath, - std::vector &features, int nClasses, - bool logistic, Value_t baseScore) +/// Construct an RBDT from an XGBoost model in its native JSON serialization. +/// +/// This reads the structured model that XGBoost writes with Booster.save_model(). +/// That format stores each tree as a set of parallel arrays and references +/// features by index, so no feature-name resolution is needed. Everything else +/// (objective, base score, number of classes) is taken from the file, which +/// makes this a self-contained, Python-free entry point. +TMVA::Experimental::RBDT TMVA::Experimental::RBDT::LoadXGBoost(std::string const &jsonPath) { - const std::string info = "constructing RBDT from " + txtpath + ": "; + const std::string info = "constructing RBDT from '" + jsonPath + "': "; - if (gSystem->AccessPathName(txtpath.c_str())) { - throw std::runtime_error(info + "file does not exists"); + if (gSystem->AccessPathName(jsonPath.c_str())) { + throw std::runtime_error(info + "file does not exist"); } - std::ifstream file(txtpath.c_str()); - return LoadText(file, features, nClasses, logistic, baseScore); -} + nlohmann::json j; + { + std::ifstream jsonFile(jsonPath.c_str()); + jsonFile >> j; + } -TMVA::Experimental::RBDT TMVA::Experimental::RBDT::LoadText(std::istream &file, std::vector &features, - int nClasses, bool logistic, Value_t baseScore) -{ - const std::string info = "constructing RBDT from istream: "; + auto const &learner = j.at("learner"); + auto const &modelParam = learner.at("learner_model_param"); + + // Map the XGBoost objective to the RBDT one. + std::string const xgbObjective = learner.at("objective").at("name").get(); + static const std::unordered_map objectiveMap{ + {"multi:softprob", "softmax"}, // Naming the objective softmax is more common today + {"binary:logistic", "logistic"}, + {"reg:linear", "identity"}, + {"reg:squarederror", "identity"}, + }; + auto foundObjective = objectiveMap.find(xgbObjective); + if (foundObjective == objectiveMap.end()) { + std::string supported; + for (auto const &item : objectiveMap) { + supported += (supported.empty() ? "" : ", ") + item.first; + } + throw std::runtime_error(info + "XGBoost model has unsupported objective \"" + xgbObjective + + "\". Supported objectives are " + supported + "."); + } + bool const logistic = foundObjective->second == "logistic"; + + // The base score is stored as a string, e.g. "5.14E-1". Since XGBoost 3.1.0 it + // is always serialized as a JSON array embedded in that string (e.g. + // "[5.14E-1]"), even for single-output models. Only a genuine multi-element + // array (multi-target base score) is unsupported. + std::string const baseScoreStr = modelParam.at("base_score").get(); + double baseScoreProb; + if (baseScoreStr.find('[') != std::string::npos) { + nlohmann::json const baseScoreArr = nlohmann::json::parse(baseScoreStr); + if (baseScoreArr.size() > 1) { + throw std::runtime_error(info + "model contains multiple base scores, which is not supported. This " + "typically occurs with XGBoost >= 3.1.0, which supports multi-target base " + "scores."); + } + baseScoreProb = baseScoreArr.at(0).get(); + } else { + baseScoreProb = std::stod(baseScoreStr); + } + // For a logistic objective the base score is a probability, but RBDT works on + // the raw margin, so we apply the logit transform (as the Python code does). + Value_t const baseScore = logistic ? std::log(baseScoreProb / (1.0 - baseScoreProb)) : baseScoreProb; + + // Only multiclass models produce more than one output. + int nClasses = 1; + if (xgbObjective.rfind("multi:", 0) == 0) { + nClasses = std::stoi(modelParam.at("num_class").get()); + } RBDT ff; ff.fLogistic = logistic; ff.fBaseScore = baseScore; ff.fBaseResponses.resize(nClasses <= 2 ? 1 : nClasses); - int treesSkipped = 0; - - int nVariables = 0; - std::unordered_map varIndices; - bool fixFeatures = false; - - if (!features.empty()) { - fixFeatures = true; - nVariables = features.size(); - for (int i = 0; i < nVariables; ++i) { - varIndices[features[i]] = i; - } - } - - std::string line; - - IndexMap nodeIndices; - IndexMap leafIndices; + auto const &trees = learner.at("gradient_booster").at("model").at("trees"); + int treesSkipped = 0; int nPreviousNodes = 0; int nPreviousLeaves = 0; + IndexMap nodeIndices; + IndexMap leafIndices; - while (std::getline(file, line)) { - std::size_t foundBegin = line.find("["); - std::size_t foundEnd = line.find("]"); - if (foundBegin != std::string::npos) { - std::string subline = line.substr(foundBegin + 1, foundEnd - foundBegin - 1); - if (util::isInteger(subline) && !ff.fResponses.empty()) { - terminateTree(ff, nPreviousNodes, nPreviousLeaves, nodeIndices, leafIndices, treesSkipped); - } else if (!util::isInteger(subline)) { - std::stringstream ss(line); - int index; - ss >> index; - line = ss.str(); - - std::vector splitstring = ROOT::Split(subline, "<"); - std::string const &varName = splitstring[0]; - Value_t cutValue; - { - std::stringstream ss1(splitstring[1]); - ss1 >> cutValue; - } - if (!varIndices.count(varName)) { - if (fixFeatures) { - throw std::runtime_error(info + "feature " + varName + " not in list of features"); - } - varIndices[varName] = nVariables; - features.push_back(varName); - ++nVariables; - } - int yes; - int no; - util::NumericAfterSubstrOutput output = util::numericAfterSubstr(line, "yes="); - if (!output.failed) { - yes = output.value; - } else { - throw std::runtime_error(info + "problem while parsing the text dump"); - } - output = util::numericAfterSubstr(output.rest, "no="); - if (!output.failed) { - no = output.value; - } else { - throw std::runtime_error(info + "problem while parsing the text dump"); - } - - ff.fCutValues.push_back(cutValue); - ff.fCutIndices.push_back(varIndices[varName]); - ff.fLeftIndices.push_back(yes); - ff.fRightIndices.push_back(no); - std::size_t nNodeIndices = nodeIndices.size(); - nodeIndices[index] = nNodeIndices + nPreviousNodes; - } - - } else { - util::NumericAfterSubstrOutput output = util::numericAfterSubstr(line, "leaf="); - if (output.found) { - std::stringstream ss(line); - int index; - ss >> index; - line = ss.str(); - - ff.fResponses.push_back(output.value); - std::size_t nLeafIndices = leafIndices.size(); - leafIndices[index] = nLeafIndices + nPreviousLeaves; + // Fill the flat RBDT arrays tree by tree, keying the index maps by the node's + // position in the XGBoost arrays. terminateTree() then remaps the child + // references to the RBDT indexing (negated for leaves), exactly as for the + // text dump. Node 0 is always the tree root, so iterating in array order + // makes it the first internal node of the tree, which is what fRootIndices + // expects. + for (auto const &tree : trees) { + auto const &leftChildren = tree.at("left_children"); + auto const &rightChildren = tree.at("right_children"); + auto const &splitIndices = tree.at("split_indices"); + auto const &splitConditions = tree.at("split_conditions"); + + std::size_t const nNodes = leftChildren.size(); + for (std::size_t i = 0; i < nNodes; ++i) { + int const left = leftChildren[i].get(); + if (left == -1) { + // Leaf node: the split condition holds the leaf response. + ff.fResponses.push_back(splitConditions[i].get()); + std::size_t const nLeafIndices = leafIndices.size(); + leafIndices[i] = nLeafIndices + nPreviousLeaves; + } else { + // Internal node: x < cut goes left (yes), otherwise right (no). + ff.fCutValues.push_back(splitConditions[i].get()); + ff.fCutIndices.push_back(splitIndices[i].get()); + ff.fLeftIndices.push_back(left); + ff.fRightIndices.push_back(rightChildren[i].get()); + std::size_t const nNodeIndices = nodeIndices.size(); + nodeIndices[i] = nNodeIndices + nPreviousNodes; } } + + terminateTree(ff, nPreviousNodes, nPreviousLeaves, nodeIndices, leafIndices, treesSkipped); } - terminateTree(ff, nPreviousNodes, nPreviousLeaves, nodeIndices, leafIndices, treesSkipped); if (nClasses > 2 && (ff.fRootIndices.size() + treesSkipped) % nClasses != 0) { std::stringstream ss; - ss << "Error in RBDT construction : Forest has " << ff.fRootIndices.size() - << " trees, which is not compatible with " << nClasses << "classes!"; + ss << info << "Forest has " << ff.fRootIndices.size() << " trees, which is not compatible with " << nClasses + << " classes!"; throw std::runtime_error(ss.str()); } return ff; } - -TMVA::Experimental::RBDT::RBDT(const std::string &key, const std::string &filename) -{ - std::unique_ptr file{TFile::Open(filename.c_str(), "READ")}; - if (!file || file->IsZombie()) { - throw std::runtime_error("Failed to open input file " + filename); - } - auto *fromFile = file->Get(key.c_str()); - if (!fromFile) { - throw std::runtime_error("No RBDT with name " + key); - } - *this = *fromFile; -} diff --git a/tmva/tmva/test/rbdt_xgboost.py b/tmva/tmva/test/rbdt_xgboost.py index 71e9a5b63a09f..acd9ca1ac71f0 100644 --- a/tmva/tmva/test/rbdt_xgboost.py +++ b/tmva/tmva/test/rbdt_xgboost.py @@ -1,3 +1,5 @@ +import os +import tempfile import unittest import numpy as np @@ -8,6 +10,18 @@ np.random.seed(1234) +def load_rbdt(xgb): + """Serialize the model to XGBoost's native JSON format and build an RBDT from + it via the C++ TMVA::Experimental::RBDT::LoadXGBoost.""" + with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as tmp_json: + json_path = tmp_json.name + try: + xgb.get_booster().save_model(json_path) + return ROOT.TMVA.Experimental.RBDT.LoadXGBoost(json_path) + finally: + os.remove(json_path) + + def create_dataset(num_events, num_features, num_outputs, dtype=np.float32): x = np.random.normal(0.0, 1.0, (num_events, num_features)).astype(dtype=dtype) if num_outputs == 1: @@ -19,22 +33,21 @@ def create_dataset(num_events, num_features, num_outputs, dtype=np.float32): return x, y -def _test_XGBBinary(label): +def _test_XGBBinary(): """ Compare response of XGB classifier and TMVA tree inference system. """ x, y = create_dataset(1000, 10, 2) xgb = xgboost.XGBClassifier(n_estimators=100, max_depth=3) xgb.fit(x, y) - ROOT.TMVA.Experimental.SaveXGBoost(xgb, "myModel", "testXGBBinary{}.root".format(label), num_inputs=10) - bdt = ROOT.TMVA.Experimental.RBDT("myModel", "testXGBBinary{}.root".format(label)) + bdt = load_rbdt(xgb) y_xgb = xgb.predict_proba(x)[:, 1].squeeze() y_bdt = bdt.Compute(x).squeeze() np.testing.assert_array_almost_equal(y_xgb, y_bdt) -def _test_XGBRegression(label): +def _test_XGBRegression(): """ Compare response of XGB regressor and TMVA tree inference system. """ @@ -44,29 +57,27 @@ def _test_XGBRegression(label): # Other than in the XGBBinary test, we're passing the training features via # a pandas DataFrame this time. In that case, XGBoost will define custom # feature names according to the column names in the dataframe, and we can - # test the case where the feature names in the .txt dump are not the - # default "f0", "f1", "f2", etc. + # test the case where the feature names are not the default "f0", "f1", + # "f2", etc. df_x = pandas.DataFrame({f"myfeature_{i}": x[:, i] for i in range(n_features)}) assert len(x) == len(df_x) xgb = xgboost.XGBRegressor(n_estimators=1, max_depth=3) xgb.fit(df_x, y) - ROOT.TMVA.Experimental.SaveXGBoost(xgb, "myModel", "testXGBRegression{}.root".format(label), num_inputs=10) - bdt = ROOT.TMVA.Experimental.RBDT("myModel", "testXGBRegression{}.root".format(label)) + bdt = load_rbdt(xgb) y_xgb = xgb.predict(x).squeeze() y_bdt = bdt.Compute(x).squeeze() np.testing.assert_array_almost_equal(y_xgb, y_bdt) -def _test_XGBMulticlass(label): +def _test_XGBMulticlass(): """ Compare response of XGB multiclass and TMVA tree inference system. """ x, y = create_dataset(1000, 10, 3) xgb = xgboost.XGBClassifier(n_estimators=100, max_depth=3) xgb.fit(x, y) - ROOT.TMVA.Experimental.SaveXGBoost(xgb, "myModel", "testXGBMulticlass{}.root".format(label), num_inputs=10) - bdt = ROOT.TMVA.Experimental.RBDT("myModel", "testXGBMulticlass{}.root".format(label)) + bdt = load_rbdt(xgb) y_xgb = xgb.predict_proba(x) y_bdt = bdt.Compute(x) @@ -82,7 +93,7 @@ def test_XGBBinary_default(self): """ Test model trained with binary XGBClassifier. """ - _test_XGBBinary("default") + _test_XGBBinary() def test_XGBMulticlass_default(self): """ @@ -90,13 +101,13 @@ def test_XGBMulticlass_default(self): """ if xgboost.__version__ >= "3.1.0": self.skipTest("We don't support multiclassification with xgboost>=3.1.0 yet") - _test_XGBMulticlass("default") + _test_XGBMulticlass() def test_XGBRegression_default(self): """ Test model trained with XGBRegressor. """ - _test_XGBRegression("default") + _test_XGBRegression() if __name__ == "__main__": diff --git a/tutorials/CMakeLists.txt b/tutorials/CMakeLists.txt index c0405be50f405..fca354e26032d 100644 --- a/tutorials/CMakeLists.txt +++ b/tutorials/CMakeLists.txt @@ -656,6 +656,7 @@ set (machine_learning-TMVACrossValidationRegression-depends tutorial-machine_lea set (machine_learning-TMVACrossValidationApplication-depends tutorial-machine_learning-TMVACrossValidation) set (machine_learning-tmva101_Training-depends tutorial-machine_learning-tmva100_DataPreparation-py) set (machine_learning-tmva102_Testing-depends tutorial-machine_learning-tmva101_Training-py) +set (machine_learning-tmva103_Application-depends tutorial-machine_learning-tmva101_Training-py) set (machine_learning-tmva003_RReader-depends tutorial-machine_learning-TMVAClassification) set (machine_learning-tmva004_RStandardScaler-depends tutorial-machine_learning-tmva003_RReader) set (machine_learning-pytorch-ApplicationClassificationPyTorch-depends tutorial-machine_learning-pytorch-ClassificationPyTorch-py) diff --git a/tutorials/machine_learning/tmva101_Training.py b/tutorials/machine_learning/tmva101_Training.py index 671cad7836c93..ff944253ca0eb 100644 --- a/tutorials/machine_learning/tmva101_Training.py +++ b/tutorials/machine_learning/tmva101_Training.py @@ -50,6 +50,8 @@ def load_data(signal_filename, background_filename): bdt = XGBClassifier(max_depth=3, n_estimators=500) bdt.fit(x, y, sample_weight=w) - # Save model in TMVA format - print("Training done on ", x.shape[0], "events. Saving model in tmva101.root") - ROOT.TMVA.Experimental.SaveXGBoost(bdt, "myBDT", "tmva101.root", num_inputs=x.shape[1]) + # Save the trained model in XGBoost's native JSON format. It can be loaded + # back for inference with TMVA's fast tree inference engine via + # TMVA::Experimental::RBDT::LoadXGBoost, both from Python and from C++. + print("Training done on ", x.shape[0], "events. Saving model in tmva101.json") + bdt.get_booster().save_model("tmva101.json") diff --git a/tutorials/machine_learning/tmva102_Testing.py b/tutorials/machine_learning/tmva102_Testing.py index 7d78e9b6147e5..59f95d37706fa 100644 --- a/tutorials/machine_learning/tmva102_Testing.py +++ b/tutorials/machine_learning/tmva102_Testing.py @@ -17,10 +17,10 @@ # Load data x, y_true, w = load_data("test_signal.root", "test_background.root") -# Load trained model -File = "tmva101.root" +# Load trained model from the XGBoost JSON written by tmva101_Training.py +File = "tmva101.json" -bdt = ROOT.TMVA.Experimental.RBDT("myBDT", File) +bdt = ROOT.TMVA.Experimental.RBDT.LoadXGBoost(File) # Make prediction y_pred = bdt.Compute(x) diff --git a/tutorials/machine_learning/tmva103_Application.C b/tutorials/machine_learning/tmva103_Application.C index 305cd927c778a..9e721bc8c3701 100644 --- a/tutorials/machine_learning/tmva103_Application.C +++ b/tutorials/machine_learning/tmva103_Application.C @@ -15,15 +15,15 @@ using namespace TMVA::Experimental; void tmva103_Application() { - const char* model_filename = "tmva101.root"; + const char* model_filename = "tmva101.json"; if (gSystem->AccessPathName(model_filename)) { Info("tmva103_Application.C", "%s does not exist", model_filename); return; } - // Load BDT model - RBDT bdt("myBDT", model_filename); + // Load BDT model from the XGBoost JSON written by tmva101_Training.py + RBDT bdt = RBDT::LoadXGBoost(model_filename); // Apply model on a single input auto y1 = bdt.Compute({1.0, 2.0, 3.0, 4.0});