🌲

LightGBM のランク学習における正規化の効果

に公開

はじめに

前回の記事では LightGBM の lambdarank と rank_xendcg を比較しました。理論上は NDCG を直接最適化する xENDCG が有利なはずなのに、なぜ、lambdarank の方が一定して優位なのか?この疑問からパラメータを調べたところ、lambdarank_norm が目に留まりました。ソースコードを追ってみたところ、この正規化は xENDCG には適用されていないことに気づきました。

そこで、本記事では以下の 2 つの観点から正規化の効果を検証しました。

  1. lambdarank_norm の効果: lambdarank における lambdarank_norm=True vs lambdarank_norm=False の比較
  2. xENDCG への正規化適用: rank_xendcg vs custom_xendcg(xENDCG + 正規化)の比較

TL;DR(3 行まとめ)

  • 正規化は学習の安定性と精度に大きく寄与: lambdarank_norm=True は False に比べて安定した高い性能
  • xENDCG にも正規化は効果的なケースあり: custom_xendcg(xENDCG + lambdarank_norm 相当)は特にクエリ間のグループサイズ不均一時に効果的
  • 候補数や条件によって最適な objective が異なる: 候補数が小〜中規模では lambdarank、大規模では xendcg 系が安定

本記事の構成

  1. 背景: lambdarank_norm とは何か、なぜ正規化が重要なのか
  2. 実験設計: 比較する objective と候補抽出方法
  3. 実験 1: 全ユーザー同一グループサイズでの比較
  4. 実験 2: ユーザーごとに異なるグループサイズでの比較
  5. 考察: なぜグループサイズ不均一で効果が大きいか
  6. 推奨 objective: 状況別の設定ガイド

背景: なぜ正規化が重要か

lambdarank_norm とは

lambdarank には lambdarank_norm パラメータがあります(デフォルト: True)。
LightGBM 公式ドキュメントでは下記のような説明がされています。

  • set this to true to normalize the lambdas for different queries, and improve the performance for unbalanced data
  • set this to false to enforce the original lambdarank algorithm

正規化の仕組み

lambdarank_norm=True では、以下の正規化がクエリ(グループ)ごとに適用されます:

{norm\_factor} = \frac{\log_2(1 + \sum|\lambda|)}{\sum|\lambda|}

ここで \lambda は 各ドキュメントの勾配 を表します。LambdaRank では、ペアワイズの順序誤りに対する勾配を各ドキュメントに割り当てたものが \lambda であり、\sum|\lambda| はグループ内の全勾配の絶対値の総和です。

この正規化により

  1. 勾配スケールの統一: グループサイズに関わらず、勾配の大きさが揃う(分母の \sum|\lambda| が大きいほど norm_factor は小さくなる)
  2. 不均衡データへの対応: 小さなグループも大きなグループも同程度の影響力を持つ
  3. 学習の安定化: \log_2 による対数スケールで勾配の急激な変化を抑制

rank_xendcg には正規化がない

一方、rank_xendcg の実装にはこのような正規化処理が見当たりませんでした。これが lambdarank と rank_xendcg の性能差の一因と考えました。

そこで本記事では、xENDCG に lambdarank_norm と同等の正規化を追加した custom_xendcg を実装し、正規化の効果を検証します。

実験設計

比較する objective

以下の 4 つの objective を比較します:

objective 正規化 説明
lambdarank (norm=True) あり lambdarank + lambdarank_norm=True(デフォルト)
lambdarank (norm=False) なし lambdarank + lambdarank_norm=False(オリジナル LambdaMART)
rank_xendcg なし xENDCG(LightGBM 組み込み)
custom_xendcg あり xENDCG + lambdarank_norm と同等の正規化

Note: custom_xendcg の実装では 1 次 Taylor 近似を採用しました。LightGBM の rank_xendcg は 3 次 Taylor 近似を使用していますが、事前実験で 1 次近似と 3 次近似の精度差がほぼ無視できる(NDCG@10 で ±0.001 程度)ことを確認したため、計算がシンプルな 1 次近似を採用しました。

