CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: CatBoost を使ってみる

今回は CatBoost という、機械学習の勾配ブースティング決定木 (Gradient Boosting Decision Tree) というアルゴリズムを扱うためのフレームワークを試してみる。 CatBoost は、同じ勾配ブースティング決定木を扱うフレームワークの LightGBMXGBoost と並んでよく用いられている。

CatBoost は学習にかかる時間が LightGBM や XGBoost に劣るものの、特にカテゴリカル変数を含むデータセットの扱いに定評がある。 ただし、今回使うデータセットはカテゴリカル変数を含まない点について先に断っておく。

使った環境は次の通り。

$ sw_vers                                     
ProductName:    Mac OS X
ProductVersion: 10.14.3
BuildVersion:   18D109
$ python -V            
Python 3.7.2

インストール

まずは pip を使って CatBoost をインストールする。 また、データセットの読み込みなどに使うため一緒に scikit-learn も入れておこう。

$ pip install catboost scikit-learn

基本的な使い方 (二値分類問題)

まずは Breast Cancer データセットを使った二値分類問題を CatBoost で処理してみよう。 以下のサンプルコードでは、CatBoost を学習させた上で汎化性能をホールドアウト検証で確認している。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from catboost import CatBoost
from catboost import Pool

from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score


def main():
    # 乳がんデータセットを読み込む
    dataset = datasets.load_breast_cancer()
    X, y = dataset.data, dataset.target
    # データセットを学習用と検証用に分割する
    X_train, X_test, y_train, y_test = train_test_split(X, y,
                                                        test_size=0.3,
                                                        shuffle=True,
                                                        random_state=42,
                                                        stratify=y)
    # CatBoost が扱うデータセットの形式に直す
    train_pool = Pool(X_train, label=y_train)
    test_pool = Pool(X_test, label=y_test)
    # 学習用のパラメータ
    params = {
        # タスク設定と損失関数
        'loss_function': 'Logloss',
        # 学習ラウンド数
        'num_boost_round': 100,
    }
    # モデルを学習する
    model = CatBoost(params)
    model.fit(train_pool)
    # 検証用データを分類する
    # NOTE: 確率がほしいときは prediction_type='Probability' を使う
    y_pred = model.predict(test_pool, prediction_type='Class')
    # 精度 (Accuracy) を検証する
    acc = accuracy_score(y_test, y_pred)
    print('Accuracy:', acc)


if __name__ == '__main__':
    main()

上記を実行してみよう。 各ラウンドでの学習データに対する損失と時間が表示される。 それぞれのラウンドでは決定木がひとつずつ作られている。

$ python helloworld.py 
Learning rate set to 0.100436
0: learn: 0.5477448   total: 108ms    remaining: 10.6s
1: learn: 0.4414628   total: 140ms    remaining: 6.85s
2: learn: 0.3561365   total: 172ms    remaining: 5.55s
...(snip)...
97:    learn: 0.0120305   total: 3.26s   remaining: 66.6ms
98:    learn: 0.0116963   total: 3.29s   remaining: 33.3ms
99:    learn: 0.0113142   total: 3.33s   remaining: 0us
Accuracy: 0.9532163742690059

最終的に精度 (Accuracy) で約 0.953 という結果が得られた。

ラウンド数を増やしてみる

先ほどの例ではラウンド数、つまり用いる決定木の数が最大で 100 本だった。 次は、試しにこの数を 1,000 まで増やしてみよう。

以下のサンプルコードではラウンド数を増やした上で、最も最適なラウンド数を用いて最終的な性能を確認している。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from catboost import CatBoost
from catboost import Pool

from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score


def main():
    dataset = datasets.load_breast_cancer()
    X, y = dataset.data, dataset.target

    X_train, X_test, y_train, y_test = train_test_split(X, y,
                                                        test_size=0.3,
                                                        shuffle=True,
                                                        random_state=42,
                                                        stratify=y)

    train_pool = Pool(X_train, label=y_train)
    test_pool = Pool(X_test, label=y_test)

    params = {
        'loss_function': 'Logloss',
        'num_boost_round': 1000,  # ラウンド数を多めにしておく
    }

    model = CatBoost(params)
    # 検証用データに対する損失を使って学習課程を評価する
    # 成果物となるモデルには、それが最も良かったものを使う
    model.fit(train_pool, eval_set=[test_pool], use_best_model=True)

    y_pred = model.predict(test_pool, prediction_type='Class')

    acc = accuracy_score(y_test, y_pred)
    print('Accuracy:', acc)


