Logo ROOT  
Reference Guide
 
Loading...
Searching...
No Matches
RBDT.cxx
Go to the documentation of this file.
1/**********************************************************************************
2 * Project: ROOT - a Root-integrated toolkit for multivariate data analysis *
3 * Package: TMVA *
4 * *
5 * *
6 * Description: *
7 * *
8 * Authors: *
9 * Jonas Rembser (jonas.rembser@cern.ch) *
10 * *
11 * Copyright (c) 2024: *
12 * CERN, Switzerland *
13 * *
14 * Redistribution and use in source and binary forms, with or without *
15 * modification, are permitted according to the terms listed in LICENSE *
16 * (see tmva/doc/LICENSE) *
17 **********************************************************************************/
18
19#include <TMVA/RBDT.hxx>
20
21#include <ROOT/StringUtils.hxx>
22
23#include <TSystem.h>
24
25#include <nlohmann/json.hpp>
26
27#include <cmath>
28#include <fstream>
29#include <iostream>
30#include <sstream>
31#include <stdexcept>
32#include <cstdlib>
33
34namespace {
35
36template <class Value_t>
37void softmaxTransformInplace(Value_t *out, int nOut)
38{
39 // Do softmax transformation inplace, mimicing exactly the Softmax function
40 // in the src/common/math.h source file of xgboost.
41 double norm = 0.;
42 Value_t wmax = *out;
43 for (int i = 1; i < nOut; ++i) {
44 wmax = std::max(out[i], wmax);
45 }
46 for (int i = 0; i < nOut; ++i) {
47 Value_t &x = out[i];
48 x = std::exp(x - wmax);
49 norm += x;
50 }
51 for (int i = 0; i < nOut; ++i) {
52 out[i] /= static_cast<float>(norm);
53 }
54}
55
56namespace util {
57
58template <class NumericType>
59struct NumericAfterSubstrOutput {
60 explicit NumericAfterSubstrOutput()
61 {
62 value = 0;
63 found = false;
64 failed = true;
65 }
67 bool found;
68 bool failed;
69 std::string rest;
70};
71
72template <class NumericType>
73inline NumericAfterSubstrOutput<NumericType> numericAfterSubstr(std::string const &str, std::string const &substr)
74{
75 std::string rest;
77 output.rest = str;
78
79 std::size_t found = str.find(substr);
80 if (found != std::string::npos) {
81 output.found = true;
82 std::stringstream ss(str.substr(found + substr.size(), str.size() - found + substr.size()));
83 ss >> output.value;
84 if (!ss.fail()) {
85 output.failed = false;
86 output.rest = ss.str();
87 }
88 }
89 return output;
90}
91
92} // namespace util
93
94} // namespace
95
97
98/// Compute model prediction on input RTensor
100{
101 std::size_t nOut = fBaseResponses.size() > 2 ? fBaseResponses.size() : 1;
102 const std::size_t rows = x.GetShape()[0];
103 const std::size_t cols = x.GetShape()[1];
104 RTensor<Value_t> y({rows, nOut}, MemoryLayout::ColumnMajor);
105 std::vector<Value_t> xRow(cols);
106 std::vector<Value_t> yRow(nOut);
107 for (std::size_t iRow = 0; iRow < rows; ++iRow) {
108 for (std::size_t iCol = 0; iCol < cols; ++iCol) {
109 xRow[iCol] = x({iRow, iCol});
110 }
111 ComputeImpl(xRow.data(), yRow.data());
112 for (std::size_t iOut = 0; iOut < nOut; ++iOut) {
113 y({iRow, iOut}) = yRow[iOut];
114 }
115 }
116 return y;
117}
118
120{
121 std::size_t nOut = fBaseResponses.size() > 2 ? fBaseResponses.size() : 1;
122 if (nOut == 1) {
123 throw std::runtime_error(
124 "Error in RBDT::softmax : binary classification models don't support softmax evaluation. Plase set "
125 "the number of classes in the RBDT-creating function if this is a multiclassification model.");
126 }
127
128 for (std::size_t i = 0; i < nOut; ++i) {
129 out[i] = fBaseScore + fBaseResponses[i];
130 }
131
132 int iRootIndex = 0;
133 for (int index : fRootIndices) {
134 do {
135 int r = fRightIndices[index];
136 int l = fLeftIndices[index];
137 index = array[fCutIndices[index]] < fCutValues[index] ? l : r;
138 } while (index > 0);
139 out[fTreeNumbers[iRootIndex] % nOut] += fResponses[-index];
140 ++iRootIndex;
141 }
142
144}
145
147{
148 std::size_t nOut = fBaseResponses.size() > 2 ? fBaseResponses.size() : 1;
149 if (nOut > 1) {
150 Softmax(array, out);
151 } else {
152 out[0] = EvaluateBinary(array);
153 if (fLogistic) {
154 out[0] = 1.0 / (1.0 + std::exp(-out[0]));
155 }
156 }
157}
158
160{
161 Value_t out = fBaseScore + fBaseResponses[0];
162
163 for (std::vector<int>::const_iterator indexIter = fRootIndices.begin(); indexIter != fRootIndices.end();
164 ++indexIter) {
165 int index = *indexIter;
166 do {
167 int r = fRightIndices[index];
168 int l = fLeftIndices[index];
169 index = array[fCutIndices[index]] < fCutValues[index] ? l : r;
170 } while (index > 0);
171 out += fResponses[-index];
172 }
173
174 return out;
175}
176
177/// RBDT uses a more efficient representation of the BDT in flat arrays. This
178/// function translates the indices to the RBDT indices. In RBDT, leaf nodes
179/// are stored in separate arrays. To encode this, the sign of the index is
180/// flipped.
182 IndexMap const &leafIndices)
183{
184 for (int &idx : indices) {
185 auto foundNode = nodeIndices.find(idx);
186 if (foundNode != nodeIndices.end()) {
187 idx = foundNode->second;
188 continue;
189 }
190 auto foundLeaf = leafIndices.find(idx);
191 if (foundLeaf != leafIndices.end()) {
192 idx = -foundLeaf->second;
193 continue;
194 } else {
195 std::stringstream errMsg;
196 errMsg << "RBDT: something is wrong in the node structure - node with index " << idx << " doesn't exist";
197 throw std::runtime_error(errMsg.str());
198 }
199 }
200}
201
204{
205 correctIndices({ff.fRightIndices.begin() + nPreviousNodes, ff.fRightIndices.end()}, nodeIndices, leafIndices);
206 correctIndices({ff.fLeftIndices.begin() + nPreviousNodes, ff.fLeftIndices.end()}, nodeIndices, leafIndices);
207
208 if (nPreviousNodes != static_cast<int>(ff.fCutValues.size())) {
209 ff.fTreeNumbers.push_back(ff.fRootIndices.size() + treesSkipped);
210 ff.fRootIndices.push_back(nPreviousNodes);
211 } else {
212 int treeNumbers = ff.fRootIndices.size() + treesSkipped;
213 ++treesSkipped;
214 ff.fBaseResponses[treeNumbers % ff.fBaseResponses.size()] += ff.fResponses.back();
215 ff.fResponses.pop_back();
216 }
217
218 nodeIndices.clear();
219 leafIndices.clear();
220 nPreviousNodes = ff.fCutValues.size();
221 nPreviousLeaves = ff.fResponses.size();
222}
223
224/// Construct an RBDT from an XGBoost model in its native JSON serialization.
225///
226/// This reads the structured model that XGBoost writes with Booster.save_model().
227/// That format stores each tree as a set of parallel arrays and references
228/// features by index, so no feature-name resolution is needed. Everything else
229/// (objective, base score, number of classes) is taken from the file, which
230/// makes this a self-contained, Python-free entry point.
232{
233 const std::string info = "constructing RBDT from '" + jsonPath + "': ";
234
235 if (gSystem->AccessPathName(jsonPath.c_str())) {
236 throw std::runtime_error(info + "file does not exist");
237 }
238
239 nlohmann::json j;
240 {
241 std::ifstream jsonFile(jsonPath.c_str());
242 jsonFile >> j;
243 }
244
245 auto const &learner = j.at("learner");
246 auto const &modelParam = learner.at("learner_model_param");
247
248 // Map the XGBoost objective to the RBDT one.
249 std::string const xgbObjective = learner.at("objective").at("name").get<std::string>();
250 static const std::unordered_map<std::string, std::string> objectiveMap{
251 {"multi:softprob", "softmax"}, // Naming the objective softmax is more common today
252 {"binary:logistic", "logistic"},
253 {"reg:linear", "identity"},
254 {"reg:squarederror", "identity"},
255 };
258 std::string supported;
259 for (auto const &item : objectiveMap) {
260 supported += (supported.empty() ? "" : ", ") + item.first;
261 }
262 throw std::runtime_error(info + "XGBoost model has unsupported objective \"" + xgbObjective +
263 "\". Supported objectives are " + supported + ".");
264 }
265 bool const logistic = foundObjective->second == "logistic";
266
267 // The base score is stored as a string, e.g. "5.14E-1". Since XGBoost 3.1.0 it
268 // is always serialized as a JSON array embedded in that string (e.g.
269 // "[5.14E-1]"), even for single-output models. Only a genuine multi-element
270 // array (multi-target base score) is unsupported.
271 std::string const baseScoreStr = modelParam.at("base_score").get<std::string>();
272 double baseScoreProb;
273 if (baseScoreStr.find('[') != std::string::npos) {
274 nlohmann::json const baseScoreArr = nlohmann::json::parse(baseScoreStr);
275 if (baseScoreArr.size() > 1) {
276 throw std::runtime_error(info + "model contains multiple base scores, which is not supported. This "
277 "typically occurs with XGBoost >= 3.1.0, which supports multi-target base "
278 "scores.");
279 }
280 baseScoreProb = baseScoreArr.at(0).get<double>();
281 } else {
282 baseScoreProb = std::stod(baseScoreStr);
283 }
284 // For a logistic objective the base score is a probability, but RBDT works on
285 // the raw margin, so we apply the logit transform (as the Python code does).
286 Value_t const baseScore = logistic ? std::log(baseScoreProb / (1.0 - baseScoreProb)) : baseScoreProb;
287
288 // Only multiclass models produce more than one output.
289 int nClasses = 1;
290 if (xgbObjective.rfind("multi:", 0) == 0) {
291 nClasses = std::stoi(modelParam.at("num_class").get<std::string>());
292 }
293
294 RBDT ff;
295 ff.fLogistic = logistic;
296 ff.fBaseScore = baseScore;
297 ff.fBaseResponses.resize(nClasses <= 2 ? 1 : nClasses);
298
299 auto const &trees = learner.at("gradient_booster").at("model").at("trees");
300
301 int treesSkipped = 0;
302 int nPreviousNodes = 0;
303 int nPreviousLeaves = 0;
306
307 // Fill the flat RBDT arrays tree by tree, keying the index maps by the node's
308 // position in the XGBoost arrays. terminateTree() then remaps the child
309 // references to the RBDT indexing (negated for leaves), exactly as for the
310 // text dump. Node 0 is always the tree root, so iterating in array order
311 // makes it the first internal node of the tree, which is what fRootIndices
312 // expects.
313 for (auto const &tree : trees) {
314 auto const &leftChildren = tree.at("left_children");
315 auto const &rightChildren = tree.at("right_children");
316 auto const &splitIndices = tree.at("split_indices");
317 auto const &splitConditions = tree.at("split_conditions");
318
319 std::size_t const nNodes = leftChildren.size();
320 for (std::size_t i = 0; i < nNodes; ++i) {
321 int const left = leftChildren[i].get<int>();
322 if (left == -1) {
323 // Leaf node: the split condition holds the leaf response.
324 ff.fResponses.push_back(splitConditions[i].get<Value_t>());
325 std::size_t const nLeafIndices = leafIndices.size();
327 } else {
328 // Internal node: x < cut goes left (yes), otherwise right (no).
329 ff.fCutValues.push_back(splitConditions[i].get<Value_t>());
330 ff.fCutIndices.push_back(splitIndices[i].get<unsigned int>());
331 ff.fLeftIndices.push_back(left);
332 ff.fRightIndices.push_back(rightChildren[i].get<int>());
333 std::size_t const nNodeIndices = nodeIndices.size();
335 }
336 }
337
339 }
340
341 if (nClasses > 2 && (ff.fRootIndices.size() + treesSkipped) % nClasses != 0) {
342 std::stringstream ss;
343 ss << info << "Forest has " << ff.fRootIndices.size() << " trees, which is not compatible with " << nClasses
344 << " classes!";
345 throw std::runtime_error(ss.str());
346 }
347
348 return ff;
349}
ROOT::Detail::TRangeCast< T, true > TRangeDynCast
TRangeDynCast is an adapter class that allows the typed iteration through a TCollection.
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void char Point_t Rectangle_t WindowAttributes_t Float_t r
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void char Point_t Rectangle_t WindowAttributes_t index
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void value
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void char Point_t Rectangle_t wmax
R__EXTERN TSystem * gSystem
Definition TSystem.h:582
const_iterator begin() const
const_iterator end() const
static void terminateTree(TMVA::Experimental::RBDT &ff, int &nPreviousNodes, int &nPreviousLeaves, IndexMap &nodeIndices, IndexMap &leafIndices, int &treesSkipped)
Definition RBDT.cxx:202
static RBDT LoadXGBoost(std::string const &jsonPath)
Construct an RBDT from an XGBoost model in its native JSON serialization.
Definition RBDT.cxx:231
static void correctIndices(std::span< int > indices, IndexMap const &nodeIndices, IndexMap const &leafIndices)
RBDT uses a more efficient representation of the BDT in flat arrays.
Definition RBDT.cxx:181
std::unordered_map< int, int > IndexMap
Map from XGBoost to RBDT indices.
Definition RBDT.hxx:65
void Softmax(const Value_t *array, Value_t *out) const
Definition RBDT.cxx:119
Value_t EvaluateBinary(const Value_t *array) const
Definition RBDT.cxx:159
std::vector< Value_t > fBaseResponses
Definition RBDT.hxx:81
Vector Compute(const Vector &x) const
Compute model prediction on a single event.
Definition RBDT.hxx:45
void ComputeImpl(const Value_t *array, Value_t *out) const
Definition RBDT.cxx:146
RTensor is a container with contiguous memory and shape information.
Definition RTensor.hxx:163
virtual Bool_t AccessPathName(const char *path, EAccessMode mode=kFileExists)
Returns FALSE if one can access a file using the specified access mode.
Definition TSystem.cxx:1311
Double_t y[n]
Definition legend1.C:17
Double_t x[n]
Definition legend1.C:17
Definition RBDT.cxx:56
TLine l
Definition textangle.C:4