候補抽出の方法

正規化の効果は「グループサイズの不均一性」で顕著に現れると予想されます。そこで 2 種類の候補抽出方法を検証しました

方法 説明
全ユーザー同一(前回の記事と同じ) 人気順 Top-N
ユーザーごとに異なる 人気順 + ユーザーの好みジャンル

「ユーザーごとに異なる」方法では、各ユーザーの評価履歴から好みジャンルを特定し、そのジャンルの人気アイテムを候補に追加します。これにより、ユーザーごとに候補数が変わるようにしました。

custom_xendcg の実装

rank_xendcg に lambdarank_norm 相当の正規化を追加したカスタム objective を実装しました。

import numpy as np


def xendcg_normalized_objective(
    y_true: np.ndarray,
    y_pred: np.ndarray,
    group: np.ndarray,
) -> tuple[np.ndarray, np.ndarray]:
    """xENDCG + lambdarank_norm 相当の正規化."""
    n_samples = len(y_true)
    n_groups = len(group)

    # グループの開始インデックス
    group_starts = np.concatenate([[0], np.cumsum(group[:-1])])
    group_indices = np.repeat(np.arange(n_groups), group)

    # 乱数は一括生成
    rng = np.random.default_rng(42)
    gamma = rng.uniform(0, 1, n_samples)

    # --- Softmax のベクトル化 ---
    # グループごとの max を reduceat で計算
    y_pred_max = np.maximum.reduceat(y_pred, group_starts)
    y_pred_max_expanded = y_pred_max[group_indices]

    # exp(x - max)
    exp_pred = np.exp(y_pred - y_pred_max_expanded)

    # グループごとの sum
    sum_exp = np.add.reduceat(exp_pred, group_starts)
    sum_exp_expanded = sum_exp[group_indices]

    # Softmax
    rho = exp_pred / sum_exp_expanded

    # --- Phi ---
    phi_values = np.power(2.0, y_true) - gamma
    sum_phi = np.add.reduceat(phi_values, group_starts)
    sum_phi = np.maximum(sum_phi, 1.0)
    sum_phi_expanded = sum_phi[group_indices]
    phi_normalized = phi_values / sum_phi_expanded

    # --- Gradient & Hessian ---
    grad = rho - phi_normalized
    hess = np.maximum(rho * (1.0 - rho), 1e-6)

    # --- lambdarank_norm 正規化(ここがポイント)---
    sum_abs_grad = np.add.reduceat(np.abs(grad), group_starts)
    norm_factor = np.where(
        sum_abs_grad > 0, np.log2(1 + sum_abs_grad) / sum_abs_grad, 1.0
    )
    norm_factor_expanded = norm_factor[group_indices]
    grad = grad * norm_factor_expanded

    return grad, hess

実験条件

前回の記事と同じ実験設定を使用しています。詳細は前回の記事を参照してください。

主要パラメータ(再掲):

  • データセット: MovieLens Latest Small(10 万件の評価データ、600 ユーザー、9000 アイテム)
  • 分割: 時系列で Train 80% / Valid 10% / Test 10%
  • ラベル: rating 値(0.5〜5.0)
  • 正例定義: rating >= 4.0(Recall@k 等の計算時)

LightGBM パラメータ:

  • num_leaves = 31
  • learning_rate = 0.05
  • n_estimators = 2000
  • early_stopping_rounds = 50

本記事での追加条件:

  • seed: 5 つのシードで実行し、平均 ± 標準偏差を計算
  • colsample_bytree = 0.25: seed の影響を与えるために追加

実験 1: 全ユーザー同一グループサイズ

まず、前回の記事と同じ条件(人気順 Top-N)で検証します。5 シードで実行し、平均 ± 標準偏差を計算しました。

実験 1 の結果

  • lambdarank: norm=True が安定して優位。大規模候補で norm=False の分散が大きい
  • xENDCG 系: rank_xendcg と custom_xendcg の差は小さい(±0.001〜0.003)
  • 全体傾向: グループサイズが揃っている条件では、正規化の有無による差は限定的

