From 503626cbb66e2469a7053630a20713707381d7cf Mon Sep 17 00:00:00 2001 From: Arne Juul Date: Thu, 11 Dec 2025 00:19:26 +0000 Subject: [PATCH 1/9] Add support for XGBoost UBJ format import This commit adds the ability to import XGBoost models saved in Universal Binary JSON (.ubj) format, in addition to the existing JSON format support. Key changes: - Add ubjson library dependency for parsing UBJ binary format - Create XGBoostUbjParser to handle UBJ model files - Extract common tree-to-expression logic into AbstractXGBoostParser base class - Convert flat UBJ array representation to hierarchical tree structure - Extract and apply base_score logit transformation from model metadata - Add test case comparing JSON and UBJ model imports - Add utility tools for UBJ-to-JSON conversion and debugging Enables base score extraction with logistic transformation --- model-integration/pom.xml | 7 + .../xgboost/AbstractXGBoostParser.java | 47 +++ .../importer/xgboost/XGBoostImporter.java | 16 +- .../importer/xgboost/XGBoostParser.java | 37 +- .../importer/xgboost/XGBoostUbjParser.java | 263 ++++++++++++++ .../importer/xgboost/UbjToJson.java | 146 ++++++++ .../xgboost/XGBoostImportTestCase.java | 28 ++ .../models/xgboost/binary_breast_cancer.json | 343 ++++++++++++++++++ .../models/xgboost/binary_breast_cancer.ubj | Bin 0 -> 25614 bytes .../src/test/models/xgboost/model_parser.py | 279 ++++++++++++++ .../src/test/models/xgboost/ubj-to-json.sh | 20 + 11 files changed, 1147 insertions(+), 39 deletions(-) create mode 100644 model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/AbstractXGBoostParser.java create mode 100644 model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostUbjParser.java create mode 100644 model-integration/src/test/java/ai/vespa/rankingexpression/importer/xgboost/UbjToJson.java create mode 100644 model-integration/src/test/models/xgboost/binary_breast_cancer.json create mode 100644 model-integration/src/test/models/xgboost/binary_breast_cancer.ubj create mode 100644 model-integration/src/test/models/xgboost/model_parser.py create mode 100755 model-integration/src/test/models/xgboost/ubj-to-json.sh diff --git a/model-integration/pom.xml b/model-integration/pom.xml index ba1e79f28cfa..8bc54301b01e 100644 --- a/model-integration/pom.xml +++ b/model-integration/pom.xml @@ -376,6 +376,13 @@ ${testcontainers.vespa.version} test + + + com.dev-smart + ubjson + 0.1.8 + + diff --git a/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/AbstractXGBoostParser.java b/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/AbstractXGBoostParser.java new file mode 100644 index 000000000000..3c79d1464809 --- /dev/null +++ b/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/AbstractXGBoostParser.java @@ -0,0 +1,47 @@ +// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package ai.vespa.rankingexpression.importer.xgboost; + +/** + * Base class for XGBoost parsers containing shared tree-to-expression conversion logic. + * + * @author arnej + */ +abstract class AbstractXGBoostParser { + + /** + * Converts an XGBoostTree node to a Vespa ranking expression string. + * This method handles both leaf nodes and split nodes recursively. + * + * @param node XGBoost tree node to convert. + * @return Vespa ranking expression for input node. + */ + protected String treeToRankExp(XGBoostTree node) { + if (node.isLeaf()) { + return Double.toString(node.getLeaf()); + } else { + assert node.getChildren().size() == 2; + String trueExp; + String falseExp; + if (node.getYes() == node.getChildren().get(0).getNodeid()) { + trueExp = treeToRankExp(node.getChildren().get(0)); + falseExp = treeToRankExp(node.getChildren().get(1)); + } else { + trueExp = treeToRankExp(node.getChildren().get(1)); + falseExp = treeToRankExp(node.getChildren().get(0)); + } + // xgboost uses float only internally, so round to closest float + float xgbSplitPoint = (float)node.getSplit_condition(); + // but Vespa expects rank profile literals in double precision: + double vespaSplitPoint = xgbSplitPoint; + String condition; + if (node.getMissing() == node.getYes()) { + // Note: this is for handling missing features, as the backend handles comparison with NaN as false. + condition = "!(" + node.getSplit() + " >= " + vespaSplitPoint + ")"; + } else { + condition = node.getSplit() + " < " + vespaSplitPoint; + } + return "if (" + condition + ", " + trueExp + ", " + falseExp + ")"; + } + } + +} diff --git a/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostImporter.java b/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostImporter.java index 4d530747d7d3..4d659e74120b 100644 --- a/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostImporter.java +++ b/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostImporter.java @@ -25,6 +25,10 @@ public boolean canImport(String modelPath) { File modelFile = new File(modelPath); if ( ! modelFile.isFile()) return false; + if (modelFile.toString().endsWith(".ubj")) { + // for now + return true; + } return modelFile.toString().endsWith(".json") && probe(modelFile); } @@ -52,9 +56,15 @@ private boolean probe(File modelFile) { public ImportedModel importModel(String modelName, String modelPath) { try { ImportedModel model = new ImportedModel(modelName, modelPath, ImportedMlModel.ModelType.XGBOOST); - XGBoostParser parser = new XGBoostParser(modelPath); - RankingExpression expression = new RankingExpression(parser.toRankingExpression()); - model.expression(modelName, expression); + if (modelPath.endsWith(".ubj")) { + XGBoostUbjParser parser = new XGBoostUbjParser(modelPath); + RankingExpression expression = new RankingExpression(parser.toRankingExpression()); + model.expression(modelName, expression); + } else { + XGBoostParser parser = new XGBoostParser(modelPath); + RankingExpression expression = new RankingExpression(parser.toRankingExpression()); + model.expression(modelName, expression); + } return model; } catch (IOException e) { throw new IllegalArgumentException("Could not import XGBoost model from '" + modelPath + "'", e); diff --git a/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostParser.java b/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostParser.java index f869875815fe..4ab7bf8a6787 100644 --- a/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostParser.java +++ b/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostParser.java @@ -13,7 +13,7 @@ /** * @author grace-lam */ -class XGBoostParser { +class XGBoostParser extends AbstractXGBoostParser { private final List xgboostTrees; @@ -49,39 +49,4 @@ String toRankingExpression() { return ret.toString(); } - /** - * Recursive helper function for toRankingExpression(). - * - * @param node XGBoost tree node to convert. - * @return Vespa ranking expression for input node. - */ - private String treeToRankExp(XGBoostTree node) { - if (node.isLeaf()) { - return Double.toString(node.getLeaf()); - } else { - assert node.getChildren().size() == 2; - String trueExp; - String falseExp; - if (node.getYes() == node.getChildren().get(0).getNodeid()) { - trueExp = treeToRankExp(node.getChildren().get(0)); - falseExp = treeToRankExp(node.getChildren().get(1)); - } else { - trueExp = treeToRankExp(node.getChildren().get(1)); - falseExp = treeToRankExp(node.getChildren().get(0)); - } - // xgboost uses float only internally, so round to closest float - float xgbSplitPoint = (float)node.getSplit_condition(); - // but Vespa expects rank profile literals in double precision: - double vespaSplitPoint = xgbSplitPoint; - String condition; - if (node.getMissing() == node.getYes()) { - // Note: this is for handling missing features, as the backend handles comparison with NaN as false. - condition = "!(" + node.getSplit() + " >= " + vespaSplitPoint + ")"; - } else { - condition = node.getSplit() + " < " + vespaSplitPoint; - } - return "if (" + condition + ", " + trueExp + ", " + falseExp + ")"; - } - } - } diff --git a/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostUbjParser.java b/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostUbjParser.java new file mode 100644 index 000000000000..c9f46fe90e21 --- /dev/null +++ b/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostUbjParser.java @@ -0,0 +1,263 @@ +// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package ai.vespa.rankingexpression.importer.xgboost; + +import com.devsmart.ubjson.UBArray; +import com.devsmart.ubjson.UBObject; +import com.devsmart.ubjson.UBReader; +import com.devsmart.ubjson.UBValue; + +import java.io.FileInputStream; +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +/** + * Parser for XGBoost models in Universal Binary JSON (UBJ) format. + * + * @author arnej + */ +class XGBoostUbjParser extends AbstractXGBoostParser { + + private final List xgboostTrees; + private final double baseScore; + + /** + * Constructor stores parsed UBJ trees. + * + * @param filePath XGBoost UBJ input file. + * @throws IOException Fails file reading or UBJ parsing. + */ + XGBoostUbjParser(String filePath) throws IOException { + this.xgboostTrees = new ArrayList<>(); + double tmpBaseScore = 0.5; // default value + try (FileInputStream fileStream = new FileInputStream(filePath); + UBReader reader = new UBReader(fileStream)) { + UBValue root = reader.read(); + + UBArray forestArray; + if (root.isArray()) { + // Simple array format (like JSON export) + forestArray = root.asArray(); + } else if (root.isObject()) { + UBObject rootObj = root.asObject(); + UBObject learner = getRequiredObject(rootObj, "learner", "UBJ root"); + + // Extract base_score if available + tmpBaseScore = extractBaseScore(learner); + + // Navigate to trees array + forestArray = navigateToTreesArray(learner); + } else { + throw new IOException("Expected UBJ array or object at root, got: " + root.getClass().getSimpleName()); + } + + // Parse each tree (UBJ format uses flat arrays, not nested objects) + for (int i = 0; i < forestArray.size(); i++) { + UBValue treeValue = forestArray.get(i); + if (!treeValue.isObject()) { + throw new IOException("Expected UBJ object for tree, got: " + treeValue.getClass().getSimpleName()); + } + this.xgboostTrees.add(convertUbjTree(treeValue.asObject())); + } + } + this.baseScore = tmpBaseScore; + } + + /** + * Converts parsed UBJ trees to Vespa ranking expressions. + * + * @return Vespa ranking expressions. + */ + String toRankingExpression() { + StringBuilder result = new StringBuilder(); + + // Convert all trees to expressions and join with " + " + for (int i = 0; i < xgboostTrees.size(); i++) { + if (i > 0) { + result.append(" + \n"); + } + result.append(treeToRankExp(xgboostTrees.get(i))); + } + + // Add base_score logit transformation + result.append(" + \n"); + result.append("log(").append(baseScore).append(") - log(").append(1.0 - baseScore).append(")"); + + return result.toString(); + } + + /** + * Extracts a required UBObject from a parent object. + * + * @param parent Parent UBObject to extract from. + * @param key Key name to extract. + * @param parentDescription Description of parent for error messages. + * @return The extracted UBObject. + * @throws IOException If the key is missing or not an object. + */ + private static UBObject getRequiredObject(UBObject parent, String key, String parentDescription) throws IOException { + UBValue value = parent.get(key); + if (value == null || !value.isObject()) { + throw new IOException("Expected '" + key + "' object in " + parentDescription); + } + return value.asObject(); + } + + /** + * Extracts the base_score from learner_model_param if available. + * + * @param learner The learner UBObject. + * @return The extracted base_score, or 0.5 if not found. + */ + private static double extractBaseScore(UBObject learner) { + UBValue learnerModelParamValue = learner.get("learner_model_param"); + if (learnerModelParamValue != null && learnerModelParamValue.isObject()) { + UBObject learnerModelParam = learnerModelParamValue.asObject(); + UBValue baseScoreValue = learnerModelParam.get("base_score"); + if (baseScoreValue != null && baseScoreValue.isString()) { + String baseScoreStr = baseScoreValue.asString(); + // Parse string like "[6.274165E-1]" - remove brackets and parse + baseScoreStr = baseScoreStr.replace("[", "").replace("]", ""); + return Double.parseDouble(baseScoreStr); + } + } + return 0.5; // default value + } + + /** + * Navigates from learner object to the trees array. + * + * @param learner The learner UBObject. + * @return The trees UBArray. + * @throws IOException If navigation fails. + */ + private static UBArray navigateToTreesArray(UBObject learner) throws IOException { + UBObject gradientBooster = getRequiredObject(learner, "gradient_booster", "learner"); + UBObject model = getRequiredObject(gradientBooster, "model", "gradient_booster"); + UBValue treesValue = model.get("trees"); + if (treesValue == null || !treesValue.isArray()) { + throw new IOException("Expected 'trees' array in model"); + } + return treesValue.asArray(); + } + + /** + * Converts a UBJ tree (flat array format) to the root XGBoostTree node (hierarchical format). + * + * @param treeObj UBJ object containing flat arrays representing the tree. + * @return Root XGBoostTree node with hierarchical structure. + */ + private static XGBoostTree convertUbjTree(UBObject treeObj) { + // Extract flat arrays from UBJ format + int[] leftChildren = treeObj.get("left_children").asInt32Array(); + int[] rightChildren = treeObj.get("right_children").asInt32Array(); + float[] splitConditions = treeObj.get("split_conditions").asFloat32Array(); + int[] splitIndices = treeObj.get("split_indices").asInt32Array(); + float[] baseWeights = treeObj.get("base_weights").asFloat32Array(); + byte[] defaultLeftBytes = extractDefaultLeft(treeObj.get("default_left")); + + // Convert from flat arrays to hierarchical tree structure, starting at root (node 0, depth 0) + return buildTreeFromArrays(0, 0, leftChildren, rightChildren, splitConditions, + splitIndices, baseWeights, defaultLeftBytes); + } + + /** + * Extracts the default_left array from UBJ value. + * Handles both UBArray and direct byte array formats. + * + * @param defaultLeftValue The UBValue containing default_left data. + * @return Byte array with default_left values. + */ + private static byte[] extractDefaultLeft(UBValue defaultLeftValue) { + if (defaultLeftValue.isArray()) { + // It's a UBArray, iterate and convert + UBArray defaultLeftArray = defaultLeftValue.asArray(); + byte[] result = new byte[defaultLeftArray.size()]; + for (int i = 0; i < defaultLeftArray.size(); i++) { + result[i] = defaultLeftArray.get(i).asByte(); + } + return result; + } else { + return defaultLeftValue.asByteArray(); + } + } + + /** + * Recursively builds a hierarchical XGBoostTree from flat arrays. + * + * @param nodeId Current node index in the arrays. + * @param depth Current depth in the tree (0 for root). + * @param leftChildren Array of left child indices. + * @param rightChildren Array of right child indices. + * @param splitConditions Array of split threshold values. + * @param splitIndices Array of feature indices to split on. + * @param baseWeights Array of base weights (leaf values). + * @param defaultLeft Array indicating if missing values go left. + * @return XGBoostTree node. + */ + private static XGBoostTree buildTreeFromArrays(int nodeId, int depth, int[] leftChildren, int[] rightChildren, + float[] splitConditions, int[] splitIndices, + float[] baseWeights, byte[] defaultLeft) { + XGBoostTree node = new XGBoostTree(); + setField(node, "nodeid", nodeId); + setField(node, "depth", depth); + + // Check if this is a leaf node + boolean isLeaf = leftChildren[nodeId] == -1; + + if (isLeaf) { + // Leaf node: set the leaf value from base_weights + // Apply float rounding to match XGBoost's internal precision + double leafValue = baseWeights[nodeId]; + setField(node, "leaf", leafValue); + } else { + // Split node: set split information + int featureIdx = splitIndices[nodeId]; + setField(node, "split", "attribute(features," + featureIdx + ")"); + // Apply float rounding to match XGBoost's internal precision (same as XGBoostParser) + double splitValue = splitConditions[nodeId]; + setField(node, "split_condition", splitValue); + + int leftChild = leftChildren[nodeId]; + int rightChild = rightChildren[nodeId]; + boolean goLeftOnMissing = defaultLeft[nodeId] != 0; + + // In XGBoost trees: + // - Left child is taken when feature < threshold + // - Right child is taken when feature >= threshold + // - default_left only controls where missing values go + setField(node, "yes", leftChild); // yes = condition is true = feature < threshold = go left + setField(node, "no", rightChild); // no = condition is false = feature >= threshold = go right + setField(node, "missing", goLeftOnMissing ? leftChild : rightChild); + + // Recursively build children + List children = new ArrayList<>(); + children.add(buildTreeFromArrays(leftChild, depth + 1, leftChildren, rightChildren, + splitConditions, splitIndices, baseWeights, defaultLeft)); + children.add(buildTreeFromArrays(rightChild, depth + 1, leftChildren, rightChildren, + splitConditions, splitIndices, baseWeights, defaultLeft)); + setField(node, "children", children); + } + + return node; + } + + /** + * Uses reflection to set a private field on an object. + * + * @param obj Object to modify. + * @param fieldName Name of the field to set. + * @param value Value to set. + */ + private static void setField(Object obj, String fieldName, Object value) { + try { + Field field = obj.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(obj, value); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException("Failed to set field '" + fieldName + "' via reflection", e); + } + } + +} diff --git a/model-integration/src/test/java/ai/vespa/rankingexpression/importer/xgboost/UbjToJson.java b/model-integration/src/test/java/ai/vespa/rankingexpression/importer/xgboost/UbjToJson.java new file mode 100644 index 000000000000..83c1194c55b6 --- /dev/null +++ b/model-integration/src/test/java/ai/vespa/rankingexpression/importer/xgboost/UbjToJson.java @@ -0,0 +1,146 @@ +// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package ai.vespa.rankingexpression.importer.xgboost; + +import com.devsmart.ubjson.UBArray; +import com.devsmart.ubjson.UBObject; +import com.devsmart.ubjson.UBReader; +import com.devsmart.ubjson.UBValue; + +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Set; + +/** + * Utility to dump UBJ files as JSON format. + * Outputs the raw UBJ structure without any conversion. + * Usage: java UbjToJson + * + * @author arnej + */ +public class UbjToJson { + + public static void main(String[] args) throws IOException { + if (args.length != 1) { + System.err.println("Usage: java UbjToJson "); + System.exit(1); + } + + String ubjPath = args[0]; + UbjToJson converter = new UbjToJson(); + String json = converter.convertUbjToJson(ubjPath); + System.out.println(json); + } + + public String convertUbjToJson(String filePath) throws IOException { + try (FileInputStream fileStream = new FileInputStream(filePath); + UBReader reader = new UBReader(fileStream)) { + UBValue root = reader.read(); + return toJson(root, 0); + } + } + + private String toJson(UBValue value, int indent) { + if (value.isNull()) { + return "null"; + } else if (value.isBool()) { + return Boolean.toString(value.asBool()); + } else if (value.isNumber()) { + if (value.isInteger()) { + return Long.toString(value.asLong()); + } else { + return Double.toString(value.asFloat64()); + } + } else if (value.isString()) { + return jsonString(value.asString()); + } else if (value.isArray()) { + return arrayToJson(value.asArray(), indent); + } else if (value.isObject()) { + return objectToJson(value.asObject(), indent); + } else { + return "\"\""; + } + } + + private String arrayToJson(UBArray array, int indent) { + if (array.size() == 0) { + return "[]"; + } + + // Check if this is a typed array and if all elements are numbers + boolean isNumberArray = true; + if (array.size() > 0) { + for (int i = 0; i < array.size(); i++) { + if (!array.get(i).isNumber()) { + isNumberArray = false; + break; + } + } + } + + StringBuilder sb = new StringBuilder(); + String spaces = " ".repeat(indent); + String itemSpaces = " ".repeat(indent + 1); + + if (isNumberArray && array.size() > 10) { + // Compact format for large number arrays + sb.append("["); + for (int i = 0; i < array.size(); i++) { + if (i > 0) sb.append(", "); + sb.append(toJson(array.get(i), indent + 1)); + } + sb.append("]"); + } else { + // Regular format + sb.append("[\n"); + for (int i = 0; i < array.size(); i++) { + sb.append(itemSpaces); + sb.append(toJson(array.get(i), indent + 1)); + if (i < array.size() - 1) { + sb.append(","); + } + sb.append("\n"); + } + sb.append(spaces).append("]"); + } + + return sb.toString(); + } + + private String objectToJson(UBObject obj, int indent) { + Set keys = obj.keySet(); + if (keys.isEmpty()) { + return "{}"; + } + + StringBuilder sb = new StringBuilder(); + String spaces = " ".repeat(indent); + String itemSpaces = " ".repeat(indent + 1); + + sb.append("{\n"); + int i = 0; + for (String key : keys) { + sb.append(itemSpaces); + sb.append(jsonString(key)); + sb.append(": "); + sb.append(toJson(obj.get(key), indent + 1)); + if (i < keys.size() - 1) { + sb.append(","); + } + sb.append("\n"); + i++; + } + sb.append(spaces).append("}"); + + return sb.toString(); + } + + private String jsonString(String str) { + return "\"" + str + .replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t") + + "\""; + } +} diff --git a/model-integration/src/test/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostImportTestCase.java b/model-integration/src/test/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostImportTestCase.java index c45d99274b89..5edb73e3019d 100644 --- a/model-integration/src/test/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostImportTestCase.java +++ b/model-integration/src/test/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostImportTestCase.java @@ -26,4 +26,32 @@ public void testXGBoost() { assertEquals(1, model.outputExpressions().size()); } + @Test + public void testXGBoostUBJ() { + // Test that UBJ format imports successfully and includes base_score adjustment + XGBoostImporter importer = new XGBoostImporter(); + ImportedModel jsonModel = importer.importModel("test", "src/test/models/xgboost/binary_breast_cancer.json"); + ImportedModel ubjModel = importer.importModel("test", "src/test/models/xgboost/binary_breast_cancer.ubj"); + + assertNotNull("JSON model should be imported", jsonModel); + assertNotNull("UBJ model should be imported", ubjModel); + + RankingExpression jsonExpression = jsonModel.expressions().get("test"); + RankingExpression ubjExpression = ubjModel.expressions().get("test"); + + assertNotNull("JSON expression should exist", jsonExpression); + assertNotNull("UBJ expression should exist", ubjExpression); + + String jsonExprStr = jsonExpression.getRoot().toString(); + String ubjExprStr = ubjExpression.getRoot().toString(); + + // UBJ should include the base_score logit transformation + assertTrue("UBJ expression should contain base_score adjustment", + ubjExprStr.contains("log(0.6274165)")); + + // UBJ expression should start with the same tree expressions as JSON + assertTrue("UBJ should contain tree expressions", + ubjExprStr.startsWith(jsonExprStr)); + } + } diff --git a/model-integration/src/test/models/xgboost/binary_breast_cancer.json b/model-integration/src/test/models/xgboost/binary_breast_cancer.json new file mode 100644 index 000000000000..4370236681c7 --- /dev/null +++ b/model-integration/src/test/models/xgboost/binary_breast_cancer.json @@ -0,0 +1,343 @@ +[ + { "nodeid": 0, "depth": 0, "split": "attribute(features,20)", "split_condition": 16.81999969482422, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "attribute(features,27)", "split_condition": 0.13570000231266022, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "split": "attribute(features,10)", "split_condition": 0.6449999809265137, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 7, "depth": 3, "leaf": 0.4603551924228668 }, + { "nodeid": 8, "depth": 3, "leaf": -0.01896175928413868 } ] }, + { "nodeid": 4, "depth": 2, "split": "attribute(features,21)", "split_condition": 25.579999923706055, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 9, "depth": 3, "split": "attribute(features,23)", "split_condition": 806.9000244140625, "yes": 15, "no": 16, "missing": 16, "children": [ + { "nodeid": 15, "depth": 4, "leaf": 0.3054772615432739 }, + { "nodeid": 16, "depth": 4, "leaf": -0.15728549659252167 } ] }, + { "nodeid": 10, "depth": 3, "split": "attribute(features,6)", "split_condition": 0.09696999937295914, "yes": 17, "no": 18, "missing": 18, "children": [ + { "nodeid": 17, "depth": 4, "leaf": -0.09545934945344925 }, + { "nodeid": 18, "depth": 4, "leaf": -0.6689253449440002 } ] } ] } ] }, + { "nodeid": 2, "depth": 1, "split": "attribute(features,21)", "split_condition": 19.59000015258789, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 2, "split": "attribute(features,7)", "split_condition": 0.06254000216722488, "yes": 11, "no": 12, "missing": 12, "children": [ + { "nodeid": 11, "depth": 3, "leaf": 0.3241020143032074 }, + { "nodeid": 12, "depth": 3, "leaf": -0.5246468782424927 } ] }, + { "nodeid": 6, "depth": 2, "split": "attribute(features,26)", "split_condition": 0.1889999955892563, "yes": 13, "no": 14, "missing": 14, "children": [ + { "nodeid": 13, "depth": 3, "leaf": -0.15728549659252167 }, + { "nodeid": 14, "depth": 3, "leaf": -0.7851951718330383 } ] } ] } ] }, + { "nodeid": 0, "depth": 0, "split": "attribute(features,22)", "split_condition": 106.0, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "attribute(features,27)", "split_condition": 0.15729999542236328, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "split": "attribute(features,27)", "split_condition": 0.13570000231266022, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 7, "depth": 3, "split": "attribute(features,13)", "split_condition": 45.189998626708984, "yes": 15, "no": 16, "missing": 16, "children": [ + { "nodeid": 15, "depth": 4, "split": "attribute(features,14)", "split_condition": 0.003289999905973673, "yes": 23, "no": 24, "missing": 24, "children": [ + { "nodeid": 23, "depth": 5, "leaf": 0.11408944427967072 }, + { "nodeid": 24, "depth": 5, "leaf": 0.40121492743492126 } ] }, + { "nodeid": 16, "depth": 4, "leaf": -0.030774641782045364 } ] }, + { "nodeid": 8, "depth": 3, "split": "attribute(features,1)", "split_condition": 19.219999313354492, "yes": 17, "no": 18, "missing": 18, "children": [ + { "nodeid": 17, "depth": 4, "leaf": 0.2960207462310791 }, + { "nodeid": 18, "depth": 4, "leaf": -0.23078586161136627 } ] } ] }, + { "nodeid": 4, "depth": 2, "split": "attribute(features,22)", "split_condition": 97.66000366210938, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 9, "depth": 3, "leaf": -0.10795557498931885 }, + { "nodeid": 10, "depth": 3, "leaf": -0.3868221342563629 } ] } ] }, + { "nodeid": 2, "depth": 1, "split": "attribute(features,7)", "split_condition": 0.04845999926328659, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 2, "split": "attribute(features,17)", "split_condition": 0.009996999986469746, "yes": 11, "no": 12, "missing": 12, "children": [ + { "nodeid": 11, "depth": 3, "split": "attribute(features,1)", "split_condition": 19.3799991607666, "yes": 19, "no": 20, "missing": 20, "children": [ + { "nodeid": 19, "depth": 4, "leaf": 0.11794889718294144 }, + { "nodeid": 20, "depth": 4, "leaf": -0.42626824975013733 } ] }, + { "nodeid": 12, "depth": 3, "leaf": 0.3407819867134094 } ] }, + { "nodeid": 6, "depth": 2, "split": "attribute(features,21)", "split_condition": 20.450000762939453, "yes": 13, "no": 14, "missing": 14, "children": [ + { "nodeid": 13, "depth": 3, "split": "attribute(features,7)", "split_condition": 0.07339999824762344, "yes": 21, "no": 22, "missing": 22, "children": [ + { "nodeid": 21, "depth": 4, "leaf": 0.25160130858421326 }, + { "nodeid": 22, "depth": 4, "leaf": -0.39930129051208496 } ] }, + { "nodeid": 14, "depth": 3, "leaf": -0.5225854516029358 } ] } ] } ] }, + { "nodeid": 0, "depth": 0, "split": "attribute(features,7)", "split_condition": 0.04938000068068504, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "attribute(features,20)", "split_condition": 16.81999969482422, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "split": "attribute(features,13)", "split_condition": 43.95000076293945, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 7, "depth": 3, "split": "attribute(features,14)", "split_condition": 0.003289999905973673, "yes": 15, "no": 16, "missing": 16, "children": [ + { "nodeid": 15, "depth": 4, "leaf": 0.0977279469370842 }, + { "nodeid": 16, "depth": 4, "split": "attribute(features,21)", "split_condition": 33.209999084472656, "yes": 21, "no": 22, "missing": 22, "children": [ + { "nodeid": 21, "depth": 5, "leaf": 0.3699623644351959 }, + { "nodeid": 22, "depth": 5, "split": "attribute(features,1)", "split_condition": 26.989999771118164, "yes": 27, "no": 28, "missing": 28, "children": [ + { "nodeid": 27, "depth": 6, "leaf": -0.05926831439137459 }, + { "nodeid": 28, "depth": 6, "leaf": 0.2650623321533203 } ] } ] } ] }, + { "nodeid": 8, "depth": 3, "leaf": -0.064254529774189 } ] }, + { "nodeid": 4, "depth": 2, "split": "attribute(features,1)", "split_condition": 15.729999542236328, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 9, "depth": 3, "leaf": 0.21864144504070282 }, + { "nodeid": 10, "depth": 3, "split": "attribute(features,17)", "split_condition": 0.009232999756932259, "yes": 17, "no": 18, "missing": 18, "children": [ + { "nodeid": 17, "depth": 4, "leaf": -0.35293275117874146 }, + { "nodeid": 18, "depth": 4, "leaf": -0.06598913669586182 } ] } ] } ] }, + { "nodeid": 2, "depth": 1, "split": "attribute(features,22)", "split_condition": 101.9000015258789, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 2, "split": "attribute(features,21)", "split_condition": 25.479999542236328, "yes": 11, "no": 12, "missing": 12, "children": [ + { "nodeid": 11, "depth": 3, "leaf": 0.3031226098537445 }, + { "nodeid": 12, "depth": 3, "leaf": -0.18023964762687683 } ] }, + { "nodeid": 6, "depth": 2, "split": "attribute(features,26)", "split_condition": 0.21230000257492065, "yes": 13, "no": 14, "missing": 14, "children": [ + { "nodeid": 13, "depth": 3, "leaf": 0.14003755152225494 }, + { "nodeid": 14, "depth": 3, "split": "attribute(features,1)", "split_condition": 15.34000015258789, "yes": 19, "no": 20, "missing": 20, "children": [ + { "nodeid": 19, "depth": 4, "split": "attribute(features,6)", "split_condition": 0.14569999277591705, "yes": 23, "no": 24, "missing": 24, "children": [ + { "nodeid": 23, "depth": 5, "leaf": 0.10976236313581467 }, + { "nodeid": 24, "depth": 5, "leaf": -0.2641395330429077 } ] }, + { "nodeid": 20, "depth": 4, "split": "attribute(features,4)", "split_condition": 0.08354999870061874, "yes": 25, "no": 26, "missing": 26, "children": [ + { "nodeid": 25, "depth": 5, "leaf": -0.11790292710065842 }, + { "nodeid": 26, "depth": 5, "leaf": -0.43853873014450073 } ] } ] } ] } ] } ] }, + { "nodeid": 0, "depth": 0, "split": "attribute(features,22)", "split_condition": 102.5, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "attribute(features,24)", "split_condition": 0.17820000648498535, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "split": "attribute(features,13)", "split_condition": 49.0, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 7, "depth": 3, "split": "attribute(features,0)", "split_condition": 14.109999656677246, "yes": 13, "no": 14, "missing": 14, "children": [ + { "nodeid": 13, "depth": 4, "split": "attribute(features,8)", "split_condition": 0.23749999701976776, "yes": 19, "no": 20, "missing": 20, "children": [ + { "nodeid": 19, "depth": 5, "leaf": 0.3398503065109253 }, + { "nodeid": 20, "depth": 5, "leaf": 0.08510195463895798 } ] }, + { "nodeid": 14, "depth": 4, "leaf": 0.06697364151477814 } ] }, + { "nodeid": 8, "depth": 3, "leaf": -0.04186965152621269 } ] }, + { "nodeid": 4, "depth": 2, "leaf": -0.16538292169570923 } ] }, + { "nodeid": 2, "depth": 1, "split": "attribute(features,7)", "split_condition": 0.04845999926328659, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 2, "split": "attribute(features,21)", "split_condition": 26.440000534057617, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 9, "depth": 3, "leaf": 0.2624058723449707 }, + { "nodeid": 10, "depth": 3, "split": "attribute(features,15)", "split_condition": 0.01631000079214573, "yes": 15, "no": 16, "missing": 16, "children": [ + { "nodeid": 15, "depth": 4, "leaf": -0.3455977141857147 }, + { "nodeid": 16, "depth": 4, "leaf": 0.20227135717868805 } ] } ] }, + { "nodeid": 6, "depth": 2, "split": "attribute(features,26)", "split_condition": 0.21230000257492065, "yes": 11, "no": 12, "missing": 12, "children": [ + { "nodeid": 11, "depth": 3, "leaf": 0.12086576223373413 }, + { "nodeid": 12, "depth": 3, "split": "attribute(features,21)", "split_condition": 18.40999984741211, "yes": 17, "no": 18, "missing": 18, "children": [ + { "nodeid": 17, "depth": 4, "leaf": -0.054553307592868805 }, + { "nodeid": 18, "depth": 4, "split": "attribute(features,16)", "split_condition": 0.10270000249147415, "yes": 21, "no": 22, "missing": 22, "children": [ + { "nodeid": 21, "depth": 5, "leaf": -0.3827503025531769 }, + { "nodeid": 22, "depth": 5, "leaf": -0.09866306185722351 } ] } ] } ] } ] } ] }, + { "nodeid": 0, "depth": 0, "split": "attribute(features,23)", "split_condition": 888.2999877929688, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "attribute(features,27)", "split_condition": 0.1606999933719635, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "split": "attribute(features,1)", "split_condition": 21.309999465942383, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 7, "depth": 3, "split": "attribute(features,10)", "split_condition": 0.550599992275238, "yes": 11, "no": 12, "missing": 12, "children": [ + { "nodeid": 11, "depth": 4, "split": "attribute(features,23)", "split_condition": 811.2999877929688, "yes": 15, "no": 16, "missing": 16, "children": [ + { "nodeid": 15, "depth": 5, "leaf": 0.3330357074737549 }, + { "nodeid": 16, "depth": 5, "split": "attribute(features,15)", "split_condition": 0.01841999962925911, "yes": 19, "no": 20, "missing": 20, "children": [ + { "nodeid": 19, "depth": 6, "leaf": 0.226839080452919 }, + { "nodeid": 20, "depth": 6, "leaf": -0.04981991648674011 } ] } ] }, + { "nodeid": 12, "depth": 4, "leaf": 0.06199278682470322 } ] }, + { "nodeid": 8, "depth": 3, "split": "attribute(features,23)", "split_condition": 648.2999877929688, "yes": 13, "no": 14, "missing": 14, "children": [ + { "nodeid": 13, "depth": 4, "leaf": 0.26304540038108826 }, + { "nodeid": 14, "depth": 4, "split": "attribute(features,14)", "split_condition": 0.005872000008821487, "yes": 17, "no": 18, "missing": 18, "children": [ + { "nodeid": 17, "depth": 5, "leaf": 0.058584075421094894 }, + { "nodeid": 18, "depth": 5, "split": "attribute(features,9)", "split_condition": 0.06066000089049339, "yes": 21, "no": 22, "missing": 22, "children": [ + { "nodeid": 21, "depth": 6, "leaf": -0.37050333619117737 }, + { "nodeid": 22, "depth": 6, "leaf": -0.07894755154848099 } ] } ] } ] } ] }, + { "nodeid": 4, "depth": 2, "leaf": -0.2753813862800598 } ] }, + { "nodeid": 2, "depth": 1, "split": "attribute(features,6)", "split_condition": 0.0729300007224083, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 2, "split": "attribute(features,1)", "split_condition": 19.510000228881836, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 9, "depth": 3, "leaf": 0.18285000324249268 }, + { "nodeid": 10, "depth": 3, "leaf": -0.2675382196903229 } ] }, + { "nodeid": 6, "depth": 2, "leaf": -0.3542742133140564 } ] } ] }, + { "nodeid": 0, "depth": 0, "split": "attribute(features,27)", "split_condition": 0.1111999973654747, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "attribute(features,3)", "split_condition": 698.7999877929688, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "split": "attribute(features,28)", "split_condition": 0.1987999975681305, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 7, "depth": 3, "leaf": 0.0755092054605484 }, + { "nodeid": 8, "depth": 3, "split": "attribute(features,21)", "split_condition": 33.209999084472656, "yes": 15, "no": 16, "missing": 16, "children": [ + { "nodeid": 15, "depth": 4, "leaf": 0.3183874487876892 }, + { "nodeid": 16, "depth": 4, "leaf": 0.11721514165401459 } ] } ] }, + { "nodeid": 4, "depth": 2, "split": "attribute(features,1)", "split_condition": 20.219999313354492, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 9, "depth": 3, "leaf": 0.08036360889673233 }, + { "nodeid": 10, "depth": 3, "leaf": -0.252962589263916 } ] } ] }, + { "nodeid": 2, "depth": 1, "split": "attribute(features,23)", "split_condition": 734.5999755859375, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 2, "split": "attribute(features,27)", "split_condition": 0.17649999260902405, "yes": 11, "no": 12, "missing": 12, "children": [ + { "nodeid": 11, "depth": 3, "split": "attribute(features,1)", "split_condition": 20.25, "yes": 17, "no": 18, "missing": 18, "children": [ + { "nodeid": 17, "depth": 4, "leaf": 0.2636157274246216 }, + { "nodeid": 18, "depth": 4, "leaf": 0.028473349288105965 } ] }, + { "nodeid": 12, "depth": 3, "leaf": -0.21361969411373138 } ] }, + { "nodeid": 6, "depth": 2, "split": "attribute(features,21)", "split_condition": 19.899999618530273, "yes": 13, "no": 14, "missing": 14, "children": [ + { "nodeid": 13, "depth": 3, "split": "attribute(features,13)", "split_condition": 43.400001525878906, "yes": 19, "no": 20, "missing": 20, "children": [ + { "nodeid": 19, "depth": 4, "leaf": 0.22751691937446594 }, + { "nodeid": 20, "depth": 4, "leaf": -0.22558310627937317 } ] }, + { "nodeid": 14, "depth": 3, "leaf": -0.32582372426986694 } ] } ] } ] }, + { "nodeid": 0, "depth": 0, "split": "attribute(features,7)", "split_condition": 0.04938000068068504, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "attribute(features,3)", "split_condition": 698.7999877929688, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "split": "attribute(features,13)", "split_condition": 41.5099983215332, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 7, "depth": 3, "split": "attribute(features,21)", "split_condition": 33.209999084472656, "yes": 15, "no": 16, "missing": 16, "children": [ + { "nodeid": 15, "depth": 4, "split": "attribute(features,19)", "split_condition": 0.0013810000382363796, "yes": 19, "no": 20, "missing": 20, "children": [ + { "nodeid": 19, "depth": 5, "leaf": 0.03624707832932472 }, + { "nodeid": 20, "depth": 5, "leaf": 0.3080938160419464 } ] }, + { "nodeid": 16, "depth": 4, "split": "attribute(features,28)", "split_condition": 0.24799999594688416, "yes": 21, "no": 22, "missing": 22, "children": [ + { "nodeid": 21, "depth": 5, "leaf": 0.17582161724567413 }, + { "nodeid": 22, "depth": 5, "leaf": -0.09668976068496704 } ] } ] }, + { "nodeid": 8, "depth": 3, "leaf": -0.03945023939013481 } ] }, + { "nodeid": 4, "depth": 2, "split": "attribute(features,1)", "split_condition": 19.510000228881836, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 9, "depth": 3, "leaf": 0.045127417892217636 }, + { "nodeid": 10, "depth": 3, "leaf": -0.2194928079843521 } ] } ] }, + { "nodeid": 2, "depth": 1, "split": "attribute(features,21)", "split_condition": 23.75, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 2, "split": "attribute(features,23)", "split_condition": 809.7000122070312, "yes": 11, "no": 12, "missing": 12, "children": [ + { "nodeid": 11, "depth": 3, "leaf": 0.24866445362567902 }, + { "nodeid": 12, "depth": 3, "split": "attribute(features,15)", "split_condition": 0.020749999210238457, "yes": 17, "no": 18, "missing": 18, "children": [ + { "nodeid": 17, "depth": 4, "leaf": -0.005317678675055504 }, + { "nodeid": 18, "depth": 4, "leaf": -0.24861639738082886 } ] } ] }, + { "nodeid": 6, "depth": 2, "split": "attribute(features,23)", "split_condition": 680.5999755859375, "yes": 13, "no": 14, "missing": 14, "children": [ + { "nodeid": 13, "depth": 3, "leaf": -0.06991633772850037 }, + { "nodeid": 14, "depth": 3, "leaf": -0.31441980600357056 } ] } ] } ] }, + { "nodeid": 0, "depth": 0, "split": "attribute(features,27)", "split_condition": 0.1111999973654747, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "attribute(features,3)", "split_condition": 698.7999877929688, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "split": "attribute(features,28)", "split_condition": 0.1987999975681305, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 7, "depth": 3, "leaf": 0.03794674202799797 }, + { "nodeid": 8, "depth": 3, "split": "attribute(features,21)", "split_condition": 33.209999084472656, "yes": 13, "no": 14, "missing": 14, "children": [ + { "nodeid": 13, "depth": 4, "leaf": 0.29745399951934814 }, + { "nodeid": 14, "depth": 4, "leaf": 0.09482062608003616 } ] } ] }, + { "nodeid": 4, "depth": 2, "split": "attribute(features,1)", "split_condition": 20.219999313354492, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 9, "depth": 3, "leaf": 0.06832267343997955 }, + { "nodeid": 10, "depth": 3, "leaf": -0.20128701627254486 } ] } ] }, + { "nodeid": 2, "depth": 1, "split": "attribute(features,22)", "split_condition": 116.19999694824219, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 2, "split": "attribute(features,21)", "split_condition": 27.489999771118164, "yes": 11, "no": 12, "missing": 12, "children": [ + { "nodeid": 11, "depth": 3, "split": "attribute(features,27)", "split_condition": 0.1606999933719635, "yes": 15, "no": 16, "missing": 16, "children": [ + { "nodeid": 15, "depth": 4, "leaf": 0.21199870109558105 }, + { "nodeid": 16, "depth": 4, "leaf": -0.09946209192276001 } ] }, + { "nodeid": 12, "depth": 3, "split": "attribute(features,23)", "split_condition": 699.4000244140625, "yes": 17, "no": 18, "missing": 18, "children": [ + { "nodeid": 17, "depth": 4, "leaf": -0.02004398964345455 }, + { "nodeid": 18, "depth": 4, "leaf": -0.25623419880867004 } ] } ] }, + { "nodeid": 6, "depth": 2, "leaf": -0.30207687616348267 } ] } ] }, + { "nodeid": 0, "depth": 0, "split": "attribute(features,27)", "split_condition": 0.14239999651908875, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "attribute(features,23)", "split_condition": 967.0, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "split": "attribute(features,13)", "split_condition": 35.2400016784668, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 7, "depth": 3, "split": "attribute(features,21)", "split_condition": 30.149999618530273, "yes": 13, "no": 14, "missing": 14, "children": [ + { "nodeid": 13, "depth": 4, "leaf": 0.2801840007305145 }, + { "nodeid": 14, "depth": 4, "split": "attribute(features,1)", "split_condition": 23.5, "yes": 17, "no": 18, "missing": 18, "children": [ + { "nodeid": 17, "depth": 5, "leaf": -0.1386641263961792 }, + { "nodeid": 18, "depth": 5, "leaf": 0.20163708925247192 } ] } ] }, + { "nodeid": 8, "depth": 3, "split": "attribute(features,19)", "split_condition": 0.002767999889329076, "yes": 15, "no": 16, "missing": 16, "children": [ + { "nodeid": 15, "depth": 4, "leaf": -0.17439478635787964 }, + { "nodeid": 16, "depth": 4, "leaf": 0.12734214961528778 } ] } ] }, + { "nodeid": 4, "depth": 2, "split": "attribute(features,28)", "split_condition": 0.2533000111579895, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 9, "depth": 3, "leaf": -0.007179913576692343 }, + { "nodeid": 10, "depth": 3, "leaf": -0.20481328666210175 } ] } ] }, + { "nodeid": 2, "depth": 1, "split": "attribute(features,13)", "split_condition": 21.459999084472656, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 2, "leaf": -4.309949144953862e-05 }, + { "nodeid": 6, "depth": 2, "split": "attribute(features,4)", "split_condition": 0.08998999744653702, "yes": 11, "no": 12, "missing": 12, "children": [ + { "nodeid": 11, "depth": 3, "leaf": -0.06140953674912453 }, + { "nodeid": 12, "depth": 3, "leaf": -0.28825199604034424 } ] } ] } ] }, + { "nodeid": 0, "depth": 0, "split": "attribute(features,7)", "split_condition": 0.04938000068068504, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "attribute(features,20)", "split_condition": 16.81999969482422, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "split": "attribute(features,15)", "split_condition": 0.012029999867081642, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 7, "depth": 3, "split": "attribute(features,16)", "split_condition": 0.012719999998807907, "yes": 15, "no": 16, "missing": 16, "children": [ + { "nodeid": 15, "depth": 4, "leaf": 0.22184161841869354 }, + { "nodeid": 16, "depth": 4, "leaf": -0.15230606496334076 } ] }, + { "nodeid": 8, "depth": 3, "leaf": 0.27187174558639526 } ] }, + { "nodeid": 4, "depth": 2, "split": "attribute(features,28)", "split_condition": 0.2653999924659729, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 9, "depth": 3, "leaf": 0.03678994998335838 }, + { "nodeid": 10, "depth": 3, "leaf": -0.13423432409763336 } ] } ] }, + { "nodeid": 2, "depth": 1, "split": "attribute(features,21)", "split_condition": 23.75, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 2, "split": "attribute(features,23)", "split_condition": 809.7000122070312, "yes": 11, "no": 12, "missing": 12, "children": [ + { "nodeid": 11, "depth": 3, "leaf": 0.20270322263240814 }, + { "nodeid": 12, "depth": 3, "leaf": -0.15306414663791656 } ] }, + { "nodeid": 6, "depth": 2, "split": "attribute(features,6)", "split_condition": 0.09060999751091003, "yes": 13, "no": 14, "missing": 14, "children": [ + { "nodeid": 13, "depth": 3, "leaf": -0.05368896201252937 }, + { "nodeid": 14, "depth": 3, "leaf": -0.2783971130847931 } ] } ] } ] }, + { "nodeid": 0, "depth": 0, "split": "attribute(features,26)", "split_condition": 0.2079000025987625, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "attribute(features,13)", "split_condition": 40.5099983215332, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "leaf": 0.27354902029037476 }, + { "nodeid": 4, "depth": 2, "leaf": -0.05269660800695419 } ] }, + { "nodeid": 2, "depth": 1, "split": "attribute(features,23)", "split_condition": 648.2999877929688, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 2, "split": "attribute(features,7)", "split_condition": 0.055959999561309814, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 7, "depth": 3, "leaf": 0.19431108236312866 }, + { "nodeid": 8, "depth": 3, "leaf": -0.042131464928388596 } ] }, + { "nodeid": 6, "depth": 2, "split": "attribute(features,21)", "split_condition": 19.899999618530273, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 9, "depth": 3, "leaf": 0.04776393994688988 }, + { "nodeid": 10, "depth": 3, "split": "attribute(features,24)", "split_condition": 0.10920000076293945, "yes": 11, "no": 12, "missing": 12, "children": [ + { "nodeid": 11, "depth": 4, "leaf": 0.011151635088026524 }, + { "nodeid": 12, "depth": 4, "leaf": -0.26751622557640076 } ] } ] } ] } ] }, + { "nodeid": 0, "depth": 0, "split": "attribute(features,23)", "split_condition": 967.0, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "attribute(features,21)", "split_condition": 29.25, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "split": "attribute(features,12)", "split_condition": 3.430000066757202, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 7, "depth": 3, "split": "attribute(features,29)", "split_condition": 0.10189999639987946, "yes": 11, "no": 12, "missing": 12, "children": [ + { "nodeid": 11, "depth": 4, "leaf": 0.25556275248527527 }, + { "nodeid": 12, "depth": 4, "leaf": 0.018566781654953957 } ] }, + { "nodeid": 8, "depth": 3, "leaf": -0.01612720638513565 } ] }, + { "nodeid": 4, "depth": 2, "split": "attribute(features,27)", "split_condition": 0.09139999747276306, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 9, "depth": 3, "leaf": 0.14816634356975555 }, + { "nodeid": 10, "depth": 3, "split": "attribute(features,24)", "split_condition": 0.13410000503063202, "yes": 13, "no": 14, "missing": 14, "children": [ + { "nodeid": 13, "depth": 4, "leaf": -0.0205707810819149 }, + { "nodeid": 14, "depth": 4, "leaf": -0.2519259452819824 } ] } ] } ] }, + { "nodeid": 2, "depth": 1, "split": "attribute(features,25)", "split_condition": 0.17110000550746918, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 2, "leaf": -0.02155451662838459 }, + { "nodeid": 6, "depth": 2, "leaf": -0.2605815827846527 } ] } ] }, + { "nodeid": 0, "depth": 0, "split": "attribute(features,22)", "split_condition": 120.4000015258789, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "attribute(features,21)", "split_condition": 29.25, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "split": "attribute(features,25)", "split_condition": 0.32350000739097595, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 3, "split": "attribute(features,25)", "split_condition": 0.08340000361204147, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 9, "depth": 4, "leaf": -0.03031761385500431 }, + { "nodeid": 10, "depth": 4, "leaf": 0.2458493560552597 } ] }, + { "nodeid": 6, "depth": 3, "split": "attribute(features,23)", "split_condition": 734.5999755859375, "yes": 11, "no": 12, "missing": 12, "children": [ + { "nodeid": 11, "depth": 4, "leaf": 0.10233080387115479 }, + { "nodeid": 12, "depth": 4, "leaf": -0.09648152440786362 } ] } ] }, + { "nodeid": 4, "depth": 2, "split": "attribute(features,26)", "split_condition": 0.20280000567436218, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 7, "depth": 3, "leaf": 0.13269340991973877 }, + { "nodeid": 8, "depth": 3, "split": "attribute(features,15)", "split_condition": 0.017960000783205032, "yes": 13, "no": 14, "missing": 14, "children": [ + { "nodeid": 13, "depth": 4, "leaf": -0.24554097652435303 }, + { "nodeid": 14, "depth": 4, "leaf": -0.033455345779657364 } ] } ] } ] }, + { "nodeid": 2, "depth": 1, "leaf": -0.23360854387283325 } ] }, + { "nodeid": 0, "depth": 0, "split": "attribute(features,23)", "split_condition": 876.5, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "attribute(features,24)", "split_condition": 0.14069999754428864, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "split": "attribute(features,15)", "split_condition": 0.01104000024497509, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 7, "depth": 3, "leaf": -0.0031496614683419466 }, + { "nodeid": 8, "depth": 3, "split": "attribute(features,6)", "split_condition": 0.1111999973654747, "yes": 11, "no": 12, "missing": 12, "children": [ + { "nodeid": 11, "depth": 4, "leaf": 0.23949843645095825 }, + { "nodeid": 12, "depth": 4, "leaf": 0.03775443509221077 } ] } ] }, + { "nodeid": 4, "depth": 2, "split": "attribute(features,22)", "split_condition": 91.62000274658203, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 9, "depth": 3, "leaf": 0.10552486777305603 }, + { "nodeid": 10, "depth": 3, "split": "attribute(features,21)", "split_condition": 27.209999084472656, "yes": 13, "no": 14, "missing": 14, "children": [ + { "nodeid": 13, "depth": 4, "leaf": -0.023241691291332245 }, + { "nodeid": 14, "depth": 4, "leaf": -0.20789241790771484 } ] } ] } ] }, + { "nodeid": 2, "depth": 1, "split": "attribute(features,6)", "split_condition": 0.05928000062704086, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 2, "leaf": 0.004834835417568684 }, + { "nodeid": 6, "depth": 2, "leaf": -0.23364728689193726 } ] } ] }, + { "nodeid": 0, "depth": 0, "split": "attribute(features,26)", "split_condition": 0.2079000025987625, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "attribute(features,13)", "split_condition": 40.5099983215332, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "leaf": 0.23598936200141907 }, + { "nodeid": 4, "depth": 2, "leaf": -0.060127921402454376 } ] }, + { "nodeid": 2, "depth": 1, "split": "attribute(features,21)", "split_condition": 25.579999923706055, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 2, "split": "attribute(features,23)", "split_condition": 811.2999877929688, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 7, "depth": 3, "leaf": 0.173164963722229 }, + { "nodeid": 8, "depth": 3, "leaf": -0.09621238708496094 } ] }, + { "nodeid": 6, "depth": 2, "split": "attribute(features,4)", "split_condition": 0.08946000039577484, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 9, "depth": 3, "leaf": -0.03863441199064255 }, + { "nodeid": 10, "depth": 3, "leaf": -0.21613681316375732 } ] } ] } ] }, + { "nodeid": 0, "depth": 0, "split": "attribute(features,13)", "split_condition": 33.0099983215332, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "attribute(features,24)", "split_condition": 0.13770000636577606, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "split": "attribute(features,14)", "split_condition": 0.004147999919950962, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 7, "depth": 3, "leaf": -0.005957402754575014 }, + { "nodeid": 8, "depth": 3, "leaf": 0.22239074110984802 } ] }, + { "nodeid": 4, "depth": 2, "split": "attribute(features,22)", "split_condition": 91.62000274658203, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 9, "depth": 3, "leaf": 0.1025158017873764 }, + { "nodeid": 10, "depth": 3, "leaf": -0.12606994807720184 } ] } ] }, + { "nodeid": 2, "depth": 1, "split": "attribute(features,24)", "split_condition": 0.11180000007152557, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 2, "leaf": 0.04408538341522217 }, + { "nodeid": 6, "depth": 2, "split": "attribute(features,21)", "split_condition": 23.190000534057617, "yes": 11, "no": 12, "missing": 12, "children": [ + { "nodeid": 11, "depth": 3, "leaf": -0.03383628651499748 }, + { "nodeid": 12, "depth": 3, "leaf": -0.21980330348014832 } ] } ] } ] }, + { "nodeid": 0, "depth": 0, "split": "attribute(features,26)", "split_condition": 0.2079000025987625, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "attribute(features,13)", "split_condition": 40.5099983215332, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "leaf": 0.21418677270412445 }, + { "nodeid": 4, "depth": 2, "leaf": -0.03940589725971222 } ] }, + { "nodeid": 2, "depth": 1, "split": "attribute(features,23)", "split_condition": 967.0, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 2, "split": "attribute(features,21)", "split_condition": 29.25, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 7, "depth": 3, "split": "attribute(features,13)", "split_condition": 23.309999465942383, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 9, "depth": 4, "leaf": 0.17243850231170654 }, + { "nodeid": 10, "depth": 4, "leaf": -0.014778186567127705 } ] }, + { "nodeid": 8, "depth": 3, "leaf": -0.1282825917005539 } ] }, + { "nodeid": 6, "depth": 2, "leaf": -0.20745433866977692 } ] } ] }, + { "nodeid": 0, "depth": 0, "split": "attribute(features,27)", "split_condition": 0.1606999933719635, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "attribute(features,1)", "split_condition": 20.200000762939453, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "split": "attribute(features,13)", "split_condition": 35.029998779296875, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 3, "split": "attribute(features,20)", "split_condition": 16.25, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 9, "depth": 4, "leaf": 0.20469191670417786 }, + { "nodeid": 10, "depth": 4, "leaf": 0.05293010547757149 } ] }, + { "nodeid": 6, "depth": 3, "leaf": 0.0055474769324064255 } ] }, + { "nodeid": 4, "depth": 2, "split": "attribute(features,23)", "split_condition": 653.5999755859375, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 7, "depth": 3, "leaf": 0.1103351041674614 }, + { "nodeid": 8, "depth": 3, "split": "attribute(features,5)", "split_condition": 0.07326000183820724, "yes": 11, "no": 12, "missing": 12, "children": [ + { "nodeid": 11, "depth": 4, "leaf": -0.19937729835510254 }, + { "nodeid": 12, "depth": 4, "leaf": -0.011575295589864254 } ] } ] } ] }, + { "nodeid": 2, "depth": 1, "leaf": -0.16972780227661133 } ] }, + { "nodeid": 0, "depth": 0, "split": "attribute(features,7)", "split_condition": 0.04938000068068504, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "attribute(features,15)", "split_condition": 0.012029999867081642, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "split": "attribute(features,17)", "split_condition": 0.0074970000423491, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 7, "depth": 3, "leaf": 0.0373166985809803 }, + { "nodeid": 8, "depth": 3, "leaf": -0.14507374167442322 } ] }, + { "nodeid": 4, "depth": 2, "leaf": 0.18085254728794098 } ] }, + { "nodeid": 2, "depth": 1, "split": "attribute(features,21)", "split_condition": 23.75, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 2, "leaf": 0.021509487181901932 }, + { "nodeid": 6, "depth": 2, "leaf": -0.1693851202726364 } ] } ] }, + { "nodeid": 0, "depth": 0, "split": "attribute(features,26)", "split_condition": 0.2079000025987625, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "attribute(features,13)", "split_condition": 40.5099983215332, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "leaf": 0.18477416038513184 }, + { "nodeid": 4, "depth": 2, "leaf": -0.027129333466291428 } ] }, + { "nodeid": 2, "depth": 1, "split": "attribute(features,23)", "split_condition": 734.5999755859375, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 2, "leaf": 0.05231140926480293 }, + { "nodeid": 6, "depth": 2, "split": "attribute(features,21)", "split_condition": 22.149999618530273, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 7, "depth": 3, "leaf": -0.007632177323102951 }, + { "nodeid": 8, "depth": 3, "leaf": -0.16687200963497162 } ] } ] } ] } ] diff --git a/model-integration/src/test/models/xgboost/binary_breast_cancer.ubj b/model-integration/src/test/models/xgboost/binary_breast_cancer.ubj new file mode 100644 index 0000000000000000000000000000000000000000..e66e270b52085479b2a2ca8a2b9265b7edbe8566 GIT binary patch literal 25614 zcmds=2UrwG`@lh>QUwG=ELdWUCN_jvZ)av#(k!v2SP|jCGYHCICCZ5+XfzgVu^UV5 zf<|NE_AJrlvqfW~Q4>pyC4od^|J20)ybJGgcif)l3lIOt49_rScc<<9{^p(8y}Q(o zE`_dQf<8JqQJP<9obaM1ywPydsVS|;dgamy;6r9!_ zJL?*TF%LKijv3X1<(fBB%l9+zjz*^#k`ki~iLpacl8d_%WNR!Q0?tsATXp_!;~Sl??%gK(#WZt%k(7q~c-%c`;A*hlP|^T5DB++F|bj z(MEmL2)!YGAdF|MI-UU$<<3l^^XEL=U3E+X-SgV(mZ-sVxOktLnNiMd-liy~2d{VN3ON*m> z=U03@Dv=9_SLqcq_KGC{oP=qV@ZrqpWz9RP?+YvbtyO_KM4;xbr44qv$vykz7h@>kNa_78Pi%J*5_v&l3?KVB_+lhKwE8xQk*!g5AnSHhq!QiH~K>8 zcJc7?Q-Ue*zeJqW62(9A%^mZF1M^;aTJiTOtdUc}5MwLbASZ#`SLZ&6d*~d5ATQOp zM;FW^FaS;+cF?o1I)L-jinW)KqivwxXf#A~IkuLp@^1B|+_2?R*qZLrf>vF{rU$dx z*mr73b@ttq=3TrbYPdcoB`H}w z$u(@um?twaM0c>!ZkpY5k!5SlRl0Q8&z286OcG+>%(a}jH#3|LY#085ZSd}#kNVDPr$5@#DSIQ2E3K zo&qSJFsGXtl&|f(i#-31@@bPA2BZr2Kx!CZ2qEpyJrElP7j70hjKjI$DisFK|KosY z5FFaz*x`j*91oH1{@7ib{Pp+Jd)4kq8MB{fFTS&s-Ol+{YCYkMGb9eR3MiRd0N)odX>)I zv&KAr?nM%H=|f9u`hK#Y`6ap4beZ_Zxmq>{9}>QLVJ`i6&vf~-E%VKpFLaSNZrN(t zzOpFDvHTU~VxNHdAJl+JHewW*Z<-96eO$3Nc+Qj4x=~sVKWdfUR;g zC)7;ieXzO$h7A1ETHQD&C{{PW4p0Oh7!DqcP&7or7zM0V!3H^*BjENqog;AQ`FS?h zIZxZn7jPcWtIB<~qIeCw);`2`Ih`|V3hkv$V985|4j+-49zG(bZVVI;o$@3-^^IUo zK!w%?zgEs#p~YcCR)9iF&z;*fGhqGz zYu|c~fO*B*!3_ro=c0oZ0W6Kc;qk(h)qfl?jXCA%6k2OusVSYP|Ei?F+)-ljMCu;% z8k^C(u$}y?gQN2`ueQ)~ZTa($J<3-88=ATmCw_4E;zldj} zV#YGl@pSs}e_Bf3owrD#9mi2qi_@&-&|`GgfPwVDv2#*|kj7HAs$=Oy(?H2}YBrr= zRQFKJY7G#MgY-|=0KvK|oqnJ#=3cZ+x_FJowDFOr{B=+sxDjG;~m%6Z=C4WL5I5MM(Nr~`(^y*reOZA@;E>s1N7y(VfKq4kw1CP|*u+EPY4Vv=*WHbcC`e~- zn?+I1^b-FZ7$N#}UQXrBM`*#9+r&FQx1&CZ86WTli8kZ*L{rlUT8iu|g+SF$}MbRm{^i7BNq2($;=hF^%;)C2o*# zDepvNlP%$qEmy^n4l~<`%l1wq5qX2eCEViWiURu@s3_tm+*;;}A~pVa{?jOmHff>5 zKv|F{peQ^Ifq{lR6j%&-mU0Y2w3Z=W!eNIs2y0zl@wIr7mhBrP4Gg*}o~sfk-V8E` z2U!>k&0Wssb)706cCA1^&95n1_LyjOrQN1{+IK~+yxtI)C;sx# zl6JK&9dxdTxz3zyq1CBOx&PiBWbCrbmU^-0%+s26l$SpIgnmFHiB$E=T^nhEJa%?R zA#lWTvPd39B0OiC50rmH9hGN_uhvFDu~@mNyC*={3fMJm!a^~cgyqLnx}tjXJavbg z$6-6R#6AR`i`8qAHds9Uu{#^TFpj0C`q9;9M!Rj!XJhZYOOE&5O%3l|;HBF*azA7R zX3&tB{$(h%;GWo5bJkJ;9%H!1_Fz+i;Gm>QviE6vVFpl$d~HJ0e$UNO1I|WRaB`qTg$B{ zV{UDGl1gcl7zO|a5l9S#1j2$GLOys$)I=MXA6Dc}d6o?cdxry{L1ZZ3k;BWhm>ewW zTC@|tT)17#`TA=y_wG8@xLFL#8T7L_VQUp}&Cw$?eaz2Pp4xyeII);jn$m%Fd-DUj zHX%k#sj*a?x9czFK4&;J{aKUwAN-wW3V(>x->5~WjjTe)eRc{fFDo{en#~)aTeWf@ zW&QV9>K^l=afHeXzwB*3=X+9~zvdgEdZHH{+N726VZmPct<#NmdRA#>KD7C(ycOXc)KS*6|jUzj)Fo*S_hdhPB)p~Cnt9*mM`X6hgh2<&i=3O z;n`cX-n>F@tjGyhVHh29)oK`R_7eE{Dp&Cw46#0^91TEAR=O6aTC(EV{0tO-#gaww z=X(s1O-|2$lK5+L6FP_%EbQl^1#8Vq>o}aN#XHqj(BPyqvDRW_fE2Q~oj5)J6Y-OI zmqb&uE3Eu?m&Hm4Ulm)#kE0oXEftfN-KVDdo9TxcRoUEwMwW3fSbRJAJUu>g4;!>~ z5S=9dmyWMkm$mm>gg2m!m{@x6;RWd8>QAB-mIqkc*SFB7x5mj2_f_8>v45w<@Nz8e zMBL4*Zm!yu|7xn-b!WCYHEV{PJ^KqXE#-BRy1Av?eAX0FPXAE$`SU(`Yt(IZ6#i9A zbjNP}O(ueJa+$_g#Y)(xMaNYe)n1vMFtKlquh`%hXVRtBP4wG$FOZp=zUMW$kS?oC zG%05{&H6nP()NxX|6{6s9(JxYmp^Kz9vTO20qkRL0- zMy$^{6N&%vw<3q`b`jTY43xa@>?7lHCWH73+dFO${|FCn`%3)Vj>KQg<12j)@qaQC zfpf~%2M^IU1Opy;=!j)JTV3HpTyfaJ(qy9eX!SedLeEa3s0$FY3f^T^ujkNnJ4c8w z#C{^Wl5WuSRg=Xa-&~Y`snci%ePu#tIp+2@-K;KyE&XbrCxZ+R$%oAk2vu@@Wsj5{ zr1Oj z6)o(Tk*DhtKjrTTwKn0PLx6u+_5;meB@BcE;=$BV;)y)ZE8(z18f4QJQY~B0U;MV> zDlz8Q~6~?top|b>GxqjsjVs_p0&gZ|LLBjPup!L&(es!59G^T&yy*Y7MtJwK8}2`BFoaS z@eT6Ye?Krcn&~C9TT#o>Vfl4ki>*E7tb3QR@@MP$Iv}RlVIN>+t9~|0n_Y0R$}Ute zo(J;mfoTYN+6&WIhbF6h*!>GH(p%p=$6B3LpLWx#w^z`-T;+KTFHLbY2JEFmhow4V zlTTV<6YyMnHrYROQQlF-0-dl3y05YbyT|jORi2oJkf*&djpaIJj}Pf_$t>0N6qKja zMFJ@3TXcbEDAocfoL|B01yGwc&|rAS6GH2iBit|Rl_gIqZxccIq?II;RqU{LT3qoF zw_K?rdR%-c_Wd=Co!eH8e%msMy_D2d41cRHJ$f>qPOSJjJ#aTutd=x|W;JvXJq9IU zp=GsJEVcbJUtQ{rEhMksRtwvBmDY+qDUS^NBrhjBM9vs=iwegJ@+Q=bAi>-7Ep@uf zB>i+P-Mo;#d8;Q>l1-0>sJ(us>>=xoTo4QF1rSC3c$qd);Bu8HJZ?i0o>nQsB*{%`;7elpbL%B`_Xg1j^!mOj@AxSX+!p|UXA$}Vow|q#y71{%kf-$ z!q{6s&@J#!OBxs!l{7qVLl~Y`>B8gc9+>^GI3s1lE^^3~AP!IlK0)NoJ?vkhvwi~{-ea_JIPx0Sjh4u1G00b(U&mW~ z@2`PuTV{W;>e+B+dhLChHBe-;26qu%wk)K3bUWxu*Fe_9y8}C!vY4(rXrNQ}&Y)kv zqds;kD;Aa7=Zk#XZW=V5%sRhXPHGuQ-8(Ll#~mF)n-u7EY3&D-iWk1JRB1Up?|z?^ zB-L_VI6m{3dH=lXa%xglvxoaA^$z@NSfuVaKLv|Gk+?vDnV|59hYE*ySb{q36}u|MRI<_}C?^ zQo&4`UF|+=lChYY&PTA^*S?|CH&vulU#dyhkB_D~nP=3tl+pUC^jNfO_`uC%(SVVb z5dWX3$9H|@_Wf$KT|9WQoFwXYy)|emjR-v>d|xxc;_~A@GK>Za)mG^9WPKmG$Hm=h zum3eH^1&`W1&g>8svJV4DpEvECSmbTSQPncO`6lSCw(FE0)1t<3yu2r8WJ(Cm$@%&P;i;9LtlS$Zc=PZisUE78D1*{R0zn-IuxELyZ zE)2C6Rd_}SqAH%=7^o$x&RGP^0hfIlt@zlgWT3pns zXU;D2%5z`dgSy-rC4bs_7#aF$bMx>wHWT+di!IfvQxev#fqB1|i#e2r%6+@tS9|$P zu?HTgp~D`)daM#Aw7CP9s_QZ24bQV#mtj5@r^y*FR%3xL{WRnSy0k+VY!RL4eT0k{ zc>~iJVrh=J;+4kh#1naFzuvO3ZLlCGHVt%BU0-2$c%EX{U>ZX#&nY*&a0dszy9dQe z`Fz3WEsko5l{P1!fw*&H%>iHsY^mEgY@h}g9EpS0TFFc75){DZMfDWjF2}JR-pO=F ztci8~Y6@F?Wg^{sraZmyd1d<9zCA4F{l;p&Wn_<(+B>K|yJ+OMWJ%w}@|CvzX_ZPV zWcP(eA!6nQOK6I2*S;2ANzBeT;rOkivTM~;v*+A=IXW;@?d>zg0>|5RKm+v~S+r>Y z7ppYjag_o*uI|jBnqY|Iq)7!IdZ+SLni((C2B%JvzkUp)FaO0K7IUkkb%WgyI&xU5 zBRV{3zt`UCfC*Mz*gK&FbYG)F~}|r04i|#`w6l6@I=K*ETc% zCRiCDfZ~pfT2B%{ZE8S+!Gm=H9v+aPbzw~ro?Yy)!5Z{{@`@dvti=x>u}#Mz>_SR+ zHmlwgHo>r$wF;_BCvF|XVt?;R(?1)=ULVz-j_Vgpe?Q!qrtcfco}W_Y?*^6Hey`Uq zw}s`&`GEh(nI{{O-rc`6*BKbt=Fyf>a*xDc$kLvTWw$=#Nwe(jdC9fa#niL)yvWhy z0rc=gNzkSTT&mIo)r03%gg6gvo)}?qn$+;3J2uau@A^%q`T;+pKvKi%dk`l_O*=3T zLM+V@EuPkv0NAN-dpe;6bW^2-RX5#>x@q%d2pi^<4yr8yd$cH4%Kiu!D{&|POrry_ z(xw755N!#tAJ8EYY&0Ajs6hiq;-JL>FR@|YcUa=KAK2L_!h+nY&@J&(*n9(HuK9m5 zA#^ysJ}{8xgtn&#Lzk)bK6AE2>&)R9M7WzSH$GCET#CIV>%2r7(yVIRtzRw_F5Zih zZ`UXwUFYu=-a8`59UD&16IOLrd;2f3CDgB3)TRMQnL>jiwJ5Qx!H<`)DR7)Lso+Bw z^!k7r9?b-yCOMP4(0<2<=@w67kOoIaxZN*|75~Rl9nnF{maw-WaK)kQU|@kxC;{D9 zC{fgXbr;o&7$s~9EY~S5eBjg0;xhhuIxc{ccj)%W=i}nUbQc#CKw*x@yIKOMO$}%; zJh6d$1`g6d4|s0{9Snkv!v<^61Ij3Nc(N8he8i9@_t?FPQS`T7giRe1#kx0fWxDD0 zXx_32n)&7?<~yk&y`OW9nv$aF{M9ef34%F5*)W#oWB_x3!({E|MTruVDT3HK+cwUvn-;96>5v*oet!p4(i zM|fp9HrhypSBB)BBku_BM2?Y9XTQDcLGXL>OBd4A&ixD2Hvl{Hl>Dtb8od4~AnE9`#QAtoW(Sp35dPyf~9p31L6L?_4V zQ`Fw@uNB7@cq9!N#J_XgFkG*G&$w;ReFqp4qmxIqNl1z}7*h-}cwwbveSDPijsLda sl List[int]: + """Convert a sequence of bytes to a list of Python integer""" + return [v for v in data] + + +@unique +class SplitType(IntEnum): + numerical = 0 + categorical = 1 + + +@dataclass +class Node: + # properties + left: int + right: int + parent: int + split_idx: int + split_cond: float + default_left: bool + split_type: SplitType + categories: List[int] + # statistic + base_weight: float + loss_chg: float + sum_hess: float + + +class Tree: + """A tree built by XGBoost.""" + + def __init__(self, tree_id: int, nodes: Sequence[Node]) -> None: + self.tree_id = tree_id + self.nodes = nodes + + def loss_change(self, node_id: int) -> float: + """Loss gain of a node.""" + return self.nodes[node_id].loss_chg + + def sum_hessian(self, node_id: int) -> float: + """Sum Hessian of a node.""" + return self.nodes[node_id].sum_hess + + def base_weight(self, node_id: int) -> float: + """Base weight of a node.""" + return self.nodes[node_id].base_weight + + def split_index(self, node_id: int) -> int: + """Split feature index of node.""" + return self.nodes[node_id].split_idx + + def split_condition(self, node_id: int) -> float: + """Split value of a node.""" + return self.nodes[node_id].split_cond + + def split_categories(self, node_id: int) -> List[int]: + """Categories in a node.""" + return self.nodes[node_id].categories + + def is_categorical(self, node_id: int) -> bool: + """Whether a node has categorical split.""" + return self.nodes[node_id].split_type == SplitType.categorical + + def is_numerical(self, node_id: int) -> bool: + return not self.is_categorical(node_id) + + def parent(self, node_id: int) -> int: + """Parent ID of a node.""" + return self.nodes[node_id].parent + + def left_child(self, node_id: int) -> int: + """Left child ID of a node.""" + return self.nodes[node_id].left + + def right_child(self, node_id: int) -> int: + """Right child ID of a node.""" + return self.nodes[node_id].right + + def is_leaf(self, node_id: int) -> bool: + """Whether a node is leaf.""" + return self.nodes[node_id].left == -1 + + def is_deleted(self, node_id: int) -> bool: + """Whether a node is deleted.""" + return self.split_index(node_id) == np.iinfo(np.uint32).max + + def __str__(self) -> str: + stack = [0] + nodes = [] + while stack: + node: Dict[str, Union[float, int, List[int]]] = {} + nid = stack.pop() + + node["node id"] = nid + node["gain"] = self.loss_change(nid) + node["cover"] = self.sum_hessian(nid) + nodes.append(node) + + if not self.is_leaf(nid) and not self.is_deleted(nid): + left = self.left_child(nid) + right = self.right_child(nid) + stack.append(left) + stack.append(right) + categories = self.split_categories(nid) + if categories: + assert self.is_categorical(nid) + node["categories"] = categories + else: + assert self.is_numerical(nid) + node["condition"] = self.split_condition(nid) + if self.is_leaf(nid): + node["weight"] = self.split_condition(nid) + + string = "\n".join(map(lambda x: " " + str(x), nodes)) + return string + + +class Model: + """Gradient boosted tree model.""" + + def __init__(self, model: dict) -> None: + """Construct the Model from a JSON object. + + parameters + ---------- + model : A dictionary loaded by json representing a XGBoost boosted tree model. + """ + # Basic properties of a model + self.learner_model_shape: ParamT = model["learner"]["learner_model_param"] + self.num_output_group = int(self.learner_model_shape["num_class"]) + self.num_feature = int(self.learner_model_shape["num_feature"]) + self.base_score: List[float] = json.loads( + self.learner_model_shape["base_score"] + ) + # A field encoding which output group a tree belongs + self.tree_info = model["learner"]["gradient_booster"]["model"]["tree_info"] + + model_shape: ParamT = model["learner"]["gradient_booster"]["model"][ + "gbtree_model_param" + ] + + # JSON representation of trees + j_trees = model["learner"]["gradient_booster"]["model"]["trees"] + + # Load the trees + self.num_trees = int(model_shape["num_trees"]) + + trees: List[Tree] = [] + for i in range(self.num_trees): + tree: Dict[str, Any] = j_trees[i] + tree_id = int(tree["id"]) + assert tree_id == i, (tree_id, i) + # - properties + left_children: List[int] = tree["left_children"] + right_children: List[int] = tree["right_children"] + parents: List[int] = tree["parents"] + split_conditions: List[float] = tree["split_conditions"] + split_indices: List[int] = tree["split_indices"] + # when ubjson is used, this is a byte array with each element as uint8 + default_left = to_integers(tree["default_left"]) + + # - categorical features + # when ubjson is used, this is a byte array with each element as uint8 + split_types = to_integers(tree["split_type"]) + # categories for each node is stored in a CSR style storage with segment as + # the begin ptr and the `categories' as values. + cat_segments: List[int] = tree["categories_segments"] + cat_sizes: List[int] = tree["categories_sizes"] + # node index for categorical nodes + cat_nodes: List[int] = tree["categories_nodes"] + assert len(cat_segments) == len(cat_sizes) == len(cat_nodes) + cats = tree["categories"] + assert len(left_children) == len(split_types) + + # The storage for categories is only defined for categorical nodes to + # prevent unnecessary overhead for numerical splits, we track the + # categorical node that are processed using a counter. + cat_cnt = 0 + if cat_nodes: + last_cat_node = cat_nodes[cat_cnt] + else: + last_cat_node = -1 + node_categories: List[List[int]] = [] + for node_id in range(len(left_children)): + if node_id == last_cat_node: + beg = cat_segments[cat_cnt] + size = cat_sizes[cat_cnt] + end = beg + size + node_cats = cats[beg:end] + # categories are unique for each node + assert len(set(node_cats)) == len(node_cats) + cat_cnt += 1 + if cat_cnt == len(cat_nodes): + last_cat_node = -1 # continue to process the rest of the nodes + else: + last_cat_node = cat_nodes[cat_cnt] + assert node_cats + node_categories.append(node_cats) + else: + # append an empty node, it's either a numerical node or a leaf. + node_categories.append([]) + + # - stats + base_weights: List[float] = tree["base_weights"] + loss_changes: List[float] = tree["loss_changes"] + sum_hessian: List[float] = tree["sum_hessian"] + + # Construct a list of nodes that have complete information + nodes: List[Node] = [ + Node( + left_children[node_id], + right_children[node_id], + parents[node_id], + split_indices[node_id], + split_conditions[node_id], + default_left[node_id] == 1, # to boolean + SplitType(split_types[node_id]), + node_categories[node_id], + base_weights[node_id], + loss_changes[node_id], + sum_hessian[node_id], + ) + for node_id in range(len(left_children)) + ] + + pytree = Tree(tree_id, nodes) + trees.append(pytree) + + self.trees = trees + + def print_model(self) -> None: + for i, tree in enumerate(self.trees): + print("\ntree_id:", i) + print(tree) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Demonstration for loading XGBoost JSON/UBJSON model." + ) + parser.add_argument( + "--model", type=str, required=True, help="Path to .json/.ubj model file." + ) + args = parser.parse_args() + if args.model.endswith("json"): + # use json format + with open(args.model, "r") as fd: + model = json.load(fd) + elif args.model.endswith("ubj"): + if ubjson is None: + raise ImportError("ubjson is not installed.") + # use ubjson format + with open(args.model, "rb") as bfd: + model = ubjson.load(bfd) + else: + raise ValueError( + "Unexpected file extension. Supported file extension are json and ubj." + ) + model = Model(model) + model.print_model() diff --git a/model-integration/src/test/models/xgboost/ubj-to-json.sh b/model-integration/src/test/models/xgboost/ubj-to-json.sh new file mode 100755 index 000000000000..12571b88d335 --- /dev/null +++ b/model-integration/src/test/models/xgboost/ubj-to-json.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# Convert XGBoost UBJ file to JSON format +# Usage: ./ubj-to-json.sh [output.json] + +if [ $# -lt 1 ]; then + echo "Usage: $0 [output.json]" + exit 1 +fi + +INPUT="$1" +OUTPUT="${2:-}" + +if [ -n "$OUTPUT" ]; then + mvn exec:java -Dexec.mainClass="ai.vespa.rankingexpression.importer.xgboost.UbjToJson" \ + -Dexec.args="$INPUT" -Dexec.classpathScope=test -q > "$OUTPUT" + echo "Converted $INPUT to $OUTPUT" +else + mvn exec:java -Dexec.mainClass="ai.vespa.rankingexpression.importer.xgboost.UbjToJson" \ + -Dexec.args="$INPUT" -Dexec.classpathScope=test -q +fi From 79d595bff34a03d8140cf6c37085622c06c486e5 Mon Sep 17 00:00:00 2001 From: Arne Juul Date: Thu, 11 Dec 2025 10:44:21 +0000 Subject: [PATCH 2/9] Allow ubjson dependency in Maven enforcer configurations Add the ubjson library (com.dev-smart:ubjson) to the allowed dependencies lists across all Maven enforcer configurations. This is required for the XGBoost UBJ format import feature added in the previous commit. --- cloud-tenant-base-dependencies-enforcer/pom.xml | 1 + config-model-fat/pom.xml | 1 + container-dependencies-enforcer/pom.xml | 1 + vespa-dependencies-enforcer/allowed-maven-dependencies.txt | 1 + 4 files changed, 4 insertions(+) diff --git a/cloud-tenant-base-dependencies-enforcer/pom.xml b/cloud-tenant-base-dependencies-enforcer/pom.xml index 0a3ec96e67b4..25e60073c708 100644 --- a/cloud-tenant-base-dependencies-enforcer/pom.xml +++ b/cloud-tenant-base-dependencies-enforcer/pom.xml @@ -43,6 +43,7 @@ aopalliance:aopalliance:${aopalliance.vespa.version}:provided + com.dev-smart:ubjson:jar:*:* com.fasterxml.jackson.core:jackson-annotations:${jackson2.vespa.version}:provided com.fasterxml.jackson.core:jackson-core:${jackson2.vespa.version}:provided com.fasterxml.jackson.core:jackson-databind:${jackson-databind.vespa.version}:provided diff --git a/config-model-fat/pom.xml b/config-model-fat/pom.xml index 2e883a0c1e35..3a06630e032f 100644 --- a/config-model-fat/pom.xml +++ b/config-model-fat/pom.xml @@ -186,6 +186,7 @@ aopalliance:aopalliance:*:* + com.dev-smart:ubjson:*:* com.google.errorprone:error_prone_annotations:*:* com.google.guava:failureaccess:*:* com.google.guava:guava:*:* diff --git a/container-dependencies-enforcer/pom.xml b/container-dependencies-enforcer/pom.xml index 896a3d6a1a75..66f6d5b52f46 100644 --- a/container-dependencies-enforcer/pom.xml +++ b/container-dependencies-enforcer/pom.xml @@ -62,6 +62,7 @@ aopalliance:aopalliance:${aopalliance.vespa.version}:provided + com.dev-smart:ubjson:jar:*:* com.fasterxml.jackson.core:jackson-annotations:${jackson2.vespa.version}:provided com.fasterxml.jackson.core:jackson-core:${jackson2.vespa.version}:provided com.fasterxml.jackson.core:jackson-databind:${jackson-databind.vespa.version}:provided diff --git a/vespa-dependencies-enforcer/allowed-maven-dependencies.txt b/vespa-dependencies-enforcer/allowed-maven-dependencies.txt index 2a116b45f065..7110a722bce1 100644 --- a/vespa-dependencies-enforcer/allowed-maven-dependencies.txt +++ b/vespa-dependencies-enforcer/allowed-maven-dependencies.txt @@ -6,6 +6,7 @@ aopalliance:aopalliance:${aopalliance.vespa.version} backport-util-concurrent:backport-util-concurrent:3.1 classworlds:classworlds:1.1-alpha-2 com.auth0:java-jwt:${java-jwt.vespa.version} +com.dev-smart:ubjson:0.1.8 com.ethlo.time:itu:1.10.3 com.fasterxml.jackson.core:jackson-annotations:${jackson2.vespa.version} com.fasterxml.jackson.core:jackson-core:${jackson2.vespa.version} From 964646a91f92a305aaf4f9f5a0bbd44d71645d83 Mon Sep 17 00:00:00 2001 From: Arne Juul Date: Thu, 11 Dec 2025 12:28:30 +0000 Subject: [PATCH 3/9] Improve XGBoost UBJ import Add a probe method to validate UBJ file structure before parsing, and precompute the base_score logit transformation instead of generating it as a runtime expression string. --- .../importer/xgboost/XGBoostImporter.java | 3 +- .../importer/xgboost/XGBoostUbjParser.java | 72 ++++++++++++++++++- .../xgboost/XGBoostImportTestCase.java | 2 +- 3 files changed, 72 insertions(+), 5 deletions(-) diff --git a/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostImporter.java b/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostImporter.java index 4d659e74120b..87cc2478835b 100644 --- a/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostImporter.java +++ b/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostImporter.java @@ -26,8 +26,7 @@ public boolean canImport(String modelPath) { if ( ! modelFile.isFile()) return false; if (modelFile.toString().endsWith(".ubj")) { - // for now - return true; + return XGBoostUbjParser.probe(modelPath); } return modelFile.toString().endsWith(".json") && probe(modelFile); } diff --git a/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostUbjParser.java b/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostUbjParser.java index c9f46fe90e21..f57114b822b5 100644 --- a/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostUbjParser.java +++ b/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostUbjParser.java @@ -22,6 +22,73 @@ class XGBoostUbjParser extends AbstractXGBoostParser { private final List xgboostTrees; private final double baseScore; + /** + * Probes a file to check if it looks like an XGBoost UBJ model. + * This performs minimal parsing to validate the structure. + * + * @param filePath Path to the file to probe. + * @return true if the file appears to be an XGBoost UBJ model. + */ + static boolean probe(String filePath) { + try (FileInputStream fileStream = new FileInputStream(filePath); + UBReader reader = new UBReader(fileStream)) { + UBValue root = reader.read(); + + // Check if it's an array (simple format) + if (root.isArray()) { + UBArray array = root.asArray(); + // Should have at least one tree + if (array.size() == 0) return false; + // First element should be an object with tree structure + if (!array.get(0).isObject()) return false; + UBObject firstTree = array.get(0).asObject(); + return hasTreeStructure(firstTree); + } + + // Check if it's an object (full format with learner) + if (root.isObject()) { + UBObject rootObj = root.asObject(); + UBValue learnerValue = rootObj.get("learner"); + if (learnerValue == null || !learnerValue.isObject()) return false; + + UBObject learner = learnerValue.asObject(); + UBValue gradientBoosterValue = learner.get("gradient_booster"); + if (gradientBoosterValue == null || !gradientBoosterValue.isObject()) return false; + + UBObject gradientBooster = gradientBoosterValue.asObject(); + UBValue modelValue = gradientBooster.get("model"); + if (modelValue == null || !modelValue.isObject()) return false; + + UBObject model = modelValue.asObject(); + UBValue treesValue = model.get("trees"); + if (treesValue == null || !treesValue.isArray()) return false; + + // Looks like a valid XGBoost model structure + return true; + } + + return false; + } catch (IOException | RuntimeException e) { + // Any error during probing means it's not a valid XGBoost UBJ file + return false; + } + } + + /** + * Checks if a UBObject has the expected XGBoost tree structure. + * + * @param treeObj Object to check. + * @return true if it has expected tree arrays. + */ + private static boolean hasTreeStructure(UBObject treeObj) { + // Check for required tree arrays + return treeObj.get("left_children") != null && + treeObj.get("right_children") != null && + treeObj.get("split_conditions") != null && + treeObj.get("split_indices") != null && + treeObj.get("base_weights") != null; + } + /** * Constructor stores parsed UBJ trees. * @@ -80,9 +147,10 @@ String toRankingExpression() { result.append(treeToRankExp(xgboostTrees.get(i))); } - // Add base_score logit transformation + // Add precomputed base_score logit transformation + double baseScoreLogit = Math.log(baseScore) - Math.log(1.0 - baseScore); result.append(" + \n"); - result.append("log(").append(baseScore).append(") - log(").append(1.0 - baseScore).append(")"); + result.append(baseScoreLogit); return result.toString(); } diff --git a/model-integration/src/test/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostImportTestCase.java b/model-integration/src/test/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostImportTestCase.java index 5edb73e3019d..ce170bfbc027 100644 --- a/model-integration/src/test/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostImportTestCase.java +++ b/model-integration/src/test/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostImportTestCase.java @@ -47,7 +47,7 @@ public void testXGBoostUBJ() { // UBJ should include the base_score logit transformation assertTrue("UBJ expression should contain base_score adjustment", - ubjExprStr.contains("log(0.6274165)")); + ubjExprStr.contains(" 0.52114942")); // UBJ expression should start with the same tree expressions as JSON assertTrue("UBJ should contain tree expressions", From b9ab62e6b31859328cfd459adf6c9275f156b5e8 Mon Sep 17 00:00:00 2001 From: Arne Juul Date: Fri, 12 Dec 2025 11:37:13 +0000 Subject: [PATCH 4/9] Let XGBoost parser extract feature names Separates feature indices from feature name formatting to enable flexible feature naming in ranking expressions. This allows models to use meaningful feature names (e.g., "mean_radius") instead of generic indexed names, improving readability of generated ranking expressions. --- .../xgboost/AbstractXGBoostParser.java | 23 +- .../importer/xgboost/XGBoostUbjParser.java | 150 +++++++- .../xgboost/XGBoostImportTestCase.java | 67 +++- .../models/xgboost/binary_breast_cancer.json | 322 +++++++++--------- 4 files changed, 397 insertions(+), 165 deletions(-) diff --git a/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/AbstractXGBoostParser.java b/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/AbstractXGBoostParser.java index 3c79d1464809..5bdf69d87fef 100644 --- a/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/AbstractXGBoostParser.java +++ b/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/AbstractXGBoostParser.java @@ -33,15 +33,34 @@ protected String treeToRankExp(XGBoostTree node) { float xgbSplitPoint = (float)node.getSplit_condition(); // but Vespa expects rank profile literals in double precision: double vespaSplitPoint = xgbSplitPoint; + String formattedSplit = formatSplit(node.getSplit()); String condition; if (node.getMissing() == node.getYes()) { // Note: this is for handling missing features, as the backend handles comparison with NaN as false. - condition = "!(" + node.getSplit() + " >= " + vespaSplitPoint + ")"; + condition = "!(" + formattedSplit + " >= " + vespaSplitPoint + ")"; } else { - condition = node.getSplit() + " < " + vespaSplitPoint; + condition = formattedSplit + " < " + vespaSplitPoint; } return "if (" + condition + ", " + trueExp + ", " + falseExp + ")"; } } + /** + * Formats a split field value for use in ranking expressions. + * If the split is a plain integer, wraps it with xgboost_input_X format. + * Otherwise, uses the split value as-is (for backward compatibility with JSON format). + * + * @param split The split field value from the tree node + * @return Formatted split expression for use in conditions + */ + protected String formatSplit(String split) { + try { + Integer.parseInt(split); + return "xgboost_input_" + split; + } catch (NumberFormatException e) { + // Not a plain integer, use as-is (JSON format already has full attribute name) + return split; + } + } + } diff --git a/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostUbjParser.java b/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostUbjParser.java index f57114b822b5..3ba7c4b24095 100644 --- a/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostUbjParser.java +++ b/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostUbjParser.java @@ -10,6 +10,7 @@ import java.io.IOException; import java.lang.reflect.Field; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -21,6 +22,7 @@ class XGBoostUbjParser extends AbstractXGBoostParser { private final List xgboostTrees; private final double baseScore; + private final List featureNames; /** * Probes a file to check if it looks like an XGBoost UBJ model. @@ -98,6 +100,7 @@ private static boolean hasTreeStructure(UBObject treeObj) { XGBoostUbjParser(String filePath) throws IOException { this.xgboostTrees = new ArrayList<>(); double tmpBaseScore = 0.5; // default value + List tmpFeatureNames = new ArrayList<>(); try (FileInputStream fileStream = new FileInputStream(filePath); UBReader reader = new UBReader(fileStream)) { UBValue root = reader.read(); @@ -113,6 +116,15 @@ private static boolean hasTreeStructure(UBObject treeObj) { // Extract base_score if available tmpBaseScore = extractBaseScore(learner); + // Extract feature_names if available + UBValue featureNamesValue = learner.get("feature_names"); + if (featureNamesValue != null && featureNamesValue.isArray()) { + UBArray featureNamesArray = featureNamesValue.asArray(); + for (int i = 0; i < featureNamesArray.size(); i++) { + tmpFeatureNames.add(featureNamesArray.get(i).asString()); + } + } + // Navigate to trees array forestArray = navigateToTreesArray(learner); } else { @@ -129,6 +141,7 @@ private static boolean hasTreeStructure(UBObject treeObj) { } } this.baseScore = tmpBaseScore; + this.featureNames = Collections.unmodifiableList(tmpFeatureNames); } /** @@ -155,6 +168,141 @@ String toRankingExpression() { return result.toString(); } + /** + * Converts parsed UBJ trees to Vespa ranking expressions using provided feature names. + * + * @param customFeatureNames List of feature names to map indices to actual names. + * Must contain enough names to cover all feature indices used. + * @return Vespa ranking expressions with named features. + * @throws IllegalArgumentException if customFeatureNames is insufficient for the indices used + */ + String toRankingExpression(List customFeatureNames) { + // Validate that we have enough feature names + validateFeatureNames(customFeatureNames); + + StringBuilder result = new StringBuilder(); + + for (int i = 0; i < xgboostTrees.size(); i++) { + if (i > 0) { + result.append(" + \n"); + } + result.append(treeToRankExpWithFeatureNames(xgboostTrees.get(i), customFeatureNames)); + } + + // Add precomputed base_score logit transformation + double baseScoreLogit = Math.log(baseScore) - Math.log(1.0 - baseScore); + result.append(" + \n"); + result.append(baseScoreLogit); + + return result.toString(); + } + + /** + * Validates that the provided feature names list has exactly the required size for the model. + * + * @param customFeatureNames List of feature names to validate + * @throws IllegalArgumentException if validation fails + */ + private void validateFeatureNames(List customFeatureNames) { + if (customFeatureNames == null || customFeatureNames.isEmpty()) { + throw new IllegalArgumentException("Feature names list cannot be null or empty"); + } + + // Find max feature index used in all trees + int maxIndex = findMaxFeatureIndex(); + int requiredSize = maxIndex + 1; + + if (customFeatureNames.size() != requiredSize) { + throw new IllegalArgumentException( + "Feature names list size mismatch: model requires exactly " + requiredSize + + " feature names (indices 0-" + maxIndex + ") but " + + customFeatureNames.size() + " names provided" + ); + } + } + + /** + * Finds the maximum feature index used across all trees. + * + * @return Maximum feature index, or -1 if no features are used + */ + private int findMaxFeatureIndex() { + int max = -1; + for (XGBoostTree tree : xgboostTrees) { + max = Math.max(max, findMaxFeatureIndexInTree(tree)); + } + return max; + } + + /** + * Recursively finds the maximum feature index in a tree. + * + * @param node Tree node to search + * @return Maximum feature index in this tree, or -1 if node is a leaf + */ + private int findMaxFeatureIndexInTree(XGBoostTree node) { + if (node.isLeaf()) { + return -1; // Leaf node + } + + int currentIndex = -1; + try { + currentIndex = Integer.parseInt(node.getSplit()); + } catch (NumberFormatException e) { + // Split is not a number, skip + } + + int childMax = -1; + if (node.getChildren() != null) { + for (XGBoostTree child : node.getChildren()) { + childMax = Math.max(childMax, findMaxFeatureIndexInTree(child)); + } + } + + return Math.max(currentIndex, childMax); + } + + /** + * Converts a tree to ranking expression using custom feature names. + * + * @param node Tree node to convert + * @param customFeatureNames List of feature names for index lookup + * @return Ranking expression string + */ + private String treeToRankExpWithFeatureNames(XGBoostTree node, List customFeatureNames) { + if (node.isLeaf()) { + return Double.toString(node.getLeaf()); + } + + assert node.getChildren().size() == 2; + String trueExp; + String falseExp; + if (node.getYes() == node.getChildren().get(0).getNodeid()) { + trueExp = treeToRankExpWithFeatureNames(node.getChildren().get(0), customFeatureNames); + falseExp = treeToRankExpWithFeatureNames(node.getChildren().get(1), customFeatureNames); + } else { + trueExp = treeToRankExpWithFeatureNames(node.getChildren().get(1), customFeatureNames); + falseExp = treeToRankExpWithFeatureNames(node.getChildren().get(0), customFeatureNames); + } + + int featureIdx = Integer.parseInt(node.getSplit()); + String featureName = customFeatureNames.get(featureIdx); + + // Use the actual feature name instead of indexed format + // Apply the same float rounding as in treeToRankExp + float xgbSplitPoint = (float)node.getSplit_condition(); + double vespaSplitPoint = xgbSplitPoint; + + String condition; + if (node.getMissing() == node.getYes()) { + condition = "!(" + featureName + " >= " + vespaSplitPoint + ")"; + } else { + condition = featureName + " < " + vespaSplitPoint; + } + + return "if (" + condition + ", " + trueExp + ", " + falseExp + ")"; + } + /** * Extracts a required UBObject from a parent object. * @@ -282,7 +430,7 @@ private static XGBoostTree buildTreeFromArrays(int nodeId, int depth, int[] left } else { // Split node: set split information int featureIdx = splitIndices[nodeId]; - setField(node, "split", "attribute(features," + featureIdx + ")"); + setField(node, "split", String.valueOf(featureIdx)); // Apply float rounding to match XGBoost's internal precision (same as XGBoostParser) double splitValue = splitConditions[nodeId]; setField(node, "split_condition", splitValue); diff --git a/model-integration/src/test/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostImportTestCase.java b/model-integration/src/test/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostImportTestCase.java index ce170bfbc027..c393da56804a 100644 --- a/model-integration/src/test/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostImportTestCase.java +++ b/model-integration/src/test/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostImportTestCase.java @@ -5,9 +5,15 @@ import ai.vespa.rankingexpression.importer.ImportedModel; import org.junit.Test; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertFalse; /** * @author bratseth @@ -49,9 +55,68 @@ public void testXGBoostUBJ() { assertTrue("UBJ expression should contain base_score adjustment", ubjExprStr.contains(" 0.52114942")); + // Both formats should use xgboost_input_X format + assertTrue("UBJ should use xgboost_input_ format", + ubjExprStr.contains("xgboost_input_")); + assertTrue("JSON should use xgboost_input_ format", + jsonExprStr.contains("xgboost_input_")); + // UBJ expression should start with the same tree expressions as JSON - assertTrue("UBJ should contain tree expressions", + assertTrue("UBJ should contain tree expressions matching JSON", ubjExprStr.startsWith(jsonExprStr)); } + @Test + public void testXGBoostUBJWithFeatureNames() throws IOException { + XGBoostUbjParser parser = new XGBoostUbjParser("src/test/models/xgboost/binary_breast_cancer.ubj"); + + // Create feature names list (30 features for breast cancer dataset) + List featureNames = Arrays.asList( + "mean_radius", "mean_texture", "mean_perimeter", "mean_area", + "mean_smoothness", "mean_compactness", "mean_concavity", + "mean_concave_points", "mean_symmetry", "mean_fractal_dimension", + "radius_error", "texture_error", "perimeter_error", "area_error", + "smoothness_error", "compactness_error", "concavity_error", + "concave_points_error", "symmetry_error", "fractal_dimension_error", + "worst_radius", "worst_texture", "worst_perimeter", "worst_area", + "worst_smoothness", "worst_compactness", "worst_concavity", + "worst_concave_points", "worst_symmetry", "worst_fractal_dimension" + ); + + String expression = parser.toRankingExpression(featureNames); + assertNotNull(expression); + assertTrue("Expression should contain custom feature name", expression.contains("mean_radius")); + assertTrue("Expression should contain custom feature name", expression.contains("mean_texture")); + assertFalse("Expression should not contain indexed format", expression.contains("xgboost_input_")); + } + + @Test + public void testXGBoostUBJWithInsufficientFeatureNames() throws IOException { + XGBoostUbjParser parser = new XGBoostUbjParser("src/test/models/xgboost/binary_breast_cancer.ubj"); + + // Only provide 5 feature names when model needs 30 + List featureNames = Arrays.asList("f0", "f1", "f2", "f3", "f4"); + + assertThrows(IllegalArgumentException.class, () -> { + parser.toRankingExpression(featureNames); + }); + } + + @Test + public void testXGBoostUBJWithTooManyFeatureNames() throws IOException { + XGBoostUbjParser parser = new XGBoostUbjParser("src/test/models/xgboost/binary_breast_cancer.ubj"); + + // Provide 35 feature names when model needs exactly 30 + List featureNames = Arrays.asList( + "f0", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", + "f10", "f11", "f12", "f13", "f14", "f15", "f16", "f17", "f18", "f19", + "f20", "f21", "f22", "f23", "f24", "f25", "f26", "f27", "f28", "f29", + "f30", "f31", "f32", "f33", "f34" + ); + + assertThrows(IllegalArgumentException.class, () -> { + parser.toRankingExpression(featureNames); + }); + } + } diff --git a/model-integration/src/test/models/xgboost/binary_breast_cancer.json b/model-integration/src/test/models/xgboost/binary_breast_cancer.json index 4370236681c7..47a4cf498928 100644 --- a/model-integration/src/test/models/xgboost/binary_breast_cancer.json +++ b/model-integration/src/test/models/xgboost/binary_breast_cancer.json @@ -1,343 +1,343 @@ [ - { "nodeid": 0, "depth": 0, "split": "attribute(features,20)", "split_condition": 16.81999969482422, "yes": 1, "no": 2, "missing": 2, "children": [ - { "nodeid": 1, "depth": 1, "split": "attribute(features,27)", "split_condition": 0.13570000231266022, "yes": 3, "no": 4, "missing": 4, "children": [ - { "nodeid": 3, "depth": 2, "split": "attribute(features,10)", "split_condition": 0.6449999809265137, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 0, "depth": 0, "split": "xgboost_input_20", "split_condition": 16.81999969482422, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "xgboost_input_27", "split_condition": 0.13570000231266022, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "split": "xgboost_input_10", "split_condition": 0.6449999809265137, "yes": 7, "no": 8, "missing": 8, "children": [ { "nodeid": 7, "depth": 3, "leaf": 0.4603551924228668 }, { "nodeid": 8, "depth": 3, "leaf": -0.01896175928413868 } ] }, - { "nodeid": 4, "depth": 2, "split": "attribute(features,21)", "split_condition": 25.579999923706055, "yes": 9, "no": 10, "missing": 10, "children": [ - { "nodeid": 9, "depth": 3, "split": "attribute(features,23)", "split_condition": 806.9000244140625, "yes": 15, "no": 16, "missing": 16, "children": [ + { "nodeid": 4, "depth": 2, "split": "xgboost_input_21", "split_condition": 25.579999923706055, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 9, "depth": 3, "split": "xgboost_input_23", "split_condition": 806.9000244140625, "yes": 15, "no": 16, "missing": 16, "children": [ { "nodeid": 15, "depth": 4, "leaf": 0.3054772615432739 }, { "nodeid": 16, "depth": 4, "leaf": -0.15728549659252167 } ] }, - { "nodeid": 10, "depth": 3, "split": "attribute(features,6)", "split_condition": 0.09696999937295914, "yes": 17, "no": 18, "missing": 18, "children": [ + { "nodeid": 10, "depth": 3, "split": "xgboost_input_6", "split_condition": 0.09696999937295914, "yes": 17, "no": 18, "missing": 18, "children": [ { "nodeid": 17, "depth": 4, "leaf": -0.09545934945344925 }, { "nodeid": 18, "depth": 4, "leaf": -0.6689253449440002 } ] } ] } ] }, - { "nodeid": 2, "depth": 1, "split": "attribute(features,21)", "split_condition": 19.59000015258789, "yes": 5, "no": 6, "missing": 6, "children": [ - { "nodeid": 5, "depth": 2, "split": "attribute(features,7)", "split_condition": 0.06254000216722488, "yes": 11, "no": 12, "missing": 12, "children": [ + { "nodeid": 2, "depth": 1, "split": "xgboost_input_21", "split_condition": 19.59000015258789, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 2, "split": "xgboost_input_7", "split_condition": 0.06254000216722488, "yes": 11, "no": 12, "missing": 12, "children": [ { "nodeid": 11, "depth": 3, "leaf": 0.3241020143032074 }, { "nodeid": 12, "depth": 3, "leaf": -0.5246468782424927 } ] }, - { "nodeid": 6, "depth": 2, "split": "attribute(features,26)", "split_condition": 0.1889999955892563, "yes": 13, "no": 14, "missing": 14, "children": [ + { "nodeid": 6, "depth": 2, "split": "xgboost_input_26", "split_condition": 0.1889999955892563, "yes": 13, "no": 14, "missing": 14, "children": [ { "nodeid": 13, "depth": 3, "leaf": -0.15728549659252167 }, { "nodeid": 14, "depth": 3, "leaf": -0.7851951718330383 } ] } ] } ] }, - { "nodeid": 0, "depth": 0, "split": "attribute(features,22)", "split_condition": 106.0, "yes": 1, "no": 2, "missing": 2, "children": [ - { "nodeid": 1, "depth": 1, "split": "attribute(features,27)", "split_condition": 0.15729999542236328, "yes": 3, "no": 4, "missing": 4, "children": [ - { "nodeid": 3, "depth": 2, "split": "attribute(features,27)", "split_condition": 0.13570000231266022, "yes": 7, "no": 8, "missing": 8, "children": [ - { "nodeid": 7, "depth": 3, "split": "attribute(features,13)", "split_condition": 45.189998626708984, "yes": 15, "no": 16, "missing": 16, "children": [ - { "nodeid": 15, "depth": 4, "split": "attribute(features,14)", "split_condition": 0.003289999905973673, "yes": 23, "no": 24, "missing": 24, "children": [ + { "nodeid": 0, "depth": 0, "split": "xgboost_input_22", "split_condition": 106.0, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "xgboost_input_27", "split_condition": 0.15729999542236328, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "split": "xgboost_input_27", "split_condition": 0.13570000231266022, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 7, "depth": 3, "split": "xgboost_input_13", "split_condition": 45.189998626708984, "yes": 15, "no": 16, "missing": 16, "children": [ + { "nodeid": 15, "depth": 4, "split": "xgboost_input_14", "split_condition": 0.003289999905973673, "yes": 23, "no": 24, "missing": 24, "children": [ { "nodeid": 23, "depth": 5, "leaf": 0.11408944427967072 }, { "nodeid": 24, "depth": 5, "leaf": 0.40121492743492126 } ] }, { "nodeid": 16, "depth": 4, "leaf": -0.030774641782045364 } ] }, - { "nodeid": 8, "depth": 3, "split": "attribute(features,1)", "split_condition": 19.219999313354492, "yes": 17, "no": 18, "missing": 18, "children": [ + { "nodeid": 8, "depth": 3, "split": "xgboost_input_1", "split_condition": 19.219999313354492, "yes": 17, "no": 18, "missing": 18, "children": [ { "nodeid": 17, "depth": 4, "leaf": 0.2960207462310791 }, { "nodeid": 18, "depth": 4, "leaf": -0.23078586161136627 } ] } ] }, - { "nodeid": 4, "depth": 2, "split": "attribute(features,22)", "split_condition": 97.66000366210938, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 4, "depth": 2, "split": "xgboost_input_22", "split_condition": 97.66000366210938, "yes": 9, "no": 10, "missing": 10, "children": [ { "nodeid": 9, "depth": 3, "leaf": -0.10795557498931885 }, { "nodeid": 10, "depth": 3, "leaf": -0.3868221342563629 } ] } ] }, - { "nodeid": 2, "depth": 1, "split": "attribute(features,7)", "split_condition": 0.04845999926328659, "yes": 5, "no": 6, "missing": 6, "children": [ - { "nodeid": 5, "depth": 2, "split": "attribute(features,17)", "split_condition": 0.009996999986469746, "yes": 11, "no": 12, "missing": 12, "children": [ - { "nodeid": 11, "depth": 3, "split": "attribute(features,1)", "split_condition": 19.3799991607666, "yes": 19, "no": 20, "missing": 20, "children": [ + { "nodeid": 2, "depth": 1, "split": "xgboost_input_7", "split_condition": 0.04845999926328659, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 2, "split": "xgboost_input_17", "split_condition": 0.009996999986469746, "yes": 11, "no": 12, "missing": 12, "children": [ + { "nodeid": 11, "depth": 3, "split": "xgboost_input_1", "split_condition": 19.3799991607666, "yes": 19, "no": 20, "missing": 20, "children": [ { "nodeid": 19, "depth": 4, "leaf": 0.11794889718294144 }, { "nodeid": 20, "depth": 4, "leaf": -0.42626824975013733 } ] }, { "nodeid": 12, "depth": 3, "leaf": 0.3407819867134094 } ] }, - { "nodeid": 6, "depth": 2, "split": "attribute(features,21)", "split_condition": 20.450000762939453, "yes": 13, "no": 14, "missing": 14, "children": [ - { "nodeid": 13, "depth": 3, "split": "attribute(features,7)", "split_condition": 0.07339999824762344, "yes": 21, "no": 22, "missing": 22, "children": [ + { "nodeid": 6, "depth": 2, "split": "xgboost_input_21", "split_condition": 20.450000762939453, "yes": 13, "no": 14, "missing": 14, "children": [ + { "nodeid": 13, "depth": 3, "split": "xgboost_input_7", "split_condition": 0.07339999824762344, "yes": 21, "no": 22, "missing": 22, "children": [ { "nodeid": 21, "depth": 4, "leaf": 0.25160130858421326 }, { "nodeid": 22, "depth": 4, "leaf": -0.39930129051208496 } ] }, { "nodeid": 14, "depth": 3, "leaf": -0.5225854516029358 } ] } ] } ] }, - { "nodeid": 0, "depth": 0, "split": "attribute(features,7)", "split_condition": 0.04938000068068504, "yes": 1, "no": 2, "missing": 2, "children": [ - { "nodeid": 1, "depth": 1, "split": "attribute(features,20)", "split_condition": 16.81999969482422, "yes": 3, "no": 4, "missing": 4, "children": [ - { "nodeid": 3, "depth": 2, "split": "attribute(features,13)", "split_condition": 43.95000076293945, "yes": 7, "no": 8, "missing": 8, "children": [ - { "nodeid": 7, "depth": 3, "split": "attribute(features,14)", "split_condition": 0.003289999905973673, "yes": 15, "no": 16, "missing": 16, "children": [ + { "nodeid": 0, "depth": 0, "split": "xgboost_input_7", "split_condition": 0.04938000068068504, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "xgboost_input_20", "split_condition": 16.81999969482422, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "split": "xgboost_input_13", "split_condition": 43.95000076293945, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 7, "depth": 3, "split": "xgboost_input_14", "split_condition": 0.003289999905973673, "yes": 15, "no": 16, "missing": 16, "children": [ { "nodeid": 15, "depth": 4, "leaf": 0.0977279469370842 }, - { "nodeid": 16, "depth": 4, "split": "attribute(features,21)", "split_condition": 33.209999084472656, "yes": 21, "no": 22, "missing": 22, "children": [ + { "nodeid": 16, "depth": 4, "split": "xgboost_input_21", "split_condition": 33.209999084472656, "yes": 21, "no": 22, "missing": 22, "children": [ { "nodeid": 21, "depth": 5, "leaf": 0.3699623644351959 }, - { "nodeid": 22, "depth": 5, "split": "attribute(features,1)", "split_condition": 26.989999771118164, "yes": 27, "no": 28, "missing": 28, "children": [ + { "nodeid": 22, "depth": 5, "split": "xgboost_input_1", "split_condition": 26.989999771118164, "yes": 27, "no": 28, "missing": 28, "children": [ { "nodeid": 27, "depth": 6, "leaf": -0.05926831439137459 }, { "nodeid": 28, "depth": 6, "leaf": 0.2650623321533203 } ] } ] } ] }, { "nodeid": 8, "depth": 3, "leaf": -0.064254529774189 } ] }, - { "nodeid": 4, "depth": 2, "split": "attribute(features,1)", "split_condition": 15.729999542236328, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 4, "depth": 2, "split": "xgboost_input_1", "split_condition": 15.729999542236328, "yes": 9, "no": 10, "missing": 10, "children": [ { "nodeid": 9, "depth": 3, "leaf": 0.21864144504070282 }, - { "nodeid": 10, "depth": 3, "split": "attribute(features,17)", "split_condition": 0.009232999756932259, "yes": 17, "no": 18, "missing": 18, "children": [ + { "nodeid": 10, "depth": 3, "split": "xgboost_input_17", "split_condition": 0.009232999756932259, "yes": 17, "no": 18, "missing": 18, "children": [ { "nodeid": 17, "depth": 4, "leaf": -0.35293275117874146 }, { "nodeid": 18, "depth": 4, "leaf": -0.06598913669586182 } ] } ] } ] }, - { "nodeid": 2, "depth": 1, "split": "attribute(features,22)", "split_condition": 101.9000015258789, "yes": 5, "no": 6, "missing": 6, "children": [ - { "nodeid": 5, "depth": 2, "split": "attribute(features,21)", "split_condition": 25.479999542236328, "yes": 11, "no": 12, "missing": 12, "children": [ + { "nodeid": 2, "depth": 1, "split": "xgboost_input_22", "split_condition": 101.9000015258789, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 2, "split": "xgboost_input_21", "split_condition": 25.479999542236328, "yes": 11, "no": 12, "missing": 12, "children": [ { "nodeid": 11, "depth": 3, "leaf": 0.3031226098537445 }, { "nodeid": 12, "depth": 3, "leaf": -0.18023964762687683 } ] }, - { "nodeid": 6, "depth": 2, "split": "attribute(features,26)", "split_condition": 0.21230000257492065, "yes": 13, "no": 14, "missing": 14, "children": [ + { "nodeid": 6, "depth": 2, "split": "xgboost_input_26", "split_condition": 0.21230000257492065, "yes": 13, "no": 14, "missing": 14, "children": [ { "nodeid": 13, "depth": 3, "leaf": 0.14003755152225494 }, - { "nodeid": 14, "depth": 3, "split": "attribute(features,1)", "split_condition": 15.34000015258789, "yes": 19, "no": 20, "missing": 20, "children": [ - { "nodeid": 19, "depth": 4, "split": "attribute(features,6)", "split_condition": 0.14569999277591705, "yes": 23, "no": 24, "missing": 24, "children": [ + { "nodeid": 14, "depth": 3, "split": "xgboost_input_1", "split_condition": 15.34000015258789, "yes": 19, "no": 20, "missing": 20, "children": [ + { "nodeid": 19, "depth": 4, "split": "xgboost_input_6", "split_condition": 0.14569999277591705, "yes": 23, "no": 24, "missing": 24, "children": [ { "nodeid": 23, "depth": 5, "leaf": 0.10976236313581467 }, { "nodeid": 24, "depth": 5, "leaf": -0.2641395330429077 } ] }, - { "nodeid": 20, "depth": 4, "split": "attribute(features,4)", "split_condition": 0.08354999870061874, "yes": 25, "no": 26, "missing": 26, "children": [ + { "nodeid": 20, "depth": 4, "split": "xgboost_input_4", "split_condition": 0.08354999870061874, "yes": 25, "no": 26, "missing": 26, "children": [ { "nodeid": 25, "depth": 5, "leaf": -0.11790292710065842 }, { "nodeid": 26, "depth": 5, "leaf": -0.43853873014450073 } ] } ] } ] } ] } ] }, - { "nodeid": 0, "depth": 0, "split": "attribute(features,22)", "split_condition": 102.5, "yes": 1, "no": 2, "missing": 2, "children": [ - { "nodeid": 1, "depth": 1, "split": "attribute(features,24)", "split_condition": 0.17820000648498535, "yes": 3, "no": 4, "missing": 4, "children": [ - { "nodeid": 3, "depth": 2, "split": "attribute(features,13)", "split_condition": 49.0, "yes": 7, "no": 8, "missing": 8, "children": [ - { "nodeid": 7, "depth": 3, "split": "attribute(features,0)", "split_condition": 14.109999656677246, "yes": 13, "no": 14, "missing": 14, "children": [ - { "nodeid": 13, "depth": 4, "split": "attribute(features,8)", "split_condition": 0.23749999701976776, "yes": 19, "no": 20, "missing": 20, "children": [ + { "nodeid": 0, "depth": 0, "split": "xgboost_input_22", "split_condition": 102.5, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "xgboost_input_24", "split_condition": 0.17820000648498535, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "split": "xgboost_input_13", "split_condition": 49.0, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 7, "depth": 3, "split": "xgboost_input_0", "split_condition": 14.109999656677246, "yes": 13, "no": 14, "missing": 14, "children": [ + { "nodeid": 13, "depth": 4, "split": "xgboost_input_8", "split_condition": 0.23749999701976776, "yes": 19, "no": 20, "missing": 20, "children": [ { "nodeid": 19, "depth": 5, "leaf": 0.3398503065109253 }, { "nodeid": 20, "depth": 5, "leaf": 0.08510195463895798 } ] }, { "nodeid": 14, "depth": 4, "leaf": 0.06697364151477814 } ] }, { "nodeid": 8, "depth": 3, "leaf": -0.04186965152621269 } ] }, { "nodeid": 4, "depth": 2, "leaf": -0.16538292169570923 } ] }, - { "nodeid": 2, "depth": 1, "split": "attribute(features,7)", "split_condition": 0.04845999926328659, "yes": 5, "no": 6, "missing": 6, "children": [ - { "nodeid": 5, "depth": 2, "split": "attribute(features,21)", "split_condition": 26.440000534057617, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 2, "depth": 1, "split": "xgboost_input_7", "split_condition": 0.04845999926328659, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 2, "split": "xgboost_input_21", "split_condition": 26.440000534057617, "yes": 9, "no": 10, "missing": 10, "children": [ { "nodeid": 9, "depth": 3, "leaf": 0.2624058723449707 }, - { "nodeid": 10, "depth": 3, "split": "attribute(features,15)", "split_condition": 0.01631000079214573, "yes": 15, "no": 16, "missing": 16, "children": [ + { "nodeid": 10, "depth": 3, "split": "xgboost_input_15", "split_condition": 0.01631000079214573, "yes": 15, "no": 16, "missing": 16, "children": [ { "nodeid": 15, "depth": 4, "leaf": -0.3455977141857147 }, { "nodeid": 16, "depth": 4, "leaf": 0.20227135717868805 } ] } ] }, - { "nodeid": 6, "depth": 2, "split": "attribute(features,26)", "split_condition": 0.21230000257492065, "yes": 11, "no": 12, "missing": 12, "children": [ + { "nodeid": 6, "depth": 2, "split": "xgboost_input_26", "split_condition": 0.21230000257492065, "yes": 11, "no": 12, "missing": 12, "children": [ { "nodeid": 11, "depth": 3, "leaf": 0.12086576223373413 }, - { "nodeid": 12, "depth": 3, "split": "attribute(features,21)", "split_condition": 18.40999984741211, "yes": 17, "no": 18, "missing": 18, "children": [ + { "nodeid": 12, "depth": 3, "split": "xgboost_input_21", "split_condition": 18.40999984741211, "yes": 17, "no": 18, "missing": 18, "children": [ { "nodeid": 17, "depth": 4, "leaf": -0.054553307592868805 }, - { "nodeid": 18, "depth": 4, "split": "attribute(features,16)", "split_condition": 0.10270000249147415, "yes": 21, "no": 22, "missing": 22, "children": [ + { "nodeid": 18, "depth": 4, "split": "xgboost_input_16", "split_condition": 0.10270000249147415, "yes": 21, "no": 22, "missing": 22, "children": [ { "nodeid": 21, "depth": 5, "leaf": -0.3827503025531769 }, { "nodeid": 22, "depth": 5, "leaf": -0.09866306185722351 } ] } ] } ] } ] } ] }, - { "nodeid": 0, "depth": 0, "split": "attribute(features,23)", "split_condition": 888.2999877929688, "yes": 1, "no": 2, "missing": 2, "children": [ - { "nodeid": 1, "depth": 1, "split": "attribute(features,27)", "split_condition": 0.1606999933719635, "yes": 3, "no": 4, "missing": 4, "children": [ - { "nodeid": 3, "depth": 2, "split": "attribute(features,1)", "split_condition": 21.309999465942383, "yes": 7, "no": 8, "missing": 8, "children": [ - { "nodeid": 7, "depth": 3, "split": "attribute(features,10)", "split_condition": 0.550599992275238, "yes": 11, "no": 12, "missing": 12, "children": [ - { "nodeid": 11, "depth": 4, "split": "attribute(features,23)", "split_condition": 811.2999877929688, "yes": 15, "no": 16, "missing": 16, "children": [ + { "nodeid": 0, "depth": 0, "split": "xgboost_input_23", "split_condition": 888.2999877929688, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "xgboost_input_27", "split_condition": 0.1606999933719635, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "split": "xgboost_input_1", "split_condition": 21.309999465942383, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 7, "depth": 3, "split": "xgboost_input_10", "split_condition": 0.550599992275238, "yes": 11, "no": 12, "missing": 12, "children": [ + { "nodeid": 11, "depth": 4, "split": "xgboost_input_23", "split_condition": 811.2999877929688, "yes": 15, "no": 16, "missing": 16, "children": [ { "nodeid": 15, "depth": 5, "leaf": 0.3330357074737549 }, - { "nodeid": 16, "depth": 5, "split": "attribute(features,15)", "split_condition": 0.01841999962925911, "yes": 19, "no": 20, "missing": 20, "children": [ + { "nodeid": 16, "depth": 5, "split": "xgboost_input_15", "split_condition": 0.01841999962925911, "yes": 19, "no": 20, "missing": 20, "children": [ { "nodeid": 19, "depth": 6, "leaf": 0.226839080452919 }, { "nodeid": 20, "depth": 6, "leaf": -0.04981991648674011 } ] } ] }, { "nodeid": 12, "depth": 4, "leaf": 0.06199278682470322 } ] }, - { "nodeid": 8, "depth": 3, "split": "attribute(features,23)", "split_condition": 648.2999877929688, "yes": 13, "no": 14, "missing": 14, "children": [ + { "nodeid": 8, "depth": 3, "split": "xgboost_input_23", "split_condition": 648.2999877929688, "yes": 13, "no": 14, "missing": 14, "children": [ { "nodeid": 13, "depth": 4, "leaf": 0.26304540038108826 }, - { "nodeid": 14, "depth": 4, "split": "attribute(features,14)", "split_condition": 0.005872000008821487, "yes": 17, "no": 18, "missing": 18, "children": [ + { "nodeid": 14, "depth": 4, "split": "xgboost_input_14", "split_condition": 0.005872000008821487, "yes": 17, "no": 18, "missing": 18, "children": [ { "nodeid": 17, "depth": 5, "leaf": 0.058584075421094894 }, - { "nodeid": 18, "depth": 5, "split": "attribute(features,9)", "split_condition": 0.06066000089049339, "yes": 21, "no": 22, "missing": 22, "children": [ + { "nodeid": 18, "depth": 5, "split": "xgboost_input_9", "split_condition": 0.06066000089049339, "yes": 21, "no": 22, "missing": 22, "children": [ { "nodeid": 21, "depth": 6, "leaf": -0.37050333619117737 }, { "nodeid": 22, "depth": 6, "leaf": -0.07894755154848099 } ] } ] } ] } ] }, { "nodeid": 4, "depth": 2, "leaf": -0.2753813862800598 } ] }, - { "nodeid": 2, "depth": 1, "split": "attribute(features,6)", "split_condition": 0.0729300007224083, "yes": 5, "no": 6, "missing": 6, "children": [ - { "nodeid": 5, "depth": 2, "split": "attribute(features,1)", "split_condition": 19.510000228881836, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 2, "depth": 1, "split": "xgboost_input_6", "split_condition": 0.0729300007224083, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 2, "split": "xgboost_input_1", "split_condition": 19.510000228881836, "yes": 9, "no": 10, "missing": 10, "children": [ { "nodeid": 9, "depth": 3, "leaf": 0.18285000324249268 }, { "nodeid": 10, "depth": 3, "leaf": -0.2675382196903229 } ] }, { "nodeid": 6, "depth": 2, "leaf": -0.3542742133140564 } ] } ] }, - { "nodeid": 0, "depth": 0, "split": "attribute(features,27)", "split_condition": 0.1111999973654747, "yes": 1, "no": 2, "missing": 2, "children": [ - { "nodeid": 1, "depth": 1, "split": "attribute(features,3)", "split_condition": 698.7999877929688, "yes": 3, "no": 4, "missing": 4, "children": [ - { "nodeid": 3, "depth": 2, "split": "attribute(features,28)", "split_condition": 0.1987999975681305, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 0, "depth": 0, "split": "xgboost_input_27", "split_condition": 0.1111999973654747, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "xgboost_input_3", "split_condition": 698.7999877929688, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "split": "xgboost_input_28", "split_condition": 0.1987999975681305, "yes": 7, "no": 8, "missing": 8, "children": [ { "nodeid": 7, "depth": 3, "leaf": 0.0755092054605484 }, - { "nodeid": 8, "depth": 3, "split": "attribute(features,21)", "split_condition": 33.209999084472656, "yes": 15, "no": 16, "missing": 16, "children": [ + { "nodeid": 8, "depth": 3, "split": "xgboost_input_21", "split_condition": 33.209999084472656, "yes": 15, "no": 16, "missing": 16, "children": [ { "nodeid": 15, "depth": 4, "leaf": 0.3183874487876892 }, { "nodeid": 16, "depth": 4, "leaf": 0.11721514165401459 } ] } ] }, - { "nodeid": 4, "depth": 2, "split": "attribute(features,1)", "split_condition": 20.219999313354492, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 4, "depth": 2, "split": "xgboost_input_1", "split_condition": 20.219999313354492, "yes": 9, "no": 10, "missing": 10, "children": [ { "nodeid": 9, "depth": 3, "leaf": 0.08036360889673233 }, { "nodeid": 10, "depth": 3, "leaf": -0.252962589263916 } ] } ] }, - { "nodeid": 2, "depth": 1, "split": "attribute(features,23)", "split_condition": 734.5999755859375, "yes": 5, "no": 6, "missing": 6, "children": [ - { "nodeid": 5, "depth": 2, "split": "attribute(features,27)", "split_condition": 0.17649999260902405, "yes": 11, "no": 12, "missing": 12, "children": [ - { "nodeid": 11, "depth": 3, "split": "attribute(features,1)", "split_condition": 20.25, "yes": 17, "no": 18, "missing": 18, "children": [ + { "nodeid": 2, "depth": 1, "split": "xgboost_input_23", "split_condition": 734.5999755859375, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 2, "split": "xgboost_input_27", "split_condition": 0.17649999260902405, "yes": 11, "no": 12, "missing": 12, "children": [ + { "nodeid": 11, "depth": 3, "split": "xgboost_input_1", "split_condition": 20.25, "yes": 17, "no": 18, "missing": 18, "children": [ { "nodeid": 17, "depth": 4, "leaf": 0.2636157274246216 }, { "nodeid": 18, "depth": 4, "leaf": 0.028473349288105965 } ] }, { "nodeid": 12, "depth": 3, "leaf": -0.21361969411373138 } ] }, - { "nodeid": 6, "depth": 2, "split": "attribute(features,21)", "split_condition": 19.899999618530273, "yes": 13, "no": 14, "missing": 14, "children": [ - { "nodeid": 13, "depth": 3, "split": "attribute(features,13)", "split_condition": 43.400001525878906, "yes": 19, "no": 20, "missing": 20, "children": [ + { "nodeid": 6, "depth": 2, "split": "xgboost_input_21", "split_condition": 19.899999618530273, "yes": 13, "no": 14, "missing": 14, "children": [ + { "nodeid": 13, "depth": 3, "split": "xgboost_input_13", "split_condition": 43.400001525878906, "yes": 19, "no": 20, "missing": 20, "children": [ { "nodeid": 19, "depth": 4, "leaf": 0.22751691937446594 }, { "nodeid": 20, "depth": 4, "leaf": -0.22558310627937317 } ] }, { "nodeid": 14, "depth": 3, "leaf": -0.32582372426986694 } ] } ] } ] }, - { "nodeid": 0, "depth": 0, "split": "attribute(features,7)", "split_condition": 0.04938000068068504, "yes": 1, "no": 2, "missing": 2, "children": [ - { "nodeid": 1, "depth": 1, "split": "attribute(features,3)", "split_condition": 698.7999877929688, "yes": 3, "no": 4, "missing": 4, "children": [ - { "nodeid": 3, "depth": 2, "split": "attribute(features,13)", "split_condition": 41.5099983215332, "yes": 7, "no": 8, "missing": 8, "children": [ - { "nodeid": 7, "depth": 3, "split": "attribute(features,21)", "split_condition": 33.209999084472656, "yes": 15, "no": 16, "missing": 16, "children": [ - { "nodeid": 15, "depth": 4, "split": "attribute(features,19)", "split_condition": 0.0013810000382363796, "yes": 19, "no": 20, "missing": 20, "children": [ + { "nodeid": 0, "depth": 0, "split": "xgboost_input_7", "split_condition": 0.04938000068068504, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "xgboost_input_3", "split_condition": 698.7999877929688, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "split": "xgboost_input_13", "split_condition": 41.5099983215332, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 7, "depth": 3, "split": "xgboost_input_21", "split_condition": 33.209999084472656, "yes": 15, "no": 16, "missing": 16, "children": [ + { "nodeid": 15, "depth": 4, "split": "xgboost_input_19", "split_condition": 0.0013810000382363796, "yes": 19, "no": 20, "missing": 20, "children": [ { "nodeid": 19, "depth": 5, "leaf": 0.03624707832932472 }, { "nodeid": 20, "depth": 5, "leaf": 0.3080938160419464 } ] }, - { "nodeid": 16, "depth": 4, "split": "attribute(features,28)", "split_condition": 0.24799999594688416, "yes": 21, "no": 22, "missing": 22, "children": [ + { "nodeid": 16, "depth": 4, "split": "xgboost_input_28", "split_condition": 0.24799999594688416, "yes": 21, "no": 22, "missing": 22, "children": [ { "nodeid": 21, "depth": 5, "leaf": 0.17582161724567413 }, { "nodeid": 22, "depth": 5, "leaf": -0.09668976068496704 } ] } ] }, { "nodeid": 8, "depth": 3, "leaf": -0.03945023939013481 } ] }, - { "nodeid": 4, "depth": 2, "split": "attribute(features,1)", "split_condition": 19.510000228881836, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 4, "depth": 2, "split": "xgboost_input_1", "split_condition": 19.510000228881836, "yes": 9, "no": 10, "missing": 10, "children": [ { "nodeid": 9, "depth": 3, "leaf": 0.045127417892217636 }, { "nodeid": 10, "depth": 3, "leaf": -0.2194928079843521 } ] } ] }, - { "nodeid": 2, "depth": 1, "split": "attribute(features,21)", "split_condition": 23.75, "yes": 5, "no": 6, "missing": 6, "children": [ - { "nodeid": 5, "depth": 2, "split": "attribute(features,23)", "split_condition": 809.7000122070312, "yes": 11, "no": 12, "missing": 12, "children": [ + { "nodeid": 2, "depth": 1, "split": "xgboost_input_21", "split_condition": 23.75, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 2, "split": "xgboost_input_23", "split_condition": 809.7000122070312, "yes": 11, "no": 12, "missing": 12, "children": [ { "nodeid": 11, "depth": 3, "leaf": 0.24866445362567902 }, - { "nodeid": 12, "depth": 3, "split": "attribute(features,15)", "split_condition": 0.020749999210238457, "yes": 17, "no": 18, "missing": 18, "children": [ + { "nodeid": 12, "depth": 3, "split": "xgboost_input_15", "split_condition": 0.020749999210238457, "yes": 17, "no": 18, "missing": 18, "children": [ { "nodeid": 17, "depth": 4, "leaf": -0.005317678675055504 }, { "nodeid": 18, "depth": 4, "leaf": -0.24861639738082886 } ] } ] }, - { "nodeid": 6, "depth": 2, "split": "attribute(features,23)", "split_condition": 680.5999755859375, "yes": 13, "no": 14, "missing": 14, "children": [ + { "nodeid": 6, "depth": 2, "split": "xgboost_input_23", "split_condition": 680.5999755859375, "yes": 13, "no": 14, "missing": 14, "children": [ { "nodeid": 13, "depth": 3, "leaf": -0.06991633772850037 }, { "nodeid": 14, "depth": 3, "leaf": -0.31441980600357056 } ] } ] } ] }, - { "nodeid": 0, "depth": 0, "split": "attribute(features,27)", "split_condition": 0.1111999973654747, "yes": 1, "no": 2, "missing": 2, "children": [ - { "nodeid": 1, "depth": 1, "split": "attribute(features,3)", "split_condition": 698.7999877929688, "yes": 3, "no": 4, "missing": 4, "children": [ - { "nodeid": 3, "depth": 2, "split": "attribute(features,28)", "split_condition": 0.1987999975681305, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 0, "depth": 0, "split": "xgboost_input_27", "split_condition": 0.1111999973654747, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "xgboost_input_3", "split_condition": 698.7999877929688, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "split": "xgboost_input_28", "split_condition": 0.1987999975681305, "yes": 7, "no": 8, "missing": 8, "children": [ { "nodeid": 7, "depth": 3, "leaf": 0.03794674202799797 }, - { "nodeid": 8, "depth": 3, "split": "attribute(features,21)", "split_condition": 33.209999084472656, "yes": 13, "no": 14, "missing": 14, "children": [ + { "nodeid": 8, "depth": 3, "split": "xgboost_input_21", "split_condition": 33.209999084472656, "yes": 13, "no": 14, "missing": 14, "children": [ { "nodeid": 13, "depth": 4, "leaf": 0.29745399951934814 }, { "nodeid": 14, "depth": 4, "leaf": 0.09482062608003616 } ] } ] }, - { "nodeid": 4, "depth": 2, "split": "attribute(features,1)", "split_condition": 20.219999313354492, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 4, "depth": 2, "split": "xgboost_input_1", "split_condition": 20.219999313354492, "yes": 9, "no": 10, "missing": 10, "children": [ { "nodeid": 9, "depth": 3, "leaf": 0.06832267343997955 }, { "nodeid": 10, "depth": 3, "leaf": -0.20128701627254486 } ] } ] }, - { "nodeid": 2, "depth": 1, "split": "attribute(features,22)", "split_condition": 116.19999694824219, "yes": 5, "no": 6, "missing": 6, "children": [ - { "nodeid": 5, "depth": 2, "split": "attribute(features,21)", "split_condition": 27.489999771118164, "yes": 11, "no": 12, "missing": 12, "children": [ - { "nodeid": 11, "depth": 3, "split": "attribute(features,27)", "split_condition": 0.1606999933719635, "yes": 15, "no": 16, "missing": 16, "children": [ + { "nodeid": 2, "depth": 1, "split": "xgboost_input_22", "split_condition": 116.19999694824219, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 2, "split": "xgboost_input_21", "split_condition": 27.489999771118164, "yes": 11, "no": 12, "missing": 12, "children": [ + { "nodeid": 11, "depth": 3, "split": "xgboost_input_27", "split_condition": 0.1606999933719635, "yes": 15, "no": 16, "missing": 16, "children": [ { "nodeid": 15, "depth": 4, "leaf": 0.21199870109558105 }, { "nodeid": 16, "depth": 4, "leaf": -0.09946209192276001 } ] }, - { "nodeid": 12, "depth": 3, "split": "attribute(features,23)", "split_condition": 699.4000244140625, "yes": 17, "no": 18, "missing": 18, "children": [ + { "nodeid": 12, "depth": 3, "split": "xgboost_input_23", "split_condition": 699.4000244140625, "yes": 17, "no": 18, "missing": 18, "children": [ { "nodeid": 17, "depth": 4, "leaf": -0.02004398964345455 }, { "nodeid": 18, "depth": 4, "leaf": -0.25623419880867004 } ] } ] }, { "nodeid": 6, "depth": 2, "leaf": -0.30207687616348267 } ] } ] }, - { "nodeid": 0, "depth": 0, "split": "attribute(features,27)", "split_condition": 0.14239999651908875, "yes": 1, "no": 2, "missing": 2, "children": [ - { "nodeid": 1, "depth": 1, "split": "attribute(features,23)", "split_condition": 967.0, "yes": 3, "no": 4, "missing": 4, "children": [ - { "nodeid": 3, "depth": 2, "split": "attribute(features,13)", "split_condition": 35.2400016784668, "yes": 7, "no": 8, "missing": 8, "children": [ - { "nodeid": 7, "depth": 3, "split": "attribute(features,21)", "split_condition": 30.149999618530273, "yes": 13, "no": 14, "missing": 14, "children": [ + { "nodeid": 0, "depth": 0, "split": "xgboost_input_27", "split_condition": 0.14239999651908875, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "xgboost_input_23", "split_condition": 967.0, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "split": "xgboost_input_13", "split_condition": 35.2400016784668, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 7, "depth": 3, "split": "xgboost_input_21", "split_condition": 30.149999618530273, "yes": 13, "no": 14, "missing": 14, "children": [ { "nodeid": 13, "depth": 4, "leaf": 0.2801840007305145 }, - { "nodeid": 14, "depth": 4, "split": "attribute(features,1)", "split_condition": 23.5, "yes": 17, "no": 18, "missing": 18, "children": [ + { "nodeid": 14, "depth": 4, "split": "xgboost_input_1", "split_condition": 23.5, "yes": 17, "no": 18, "missing": 18, "children": [ { "nodeid": 17, "depth": 5, "leaf": -0.1386641263961792 }, { "nodeid": 18, "depth": 5, "leaf": 0.20163708925247192 } ] } ] }, - { "nodeid": 8, "depth": 3, "split": "attribute(features,19)", "split_condition": 0.002767999889329076, "yes": 15, "no": 16, "missing": 16, "children": [ + { "nodeid": 8, "depth": 3, "split": "xgboost_input_19", "split_condition": 0.002767999889329076, "yes": 15, "no": 16, "missing": 16, "children": [ { "nodeid": 15, "depth": 4, "leaf": -0.17439478635787964 }, { "nodeid": 16, "depth": 4, "leaf": 0.12734214961528778 } ] } ] }, - { "nodeid": 4, "depth": 2, "split": "attribute(features,28)", "split_condition": 0.2533000111579895, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 4, "depth": 2, "split": "xgboost_input_28", "split_condition": 0.2533000111579895, "yes": 9, "no": 10, "missing": 10, "children": [ { "nodeid": 9, "depth": 3, "leaf": -0.007179913576692343 }, { "nodeid": 10, "depth": 3, "leaf": -0.20481328666210175 } ] } ] }, - { "nodeid": 2, "depth": 1, "split": "attribute(features,13)", "split_condition": 21.459999084472656, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 2, "depth": 1, "split": "xgboost_input_13", "split_condition": 21.459999084472656, "yes": 5, "no": 6, "missing": 6, "children": [ { "nodeid": 5, "depth": 2, "leaf": -4.309949144953862e-05 }, - { "nodeid": 6, "depth": 2, "split": "attribute(features,4)", "split_condition": 0.08998999744653702, "yes": 11, "no": 12, "missing": 12, "children": [ + { "nodeid": 6, "depth": 2, "split": "xgboost_input_4", "split_condition": 0.08998999744653702, "yes": 11, "no": 12, "missing": 12, "children": [ { "nodeid": 11, "depth": 3, "leaf": -0.06140953674912453 }, { "nodeid": 12, "depth": 3, "leaf": -0.28825199604034424 } ] } ] } ] }, - { "nodeid": 0, "depth": 0, "split": "attribute(features,7)", "split_condition": 0.04938000068068504, "yes": 1, "no": 2, "missing": 2, "children": [ - { "nodeid": 1, "depth": 1, "split": "attribute(features,20)", "split_condition": 16.81999969482422, "yes": 3, "no": 4, "missing": 4, "children": [ - { "nodeid": 3, "depth": 2, "split": "attribute(features,15)", "split_condition": 0.012029999867081642, "yes": 7, "no": 8, "missing": 8, "children": [ - { "nodeid": 7, "depth": 3, "split": "attribute(features,16)", "split_condition": 0.012719999998807907, "yes": 15, "no": 16, "missing": 16, "children": [ + { "nodeid": 0, "depth": 0, "split": "xgboost_input_7", "split_condition": 0.04938000068068504, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "xgboost_input_20", "split_condition": 16.81999969482422, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "split": "xgboost_input_15", "split_condition": 0.012029999867081642, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 7, "depth": 3, "split": "xgboost_input_16", "split_condition": 0.012719999998807907, "yes": 15, "no": 16, "missing": 16, "children": [ { "nodeid": 15, "depth": 4, "leaf": 0.22184161841869354 }, { "nodeid": 16, "depth": 4, "leaf": -0.15230606496334076 } ] }, { "nodeid": 8, "depth": 3, "leaf": 0.27187174558639526 } ] }, - { "nodeid": 4, "depth": 2, "split": "attribute(features,28)", "split_condition": 0.2653999924659729, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 4, "depth": 2, "split": "xgboost_input_28", "split_condition": 0.2653999924659729, "yes": 9, "no": 10, "missing": 10, "children": [ { "nodeid": 9, "depth": 3, "leaf": 0.03678994998335838 }, { "nodeid": 10, "depth": 3, "leaf": -0.13423432409763336 } ] } ] }, - { "nodeid": 2, "depth": 1, "split": "attribute(features,21)", "split_condition": 23.75, "yes": 5, "no": 6, "missing": 6, "children": [ - { "nodeid": 5, "depth": 2, "split": "attribute(features,23)", "split_condition": 809.7000122070312, "yes": 11, "no": 12, "missing": 12, "children": [ + { "nodeid": 2, "depth": 1, "split": "xgboost_input_21", "split_condition": 23.75, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 2, "split": "xgboost_input_23", "split_condition": 809.7000122070312, "yes": 11, "no": 12, "missing": 12, "children": [ { "nodeid": 11, "depth": 3, "leaf": 0.20270322263240814 }, { "nodeid": 12, "depth": 3, "leaf": -0.15306414663791656 } ] }, - { "nodeid": 6, "depth": 2, "split": "attribute(features,6)", "split_condition": 0.09060999751091003, "yes": 13, "no": 14, "missing": 14, "children": [ + { "nodeid": 6, "depth": 2, "split": "xgboost_input_6", "split_condition": 0.09060999751091003, "yes": 13, "no": 14, "missing": 14, "children": [ { "nodeid": 13, "depth": 3, "leaf": -0.05368896201252937 }, { "nodeid": 14, "depth": 3, "leaf": -0.2783971130847931 } ] } ] } ] }, - { "nodeid": 0, "depth": 0, "split": "attribute(features,26)", "split_condition": 0.2079000025987625, "yes": 1, "no": 2, "missing": 2, "children": [ - { "nodeid": 1, "depth": 1, "split": "attribute(features,13)", "split_condition": 40.5099983215332, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 0, "depth": 0, "split": "xgboost_input_26", "split_condition": 0.2079000025987625, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "xgboost_input_13", "split_condition": 40.5099983215332, "yes": 3, "no": 4, "missing": 4, "children": [ { "nodeid": 3, "depth": 2, "leaf": 0.27354902029037476 }, { "nodeid": 4, "depth": 2, "leaf": -0.05269660800695419 } ] }, - { "nodeid": 2, "depth": 1, "split": "attribute(features,23)", "split_condition": 648.2999877929688, "yes": 5, "no": 6, "missing": 6, "children": [ - { "nodeid": 5, "depth": 2, "split": "attribute(features,7)", "split_condition": 0.055959999561309814, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 2, "depth": 1, "split": "xgboost_input_23", "split_condition": 648.2999877929688, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 2, "split": "xgboost_input_7", "split_condition": 0.055959999561309814, "yes": 7, "no": 8, "missing": 8, "children": [ { "nodeid": 7, "depth": 3, "leaf": 0.19431108236312866 }, { "nodeid": 8, "depth": 3, "leaf": -0.042131464928388596 } ] }, - { "nodeid": 6, "depth": 2, "split": "attribute(features,21)", "split_condition": 19.899999618530273, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 6, "depth": 2, "split": "xgboost_input_21", "split_condition": 19.899999618530273, "yes": 9, "no": 10, "missing": 10, "children": [ { "nodeid": 9, "depth": 3, "leaf": 0.04776393994688988 }, - { "nodeid": 10, "depth": 3, "split": "attribute(features,24)", "split_condition": 0.10920000076293945, "yes": 11, "no": 12, "missing": 12, "children": [ + { "nodeid": 10, "depth": 3, "split": "xgboost_input_24", "split_condition": 0.10920000076293945, "yes": 11, "no": 12, "missing": 12, "children": [ { "nodeid": 11, "depth": 4, "leaf": 0.011151635088026524 }, { "nodeid": 12, "depth": 4, "leaf": -0.26751622557640076 } ] } ] } ] } ] }, - { "nodeid": 0, "depth": 0, "split": "attribute(features,23)", "split_condition": 967.0, "yes": 1, "no": 2, "missing": 2, "children": [ - { "nodeid": 1, "depth": 1, "split": "attribute(features,21)", "split_condition": 29.25, "yes": 3, "no": 4, "missing": 4, "children": [ - { "nodeid": 3, "depth": 2, "split": "attribute(features,12)", "split_condition": 3.430000066757202, "yes": 7, "no": 8, "missing": 8, "children": [ - { "nodeid": 7, "depth": 3, "split": "attribute(features,29)", "split_condition": 0.10189999639987946, "yes": 11, "no": 12, "missing": 12, "children": [ + { "nodeid": 0, "depth": 0, "split": "xgboost_input_23", "split_condition": 967.0, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "xgboost_input_21", "split_condition": 29.25, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "split": "xgboost_input_12", "split_condition": 3.430000066757202, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 7, "depth": 3, "split": "xgboost_input_29", "split_condition": 0.10189999639987946, "yes": 11, "no": 12, "missing": 12, "children": [ { "nodeid": 11, "depth": 4, "leaf": 0.25556275248527527 }, { "nodeid": 12, "depth": 4, "leaf": 0.018566781654953957 } ] }, { "nodeid": 8, "depth": 3, "leaf": -0.01612720638513565 } ] }, - { "nodeid": 4, "depth": 2, "split": "attribute(features,27)", "split_condition": 0.09139999747276306, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 4, "depth": 2, "split": "xgboost_input_27", "split_condition": 0.09139999747276306, "yes": 9, "no": 10, "missing": 10, "children": [ { "nodeid": 9, "depth": 3, "leaf": 0.14816634356975555 }, - { "nodeid": 10, "depth": 3, "split": "attribute(features,24)", "split_condition": 0.13410000503063202, "yes": 13, "no": 14, "missing": 14, "children": [ + { "nodeid": 10, "depth": 3, "split": "xgboost_input_24", "split_condition": 0.13410000503063202, "yes": 13, "no": 14, "missing": 14, "children": [ { "nodeid": 13, "depth": 4, "leaf": -0.0205707810819149 }, { "nodeid": 14, "depth": 4, "leaf": -0.2519259452819824 } ] } ] } ] }, - { "nodeid": 2, "depth": 1, "split": "attribute(features,25)", "split_condition": 0.17110000550746918, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 2, "depth": 1, "split": "xgboost_input_25", "split_condition": 0.17110000550746918, "yes": 5, "no": 6, "missing": 6, "children": [ { "nodeid": 5, "depth": 2, "leaf": -0.02155451662838459 }, { "nodeid": 6, "depth": 2, "leaf": -0.2605815827846527 } ] } ] }, - { "nodeid": 0, "depth": 0, "split": "attribute(features,22)", "split_condition": 120.4000015258789, "yes": 1, "no": 2, "missing": 2, "children": [ - { "nodeid": 1, "depth": 1, "split": "attribute(features,21)", "split_condition": 29.25, "yes": 3, "no": 4, "missing": 4, "children": [ - { "nodeid": 3, "depth": 2, "split": "attribute(features,25)", "split_condition": 0.32350000739097595, "yes": 5, "no": 6, "missing": 6, "children": [ - { "nodeid": 5, "depth": 3, "split": "attribute(features,25)", "split_condition": 0.08340000361204147, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 0, "depth": 0, "split": "xgboost_input_22", "split_condition": 120.4000015258789, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "xgboost_input_21", "split_condition": 29.25, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "split": "xgboost_input_25", "split_condition": 0.32350000739097595, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 3, "split": "xgboost_input_25", "split_condition": 0.08340000361204147, "yes": 9, "no": 10, "missing": 10, "children": [ { "nodeid": 9, "depth": 4, "leaf": -0.03031761385500431 }, { "nodeid": 10, "depth": 4, "leaf": 0.2458493560552597 } ] }, - { "nodeid": 6, "depth": 3, "split": "attribute(features,23)", "split_condition": 734.5999755859375, "yes": 11, "no": 12, "missing": 12, "children": [ + { "nodeid": 6, "depth": 3, "split": "xgboost_input_23", "split_condition": 734.5999755859375, "yes": 11, "no": 12, "missing": 12, "children": [ { "nodeid": 11, "depth": 4, "leaf": 0.10233080387115479 }, { "nodeid": 12, "depth": 4, "leaf": -0.09648152440786362 } ] } ] }, - { "nodeid": 4, "depth": 2, "split": "attribute(features,26)", "split_condition": 0.20280000567436218, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 4, "depth": 2, "split": "xgboost_input_26", "split_condition": 0.20280000567436218, "yes": 7, "no": 8, "missing": 8, "children": [ { "nodeid": 7, "depth": 3, "leaf": 0.13269340991973877 }, - { "nodeid": 8, "depth": 3, "split": "attribute(features,15)", "split_condition": 0.017960000783205032, "yes": 13, "no": 14, "missing": 14, "children": [ + { "nodeid": 8, "depth": 3, "split": "xgboost_input_15", "split_condition": 0.017960000783205032, "yes": 13, "no": 14, "missing": 14, "children": [ { "nodeid": 13, "depth": 4, "leaf": -0.24554097652435303 }, { "nodeid": 14, "depth": 4, "leaf": -0.033455345779657364 } ] } ] } ] }, { "nodeid": 2, "depth": 1, "leaf": -0.23360854387283325 } ] }, - { "nodeid": 0, "depth": 0, "split": "attribute(features,23)", "split_condition": 876.5, "yes": 1, "no": 2, "missing": 2, "children": [ - { "nodeid": 1, "depth": 1, "split": "attribute(features,24)", "split_condition": 0.14069999754428864, "yes": 3, "no": 4, "missing": 4, "children": [ - { "nodeid": 3, "depth": 2, "split": "attribute(features,15)", "split_condition": 0.01104000024497509, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 0, "depth": 0, "split": "xgboost_input_23", "split_condition": 876.5, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "xgboost_input_24", "split_condition": 0.14069999754428864, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "split": "xgboost_input_15", "split_condition": 0.01104000024497509, "yes": 7, "no": 8, "missing": 8, "children": [ { "nodeid": 7, "depth": 3, "leaf": -0.0031496614683419466 }, - { "nodeid": 8, "depth": 3, "split": "attribute(features,6)", "split_condition": 0.1111999973654747, "yes": 11, "no": 12, "missing": 12, "children": [ + { "nodeid": 8, "depth": 3, "split": "xgboost_input_6", "split_condition": 0.1111999973654747, "yes": 11, "no": 12, "missing": 12, "children": [ { "nodeid": 11, "depth": 4, "leaf": 0.23949843645095825 }, { "nodeid": 12, "depth": 4, "leaf": 0.03775443509221077 } ] } ] }, - { "nodeid": 4, "depth": 2, "split": "attribute(features,22)", "split_condition": 91.62000274658203, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 4, "depth": 2, "split": "xgboost_input_22", "split_condition": 91.62000274658203, "yes": 9, "no": 10, "missing": 10, "children": [ { "nodeid": 9, "depth": 3, "leaf": 0.10552486777305603 }, - { "nodeid": 10, "depth": 3, "split": "attribute(features,21)", "split_condition": 27.209999084472656, "yes": 13, "no": 14, "missing": 14, "children": [ + { "nodeid": 10, "depth": 3, "split": "xgboost_input_21", "split_condition": 27.209999084472656, "yes": 13, "no": 14, "missing": 14, "children": [ { "nodeid": 13, "depth": 4, "leaf": -0.023241691291332245 }, { "nodeid": 14, "depth": 4, "leaf": -0.20789241790771484 } ] } ] } ] }, - { "nodeid": 2, "depth": 1, "split": "attribute(features,6)", "split_condition": 0.05928000062704086, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 2, "depth": 1, "split": "xgboost_input_6", "split_condition": 0.05928000062704086, "yes": 5, "no": 6, "missing": 6, "children": [ { "nodeid": 5, "depth": 2, "leaf": 0.004834835417568684 }, { "nodeid": 6, "depth": 2, "leaf": -0.23364728689193726 } ] } ] }, - { "nodeid": 0, "depth": 0, "split": "attribute(features,26)", "split_condition": 0.2079000025987625, "yes": 1, "no": 2, "missing": 2, "children": [ - { "nodeid": 1, "depth": 1, "split": "attribute(features,13)", "split_condition": 40.5099983215332, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 0, "depth": 0, "split": "xgboost_input_26", "split_condition": 0.2079000025987625, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "xgboost_input_13", "split_condition": 40.5099983215332, "yes": 3, "no": 4, "missing": 4, "children": [ { "nodeid": 3, "depth": 2, "leaf": 0.23598936200141907 }, { "nodeid": 4, "depth": 2, "leaf": -0.060127921402454376 } ] }, - { "nodeid": 2, "depth": 1, "split": "attribute(features,21)", "split_condition": 25.579999923706055, "yes": 5, "no": 6, "missing": 6, "children": [ - { "nodeid": 5, "depth": 2, "split": "attribute(features,23)", "split_condition": 811.2999877929688, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 2, "depth": 1, "split": "xgboost_input_21", "split_condition": 25.579999923706055, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 2, "split": "xgboost_input_23", "split_condition": 811.2999877929688, "yes": 7, "no": 8, "missing": 8, "children": [ { "nodeid": 7, "depth": 3, "leaf": 0.173164963722229 }, { "nodeid": 8, "depth": 3, "leaf": -0.09621238708496094 } ] }, - { "nodeid": 6, "depth": 2, "split": "attribute(features,4)", "split_condition": 0.08946000039577484, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 6, "depth": 2, "split": "xgboost_input_4", "split_condition": 0.08946000039577484, "yes": 9, "no": 10, "missing": 10, "children": [ { "nodeid": 9, "depth": 3, "leaf": -0.03863441199064255 }, { "nodeid": 10, "depth": 3, "leaf": -0.21613681316375732 } ] } ] } ] }, - { "nodeid": 0, "depth": 0, "split": "attribute(features,13)", "split_condition": 33.0099983215332, "yes": 1, "no": 2, "missing": 2, "children": [ - { "nodeid": 1, "depth": 1, "split": "attribute(features,24)", "split_condition": 0.13770000636577606, "yes": 3, "no": 4, "missing": 4, "children": [ - { "nodeid": 3, "depth": 2, "split": "attribute(features,14)", "split_condition": 0.004147999919950962, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 0, "depth": 0, "split": "xgboost_input_13", "split_condition": 33.0099983215332, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "xgboost_input_24", "split_condition": 0.13770000636577606, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "split": "xgboost_input_14", "split_condition": 0.004147999919950962, "yes": 7, "no": 8, "missing": 8, "children": [ { "nodeid": 7, "depth": 3, "leaf": -0.005957402754575014 }, { "nodeid": 8, "depth": 3, "leaf": 0.22239074110984802 } ] }, - { "nodeid": 4, "depth": 2, "split": "attribute(features,22)", "split_condition": 91.62000274658203, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 4, "depth": 2, "split": "xgboost_input_22", "split_condition": 91.62000274658203, "yes": 9, "no": 10, "missing": 10, "children": [ { "nodeid": 9, "depth": 3, "leaf": 0.1025158017873764 }, { "nodeid": 10, "depth": 3, "leaf": -0.12606994807720184 } ] } ] }, - { "nodeid": 2, "depth": 1, "split": "attribute(features,24)", "split_condition": 0.11180000007152557, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 2, "depth": 1, "split": "xgboost_input_24", "split_condition": 0.11180000007152557, "yes": 5, "no": 6, "missing": 6, "children": [ { "nodeid": 5, "depth": 2, "leaf": 0.04408538341522217 }, - { "nodeid": 6, "depth": 2, "split": "attribute(features,21)", "split_condition": 23.190000534057617, "yes": 11, "no": 12, "missing": 12, "children": [ + { "nodeid": 6, "depth": 2, "split": "xgboost_input_21", "split_condition": 23.190000534057617, "yes": 11, "no": 12, "missing": 12, "children": [ { "nodeid": 11, "depth": 3, "leaf": -0.03383628651499748 }, { "nodeid": 12, "depth": 3, "leaf": -0.21980330348014832 } ] } ] } ] }, - { "nodeid": 0, "depth": 0, "split": "attribute(features,26)", "split_condition": 0.2079000025987625, "yes": 1, "no": 2, "missing": 2, "children": [ - { "nodeid": 1, "depth": 1, "split": "attribute(features,13)", "split_condition": 40.5099983215332, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 0, "depth": 0, "split": "xgboost_input_26", "split_condition": 0.2079000025987625, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "xgboost_input_13", "split_condition": 40.5099983215332, "yes": 3, "no": 4, "missing": 4, "children": [ { "nodeid": 3, "depth": 2, "leaf": 0.21418677270412445 }, { "nodeid": 4, "depth": 2, "leaf": -0.03940589725971222 } ] }, - { "nodeid": 2, "depth": 1, "split": "attribute(features,23)", "split_condition": 967.0, "yes": 5, "no": 6, "missing": 6, "children": [ - { "nodeid": 5, "depth": 2, "split": "attribute(features,21)", "split_condition": 29.25, "yes": 7, "no": 8, "missing": 8, "children": [ - { "nodeid": 7, "depth": 3, "split": "attribute(features,13)", "split_condition": 23.309999465942383, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 2, "depth": 1, "split": "xgboost_input_23", "split_condition": 967.0, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 2, "split": "xgboost_input_21", "split_condition": 29.25, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 7, "depth": 3, "split": "xgboost_input_13", "split_condition": 23.309999465942383, "yes": 9, "no": 10, "missing": 10, "children": [ { "nodeid": 9, "depth": 4, "leaf": 0.17243850231170654 }, { "nodeid": 10, "depth": 4, "leaf": -0.014778186567127705 } ] }, { "nodeid": 8, "depth": 3, "leaf": -0.1282825917005539 } ] }, { "nodeid": 6, "depth": 2, "leaf": -0.20745433866977692 } ] } ] }, - { "nodeid": 0, "depth": 0, "split": "attribute(features,27)", "split_condition": 0.1606999933719635, "yes": 1, "no": 2, "missing": 2, "children": [ - { "nodeid": 1, "depth": 1, "split": "attribute(features,1)", "split_condition": 20.200000762939453, "yes": 3, "no": 4, "missing": 4, "children": [ - { "nodeid": 3, "depth": 2, "split": "attribute(features,13)", "split_condition": 35.029998779296875, "yes": 5, "no": 6, "missing": 6, "children": [ - { "nodeid": 5, "depth": 3, "split": "attribute(features,20)", "split_condition": 16.25, "yes": 9, "no": 10, "missing": 10, "children": [ + { "nodeid": 0, "depth": 0, "split": "xgboost_input_27", "split_condition": 0.1606999933719635, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "xgboost_input_1", "split_condition": 20.200000762939453, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "split": "xgboost_input_13", "split_condition": 35.029998779296875, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 5, "depth": 3, "split": "xgboost_input_20", "split_condition": 16.25, "yes": 9, "no": 10, "missing": 10, "children": [ { "nodeid": 9, "depth": 4, "leaf": 0.20469191670417786 }, { "nodeid": 10, "depth": 4, "leaf": 0.05293010547757149 } ] }, { "nodeid": 6, "depth": 3, "leaf": 0.0055474769324064255 } ] }, - { "nodeid": 4, "depth": 2, "split": "attribute(features,23)", "split_condition": 653.5999755859375, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 4, "depth": 2, "split": "xgboost_input_23", "split_condition": 653.5999755859375, "yes": 7, "no": 8, "missing": 8, "children": [ { "nodeid": 7, "depth": 3, "leaf": 0.1103351041674614 }, - { "nodeid": 8, "depth": 3, "split": "attribute(features,5)", "split_condition": 0.07326000183820724, "yes": 11, "no": 12, "missing": 12, "children": [ + { "nodeid": 8, "depth": 3, "split": "xgboost_input_5", "split_condition": 0.07326000183820724, "yes": 11, "no": 12, "missing": 12, "children": [ { "nodeid": 11, "depth": 4, "leaf": -0.19937729835510254 }, { "nodeid": 12, "depth": 4, "leaf": -0.011575295589864254 } ] } ] } ] }, { "nodeid": 2, "depth": 1, "leaf": -0.16972780227661133 } ] }, - { "nodeid": 0, "depth": 0, "split": "attribute(features,7)", "split_condition": 0.04938000068068504, "yes": 1, "no": 2, "missing": 2, "children": [ - { "nodeid": 1, "depth": 1, "split": "attribute(features,15)", "split_condition": 0.012029999867081642, "yes": 3, "no": 4, "missing": 4, "children": [ - { "nodeid": 3, "depth": 2, "split": "attribute(features,17)", "split_condition": 0.0074970000423491, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 0, "depth": 0, "split": "xgboost_input_7", "split_condition": 0.04938000068068504, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "xgboost_input_15", "split_condition": 0.012029999867081642, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 3, "depth": 2, "split": "xgboost_input_17", "split_condition": 0.0074970000423491, "yes": 7, "no": 8, "missing": 8, "children": [ { "nodeid": 7, "depth": 3, "leaf": 0.0373166985809803 }, { "nodeid": 8, "depth": 3, "leaf": -0.14507374167442322 } ] }, { "nodeid": 4, "depth": 2, "leaf": 0.18085254728794098 } ] }, - { "nodeid": 2, "depth": 1, "split": "attribute(features,21)", "split_condition": 23.75, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 2, "depth": 1, "split": "xgboost_input_21", "split_condition": 23.75, "yes": 5, "no": 6, "missing": 6, "children": [ { "nodeid": 5, "depth": 2, "leaf": 0.021509487181901932 }, { "nodeid": 6, "depth": 2, "leaf": -0.1693851202726364 } ] } ] }, - { "nodeid": 0, "depth": 0, "split": "attribute(features,26)", "split_condition": 0.2079000025987625, "yes": 1, "no": 2, "missing": 2, "children": [ - { "nodeid": 1, "depth": 1, "split": "attribute(features,13)", "split_condition": 40.5099983215332, "yes": 3, "no": 4, "missing": 4, "children": [ + { "nodeid": 0, "depth": 0, "split": "xgboost_input_26", "split_condition": 0.2079000025987625, "yes": 1, "no": 2, "missing": 2, "children": [ + { "nodeid": 1, "depth": 1, "split": "xgboost_input_13", "split_condition": 40.5099983215332, "yes": 3, "no": 4, "missing": 4, "children": [ { "nodeid": 3, "depth": 2, "leaf": 0.18477416038513184 }, { "nodeid": 4, "depth": 2, "leaf": -0.027129333466291428 } ] }, - { "nodeid": 2, "depth": 1, "split": "attribute(features,23)", "split_condition": 734.5999755859375, "yes": 5, "no": 6, "missing": 6, "children": [ + { "nodeid": 2, "depth": 1, "split": "xgboost_input_23", "split_condition": 734.5999755859375, "yes": 5, "no": 6, "missing": 6, "children": [ { "nodeid": 5, "depth": 2, "leaf": 0.05231140926480293 }, - { "nodeid": 6, "depth": 2, "split": "attribute(features,21)", "split_condition": 22.149999618530273, "yes": 7, "no": 8, "missing": 8, "children": [ + { "nodeid": 6, "depth": 2, "split": "xgboost_input_21", "split_condition": 22.149999618530273, "yes": 7, "no": 8, "missing": 8, "children": [ { "nodeid": 7, "depth": 3, "leaf": -0.007632177323102951 }, { "nodeid": 8, "depth": 3, "leaf": -0.16687200963497162 } ] } ] } ] } ] From eb84d92123cee2f01edf88c99bb38ce50557646c Mon Sep 17 00:00:00 2001 From: Arne Juul Date: Fri, 12 Dec 2025 11:50:55 +0000 Subject: [PATCH 5/9] Add automatic feature names loading from optional text file When loading an XGBoost UBJ model, automatically checks for and loads feature names from an optional companion text file. For example, when reading "model.ubj", will look for "model-features.txt" and use those names if present. Key features: - Automatically loads model-features.txt alongside model.ubj - One feature name per line, supports # comments and blank lines - Feature names from file override any names in the UBJ file - Graceful fallback to xgboost_input_X format if file missing or invalid - No-arg toRankingExpression() automatically uses loaded names when valid This enables easy customization of feature names without modifying model files, improving readability of generated ranking expressions. --- .../importer/xgboost/XGBoostUbjParser.java | 61 +++++++++++++++++++ .../xgboost/XGBoostImportTestCase.java | 27 ++++++-- .../xgboost/binary_breast_cancer-features.txt | 32 ++++++++++ 3 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 model-integration/src/test/models/xgboost/binary_breast_cancer-features.txt diff --git a/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostUbjParser.java b/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostUbjParser.java index 3ba7c4b24095..b47024353da8 100644 --- a/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostUbjParser.java +++ b/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostUbjParser.java @@ -6,9 +6,14 @@ import com.devsmart.ubjson.UBReader; import com.devsmart.ubjson.UBValue; +import java.io.BufferedReader; import java.io.FileInputStream; +import java.io.FileReader; import java.io.IOException; import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -141,15 +146,36 @@ private static boolean hasTreeStructure(UBObject treeObj) { } } this.baseScore = tmpBaseScore; + + // Check for optional feature names file (e.g., foobar-features.txt for foobar.ubj) + List overrideFeatureNames = loadFeatureNamesFromFile(filePath); + if (overrideFeatureNames != null) { + tmpFeatureNames = overrideFeatureNames; + } + this.featureNames = Collections.unmodifiableList(tmpFeatureNames); } /** * Converts parsed UBJ trees to Vespa ranking expressions. + * If feature names were loaded from a -features.txt file or extracted from the UBJ file, + * and they match the required count, they will be used automatically. + * Otherwise, uses indexed format (xgboost_input_X). * * @return Vespa ranking expressions. */ String toRankingExpression() { + // Check if we have valid feature names loaded + if (!featureNames.isEmpty()) { + try { + // Try to use the loaded feature names + return toRankingExpression(featureNames); + } catch (IllegalArgumentException e) { + // Feature names don't match required count, fall through to indexed format + } + } + + // Use indexed format (xgboost_input_X) StringBuilder result = new StringBuilder(); // Convert all trees to expressions and join with " + " @@ -303,6 +329,41 @@ private String treeToRankExpWithFeatureNames(XGBoostTree node, List cust return "if (" + condition + ", " + trueExp + ", " + falseExp + ")"; } + /** + * Attempts to load feature names from an optional text file. + * For a UBJ file "path/to/model.ubj", looks for "path/to/model-features.txt". + * Each line in the file should contain one feature name. + * + * @param ubjFilePath Path to the UBJ file + * @return List of feature names if file exists and is valid, null otherwise + */ + private static List loadFeatureNamesFromFile(String ubjFilePath) { + // Construct the features file path by replacing .ubj with -features.txt + String featuresFilePath = ubjFilePath.replaceFirst("\\.ubj$", "-features.txt"); + Path path = Paths.get(featuresFilePath); + + if (!Files.exists(path)) { + return null; // File doesn't exist, that's okay + } + + try { + List featureNames = new ArrayList<>(); + try (BufferedReader reader = new BufferedReader(new FileReader(featuresFilePath))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (!line.isEmpty() && !line.startsWith("#")) { + featureNames.add(line); + } + } + } + return featureNames.isEmpty() ? null : featureNames; + } catch (IOException e) { + // If we can't read the file, just return null and use default naming + return null; + } + } + /** * Extracts a required UBObject from a parent object. * diff --git a/model-integration/src/test/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostImportTestCase.java b/model-integration/src/test/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostImportTestCase.java index c393da56804a..c01ee9a4ee6b 100644 --- a/model-integration/src/test/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostImportTestCase.java +++ b/model-integration/src/test/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostImportTestCase.java @@ -55,15 +55,15 @@ public void testXGBoostUBJ() { assertTrue("UBJ expression should contain base_score adjustment", ubjExprStr.contains(" 0.52114942")); - // Both formats should use xgboost_input_X format - assertTrue("UBJ should use xgboost_input_ format", - ubjExprStr.contains("xgboost_input_")); + // JSON should use xgboost_input_X format (from the JSON file) assertTrue("JSON should use xgboost_input_ format", jsonExprStr.contains("xgboost_input_")); - // UBJ expression should start with the same tree expressions as JSON - assertTrue("UBJ should contain tree expressions matching JSON", - ubjExprStr.startsWith(jsonExprStr)); + // UBJ should use feature names (auto-loaded from binary_breast_cancer-features.txt) + assertTrue("UBJ should use feature names from file", + ubjExprStr.contains("mean_radius")); + assertFalse("UBJ should not use indexed format", + ubjExprStr.contains("xgboost_input_")); } @Test @@ -119,4 +119,19 @@ public void testXGBoostUBJWithTooManyFeatureNames() throws IOException { }); } + @Test + public void testXGBoostUBJAutoLoadFeatureNames() throws IOException { + // The binary_breast_cancer-features.txt file should be automatically loaded + XGBoostUbjParser parser = new XGBoostUbjParser("src/test/models/xgboost/binary_breast_cancer.ubj"); + + // Call no-arg toRankingExpression() - should use feature names from file + String expression = parser.toRankingExpression(); + assertNotNull(expression); + + // Verify that custom feature names are used (from the -features.txt file) + assertTrue("Expression should contain feature name from file", expression.contains("mean_radius")); + assertTrue("Expression should contain feature name from file", expression.contains("worst_texture")); + assertFalse("Expression should not contain indexed format", expression.contains("xgboost_input_")); + } + } diff --git a/model-integration/src/test/models/xgboost/binary_breast_cancer-features.txt b/model-integration/src/test/models/xgboost/binary_breast_cancer-features.txt new file mode 100644 index 000000000000..dd48a897c86b --- /dev/null +++ b/model-integration/src/test/models/xgboost/binary_breast_cancer-features.txt @@ -0,0 +1,32 @@ +# Feature names for Wisconsin Breast Cancer dataset +# One feature name per line +mean_radius +mean_texture +mean_perimeter +mean_area +mean_smoothness +mean_compactness +mean_concavity +mean_concave_points +mean_symmetry +mean_fractal_dimension +radius_error +texture_error +perimeter_error +area_error +smoothness_error +compactness_error +concavity_error +concave_points_error +symmetry_error +fractal_dimension_error +worst_radius +worst_texture +worst_perimeter +worst_area +worst_smoothness +worst_compactness +worst_concavity +worst_concave_points +worst_symmetry +worst_fractal_dimension From 24257607f1c86afeb0440e9186d649c32c2c1ac4 Mon Sep 17 00:00:00 2001 From: Arne Juul Date: Fri, 12 Dec 2025 11:58:28 +0000 Subject: [PATCH 6/9] Clean up XGBoost feature filename handling --- .../importer/xgboost/XGBoostUbjParser.java | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostUbjParser.java b/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostUbjParser.java index b47024353da8..e72788fa5276 100644 --- a/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostUbjParser.java +++ b/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostUbjParser.java @@ -148,7 +148,8 @@ private static boolean hasTreeStructure(UBObject treeObj) { this.baseScore = tmpBaseScore; // Check for optional feature names file (e.g., foobar-features.txt for foobar.ubj) - List overrideFeatureNames = loadFeatureNamesFromFile(filePath); + String featuresPath = withFeaturesSuffix(filePath); + List overrideFeatureNames = loadFeatureNamesFromFile(featuresPath); if (overrideFeatureNames != null) { tmpFeatureNames = overrideFeatureNames; } @@ -165,14 +166,9 @@ private static boolean hasTreeStructure(UBObject treeObj) { * @return Vespa ranking expressions. */ String toRankingExpression() { - // Check if we have valid feature names loaded if (!featureNames.isEmpty()) { - try { - // Try to use the loaded feature names - return toRankingExpression(featureNames); - } catch (IllegalArgumentException e) { - // Feature names don't match required count, fall through to indexed format - } + // note: requires exactly the correct number of feature names. + return toRankingExpression(featureNames); } // Use indexed format (xgboost_input_X) @@ -203,7 +199,7 @@ String toRankingExpression() { * @throws IllegalArgumentException if customFeatureNames is insufficient for the indices used */ String toRankingExpression(List customFeatureNames) { - // Validate that we have enough feature names + // Validate that we have the right number of feature names validateFeatureNames(customFeatureNames); StringBuilder result = new StringBuilder(); @@ -329,6 +325,13 @@ private String treeToRankExpWithFeatureNames(XGBoostTree node, List cust return "if (" + condition + ", " + trueExp + ", " + falseExp + ")"; } + private static String withFeaturesSuffix(String ubjFilePath) { + if (ubjFilePath.endsWith(".ubj")) { + ubjFilePath = ubjFilePath.substring(0, ubjFilePath.length() - 4); + } + return ubjFilePath + "-features.txt"; + } + /** * Attempts to load feature names from an optional text file. * For a UBJ file "path/to/model.ubj", looks for "path/to/model-features.txt". @@ -337,9 +340,7 @@ private String treeToRankExpWithFeatureNames(XGBoostTree node, List cust * @param ubjFilePath Path to the UBJ file * @return List of feature names if file exists and is valid, null otherwise */ - private static List loadFeatureNamesFromFile(String ubjFilePath) { - // Construct the features file path by replacing .ubj with -features.txt - String featuresFilePath = ubjFilePath.replaceFirst("\\.ubj$", "-features.txt"); + private static List loadFeatureNamesFromFile(String featuresFilePath) { Path path = Paths.get(featuresFilePath); if (!Files.exists(path)) { From 9aa9efb72b83d75a19ea86c0b0345967910102de Mon Sep 17 00:00:00 2001 From: Arne Juul Date: Fri, 12 Dec 2025 13:33:54 +0000 Subject: [PATCH 7/9] Make ubj handler objective-aware The importer now extracts and tracks the model's objective function type (e.g., reg:squarederror, binary:logistic) to correctly handle base_score: - Apply logit transformation only for logistic objectives - Use base_score directly for regression objectives - Use objective-specific defaults (0.5 for logistic, 0.0 for regression) - Relax feature name validation to require "at least N" instead of "exactly N" --- .../importer/xgboost/XGBoostUbjParser.java | 66 +++++++++++++++---- .../xgboost/XGBoostImportTestCase.java | 17 ----- 2 files changed, 54 insertions(+), 29 deletions(-) diff --git a/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostUbjParser.java b/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostUbjParser.java index e72788fa5276..207c6d69e28d 100644 --- a/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostUbjParser.java +++ b/model-integration/src/main/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostUbjParser.java @@ -28,6 +28,7 @@ class XGBoostUbjParser extends AbstractXGBoostParser { private final List xgboostTrees; private final double baseScore; private final List featureNames; + private final String objective; /** * Probes a file to check if it looks like an XGBoost UBJ model. @@ -106,6 +107,7 @@ private static boolean hasTreeStructure(UBObject treeObj) { this.xgboostTrees = new ArrayList<>(); double tmpBaseScore = 0.5; // default value List tmpFeatureNames = new ArrayList<>(); + String tmpObjective = "reg:squarederror"; // default objective if not found try (FileInputStream fileStream = new FileInputStream(filePath); UBReader reader = new UBReader(fileStream)) { UBValue root = reader.read(); @@ -118,8 +120,11 @@ private static boolean hasTreeStructure(UBObject treeObj) { UBObject rootObj = root.asObject(); UBObject learner = getRequiredObject(rootObj, "learner", "UBJ root"); + // Extract objective if available + tmpObjective = extractObjective(learner); + // Extract base_score if available - tmpBaseScore = extractBaseScore(learner); + tmpBaseScore = extractBaseScore(learner, tmpObjective); // Extract feature_names if available UBValue featureNamesValue = learner.get("feature_names"); @@ -146,6 +151,7 @@ private static boolean hasTreeStructure(UBObject treeObj) { } } this.baseScore = tmpBaseScore; + this.objective = tmpObjective; // Check for optional feature names file (e.g., foobar-features.txt for foobar.ubj) String featuresPath = withFeaturesSuffix(filePath); @@ -167,7 +173,7 @@ private static boolean hasTreeStructure(UBObject treeObj) { */ String toRankingExpression() { if (!featureNames.isEmpty()) { - // note: requires exactly the correct number of feature names. + // note: requires enough feature names. return toRankingExpression(featureNames); } @@ -182,10 +188,20 @@ String toRankingExpression() { result.append(treeToRankExp(xgboostTrees.get(i))); } - // Add precomputed base_score logit transformation - double baseScoreLogit = Math.log(baseScore) - Math.log(1.0 - baseScore); + // Add base_score, with logit transformation only for logistic objectives result.append(" + \n"); - result.append(baseScoreLogit); + if (objective.endsWith(":logistic")) { + if (baseScore > 0.0 && baseScore < 1.0) { + // Add precomputed base_score logit transformation + double baseScoreLogit = Math.log(baseScore) - Math.log(1.0 - baseScore); + result.append(baseScoreLogit); + } else { + System.err.println("Bad basescore " + baseScore + " for logistic model, should be in range (0.0, 1.0)"); + result.append("0.0"); + } + } else { + result.append(baseScore); + } return result.toString(); } @@ -211,10 +227,14 @@ String toRankingExpression(List customFeatureNames) { result.append(treeToRankExpWithFeatureNames(xgboostTrees.get(i), customFeatureNames)); } - // Add precomputed base_score logit transformation - double baseScoreLogit = Math.log(baseScore) - Math.log(1.0 - baseScore); + // Add base_score, with logit transformation only for logistic objectives result.append(" + \n"); - result.append(baseScoreLogit); + if (objective.endsWith(":logistic")) { + double baseScoreLogit = Math.log(baseScore) - Math.log(1.0 - baseScore); + result.append(baseScoreLogit); + } else { + result.append(baseScore); + } return result.toString(); } @@ -234,9 +254,9 @@ private void validateFeatureNames(List customFeatureNames) { int maxIndex = findMaxFeatureIndex(); int requiredSize = maxIndex + 1; - if (customFeatureNames.size() != requiredSize) { + if (customFeatureNames.size() < requiredSize) { throw new IllegalArgumentException( - "Feature names list size mismatch: model requires exactly " + requiredSize + + "Feature names list size mismatch: model requires at least " + requiredSize + " feature names (indices 0-" + maxIndex + ") but " + customFeatureNames.size() + " names provided" ); @@ -388,7 +408,7 @@ private static UBObject getRequiredObject(UBObject parent, String key, String pa * @param learner The learner UBObject. * @return The extracted base_score, or 0.5 if not found. */ - private static double extractBaseScore(UBObject learner) { + private static double extractBaseScore(UBObject learner, String objective) { UBValue learnerModelParamValue = learner.get("learner_model_param"); if (learnerModelParamValue != null && learnerModelParamValue.isObject()) { UBObject learnerModelParam = learnerModelParamValue.asObject(); @@ -400,7 +420,29 @@ private static double extractBaseScore(UBObject learner) { return Double.parseDouble(baseScoreStr); } } - return 0.5; // default value + if (objective != null && objective.endsWith(":logistic")) { + return 0.5; // default value for logistic + } else { + return 0.0; // default value for simple regression + } + } + + /** + * Extracts the objective name from the objective object if available. + * + * @param learner The learner UBObject. + * @return The extracted objective name, or "reg:squarederror" if not found. + */ + private static String extractObjective(UBObject learner) { + UBValue objectiveValue = learner.get("objective"); + if (objectiveValue != null && objectiveValue.isObject()) { + UBObject objective = objectiveValue.asObject(); + UBValue nameValue = objective.get("name"); + if (nameValue != null && nameValue.isString()) { + return nameValue.asString(); + } + } + return "reg:squarederror"; // default objective if not found } /** diff --git a/model-integration/src/test/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostImportTestCase.java b/model-integration/src/test/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostImportTestCase.java index c01ee9a4ee6b..845daa0cdad6 100644 --- a/model-integration/src/test/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostImportTestCase.java +++ b/model-integration/src/test/java/ai/vespa/rankingexpression/importer/xgboost/XGBoostImportTestCase.java @@ -102,23 +102,6 @@ public void testXGBoostUBJWithInsufficientFeatureNames() throws IOException { }); } - @Test - public void testXGBoostUBJWithTooManyFeatureNames() throws IOException { - XGBoostUbjParser parser = new XGBoostUbjParser("src/test/models/xgboost/binary_breast_cancer.ubj"); - - // Provide 35 feature names when model needs exactly 30 - List featureNames = Arrays.asList( - "f0", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", - "f10", "f11", "f12", "f13", "f14", "f15", "f16", "f17", "f18", "f19", - "f20", "f21", "f22", "f23", "f24", "f25", "f26", "f27", "f28", "f29", - "f30", "f31", "f32", "f33", "f34" - ); - - assertThrows(IllegalArgumentException.class, () -> { - parser.toRankingExpression(featureNames); - }); - } - @Test public void testXGBoostUBJAutoLoadFeatureNames() throws IOException { // The binary_breast_cancer-features.txt file should be automatically loaded From 92013a9f574ce70c2322ab2d94068f6d27bffe8d Mon Sep 17 00:00:00 2001 From: Arne Juul Date: Fri, 12 Dec 2025 14:28:45 +0000 Subject: [PATCH 8/9] minimize visibility of ubjson library --- application/pom.xml | 4 ++++ cloud-tenant-base-dependencies-enforcer/pom.xml | 1 - container-dependencies-enforcer/pom.xml | 1 - container-dev/pom.xml | 4 ++++ 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/application/pom.xml b/application/pom.xml index 2fffd911d97a..d526309d40fd 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -53,6 +53,10 @@ javax.annotation javax.annotation-api + + com.dev-smart + ubjson + diff --git a/cloud-tenant-base-dependencies-enforcer/pom.xml b/cloud-tenant-base-dependencies-enforcer/pom.xml index 25e60073c708..0a3ec96e67b4 100644 --- a/cloud-tenant-base-dependencies-enforcer/pom.xml +++ b/cloud-tenant-base-dependencies-enforcer/pom.xml @@ -43,7 +43,6 @@ aopalliance:aopalliance:${aopalliance.vespa.version}:provided - com.dev-smart:ubjson:jar:*:* com.fasterxml.jackson.core:jackson-annotations:${jackson2.vespa.version}:provided com.fasterxml.jackson.core:jackson-core:${jackson2.vespa.version}:provided com.fasterxml.jackson.core:jackson-databind:${jackson-databind.vespa.version}:provided diff --git a/container-dependencies-enforcer/pom.xml b/container-dependencies-enforcer/pom.xml index 66f6d5b52f46..896a3d6a1a75 100644 --- a/container-dependencies-enforcer/pom.xml +++ b/container-dependencies-enforcer/pom.xml @@ -62,7 +62,6 @@ aopalliance:aopalliance:${aopalliance.vespa.version}:provided - com.dev-smart:ubjson:jar:*:* com.fasterxml.jackson.core:jackson-annotations:${jackson2.vespa.version}:provided com.fasterxml.jackson.core:jackson-core:${jackson2.vespa.version}:provided com.fasterxml.jackson.core:jackson-databind:${jackson-databind.vespa.version}:provided diff --git a/container-dev/pom.xml b/container-dev/pom.xml index d4365be0408e..ed9e8e340d6b 100644 --- a/container-dev/pom.xml +++ b/container-dev/pom.xml @@ -94,6 +94,10 @@ org.lz4 lz4-java + + com.dev-smart + ubjson + From b55f4f559aa14e27dd83ee05049e80f22115002e Mon Sep 17 00:00:00 2001 From: Arne Juul Date: Fri, 12 Dec 2025 14:44:15 +0000 Subject: [PATCH 9/9] use standard mechanism for ubjson version management --- dependency-versions/pom.xml | 1 + model-integration/pom.xml | 3 --- parent/pom.xml | 5 +++++ vespa-dependencies-enforcer/allowed-maven-dependencies.txt | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/dependency-versions/pom.xml b/dependency-versions/pom.xml index 5c650cc49195..27923acf5a4a 100644 --- a/dependency-versions/pom.xml +++ b/dependency-versions/pom.xml @@ -33,6 +33,7 @@ 1.0 + 0.1.8 2.30.0 33.2.1-jre 6.0.0 diff --git a/model-integration/pom.xml b/model-integration/pom.xml index 8bc54301b01e..58ee4017bd05 100644 --- a/model-integration/pom.xml +++ b/model-integration/pom.xml @@ -376,13 +376,10 @@ ${testcontainers.vespa.version} test - com.dev-smart ubjson - 0.1.8 - diff --git a/parent/pom.xml b/parent/pom.xml index 2183736b0cba..0a7b87f83271 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -529,6 +529,11 @@ java-jwt ${java-jwt.vespa.version} + + com.dev-smart + ubjson + ${dev-smart-ubjson.vespa.version} + com.fasterxml.jackson.dataformat jackson-dataformat-cbor diff --git a/vespa-dependencies-enforcer/allowed-maven-dependencies.txt b/vespa-dependencies-enforcer/allowed-maven-dependencies.txt index 7110a722bce1..0a0df812e6a0 100644 --- a/vespa-dependencies-enforcer/allowed-maven-dependencies.txt +++ b/vespa-dependencies-enforcer/allowed-maven-dependencies.txt @@ -6,7 +6,7 @@ aopalliance:aopalliance:${aopalliance.vespa.version} backport-util-concurrent:backport-util-concurrent:3.1 classworlds:classworlds:1.1-alpha-2 com.auth0:java-jwt:${java-jwt.vespa.version} -com.dev-smart:ubjson:0.1.8 +com.dev-smart:ubjson:${dev-smart-ubjson.vespa.version} com.ethlo.time:itu:1.10.3 com.fasterxml.jackson.core:jackson-annotations:${jackson2.vespa.version} com.fasterxml.jackson.core:jackson-core:${jackson2.vespa.version}