if __name__ == '__main__':
    main()

上記を実行してみる。

$ python valset.py 
Learning rate set to 0.070834
0: learn: 0.5872722   test: 0.5996097  best: 0.5996097 (0)   total: 109ms    remaining: 1m 49s
1: learn: 0.5025692   test: 0.5240925  best: 0.5240925 (1)   total: 140ms    remaining: 1m 9s
2: learn: 0.4279025   test: 0.4535490  best: 0.4535490 (2)   total: 172ms    remaining: 57s
...(snip)...
997:   learn: 0.0007559   test: 0.0891592  best: 0.0872851 (771) total: 37s  remaining: 74.1ms
998:   learn: 0.0007554   test: 0.0891578  best: 0.0872851 (771) total: 37s  remaining: 37ms
999:   learn: 0.0007543   test: 0.0891842  best: 0.0872851 (771) total: 37s  remaining: 0us

bestTest = 0.08728505181
bestIteration = 771

Shrink model to first 772 iterations.
Accuracy: 0.9590643274853801

最終的に、学習データの損失において最も優れていたのは 771 ラウンドで、そのときの精度 (Accuracy) は約 0.959 だった。 先ほどよりも精度が 0.6 ポイント改善していることが分かる。

学習の過程を可視化する

続いては、モデルが学習する過程を可視化してみよう。 次のサンプルコードでは 1,000 ラウンド回した場合の、学習データと検証データに対する損失の変化を可視化している。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from catboost import CatBoost
from catboost import Pool

from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from matplotlib import pyplot as plt


def main():
    dataset = datasets.load_breast_cancer()
    X, y = dataset.data, dataset.target

    X_train, X_test, y_train, y_test = train_test_split(X, y,
                                                        test_size=0.3,
                                                        shuffle=True,
                                                        random_state=42,
                                                        stratify=y)

    train_pool = Pool(X_train, label=y_train)
    test_pool = Pool(X_test, label=y_test)

    params = {
        'loss_function': 'Logloss',
        'num_boost_round': 1000,
    }

    model = CatBoost(params)
    model.fit(train_pool, eval_set=[test_pool], use_best_model=True)

    y_pred = model.predict(test_pool, prediction_type='Class')

    acc = accuracy_score(y_test, y_pred)
    print('Accuracy:', acc)

    # メトリックの推移を取得する
    history = model.get_evals_result()
    # グラフにプロットする
    train_metric = history['learn']['Logloss']
    plt.plot(train_metric, label='train metric')
    eval_metric = history['validation_0']['Logloss']
    plt.plot(eval_metric, label='eval metric')

    plt.legend()
    plt.grid()
    plt.show()


if __name__ == '__main__':
    main()

上記を実行してみる。

$ python visualize.py 
Learning rate set to 0.070834
0: learn: 0.5872722   test: 0.5996097  best: 0.5996097 (0)   total: 117ms    remaining: 1m 56s
1: learn: 0.5025692   test: 0.5240925  best: 0.5240925 (1)   total: 150ms    remaining: 1m 14s
2: learn: 0.4279025   test: 0.4535490  best: 0.4535490 (2)   total: 182ms    remaining: 1m
...(snip)...
997:   learn: 0.0007559   test: 0.0891592  best: 0.0872851 (771) total: 36.6s   remaining: 73.3ms
998:   learn: 0.0007554   test: 0.0891578  best: 0.0872851 (771) total: 36.6s   remaining: 36.7ms
999:   learn: 0.0007543   test: 0.0891842  best: 0.0872851 (771) total: 36.7s   remaining: 0us

bestTest = 0.08728505181
bestIteration = 771

Shrink model to first 772 iterations.
Accuracy: 0.9590643274853801

最終的な結果は先ほどと変わらない。

学習が完了すると、次のようなグラフが得られる。

f:id:momijiame:20190216194828p:plain

上記を見ると 200 ラウンドを待たずに検証用データに対する損失は底を打っていることが分かる。