Recall

小規模では lambdarank が優位

top_n Recall@10 Recall@50 Recall@100
λ(T) λ(F) xendcg custom λ(T) λ(F) xendcg custom λ(T) λ(F) xendcg custom
30 0.067±0.001 0.068±0.001 0.067±0.002 0.065±0.001 0.105 0.105 0.105 0.105 0.105 0.105 0.105 0.105
100 0.067±0.001 0.067±0.002 0.064±0.001 0.064±0.001 0.161±0.007 0.161±0.004 0.161±0.008 0.157±0.005 0.208 0.208 0.208 0.208
500 0.068±0.001 0.067±0.004 0.065±0.002 0.065±0.002 0.142±0.006 0.144±0.009 0.151±0.002 0.151±0.003 0.206±0.005 0.203±0.011 0.211±0.017 0.208±0.007
1000 0.067±0.004 0.066±0.003 0.065±0.002 0.065±0.001 0.145±0.008 0.144±0.002 0.149±0.002 0.148±0.003 0.200±0.007 0.193±0.008 0.196±0.004 0.196±0.004
3000 0.065±0.003 0.063±0.004 0.063±0.002 0.063±0.001 0.141±0.009 0.141±0.005 0.148±0.002 0.147±0.002 0.191±0.010 0.194±0.011 0.197±0.005 0.196±0.002
5000 0.062±0.004 0.065±0.003 0.061±0.001 0.062±0.002 0.139±0.009 0.145±0.009 0.147±0.003 0.146±0.002 0.192±0.006 0.201±0.010 0.198±0.003 0.199±0.005
7000 0.056±0.009 0.065±0.002 0.061±0.003 0.061±0.003 0.139±0.013 0.144±0.010 0.147±0.004 0.146±0.003 0.185±0.019 0.198±0.010 0.201±0.005 0.200±0.003
9000 0.065±0.002 0.056±0.021 0.062±0.003 0.062±0.002 0.138±0.014 0.124±0.033 0.145±0.002 0.145±0.002 0.200±0.005 0.173±0.043 0.199±0.005 0.196±0.002

MAP

大きな差はないが、lamrabdarankの方が標準偏差が大きい傾向

top_n MAP@10 MAP@50 MAP@100
λ(T) λ(F) xendcg custom λ(T) λ(F) xendcg custom λ(T) λ(F) xendcg custom
30 0.045±0.002 0.047±0.001 0.047±0.001 0.047±0.002 0.062±0.002 0.062±0.001 0.063±0.001 0.063±0.001 0.062±0.002 0.062±0.001 0.063±0.001 0.063±0.001
100 0.047±0.000 0.048±0.002 0.046±0.002 0.046±0.001 0.076±0.003 0.076±0.002 0.076±0.005 0.075±0.003 0.084±0.002 0.084±0.002 0.084±0.004 0.084±0.002
500 0.048±0.002 0.046±0.004 0.046±0.001 0.046±0.001 0.072±0.002 0.070±0.002 0.071±0.003 0.072±0.002 0.083±0.002 0.079±0.002 0.081±0.001 0.081±0.002
1000 0.047±0.002 0.046±0.002 0.045±0.000 0.047±0.001 0.072±0.003 0.070±0.002 0.070±0.001 0.071±0.001 0.081±0.003 0.077±0.001 0.078±0.001 0.079±0.001
3000 0.045±0.003 0.042±0.003 0.043±0.001 0.043±0.001 0.069±0.005 0.066±0.004 0.067±0.002 0.067±0.002 0.077±0.004 0.074±0.004 0.075±0.002 0.075±0.002
5000 0.041±0.005 0.041±0.005 0.042±0.003 0.042±0.003 0.065±0.004 0.066±0.006 0.067±0.003 0.067±0.003 0.073±0.003 0.075±0.006 0.076±0.003 0.076±0.003
7000 0.035±0.009 0.043±0.005 0.040±0.002 0.040±0.003 0.060±0.014 0.068±0.005 0.065±0.002 0.065±0.003 0.068±0.015 0.076±0.006 0.074±0.002 0.074±0.003
9000 0.043±0.004 0.036±0.017 0.041±0.003 0.042±0.003 0.067±0.006 0.056±0.024 0.066±0.002 0.066±0.003 0.076±0.005 0.063±0.027 0.075±0.002 0.074±0.003

