Skip to content

Commit e14c3b9

Browse files
authored
Optional normalization for learning to rank. (dmlc#10094)
1 parent bc51619 commit e14c3b9

File tree

8 files changed

+44
-5
lines changed

8 files changed

+44
-5
lines changed

doc/parameter.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,11 @@ These are parameters specific to learning to rank task. See :doc:`Learning to Ra
500500

501501
It specifies the number of pairs sampled for each document when pair method is ``mean``, or the truncation level for queries when the pair method is ``topk``. For example, to train with ``ndcg@6``, set ``lambdarank_num_pair_per_sample`` to :math:`6` and ``lambdarank_pair_method`` to ``topk``.
502502

503-
* ``lambdarank_unbiased`` [default = ``false``]
503+
* ``lambdarank_normalization`` [default = ``true``]
504+
505+
Whether to normalize the leaf value by lambda gradient. This can sometimes stagnate the training progress.
506+
507+
* ``lambdarank_unbiased`` [default = ``false``]
504508

505509
Specify whether do we need to debias input click data.
506510

doc/tutorials/learning_to_rank.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ Notice that the samples are sorted based on their query index in a non-decreasin
4848
import xgboost as xgb
4949
5050
# Make a synthetic ranking dataset for demonstration
51-
seed = 1994
51+
seed = 1994
5252
X, y = make_classification(random_state=seed)
5353
rng = np.random.default_rng(seed)
5454
n_query_groups = 3
@@ -146,7 +146,8 @@ The consideration of effective pairs also applies to the choice of pair method (
146146

147147
When using the mean strategy for generating pairs, where the target metric (like ``NDCG``) is computed over the whole query list, users can specify how many pairs should be generated per each document, by setting the ``lambdarank_num_pair_per_sample``. XGBoost will randomly sample ``lambdarank_num_pair_per_sample`` pairs for each element in the query group (:math:`|pairs| = |query| \times num\_pairsample`). Often, setting it to 1 can produce reasonable results. In cases where performance is inadequate due to insufficient number of effective pairs being generated, set ``lambdarank_num_pair_per_sample`` to a higher value. As more document pairs are generated, more effective pairs will be generated as well.
148148

149-
On the other hand, if you are prioritizing the top :math:`k` documents, the ``lambdarank_num_pair_per_sample`` should be set slightly higher than :math:`k` (with a few more documents) to obtain a good training result.
149+
On the other hand, if you are prioritizing the top :math:`k` documents, the ``lambdarank_num_pair_per_sample`` should be set slightly higher than :math:`k` (with a few more documents) to obtain a good training result. Lastly, XGBoost employs additional regularization for learning to rank objectives, which can be disabled by setting the ``lambdarank_normalization`` to ``False``.
150+
150151

151152
**Summary** If you have large amount of training data:
152153

python-package/xgboost/testing/ranking.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,21 @@ def run_ranking_categorical(device: str) -> None:
100100
scores = cross_val_score(ltr, X, y)
101101
for s in scores:
102102
assert s > 0.7
103+
104+
105+
def run_normalization(device: str) -> None:
106+
"""Test normalization."""
107+
X, y, qid, _ = tm.make_ltr(2048, 4, 64, 3)
108+
ltr = xgb.XGBRanker(objective="rank:pairwise", n_estimators=4, device=device)
109+
ltr.fit(X, y, qid=qid, eval_set=[(X, y)], eval_qid=[qid])
110+
e0 = ltr.evals_result()
111+
112+
ltr = xgb.XGBRanker(
113+
objective="rank:pairwise",
114+
n_estimators=4,
115+
device=device,
116+
lambdarank_normalization=False,
117+
)
118+
ltr.fit(X, y, qid=qid, eval_set=[(X, y)], eval_qid=[qid])
119+
e1 = ltr.evals_result()
120+
assert e1["validation_0"]["ndcg@32"][-1] > e0["validation_0"]["ndcg@32"][-1]

src/common/ranking_utils.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ struct LambdaRankParam : public XGBoostParameter<LambdaRankParam> {
7878

7979
// unbiased
8080
bool lambdarank_unbiased{false};
81+
bool lambdarank_normalization{true};
8182
double lambdarank_bias_norm{1.0};
8283
// ndcg
8384
bool ndcg_exp_gain{true};
@@ -86,6 +87,7 @@ struct LambdaRankParam : public XGBoostParameter<LambdaRankParam> {
8687
return lambdarank_pair_method == that.lambdarank_pair_method &&
8788
lambdarank_num_pair_per_sample == that.lambdarank_num_pair_per_sample &&
8889
lambdarank_unbiased == that.lambdarank_unbiased &&
90+
lambdarank_normalization == that.lambdarank_normalization &&
8991
lambdarank_bias_norm == that.lambdarank_bias_norm && ndcg_exp_gain == that.ndcg_exp_gain;
9092
}
9193
bool operator!=(LambdaRankParam const& that) const { return !(*this == that); }
@@ -134,6 +136,9 @@ struct LambdaRankParam : public XGBoostParameter<LambdaRankParam> {
134136
DMLC_DECLARE_FIELD(lambdarank_unbiased)
135137
.set_default(false)
136138
.describe("Unbiased lambda mart. Use extended IPW to debias click position");
139+
DMLC_DECLARE_FIELD(lambdarank_normalization)
140+
.set_default(true)
141+
.describe("Whether to normalize the leaf value for lambda rank.");
137142
DMLC_DECLARE_FIELD(lambdarank_bias_norm)
138143
.set_default(1.0)
139144
.set_lower_bound(0.0)

src/objective/lambdarank_obj.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ class LambdaRankObj : public FitIntercept {
222222
};
223223

224224
MakePairs(ctx_, iter, p_cache_, g, g_label, g_rank, loop);
225-
if (sum_lambda > 0.0) {
225+
if (sum_lambda > 0.0 && param_.lambdarank_normalization) {
226226
double norm = std::log2(1.0 + sum_lambda) / sum_lambda;
227227
std::transform(g_gpair.Values().data(), g_gpair.Values().data() + g_gpair.Size(),
228228
g_gpair.Values().data(), [norm](GradientPair const& g) { return g * norm; });

src/objective/lambdarank_obj.cu

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,12 +266,13 @@ void CalcGrad(Context const* ctx, MetaInfo const& info, std::shared_ptr<ltr::Ran
266266
*/
267267
auto d_weights = common::MakeOptionalWeights(ctx, info.weights_);
268268
auto w_norm = p_cache->WeightNorm();
269+
auto norm = p_cache->Param().lambdarank_normalization;
269270
thrust::for_each_n(ctx->CUDACtx()->CTP(), thrust::make_counting_iterator(0ul), d_gpair.Size(),
270271
[=] XGBOOST_DEVICE(std::size_t i) mutable {
271272
auto g = dh::SegmentId(d_gptr, i);
272273
auto sum_lambda = thrust::get<2>(d_max_lambdas[g]);
273274
// Normalization
274-
if (sum_lambda > 0.0) {
275+
if (sum_lambda > 0.0 && norm) {
275276
double norm = std::log2(1.0 + sum_lambda) / sum_lambda;
276277
d_gpair(i, 0) *= norm;
277278
}

tests/python-gpu/test_gpu_ranking.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import xgboost
88
from xgboost import testing as tm
9+
from xgboost.testing.ranking import run_normalization
910

1011
pytestmark = tm.timeout(30)
1112

@@ -126,3 +127,7 @@ def test_with_mq2008(objective, metric) -> None:
126127
dtest = xgboost.DMatrix(x_test, y_test, qid=qid_test)
127128

128129
comp_training_with_rank_objective(dtrain, dtest, objective, metric)
130+
131+
132+
def test_normalization() -> None:
133+
run_normalization("cuda")

tests/python/test_ranking.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from xgboost import testing as tm
1414
from xgboost.testing.data import RelDataCV, simulate_clicks, sort_ltr_samples
1515
from xgboost.testing.params import lambdarank_parameter_strategy
16+
from xgboost.testing.ranking import run_normalization
1617

1718

1819
def test_ndcg_custom_gain():
@@ -188,6 +189,10 @@ def after_training(self, model) -> bool:
188189
assert df["ti+"].iloc[-1] < df["ti+"].iloc[0]
189190

190191

192+
def test_normalization() -> None:
193+
run_normalization("cpu")
194+
195+
191196
class TestRanking:
192197
@classmethod
193198
def setup_class(cls):

0 commit comments

Comments
 (0)