From 46fee4332f428345c2687cabe330cf17012d3343 Mon Sep 17 00:00:00 2001 From: Olcmyk Date: Sat, 23 May 2026 19:16:14 +0800 Subject: [PATCH] fix: LightGBM 4.0+ compatibility for early_stopping_rounds=None - Only create early_stopping callback when rounds is not None - LightGBM 4.0+ requires stopping_rounds to be an integer, not None - Apply fix to gbdt.py, ddgda.py, and highfreq_gdbt_model.py - Follow the pattern already used in double_ensemble.py This fixes the TypeError that occurs when early_stopping_rounds=None is passed to lgb.early_stopping() in LightGBM 4.0+. Affected files: - qlib/contrib/model/gbdt.py: Core LGBModel fix - qlib/contrib/rolling/ddgda.py: Remove explicit None assignment - qlib/contrib/model/highfreq_gdbt_model.py: Add robustness check --- qlib/contrib/model/gbdt.py | 19 +++++++++++++------ qlib/contrib/model/highfreq_gdbt_model.py | 13 +++++++++---- qlib/contrib/rolling/ddgda.py | 4 +++- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/qlib/contrib/model/gbdt.py b/qlib/contrib/model/gbdt.py index 22c29cd4997..0155cbb88db 100644 --- a/qlib/contrib/model/gbdt.py +++ b/qlib/contrib/model/gbdt.py @@ -68,19 +68,26 @@ def fit( evals_result = {} # in case of unsafety of Python default values ds_l = self._prepare_data(dataset, reweighter) ds, names = list(zip(*ds_l)) - early_stopping_callback = lgb.early_stopping( - self.early_stopping_rounds if early_stopping_rounds is None else early_stopping_rounds - ) + + # Build callbacks list + callbacks = [] + + # Only add early_stopping callback if rounds is not None (LightGBM 4.0+ doesn't accept None) + early_stop_rounds = self.early_stopping_rounds if early_stopping_rounds is None else early_stopping_rounds + if early_stop_rounds is not None: + callbacks.append(lgb.early_stopping(early_stop_rounds)) + # NOTE: if you encounter error here. Please upgrade your lightgbm - verbose_eval_callback = lgb.log_evaluation(period=verbose_eval) - evals_result_callback = lgb.record_evaluation(evals_result) + callbacks.append(lgb.log_evaluation(period=verbose_eval)) + callbacks.append(lgb.record_evaluation(evals_result)) + self.model = lgb.train( self.params, ds[0], # training dataset num_boost_round=self.num_boost_round if num_boost_round is None else num_boost_round, valid_sets=ds, valid_names=names, - callbacks=[early_stopping_callback, verbose_eval_callback, evals_result_callback], + callbacks=callbacks, **kwargs, ) for k in names: diff --git a/qlib/contrib/model/highfreq_gdbt_model.py b/qlib/contrib/model/highfreq_gdbt_model.py index ad0641136f2..bf7f31a7050 100644 --- a/qlib/contrib/model/highfreq_gdbt_model.py +++ b/qlib/contrib/model/highfreq_gdbt_model.py @@ -124,16 +124,21 @@ def fit( if evals_result is None: evals_result = dict() dtrain, dvalid = self._prepare_data(dataset) - early_stopping_callback = lgb.early_stopping(early_stopping_rounds) - verbose_eval_callback = lgb.log_evaluation(period=verbose_eval) - evals_result_callback = lgb.record_evaluation(evals_result) + + # Build callbacks list + callbacks = [] + if early_stopping_rounds is not None: + callbacks.append(lgb.early_stopping(early_stopping_rounds)) + callbacks.append(lgb.log_evaluation(period=verbose_eval)) + callbacks.append(lgb.record_evaluation(evals_result)) + self.model = lgb.train( self.params, dtrain, num_boost_round=num_boost_round, valid_sets=[dtrain, dvalid], valid_names=["train", "valid"], - callbacks=[early_stopping_callback, verbose_eval_callback, evals_result_callback], + callbacks=callbacks, ) evals_result["train"] = list(evals_result["train"].values())[0] evals_result["valid"] = list(evals_result["valid"].values())[0] diff --git a/qlib/contrib/rolling/ddgda.py b/qlib/contrib/rolling/ddgda.py index 0fe01d04550..98600296269 100644 --- a/qlib/contrib/rolling/ddgda.py +++ b/qlib/contrib/rolling/ddgda.py @@ -241,7 +241,9 @@ def _dump_meta_ipt(self): sim_task = replace_task_handler_with_cache(sim_task, self.working_dir) if self.sim_task_model == "gbdt": - sim_task["model"].setdefault("kwargs", {}).update({"early_stopping_rounds": None, "num_boost_round": 150}) + sim_task["model"].setdefault("kwargs", {}).update({"num_boost_round": 150}) + # Don't set early_stopping_rounds to disable it (LightGBM 4.0+ doesn't accept None) + sim_task["model"]["kwargs"].pop("early_stopping_rounds", None) exp_name_sim = f"data_sim_s{self.step}"