LightGBMでdownsampling+bagging
はじめに
新年初の技術系の記事です。
年末年始から最近にかけては、PyTorchの勉強などインプット重視で過ごしています。その一環で不均衡データの扱いも勉強しました。
クラス比1:99の人工的な不均衡データ使ってダウンサンプリングを試してるけど、バカみたいに負例を捨てちゃっても意外と大丈夫なんだな。計算時間が圧倒的に減るので、その時間でアンサンブル的なことすれば精度も確保できそう。
— u++ (@upura0) January 8, 2019
上記のツイートを契機に多くのリプライなどで情報を頂戴しましたが、以前に話題になった「downsampling+bagging」の手法が良さそうでした。本記事では、模擬的に作成したデータセットにLightGBMを使い、「downsampling+bagging」の手法を試してみたいと思います。
imbalanced data に対する対処を勉強していたのだけど,[Wallace et al. ICDM'11] https://t.co/ltQ942lKPm … で「undersampling + bagging をせよ」という結論が出ていた.
— ™ (@tmaehara) July 29, 2017
データセットの作成
データセットの作成に当たっては、下記の記事を参考にしました。
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%の不均衡データを作成しました。
ラベルの割合が均等になるように、データを学習・検証・テスト用に分割しておきます。
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というライブラリで、簡単に処理を記述できます。
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)
(学習データの)正例の数に揃えているので、データサイズはかなり小さくなっています。
先ほどと同じく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