NDCG

top_n=9000 で norm=False の標準偏差が極端に大きく、学習が不安定

top_n NDCG@10 NDCG@50 NDCG@100
λ(T) λ(F) xendcg custom λ(T) λ(F) xendcg custom λ(T) λ(F) xendcg custom
30 0.217±0.004 0.221±0.002 0.222±0.003 0.223±0.002 0.154±0.003 0.155±0.001 0.157±0.001 0.158±0.001 0.141±0.003 0.142±0.001 0.144±0.001 0.145±0.001
100 0.219±0.003 0.217±0.006 0.211±0.005 0.214±0.002 0.197±0.006 0.198±0.004 0.198±0.008 0.197±0.005 0.206±0.002 0.207±0.002 0.207±0.004 0.209±0.002
500 0.223±0.006 0.211±0.011 0.213±0.003 0.212±0.004 0.185±0.005 0.181±0.005 0.188±0.004 0.189±0.003 0.206±0.003 0.200±0.007 0.207±0.007 0.206±0.004
1000 0.218±0.008 0.211±0.006 0.209±0.003 0.212±0.002 0.186±0.006 0.182±0.003 0.185±0.002 0.187±0.002 0.202±0.005 0.194±0.003 0.198±0.003 0.199±0.003
3000 0.214±0.010 0.205±0.006 0.204±0.005 0.203±0.005 0.181±0.010 0.178±0.005 0.182±0.003 0.182±0.003 0.193±0.008 0.191±0.006 0.195±0.005 0.195±0.004
5000 0.199±0.014 0.204±0.011 0.203±0.004 0.205±0.009 0.173±0.006 0.178±0.010 0.182±0.005 0.182±0.005 0.189±0.004 0.194±0.010 0.197±0.004 0.198±0.006
7000 0.188±0.026 0.208±0.011 0.196±0.007 0.198±0.009 0.170±0.022 0.179±0.009 0.179±0.005 0.179±0.006 0.181±0.023 0.194±0.010 0.195±0.004 0.195±0.004
9000 0.208±0.011 0.175±0.074 0.201±0.009 0.200±0.009 0.176±0.013 0.153±0.052 0.180±0.004 0.179±0.003 0.196±0.007 0.168±0.053 0.196±0.004 0.195±0.004

多様性メトリクスの評価

同じようなアイテムばかり推薦すると、ユーザー体験が単調になってしまうため、多様性も評価しました。

多様性メトリクスの知見

  1. custom_xendcg は ILD と Coverage で優位: 精度だけでなく多様性も向上
  2. lambdarank_norm_false は Novelty が高い: ただし標準偏差が非常に大きい、学習がうまくいっていない結果とも考えられる
  3. 正規化は多様性にも好影響: 安定した学習が多様性にも寄与

ILD (Intra-List Diversity)

推薦リスト内のアイテム間の多様性を測定。ジャンル特徴量のコサイン距離の平均値。高いほど多様。

xENDCG 系が lambdarank より高い傾向
norm=False は標準偏差が大きい

