CatBoost

中級

2.4.8

CatBoost

最終更新 2020-05-06 読了時間 3 分
まとめ
  • CatBoostはYandexが開発した勾配ブースティング木で、カテゴリ変数をワンホットやラベルエンコーディングなしにそのまま扱える。
  • Ordered Target Statistics(順序付きターゲット統計量)により、ターゲットリークを防ぎながらカテゴリ変数を数値に変換する。
  • 対称木(Oblivious Tree)を採用し、全ノードで同じ分割条件を使うことで高速な推論と過学習の抑制を両立する。

直感 #

カテゴリ変数(都市名、商品カテゴリなど)を含むデータでは、前処理としてワンホットやラベルエンコーディングが必要になることが多い。CatBoostはカテゴリ変数をネイティブに処理し、ターゲット統計量を使って数値に変換する。さらに、学習データの順序をシャッフルしてから統計量を計算するため、ターゲットリーク(未来の情報の漏洩)を防ぐ仕組みが組み込まれている。

flowchart LR A["入力データ\n(カテゴリ+数値)"] --> B["Ordered Target\nStatistics"] B --> C["対称木\n(Oblivious Tree)"] C --> D["ブースティング\nで逐次追加"] D --> E{"収束?"} E -->|No| C E -->|Yes| F["最終予測"] style A fill:#2563eb,color:#fff style C fill:#1e40af,color:#fff style F fill:#10b981,color:#fff

アルゴリズムの詳細 #

Ordered Target Statistics(OTS) #

カテゴリ変数を数値に変換する際、単純にターゲット平均を使うとターゲットリークが発生します。サンプル\(x_i\)自身のラベル\(y_i\)が特徴量の計算に含まれてしまうためです。CatBoostはデータにランダムな順列\(\sigma\)を与え、各サンプルより前に出現したデータだけでターゲット統計量を計算します。

カテゴリ値\(c\)を持つサンプル\(x_{\sigma(i)}\)に対する変換値は以下で定義されます。

$$ \\hat{x}_{\\sigma(i)}^k = \\frac{\\sum_{j=1}^{i-1} \\mathbf{1}[x_{\\sigma(j)}^k = x_{\\sigma(i)}^k] \\cdot y_{\\sigma(j)} + a \\cdot p}{\\sum_{j=1}^{i-1} \\mathbf{1}[x_{\\sigma(j)}^k = x_{\\sigma(i)}^k] + a} $$

ここで\(p\)は事前確率(通常はデータセット全体のターゲット平均)、\(a\)はスムージングパラメーターです。\(a\)が大きいほど事前確率に近づき、カテゴリの出現頻度が少ない場合のノイズを抑えます。

予測シフトとOrdered Boosting #

通常の勾配ブースティングでは、\(t\)番目の木を学習する際の残差(勾配)が、同じ学習データで構築されたモデル\(F^{t-1}\)に基づいて計算されます。これにより予測シフト(conditional shift)が生じます。

$$ \\mathbb{E}[F^{t-1}(\\mathbf{x}_i) \\mid \\mathbf{x}_i] \\neq \\mathbb{E}[F^{t-1}(\\mathbf{x}) \\mid \\mathbf{x}] $$

学習データに対するモデルの予測は、テストデータに対する予測と系統的にずれるということです。CatBoostのOrdered Boostingはこれを以下のように解決します。

  1. 学習開始時にランダムな順列\(\sigma\)を生成します
  2. 各サンプル\(x_{\sigma(i)}\)の残差を、\(x_{\sigma(1)}, \dots, x_{\sigma(i-1)}\)だけで学習したモデルから計算します
  3. これにより、残差の計算に使うモデルとサンプル自身が独立になり、予測シフトが解消されます

各ブースティングステップで異なる順列を使うことで、バリアンスも制御されます。

Oblivious Decision Tree(対称木) #

