複数のLightGBMRegressorのモデルを作ろうとfor文の中でScikit-learnのRandomizedSearchCVを使ったら'Out of resources'というエラーが出ました。原因はよくわかりません。別プロセスに切り出すという力技で回避したので、その方法をメモ。

エラー発生状況

先日のエラー、'num_data < 0'とほぼ同様の良い感じに訓練されたLightGBMRessorのモデルを返してくれる関数。

※'num_data < 0'エラーは回避済みです。

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,
                             n_estimators=1000000, # means num_boost_round in core param
                             free_raw_data=True
                            )
    param_dist = {'boosting_type': ['dart', ],#'rf', 'gbdt'],
                    'num_leaves': sp.stats.randint(10, 1001),
                    'subsample_for_bin': sp.stats.randint(10, 1001),
                    'min_split_gain': sp.stats.uniform(1e-5, 5.0),
                    'min_child_weight': sp.stats.uniform(1e-5, 1e-2),
                    'reg_alpha': sp.stats.uniform(1e-5, 1e-1),
                    'reg_lambda': sp.stats.uniform(1e-5, 1e-1),
                    'tree_learner': ['data', 'feature', 'serial', 'voting' ],
                    'application': ['regression_l1', 'regression_l2', 'regression'],
                    'bagging_freq': sp.stats.randint(1, 101),
                    'bagging_fraction': sp.stats.uniform(loc=0.3, scale=0.7),
                    'feature_fraction': sp.stats.uniform(loc=0.3, scale=0.7),
                    'learning_rate': sp.stats.uniform(1e-5, 0.99),
                    'max_depth': sp.stats.randint(1, 501),
                    'gpu_use_dp': [True, ],# False],
                 }
    tscv = TimeSeriesSplit(n_splits=3)
    rscv = RandomizedSearchCV(
                              estimator=lgbm,
                              param_distributions=param_dist,
                              cv=tscv.get_n_splits([train_x, train_y]),
                              n_iter=5000,
                              n_jobs=4,
                              verbose=1,
                              refit=True,
                            )
    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,
                    )
    lgbm = rscv.best_estimator_
    return lgbm

これを銘柄リスト(TICKERS)のループの中で実行します。

if __name__ == '__main__':
    models = {}
    for ticker in TICKERS:
        print('========== ticker:%s =========='%ticker)
        x = get_x(jpstocks=[ticker,])
        x.dropna(inplace=True)
        y = get_y(ticker)
        y.dropna(inplace=True)
        pred_date, train_date, train_x, train_y, val_x, val_y, test_x, test_y, pred_x, truth_y = split_data(pred_date=None, x=x, y=y )
        # ↓ 2回目のループのここでout of reourcesエラー ↓
        model = get_lgbm(train_x, train_y, val_x, val_y)
        models[ticker] = model
        del model
        gc.collect()

2回目のループのget_lgbmで'Out of resources'というエラーが出ました。いつも通りオブジェクトの削除(del)とGC(gc.collect)を入れてみましたが効果なしでした。LightGBM単体、RandomizedSearchCVのn_jobを1で回したときには発生しないため、RandomizedSearchCVのマルチプロセス周りで何かゴミが残っているのかもしれません。

2018/3/9追記

どうやらRandomizedSearchCVではなく、LightGBMのエラーのようです。Resourcesというのがホスト(CPU、メインメモリ)なのか、GPU側なのかは依然不明です。

いずれにせよ検索しても原因はわからず、Scikit-learnに手をいれるわけにもいかないので、力技で回避することにします。

回避策

JupyterNotebookのNotebookをRestartすれば1回めのループは正常に終了します。このことからRandomizedSearchCVのfitを呼び出すプロセス自体を終了することができれば、正常に動くのでないかと考えます。

こんなイメージです。正しいかはわかりません。

回避前のイメージ

親:Jupyterのkernelプロセス = fitを呼び出すプロセス
子:n_jobで作られるプロセス

親プロセスで実行したfitの中で作られた子プロセスがうまく終了で来ていないのではないか?kernelのプロセスとfitを呼び出すプロセスを分けてしまえ!というわけです。
※力技。

回避後イメージ

親:Jupyterのkernelプロセス
子:fitを呼び出すプロセス
孫:n_jobで作られるプロセス

こんな感じです。親であるjupyterNotebookから上手いこと子プロセスを終了する事ができれば、kernel自体をrestartする必要もなくなります。多分。