top_n ILD@10 ILD@50 ILD@100
λ(T) λ(F) xendcg custom λ(T) λ(F) xendcg custom λ(T) λ(F) xendcg custom
100 0.703±0.023 0.686±0.029 0.691±0.010 0.705±0.026 0.734±0.010 0.730±0.008 0.743±0.002 0.741±0.004 0.712±0.000 0.712±0.000 0.712±0.000 0.712±0.000
500 0.638±0.060 0.613±0.095 0.695±0.025 0.707±0.005 0.731±0.013 0.687±0.087 0.726±0.016 0.733±0.011 0.739±0.017 0.704±0.072 0.727±0.006 0.735±0.008
1000 0.663±0.045 0.602±0.099 0.722±0.005 0.718±0.010 0.722±0.018 0.700±0.043 0.734±0.009 0.732±0.017 0.730±0.015 0.705±0.042 0.727±0.007 0.725±0.007
3000 0.652±0.088 0.665±0.050 0.724±0.010 0.728±0.008 0.703±0.039 0.703±0.047 0.739±0.010 0.732±0.006 0.715±0.035 0.717±0.018 0.724±0.003 0.728±0.005
5000 0.650±0.092 0.618±0.129 0.729±0.013 0.731±0.007 0.684±0.082 0.662±0.112 0.729±0.013 0.725±0.005 0.683±0.071 0.670±0.108 0.723±0.003 0.726±0.006

Novelty

推薦アイテムの「珍しさ」。人気の低いアイテムを推薦するほど高い。

norm=False が最も高いが、標準偏差が極端に大きい(±1.34 など)
学習の不安定さにより偶然マイナーなアイテムを推薦している可能性

top_n Novelty@10 Novelty@50 Novelty@100
λ(T) λ(F) xendcg custom λ(T) λ(F) xendcg custom λ(T) λ(F) xendcg custom
100 1.30±0.03 1.28±0.01 1.31±0.01 1.31±0.02 1.75±0.01 1.76±0.01 1.75±0.01 1.76±0.02 1.98±0.00 1.98±0.00 1.98±0.00 1.98±0.00
500 1.35±0.08 1.40±0.22 1.29±0.03 1.28±0.01 1.87±0.15 1.99±0.30 1.85±0.27 1.76±0.05 2.26±0.13 2.29±0.27 2.13±0.13 2.13±0.10
1000 1.31±0.05 1.43±0.19 1.29±0.07 1.26±0.02 1.86±0.15 2.08±0.37 1.76±0.07 1.73±0.01 2.28±0.12 2.50±0.39 2.10±0.14 2.03±0.01
3000 1.36±0.16 2.13±1.34 1.27±0.02 1.27±0.01 1.99±0.29 2.29±0.73 1.72±0.01 1.73±0.01 2.53±0.49 2.76±0.90 2.04±0.02 2.04±0.02
5000 1.47±0.32 1.67±0.63 1.28±0.01 1.28±0.01 2.13±0.58 2.27±0.80 1.73±0.03 1.73±0.02 2.54±0.63 2.67±0.80 2.02±0.02 2.03±0.02

Coverage

全アイテムに対して推薦されるアイテムの割合。高いほど多くのアイテムが推薦される。

custom_xendcg が一貫して高い傾向
lambdarank は候補数増加に伴い Coverage が低下しやすそう?

top_n Coverage@10 Coverage@50 Coverage@100
λ(T) λ(F) xendcg custom λ(T) λ(F) xendcg custom λ(T) λ(F) xendcg custom
100 0.569±0.021 0.569±0.023 0.589±0.014 0.602±0.047 0.889±0.011 0.894±0.001 0.895±0.000 0.884±0.024 0.895±0.000 0.895±0.000 0.895±0.000 0.895±0.000
500 0.554±0.051 0.531±0.058 0.576±0.029 0.597±0.011 0.913±0.030 0.839±0.101 0.887±0.056 0.921±0.018 0.943±0.007 0.923±0.055 0.947±0.000 0.947±0.000
1000 0.562±0.022 0.520±0.080 0.574±0.016 0.580±0.010 0.915±0.046 0.877±0.039 0.907±0.021 0.920±0.023 0.945±0.007 0.930±0.040 0.947±0.000 0.947±0.000
3000 0.526±0.050 0.546±0.027 0.583±0.024 0.602±0.034 0.857±0.059 0.885±0.045 0.909±0.017 0.934±0.011 0.920±0.044 0.965±0.032 0.947±0.000 0.948±0.000
5000 0.535±0.029 0.551±0.043 0.599±0.043 0.613±0.026 0.860±0.055 0.859±0.053 0.899±0.007 0.911±0.009 0.908±0.040 0.928±0.042 0.947±0.000 0.947±0.000

