【LightGBM】pred_leafで葉を特徴量にする(GBDT+線形モデル)

はじめに

決定木は、条件分岐でデータをたどり、各サンプルを必ず1つの葉に振り分けます。1つの葉は「同じ条件をたどってきたサンプルの集まり」——つまりデータのセグメントです。

LightGBMのpredict(X, pred_leaf=True)は、各サンプルが**各木のどの葉に落ちたか(葉ID)**を返します。この葉IDを特徴量として使うと、GBDTを「セグメント抽出器」として使い、その結果を線形モデルに渡す、という組み合わせ(GBDT+線形モデル)が作れます。Facebookの広告クリック予測の論文(He et al., 2014)で知られる手法です。ダミーデータで試してみましょう。

pred_leaf の出力を見る

まず出力の形を確認します。二値分類のモデルを100本の木で学習し、pred_leaf=Trueで予測します。

import numpy as np
import lightgbm as lgb

def make(n=40000, seed=0):
    rng = np.random.RandomState(seed)
    X = rng.uniform(-1, 1, (n, 10))
    # 交互作用と非線形が主、線形成分(x4)は少しだけ
    logit = (3.0*X[:,0]*X[:,1]
             + 2.0*((X[:,2] > 0) & (X[:,3] > 0))
             - 2.0*((X[:,2] < 0) & (X[:,3] < 0))
             + 1.0*X[:,4] + rng.randn(n)*0.3)
    y = (rng.uniform(size=n) < 1/(1+np.exp(-logit))).astype(int)
    return X, y

X, y = make()
tr = np.arange(len(X)) < 30000
Xtr, Xte, ytr, yte = X[tr], X[~tr], y[tr], y[~tr]

params = dict(objective="binary", num_leaves=31, learning_rate=0.05,
              min_child_samples=30, verbose=-1, seed=0)
gbm = lgb.train(params, lgb.Dataset(Xtr, ytr), num_boost_round=100)

leaf = gbm.predict(Xtr, pred_leaf=True)
print("shape:", leaf.shape)             # (n_samples, n_trees)
print(leaf[:2, :10])                    # 先頭2サンプル・先頭10本の葉ID
shape: (30000, 100)
[[ 6  6 28  6 12  6 12  6  6 12]
 [ 3  3  3 25 17  3  3  3 27  3]]

木が100本なので、各サンプルは長さ100の葉IDベクトルになります。値は葉の番号(num_leaves=31なので0〜30)です。1行目のサンプルは木0で葉6、木1で葉6、木2で葉28…と振り分けられています。同じ木で同じ葉に落ちたサンプルどうしは、その木にとって同じセグメントです。

葉IDを特徴量にする(GBDT+線形モデル)

葉IDはそのままでは大小に意味のない番号(カテゴリ)なので、one-hotエンコードして線形モデルに渡します。木100本 × 葉の種類だけの疎なバイナリ特徴になります。

from sklearn.preprocessing import OneHotEncoder
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score

leaf_tr = gbm.predict(Xtr, pred_leaf=True)
leaf_te = gbm.predict(Xte, pred_leaf=True)

enc = OneHotEncoder(handle_unknown="ignore").fit(leaf_tr)
Ztr, Zte = enc.transform(leaf_tr), enc.transform(leaf_te)
print("one-hot後の次元:", Ztr.shape[1])

# 葉特徴は高次元なので、線形モデルは正則化を効かせる(C=0.1)
lr = LogisticRegression(max_iter=2000, C=0.1).fit(Ztr, ytr)
print("GBDT葉+線形 AUC:", roc_auc_score(yte, lr.predict_proba(Zte)[:,1]))
one-hot後の次元: 3100
GBDT葉+線形 AUC: 0.8226

葉特徴は3100次元(100本 × 各木の葉)と高次元になります。そのまま学習すると過学習しやすいので、線形モデルには正則化を効かせます(ここではC=0.1)。

精度比較

①生特徴の線形モデル、②GBDT単体、③GBDT葉+線形の3つを比べます。5シード平均のテストAUCです。

① 線形単体(生特徴)   AUC=0.7644
② GBDT単体            AUC=0.8345
③ GBDT葉+線形         AUC=0.8247

lightgbm-pred-leaf

生特徴の線形モデル(0.7644)は、このデータの交互作用・非線形をとらえきれません。一方、葉IDを特徴にした線形モデル(0.8247)は、線形→GBDTの差の約86%を埋めています。GBDTが見つけたセグメント(交互作用や非線形の切り分け)を特徴として渡すことで、線形モデルでもGBDTに近い表現力が得られる、ということです。

同時に、③はGBDT単体(0.8345)を超えてはいません。同じ特徴でGBDTと線形モデルを二段に重ねても、GBDT単体以上の精度が保証されるわけではない点は押さえておきます。

どんなときに使うか

表形式データで単に精度を上げたいだけなら、GBDT単体で十分なことが多く、この二段構成はむしろ手間が増えます。GBDT+線形が効くのは、次のような場面です。

  • 大規模な疎特徴と組み合わせたい:広告CTRのように、ユーザーIDや広告IDなど数百万次元の疎特徴を線形モデルで扱っている場合、そこにGBDT葉特徴を足すと、GBDTが学習した非線形・交互作用を線形システムに注入できる。上の実測で③がGBDT単体を超えなかったのは、GBDTと同じ特徴だけで二段に重ねたためで、GBDT葉特徴が真価を発揮するのは、こうしたGBDTが扱いにくい高次元の疎特徴と併用するときです。
  • 推論を線形モデルで軽くしたい:葉特徴さえ作れれば本体は線形モデルなので、オンライン学習や高速なサービングと相性が良い。
  • 特徴表現として使いたい:葉IDはGBDTが作った離散的なセグメント表現で、他モデルの入力特徴としても使えます。

注意点

  • pred_leaf=Trueの出力は(サンプル数, 木の本数)です。次元は木の本数に一致します。
  • one-hot後は「木の本数 × 葉の種類」の高次元疎特徴になります。線形モデルは正則化を前提にします。
  • num_iterationで使う木の本数を指定できます(一部の木だけで葉特徴を作る、など)。
  • GBDTの学習に使ったデータでそのまま葉特徴を作ると過学習しやすくなります。気になる場合は、GBDT学習用と線形モデル学習用でデータを分ける方法もあります。
  • predictには他にも、SHAP値を出すpred_contribなどのモードがあります。用途に応じて使い分けます。

まとめ

  • predict(X, pred_leaf=True)は、各サンプルが各木のどの葉に落ちたか(葉ID)を(サンプル数, 木の本数)で返す
  • 葉IDはGBDTが作ったセグメントで、one-hotして線形モデルに渡すとGBDT+線形モデルになる
  • 生特徴の線形モデルに比べAUCが大きく改善し、この例では線形→GBDTの差の約86%を埋めた(GBDT単体を超えはしない)
  • 疎特徴との併用や線形サービングなど、GBDTの表現力を線形モデルに持ち込みたい場面で効く

参考リンク