CatBoostの基本学習器はOblivious Decision Tree(対称木)です。通常の決定木では各ノードが独立に分割条件を持ちますが、対称木では同じ深さのノードがすべて同じ分割条件(同じ特徴量・同じ閾値)を共有します。

深さ\(d\)の対称木は以下の性質を持ちます。

  • 分割条件の数は\(d\)個(深さごとに1つ)
  • 葉の数は\(2^d\)個
  • 任意のサンプルの所属する葉は、\(d\)個の二値条件の組み合わせ(ビットベクトル)で決まります
$$ \\text{leaf}(\\mathbf{x}) = \\sum_{j=1}^{d} 2^{j-1} \\cdot \\mathbf{1}[f_j(\\mathbf{x}) > t_j] $$

ここで\(f_j\)は深さ\(j\)で使われる特徴量、\(t_j\)はその閾値です。

通常の決定木Oblivious Tree
分割条件ノードごとに異なる同じ深さで共通
パラメーター数最大\(2^d - 1\)個\(d\)個
推論速度分岐ごとに条件判定ビット演算で高速
過学習耐性表現力が高く過学習しやすい制約が強く過学習しにくい
flowchart TD A["カテゴリ変数"] --> B["ランダム順列 σ を生成"] B --> C["Ordered Target Statistics\nσ(1)...σ(i-1) のみで統計量計算"] C --> D["数値特徴量と統合"] D --> E["Oblivious Tree\n深さdで同一分割条件"] E --> F["Ordered Boosting\n残差を独立なモデルから計算"] F --> G{"収束?"} G -->|No| E G -->|Yes| H["最終モデル"] style A fill:#2563eb,color:#fff style E fill:#1e40af,color:#fff style H fill:#10b981,color:#fff

参考リンク

Prokhorenkova, L., Gusev, G., Vorobev, A., Dorogush, A. V., & Gulin, A. (2018). CatBoost: unbiased boosting with categorical features. Advances in Neural Information Processing Systems (NeurIPS), 31.

詳細な解説 #

ライブラリと実験データ #

1
2
3
4
5
6
7
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score
from catboost import CatBoostClassifier

まず数値特徴量のみのデータで基本動作を確認します。

1
2
3
4
5
6
7
X, y = make_classification(
    n_samples=1000, n_features=20, n_informative=10,
    n_redundant=5, random_state=42
)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42
)

基本的な学習と評価 #

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
model = CatBoostClassifier(
    iterations=100,
    depth=6,
    learning_rate=0.1,
    random_state=42,
    verbose=0,
)
model.fit(X_train, y_train)
y_prob = model.predict_proba(X_test)[:, 1]
print(f"ROC-AUC: {roc_auc_score(y_test, y_prob):.4f}")

カテゴリ変数の処理 #

CatBoostの強みはカテゴリ変数をそのまま渡せることです。cat_featuresパラメーターでカテゴリ列のインデックスを指定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# カテゴリ変数を含むデータの例
np.random.seed(42)
n = 500
df = pd.DataFrame({
    "num_feat1": np.random.randn(n),
    "num_feat2": np.random.randn(n),
    "cat_feat1": np.random.choice(["A", "B", "C", "D"], n),
    "cat_feat2": np.random.choice(["low", "mid", "high"], n),
})
# カテゴリ変数に依存するターゲット
target = (
    (df["cat_feat1"].isin(["A", "B"])).astype(int)
    + (df["num_feat1"] > 0).astype(int)
    + np.random.binomial(1, 0.2, n)
) >= 2
target = target.astype(int)

X_train_c, X_test_c, y_train_c, y_test_c = train_test_split(
    df, target, test_size=0.3, random_state=42
)

model_cat = CatBoostClassifier(
    iterations=100, depth=6, learning_rate=0.1,
    random_state=42, verbose=0,
)
model_cat.fit(
    X_train_c, y_train_c,
    cat_features=["cat_feat1", "cat_feat2"],
)
y_prob_cat = model_cat.predict_proba(X_test_c)[:, 1]
print(f"ROC-AUC (カテゴリ変数あり): {roc_auc_score(y_test_c, y_prob_cat):.4f}")