実験 2: ユーザーごとに異なるグループサイズ

次に、より実践的な条件で検証します。人気順 Top-N に加えて、ユーザーの好みジャンルに基づく候補を追加するため、グループサイズがユーザーによって異なります。

仮説

実験 1 では「グループサイズが揃っている条件では正規化の効果が限定的」でした。では、グループサイズが不均一な場合はどうか?

正規化の理論的な効果(大きなグループの勾配支配を防ぐ)から、下記の結果になると予想しました

  1. lambdarank: norm=True と norm=False の差が拡大する
  2. xENDCG 系: custom_xendcg(正規化あり)が rank_xendcg(正規化なし)を上回る

候補数 1000 以上では、ほとんどのユーザーで人気順に好みのジャンルに基づいた候補の全映画が候補に含まれる結果になりました。「ユーザーごとに異なるグループサイズ」の検証という目的を達成できないため、候補数 500 以下のみを実験結果として掲載しています。

候補数ベース グループサイズ (平均 ± 標準偏差)
30 241±17
100 257±18
500 505±15

Recall

custom_xendcg が高く安定
norm=False は標準偏差が大きく、グループサイズ不均一の影響が顕著

top_n Recall@10 Recall@50 Recall@100
λ(T) λ(F) xendcg custom λ(T) λ(F) xendcg custom λ(T) λ(F) xendcg custom
30 0.057±0.008 0.054±0.011 0.062±0.005 0.062±0.004 0.115±0.002 0.114±0.002 0.114±0.002 0.115±0.001 0.132±0.003 0.128±0.004 0.128±0.003 0.132±0.003
100 0.064±0.005 0.056±0.013 0.060±0.008 0.064±0.001 0.159±0.009 0.148±0.010 0.151±0.010 0.161±0.004 0.231±0.002 0.223±0.002 0.224±0.004 0.225±0.004
500 0.055±0.005 0.046±0.009 0.058±0.006 0.064±0.003 0.142±0.004 0.130±0.012 0.150±0.002 0.151±0.003 0.220±0.017 0.192±0.016 0.203±0.011 0.212±0.008

MAP

custom_xendcg が候補数 500 で最大 +0.010 の改善(MAP@ALL: 0.105 vs 0.095)
norm=False は全条件で最下位かつ標準偏差が高い

top_n MAP@10 MAP@50 MAP@100 MAP@ALL
λ(T) λ(F) xendcg custom λ(T) λ(F) xendcg custom λ(T) λ(F) xendcg custom λ(T) λ(F) xendcg custom
30 0.039±0.007 0.032±0.010 0.044±0.003 0.043±0.003 0.058±0.007 0.050±0.010 0.061±0.002 0.061±0.003 0.060±0.007 0.052±0.010 0.062±0.003 0.063±0.003 0.062±0.007 0.054±0.010 0.064±0.003 0.064±0.004
100 0.044±0.005 0.035±0.010 0.043±0.005 0.045±0.001 0.072±0.003 0.061±0.012 0.068±0.008 0.073±0.001 0.084±0.004 0.073±0.011 0.080±0.007 0.084±0.002 0.087±0.004 0.076±0.011 0.082±0.007 0.087±0.002
500 0.037±0.006 0.027±0.010 0.041±0.005 0.046±0.003 0.060±0.005 0.049±0.012 0.067±0.004 0.071±0.002 0.073±0.005 0.058±0.013 0.076±0.004 0.082±0.002 0.095±0.004 0.080±0.013 0.099±0.004 0.105±0.001

NDCG

custom_xendcg が全条件で最高または同等
norm=True と norm=False の差が実験 1 より拡大(候補数 500 で ndcg@10が 0.032 差)しており、正規化の効果が明確に現れていそう

