diff --git a/include/xgboost/multi_target_tree_model.h b/include/xgboost/multi_target_tree_model.h index fc78fbd9537f..35a4ddecc1ba 100644 --- a/include/xgboost/multi_target_tree_model.h +++ b/include/xgboost/multi_target_tree_model.h @@ -1,5 +1,5 @@ /** - * Copyright 2023-2025, XGBoost contributors + * Copyright 2023-2026, XGBoost contributors * * @brief Core data structure for multi-target trees. */ @@ -58,6 +58,10 @@ class MultiTargetTree : public Model { HostDeviceVector weights_; // Output weights. HostDeviceVector leaf_weights_; + // Loss change for each node. + HostDeviceVector loss_chg_; + // Sum of hessians for each node (coverage). + HostDeviceVector sum_hess_; [[nodiscard]] linalg::VectorView NodeWeight(bst_node_t nidx) const { auto beg = nidx * this->NumSplitTargets(); @@ -81,16 +85,20 @@ class MultiTargetTree : public Model { MultiTargetTree& operator=(MultiTargetTree&& that) = delete; /** - * @brief Set the weight for the root. + * @brief Set the weight and statistics for the root. + * + * @param weight The weight vector for the root node. + * @param sum_hess The sum of hessians for the root node (coverage). */ - void SetRoot(linalg::VectorView weight); + void SetRoot(linalg::VectorView weight, float sum_hess); /** * @brief Expand a leaf into split node. */ void Expand(bst_node_t nidx, bst_feature_t split_idx, float split_cond, bool default_left, linalg::VectorView base_weight, linalg::VectorView left_weight, - linalg::VectorView right_weight); + linalg::VectorView right_weight, float loss_chg, float sum_hess, + float left_sum, float right_sum); /** @see RegTree::SetLeaves */ void SetLeaves(std::vector leaves, common::Span weights); /** @brief Copy base weight into leaf weight for a non-reduced multi-target tree. */ diff --git a/include/xgboost/tree_model.h b/include/xgboost/tree_model.h index bcb8aa72b3e0..116e14c426f8 100644 --- a/include/xgboost/tree_model.h +++ b/include/xgboost/tree_model.h @@ -1,5 +1,5 @@ /** - * Copyright 2014-2025, XGBoost Contributors + * Copyright 2014-2026, XGBoost Contributors * * @brief model structure for tree * \author Tianqi Chen @@ -322,11 +322,17 @@ class RegTree : public Model { bst_node_t leaf_right_child = kInvalidNodeId); /** * @brief Expands a leaf node into two additional leaf nodes for a multi-target tree. + * + * @param gain The gain (loss change) from this split. + * @param sum_hess The sum of hessians for the parent node (coverage). + * @param left_sum The sum of hessians for the left child (coverage). + * @param right_sum The sum of hessians for the right child (coverage). */ void ExpandNode(bst_node_t nidx, bst_feature_t split_index, float split_cond, bool default_left, linalg::VectorView base_weight, linalg::VectorView left_weight, - linalg::VectorView right_weight); + linalg::VectorView right_weight, float loss_chg, float sum_hess, + float left_sum, float right_sum); /** * @brief Set all leaf weights for a multi-target tree. * @@ -407,13 +413,14 @@ class RegTree : public Model { */ [[nodiscard]] bst_node_t GetDepth(bst_node_t nidx) const; /** - * @brief Set the root weight for a multi-target tree. + * @brief Set the root weight and statistics for a multi-target tree. * - * @param weight Internal split weight, with size equals to reduced targets. + * @param weight Internal split weight, with size equals to reduced targets. + * @param sum_hess The sum of hessians for the root node (coverage). */ - void SetRoot(linalg::VectorView weight) { + void SetRoot(linalg::VectorView weight, float sum_hess) { CHECK(IsMultiTarget()); - return this->p_mt_tree_->SetRoot(weight); + return this->p_mt_tree_->SetRoot(weight, sum_hess); } /** * @brief Get the maximum depth. diff --git a/python-package/xgboost/_data_utils.py b/python-package/xgboost/_data_utils.py index c72a133ce0c7..295eb55b763e 100644 --- a/python-package/xgboost/_data_utils.py +++ b/python-package/xgboost/_data_utils.py @@ -408,7 +408,14 @@ def pd_cat_inf( # pylint: disable=too-many-locals # pandas uses -1 to represent missing values for categorical features codes = codes.replace(-1, np.nan) - if np.issubdtype(cats.dtype, np.floating) or np.issubdtype(cats.dtype, np.integer): + def is_prim() -> bool: + dtype = cats.dtype + try: + return np.issubdtype(dtype, np.floating) or np.issubdtype(dtype, np.integer) + except TypeError: + return False + + if is_prim(): # Numeric index type name_values_num = cats.values jarr_values = array_interface_dict(name_values_num) diff --git a/python-package/xgboost/compat.py b/python-package/xgboost/compat.py index 7198dbf22208..46322a2f5d74 100644 --- a/python-package/xgboost/compat.py +++ b/python-package/xgboost/compat.py @@ -199,11 +199,15 @@ def _is_cudf_pandas(data: DataType) -> bool: def _is_pandas_df(data: DataType) -> TypeGuard["pd.DataFrame"]: - return lazy_isinstance(data, "pandas.core.frame", "DataFrame") + return lazy_isinstance(data, "pandas.core.frame", "DataFrame") or lazy_isinstance( + data, "pandas", "DataFrame" + ) def _is_pandas_series(data: DataType) -> TypeGuard["pd.Series"]: - return lazy_isinstance(data, "pandas.core.series", "Series") + return lazy_isinstance(data, "pandas.core.series", "Series") or lazy_isinstance( + data, "pandas", "Series" + ) def _is_modin_df(data: DataType) -> bool: diff --git a/python-package/xgboost/testing/multi_target.py b/python-package/xgboost/testing/multi_target.py index f943aea86c00..167addf6ae6c 100644 --- a/python-package/xgboost/testing/multi_target.py +++ b/python-package/xgboost/testing/multi_target.py @@ -2,7 +2,7 @@ # pylint: disable=unbalanced-tuple-unpacking from types import ModuleType -from typing import Dict, Optional, Tuple +from typing import Any, Dict, Optional, Tuple import numpy as np import pytest @@ -11,6 +11,7 @@ make_multilabel_classification, make_regression, ) +from sklearn.metrics.pairwise import cosine_similarity import xgboost.testing as tm @@ -384,12 +385,12 @@ def run() -> Booster: def run_column_sampling(device: Device) -> None: - """Test with column sampling.""" + """Test column sampling with feature importance for multi-target trees.""" n_features = 32 X, y = make_regression( n_samples=1024, n_features=n_features, random_state=1994, n_targets=3 ) - # First half is valid, second half is 0. + # First half of features have weight, second half has 0 weight (not sampled). feature_weights = np.zeros(shape=(n_features, 1), dtype=np.float32) feature_weights[: n_features // 2] = 1.0 / (n_features / 2) Xy = QuantileDMatrix(X, y, feature_weights=feature_weights) @@ -401,13 +402,39 @@ def run_column_sampling(device: Device) -> None: "colsample_bynode": 0.4, } booster = train(params, Xy, num_boost_round=16) - fscores = booster.get_fscore() - # sampled - for f in range(0, n_features // 2): - assert f"f{f}" in fscores - # not sampled - for f in range(n_features // 2, n_features): - assert f"f{f}" not in fscores + + # Test all importance types + for importance_type in ["weight", "gain", "total_gain", "cover", "total_cover"]: + scores: dict = booster.get_score(importance_type=importance_type) + assert len(scores) > 0, f"No scores for {importance_type}" + + # Sampled features (first half) should be in scores + for f in range(0, n_features // 2): + assert f"f{f}" in scores, f"f{f} not in {importance_type} scores" + + # Non-sampled features (second half) should NOT be in scores + for f in range(n_features // 2, n_features): + assert f"f{f}" not in scores + + for score in scores.values(): + assert isinstance(score, float) + assert score >= 0 + + # sklearn Coef + X, y = make_multilabel_classification(random_state=1994) + clf = XGBClassifier( + multi_strategy="multi_output_tree", + importance_type="weight", + device=device, + colsample_bynode=0.2, + ) + clf.fit(X, y, feature_weights=np.arange(0, X.shape[1])) + fi = clf.feature_importances_ + assert fi[0] == 0.0 + assert fi[-1] > fi[1] * 5 + + w = np.polynomial.Polynomial.fit(np.arange(0, X.shape[1]), fi, deg=1) + assert w.coef[1] > 0.03 def run_grow_policy(device: Device, grow_policy: str) -> None: @@ -426,3 +453,104 @@ def run_grow_policy(device: Device, grow_policy: str) -> None: evals_result = train_result(params, Xy, num_rounds=10) assert non_increasing(evals_result["train"]["rmse"]) + + +def run_mixed_strategy(device: Device) -> None: + """Test mixed multi_strategy with ResetStrategy callback.""" + X, y = make_classification( + n_samples=1024, n_informative=8, n_classes=3, random_state=1994 + ) + Xy = DMatrix(data=X, label=y) + + booster = train( + { + "num_parallel_tree": 4, + "num_class": 3, + "objective": "multi:softprob", + "multi_strategy": "multi_output_tree", + "device": device, + "debug_synchronize": True, + "base_score": 0, + }, + num_boost_round=16, + dtrain=Xy, + callbacks=[ResetStrategy()], + ) + + # Test model slicing - each boosting round should be iterable + assert len(list(booster)) == 16 + + # Test that sliced predictions sum to full prediction + predt = booster.predict(Xy, output_margin=True) + predt_sum = np.zeros(predt.shape) + for t in booster: + predt_sum += t.predict(Xy, output_margin=True) + np.testing.assert_allclose(predt, predt_sum, atol=1e-5) + + # Test feature importance works with mixed trees + for importance_type in ["weight", "gain", "total_gain", "cover", "total_cover"]: + scores = booster.get_score(importance_type=importance_type) + assert len(scores) > 0 + for score in scores.values(): + assert isinstance(score, float) + assert score >= 0 + + +def run_feature_importance_strategy_compare(device: Device) -> None: + """Different strategies produce similar feature importance ratios.""" + n_features = 16 + X, y = make_classification( + n_samples=2048, + n_features=n_features, + n_informative=10, + n_classes=4, + random_state=1994, + ) + Xy = DMatrix(data=X, label=y) + + base_params: Dict[str, Any] = { + "num_class": 4, + "objective": "multi:softprob", + "device": device, + "debug_synchronize": True, + "max_depth": 5, + } + + # Train models with different strategies + boosters = [ + train( + {**base_params, "multi_strategy": "multi_output_tree"}, + Xy, + num_boost_round=32, + ), + train( + {**base_params, "multi_strategy": "one_output_per_tree"}, + Xy, + num_boost_round=32, + ), + train( + {**base_params, "multi_strategy": "multi_output_tree"}, + Xy, + num_boost_round=32, + callbacks=[ResetStrategy()], + ), + ] + + def get_normalized_importance(booster: Booster, importance_type: str) -> np.ndarray: + """Get feature importance as normalized array (sums to 1).""" + scores = booster.get_score(importance_type=importance_type) + arr = np.array([scores.get(f"f{i}", 0.0) for i in range(n_features)]) + return arr / arr.sum() if arr.sum() > 0 else arr + + for importance_type in ["weight", "gain", "total_gain", "cover", "total_cover"]: + imps = [get_normalized_importance(b, importance_type) for b in boosters] + + # Check that importances are not exactly the same (different strategies) + assert not np.allclose(imps[0], imps[1]) + assert not np.allclose(imps[0], imps[2]) + + # Check that normalized importances are similar (correlated) + # All strategies should have reasonably similar importance patterns + assert cosine_similarity([imps[0]], [imps[1]])[0, 0] > 0.9 + assert cosine_similarity([imps[0]], [imps[2]])[0, 0] > 0.9 + assert cosine_similarity([imps[1]], [imps[2]])[0, 0] > 0.9 diff --git a/src/gbm/gbtree.h b/src/gbm/gbtree.h index ec39e2748799..739d196769f9 100644 --- a/src/gbm/gbtree.h +++ b/src/gbm/gbtree.h @@ -1,5 +1,5 @@ /** - * Copyright 2014-2025, XGBoost Contributors + * Copyright 2014-2026, XGBoost Contributors * \file gbtree.cc * \brief gradient boosted tree implementation. * \author Tianqi Chen @@ -256,7 +256,7 @@ class GBTree : public GradientBooster { if constexpr (tree::IsScalarTree()) { gain_map[split] += tree.Stat(nidx).loss_chg; } else { - LOG(FATAL) << "gain/total_gain " << MTNotImplemented(); + gain_map[split] += tree.LossChg(nidx); } }); } else if (importance_type == "cover" || importance_type == "total_cover") { @@ -264,7 +264,7 @@ class GBTree : public GradientBooster { if constexpr (tree::IsScalarTree()) { gain_map[split] += tree.Stat(nidx).sum_hess; } else { - LOG(FATAL) << "cover/total_cover " << MTNotImplemented(); + gain_map[split] += tree.SumHess(nidx); } }); } else { diff --git a/src/predictor/gbtree_view.h b/src/predictor/gbtree_view.h index 0d20d1628bfe..cb3910aacffc 100644 --- a/src/predictor/gbtree_view.h +++ b/src/predictor/gbtree_view.h @@ -1,5 +1,5 @@ /** - * Copyright 2025, XGBoost Contributors + * Copyright 2025-2026, XGBoost Contributors */ #pragma once @@ -51,7 +51,7 @@ class GBTreeModelView { for (bst_tree_t tree_idx = this->tree_begin; tree_idx < this->tree_end; ++tree_idx) { auto const& p_tree = model.trees[tree_idx]; if (p_tree->IsMultiTarget()) { - auto tree = tree::MultiTargetTreeView{device, p_tree.get()}; + auto tree = tree::MultiTargetTreeView{device, need_stat, p_tree.get()}; this->n_nodes += tree.Size(); trees.emplace_back(tree); } else { diff --git a/src/tree/gpu_hist/expand_entry.cuh b/src/tree/gpu_hist/expand_entry.cuh index c3ee694c442c..555dabde1e25 100644 --- a/src/tree/gpu_hist/expand_entry.cuh +++ b/src/tree/gpu_hist/expand_entry.cuh @@ -135,9 +135,9 @@ struct MultiExpandEntry { MultiSplitCandidate split; common::Span base_weight; - // Sum Hessian of the first target. Used as a surrogate for node size. - double left_fst_hess{0}; - double right_fst_hess{0}; + // Sum of hessians across all targets for left/right children. + double left_sum{0}; + double right_sum{0}; MultiExpandEntry() = default; @@ -168,9 +168,14 @@ struct MultiExpandEntry { return true; } - __device__ void UpdateFirstHessian(GradientPairPrecise const& lg, GradientPairPrecise const& rg) { - this->left_fst_hess = lg.GetHess(); - this->right_fst_hess = rg.GetHess(); + /** + * @brief Update hessian statistics. + * @param left_hess Sum of hessians across all targets for left child. + * @param right_hess Sum of hessians across all targets for right child. + */ + __device__ void UpdateHessian(double left_hess, double right_hess) { + this->left_sum = left_hess; + this->right_sum = right_hess; } friend std::ostream& operator<<(std::ostream& os, MultiExpandEntry const& entry); diff --git a/src/tree/gpu_hist/multi_evaluate_splits.cu b/src/tree/gpu_hist/multi_evaluate_splits.cu index 1f8dbab4d9a1..7a19215f7343 100644 --- a/src/tree/gpu_hist/multi_evaluate_splits.cu +++ b/src/tree/gpu_hist/multi_evaluate_splits.cu @@ -384,7 +384,7 @@ void MultiHistEvaluator::EvaluateSplits(Context const *ctx, bool l = true, r = true; float parent_gain = 0; - GradientPairPrecise lg_fst, rg_fst; + double left_hess = 0, right_hess = 0; // Sum of child hessians across all targets auto eta = shared_inputs.param.learning_rate; for (bst_target_t t = 0; t < n_targets; ++t) { @@ -417,16 +417,14 @@ void MultiHistEvaluator::EvaluateSplits(Context const *ctx, left_weight[t] = CalcWeight(shared_inputs.param, lg.GetGrad(), lg.GetHess()) * eta; } - if (t == 0) { - lg_fst = lg; - rg_fst = rg; - } + left_hess += lg.GetHess(); + right_hess += rg.GetHess(); } // Set up the output entry with spans pointing to persistent weight storage out_splits[nidx_in_set] = {nidx, input.depth, best_split, base_weight}; out_splits[nidx_in_set].split.loss_chg -= parent_gain; - out_splits[nidx_in_set].UpdateFirstHessian(lg_fst, rg_fst); + out_splits[nidx_in_set].UpdateHessian(left_hess, right_hess); if (l || r) { out_splits[nidx_in_set].split.loss_chg = -std::numeric_limits::max(); @@ -438,7 +436,7 @@ void MultiHistEvaluator::ApplyTreeSplit(Context const *ctx, RegTree const *p_tre common::Span d_candidates, bst_target_t n_targets) { // Assign the node sums here, for the next evaluate split call. - auto mt_tree = MultiTargetTreeView{ctx->Device(), p_tree}; + auto mt_tree = MultiTargetTreeView{ctx->Device(), false, p_tree}; auto max_in_it = dh::MakeIndexTransformIter([=] __device__(std::size_t i) -> bst_node_t { return std::max(mt_tree.LeftChild(d_candidates[i].nidx), mt_tree.RightChild(d_candidates[i].nidx)); diff --git a/src/tree/hist/evaluate_splits.h b/src/tree/hist/evaluate_splits.h index bb1f9ffa5260..de89dfffe8e1 100644 --- a/src/tree/hist/evaluate_splits.h +++ b/src/tree/hist/evaluate_splits.h @@ -681,8 +681,19 @@ class HistMultiEvaluator { linalg::MakeVec(candidate.split.right_sum.data(), candidate.split.right_sum.size()); CalcWeight(*param_, right_sum, param_->learning_rate, right_weight); + // Compute the loss_chg and sum hessians for parent and children + float loss_chg = candidate.split.loss_chg; + // Sum hessians across all targets for each child + float left_sum_hess = 0.0f, right_sum_hess = 0.0f; + for (std::size_t t = 0; t < candidate.split.left_sum.size(); ++t) { + left_sum_hess += candidate.split.left_sum[t].GetHess(); + right_sum_hess += candidate.split.right_sum[t].GetHess(); + } + float sum_hess = left_sum_hess + right_sum_hess; + p_tree->ExpandNode(candidate.nid, candidate.split.SplitIndex(), candidate.split.split_value, - candidate.split.DefaultLeft(), base_weight, left_weight, right_weight); + candidate.split.DefaultLeft(), base_weight, left_weight, right_weight, + loss_chg, sum_hess, left_sum_hess, right_sum_hess); CHECK(p_tree->IsMultiTarget()); auto mt_tree = p_tree->HostMtView(); diff --git a/src/tree/multi_target_tree_model.cc b/src/tree/multi_target_tree_model.cc index 9197d079907a..21642ac9854b 100644 --- a/src/tree/multi_target_tree_model.cc +++ b/src/tree/multi_target_tree_model.cc @@ -27,7 +27,9 @@ MultiTargetTree::MultiTargetTree(TreeParam const* param) parent_(1ul, InvalidNodeId()), split_index_(1ul, 0), default_left_(1ul, 0), - split_conds_(1ul, DftBadValue()) { + split_conds_(1ul, DftBadValue()), + loss_chg_(1ul, 0.0f), + sum_hess_(1ul, 0.0f) { CHECK_GT(param_->size_leaf_vector, 1); } @@ -40,7 +42,9 @@ MultiTargetTree::MultiTargetTree(MultiTargetTree const& that) default_left_(that.default_left_.Size(), 0, that.default_left_.Device()), split_conds_(that.split_conds_.Size(), 0.0f, that.split_conds_.Device()), weights_(that.weights_.Size(), 0.0f, that.weights_.Device()), - leaf_weights_(that.leaf_weights_.Size(), 0.0f, that.leaf_weights_.Device()) { + leaf_weights_(that.leaf_weights_.Size(), 0.0f, that.leaf_weights_.Device()), + loss_chg_(that.loss_chg_.Size(), 0.0f, that.loss_chg_.Device()), + sum_hess_(that.sum_hess_.Size(), 0.0f, that.sum_hess_.Device()) { this->left_.Copy(that.left_); this->right_.Copy(that.right_); this->parent_.Copy(that.parent_); @@ -49,9 +53,11 @@ MultiTargetTree::MultiTargetTree(MultiTargetTree const& that) this->split_conds_.Copy(that.split_conds_); this->weights_.Copy(that.weights_); this->leaf_weights_.Copy(that.leaf_weights_); + this->loss_chg_.Copy(that.loss_chg_); + this->sum_hess_.Copy(that.sum_hess_); } -void MultiTargetTree::SetRoot(linalg::VectorView weight) { +void MultiTargetTree::SetRoot(linalg::VectorView weight, float sum_hess) { CHECK(!weight.Empty()); auto const next_nidx = RegTree::kRoot + 1; @@ -73,6 +79,11 @@ void MultiTargetTree::SetRoot(linalg::VectorView weight) { } } + // Set root statistics + sum_hess_.Resize(next_nidx, 0.0f); + sum_hess_.HostVector()[RegTree::kRoot] = sum_hess; + loss_chg_.Resize(next_nidx, 0.0f); + CHECK_EQ(this->param_->num_nodes, 1); CHECK_EQ(this->NumSplitTargets(), weight.Size()); } @@ -80,7 +91,8 @@ void MultiTargetTree::SetRoot(linalg::VectorView weight) { void MultiTargetTree::Expand(bst_node_t nidx, bst_feature_t split_idx, float split_cond, bool default_left, linalg::VectorView base_weight, linalg::VectorView left_weight, - linalg::VectorView right_weight) { + linalg::VectorView right_weight, float loss_chg, + float sum_hess, float left_sum, float right_sum) { CHECK(this->IsLeaf(nidx)); CHECK_GE(parent_.Size(), 1); CHECK_EQ(parent_.Size(), left_.Size()); @@ -135,6 +147,15 @@ void MultiTargetTree::Expand(bst_node_t nidx, bst_feature_t split_idx, float spl l_weight(i) = left_weight(i); r_weight(i) = right_weight(i); } + + loss_chg_.Resize(n, 0.0f); + loss_chg_.HostVector()[nidx] = loss_chg; + + sum_hess_.Resize(n, 0.0f); + auto& h_hess = sum_hess_.HostVector(); + h_hess[nidx] = sum_hess; + h_hess[left_child] = left_sum; + h_hess[right_child] = right_sum; } void MultiTargetTree::SetLeaves(std::vector leaves, common::Span weights) { @@ -191,7 +212,8 @@ void LoadModelImpl(Json const& in, HostDeviceVector* p_weights, HostDeviceVector* p_leaf_weights, HostDeviceVector* p_lefts, HostDeviceVector* p_rights, HostDeviceVector* p_parents, HostDeviceVector* p_conds, HostDeviceVector* p_fidx, - HostDeviceVector* p_dft_left) { + HostDeviceVector* p_dft_left, HostDeviceVector* p_gain, + HostDeviceVector* p_sum_hess) { namespace tf = tree_field; auto get_float = [&](std::string_view name, HostDeviceVector* p_out) { @@ -232,6 +254,10 @@ void LoadModelImpl(Json const& in, HostDeviceVector* p_weights, for (std::size_t i = 0; i < dft_left.size(); ++i) { out_dft_l[i] = GetElem(dft_left, i); } + + // Load statistics + get_float(tf::kLossChg, p_gain); + get_float(tf::kSumHess, p_sum_hess); } void MultiTargetTree::LoadModel(Json const& in) { @@ -241,16 +267,19 @@ void MultiTargetTree::LoadModel(Json const& in) { if (typed && feature_is_64) { LoadModelImpl(in, &weights_, &leaf_weights_, &left_, &right_, &parent_, - &split_conds_, &split_index_, &default_left_); + &split_conds_, &split_index_, &default_left_, &loss_chg_, &sum_hess_); } else if (typed && !feature_is_64) { LoadModelImpl(in, &weights_, &leaf_weights_, &left_, &right_, &parent_, - &split_conds_, &split_index_, &default_left_); + &split_conds_, &split_index_, &default_left_, &loss_chg_, + &sum_hess_); } else if (!typed && feature_is_64) { LoadModelImpl(in, &weights_, &leaf_weights_, &left_, &right_, &parent_, - &split_conds_, &split_index_, &default_left_); + &split_conds_, &split_index_, &default_left_, &loss_chg_, + &sum_hess_); } else { LoadModelImpl(in, &weights_, &leaf_weights_, &left_, &right_, &parent_, - &split_conds_, &split_index_, &default_left_); + &split_conds_, &split_index_, &default_left_, &loss_chg_, + &sum_hess_); } } @@ -267,6 +296,8 @@ void MultiTargetTree::SaveModel(Json* p_out) const { F32Array conds(n_nodes); U8Array default_left(n_nodes); F32Array weights(this->weights_.Size()); + F32Array loss_chg(n_nodes); + F32Array sum_hess(n_nodes); auto n_leaves = this->NumLeaves(); CHECK_GE(n_leaves, 1); @@ -278,6 +309,9 @@ void MultiTargetTree::SaveModel(Json* p_out) const { auto const& h_split_index = this->split_index_.ConstHostVector(); auto const& h_split_conds = this->split_conds_.ConstHostVector(); auto const& h_default_left = this->default_left_.ConstHostVector(); + auto const& h_loss_chg = this->loss_chg_.ConstHostVector(); + auto const& h_sum_hess = this->sum_hess_.ConstHostVector(); + auto save_tree = [&](auto* p_indices_array) { auto& indices_array = *p_indices_array; for (bst_node_t nidx = 0; nidx < n_nodes; ++nidx) { @@ -291,6 +325,8 @@ void MultiTargetTree::SaveModel(Json* p_out) const { indices_array.Set(nidx, h_split_index[nidx]); conds.Set(nidx, h_split_conds[nidx]); default_left.Set(nidx, h_default_left[nidx]); + loss_chg.Set(nidx, h_loss_chg[nidx]); + sum_hess.Set(nidx, h_sum_hess[nidx]); // Save internal weights auto in_weight = this->NodeWeight(nidx); @@ -332,6 +368,8 @@ void MultiTargetTree::SaveModel(Json* p_out) const { out[tf::kSplitCond] = std::move(conds); out[tf::kDftLeft] = std::move(default_left); + out[tf::kLossChg] = std::move(loss_chg); + out[tf::kSumHess] = std::move(sum_hess); } [[nodiscard]] bst_target_t MultiTargetTree::NumTargets() const { return param_->size_leaf_vector; } @@ -358,6 +396,8 @@ void MultiTargetTree::SaveModel(Json* p_out) const { n_bytes += split_conds_.SizeBytes(); n_bytes += weights_.SizeBytes(); n_bytes += leaf_weights_.SizeBytes(); + n_bytes += loss_chg_.SizeBytes(); + n_bytes += sum_hess_.SizeBytes(); return n_bytes; } } // namespace xgboost diff --git a/src/tree/tree_model.cc b/src/tree/tree_model.cc index d8a5e354c94a..f65f462176eb 100644 --- a/src/tree/tree_model.cc +++ b/src/tree/tree_model.cc @@ -1,5 +1,5 @@ /** - * Copyright 2015-2025, XGBoost Contributors + * Copyright 2015-2026, XGBoost Contributors * \file tree_model.cc * \brief model structure for tree */ @@ -876,14 +876,15 @@ void RegTree::ExpandNode(bst_node_t nid, unsigned split_index, bst_float split_v void RegTree::ExpandNode(bst_node_t nidx, bst_feature_t split_index, float split_cond, bool default_left, linalg::VectorView base_weight, linalg::VectorView left_weight, - linalg::VectorView right_weight) { + linalg::VectorView right_weight, float loss_chg, + float sum_hess, float left_sum, float right_sum) { CHECK(IsMultiTarget()); CHECK_LT(split_index, this->param_.num_feature); CHECK(this->p_mt_tree_); CHECK_GT(param_.size_leaf_vector, 1); this->p_mt_tree_->Expand(nidx, split_index, split_cond, default_left, base_weight, left_weight, - right_weight); + right_weight, loss_chg, sum_hess, left_sum, right_sum); split_types_.HostVector().resize(this->Size(), FeatureType::kNumerical); split_categories_segments_.HostVector().resize(this->Size()); diff --git a/src/tree/tree_view.cc b/src/tree/tree_view.cc index 91b891f998f5..9ca594294355 100644 --- a/src/tree/tree_view.cc +++ b/src/tree/tree_view.cc @@ -1,5 +1,5 @@ /** - * Copyright 2025, XGBoost Contributors + * Copyright 2025-2026, XGBoost Contributors */ #include "tree_view.h" @@ -39,7 +39,7 @@ ScalarTreeView::ScalarTreeView(DeviceOrd device, bool need_stat, RegTree const* CHECK(!tree->IsMultiTarget()); } -MultiTargetTreeView::MultiTargetTreeView(DeviceOrd device, RegTree const* tree) +MultiTargetTreeView::MultiTargetTreeView(DeviceOrd device, bool need_stat, RegTree const* tree) : CategoriesMixIn{tree->GetCategoriesMatrix(device)}, left{DispatchPtr(device, tree->GetMultiTargetTree()->left_)}, right{DispatchPtr(device, tree->GetMultiTargetTree()->right_)}, @@ -48,8 +48,11 @@ MultiTargetTreeView::MultiTargetTreeView(DeviceOrd device, RegTree const* tree) default_left{DispatchPtr(device, tree->GetMultiTargetTree()->default_left_)}, split_conds{DispatchPtr(device, tree->GetMultiTargetTree()->split_conds_)}, n{tree->NumNodes()}, - leaf_weights{DispatchWeight(device, tree)} {} + leaf_weights{DispatchWeight(device, tree)}, + loss_chg{need_stat ? DispatchPtr(device, tree->GetMultiTargetTree()->loss_chg_) : nullptr}, + sum_hess{need_stat ? DispatchPtr(device, tree->GetMultiTargetTree()->sum_hess_) : nullptr} {} MultiTargetTreeView::MultiTargetTreeView(RegTree const* tree) - : MultiTargetTreeView{DeviceOrd::CPU(), tree} {} + : MultiTargetTreeView{DeviceOrd::CPU(), true, tree} {} + } // namespace xgboost::tree diff --git a/src/tree/tree_view.h b/src/tree/tree_view.h index bf5dba4320e0..6b333bcccb19 100644 --- a/src/tree/tree_view.h +++ b/src/tree/tree_view.h @@ -185,6 +185,10 @@ struct MultiTargetTreeView : public WalkTreeMixIn, public C linalg::MatrixView leaf_weights; + // Statistics + float const* loss_chg{nullptr}; + float const* sum_hess{nullptr}; + [[nodiscard]] XGBOOST_DEVICE bool IsLeaf(bst_node_t nidx) const { return left[nidx] == InvalidNodeId(); } @@ -216,16 +220,16 @@ struct MultiTargetTreeView : public WalkTreeMixIn, public C [[nodiscard]] bst_node_t Size() const { return this->n; } [[nodiscard]] XGBOOST_DEVICE bool IsRoot(bst_node_t nidx) const { return nidx == RegTree::kRoot; } - [[nodiscard]] auto SumHess(bst_node_t) const { - LOG(FATAL) << "Tree statistic " << MTNotImplemented(); - return linalg::MakeVec(nullptr, 0); - } - [[nodiscard]] auto LossChg(bst_node_t) const { - LOG(FATAL) << "Tree statistic " << MTNotImplemented(); - return 0.0f; - } - /** @brief Create a device view */ - explicit MultiTargetTreeView(DeviceOrd device, RegTree const* tree); + // These methods require need_stat=true when constructing the view. + // Will crash with nullptr dereference if stats were not loaded. + [[nodiscard]] float SumHess(bst_node_t nidx) const { return sum_hess[nidx]; } + [[nodiscard]] float LossChg(bst_node_t nidx) const { return loss_chg[nidx]; } + /** + * @brief Create a device view + * + * @param need_stat We can skip the stat when performing normal inference. + */ + explicit MultiTargetTreeView(DeviceOrd device, bool need_stat, RegTree const* tree); /** @brief Create a host view */ explicit MultiTargetTreeView(RegTree const* tree); }; diff --git a/src/tree/updater_gpu_hist.cuh b/src/tree/updater_gpu_hist.cuh index 591e7f379e89..c30d1a40bfe0 100644 --- a/src/tree/updater_gpu_hist.cuh +++ b/src/tree/updater_gpu_hist.cuh @@ -245,7 +245,10 @@ class MultiTargetHistMaker { auto shared_inputs = MakeSharedInputs(static_cast(feature_set.size())); auto entry = this->evaluator_.EvaluateSingleSplit(ctx_, input, shared_inputs); auto weights = this->evaluator_.GetNodeWeights(n_targets); - p_tree->SetRoot(linalg::MakeVec(this->ctx_->Device(), weights.Base(RegTree::kRoot))); + // Root's sum_hess is the sum of left and right child hessians + float root_sum_hess = static_cast(entry.left_sum + entry.right_sum); + p_tree->SetRoot(linalg::MakeVec(this->ctx_->Device(), weights.Base(RegTree::kRoot)), + root_sum_hess); return entry; } @@ -263,9 +266,15 @@ class MultiTargetHistMaker { std::vector h_base_weight, h_left_weight, h_right_weight; this->evaluator_.CopyNodeWeightsToHost(candidate.nidx, n_targets, &h_base_weight, &h_left_weight, &h_right_weight); + // Get loss_chg from the split, and sum hessians for parent and children + float loss_chg = candidate.split.loss_chg; + float left_sum = static_cast(candidate.left_sum); + float right_sum = static_cast(candidate.right_sum); + float sum_hess = left_sum + right_sum; p_tree->ExpandNode(candidate.nidx, candidate.split.findex, candidate.split.fvalue, candidate.split.dir == kLeftDir, linalg::MakeVec(h_base_weight), - linalg::MakeVec(h_left_weight), linalg::MakeVec(h_right_weight)); + linalg::MakeVec(h_left_weight), linalg::MakeVec(h_right_weight), loss_chg, + sum_hess, left_sum, right_sum); } dh::device_vector candidates{h_candidates}; @@ -427,7 +436,7 @@ class MultiTargetHistMaker { std::vector subtraction_nidx(candidates.size()); auto mt_tree = p_tree->HostMtView(); AssignNodes(mt_tree, candidates, build_nidx, subtraction_nidx, [](MultiExpandEntry const& e) { - bool fewer_right = e.right_fst_hess < e.left_fst_hess; + bool fewer_right = e.right_sum < e.left_sum; return fewer_right; }); @@ -436,8 +445,8 @@ class MultiTargetHistMaker { histogram_.AllocateHistograms(this->ctx_, build_nidx, subtraction_nidx); - // Pull to device - mt_tree = MultiTargetTreeView{this->ctx_->Device(), p_tree}; + // Pull to device (stats not needed for partitioning) + mt_tree = MultiTargetTreeView{this->ctx_->Device(), false, p_tree}; std::int32_t k{0}; for (auto const& page : @@ -559,7 +568,7 @@ class MultiTargetHistMaker { CHECK_EQ(out_position.size(), 1); auto d_position = out_position.front().ConstDeviceSpan(); CHECK_EQ(out_preds_d.Shape(0), d_position.size()); - auto mt_tree = MultiTargetTreeView{this->ctx_->Device(), p_tree}; + auto mt_tree = MultiTargetTreeView{this->ctx_->Device(), false, p_tree}; thrust::for_each_n(this->ctx_->CUDACtx()->CTP(), thrust::make_counting_iterator(0ul), out_preds_d.Size(), [=] XGBOOST_DEVICE(std::size_t i) mutable { auto [sample_idx, target_idx] = diff --git a/src/tree/updater_quantile_hist.cc b/src/tree/updater_quantile_hist.cc index e223de43156f..933deea2b3ef 100644 --- a/src/tree/updater_quantile_hist.cc +++ b/src/tree/updater_quantile_hist.cc @@ -258,7 +258,12 @@ class MultiTargetHistBuilder { std::transform(linalg::cbegin(weight_t), linalg::cend(weight_t), linalg::begin(weight_t), [&](float w) { return w * param_->learning_rate; }); - p_tree->SetRoot(weight_t); + // Compute root sum_hess by summing hessians across all targets + float root_sum_hess = 0.0f; + for (bst_target_t t{0}; t < n_targets; ++t) { + root_sum_hess += static_cast(root_sum(t).GetHess()); + } + p_tree->SetRoot(weight_t, root_sum_hess); std::vector hists; std::vector nodes{{RegTree::kRoot, 0}}; diff --git a/tests/cpp/predictor/test_predictor.cc b/tests/cpp/predictor/test_predictor.cc index 946959182737..ad07a400e711 100644 --- a/tests/cpp/predictor/test_predictor.cc +++ b/tests/cpp/predictor/test_predictor.cc @@ -1,5 +1,5 @@ /** - * Copyright 2020-2025, XGBoost Contributors + * Copyright 2020-2026, XGBoost Contributors */ #include "test_predictor.h" @@ -749,10 +749,11 @@ void TestVectorLeafPrediction(Context const *ctx) { std::vector r_w(mparam.LeafLength(), 2.0f); auto &tree = trees.front(); - tree->SetRoot(linalg::MakeVec(p_w.data(), p_w.size())); + tree->SetRoot(linalg::MakeVec(p_w.data(), p_w.size()), /*sum_hess=*/1.0f); tree->ExpandNode(0, static_cast(1), 2.0, true, linalg::MakeVec(p_w.data(), p_w.size()), linalg::MakeVec(l_w.data(), l_w.size()), - linalg::MakeVec(r_w.data(), r_w.size())); + linalg::MakeVec(r_w.data(), r_w.size()), /*loss_chg=*/0.5f, /*sum_hess=*/1.0f, + /*left_sum=*/0.6f, /*right_sum=*/0.4f); tree->GetMultiTargetTree()->SetLeaves(); ASSERT_TRUE(tree->IsMultiTarget()); ASSERT_TRUE(mparam.IsVectorLeaf()); diff --git a/tests/cpp/tree/hist/test_evaluate_splits.cc b/tests/cpp/tree/hist/test_evaluate_splits.cc index a5858bb3e890..e3123058c38d 100644 --- a/tests/cpp/tree/hist/test_evaluate_splits.cc +++ b/tests/cpp/tree/hist/test_evaluate_splits.cc @@ -1,5 +1,5 @@ /** - * Copyright 2021-2024, XGBoost Contributors + * Copyright 2021-2026, XGBoost Contributors */ #include "../test_evaluate_splits.h" @@ -204,7 +204,12 @@ TEST(HistMultiEvaluator, Evaluate) { RegTree tree{n_targets, n_features}; auto weight = evaluator.InitRoot(root_sum.HostView()); - tree.SetRoot(weight.HostView()); + // Compute root sum_hess by summing hessians across all targets + float root_sum_hess = 0.0f; + for (bst_target_t t{0}; t < n_targets; ++t) { + root_sum_hess += static_cast(root_sum.HostView()(t).GetHess()); + } + tree.SetRoot(weight.HostView(), root_sum_hess); auto w = weight.HostView(); ASSERT_EQ(w.Size(), n_targets); ASSERT_EQ(w(0), -1.5); diff --git a/tests/cpp/tree/test_multi_target_tree_model.cc b/tests/cpp/tree/test_multi_target_tree_model.cc index 816b1d1af58d..88118ca31136 100644 --- a/tests/cpp/tree/test_multi_target_tree_model.cc +++ b/tests/cpp/tree/test_multi_target_tree_model.cc @@ -32,7 +32,7 @@ std::unique_ptr MakeMtTreeForTest(bst_target_t n_targets) { base_weight.ModifyInplace([&](HostDeviceVector* data, common::Span shape) { iota_weights(1.0f, data, shape); }); - tree->SetRoot(base_weight.HostView()); + tree->SetRoot(base_weight.HostView(), /*sum_hess=*/1.0f); linalg::Vector left_weight; left_weight.ModifyInplace([&](HostDeviceVector* data, common::Span shape) { @@ -44,7 +44,8 @@ std::unique_ptr MakeMtTreeForTest(bst_target_t n_targets) { }); tree->ExpandNode(RegTree::kRoot, /*split_idx=*/1, 0.5f, true, base_weight.HostView(), - left_weight.HostView(), right_weight.HostView()); + left_weight.HostView(), right_weight.HostView(), /*loss_chg=*/0.5f, + /*sum_hess=*/1.0f, /*left_sum=*/0.6f, /*right_sum=*/0.4f); tree->GetMultiTargetTree()->SetLeaves(); return tree; } @@ -111,9 +112,10 @@ void TestTreeDump(std::string format, std::string leaf_key) { bst_target_t n_targets{4}; RegTree tree{n_targets, n_features}; linalg::Vector weight{{1.0f, 2.0f, 3.0f, 4.0f}, {4ul}, DeviceOrd::CPU()}; - tree.SetRoot(weight.HostView()); + tree.SetRoot(weight.HostView(), /*sum_hess=*/1.0f); tree.ExpandNode(RegTree::kRoot, /*split_idx=*/1, 0.5f, true, weight.HostView(), - weight.HostView(), weight.HostView()); + weight.HostView(), weight.HostView(), /*loss_chg=*/0.5f, /*sum_hess=*/1.0f, + /*left_sum=*/0.6f, /*right_sum=*/0.4f); tree.GetMultiTargetTree()->SetLeaves(); auto str = tree.DumpModel(fmap, false, format); ASSERT_NE(str.find(leaf_key + "[1, 2, ..., 4]"), std::string::npos); @@ -143,13 +145,14 @@ TEST(MultiTargetTree, SetLeaves) { CHECK(tree->IsMultiTarget()); // Reduce to 2 targets linalg::Vector base_weight{{1.0f, 2.0f}, {2ul}, DeviceOrd::CPU()}; - tree->SetRoot(base_weight.HostView()); + tree->SetRoot(base_weight.HostView(), /*sum_hess=*/1.0f); ASSERT_EQ(tree->GetMultiTargetTree()->NumSplitTargets(), 2); linalg::Vector left_weight{{2.0f, 3.0f}, {2ul}, DeviceOrd::CPU()}; linalg::Vector right_weight{{3.0f, 4.0f}, {2ul}, DeviceOrd::CPU()}; tree->ExpandNode(RegTree::kRoot, /*split_idx=*/1, 0.5f, true, base_weight.HostView(), - left_weight.HostView(), right_weight.HostView()); + left_weight.HostView(), right_weight.HostView(), /*loss_chg=*/0.5f, + /*sum_hess=*/1.0f, /*left_sum=*/0.6f, /*right_sum=*/0.4f); std::vector leaf_weights(n_targets * 2); std::iota(leaf_weights.begin(), leaf_weights.end(), 0); @@ -175,4 +178,40 @@ TEST(MultiTargetTree, SetLeaves) { ASSERT_EQ(right.Values()[i], i + left.Size()); } } + +TEST(MultiTargetTree, Statistics) { + // Test that gain and sum_hess are serialized and deserialized correctly + auto tree = MakeMtTreeForTest(3); + // Following values are defined by the `MakeMtTreeForTest. + auto view = tree->HostMtView(); + // Gain and sum_hess stored at the parent (split node) + ASSERT_FLOAT_EQ(view.LossChg(0), 0.5f); + ASSERT_FLOAT_EQ(view.SumHess(0), 1.0f); + // Child nodes have their sum_hess values + ASSERT_FLOAT_EQ(view.LossChg(1), 0.0f); // Leaves have no gain + ASSERT_FLOAT_EQ(view.SumHess(1), 0.6f); // Left child + ASSERT_FLOAT_EQ(view.LossChg(2), 0.0f); + ASSERT_FLOAT_EQ(view.SumHess(2), 0.4f); // Right child + + // Test serialization round-trip + Json jtree{Object{}}; + tree->SaveModel(&jtree); + + // Check that statistics are in the JSON + auto const& obj = get(jtree); + ASSERT_TRUE(obj.find("loss_changes") != obj.end()); + ASSERT_TRUE(obj.find("sum_hessian") != obj.end()); + auto const& gains = get(jtree["loss_changes"]); + ASSERT_EQ(gains.size(), tree->NumNodes()); + ASSERT_FLOAT_EQ(gains[0], 0.5f); + + // Load and verify statistics are preserved + RegTree loaded; + loaded.LoadModel(jtree); + auto loaded_view = loaded.HostMtView(); + ASSERT_FLOAT_EQ(loaded_view.LossChg(0), 0.5f); + ASSERT_FLOAT_EQ(loaded_view.SumHess(0), 1.0f); + ASSERT_FLOAT_EQ(loaded_view.SumHess(1), 0.6f); + ASSERT_FLOAT_EQ(loaded_view.SumHess(2), 0.4f); +} } // namespace xgboost diff --git a/tests/cpp/tree/test_partitioner.h b/tests/cpp/tree/test_partitioner.h index 80256c0e3b3c..4afeb540670d 100644 --- a/tests/cpp/tree/test_partitioner.h +++ b/tests/cpp/tree/test_partitioner.h @@ -1,5 +1,5 @@ /** - * Copyright 2021-2023 by XGBoost contributors. + * Copyright 2021-2026 by XGBoost contributors. */ #ifndef XGBOOST_TESTS_CPP_TREE_TEST_PARTITIONER_H_ #define XGBOOST_TESTS_CPP_TREE_TEST_PARTITIONER_H_ @@ -33,10 +33,11 @@ inline void GetMultiSplitForTest(RegTree *tree, float split_value, linalg::Vector base_weight{linalg::Constant(&ctx, 0.0f, n_targets)}; linalg::Vector left_weight{linalg::Constant(&ctx, 0.0f, n_targets)}; linalg::Vector right_weight{linalg::Constant(&ctx, 0.0f, n_targets)}; - tree->SetRoot(base_weight.HostView()); + tree->SetRoot(base_weight.HostView(), /*sum_hess=*/0.0f); tree->ExpandNode(/*nidx=*/RegTree::kRoot, /*split_index=*/0, /*split_value=*/split_value, /*default_left=*/true, base_weight.HostView(), left_weight.HostView(), - right_weight.HostView()); + right_weight.HostView(), /*loss_chg=*/0.0f, /*sum_hess=*/0.0f, /*left_sum=*/0.0f, + /*right_sum=*/0.0f); candidates->front().split.split_value = split_value; candidates->front().split.sindex = 0; candidates->front().split.sindex |= (1U << 31); diff --git a/tests/python-gpu/test_gpu_multi_target.py b/tests/python-gpu/test_gpu_multi_target.py index 9dde1c1b716b..31d786aa78ad 100644 --- a/tests/python-gpu/test_gpu_multi_target.py +++ b/tests/python-gpu/test_gpu_multi_target.py @@ -10,7 +10,9 @@ run_column_sampling, run_deterministic, run_eta, + run_feature_importance_strategy_compare, run_grow_policy, + run_mixed_strategy, run_multiclass, run_multilabel, run_quantile_loss, @@ -73,6 +75,14 @@ def test_grow_policy(grow_policy: str) -> None: run_grow_policy("cuda", grow_policy) +def test_mixed_strategy() -> None: + run_mixed_strategy("cuda") + + +def test_feature_importance_strategy_compare() -> None: + run_feature_importance_strategy_compare("cuda") + + @given(hist_parameter_strategy, strategies.integers(1, 20), tm.multi_dataset_strategy) @settings(deadline=None, max_examples=50, print_blob=True) def test_hist(param: Dict[str, Any], num_rounds: int, dataset: tm.TestDataset) -> None: diff --git a/tests/python/test_basic_models.py b/tests/python/test_basic_models.py index 5f4b616c9a40..88d275a6d2ee 100644 --- a/tests/python/test_basic_models.py +++ b/tests/python/test_basic_models.py @@ -9,7 +9,7 @@ from xgboost import testing as tm from xgboost.core import Integer from xgboost.testing.basic_models import run_custom_objective -from xgboost.testing.updater import ResetStrategy, get_basescore +from xgboost.testing.updater import get_basescore class TestModels: @@ -414,41 +414,6 @@ def test_slice(self, booster_name: str) -> None: booster, dtrain, num_parallel_tree, num_classes, num_boost_round, False ) - def test_slice_multi(self) -> None: - from sklearn.datasets import make_classification - - num_classes = 3 - X, y = make_classification( - n_samples=1000, n_informative=5, n_classes=num_classes - ) - Xy = xgb.DMatrix(data=X, label=y) - num_parallel_tree = 4 - num_boost_round = 16 - - booster = xgb.train( - { - "num_parallel_tree": num_parallel_tree, - "num_class": num_classes, - "booster": "gbtree", - "objective": "multi:softprob", - "multi_strategy": "multi_output_tree", - "tree_method": "hist", - "base_score": 0, - }, - num_boost_round=num_boost_round, - dtrain=Xy, - callbacks=[ResetStrategy()], - ) - sliced = [t for t in booster] - assert len(sliced) == 16 - - predt0 = booster.predict(Xy, output_margin=True) - predt1 = np.zeros(predt0.shape) - for t in booster: - predt1 += t.predict(Xy, output_margin=True) - - np.testing.assert_allclose(predt0, predt1, atol=1e-5) - @pytest.mark.skipif(**tm.no_pandas()) @pytest.mark.parametrize("ext", ["json", "ubj"]) def test_feature_info(self, ext: str) -> None: diff --git a/tests/python/test_multi_target.py b/tests/python/test_multi_target.py index 6167ee1bba18..457968535a03 100644 --- a/tests/python/test_multi_target.py +++ b/tests/python/test_multi_target.py @@ -6,7 +6,10 @@ from xgboost import testing as tm from xgboost.testing.multi_target import ( run_absolute_error, + run_column_sampling, + run_feature_importance_strategy_compare, run_grow_policy, + run_mixed_strategy, run_multiclass, run_multilabel, run_quantile_loss, @@ -121,3 +124,15 @@ def test_with_iter() -> None: @pytest.mark.parametrize("grow_policy", ["depthwise", "lossguide"]) def test_grow_policy(grow_policy: str) -> None: run_grow_policy("cpu", grow_policy) + + +def test_column_sampling() -> None: + run_column_sampling("cpu") + + +def test_mixed_strategy() -> None: + run_mixed_strategy("cpu") + + +def test_feature_importance_strategy_compare() -> None: + run_feature_importance_strategy_compare("cpu") diff --git a/tests/python/test_with_sklearn.py b/tests/python/test_with_sklearn.py index 3ec8849b9033..936d22c0200e 100644 --- a/tests/python/test_with_sklearn.py +++ b/tests/python/test_with_sklearn.py @@ -344,36 +344,6 @@ def test_feature_importances_weight(): cls.feature_importances_ -def test_feature_importances_weight_vector_leaf() -> None: - from sklearn.datasets import make_multilabel_classification - - X, y = make_multilabel_classification(random_state=1994) - with pytest.raises(ValueError, match="gain/total_gain"): - clf = xgb.XGBClassifier(multi_strategy="multi_output_tree") - clf.fit(X, y) - clf.feature_importances_ - - with pytest.raises(ValueError, match="cover/total_cover"): - clf = xgb.XGBClassifier( - multi_strategy="multi_output_tree", importance_type="cover" - ) - clf.fit(X, y) - clf.feature_importances_ - - clf = xgb.XGBClassifier( - multi_strategy="multi_output_tree", - importance_type="weight", - colsample_bynode=0.2, - ) - clf.fit(X, y, feature_weights=np.arange(0, X.shape[1])) - fi = clf.feature_importances_ - assert fi[0] == 0.0 - assert fi[-1] > fi[1] * 5 - - w = np.polynomial.Polynomial.fit(np.arange(0, X.shape[1]), fi, deg=1) - assert w.coef[1] > 0.03 - - @pytest.mark.skipif(**tm.no_pandas()) def test_feature_importances_gain(): from sklearn.datasets import load_digits