🌲

LightGBM ~ feature_fraction と feature_fraction_bynode どっちが正義? ~

に公開

背景

もはや古典になりつつある勾配ブースティング(既に古典...?)ですが、その汎用性の高さから未だに Kaggle などのコンペティションでも現役と聞きます。

テーブルデータ用ベンチマーク

2025年11月に TabPFN と呼ばれるモデルの最新バージョン TabPFN-2.5 が出ました。モデルの解説については、別の機会で試みたいのですが、今回の記事の動機は以下の図です。

こちらは TabArena (-lite) と呼ばれるデータセット群で評価されたEloレーティングのグラフです。様々なモデルで評価されており、非常に詳細な検証内容が論文には記載されています。

今回気になったのは、NNでテーブルデータって本当に強いの? LightGBM / XGBoost / CatBoost って本当はどれが良いの? という事です。いや、論文に書いてるよ、ってのはそうなのですが、なんか色々気になって自分で試してみたくなりました。

という訳で、LightGBM / XGBoost / CatBoost って本当はどれが良いの? という検証を行う前段階として、今回の記事「feature_fraction と feature_fraction_bynode どっちが正義?」を書く事にしました。

CatBoost に feature_fraction は無い

Kaggle や X などでも度々話題になりますが、実は CatBoost の性能が良いというのは前々から知っていました。私はほとんど LightGBM しか使ってこなかったのですが、今回の論文の図を見て CatBoost が気になりパラメータを眺めていたところ、feature_fraction にあたる、決定木を作る際の木単位の特徴量ランダム選択比率、のパラメータが無い事に気づきました。CatBoost は代わり?に RSM という、木の階層レベル単位の特徴量ランダム選択比率、のパラメータを持ちます。

私は何故か、ずっと feature_fraction が重要と思っていたので、CatBoostの性能の良さって実はノード単位(or 階層レベル単位)の特徴量ランダムサンプルと関係があるのでは? と気になりました。

※しかし LightGBM には rsm 相当のパラメータがありません。なので ノード単位のパラメータである feature_fraction_bynode を比較します。

それが今回の調査の動機で、feature_fraction と feature_fraction_bynode を比較しようと思い至りました。

注意事項

今回は主にパラメータチューニングについての記事になります。

チューニングするハイパーパラメータの数は、少ないに越した事はありません。が、NNに比べると、勾配ブースティングの各パッケージは、非常に多い数のハイパーパラメータが搭載されています。

勿論全てをチューニングする時間があれば良いですが、一部のパラメータに絞ってチューニングするのが現実的です。さらに、テーブルデータは画像や言語のドメインと異なり、そのデータセットによって最適なパラメータがそれぞれ異なる、と認識しています。

機械学習というかデータ分析全般に言える事ですが、その検証や評価の方針は、ある種の信仰が伴います。え、信仰? となるかもしれませんが、例えば私は分類においては logloss 信仰です(昔は ROC-AUC を信仰していました)。 ある人は F値 信仰かもしれません。そしてパラメータチューニングにも、私の信仰があります

要は何が言いたいかというと、データ分析にはその選択肢が多すぎて、何か方針を固定(つまり信仰)しないとキリが無いのです。こっちの指標で試すと? 他のデータセットでは違う結果にならないか? こちらとあちらの適用順番を逆にすると? などオプションが無限です。

なので以下の検証方法や評価で「何故そうしているのか」について、特別な理由が無い事の方が多いです。もしそういった疑問が浮かべば、それはこの人の信仰なんだろうな、と寛大な気持ちで読んでいただけると幸いです。

目的

前置きが長くなりましたが、本記事の目的は

です。ちなみにこの記事執筆時は、どちらか一方のチューニングが、他方を包括していると思っているため、両方を合わせてチューニングしませんでした。しかし、結果が良く分からなかったので両方チューニングするバージョンも比較に加えました。さらに事前予想に関しては、feature_fraction_bynode だけチューニングすればいいと思っていました。

検証方法

私は多クラス分類のみに関心があります。経験則として、回帰タスクが役に立ったケースがほとんどないからです。なので今回は多クラス分類に絞って検証を行います。

どちらをパラメータチューニングすべきか、について

比較方法

まず、共通にチューニングすべきハイパーパラメータ群(common parameter) があります(後述)。その上で

  • A: common parameter + feature_fraction をチューニングし best model で評価
  • B: common parameter + feature_fraction_bynode をチューニングし best model で評価
  • C: common parameter + feature_fraction & feature_fraction_bynode をチューニングし best model で評価

