LightGBMのランダムサーチで'Check failed: num_data > 0'エラー
4 min read

LightGBMのランダムサーチで'Check failed: num_data > 0'エラー

システムトレード関連でLightGBMRegressorのパラメータをScikit-learnのRandomizedSearchCVでチューニングをしていてはまりました。Stack Overflow(英語)にも上げたんですが、なかなか回答がないので、日本語でも書いておくことにしました。
LightGBMのランダムサーチで'Check failed: num_data > 0'エラー

システムトレード関連でLightGBMRegressorのパラメータをScikit-learnのRandomizedSearchCVでチューニングをしていてハマりました。Stack Overflow(英語)にも上げたんですが、なかなか回答がないので、日本語でも書いておくことにしました。
LightGBMError “Check failed: num_data > 0” with Sklearn RandomizedSearchCV

ソースコード

実際のコードとは少し違うんですが、こんな感じのチューニングされたLightGBMRegressorのモデルを返す関数を実装しました。

import lightgbm as lgb
from sklearn.model_selection import RandomizedSearchCV
import scipy as sp
import numpy as np

def get_lgbm(train_x:np.array, train_y:np.array, val_x:np.array, val_y:np.array) -> lgb.LGBMRegressor:
    # ↑ この型の書き方あってる?
    lgbm = lgb.LGBMRegressor(
                             objective='regression',
                             device='gpu',
                             n_jobs=1,
                            )
    param_dist = {'boosting_type': ['gbdt', 'dart', 'rf'],
                    'num_leaves': sp.stats.randint(2, 1001),
                    'subsample_for_bin': sp.stats.randint(10, 1001),
                    'min_split_gain': sp.stats.uniform(0, 5.0),
                    'min_child_weight': sp.stats.uniform(1e-6, 1e-2),
                    'reg_alpha': sp.stats.uniform(0, 1e-2),
                    'reg_lambda': sp.stats.uniform(0, 1e-2),
                    'tree_learner': ['data', 'feature', 'serial', 'voting' ],
                    'application': ['regression_l1', 'regression_l2', 'regression'],
                    'bagging_freq': sp.stats.randint(1, 11),
                    'bagging_fraction': sp.stats.uniform(1e-3, 0.99),
                    'feature_fraction': sp.stats.uniform(1e-3, 0.99),
                    'learning_rate': sp.stats.uniform(1e-6, 0.99),
                    'max_depth': sp.stats.randint(1, 501),
                    'n_estimators': sp.stats.randint(100, 20001),
                    'gpu_use_dp': [True, False],
                 }
    rscv = RandomizedSearchCV(
                              estimator=lgbm,
                              param_distributions=param_dist,
                              cv=3,
                              n_iter=3000, # イテレーションが少ないと起きない
                              n_jobs=4,
                              verbose=1,
                              refit=True,
                            )
    # ↓↓↓ RandomizedSearchCVでfitしている最中に発生↓↓↓
    rscv = rscv.fit(train_x, 
                    train_y.ravel(), 
                    eval_set=(val_x, val_y.ravel()),
                    early_stopping_rounds=1,
                    eval_metric=['l2', 'l1'],
                    verbose=False,
                    )
    return rscv.best_estimator_

学習データ

学習データはXの方が1631日分、1565変数(=特徴量ってやつ?)です。中身としては、数年分の指数、為替などですね。主に変化率とかその辺を入れてますが、マクロな指標などは使っていません。

エラー

すごく長いので、全部は載せられないのですが、大まかに3つに大別できるような気がします。本質的には、RandomizedSearchCVで生成したパラメータでLightGBMが落ちたんだと思ってます。

※階層的にはこんな感じ?

  1. LightGBMRegressorのfit中に発生したエラー(本質的なエラー?)
  2. RandomizedSearchCVのエラー
  3. JupyterNotebookのエラー

LightGBMError: b'Check failed: num_data > 0 at...

どうやらBoosterを生成する前にチェックをしているようで、そこでエラーが発生しているようです。

...........................................................................
/opt/conda/lib/python3.6/site-packages/lightgbm/engine.py in train(params={'application': 'regression_l1', 'bagging_fraction': 0.0013516565394267757, 'bagging_freq': 8, 'boosting_type': 'dart', 'colsample_bytree': 1.0, 'device': 'gpu', 'feature_fraction': 0.18574060093496944, 'gpu_use_dp': True, 'learning_rate': 0.06354739024799887, 'max_depth': 267, ...}, train_set=<lightgbm.basic.Dataset object>, num_boost_round=11610, valid_sets=[<lightgbm.basic.Dataset object>], valid_names=None, fobj=None, feval=None, init_model=None, feature_name='auto', categorical_feature='auto', early_stopping_rounds=1, evals_result={}, verbose_eval=False, learning_rates=None, keep_training_booster=False, callbacks={<function print_evaluation.<locals>.callback>, <function early_stopping.<locals>.callback>, <function record_evaluation.<locals>.callback>})
    175     callbacks_before_iter = sorted(callbacks_before_iter, key=attrgetter('order'))
    176     callbacks_after_iter = sorted(callbacks_after_iter, key=attrgetter('order'))
    177 
    178     # construct booster
    179     try:
