nykergoto’s blog

機械学習とpythonをメインに

勾配ブースティングで大事なパラメータの気持ち

LightGBMXGBoost などで使われている勾配ブースティングのパラメータについて、チューニングノウハウというよりもそのパラメータがどういう意味を持っているのか、に焦点をあててまとめて見ました。

各ライブラリのパラメータすべては以下から確認できます。

NOTE: 以下では lightGBM のパラメータの名前で説明しています。微妙に名前が違うものがあるので適宜読み替えてください。

勾配ブースティングについてざっくりと

一般的な決定木では木はひとつだけで、その木に対してたくさんの分割ルールを適用していきます。

勾配ブースティング木では、木をたくさん作ります。たくさん作る代わりに、一つ一つの木の分割をざっくりとしたものにします。 そして作った木すべての予測の合計を使うことで、ひとつの木では表せないような複雑な予測を可能にしています。

もうちょっとくわしく: Gradient Boosted Tree (Xgboost) の取り扱い説明書

木のパラメータ

たくさん作られるそれぞれの木をどのように作成するか、に関する制約条件についてです。

はじめに木の大きさや分割方法に関するパラメータから説明します。

  • max_depth: 各木の最大の深さです。たぶん木の構造パラメータの中で一番シビアに効いてくるパラメータだと思っています(あくまで筆者の感覚ですが)。普通 3 ~ 8 ぐらいを設定します。あまり大きい値を使用すると、ひとつの木がとても大きなものになってしまいオーバーフィッティングする可能性がありますから、あまりおおきい値は設定しないほうが良いでしょう。

木はたくさん作られるのでたとえ小さい値を指定したとしても、有用な分割方法であれば次の木が分割してくれるので、まあ問題はないという印象です。

  • max_leaves: 各木の末端ノードの数です。ひとつの分割だけがあるとノードは2つになり、もう一回分割が起こるとノードは3になります。というふうに分割数 + 1 だけ末端ノードは生成されます。 このノード数は max_depth と深い関係があります。というのも max_depth を指定すると、最大の末端ノード数は 2 ** max_depth で制限されるためです。(二分分割が depth だけ起こるので)
    この時 2 ** max_depth 以上の数の max_leaves を指定すると depth の指定によるノード数の上限よりも大きくなるため無意味になってしまうことに注意してください。 (lightGBM だとエラーメッセージが出ます)
    以上のような理由から int(.7 * max_depth ** 2) などを指定して depth からくる最大ノード数の 7 割だけ分割させる、とかをやったりします。

  • min_child_samples: 末端ノードに含まれる最小のデータ数。これを下回るような分割ルールでは分割されなくなります。 例えば 40 に設定したとすると、新しい分割を行ったあとの集合にデータ数が 40 を下回るようなルールでは分割できません。 最小の数をどのぐらいにすればよいかはデータの数に依存する部分も大きいため, データ数が大きい時は少し大きい値にすることを検討してみてください。

  • gamma: 分割を増やす際の目的関数の減少量の下限値です。分割を行うと、よりデータに適合できるようになるためからなず目的関数のデータへの当てはまり部分は改善します。一方で適合し過ぎるとオーバーフィットとなります。(バイアスバリアンス)
    gamma はこの当てはまり改善に対して制限をかけることに相当します。カジュアルに言うと、めっちゃロスが下がるような明確な違いで分割してもいいけど、微妙にロスが下がるような分割はゆるさないよ、というようなイメージでしょうか。

以下は使用するデータを制限することでロバストなモデルを作成しよう、という狙いのためのパラメータです。

  • colsample_bytree: 木を作成する際に使用する特徴量の数を選択する割合です。まあまあ大事です。 これが 1 以下のとき、各木を作成する際に使用する特徴量をこの確率を持って選び出し、選ばれた特徴量だけを使います。
    カラム数がとても多い時なんかに小さめの値を設定していることが多い印象です。個人的には 0.7 あるいは 0.4 をつかうことが多いです。
  • subsamples: 使用するデータの選択割合です。 colsample とは反対にデータの選択を行います。

つぎに木を成長させるアルゴリズムについてです。

  • boosting: boosting アルゴリズムです。このパラメータはlightGBM のみ使えます。 基本は勾配ブースティングをしたいのでデフォルトの gbdt を使うと良いでしょう。 その他にも dropout の考え方を応用して新しい木を作る際の勾配/ヘシアンの計算に今まで作った木の一部を確率的に取り出して使う dart や, random foreset (rf) も指定できます。

    random forest は stacking をする際に勾配ブースティングのモデルと相性がいいことが経験的に知られているので, 一段目のモデルとして使うことは有効かもしれません。

目的関数のパラメータ

木構造の目的関数は一般的な機械学習モデルと同様にデータへのあてはまりと、正則化の項からできています。 正確には分割を作る際にできる2つのノードに対して割り当てる予測値の値を、正則化 (l1, l2) を使って 0 に近づけるような働きをします。