学習が進まなくなったら打ち切る

ここまで見てきた通り、CatBoost ではとりあえず多めのラウンドを回して、最終的に良かったものを使うことができる。 とはいえ、一旦学習が進まなくなると、そこからまた改善するということはそんなにない。 大抵の場合は大して改善しない状況が続くか、過学習が進みやすい。 そこで、続いては学習が進まなくなったらそこで打ち切る Early Stopping という機能を使ってみる。 この機能は LightGBM や XGBoost でも実装されており、よく用いられるものの一つとなっている。

次のサンプルコードでは Early Stopping を用いて 1,000 ラウンド回す中で、検証用データの損失が 10 ラウンド改善しなかったらそこで学習を打ち切るようになっている。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from catboost import CatBoost
from catboost import Pool

from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score


def main():
    dataset = datasets.load_breast_cancer()
    X, y = dataset.data, dataset.target

    X_train, X_test, y_train, y_test = train_test_split(X, y,
                                                        test_size=0.3,
                                                        shuffle=True,
                                                        random_state=42,
                                                        stratify=y)

    train_pool = Pool(X_train, label=y_train)
    test_pool = Pool(X_test, label=y_test)

    params = {
        'loss_function': 'Logloss',
        'num_boost_round': 1000,
        # 検証用データの損失が既定ラウンド数減らなかったら学習を打ち切る
        'early_stopping_rounds': 10,
    }

    model = CatBoost(params)
    model.fit(train_pool, eval_set=[test_pool])

    y_pred = model.predict(test_pool, prediction_type='Class')

    acc = accuracy_score(y_test, y_pred)
    print('Accuracy:', acc)


if __name__ == '__main__':
    main()

上記を実行してみる。

$ python earlystop.py 
Learning rate set to 0.070834
0: learn: 0.5872722   test: 0.5996097  best: 0.5996097 (0)   total: 115ms    remaining: 1m 55s
1: learn: 0.5025692   test: 0.5240925  best: 0.5240925 (1)   total: 150ms    remaining: 1m 15s
2: learn: 0.4279025   test: 0.4535490  best: 0.4535490 (2)   total: 183ms    remaining: 1m
...(snip)...
136:   learn: 0.0139911   test: 0.0939635  best: 0.0932902 (128) total: 5.52s   remaining: 34.8s
137:   learn: 0.0139071   test: 0.0936872  best: 0.0932902 (128) total: 5.55s   remaining: 34.7s
138:   learn: 0.0138493   test: 0.0938802  best: 0.0932902 (128) total: 5.59s   remaining: 34.6s
Stopped by overfitting detector  (10 iterations wait)

bestTest = 0.09329017842
bestIteration = 128

Shrink model to first 129 iterations.
Accuracy: 0.9590643274853801

今度は 138 ラウンドで学習が止まったものの、最終的な汎化性能は先ほどと変わらないものが得られている。 このように Early Stopping は計算量を節約する上で重要な機能になっている。

scikit-learn インターフェースを使ってみる

CatBoost には scikit-learn のインターフェースもある。 続いてはそれを使ってみることにしよう。

以下のサンプルコードでは scikit-learn のインターフェースを備えた CatBoostClassifier を使っている。 これを用いることで使い慣れた scikit-learn のオブジェクトとして CatBoost を扱うことができる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from catboost import CatBoostClassifier

from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score


def main():
    dataset = datasets.load_breast_cancer()
    X, y = dataset.data, dataset.target

    X_train, X_test, y_train, y_test = train_test_split(X, y,
                                                        test_size=0.3,
                                                        shuffle=True,
                                                        random_state=42,
                                                        stratify=y)

    # scikit-learn インターフェースを備えたラッパー
    model = CatBoostClassifier(num_boost_round=1000,
                               loss_function='Logloss',
                               early_stopping_rounds=10,
                               )
    model.fit(X_train, y_train, eval_set=[(X_test, y_test)])
    # 確率がほしいときは predict_proba() を使う
    y_pred = model.predict(X_test)

    acc = accuracy_score(y_test, y_pred)
    print('Accuracy:', acc)


if __name__ == '__main__':
    main()

上記を実行してみる。

