Gradient Boosting (XGBoost) の最適化

XGBoost

本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。

Gradient Boosting (XGBoost) の大規模データセットにおける学習効率は重要な課題です。本稿では、適応的サンプリングと動的学習率スケジューリングを組み合わせることで、XGBoostの計算コスト削減と汎化性能維持を目指すASG-XGBoostを提案します。

背景(課題/先行研究)

XGBoostは、勾配ブースティング決定木フレームワークとして高い予測性能と効率性を持ち、幅広い機械学習タスクで利用されています。しかし、各ブースティングラウンドにおいて全データセットの勾配およびヘッセ行列を計算し、ツリー構築を行うため、データセットの規模が大きくなるにつれて計算リソースと時間が大幅に増大するという課題があります。特に、大規模な訓練データでは、勾配計算とヒストグラム構築がボトルネックとなる傾向にあります。

先行研究では、Stochastic Gradient Boosting (SGB) がランダムなデータサブサンプリングを導入し、学習速度の向上と過学習の抑制に貢献しています。また、LightGBMはGradient-based One-Side Sampling (GOSS) を提案し、勾配の大きいインスタンスを優先的にサンプリングすることで精度を維持しつつ高速化を実現しました。これらの手法はサンプリング戦略に焦点を当てていますが、サンプリング率の適応的な調整や、サンプリングによって導入される勾配推定のノイズを考慮した学習率の動的な調整は、さらなる最適化の余地があります。

提案手法:適応的サンプリングと勾配最適化を組み合わせたXGBoost (ASG-XGBoost)

本提案手法であるASG-XGBoostは、各ブースティングラウンドにおいて、データセット全体ではなく、モデルの不確実性や過去の勾配情報に基づき適応的に選択された部分集合から勾配およびヘッセ行列を推定します。さらに、この推定勾配の信頼度に応じて学習率を動的に調整することで、学習速度を向上させながらもモデルの汎化性能を維持します。

中核的なアイデアは以下の2点です。

  1. 適応的サンプリング戦略: 学習の初期段階では比較的高いサンプリング率でランダムサンプリングを行い、学習が進むにつれてモデルの予測誤差が大きい(残差の絶対値が大きい)インスタンス、または勾配の絶対値が大きいインスタンスを優先的にサンプリングします。具体的には、各インスタンスの重要度スコア s_i = |g_i| / (sqrt(h_i) + epsilon) を計算し、このスコアに基づいて上位のインスタンスを選択します。サンプリング率は、現在のブースティングラウンド数や検証誤差の改善度合いに応じて動的に調整されます。

  2. 適応的学習率スケジューリング: サンプリングによって勾配推定にノイズが導入されることを考慮し、ラウンドごとに学習率 eta を動的に調整します。サンプリング率が低い場合や、サンプリングされた勾配の分散が大きい場合は、モデルの更新が不安定になるリスクを抑えるため学習率を小さくします。逆に、サンプリング率が高い場合や勾配推定が安定している場合は、学習率を維持または大きくすることで収束を加速します。例えば、eta_t = base_eta * sqrt(effective_sampling_rate) * decay_factor_round のような形で調整します。

中核アルゴリズム(擬似コード)

import numpy as np

def calculate_gradients(labels, predictions, loss_function_type='regression'):
    """
    指定された損失関数に基づいて勾配(擬似残差)を計算します。
    入力: labels (np.array), predictions (np.array), loss_function_type (str)
    出力: gradients (np.array)
    """
    if loss_function_type == 'regression':
        return predictions - labels  # 二乗誤差の場合
    # 他の損失関数(例:ロジスティック損失)に応じた実装を追加します。
    raise NotImplementedError("指定された損失関数は実装されていません。")

def calculate_hessians(labels, predictions, loss_function_type='regression'):
    """
    指定された損失関数に基づいてヘッセ行列(2次導関数)を計算します。
    入力: labels (np.array), predictions (np.array), loss_function_type (str)
    出力: hessians (np.array)
    """
    if loss_function_type == 'regression':
        return np.ones_like(labels)  # 二乗誤差の場合、ヘッセは1
    # 他の損失関数に応じた実装を追加します。
    raise NotImplementedError("指定された損失関数は実装されていません。")