--> 180         booster = Booster(params=params, train_set=train_set)
        booster = undefined
        params = {'application': 'regression_l1', 'bagging_fraction': 0.0013516565394267757, 'bagging_freq': 8, 'boosting_type': 'dart', 'colsample_bytree': 1.0, 'device': 'gpu', 'feature_fraction': 0.18574060093496944, 'gpu_use_dp': True, 'learning_rate': 0.06354739024799887, 'max_depth': 267, ...}
        train_set = <lightgbm.basic.Dataset object>
    181         if is_valid_contain_train:
    182             booster.set_train_data_name(train_data_name)
    183         for valid_set, name_valid_set in zip(reduced_valid_sets, name_valid_sets):
    184             booster.add_valid(valid_set, name_valid_set)

...........................................................................
/opt/conda/lib/python3.6/site-packages/lightgbm/basic.py in __init__(self=<lightgbm.basic.Booster object>, params={'application': 'regression_l1', 'bagging_fraction': 0.0013516565394267757, 'bagging_freq': 8, 'boosting_type': 'dart', 'colsample_bytree': 1.0, 'device': 'gpu', 'feature_fraction': 0.18574060093496944, 'gpu_use_dp': True, 'learning_rate': 0.06354739024799887, 'max_depth': 267, ...}, train_set=<lightgbm.basic.Dataset object>, model_file=None, silent=False)
   1290             # construct booster object
   1291             self.handle = ctypes.c_void_p()
   1292             _safe_call(_LIB.LGBM_BoosterCreate(
   1293                 train_set.construct().handle,
   1294                 c_str(params_str),
-> 1295                 ctypes.byref(self.handle)))
        self.handle = c_void_p(None)
   1296             # save reference to data
   1297             self.train_set = train_set
   1298             self.valid_sets = []
   1299             self.name_valid_sets = []

...........................................................................
/opt/conda/lib/python3.6/site-packages/lightgbm/basic.py in _safe_call(ret=-1)
     43     ----------
     44     ret : int
     45         return value from API calls
     46     """
     47     if ret != 0:
---> 48         raise LightGBMError(_LIB.LGBM_GetLastError())
     49 
     50 
     51 def is_numeric(obj):
     52     """Check is a number or not, include numpy number etc."""

LightGBMError: b'Check failed: num_data > 0 at /usr/local/src/lightgbm/LightGBM/src/io/dataset.cpp, line 27 .\n'

問題点と解決策のアタリ

LightGBMのソースを読むところまでは至っていないため、状況から予想するしか無いのですが、、、

問題点

  • LightGBMが木構造であること
  • イテレーションを増やしたときに発生していること

これらの状況から、学習データを木構造で分岐できないパラメータが選ばれたときに発生するんじゃないかなと考えています。

解決策

  1. 学習データの特徴とGBDTのアルゴリズムを考慮し、適切なパラメータ範囲を設定する
  2. よくわからんけど、エラーが発生しても無視して次のパラメータを選択し、継続する

現実的には2.かなと思っています。1.の学習データの特徴を捉えることは出来そうですが、GBDTを理解するには、自分ではスペック不足な気がします。もちろん、知っておくほうが幅は広がりますが、結果的に使えれば良いのです。

ざっと見た感じ、引数などで回避できなさそうなので、LightGBMのPythonのソースに手を入れる必要がありそうです。この180行目あたり -> lightgbm/engine.py

さいごに

Stack Overflowに質問上げましたが、エラーが上記の予想通り学習データとパラメータの関係で起きているエラーなのだとすれば、データ解析もやってくれ、と言っているようなものですので、無謀でした。

(再掲)
LightGBMError “Check failed: num_data > 0” with Sklearn RandomizedSearchCV

とりあえず、上に書いた解決策2.をやるべく引続き調査を進めていこうと思います。

2018/3/9追記

bagging_fractionとfeature_fractionが小さすぎたのが原因かもしれません。乱数の範囲を下記のように変更してからは発生しなくなりました。小さすぎるとランダムに選択する際にデータが0件になる?そんな感じなのかな。

{
    'bagging_fraction': sp.stats.uniform(loc=0.1, scale=0.9), 
    'feature_fraction': sp.stats.uniform(loc=0.1, scale=0.9)
}