top_n NDCG@10 NDCG@50 NDCG@100 NDCG@ALL
λ(T) λ(F) xendcg custom λ(T) λ(F) xendcg custom λ(T) λ(F) xendcg custom λ(T) λ(F) xendcg custom
30 0.192±0.022 0.167±0.035 0.209±0.011 0.209±0.011 0.156±0.009 0.145±0.015 0.161±0.003 0.161±0.003 0.152±0.008 0.139±0.015 0.155±0.003 0.156±0.003 0.192±0.009 0.179±0.016 0.197±0.003 0.196±0.005
100 0.210±0.014 0.175±0.035 0.202±0.017 0.212±0.005 0.195±0.004 0.173±0.020 0.185±0.013 0.195±0.002 0.217±0.003 0.200±0.014 0.210±0.008 0.215±0.003 0.227±0.003 0.212±0.014 0.222±0.007 0.226±0.002
500 0.182±0.015 0.150±0.033 0.196±0.015 0.209±0.010 0.171±0.003 0.150±0.024 0.183±0.005 0.188±0.004 0.199±0.008 0.172±0.022 0.198±0.007 0.207±0.006 0.292±0.003 0.276±0.018 0.297±0.004 0.303±0.000

仮説の検証結果

仮説 1: lambdarank の norm=True vs norm=False の差が拡大する → 予想通り

  • 差が拡大: 候補数 500 で NDCG@10 の差が 0.032(0.182 vs 0.150)。実験 1 では同条件で 0.012 だった
  • norm=False の劣化が顕著: 標準偏差も ±0.033 と大きく、学習が不安定に

仮説 2: custom_xendcg が rank_xendcg を上回る → 予想通り

  • 明確な差: NDCG@10 で +0.010〜0.013 の改善(実験 1 では ±0.001〜0.003)
  • 安定性も向上: rank_xendcg は ±0.015〜0.017 だが、custom_xendcg は ±0.005〜0.011 と安定
  • @ALL でも同傾向: 上位だけでなく全体のランキング品質でも custom_xendcg が優位

推奨 objective

実験結果からケースごとに推奨 objective を整理してみました。

状況 推奨 objective 理由
小〜中規模候補(30〜1000) lambdarank (norm=True) 安定した高性能
大規模候補(3000 以上) rank_xendcg または custom_xendcg 分散が小さく安定
グループサイズが不均一 custom_xendcg 正規化によって安定性が向上
学習速度重視 rank_xendcg カスタム objective より高速
多様性も重視 custom_xendcg ILD、Coverage が高い傾向

まとめ

正規化の効果

本記事では、LightGBM のランク学習における正規化の効果を 2 つの観点から検証しました。

1. lambdarank_norm の効果

  • lambdarank_norm=True は False に比べて、特に中規模候補(100〜1000)で +0.007〜0.012 の NDCG 改善
  • 大規模候補(7000〜9000)では False の標準偏差が ±0.074 と非常に大きく不安定
  • デフォルトの True を変更する理由は基本的にない

2. xENDCG への正規化適用

  • グループサイズ不均一時に custom_xendcg が rank_xendcg を +0.010〜0.013(NDCG@10)上回る
  • 安定性も向上(複数シードでの標準偏差が減少)
  • グループサイズが不均一な実務環境では custom_xendcg を推奨

正規化が効く条件

  • グループサイズが不均一な場合: 正規化の効果が顕著
  • 大規模候補の場合: 学習の安定性が向上

副次的な知見: 1 次近似 vs 3 次近似(事前実験)

本記事の custom_xendcg は 1 次 Taylor 近似を採用していますが、LightGBM の rank_xendcg は 3 次近似を使用しています。

事前実験で両者を比較した結果、精度差がほぼ無視できるため、計算がシンプルかつ高速な 1 次近似を採用しました。

  • 精度差: 全条件で NDCG@10 の差が ±0.001〜0.002 程度
  • 学習時間: 3 次近似は 1 次近似の約 1.3 倍

補足: カスタム評価指標の実装

LightGBM でカスタム objective を使用する際、Early Stopping 用の評価指標もカスタム実装が必要になります。本実験では NDCG@K のカスタム評価指標を実装しました。