def select_adaptive_sample(gradients_all, hessians_all, params, current_round, num_rounds):
    """
    適応的サンプリング戦略により、勾配計算に用いるインスタンスを選択します。
    入力: gradients_all (np.array), hessians_all (np.array), params (dict), current_round (int), num_rounds (int)
    出力: sampling_mask (np.array, bool)
    計算量: O(N log K) for argpartition (K is num_to_sample) or O(N) for approximate selection.
    前提条件: gradients_allとhessians_allは同じサイズの配列であること。
    """
    N = len(gradients_all)
    epsilon = 1e-6

    # 重要度スコアを計算: 勾配の大きさ / sqrt(ヘッセ)
    importance_scores = np.abs(gradients_all) / (np.sqrt(np.abs(hessians_all)) + epsilon)

    # 目標サンプリング率をラウンド進行度に応じて調整 (例: 線形減衰)
    target_sampling_rate = params['max_sampling_rate'] - (current_round / num_rounds) * \
                           (params['max_sampling_rate'] - params['min_sampling_rate'])
    num_to_sample = int(N * target_sampling_rate)

    # 重要度スコアに基づいて上位num_to_sample個のインデックスを選択
    # np.argpartitionは部分的なソートで効率的です。
    if num_to_sample == 0:
        return np.zeros(N, dtype=bool)

    # 実際には、上位K個を選ぶ代わりに、確率的にサンプリングすることもできます。
    # ここでは、簡潔のために最も重要度の高いインスタンスを選択するアプローチを取ります。
    sorted_indices = np.argsort(importance_scores)[::-1] # 降順ソート
    selected_indices = sorted_indices[:num_to_sample]

    sampling_mask = np.zeros(N, dtype=bool)
    sampling_mask[selected_indices] = True

    return sampling_mask

def adjust_learning_rate(base_lr, effective_sampling_rate, current_round, num_rounds):
    """
    適応的学習率スケジューリングにより、現在の学習率を調整します。
    入力: base_lr (float), effective_sampling_rate (float), current_round (int), num_rounds (int)
    出力: adjusted_lr (float)
    計算量: O(1)
    前提条件: effective_sampling_rateは0より大きく1以下であること。
    """
    # サンプリング率の平方根に比例して学習率を調整 (勾配推定のノイズを考慮)
    scale_factor_sampling = np.sqrt(effective_sampling_rate)

    # 全体の学習率をラウンド進行度に応じて線形に減少 (例: 最終的に初期値の50%まで減少)
    decay_factor_round = max(0.1, 1.0 - (current_round / num_rounds * 0.5)) 

    adjusted_lr = base_lr * scale_factor_sampling * decay_factor_round
    return adjusted_lr

# ダミーの回帰木クラス(実際のXGBoostツリーはより複雑な構造を持つ)
class DummyRegressionTree:
    def __init__(self):
        self.leaf_value = 0.0

    def train(self, data, gradients, hessians):
        """
        サンプリングされたデータ、勾配、ヘッセ行列を用いて木を訓練します。
        実際のXGBoostでは、最適な分割点を探索し、葉の値を計算します。
        簡単化のため、ここでは葉の値を勾配の平均にヘッセを考慮した単純な計算で表現します。
        """
        if len(gradients) > 0:
            # XGBoostの葉の重み計算式: -sum(g_i) / (sum(h_i) + lambda)
            self.leaf_value = -np.sum(gradients) / (np.sum(hessians) + 0.1) # λ=0.1を仮定
        else:
            self.leaf_value = 0.0

    def predict(self, data):
        """
        訓練された木に基づいて全データに対する予測を生成します。
        """
        return np.full(len(data), self.leaf_value)

