u++の備忘録

LightGBMでdownsampling+bagging

はじめに

新年初の技術系の記事です。

年末年始から最近にかけては、PyTorchの勉強などインプット重視で過ごしています。その一環で不均衡データの扱いも勉強しました。

上記のツイートを契機に多くのリプライなどで情報を頂戴しましたが、以前に話題になった「downsampling+bagging」の手法が良さそうでした。本記事では、模擬的に作成したデータセットにLightGBMを使い、「downsampling+bagging」の手法を試してみたいと思います。

tjo.hatenablog.com

データセットの作成

データセットの作成に当たっては、下記の記事を参考にしました。

blog.amedama.jp

from sklearn.datasets import make_classification
from sklearn.model_selection import StratifiedKFold
from sklearn.model_selection import StratifiedShuffleSplit

args = {
    'n_samples': 7000000,
    'n_features': 80,
    'n_informative': 3,
    'n_redundant': 0,
    'n_repeated': 0,
    'n_classes': 2,
    'n_clusters_per_class': 1,
    'weights': [0.99, 0.01],
    'random_state': 42,
}

X, y = make_classification(**args)

目的変数は{0, 1}の2値分類で、合計700万件のデータのうち正例(ラベル1)が約1%の不均衡データを作成しました。

f:id:upura:20190112140214p:plain

ラベルの割合が均等になるように、データを学習・検証・テスト用に分割しておきます。

def imbalanced_data_split(X, y, test_size=0.2):
    sss = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=0)
    for train_index, test_index in sss.split(X, y):
        X_train, X_test = X[train_index], X[test_index]
        y_train, y_test = y[train_index], y[test_index]
        return X_train, X_test, y_train, y_test

X_train, X_test, y_train, y_test = imbalanced_data_split(X, y, test_size=0.2)
# for validation
X_train2, X_valid, y_train2, y_valid = imbalanced_data_split(X_train, y_train, test_size=0.2)

LightGBM

まずは、普通にLightGBMを試してみます。

import lightgbm as lgb
from sklearn.metrics import roc_auc_score

lgbm_params = {
    'learning_rate': 0.1,
    'num_leaves': 8,
    'boosting_type' : 'gbdt',
    'reg_alpha' : 1,
    'reg_lambda' : 1,
    'objective': 'binary',
    'metric': 'auc',
}

def lgbm_train(X_train_df, X_valid_df, y_train_df, y_valid_df, lgbm_params):
    lgb_train = lgb.Dataset(X_train_df, y_train_df)
    lgb_eval = lgb.Dataset(X_valid_df, y_valid_df, reference=lgb_train)

    # 上記のパラメータでモデルを学習する
    model = lgb.train(lgbm_params, lgb_train,
                      # モデルの評価用データを渡す
                      valid_sets=lgb_eval,
                      # 最大で 1000 ラウンドまで学習する
                      num_boost_round=1000,
                      # 10 ラウンド経過しても性能が向上しないときは学習を打ち切る
                      early_stopping_rounds=10)
    
    return model

モデルの学習時間は2min 21sでした。

%%time
model_normal = lgbm_train(X_train2, X_valid, y_train2, y_valid, lgbm_params)
(前略)
[62]	valid_0's auc: 0.831404
Early stopping, best iteration is:
[52]	valid_0's auc: 0.831614
CPU times: user 2min 16s, sys: 4.87 s, total: 2min 21s
Wall time: 58.7 s

テストデータで予測してみたところ、aucで0.829287295077となりました。

y_pred_normal = model_normal.predict(X_test, num_iteration=model_normal.best_iteration)

# auc を計算する
auc = roc_auc_score(y_test, y_pred_normal)
print(auc)

downsampling

次いで、downsamplingを試してみます。

downsamplingは、不均衡データの多い方のラベルのデータを、少ない方のラベルのデータ数と等しくなるまでランダムに除外する手法です。今回の場合、負例(ラベル0)のデータを大量に捨ててしまいます。

imbalanced-learnというライブラリで、簡単に処理を記述できます。

imbalanced-learn.org

from imblearn.under_sampling import RandomUnderSampler

sampler = RandomUnderSampler(random_state=42)
# downsampling
X_resampled, y_resampled = sampler.fit_resample(X_train, y_train)
# for validation
X_train2, X_valid, y_train2, y_valid = imbalanced_data_split(X_resampled, y_resampled, test_size=0.2)

f:id:upura:20190112142420p:plain

(学習データの)正例の数に揃えているので、データサイズはかなり小さくなっています。

先ほどと同じくLightGBMで学習させたところ、モデルの学習時間は5.24 sまで短縮されました。

%%time
model_under_sample = lgbm_train(X_train2, X_valid, y_train2, y_valid, lgbm_params)
(前略)
[38]	valid_0's auc: 0.83336
Early stopping, best iteration is:
[28]	valid_0's auc: 0.833389
CPU times: user 5.02 s, sys: 229 ms, total: 5.24 s
Wall time: 2.76 s

テストデータで予測してみたところ、aucは0.828820480993になりました。aucは多少悪化しています。

手法 auc 実行時間
LightGBM 0.829287295077
2min 21s
LightGBM + downsampling 0.828820480993
5.24 s

downsampling+bagging

最後に、baggingを追加してみます。

baggingは、最初の不均衡データから重複を許して複数個のデータセットを作成し、それぞれ学習させたモデルをアンサンブルする手法です。

imbalanced-learnのRandomUnderSampler()では、replacementの引数をTrueにすることで、重複を許したデータ抽出を実行してくれます。

今回は乱数のseedを変えながら、10個のモデルを学習させてみます。

def bagging(seed):
    sampler = RandomUnderSampler(random_state=seed, replacement=True)
    X_resampled, y_resampled = sampler.fit_resample(X_train, y_train)
    X_train2, X_valid, y_train2, y_valid = imbalanced_data_split(X_resampled, y_resampled, test_size=0.2)
    model_bagging = lgbm_train(X_train2, X_valid, y_train2, y_valid, lgbm_params)
    return model_bagging

10個のモデルの学習時間は、1min 24sでした。

%%time
models = []

for i in range(10):
    models.append(bagging(i))
(前略)
CPU times: user 1min 17s, sys: 6.4 s, total: 1min 24s
Wall time: 47.9 s

今回のアンサンブルでは、それぞれのモデルで予測した結果の平均値を、全体の予測値とみなしてみます。
aucを計算したところ、単独のモデルよりも少々高い0.829094611662になりました。

y_preds = []

for m in models:
    y_preds.append(m.predict(X_test, num_iteration=m.best_iteration))

y_preds_bagging = sum(y_preds)/len(y_preds)
# auc を計算する
auc = roc_auc_score(y_test, y_preds_bagging)
print(auc)
手法 auc 実行時間
LightGBM 0.829287295077
2min 21s
LightGBM + downsampling 0.828820480993
5.24 s
LightGBM + downsampling + bagging (10 models) 0.829094611662
1min 24s

おわりに

本記事では不均衡データの扱い方の勉強として、LightGBMを使い、「downsampling+bagging」の手法を試しました。

当然ながらデータの不均衡度合いや大きさなどの特性に依存する部分が大きいと思いますが、今回作成したデータに関していえば、以下のような実感を抱きました。

  • downsamplingで大雑把にデータを捨てても、そこまで性能は悪化しない
  • 削減された実行時間を利用して、特徴量を増やしたりアンサンブルをしたりで性能を担保できそう

世の中で扱うデータには不均衡データが多いので、今後いろいろなデータに対して試していきたいアプローチだと思いました。

実装はGitHubで公開しています。
github.com