NDCG@K の実装

import numpy as np
from typing import Callable
import lightgbm as lgb


def create_ndcg_eval(k: int = 10) -> Callable:
    """NDCG@k 評価関数を作成.

    グループごとに NDCG@k を計算し、平均を返す。

    Args:
        k: NDCG の截断位置

    Returns:
        LightGBM のカスタム評価関数
    """

    def ndcg_eval(preds: np.ndarray, data: lgb.Dataset) -> tuple[str, float, bool]:
        labels = data.get_label()
        groups = data.get_group()

        if groups is None:
            groups = np.array([len(labels)])

        ndcg_scores = []
        start_idx = 0

        for group_size in groups:
            end_idx = start_idx + group_size

            group_labels = labels[start_idx:end_idx]
            group_preds = preds[start_idx:end_idx]

            # 予測スコアで降順ソート
            sorted_indices = np.argsort(-group_preds)
            sorted_labels = group_labels[sorted_indices]

            # DCG@k
            positions = np.arange(1, min(k, len(sorted_labels)) + 1)
            gains = np.power(2.0, sorted_labels[:len(positions)]) - 1
            discounts = np.log2(positions + 1)
            dcg = np.sum(gains / discounts)

            # IDCG@k(理想的な順位)
            ideal_sorted_labels = np.sort(group_labels)[::-1]
            ideal_gains = np.power(2.0, ideal_sorted_labels[:len(positions)]) - 1
            idcg = np.sum(ideal_gains / discounts)

            if idcg > 0:
                ndcg = dcg / idcg
                ndcg_scores.append(ndcg)

            start_idx = end_idx

        mean_ndcg = np.mean(ndcg_scores) if ndcg_scores else 0.0
        return f"ndcg@{k}", float(mean_ndcg), True

    return ndcg_eval

注意点

  • 計算コスト: カスタム評価指標は Python で実装されるため、C++ 組み込みの ndcg より遅くなります。大規模データでは注意が必要です。

実装例

カスタム objective とカスタム評価指標を LightGBM で使用する方法を紹介します。

import lightgbm as lgb
import numpy as np

# 上記の xendcg_normalized_objective を定義済みとする

def custom_obj(preds: np.ndarray, data: lgb.Dataset) -> tuple[np.ndarray, np.ndarray]:
    """LightGBM カスタム objective のラッパー."""
    labels = data.get_label()
    groups = data.get_group()
    if groups is None:
        groups = np.array([len(labels)])
    return xendcg_normalized_objective(labels, preds, groups)


# カスタム評価指標を生成
ndcg_10 = create_ndcg_eval(k=10)

# LightGBM で使用
train_data = lgb.Dataset(X_train, label=y_train, group=group_train)
valid_data = lgb.Dataset(X_valid, label=y_valid, group=group_valid, reference=train_data)

params = {
    "objective": custom_obj,
    "num_leaves": 31,
    "learning_rate": 0.05,
}

model = lgb.train(
    params,
    train_data,
    num_boost_round=2000,
    valid_sets=[valid_data],
    feval=ndcg_10,  # カスタム評価指標で Early Stopping
    callbacks=[lgb.early_stopping(100, verbose=False)],
)

おわりに

本記事では、LightGBM のランク学習における正規化の効果を検証しました。主な発見は以下の通りです。

  1. lambdarank_norm=True は効果的: デフォルトで True になっている理由がよくわかった
  2. xENDCG にも正規化が有効: 特にグループサイズ不均一時
  3. 正規化は精度と多様性の両方に好影響: 安定した学習のキーになっている

正直なところ、LightGBM の正規化ロジックを参考にカスタム objective を実装してみたものの、「本当にうまくいくのかな」と半信半疑でした。実際に実験して効果が確認できて嬉しかったです。

ぜひみなさんの環境でも試してみていただけたら嬉しいです。
この実験で得られた知見が、皆さんのランキングモデル改善の一助になれば幸いです。

参考

Discussion