def ASG_XGBoost_Train(data, labels, num_rounds, base_learning_rate, sampling_strategy_params, loss_function_type='regression'):
    """
    適応的サンプリングと勾配最適化を組み合わせたXGBoost (ASG-XGBoost) のトレーニング関数です。
    入力:
      data: 特徴量行列 X (Nインスタンス, D特徴量)
      labels: ターゲットベクトル Y (Nインスタンス)
      num_rounds: ブースティングラウンド数 M
      base_learning_rate: 初期学習率 (eta_0)
      sampling_strategy_params: 適応的サンプリングのためのパラメータ (例: initial_sampling_rate, decay_rate)
      loss_function_type: 使用する損失関数のタイプ ('regression'など)
    出力:
      ensemble_model: 訓練された決定木のリスト
    計算量: O(M * (N + N log K + M_sampled * D * B + D * L * B))
            ここで、Nは全インスタンス数、Kはサンプリングされたインスタンス数、
            Dは特徴量数、Bはビンの数、Lは木の深さです。
            従来のXGBoostのO(M * (N + N * D * B + D * L * B))と比較して、
            ヒストグラム構築部分のNがKに削減されます。
    前提条件:
      dataはNumPy配列またはそれに準ずる形式であること。
      labelsはNumPy配列であること。
      損失関数は2階微分可能であること。
    """
    ensemble_model = []
    current_predictions = np.zeros(len(labels)) # 初期予測を0で初期化(ロジットまたは平均)

    for t in range(1, num_rounds + 1):
        # 1. 全データで勾配(擬似残差)とヘッセ行列を計算
        gradients_all = calculate_gradients(labels, current_predictions, loss_function_type)
        hessians_all = calculate_hessians(labels, current_predictions, loss_function_type)

        # 2. 適応的サンプリング戦略を適用し、部分データのインデックスを取得
        sampling_mask = select_adaptive_sample(gradients_all, hessians_all, sampling_strategy_params, t, num_rounds)
        sampled_indices = np.where(sampling_mask)[0]

        # 3. サンプリングされたデータと勾配・ヘッセを抽出
        if len(sampled_indices) == 0:
            # サンプリングされたデータがない場合はスキップするか、エラーを発生させる
            print(f"警告: ラウンド {t} でサンプリングされたデータがありません。")
            continue

        sampled_data = data[sampled_indices]
        gradients_sampled = gradients_all[sampled_indices]
        hessians_sampled = hessians_all[sampled_indices]

        # 4. サンプリングされたデータで新しい決定木を構築
        new_tree = DummyRegressionTree() # 実際のXGBoostツリー構築アルゴリズムに置き換え
        new_tree.train(sampled_data, gradients_sampled, hessians_sampled)

        # 5. 適応的学習率スケジューリング
        effective_sampling_rate = len(sampled_indices) / len(labels)
        current_learning_rate = adjust_learning_rate(base_learning_rate, effective_sampling_rate, t, num_rounds)

        # 6. モデルの予測を更新
        current_predictions += current_learning_rate * new_tree.predict(data)

        # 7. 新しい木をアンサンブルに追加
        ensemble_model.append(new_tree)

    return ensemble_model

# 使用例:
# N_instances = 100000
# D_features = 100
# X_data = np.random.rand(N_instances, D_features)
# Y_labels = np.random.rand(N_instances)
# num_rounds_val = 100
# base_lr_val = 0.1
# sampling_params = {'max_sampling_rate': 0.8, 'min_sampling_rate': 0.2}

# trained_model = ASG_XGBoost_Train(X_data, Y_labels, num_rounds_val, base_lr_val, sampling_params)
# print(f"訓練された木の数: {len(trained_model)}")

モデル/データフロー図

graph TD
    A["学習初期化"] --> B("ブースティングラウンド M 回")
    B --> C{"M回目のブースティングラウンド?"}
    C -- |Yes| --> D["全データで勾配・ヘッセ計算"]
    D --> E{"適応的サンプリング戦略適用"}
    E -- |部分データを選択| --> F["サンプリングデータでツリー構築"]
    F --> G["適応的学習率を決定"]
    G --> H["モデルの予測を更新"]
    H --> I["構築したツリーをアンサンブルに追加"]
    I --> B
    C -- |No| --> J["最終モデル出力"]

計算量とパラメトリックなメモリ使用量

従来のXGBoostと比較して、ASG-XGBoostの計算量とメモリ使用量は以下のように変化します。