depthの影響 #

CatBoostのdepthは対称木の深さを制御します。対称木では全ノードが同じ分割条件を使うため、深さが増えると指数的にノード数が増加します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
depths = [2, 4, 6, 8, 10]
scores = []
for d in depths:
    m = CatBoostClassifier(
        iterations=100, depth=d,
        learning_rate=0.1, random_state=42, verbose=0,
    )
    m.fit(X_train, y_train)
    scores.append(roc_auc_score(y_test, m.predict_proba(X_test)[:, 1]))

plt.figure(figsize=(8, 4))
plt.plot(depths, scores, "o-")
plt.xlabel("depth")
plt.ylabel("ROC-AUC")
plt.title("depthと精度の関係")
plt.grid(True)
plt.show()

l2_leaf_regの影響 #

葉の値に対するL2正則化の強さを制御します。大きくすると過学習を抑えられます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
regs = [0.1, 1.0, 3.0, 5.0, 10.0]
scores = []
for r in regs:
    m = CatBoostClassifier(
        iterations=100, depth=6,
        learning_rate=0.1, l2_leaf_reg=r,
        random_state=42, verbose=0,
    )
    m.fit(X_train, y_train)
    scores.append(roc_auc_score(y_test, m.predict_proba(X_test)[:, 1]))

plt.figure(figsize=(8, 4))
plt.plot(regs, scores, "o-")
plt.xlabel("l2_leaf_reg")
plt.ylabel("ROC-AUC")
plt.title("L2正則化と精度の関係")
plt.grid(True)
plt.show()

特徴量の重要度 #

1
2
3
4
5
6
7
8
9
importances = model.feature_importances_
indices = np.argsort(importances)[::-1]

plt.figure(figsize=(10, 5))
plt.bar(range(len(importances)), importances[indices])
plt.xlabel("特徴量インデックス")
plt.ylabel("重要度")
plt.title("CatBoost 特徴量重要度")
plt.show()

対称木(Oblivious Tree)とは #

CatBoostはデフォルトで対称木を使います。通常の決定木は各ノードで異なる特徴量・閾値で分割しますが、対称木は同じ深さのノードで同じ分割条件を使います。

  • 利点: 推論が高速(全データに同じ条件を一括適用できる)、過学習しにくい
  • 制約: 表現力は通常の木より限定的だが、アンサンブルで補う

depthを大きくすると対称木のノード数が指数的に増加し、メモリ使用量と計算時間が急増します。デフォルトのdepth=6から始め、l2_leaf_regで正則化を調整してください。

カテゴリ変数はcat_featuresで明示的に指定するだけで前処理なしに扱えます。GPU学習(task_type="GPU")を使うと大規模データでも高速に学習できます。ハイパーパラメーターの自動探索にはCatBoost組み込みのgrid_searchメソッドが便利です。

まとめ #

  • CatBoostはカテゴリ変数をネイティブに処理でき、ワンホットエンコーディングなしに学習できます。
  • Ordered Target Statisticsによりターゲットリークを防ぎながらカテゴリ変数を数値に変換します。
  • 対称木を採用することで高速な推論と過学習の抑制を両立しています。
  • depthl2_leaf_regの調整が精度と過学習のバランスを決める主要なハイパーパラメーターです。
  • Gradient Boosting(回帰) — 勾配ブースティングの基礎
  • XGBoost — 正則化重視の勾配ブースティング
  • LightGBM — ヒストグラム近似に特化した高速実装
  • SHAP — モデルの予測を特徴量ごとに分解して解釈
  • PDP / ICE — 特徴量と予測の関係を可視化

推定器数と CatBoost #

推定器数を増やすと予測がどう改善するか確認できます。