A のチューニングでは feature_fraction_bynode=1 で固定し、B では逆に feature_fraction=1 で固定します。

この評価値を以下のデータセットに対して算出し、A/B どちらが優れているかを評価します。

データセット

なるべくデータセットの種類は多い方が良いと思い、以下を用意しました。全て openml で提供されているデータセットです。

name n_data n_classes n_features
SDSS17 78053 3 11
covertype 581012 7 54
gas-drift 13910 6 128
helena 65196 100 27
hiva_agnostic 3845 3 1617
nursery 12958 4 8
shuttle 58000 7 9
splice 3190 3 60
students_dropout_and_academic_success 4424 3 36
tamilnadu-electricity 45781 20 2
walking-activity 149332 22 4

nursery について、一部ごく少数のサンプルがあったため、全データ数の 0.01% 以下もしくはサンプル数3以下のクラスは除外しています

評価手順

まず、各データセットでパラメータチューニングを行います(後述)。そして得られた best-parameter を用いて、モデルを訓練しデータセットを評価します。

データセットの評価値は、以下の手順により作成します。

バリデーションやテストなどの分割方法は TabArena を参考にしました。TabArena では、Outer loop -> Inner loop の方法で分割し評価しています。下の絵では、Outer loop で 3-fold、inner loop で 4-fold CV を行い、訓練された 4 model をアンサンブルして test を評価し、それを outer loop の fold 回数分繰り返して評価します。

便宜上、outer loop -> inner loop の1セット自体の繰り返し回数(テストデータセットのランダムネスの軽減目的)を nloop, outer loop の fold 回数を nfold, inner loop の n-fold CV を ncv と呼びます。

今回は nloop=2, nfold=3, ncv=5 で評価します。分割は全て from sklearn.model_selection import StratifiedKFold を用います。

評価指標

from sklearn.metrics import log_loss を使用します。normalize=True

\text{loss} = \sum_{i=1}^C \left( -y_i \log {p_i} \right)

logloss を使って、以下の2つの A/B 比較用評価値を用意します。

  • 標準化
    (logloss - mean) / std で標準化します
  • 比率
    log(A/B), log(A/C), log(B/C) の値を計算します. logとしているのは A/BB/A を値の大きさとして同等と扱いたいからです
標準化に関して

mean は A,B,C のそれぞれで nfold x nloop の数の評価値を得られます。データセット毎に、その全ての値の平均を計算します。

次に std です。mean と同様それらの値で std を計算しても良かったのですが、A,B,C の良し悪しで大きく値が離れる事を考慮し、別の方法で測定しました。何のモデルで測っても良かったのですが、今回は extra_trees=True として LightGBM で測定しました。これを nloop=5, nfold=3, ncv=5 で回して、評価値を各データセットで 5 x 3 = 15 個得ます。この値の std を採用します。

パラメータチューニング

パラメータチューニングには optuna を用います。

common parameter は以下です。

  • min_sum_hessian_in_leaf
    あるノードにおいて、そのノードにあるサンプルが持つ必要な hessian の合計値. その合計値以下になるように分割はできない
  • lambda_l2
    Leaf に付く値の l2 正則化
  • bagging_fraction
    各決定木における sample のランダムサンプリング比率. bagging_freq=1 とする事に注意