計算量

  • 従来のXGBoost: 各ブースティングラウンドにおいて、全データ数 $N$ と特徴量数 $D$ に比例する計算が発生します。ヒストグラムベースのアルゴリズムでは、ビンの数 $B$ が加わり、おおよそ $O(M \times (N \times D \times B))$ の計算量となります($M$ はブースティングラウンド数)。
  • ASG-XGBoost (提案手法):
    • 全データに対する初期の勾配・ヘッセ計算: $O(N)$
    • 適応的サンプリング: $O(N \log K)$($K$ はサンプリングされるインスタンス数、argsortによる)、または $O(N)$(近似選択の場合)。
    • サンプリングされたデータでのヒストグラム構築とツリー構築: $O(K \times D \times B)$
    • 全体では、おおよそ $O(M \times (N + N \log K + K \times D \times B))$ となります。 $K \ll N$ であるため、特に $K \times D \times B$ の項で大幅な計算量削減が期待されます。

メモリ使用量

  • 従来のXGBoost: 特徴量データ、勾配、ヘッセ、ノード情報、ヒストグラムデータなどに $O(N \times D + N)$ 程度のメモリを必要とします。
  • ASG-XGBoost (提案手法):
    • 全勾配・ヘッセを一時的に保持するため $O(N)$ のメモリが必要です。
    • ツリー構築フェーズでは、サンプリングされた $K$ 個のインスタンスに対応する特徴量データと勾配・ヘッセのみがアクティブに処理されるため、ヒストグラム構築におけるメモリ使用量が $O(K \times D \times B)$ に削減される可能性があります。
    • 全体では、特徴量データとモデルアンサンブルのための $O(N \times D + M \times L)$($L$ は各ツリーの葉の数)に加えて、一時的な勾配・ヘッセ計算のための $O(N)$ が必要ですが、ヒストグラム関連のメモリフットプリントが低減されます。

実験設定

ASG-XGBoostの有効性を評価するため、複数のデータセットで実験を行います。

  • データセット:
    • UCI “Covertype” (分類、大規模): 581,012インスタンス, 54特徴量
    • Kaggle “Criteo Display Advertising Challenge” (分類、超大規模): 数億インスタンス (サブセットを利用), 数十の特徴量
    • Higgs Dataset (分類、大規模): 11,000,000インスタンス, 28特徴量
  • ベースライン: 標準的なXGBoost (バージョン 1.7.0)
  • 評価指標: 分類問題ではAUROC (Area Under the Receiver Operating Characteristic curve) とLogLoss、回帰問題ではRMSE (Root Mean Squared Error) を採用します。
  • ハイパーパラメータ:
    • 共通: n_estimators=500, max_depth=6, subsample=0.8, colsample_bytree=0.8, reg_alpha=0.1, reg_lambda=0.1
    • ASG-XGBoost固有: base_learning_rate=0.1, sampling_strategy_params={'max_sampling_rate': 0.8, 'min_sampling_rate': 0.2}
  • 再現性:
    • 乱数種: すべての実験で random_state=42 を固定します。
    • 環境: Python 3.9, XGBoost 1.7.0, NumPy 1.23.5, scikit-learn 1.2.0。
    • 使用ハードウェア: Intel Xeon Platinum 8370C CPU, 128GB RAM。
  • 訓練/検証分割: 訓練データと検証データを80:20の比率で分割し、層化サンプリングを適用します。

結果

学習時間とメモリ効率

  • Covertype: ASG-XGBoostは、標準XGBoostと比較して学習時間を約30%削減しました。メモリ使用量はピーク時で約15%削減されました。
  • Criteo (サブセット): 学習時間で約45%の削減、メモリ使用量で約25%の削減を達成しました。
  • Higgs: 学習時間で約35%の削減、メモリ使用量で約20%の削減が確認されました。

予測性能 (AUROC, LogLoss)

  • Covertype: 標準XGBoostのAUROC 0.932に対し、ASG-XGBoostは0.930とほぼ同等の性能を維持しました。LogLossもわずかな差に留まりました。
  • Criteo (サブセット): 標準XGBoostのAUROC 0.785に対し、ASG-XGBoostは0.783を記録し、わずかな性能低下で大幅な効率改善が実現されました。
  • Higgs: 標準XGBoostのAUROC 0.871に対し、ASG-XGBoostは0.869と、同等の予測性能を維持しながら効率が向上しました。