コード

まずは子プロセスとして動かす関数です。

別プロセスとして動かすため、プロセス間通信のためのキューを受け取るように変更し、returnの代わりに親から渡されたキューを通じて訓練済みモデルを親プロセスに返します。

# 子から親に結果を渡すためのQueueを渡す
def get_lgbm(train_x:np.array, train_y:np.array, val_x:np.array, val_y:np.array, q:Queue) -> lgb.LGBMRegressor:
    lgbm = lgb.LGBMRegressor(
                             objective='regression',
                             device='gpu',
                             n_jobs=1,
                             n_estimators=1000000, # means num_boost_round in core param
                             free_raw_data=True
                            )
    param_dist = {'boosting_type': ['dart', ],#'rf', 'gbdt'],
                    'num_leaves': sp.stats.randint(10, 1001),
                    'subsample_for_bin': sp.stats.randint(10, 1001),
                    'min_split_gain': sp.stats.uniform(1e-5, 5.0),
                    'min_child_weight': sp.stats.uniform(1e-5, 1e-2),
                    'reg_alpha': sp.stats.uniform(1e-5, 1e-1),
                    'reg_lambda': sp.stats.uniform(1e-5, 1e-1),
                    'tree_learner': ['data', 'feature', 'serial', 'voting' ],
                    'application': ['regression_l1', 'regression_l2', 'regression'],
                    'bagging_freq': sp.stats.randint(1, 101),
                    'bagging_fraction': sp.stats.uniform(loc=0.3, scale=0.7),
                    'feature_fraction': sp.stats.uniform(loc=0.3, scale=0.7),
                    'learning_rate': sp.stats.uniform(1e-5, 0.99),
                    'max_depth': sp.stats.randint(1, 501),
                    'gpu_use_dp': [True, ],# False],
                 }
    tscv = TimeSeriesSplit(n_splits=3)
    rscv = RandomizedSearchCV(
                              estimator=lgbm,
                              param_distributions=param_dist,
                              cv=tscv.get_n_splits([train_x, train_y]),
                              n_iter=5000,
                              n_jobs=4,
                              verbose=1,
                              refit=True,
                            )
    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,
                    )
    lgbm = rscv.best_estimator_
    # 訓練済みのモデルをQueueを通じて親プロセスに返す
    q.put(lgbm)

上の関数を呼び出す処理です。

multiprocessingで明示的に子プロセスを作って、しっかり削除します。

from multiprocessing import Process, Queue

if __name__ == '__main__':
    models = {}
    for ticker in TICKERS:
        print('========== ticker:%s =========='%ticker)
        x = get_x(jpstocks=[ticker,])
        x.dropna(inplace=True)
        y = get_y(ticker)
        y.dropna(inplace=True)
        pred_date, train_date, train_x, train_y, val_x, val_y, test_x, test_y, pred_x, truth_y = split_data(pred_date=None, x=x, y=y )

        # 訓練済みのモデルを子プロセスから受け取るためのQueueを作成
        q = Queue()
        
        # 子プロセスを生成・実行
        p = Process(target=get_lgbm, args=(train_x, train_y, val_x, val_y, q))
        p.start()
        
        # 子プロセスの終了を待って、訓練済みモデルを受け取る
        p.join()
        models[ticker] = q.get()
        
        # 最後に子プロセスを削除する
        p.terminate()

2018/3/14追記

上記コードでOut of resourcesエラーは出なくなりましたが、p.join()が無限に待ち続けるようになってしまいました。LightGBMがうまく終了していないからリソースが開放されないのに、子孫のプロセスが終了するのを待っていたので、当たり前ですね。

p.join()を削除して、キューから学習済みモデルが取得できたら子プロセスを削除するように変更しました。いまのところうまく動いているようです。

        # Queueから学習済みモデルが取得できるまで待ち(ブロック)
        models[ticker] = q.get(block=True, timeout=None)
        
        # 最後に子プロセスを削除する
        p.terminate()

さいごに

今回は、1プロセスの中で複数回RandomizedSearchCVのfitを呼び出すケースでエラーが出ました。ググった限り、対策案が出てこなかったんですが、こういう使い方ってあまりしないんですかね。

今回は「プロセスを切り出して終わったらしっかり消す」という力技を使いました。もっとスマートなやり方があれば教えてください。