これ以外は固定のパラメータとします。何故それを固定するかについての気持ちを解説してみます。

  • iteration
    early stopping で学習を止めるため iteration は適当に大きい数字で良いです
  • learning_rate
    基本的に小さくすれば精度が上がり木の数が増え(訓練時間が伸び)ます。なので、訓練時間を許す範囲内の値で固定すれば良いです。このデータセットにはこの learning rate が良い! みたいなものは無い認識です( NNは除きます!
  • num_leaves と max_depth
    LightGBM はそのコンセプトとして Leaf-wise-growth なので、max_depth=-1 で固定します。num_leaves はメモリの許す限り多くすれば良いです。ノードの分割ストップは min_sum_hessian_in_leaf で決めます
  • min_child_samples
    こちらも同様固定で良いです。min_sum_hessian_in_leaf にノード分割を委ねます
  • min_split_gain
    min_sum_hessian_in_leaf の loss の値版といったパラメータですが、経験的に min_sum_hessian_in_leaf の方が重要度が高いケースが多いので、min_split_gain=0 で固定します
  • lambda_l1
    CatBoost に実はこのパラメータが無いので、思い切って lambda_l2 に委ねてこちらは固定します。個人的にも、L1は木の数に対する正則化なので、0固定で良いのではと思います
  • max_bin
    histgrum を作る際の bin の数です。こちらも固定で良いです。

以下の値に設定します。

iteration=1000
learning_rate=0.1
num_leaves=256 # depth = 8 相当
max_depth=-1
min_child_samples=5
min_split_gain=0
lambda_l1=0
max_bin=128

次に optuna で探索するcommon parameterですが、以下の範囲で探索します。

trial.suggest_float("min_sum_hessian_in_leaf", 1e-4, 1e3, log=True)
trial.suggest_float("lambda_l2",               1e-4, 1e3, log=True)
trial.suggest_float("bagging_fraction",        0.1, 1.0, log=False)

これに以下の A or B のどちらかを加えて同時に探索させます。探索回数は各データセットで50回です。

trial.suggest_float("feature_fraction",        0.1, 1.0, log=False) # A

or

trial.suggest_float("feature_fraction_bynode", 0.1, 1.0, log=False) # B

結果

ひとまずデータセット全体の平均を出します。分かりずらいですが、標準化は値が小さい方が良く、log比では log(A/B) の値が正のとき B の方が評価が良いという事です。

標準化 bytree          -0.298569
標準化 bynode          -0.804466
標準化 bytree & bynode  1.103035
log比 bynode / bytree             0.038951
log比 bynode / (bynode & bytree)  0.047077
log比 bytree / (bynode & bytree)  0.008125

つまり、標準化基準では bynode のみが最もよく、log比基準では bynode & bytree が最も良いです。細かい数値とプロットを以下に載せます。

dataset 平均_A 平均_B 平均_C ABC平均 extra_tree評価値分散 標準化_A 標準化_B 標準化_C log(B/A) log(B/C) log(A/C)
SDSS17 0.0740802 0.0744852 0.0746289 0.0743981 0.00237945 -0.133603 0.0365945 0.0970089 0.00545186 -0.0019281 -0.00737996
covertype 0.0840811 0.0817237 0.0839117 0.0832388 0.00503774 0.167192 -0.300758 0.133566 -0.0284379 -0.0264211 0.00201673
gas-drift 0.0291754 0.0291252 0.0276443 0.0286483 0.00476336 0.11065 0.100131 -0.210781 -0.00171901 0.0521871 0.0539061
helena 2.63883 2.62758 2.63218 2.63286 0.0150716 0.395712 -0.3503 -0.0454124 -0.00426993 -0.00174728 0.00252265
hiva_agnostic 0.18721 0.186659 0.18941 0.18776 0.00521162 -0.105413 -0.211297 0.316711 -0.00295198 -0.0146347 -0.0116827
nursery 0.00133429 0.00174878 0.0011652 0.00141609 0.00715337 -0.0114349 0.0465081 -0.0350731 0.270518 0.406026 0.135509
shuttle 0.000402254 0.000404696 0.000405506 0.000404152 0.000424648 -0.00446887 0.00128098 0.00318789 0.0060516 -0.00199893 -0.00805053
splice 0.102394 0.122131 0.108331 0.110952 0.0156164 -0.54799 0.715838 -0.167847 0.176261 0.119903 -0.0563581
students_dropout_and_academic_success 0.568858 0.570679 0.570596 0.570044 0.0209764 -0.0565532 0.0302459 0.0263072 0.00319558 0.000144786 -0.00305079
tamilnadu-electricity 2.99454 2.994 2.9957 2.99474 8.66048e-05 -2.39791 -8.64491 11.0428 -0.000180685 -0.000569328 -0.000388643
walking-activity 0.998832 1.00338 1.01663 1.00628 0.0106384 -0.70044 -0.272459 0.972898 0.00454802 -0.0131176 -0.0176656

結論と考察

bynode & bytree では nursery がその log比で大きく目立ち、bynode では tamilnadu-electricity がその標準化指標で大きく影響しています。

今回、各データセットに対する重要度などは考慮しておらず、全て同じ比重で評価しています。というのも、少なくとも標準化ではそのデータセットの分割などに関わる分散が考慮されているので、大雑把には同じ比重で良いはずです。

なので、正直、良く分からない というのが結論になります。少なくとも現状では、割とケースバイケースという事で、bynode & bytree でチューニングしようかなと思っています。

Discussion