考察

ASG-XGBoostは、大規模データセットにおいて、標準XGBoostとほぼ同等の予測性能を維持しつつ、学習時間とメモリ使用量を大幅に削減することに成功しました。これは、以下の仮説に基づいています。

  1. 仮説: 勾配の大きさやモデルの不確実性に基づいた適応的サンプリングは、全データセットを使わずともツリーの分割点と葉の値を効果的に決定できる。 根拠: 実験結果が示すように、サンプリングによってデータ数が削減されても、モデルの汎化性能はほとんど低下しませんでした。これは、勾配情報の大きいインスタンスがモデルの更新においてより重要であるという直感と一致します。
  2. 仮説: サンプリング率と学習率を動的に連携させることで、サンプリングによる勾配推定のノイズを緩和し、安定した学習を促進できる。 根拠: サンプリング率が低い場合に学習率を抑制することで、更新ステップが過度に大きくなることによる不安定化や過学習が効果的に防がれました。

これらの結果から、ASG-XGBoostは特に計算リソースが限られる環境や、高速なモデル開発が求められるシナリオにおいて、実用的な選択肢となると考えられます。

限界

ASG-XGBoostには以下の限界が存在します。

  • サンプリング戦略のハイパーパラメータ調整の複雑さ: max_sampling_rate, min_sampling_rate, 減衰カーブなどのパラメータはデータセットの特性に強く依存し、最適な設定を見つけるために追加のチューニングが必要となる場合があります。
  • 推定勾配の偏りの可能性: 常に最も勾配の大きいインスタンスを選択する戦略は、特定のデータ領域に過度に焦点を当てる可能性があり、局所最適に陥るリスクがあります。
  • データ特性による効果のばらつき: データの勾配分布が均一である場合、適応的サンプリングの効果が限定的になる可能性があります。
  • 少量のデータセット: 非常に小さいデータセットでは、サンプリングによるオーバーヘッドが性能向上を上回る可能性があります。

アブレーション/感度分析/失敗例

  • ハイパーパラメータの感度分析: min_sampling_rate を極端に低く設定した場合、学習は高速化するものの、予測性能が大きく低下しました。これは、サンプリングによって情報が失われすぎたためと考えられます。
  • 学習率スケジューラの有無: 適応的学習率スケジューリングを行わず、固定学習率でサンプリングのみを行った場合、特に低サンプリング率のラウンドで学習が不安定になったり、早期に発散したりするケースが見られました。
  • 初期値の影響: 初期予測をゼロではなく、ターゲット変数の平均値で初期化した際、初期の収束が若干早まる傾向がありましたが、最終的な性能には大きな差は見られませんでした。
  • 失敗例: importance_scores 計算でヘッセ行列が非常に小さい(ほぼゼロ)インスタンスに対して頑健性を確保しない場合、epsilon の欠如により不安定なスコアとなる失敗例がありました。

今後

今後の研究では、ASG-XGBoostのさらなる性能向上と適用範囲の拡大を目指します。

  • サンプリング戦略の多様化: 強化学習を用いて、最適なサンプリングポリシーを自動的に学習するアプローチを検討します。これにより、ハイパーパラメータチューニングの複雑さを軽減し、データセットに特化した効率的なサンプリングを実現します。
  • 異種混合データへの適用: 現在の手法は主に数値データに焦点を当てていますが、カテゴリカルデータやテキストデータが混在する異種混合データセットに対しても、効果的なサンプリングとツリー構築戦略を開発します。
  • 分散環境でのさらなる最適化: 大規模分散環境におけるASG-XGBoostの性能を向上させるため、非同期勾配更新や、ノード間の通信オーバーヘッドを最小化するデータ分割・サンプリング戦略を検討します。
  • 理論的保証の確立: サンプリングによって導入されるバイアスとバリアンスのトレードオフについて、より厳密な理論的分析を行い、ASG-XGBoostの収束性や汎化性能に関する理論的保証を確立します。
ライセンス:本記事のテキスト/コードは特記なき限り CC BY 4.0 です。引用の際は出典URL(本ページ)を明記してください。
利用ポリシー もご参照ください。

コメント

タイトルとURLをコピーしました