$ python skapi.py 
Learning rate set to 0.070834
0: learn: 0.5872722   test: 0.5996097  best: 0.5996097 (0)   total: 112ms    remaining: 1m 51s
1: learn: 0.5025692   test: 0.5240925  best: 0.5240925 (1)   total: 143ms    remaining: 1m 11s
2: learn: 0.4279025   test: 0.4535490  best: 0.4535490 (2)   total: 176ms    remaining: 58.4s
...(snip)...
136:   learn: 0.0139911   test: 0.0939635  best: 0.0932902 (128) total: 5.8s    remaining: 36.5s
137:   learn: 0.0139071   test: 0.0936872  best: 0.0932902 (128) total: 5.83s   remaining: 36.4s
138:   learn: 0.0138493   test: 0.0938802  best: 0.0932902 (128) total: 5.88s   remaining: 36.4s
Stopped by overfitting detector  (10 iterations wait)

bestTest = 0.09329017842
bestIteration = 128

Shrink model to first 129 iterations.
Accuracy: 0.9590643274853801

結果は先ほどと変わらない。

多値分類問題を処理してみる

ここまでは二値分類問題を扱ってきた。 続いては Iris データセットを用いて多値分類問題を解かせてみよう。

次のサンプルコードでは Iris データセットを CatBoost で分類している。 検証方法は先ほどと同じホールドアウト検証で、精度 (Accuracy) について評価している。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from catboost import CatBoost
from catboost import Pool

from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score


def main():
    # Iris データセットを読み込む
    dataset = datasets.load_iris()
    X, y = dataset.data, dataset.target

    X_train, X_test, y_train, y_test = train_test_split(X, y,
                                                        test_size=0.3,
                                                        shuffle=True,
                                                        random_state=42,
                                                        stratify=y)

    train_pool = Pool(X_train, label=y_train)
    test_pool = Pool(X_test, label=y_test)

    params = {
        # 多値分類問題
        'loss_function': 'MultiClass',
        'num_boost_round': 1000,
        'early_stopping_rounds': 10,
    }

    model = CatBoost(params)
    model.fit(train_pool, eval_set=[test_pool])

    y_pred = model.predict(test_pool, prediction_type='Class')

    acc = accuracy_score(y_test, y_pred)
    print('Accuracy:', acc)


if __name__ == '__main__':
    main()

上記の実行結果は次の通り。

$ python multiclass.py 
0: learn: -1.0663112  test: -1.0680680 best: -1.0680680 (0)  total: 65.3ms  remaining: 1m 5s
1: learn: -1.0369337  test: -1.0404371 best: -1.0404371 (1)  total: 74.7ms  remaining: 37.3s
2: learn: -1.0094257  test: -1.0172382 best: -1.0172382 (2)  total: 78.9ms  remaining: 26.2s
...(snip)...
268:   learn: -0.0561336  test: -0.1770632 best: -0.1768477 (260)    total: 986ms    remaining: 2.68s
269:   learn: -0.0557735  test: -0.1773530 best: -0.1768477 (260)    total: 991ms    remaining: 2.68s
270:   learn: -0.0556859  test: -0.1772400 best: -0.1768477 (260)    total: 994ms    remaining: 2.67s
Stopped by overfitting detector  (10 iterations wait)

bestTest = -0.1768476949
bestIteration = 260

Shrink model to first 261 iterations.
Accuracy: 0.9111111111111111

特徴量の重要度を可視化する

CatBoost が採用している勾配ブースティング決定木は名前の通り決定木の仲間なので特徴量の重要度を取得できる。 続いては特徴量の重要度を可視化してみよう。

以下のサンプルコードでは、先ほどと同じ条件で学習したモデルから特徴量の重要度を取得してグラフにプロットしている。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from catboost import CatBoost
from catboost import Pool

from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from matplotlib import pyplot as plt