目的関数は objective で定義します。

目的関数の選択方針

目的関数の選択はターゲット変数の分布に大きく依存します。 カテゴリ変数の予測の時は普通に logloss を使えば事足りることが多いですが回帰問題の時はちょっと工夫すると良いモデルを作れることがあります。 以下でそれぞれ述べていきます。

普通の回帰問題であれば rmse を使います。(デフォルトの値は rmse です). rmse はノイズの分布に正規分布を仮定することになります。 例えば工場で特定の場所にドリルで穴を開ける作業があり、実際に開けられた穴の場所を予測するスクがあったとします。このときドリルの位置のズレは正規分布に従うことが想定されるため、目的関数として rmse を使うのが良いでしょう。

一方で年収やある出来事が起こる間隔など裾の広い分布に対しては gamma を使ったり, 目的変数をログ変換して擬似的に対数正規分布に対するあてはめに変えることを検討してください。これらの分布は正規分布にならないことが多いです。

また一定期間内に起こるランダムな現象のカウントを予測する場合には poisson を使うことを検討してみてください。 例えばサッカーの試合のゴール数などがこれに相当します。

その他

その他の目的関数に関するパラメータは主に正則化に関するものを設定することが多いです。

  • reg_alpha: L1 正則化に相当するものです。デフォルトでは 0.1 ぐらいを使うことが多いです。L1正則化にあたるのであまり大きな値を使用するとかなり重要な変数以外を無視するようなモデルになってしまうため, 精度を求めている場合あまり大きくしないほうが良いでしょう。(説明変数を削減したい場合にはこの限りではありません。大きな reg_alpha を設定して feature_importance を見て余り使われていない変数を削除する、という方法は正当な方法です。)
  • reg_lambda: L2 正則化に相当するものです。デフォルトでは L1 と同様に 0.1 ぐらいを指定します。L1と違い大きな値を設定してもその変数が使われないということはありませんので、特徴量の数が多い時や、徐々に木を大きくしたいすぐにオーバーフィットするような問題に対して大きく取ることが多いです。

学習時のパラメータ

学習を行うときに validation データとともに渡すパラメータです。

  • learing_rate: たくさん作った木を足し合わせるときに使う重み係数です。
    これを大きくするとひとつひとつの木の予測を多く使うようになるため、一般に収束するまでの木の数 n_estimators は少なくなり学習にかかる時間は短くなります。一方で分割法が雑になるため、精度とのトレードオフとなります。
  • eval_metric: validation データを評価する評価基準です。何も指定しないと objective で指定したものが使われます。例えば rmse ならば validation set に対しても rmse を計算します。
    目的と異なるへんな評価基準を入れると early_stopping_round を指定している時に学習が収束する前に止まってしまうこともあるので注意してください。
  • early_stopping_rounds: 指定された回数木を作っても評価基準が改善されない時に学習を途中でやめるようになります。オーバーフィットを防ぐために指定します。
  • verbose: 指定された回数に一度コンソールに eval_metric を表示します。チューニングしないでね。

チューニング

チューニングは optuna を使うとらくちんにできるのでおすすめです。 いろいろな場所で使いまわせるよう、パラメータを生成するような関数だけ切り出して定義しておくと便利だと思います。以下はその実装例です。

def get_boosting_parameter_suggestions(trial: Trial) -> dict:
    """
    Get parameter sample for Boosting (like XGBoost, LightGBM)

    Args:
        trial(trial.Trial):

    Returns:
        dict: parameter sample generated by trial object
    """
    return {
        # L2 正則化
        'reg_lambda': trial.suggest_loguniform('reg_lambda', 1e-3, 1e3),
        # L1 正則化
        'reg_alpha': trial.suggest_loguniform('reg_alpha', 1e-3, 1e3),
        # 弱学習木ごとに使う特徴量の割合
        # 0.5 だと全体のうち半分の特徴量を最初に選んで, その範囲内で木を成長させる
        'colsample_bytree': trial.suggest_loguniform('colsample_bytree', .5, 1.),
        # 学習データ全体のうち使用する割合
        # colsample とは反対に row 方向にサンプルする
        'subsample': trial.suggest_loguniform('subsample', .5, 1.),
        # 木の最大の深さ
        # たとえば 5 の時各弱学習木のぶん機は最大でも5に制限される.
        'max_depth': trial.suggest_int('max_depth', low=3, high=8),
        # 末端ノードに含まれる最小のサンプル数
        # これを下回るような分割は作れなくなるため, 大きく設定するとより全体の傾向でしか分割ができなくなる
        # [NOTE]: 数であるのでデータセットの大きさ依存であることに注意
        'min_child_weight': trial.suggest_uniform('min_child_weight', low=.5, high=40)
    }