Repository navigation
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #156 +/- ##
============================================
+ Coverage 81.76% 82.74% +0.98%
- Complexity 525 579 +54
============================================
Files 52 58 +6
Lines 1771 1930 +159
Branches 179 193 +14
============================================
+ Hits 1448 1597 +149
- Misses 234 242 +8
- Partials 89 91 +2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| /** | ||
| * Specifies the XGBoost algorithms that can be imported and trained through this provider. | ||
| * | ||
| * @since 1.0.0 |
There was a problem hiding this comment.
Not true, also, not needed.
jpleitao
left a comment
There was a problem hiding this comment.
Overall the code looks good, just added some comments regarding (i) the set of parameters being exposed, (ii) default values for some parameters, and (iii) validations
| XGBOOST_BINARY_CLASSIFIER(createDescriptor( | ||
| "DMLC - XGBoost", | ||
| XgboostDescriptorUtil.PARAMS, | ||
| MachineLearningAlgorithmType.SUPERVISED_BINARY_CLASSIFICATION, |
There was a problem hiding this comment.
See also the comment in validateForFit(), but in short XGBoost is being declared here as binary classification, but no such validation that the target is actually binary is being performed
| /** | ||
| * The set of parameters accepted when training an XGBoost model. | ||
| */ | ||
| static final Set<ModelParameter> PARAMS = ImmutableSet.of( |
There was a problem hiding this comment.
Looking at the XGBoost support in an internal data science Python package, and the XGBoost documentation, it seems to me that some supported parameters are note defined here:
max_delta_step(default 0.0) — Maximum absolute value of a leaf node prediction- colsample_bylevel (default 1.0) — Column sample rate
tree_method(defaultauto/hist) - Tree construction algorithm used in XGBoost. Possible values:auto,exact,approx,hist, whereautois the same ashistgrow_policy(defaultdepthwise) - Controls a way new nodes are added to the tree. Possible values:depthwise,lossguidemax_leaves(default 0) — Maximum number of nodes to be added. Not used byexacttree method.max_bin(default 256) — Maximum number of bins. Only used iftree_methodis set tohistorapprox
While not defined in the internal DS Python package, could also make sense to define:
scale_pos_weight(default 1.0) — Control the balance of positive and negative weights, useful for unbalanced classes.
| new ModelParameter( | ||
| OBJECTIVE_PARAMETER_NAME, | ||
| "Objective", | ||
| "The learning task and corresponding objective:\n" |
There was a problem hiding this comment.
If XGBoost is declared to be MachineLearningAlgorithmType.SUPERVISED_BINARY_CLASSIFICATION, then it doesn't make sense to allow for objective = multi:softprob (it also requires a num_class parameter indicating the number of classes, but since this XGboost is for binary classification, no such parameter is exposed).
My suggestion is to remove multi:softprob from the set of possible values of this parameter and add a check in XgboostModelCreator.validateForFit() thorwing an error in case objective=multi:softprob is specified
| "Objective", | ||
| "The learning task and corresponding objective:\n" | ||
| + "'binary:logistic' outputs the probability of the positive class,\n" | ||
| + "'binary:logitraw' outputs the raw (pre-sigmoid) score,\n" |
There was a problem hiding this comment.
objective = binary:logitraw outputs a raw pre-sigmoid score (unbounded, can be negative), but XgboostClassificationModel.toClassDistribution() treats any single-value output as a probability and returns [1-p, p]. As such, when objective = binary:logitraw is specified, [1-p, p] can fall outside the [0, 1] interval.
My suggestion is to remove binary:logitraw from the set of possible values for this parameter, and add a check in XgboostModelCreator.validateForFit() throwing an error in case objective=binary:logitraw is specified. It is also possible to keep allowing binary:logitraw and apply a sigmoid in XgboostModelCreator.validateForFit(), but that would produce the same as using objective=binary:logistic
| "Number of boosting rounds", | ||
| "Number of boosting iterations (trees) to build.", | ||
| MANDATORY, | ||
| intRange(1, Integer.MAX_VALUE, 100) |
There was a problem hiding this comment.
In the internal DS Python package a default value of 50 is being used. Consider adopting the same default value here in order to align with the previous implementation
| "Number of threads", | ||
| "Number of parallel threads used to run XGBoost. Defaults to 1 for deterministic behavior.", | ||
| NOT_MANDATORY, | ||
| intRange(1, Integer.MAX_VALUE, 1) |
There was a problem hiding this comment.
In the internal DS Python package, this parameter takes None as default value, which means all available threads can be used. In native XGBoost, this is achieved by leaving this unset or specifying nthread=0, so I think the minimum value of this parameter should be changed to 0. The description of the parameter could also be updated in order to specify that 0 means using all available threads.
It is also worth noting that, while keeping 1 as the default value is safe, the default in H2O was to use all threads, meaning that this code could introduce slower default training.
|
|
||
| booster = XGBoost.train(trainMatrix, boosterParams, numRound, new HashMap<>(), null, null); | ||
|
|
||
| final Path exportDir = exportModel(booster); |
There was a problem hiding this comment.
Could be worth adding a comment explaining the rationale being saving the model to file and reading again: my understanding is that something similar is already done in this repository for LightGBM, and we are also ensuring that fit() shares the same code as model import, so a freshly-trained model scores identically to the same model saved and reloaded.
But I could be missing or misunderstanding something.
| booster = XGBoost.train(trainMatrix, boosterParams, numRound, new HashMap<>(), null, null); | ||
|
|
||
| final Path exportDir = exportModel(booster); | ||
| return loadModel(exportDir, schema); |
There was a problem hiding this comment.
In exportModel() a temporary directory is created but after loadModel() it is never removed.
Consider clearing that temporary folder in the finally block. As a starting point, this is what is currently being done for LightGBM (single temporary file instead of temporary directory):
try {
Files.delete(tmpModelFilePath);
} catch (final IOException e) {
logger.error("Could not delete temporary model file: {}", e.getMessage());
}
| @Override | ||
| public List<ParamValidationError> validateForFit(final Path pathToPersist, | ||
| final DatasetSchema schema, | ||
| final Map<String, String> params) { |
There was a problem hiding this comment.
Not sure if this is the best place to this, but could be worth performing some validations that the target is binary, just like it is done for LightGBM:
if (getNumTargetClasses(schema).orElse(-1) != 2) {
errorsBuilder.add(new ParamValidationError(ERROR_MSG_NON_BINARY_TARGET));
}
Adds a new OpenML provider, openml-xgboost, built directly on the upstream XGBoost JVM binding (ml.dmlc:xgboost4j_2.12:3.4.0) to be a replacement of the H2O-embedded XGBoost.
Phase 1 covers:
Key classes
Native runtime
Dependency handling (Kryo)
xgboost4j_2.12 transitively pulls Kryo 5, whose com.esotericsoftware.kryo.Kryo collides on the classpath with the Kryo 4 (kryo-shaded) used by Spark/consumers (Kryo 5 removed the nested Kryo.DefaultInstantiatorStrategy). Since Booster implements com.esotericsoftware.kryo.KryoSerializable, kryo must still be
present at class-load. Resolution: exclude kryo from the xgboost4j dependency and re-declare it in provided scope — available for this module's own compile/tests, not propagated transitively to consumers (which supply their own Kryo).
Testing