def main():
    # Iris データセットを読み込む
    dataset = datasets.load_iris()
    X, y = dataset.data, dataset.target

    X_train, X_test, y_train, y_test = train_test_split(X, y,
                                                        test_size=0.3,
                                                        shuffle=True,
                                                        random_state=42,
                                                        stratify=y)

    train_pool = Pool(X_train, label=y_train)
    test_pool = Pool(X_test, label=y_test)

    params = {
        'loss_function': 'MultiClass',
        'num_boost_round': 1000,
        'early_stopping_rounds': 10,
    }

    model = CatBoost(params)
    model.fit(train_pool, eval_set=[test_pool])

    y_pred = model.predict(test_pool, prediction_type='Class')

    acc = accuracy_score(y_test, y_pred)
    print('Accuracy:', acc)

    # 特徴量の重要度を取得する
    feature_importance = model.get_feature_importance()
    # 棒グラフとしてプロットする
    plt.figure(figsize=(12, 4))
    plt.barh(range(len(feature_importance)),
            feature_importance,
            tick_label=dataset.feature_names)

    plt.xlabel('importance')
    plt.ylabel('features')
    plt.grid()
    plt.show()


if __name__ == '__main__':
    main()

上記を実行してみる。

$ python featimp.py   
0: learn: -1.0663112  test: -1.0680680 best: -1.0680680 (0)  total: 61.7ms  remaining: 1m 1s
1: learn: -1.0369337  test: -1.0404371 best: -1.0404371 (1)  total: 67.2ms  remaining: 33.6s
2: learn: -1.0094257  test: -1.0172382 best: -1.0172382 (2)  total: 71ms remaining: 23.6s
...(snip)...
268:   learn: -0.0561336  test: -0.1770632 best: -0.1768477 (260)    total: 961ms    remaining: 2.61s
269:   learn: -0.0557735  test: -0.1773530 best: -0.1768477 (260)    total: 964ms    remaining: 2.61s
270:   learn: -0.0556859  test: -0.1772400 best: -0.1768477 (260)    total: 967ms    remaining: 2.6s
Stopped by overfitting detector  (10 iterations wait)

bestTest = -0.1768476949
bestIteration = 260

Shrink model to first 261 iterations.
Accuracy: 0.9111111111111111

得られたグラフは次の通り。

f:id:momijiame:20190216201841p:plain

上記から Petal length と Petal width が分類において有効に作用していたことが確認できる。

回帰問題を処理してみる

続いては回帰問題を扱ってみる。 以下のサンプルコードでは Boston データセットを CatBoost で回帰している。 汎化性能の確認してはホールドアウト検証で RMSE (Root Mean Squared Error) を評価している。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import math

from catboost import CatBoost
from catboost import Pool

from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error


def main():
    # Boston データセットを読み込む
    dataset = datasets.load_boston()
    X, y = dataset.data, dataset.target

    X_train, X_test, y_train, y_test = train_test_split(X, y,
                                                        test_size=0.3,
                                                        shuffle=True,
                                                        random_state=42)

    train_pool = Pool(X_train, label=y_train)
    test_pool = Pool(X_test, label=y_test)

    params = {
        # 損失関数に RMSE を使う
        'loss_function': 'RMSE',
        'num_boost_round': 1000,
        'early_stopping_rounds': 10,
    }

    model = CatBoost(params)
    model.fit(train_pool, eval_set=[test_pool])

    y_pred = model.predict(test_pool)

    # 最終的なモデルの RMSE を計算する
    mse = mean_squared_error(y_test, y_pred)
    print('RMSE:', math.sqrt(mse))


if __name__ == '__main__':
    main()

上記を実行してみる。

$ python regress.py 
0: learn: 24.2681654  test: 22.5038434 best: 22.5038434 (0)  total: 75.5ms  remaining: 1m 15s
1: learn: 23.6876829  test: 21.9483221 best: 21.9483221 (1)  total: 84.6ms  remaining: 42.2s
2: learn: 23.1173979  test: 21.3856082 best: 21.3856082 (2)  total: 92.1ms  remaining: 30.6s
...(snip)...
466:   learn: 2.4850147   test: 3.3697155  best: 3.3675366 (458) total: 4.11s   remaining: 4.69s
467:   learn: 2.4833511   test: 3.3700655  best: 3.3675366 (458) total: 4.13s   remaining: 4.69s
468:   learn: 2.4828831   test: 3.3701698  best: 3.3675366 (458) total: 4.14s   remaining: 4.69s
Stopped by overfitting detector  (10 iterations wait)

bestTest = 3.36753664
bestIteration = 458

Shrink model to first 459 iterations.
RMSE: 3.367536638941265

いじょう。