LightGBMのearly_stoppingの仕様が変わったので、使用法を調べてみた

# LightGBMとearly_stopping
LightGBMは2022年現在、回帰問題において最も広く用いられている学習器の一つであり、**機械学習を学ぶ上で避けては通れない手法**と言えます。

LightGBMの一機能である**early_stopping**は学習を効率化できる（詳細は[後述](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#early_stoppingとは)）人気機能ですが、この度**使用方法に大きな変更があった**ようなので、**内容を記事にまとめたい**と思います

## 変更の概要
early_stoppingを使用するためには、元来は学習実行メソッド（[`train()`](https://lightgbm.readthedocs.io/en/latest/pythonapi/lightgbm.train.html)または[`fit()`](https://lightgbm.readthedocs.io/en/latest/pythonapi/lightgbm.LGBMRegressor.html#lightgbm.LGBMRegressor.fit)）に`early_stopping_rounds`引数を指定していましたが、2021年の年末（[こちらのissues](https://github.com/microsoft/LightGBM/pull/4908)）よりこの指定法は非推奨となり、**コールバック関数[`lightgbm.early_stopping()`](https://lightgbm.readthedocs.io/en/latest/pythonapi/lightgbm.early_stopping.html#lightgbm.early_stopping)で指定**する方式へと変更になりました。

新たな方式である**コールバック関数によるearly_stoppingの指定法**に関して、現状では情報が少なく

**「どう使えばいいのか分からん！」**

と思われている方も多いかと思うので、使用法を調べた結果を本記事にまとめます。

### TL;DR

2024/1現在、実装のお手軽さとXGBoostとの共通化の観点から、[本記事でおまけのように紹介している「**第三の指定方法**」](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#参考-第3のearly_stopping指定方法)が**個人的にはベストな実装方法**だと考えております。

## 旧指定方法の現状
2022/5現在、`early_stopping_rounds`引数を使用すると、以下のような`UserWarning`が発生します。

※なお、PythonでLightGBMを使用するためには「**Training API**」([train()](https://lightgbm.readthedocs.io/en/latest/pythonapi/lightgbm.train.html)メソッド)と「**Scikit-Learn API**」([fit()](https://lightgbm.readthedocs.io/en/latest/pythonapi/lightgbm.LGBMRegressor.html#lightgbm.LGBMRegressor.fit)メソッド)の**2種類の実装方法**が存在しますが、本記事では両者について検証していきます（[両APIの概要については後述](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#参考-training-apiとscikit-learn-apiの差)）

### Training APIの場合

```
lightgbm/engine.py:181: UserWarning: 'early_stopping_rounds' argument is deprecated
 and will be removed in a future release of LightGBM.
 Pass 'early_stopping()' callback via 'callbacks' argument instead.
  _log_warning("'early_stopping_rounds' argument is deprecated
 and will be removed in a future release of LightGBM. "
```

### Scikit-Learn APIの場合

```
lightgbm/sklearn.py:726: UserWarning: 'early_stopping_rounds' argument is deprecated
 and will be removed in a future release of LightGBM.
 Pass 'early_stopping()' callback via 'callbacks' argument instead.
  _log_warning("'early_stopping_rounds' argument is deprecated
 and will be removed in a future release of LightGBM. "
```

メッセージを見ると分かる通り、**`early_stopping_rounds`引数はそのうち使えなくなる**可能性が高い([最新版のマスターブランチ](https://github.com/microsoft/LightGBM/blob/master/python-package/lightgbm/engine.py)では既に削除済)ため、今後はコールバック関数を用いた新たな指定方法を用いる必要がありそうです。


## early_stoppingとは？
LightGBMやXGBoostは、学習に「**勾配ブースティング決定木**」と呼ばれる手法が用いられています。詳しくは以下の記事が分かりやすいですが、

https://qiita.com/kuroitu/items/57425380546f7b9ed91c

学習手順を超シンプルにまとめると
「**前回の学習結果に基づき、誤判定を減らすよう再学習を繰り返す**」
という処理が行われています。

ここで重要となるのが、「**学習（イテレーション）を何回繰り返すか**」です。

early_stoppingを使用しない場合、Training APIでは`num_boost_round`引数、Scikit-Learn APIでは`n_estimators`引数で指定した**固定回数だけ学習が繰り返されます**。
最適な学習回数はデータセットによって変わるため、多くのデータセットで性能を発揮するためには指定する学習回数（イテレーション数）を増やす必要がありますが、学習回数を増やすと所要時間が増えてしまうため、**性能と所要時間のトレードオフ**が生じてしまいます。

また学習回数を増やしすぎると学習データにモデルが過剰に適合する「過学習」が生じやすくなるため、**過学習と未学習のトレードオフ**も発生します。

これらの**トレードオフを最適化**するために用いられるのが[**early_stopping**](https://lightgbm.readthedocs.io/en/latest/Python-Intro.html#early-stopping)で、

「**評価用データで一定回数連続でスコアが改善しなかったら、指定した学習回数に達しなくとも学習を打ち切る**」

というフローが加わります（この「一定回数」を`early_stopping_rounds`引数で、「評価用データ」を`valid_sets`または`eval_set`引数で指定します）
これによりデータセットが変わっても「性能がサチる学習回数 ≒ これ以上続けても性能向上が見込めない学習回数」で動的に学習を打ち切る事ができ、効率的な学習を実現できます。

参考までにearly_stopping使用時の学習フローをまとめると、以下のようになります。

**・early_stopping使用時のLightGBM学習フロー**
<img width=540 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/e474294f-d0ef-3c77-4ecd-38059979e893.png">
なお、early_stopping使用時は通例、`num_boost_round`引数（Scikit-Learn APIでは`n_estimators`引数）に大きな値（例: 10000）を指定し、常にearly_stoppingで学習が終了するようにフローが組まれます。

また、`early_stopping_rounds`の数ですが、一般的には（筆者の経験とGoogle検索した結果）10〜100前後が指定されることが多いようです。本記事では例として10を使用することとします。

# early_stoppingの実装方法
旧指定方法（`early_stopping_rounds`引数）、現在の推奨方法（コールバック関数`early_stopping()`）それぞれについて、実装方法を解説します。

## 旧指定方法 ('early_stopping_rounds'引数)
非推奨となった`early_stopping_rounds`引数での実装方法を、Training API、Scikit-Learn APIそれぞれについて示します

### Training APIの場合
学習実行メソッド`train()`の`early_stopping_rounds`引数に学習を打ち切るイテレーション数（前記「一定回数」に相当）を指定することで、early_stoppingを実現できます。

```python:TrainingAPI+旧指定方法(early_stopping_rounds)での実装法
import pandas as pd
import numpy as np
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
import lightgbm as lgb
# データセット読込(カリフォルニア住宅価格)
TARGET_VARIABLE = 'price'  # 目的変数名
USE_EXPLANATORY = ['MedInc', 'AveOccup', 'Latitude', 'HouseAge']  # 説明変数名
california_housing = pd.DataFrame(np.column_stack((fetch_california_housing().data, fetch_california_housing().target)),
        columns = np.append(fetch_california_housing().feature_names, TARGET_VARIABLE))
california_housing = california_housing.sample(n=1000, random_state=42)  # データ数多いので1000にサンプリング
y = california_housing[TARGET_VARIABLE].values  # 目的変数のnumpy配列
X = california_housing[USE_EXPLANATORY].values  # 説明変数のnumpy配列
# テストデータと学習データ分割
X_train_raw, X_test, y_train_raw, y_test = train_test_split(X, y, test_size=0.25, random_state=42)
# early_stopping用の評価データをさらに分割
X_train, X_valid, y_train, y_valid = train_test_split(X_train_raw, y_train_raw, test_size=0.25, random_state=42)

###### ここからがLightGBMの実装 ######
# データをDatasetクラスに格納
dtrain = lgb.Dataset(X_train, label=y_train)  # 学習用
dvalid = lgb.Dataset(X_valid, label=y_valid)  # early_stopping用
# 使用するパラメータ
params = {'objective': 'regression',  # 最小化させるべき損失関数(2クラス分類の場合'binary'、多クラス分類の場合'multiclass')
         'metric': 'rmse',  # 学習時に使用する評価指標(2クラス分類の場合'binary_logloss'等、多クラス分類の場合'multi_logloss'等)
         'random_state': 42,  # 乱数シード
         'boosting_type': 'gbdt',  # boosting_type
         'verbose': -1  # これを指定しないと`No further splits with positive gain, best gain: -inf`というWarningが表示される
         }
verbose_eval = 0  # この数字を1にすると学習時のスコア推移がコマンドライン表示される
# early_stoppingを指定してLightGBM学習
gbm = lgb.train(params, dtrain,
                valid_sets=[dvalid],  # early_stoppingの評価用データ
                num_boost_round=10000,  # 最大学習サイクル数。early_stopping使用時は大きな値を入力
                early_stopping_rounds=10,
                verbose_eval=verbose_eval
                )

# スコア(RMSE)算出
y_pred = gbm.predict(X_test)
score = mean_squared_error(y_true=y_test, y_pred=y_pred, squared=False)
print(f'RMSE={score}')
```

```:実行結果
lightgbm/engine.py:181: UserWarning: 'early_stopping_rounds' argument is deprecated
 and will be removed in a future release of LightGBM.
 Pass 'early_stopping()' callback via 'callbacks' argument instead.
  _log_warning("'early_stopping_rounds' argument is deprecated
 and will be removed in a future release of LightGBM. "
:
(中略)
:
RMSE=0.722485359165361
```

UserWarningは出ていますが、処理自体は正常に実行できていることが分かります。
（ただし、[前述のように](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#旧指定方法の現状)将来的に動作しなくなる恐れあり）

### Scikit-Learn APIの場合
学習実行メソッド`fit()`の`early_stopping_rounds`引数に学習を打ち切るイテレーション数を指定することで、early_stoppingを実現できます。

```python:Scikit-LearnAPI+旧指定方法(early_stopping_rounds)での実装法
:
データ読込、データ分割処理は省略（上記コードと同じ）
:
###### ここからがLightGBMの実装 ######
# 使用するパラメータ
params = {'objective': 'regression',  # 最小化させるべき損失関数(2クラス分類の場合'binary'、多クラス分類の場合'multiclass')
         'random_state': 42,  # 乱数シード
         'boosting_type': 'gbdt',  # boosting_type
         'n_estimators': 10000  # 最大学習サイクル数。early_stopping使用時は大きな値を入力
         }
verbose_eval = 0  # この数字を1にすると学習時のスコア推移がコマンドライン表示される
# early_stoppingを指定してLightGBM学習
lgbr = lgb.LGBMRegressor(**params)
lgbr.fit(X_train, y_train, 
         eval_metric='rmse',  # early_stoppingの評価指標(学習用の'metric'パラメータにも同じ指標が自動入力される)
         eval_set=[(X_valid, y_valid)],
         early_stopping_rounds=10,
         verbose=verbose_eval
         )

# スコア(RMSE)算出
y_pred = lgbr.predict(X_test)
score = mean_squared_error(y_true=y_test, y_pred=y_pred, squared=False)
print(f'RMSE={score}')
```

```:実行結果
lightgbm/sklearn.py:726: UserWarning: 'early_stopping_rounds' argument is deprecated
 and will be removed in a future release of LightGBM.
 Pass 'early_stopping()' callback via 'callbacks' argument instead.
  _log_warning("'early_stopping_rounds' argument is deprecated
 and will be removed in a future release of LightGBM. "
:
(中略)
:
RMSE=0.722485359165361
```
UserWarningは出ていますが、処理自体は正常に実行できていることが分かります。
（ただし、[前述のように](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#旧指定方法の現状)将来的に動作しなくなる恐れあり）


## 現在の推奨方法 (コールバック関数'early_stopping()')
**2022/5現在の推奨手法**であるコールバック関数`lightgbm.early_stopping()`での実装方法を、Training API、Scikit-Learn APIそれぞれについて示します

### Training APIの場合
学習実行メソッド`train()`の`early_stopping_rounds`引数の代わりに、`callbacks`引数にコールバック関数`lightgbm.early_stopping()`をリスト指定することで、early_stoppingを実現できます。

```python:TrainingAPI+新指定方法(コールバック関数)での実装法
:
データ読込、データ分割処理は省略（上記コードと同じ）
:
###### ここからがLightGBMの実装 ######
# データをDatasetクラスに格納
dtrain = lgb.Dataset(X_train, label=y_train)  # 学習用
dvalid = lgb.Dataset(X_valid, label=y_valid)  # early_stopping用
# 使用するパラメータ
params = {'objective': 'regression',  # 最小化させるべき損失関数(2クラス分類の場合'binary'、多クラス分類の場合'multiclass')
         'metric': 'rmse',  # 学習時に使用する評価指標(2クラス分類の場合'binary_logloss'等、多クラス分類の場合'multi_logloss'等)
         'random_state': 42,  # 乱数シード
         'boosting_type': 'gbdt',  # boosting_type
         'verbose': -1  # これを指定しないと`No further splits with positive gain, best gain: -inf`というWarningが表示される
         }
verbose_eval = 0  # この数字を1にすると学習時のスコア推移がコマンドライン表示される
# early_stoppingを指定してLightGBM学習
gbm = lgb.train(params, dtrain,
                valid_sets=[dvalid],  # early_stoppingの評価用データ
                num_boost_round=10000,  # 最大学習サイクル数。early_stopping使用時は大きな値を入力
                callbacks=[lgb.early_stopping(stopping_rounds=10, 
                                verbose=True), # early_stopping用コールバック関数
                           lgb.log_evaluation(verbose_eval)] # コマンドライン出力用コールバック関数
                )

# スコア(RMSE)算出
y_pred = gbm.predict(X_test)
score = mean_squared_error(y_true=y_test, y_pred=y_pred, squared=False)
print(f'RMSE={score}')
```

```:実行結果
Training until validation scores don't improve for 10 rounds
Early stopping, best iteration is:
[43]	valid_0's rmse: 0.588869	valid_0's l2: 0.346766
RMSE=0.722485359165361
```
Warningが出ておらず、かつ[`early_stopping_round`引数での指定時](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#training-apiの場合-1)と同スコア＝同じ処理を実現できていることが分かります。

なお、コールバック関数`lightgbm.early_stopping()`の引数`stopping_rounds`には旧手法の`early_stopping_rounds`に相当する数を入力します。
また`verbose`引数をTrueに指定することで、何回目の学習でスコアが最大となったか（early_stoppingで学習が終了したイテレーション - stopping_rounds）を表示することができます。
上例では43回目の学習でスコアが最大となり、そこから10回連続でスコアが改善せずに53回目で処理が打ち切られたことが分かります。

コールバック関数`log_evaluation()`はコマンドライン出力の詳細さを指定するためのメソッドで、上例のように`callbacks`引数のリストに追加すると結果の詳細確認ができて便利ですが、追加しなくともearly_stoppingの動作自体は正常に実行できます。

### Scikit-Learn APIの場合
学習実行メソッド`fit()`の`early_stopping_rounds`引数の代わりに、`callbacks`引数にコールバック関数`lightgbm.early_stopping()`をリスト指定することで、early_stoppingを実現できます。

```python:Scikit-LearnAPI+新指定方法(コールバック関数)での実装法
:
データ読込、データ分割処理は省略（上記コードと同じ）
:
###### ここからがLightGBMの実装 ######
# 使用するパラメータ
params = {'objective': 'regression',  # 最小化させるべき損失関数(2クラス分類の場合'binary'、多クラス分類の場合'multiclass')
         'random_state': 42,  # 乱数シード
         'boosting_type': 'gbdt',  # boosting_type
         'n_estimators': 10000  # 最大学習サイクル数。early_stopping使用時は大きな値を入力
         }
verbose_eval = 0  # この数字を1にすると学習時のスコア推移がコマンドライン表示される
# early_stoppingを指定してLightGBM学習
lgbr = lgb.LGBMRegressor(**params)
lgbr.fit(X_train, y_train, 
         eval_metric='rmse',  # early_stoppingの評価指標(学習用の'metric'パラメータにも同じ指標が自動入力される)
         eval_set=[(X_valid, y_valid)],
         callbacks=[lgb.early_stopping(stopping_rounds=10, 
                        verbose=True), # early_stopping用コールバック関数
                    lgb.log_evaluation(verbose_eval)] # コマンドライン出力用コールバック関数
         )

# スコア(RMSE)算出
y_pred = lgbr.predict(X_test)
score = mean_squared_error(y_true=y_test, y_pred=y_pred, squared=False)
print(f'RMSE={score}')
```

```:実行結果
Training until validation scores don't improve for 10 rounds
Early stopping, best iteration is:
[43]	valid_0's rmse: 0.588869	valid_0's l2: 0.346766
RMSE=0.722485359165361
```
Warningが出ておらず、かつ[`early_stopping_round`引数での指定時](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#scikit-learn-apiの場合)と同スコア＝同じ処理を実現できていることが分かります。

コールバック関数や各引数の意味は[Training APIの場合](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#training-apiの場合-2)と同様です


# クロスバリデーション実行時の挙動
**クロスバリデーション**でモデル評価を行う際は、early_stoppingの実装に更なる注意が必要となります。

以下のように様々な実装方法がありますが、計算結果が異なるパターンがあったり、そのままではうまく動作しないパターン（×部分が相当）があったりするので、適切な実装法を選択する必要があります。

||旧指定方法<br>('early_stopping_rounds'引数)|現在の推奨方法<br>(コールバック関数'early_stopping()')|
|---|---|---|
|**Training API**<br>(スクラッチ実装)|[○](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#training-api-スクラッチ実装の場合)|[○](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#training-api-スクラッチ実装の場合-1)|
|**Training API**<br>(`lightgbm.cv`メソッド)|[○ (他方式とは別計算法)](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#training-api-lightgbmcvメソッドの場合)|**[○ (他方式とは別計算法)](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#training-api-lightgbmcvメソッドの場合-1)**|
|**Scikit-Learn API**<br>(スクラッチ実装)|[○](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#scikit-learn-api-スクラッチ実装の場合)|**[○](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#scikit-learn-api-スクラッチ実装の場合-1)**|
|**Scikit-Learn API**<br>(`cross_val_score`メソッド)|[× (自作メソッドで対応)](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#scikit-learn-api-cross_val_scoreメソッドの場合)|**[× (自作メソッドで対応)](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#scikit-learn-api-cross_val_scoreメソッドの場合-1)**|

たくさん種類があって

**「どれ使えばええねん！」**

と思われた方も多いかと思いますが、
スクラッチだと実装の手間が増える＆並列処理が効かず速度面でも不利なので、

**基本 → [lightgbm.cv()](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#training-api-lightgbmcvメソッドの場合-1)メソッド**
**どうしてもScikit-LearnのAPIを使用したい場合 → [cross_val_scoreメソッド改](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#scikit-learn-api-cross_val_scoreメソッドの場合-1)**

の方法が良いかと思います（表中の太字部分に相当）
以下詳説します

なお、[クロスバリデーションなしの場合](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#early_stoppingの実装方法)ではearly_stopping用の評価データをテストデータとは別に準備（LightGBMの亜種である[sklearn.ensemble.HistGradientBoostingRegressor](https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.HistGradientBoostingRegressor.html)の`validation_fraction`のデータ分割仕様に準拠）していましたが、本項ではクロスバリデーションのテストデータをearly_stopping用の評価データにも使用（[lightgbm.cv](https://lightgbm.readthedocs.io/en/latest/pythonapi/lightgbm.cv.html)のデータ分割仕様に準拠）していることにご注意ください。

<img width=800 src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/56557682-4472-debf-e2f7-441869c7a185.png>

　気付かれた方もいるかと思いますが、クロスバリデーションありの場合ではearly_stopping評価用データとスコア評価用のデータが同一となっています。これでパラメータチューニング等を行うとearly_stopping用のデータがリークした状態となっており、「弱い過学習」とも言えるような状態になってしまいます。本記事では公式のlightgbm.cv()メソッドの仕様に準拠してこの分割方法を採用しています（定性的ですが、私の経験上もこの分割法で過学習により未知データに対する性能が極端に低下する現象は観測されませんでした）。
　リークを気にされる方は、クロスバリデーションのスコア評価用データとは別にearly_stopping用のデータを確保してください。
（[後述の自作ライブラリによる対処]()も参照ください）

## 旧指定方法 ('early_stopping_rounds'引数)

### Training API (スクラッチ実装)の場合
[Scikit-Learnのデータ分割用クラス](https://qiita.com/c60evaporator/items/ca7eb70e1508d2ba5359#24-クロスバリデーション)を使用して`split()`メソッドでデータを分割し、for文でFoldごとにTraining APIでスコアを求めるスクラッチな手法です。

```python:TrainingAPI+旧指定方法(early_stopping_rounds)+スクラッチ実装のクロスバリデーション
import pandas as pd
import numpy as np
from sklearn.datasets import fetch_california_housing
from sklearn.metrics import mean_squared_error
import lightgbm as lgb
from sklearn.model_selection import KFold
# データセット読込(カリフォルニア住宅価格)
TARGET_VARIABLE = 'price'  # 目的変数名
USE_EXPLANATORY = ['MedInc', 'AveOccup', 'Latitude', 'HouseAge']  # 説明変数名
california_housing = pd.DataFrame(np.column_stack((fetch_california_housing().data, fetch_california_housing().target)),
    columns = np.append(fetch_california_housing().feature_names, TARGET_VARIABLE))
california_housing = california_housing.sample(n=1000, random_state=42)  # データ数多いので1000にサンプリング
y = california_housing[TARGET_VARIABLE].values  # 目的変数のnumpy配列
X = california_housing[USE_EXPLANATORY].values  # 説明変数のnumpy配列

# クロスバリデーション用のScikit-Learnクラス（5分割KFold）
cv = KFold(n_splits=5, shuffle=True, random_state=42)

# クロスバリデーションのデータ分割
scores=[]
for i, (train, test) in enumerate(cv.split(X, y)):
    ###### ここからがLightGBMの実装 ######
    # データをDatasetクラスに格納
    dtrain = lgb.Dataset(X[train], label=y[train])  # 学習データ
    dvalid = lgb.Dataset(X[test], label=y[test])  # early_stopping用(テストデータを使用)
    # 使用するパラメータ
    params = {'objective': 'regression',  # 最小化させるべき損失関数(2クラス分類の場合'binary'、多クラス分類の場合'multiclass')
             'metric': 'rmse',  # 学習時に使用する評価指標(2クラス分類の場合'binary_logloss'等、多クラス分類の場合'multi_logloss'等)
             'random_state': 42,  # 乱数シード
             'boosting_type': 'gbdt',  # boosting_type
             'verbose': -1  # これを指定しないと`No further splits with positive gain, best gain: -inf`というWarningが表示される
             }
    verbose_eval = 0  # この数字を1にすると学習時のスコア推移がコマンドライン表示される
    # early_stoppingを指定してLightGBM学習
    gbm = lgb.train(params, dtrain,
                    valid_sets=[dvalid],  # early_stoppingの評価用データ
                    num_boost_round=10000,  # 最大学習サイクル数。early_stopping使用時は大きな値を入力
                    early_stopping_rounds=10,
                    verbose_eval=verbose_eval
                    )

    # スコア(RMSE)算出
    y_pred = gbm.predict(X[test])
    score = mean_squared_error(y_true=y[test], y_pred=y_pred, squared=False)
    scores.append(score)
print(f'RMSE={scores} \nRMSE mean={np.mean(scores)}')
```

```:実行結果
lightgbm/engine.py:181: UserWarning: 'early_stopping_rounds' argument is deprecated
 and will be removed in a future release of LightGBM.
 Pass 'early_stopping()' callback via 'callbacks' argument instead.
  _log_warning("'early_stopping_rounds' argument is deprecated
 and will be removed in a future release of LightGBM. "
:
(中略)
:
RMSE=[0.738983559441704, 0.6150024788535946, 0.6803952077929987, 0.6445438943448907, 0.6913201783978795] 
RMSE mean=0.6740490637662134
```

UserWarningは出ていますが、処理自体は正常に実行できていることが分かります（データ分割後の処理は[クロスバリデーションなしのTraining APIの場合](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#training-apiの場合-1)と同じなので当然といえば当然ですが）

なお注意点として、**コールバック関数は`train()`メソッドを1回実行する度に初期化する必要がある**ことです（Scikit-Learn APIの`fit()`メソッド等も同様です）
for文の外側でコールバック関数を定義しないようにご注意ください（初期化されないまま複数の学習が走って正常に処理されなくなる）

### Training API (`lightgbm.cv()`メソッド)の場合
LightGBMには、クロスバリデーションの全Fold一括でスコアを計算する`lightgbm.cv()`というメソッドが用意されています。

`lightgbm.cv()`メソッドの実装は、以下のように通常の学習に用いられる`train()`メソッドと近いAPIで実現可能であり、[スクラッチの場合](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#training-api-スクラッチ実装の場合)よりも短いコードで実装できます。
（`valid_sets`引数の指定が不要となることにご注意ください。代わりにクロスバリデーションのテストデータがearly_stoppingの評価用データに使用されます）

```python:TrainingAPI+旧指定方法(early_stopping_rounds)+lightgbm.cv()メソッドのクロスバリデーション
:
データ読込処理は省略（上記コードと同じ）
:
# クロスバリデーション用のScikit-Learnクラス（5分割KFold）
cv = KFold(n_splits=5, shuffle=True, random_state=42)

###### ここからがLightGBMの実装 ######
# データをDatasetクラスに格納
dcv = lgb.Dataset(X, label=y)  # クロスバリデーション用
# 使用するパラメータ
params = {'objective': 'regression',  # 最小化させるべき損失関数(2クラス分類の場合'binary'、多クラス分類の場合'multiclass')
         'metric': 'rmse',  # 学習時に使用する評価指標(2クラス分類の場合'binary_logloss'等、多クラス分類の場合'multi_logloss'等)
         'random_state': 42,  # 乱数シード
         'boosting_type': 'gbdt',  # boosting_type
         'verbose': -1  # これを指定しないと`No further splits with positive gain, best gain: -inf`というWarningが表示される
         }
verbose_eval = 0  # この数字を1にすると学習時のスコア推移がコマンドライン表示される
# early_stoppingを指定してLightGBMをクロスバリデーション
cv_result = lgb.cv(params, dcv,
                num_boost_round=10000,  # 最大学習サイクル数。early_stopping使用時は大きな値を入力
                early_stopping_rounds=10,
                verbose_eval=verbose_eval,
                folds=cv
                )
print(cv_result)
print(f'RMSE mean={cv_result["rmse-mean"][-1]}')
```

以下の記事で詳細に解説されていますが、

https://blog.amedama.jp/entry/lightgbm-cv-implementation

[前述のスクラッチ実装](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#training-api-スクラッチ実装の場合)や[後述のScikit-Learn cross_validate()メソッド](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#scikit-learn-api-cross_val_scoreメソッドの場合)では
「各Foldごとに`early_stopping`を適用して学習→スコア平均を算出」
という手順で処理が進むのに対し、

`lightgbm.cv()`メソッドでは
「全Foldのスコア平均をイテレーション毎に算出→スコア平均に`early_stopping`を適用」
という手順で処理が進むため、**他方式とは異なる最終スコア**が計算されます

他方式と比べたメリットデメリットは上記参考リンクに記載されていますが、一長一短といった感じのようです。

```:実行結果
lightgbm/engine.py:577: UserWarning: 'early_stopping_rounds' argument is deprecated
 and will be removed in a future release of LightGBM.
 Pass 'early_stopping()' callback via 'callbacks' argument instead.
  _log_warning("'early_stopping_rounds' argument is deprecated
 and will be removed in a future release of LightGBM. "
:
(中略)
:
{'rmse-mean': [1.092268418453115, 1.0310450173717558, 0.9795686341063112, 
0.9357050975378067, 0.898026355776955, 0.8653287143548599, 0.8361953918654839, 
0.8118105375696019, 0.7913531404345221, 0.7740533884697003, 0.7581006340525287, 
0.7457239616368891, 0.734370054531101, 0.7257891055825861, 0.7187968572833688, 
0.711875704872069, 0.7061541973418042, 0.7016815498175433, 0.6974989018815776, 
0.6953583144477753, 0.6934006796597723, 0.6913382320507665, 0.6891147218487653, 
0.6873271643189067, 0.6859662264261306, 0.6848257970905169, 0.6834265660057884, 
0.6821396043772722, 0.6813493570473369, 0.6802932554692094, 0.6794027048145301, 
0.6784863031615622, 0.6777672012337602, 0.6775112510272382, 0.6767615074384222],
'rmse-stdv': [0.047163893109934095, 0.04330692544819591, 0.0412303842717346, 
0.03918788111501026, 0.037610483543590825, 0.03572218937059543, 0.03549682540172055, 
0.03524086095052791, 0.03624560680763654, 0.037586214743957506, 0.03928402431293732, 
0.0378483690801823, 0.03818586654066806, 0.03869355751568724, 0.03910746326923766, 
0.03936895674923746, 0.03947841587505398, 0.039117262719758225, 0.03959543152820096, 
0.03881841849115544, 0.03881540132987051, 0.03799963684609526, 0.0369718932140366, 
0.03680779639424503, 0.03670904081679501, 0.03661096377218453, 0.03642053463285243, 
0.03642331278429394, 0.036879608212898304, 0.03692147800332548, 0.03672366274081491, 
0.03609096933230052, 0.03617965035489701, 0.0362180844993717, 0.03719491159161849]}
RMSE mean=0.6767615074384222
```

出力結果の'rmse-mean'は、イテレーション毎のスコア平均を表しています。
上例では'rmse-mean'の要素数が35個であることから、35番目のイテレーションでスコア平均が最良となり、そこから10イテレーション連続でスコア平均が改善しなかったため、45回目のイテレーションでearly_stoppingにより学習が打ち切られたことが分かります。


### Scikit-Learn API (スクラッチ実装)の場合
[Scikit-Learnのデータ分割用クラス](https://qiita.com/Hatomugi/items/620c1bc757266b00e87f)を使用して`split()`メソッドでデータを分割し、FoldごとにScikit-Learn APIでスコアを求めるスクラッチな手法です。

```python:Scikit-LearnAPI+旧指定方法(early_stopping_rounds)+スクラッチ実装のクロスバリデーション
:
データ読込処理は省略（上記コードと同じ）
:
# クロスバリデーション用のScikit-Learnクラス（5分割KFold）
cv = KFold(n_splits=5, shuffle=True, random_state=42)

# クロスバリデーションのデータ分割
scores=[]
for i, (train, test) in enumerate(cv.split(X, y)):
    ###### ここからがLightGBMの実装 ######
    # 使用するパラメータ
    params = {'objective': 'regression',  # 最小化させるべき損失関数(2クラス分類の場合'binary'、多クラス分類の場合'multiclass')
            'random_state': 42,  # 乱数シード
            'boosting_type': 'gbdt',  # boosting_type
            'n_estimators': 10000  # 最大学習サイクル数。early_stopping使用時は大きな値を入力
            }
    verbose_eval = 0  # この数字を1にすると学習時のスコア推移がコマンドライン表示される
    # early_stoppingを指定してLightGBM学習
    lgbr = lgb.LGBMRegressor(**params)
    lgbr.fit(X[train], y[train], 
            eval_metric='rmse',  # early_stoppingの評価指標(学習用の'metric'パラメータにも同じ指標が自動入力される)
            eval_set=[(X[test], y[test])],
            early_stopping_rounds=10,
            verbose=verbose_eval
            )

    # スコア(RMSE)算出
    y_pred = lgbr.predict(X[test])

    score = mean_squared_error(y_true=y[test], y_pred=y_pred, squared=False)
    scores.append(score)
print(f'RMSE={scores} \nRMSE mean={np.mean(scores)}')
```

```:実行結果
lightgbm/engine.py:181: UserWarning: 'early_stopping_rounds' argument is deprecated
 and will be removed in a future release of LightGBM.
 Pass 'early_stopping()' callback via 'callbacks' argument instead.
  _log_warning("'early_stopping_rounds' argument is deprecated
 and will be removed in a future release of LightGBM. "
:
(中略)
:
RMSE=[0.738983559441704, 0.6150024788535946, 0.6803952077929987, 0.6445438943448907, 0.6913201783978795] 
RMSE mean=0.6740490637662134
```
[Training APIのスクラッチ実装](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#training-api-スクラッチ実装の場合)と同スコアになっており、同様の処理が実現できていることが分かります

### Scikit-Learn API (`cross_val_score`メソッド)の場合
Scikit-Learnには`cross_val_score()`という、クロスバリデーションでスコアを計算するためのメソッドが存在し、これを利用することでクロスバリデーションを簡単に実装することができます。

しかしこの`cross_val_score()`、クロスバリデーション分割後のデータをearly_stoppingの評価用データである`eval_set`に渡せないため、[lightgbm.cv()](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#training-api-lightgbmcvメソッドの場合)メソッドのときのように**テストデータからearly_stoppingを判定することができません**。

また[前述のように](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#training-api-%E3%82%B9%E3%82%AF%E3%83%A9%E3%83%83%E3%83%81%E5%AE%9F%E8%A3%85%E3%81%AE%E5%A0%B4%E5%90%88)コールバック関数は学習を1回実行するごとに初期化する必要がありますが、`cross_val_score()`メソッド内ではこの初期化が行われていないため、初期化されないまま複数の学習が走って正常に処理されなくなります。

そこでクロスバリデーション分割後の**テストデータを**`eval_set`**に渡せるようにしたメソッド**`cross_val_score_eval_set()`を**新たに自作**しました。
内部的には現在の推奨方法（コールバック関数）でearly_stoppingを実現しているため、詳細は後述します。
以下リンクを参照ください

https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#scikit-learn-api-cross_val_scoreメソッドの場合-1

## 現在の推奨方法 (コールバック関数'early_stopping()')

### Training API (スクラッチ実装)の場合
[クロスバリデーションなしの場合](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#training-apiの場合-2)と同様、学習実行メソッド`train()`の`early_stopping_rounds`引数の代わりに、`callbacks`引数にコールバック関数`lightgbm.early_stopping()`をリスト指定することで、early_stoppingを実現できます。

```python:TrainingAPI+新指定方法(コールバック関数)+スクラッチ実装のクロスバリデーション
:
データ読込処理は省略（上記コードと同じ）
:
# クロスバリデーション用のScikit-Learnクラス（5分割KFold）
cv = KFold(n_splits=5, shuffle=True, random_state=42)

# クロスバリデーションのデータ分割
scores=[]
for i, (train, test) in enumerate(cv.split(X, y)):
    ###### ここからがLightGBMの実装 ######
    # データをDatasetクラスに格納
    dtrain = lgb.Dataset(X[train], label=y[train])  # 学習データ
    dvalid = lgb.Dataset(X[test], label=y[test])  # early_stopping用(テストデータを使用)
    # 使用するパラメータ
    params = {'objective': 'regression',  # 最小化させるべき損失関数(2クラス分類の場合'binary'、多クラス分類の場合'multiclass')
             'metric': 'rmse',  # 学習時に使用する評価指標(2クラス分類の場合'binary_logloss'等、多クラス分類の場合'multi_logloss'等)
             'random_state': 42,  # 乱数シード
             'boosting_type': 'gbdt',  # boosting_type
             'verbose': -1  # これを指定しないと`No further splits with positive gain, best gain: -inf`というWarningが表示される
             }
    verbose_eval = 0  # この数字を1にすると学習時のスコア推移がコマンドライン表示される
    # early_stoppingを指定してLightGBM学習
    gbm = lgb.train(params, dtrain,
                    valid_sets=[dvalid],  # early_stoppingの評価用データ
                    num_boost_round=10000,  # 最大学習サイクル数。early_stopping使用時は大きな値を入力
                    callbacks=[lgb.early_stopping(stopping_rounds=10, 
                                verbose=True), # early_stopping用コールバック関数
                           lgb.log_evaluation(verbose_eval)] # コマンドライン出力用コールバック関数
                    )

    # スコア(RMSE)算出
    y_pred = gbm.predict(X[test])
    score = mean_squared_error(y_true=y[test], y_pred=y_pred, squared=False)
    scores.append(score)
print(f'RMSE={scores} \nRMSE mean={np.mean(scores)}')
```

```:実行結果
Training until validation scores don't improve for 10 rounds
Early stopping, best iteration is:
[30]	valid_0's rmse: 0.738984
Training until validation scores don't improve for 10 rounds
Early stopping, best iteration is:
[37]	valid_0's rmse: 0.615002
Training until validation scores don't improve for 10 rounds
Early stopping, best iteration is:
[43]	valid_0's rmse: 0.680395
Training until validation scores don't improve for 10 rounds
Early stopping, best iteration is:
[36]	valid_0's rmse: 0.644544
Training until validation scores don't improve for 10 rounds
Early stopping, best iteration is:
[36]	valid_0's rmse: 0.69132
RMSE=[0.738983559441704, 0.6150024788535946, 0.6803952077929987, 0.6445438943448907, 0.6913201783978795] 
RMSE mean=0.6740490637662134
```

UserWarningが消えており、かつ[旧実装方法の時](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#training-api-スクラッチ実装の場合)と同スコアとなっており、同様の処理が実現できていることが分かります


### Training API (`lightgbm.cv()`メソッド)の場合
上例と同様、クロスバリデーション実行メソッド`cv()`の`early_stopping_rounds`引数の代わりに、`callbacks`引数にコールバック関数`lightgbm.early_stopping()`をリスト指定することで、early_stoppingを実現できます。

```python:TrainingAPI+新指定方法(コールバック関数)+lightgbm.cv()メソッドのクロスバリデーション
:
データ読込処理は省略（上記コードと同じ）
:
# クロスバリデーション用のScikit-Learnクラス（5分割KFold）
cv = KFold(n_splits=5, shuffle=True, random_state=42)

###### ここからがLightGBMの実装 ######
# データをDatasetクラスに格納
dcv = lgb.Dataset(X, label=y)  # クロスバリデーション用
# 使用するパラメータ
params = {'objective': 'regression',  # 最小化させるべき損失関数(2クラス分類の場合'binary'、多クラス分類の場合'multiclass')
         'metric': 'rmse',  # 学習時に使用する評価指標(2クラス分類の場合'binary_logloss'等、多クラス分類の場合'multi_logloss'等)
         'random_state': 42,  # 乱数シード
         'boosting_type': 'gbdt',  # boosting_type
         'verbose': -1  # これを指定しないと`No further splits with positive gain, best gain: -inf`というWarningが表示される
         }
verbose_eval = 0  # この数字を1にすると学習時のスコア推移がコマンドライン表示される
# early_stoppingを指定してLightGBMをクロスバリデーション
cv_result = lgb.cv(params, dcv,
                num_boost_round=10000,  # 最大学習サイクル数。early_stopping使用時は大きな値を入力
                folds=cv,
                callbacks=[lgb.early_stopping(stopping_rounds=10, 
                                verbose=True), # early_stopping用コールバック関数
                           lgb.log_evaluation(verbose_eval)] # コマンドライン出力用コールバック関数
                )
print(cv_result)
print(f'RMSE mean={cv_result["rmse-mean"][-1]}')
```

```:実行結果
Training until validation scores don't improve for 10 rounds
Early stopping, best iteration is:
[35]	cv_agg's rmse: 0.676762 + 0.0371949
{'rmse-mean': [1.092268418453115, 1.0310450173717558, 0.9795686341063112, 0.9357050975378067, 0.898026355776955, 0.8653287143548599, 0.8361953918654839, 0.8118105375696019, 0.7913531404345221, 0.7740533884697003, 0.7581006340525287, 0.7457239616368891, 0.734370054531101, 0.7257891055825861, 0.7187968572833688, 0.711875704872069, 0.7061541973418042, 0.7016815498175433, 0.6974989018815776, 0.6953583144477753, 0.6934006796597723, 0.6913382320507665, 0.6891147218487653, 0.6873271643189067, 0.6859662264261306, 0.6848257970905169, 0.6834265660057884, 0.6821396043772722, 0.6813493570473369, 0.6802932554692094, 0.6794027048145301, 0.6784863031615622, 0.6777672012337602, 0.6775112510272382, 0.6767615074384222], 'rmse-stdv': [0.047163893109934095, 0.04330692544819591, 0.0412303842717346, 0.03918788111501026, 0.037610483543590825, 0.03572218937059543, 0.03549682540172055, 0.03524086095052791, 0.03624560680763654, 0.037586214743957506, 0.03928402431293732, 0.0378483690801823, 0.03818586654066806, 0.03869355751568724, 0.03910746326923766, 0.03936895674923746, 0.03947841587505398, 0.039117262719758225, 0.03959543152820096, 0.03881841849115544, 0.03881540132987051, 0.03799963684609526, 0.0369718932140366, 0.03680779639424503, 0.03670904081679501, 0.03661096377218453, 0.03642053463285243, 0.03642331278429394, 0.036879608212898304, 0.03692147800332548, 0.03672366274081491, 0.03609096933230052, 0.03617965035489701, 0.0362180844993717, 0.03719491159161849]}
RMSE mean=0.6767615074384222
```
UserWarningが消えており、かつ[旧指定方法の時](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#training-api-lightgbmcvメソッドの場合)と同スコアとなっており、同様の処理が実現できていることが分かります

また35回目のイテレーションで最大値となった(45回目でearly_stoppingが適用された)ことが表示されており、より結果が見やすくなっています。


### Scikit-Learn API (スクラッチ実装)の場合
[クロスバリデーションなしの場合](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#scikit-learn-api-1)と同様、学習実行メソッド`fit()`の`early_stopping_rounds`引数の代わりに、`callbacks`引数にコールバック関数`lightgbm.early_stopping()`をリスト指定することで、early_stoppingを実現できます。

```python:Scikit-LearnAPI+新指定方法(コールバック関数)+スクラッチ実装のクロスバリデーション
:
データ読込処理は省略（上記コードと同じ）
:
# クロスバリデーション用のScikit-Learnクラス（5分割KFold）
cv = KFold(n_splits=5, shuffle=True, random_state=42)

# クロスバリデーションのデータ分割
scores=[]
for i, (train, test) in enumerate(cv.split(X, y)):
    ###### ここからがLightGBMの実装 ######
    # 使用するパラメータ
    params = {'objective': 'regression',  # 最小化させるべき損失関数(2クラス分類の場合'binary'、多クラス分類の場合'multiclass')
            'random_state': 42,  # 乱数シード
            'boosting_type': 'gbdt',  # boosting_type
            'n_estimators': 10000  # 最大学習サイクル数。early_stopping使用時は大きな値を入力
            }
    verbose_eval = 0  # この数字を1にすると学習時のスコア推移がコマンドライン表示される
    # early_stoppingを指定してLightGBM学習
    lgbr = lgb.LGBMRegressor(**params)
    lgbr.fit(X[train], y[train], 
            eval_metric='rmse',  # early_stoppingの評価指標(学習用の'metric'パラメータにも同じ指標が自動入力される)
            eval_set=[(X[test], y[test])],
            callbacks=[lgb.early_stopping(stopping_rounds=10, 
                                verbose=True), # early_stopping用コールバック関数
                           lgb.log_evaluation(verbose_eval)] # コマンドライン出力用コールバック関数
            )

    # スコア(RMSE)算出
    y_pred = lgbr.predict(X[test])

    score = mean_squared_error(y_true=y[test], y_pred=y_pred, squared=False)
    scores.append(score)
print(f'RMSE={scores} \nRMSE mean={np.mean(scores)}')
```

```:実行結果
Training until validation scores don't improve for 10 rounds
Early stopping, best iteration is:
[30]	valid_0's rmse: 0.738984	valid_0's l2: 0.546097
Training until validation scores don't improve for 10 rounds
Early stopping, best iteration is:
[37]	valid_0's rmse: 0.615002	valid_0's l2: 0.378228
Training until validation scores don't improve for 10 rounds
Early stopping, best iteration is:
[43]	valid_0's rmse: 0.680395	valid_0's l2: 0.462938
Training until validation scores don't improve for 10 rounds
Early stopping, best iteration is:
[36]	valid_0's rmse: 0.644544	valid_0's l2: 0.415437
Training until validation scores don't improve for 10 rounds
Early stopping, best iteration is:
[36]	valid_0's rmse: 0.69132	valid_0's l2: 0.477924
RMSE=[0.738983559441704, 0.6150024788535946, 0.6803952077929987, 0.6445438943448907, 0.6913201783978795] 
RMSE mean=0.6740490637662134
```
[旧方式での実装時](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#scikit-learn-api-スクラッチ実装の場合)、および[Training APIのスクラッチ実装](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#training-api-スクラッチ実装の場合-1)の時と同スコアになっており、同様の処理が実現できていることが分かります。

### Scikit-Learn API (`cross_val_score`メソッド)の場合
繰り返しとなりますが、Scikit-Learnには`cross_val_score()`という、クロスバリデーションでスコアを計算するためのメソッドが存在し、これを利用することでクロスバリデーションを簡単に実装することができます。

しかしこの`cross_val_score()`、クロスバリデーション分割後のデータをearly_stoppingの評価用データである`eval_set`に渡せないため、[lightgbm.cv()](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#training-api-lightgbmcvメソッドの場合-1)メソッドのときのように**テストデータからearly_stoppingを判定することができません**。

そこでクロスバリデーション分割後の**テストデータを**`eval_set`**に渡せるようにしたメソッド**`cross_val_score_eval_set()`を**新たに自作**し、[こちらの記事で紹介した](https://qiita.com/c60evaporator/items/8cb774e65d4423fd93ee)ライブラリ[seaborn-anlayzer](https://github.com/c60evaporator/seaborn-analyzer)に追加しました。以下に使用例を記載します。

```python:Scikit-LearnAPI+新指定方法(コールバック関数)+cross_val_score_eval_setでクロスバリデーション
:
データ読込処理は省略（上記コードと同じ）
:
# クロスバリデーション用のScikit-Learnクラス（5分割KFold）
cv = KFold(n_splits=5, shuffle=True, random_state=42)

###### ここからがLightGBMの実装 ######
from seaborn_analyzer._cv_eval_set_old import cross_val_score_eval_set
# 使用するパラメータ
params = {'objective': 'regression',  # 最小化させるべき損失関数(2クラス分類の場合'binary'、多クラス分類の場合'multiclass')
        'random_state': 42,  # 乱数シード
        'boosting_type': 'gbdt',  # boosting_type
        'n_estimators': 10000  # 最大学習サイクル数。early_stopping使用時は大きな値を入力
        }
verbose_eval = 0  # この数字を1にすると学習時のスコア推移がコマンドライン表示される
# early_stoppingを指定してLightGBM学習
lgbr = lgb.LGBMRegressor(**params)
# クロスバリデーション内部で`fit()`メソッドに渡すパラメータ
fit_params = {'eval_metric':'rmse',
              'eval_set':[(X, y)],
              'early_stopping_rounds': 10,
              'verbose': verbose_eval}
# クロスバリデーション実行
scores = cross_val_score_eval_set(
        eval_set_selection='test',  # 'test'と指定するとテストデータを'eval_set'に渡せる
        estimator=lgbr,  # 学習器
        X=X, y=y,  # クロスバリデーション分割前のデータを渡す
        scoring='neg_root_mean_squared_error',  # RMSE（の逆数）を指定
        cv=cv, verbose=verbose_eval, fit_params=fit_params
        )
print(f'RMSE={scores} \nRMSE mean={np.mean(scores)}')
```

```:実行結果
RMSE=[-0.73898356 -0.61500248 -0.68039521 -0.64454389 -0.69132018] 
RMSE mean=-0.6740490637662134
```
[Training API](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#training-api-%E3%82%B9%E3%82%AF%E3%83%A9%E3%83%83%E3%83%81%E5%AE%9F%E8%A3%85%E3%81%AE%E5%A0%B4%E5%90%88-1)および[Scikit-Learn APIのスクラッチ実装](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#scikit-learn-api-%E3%82%B9%E3%82%AF%E3%83%A9%E3%83%83%E3%83%81%E5%AE%9F%E8%A3%85%E3%81%AE%E5%A0%B4%E5%90%88-1)と同スコアになっており、同様の処理がループを実装することなく実現できていることが分かります。

またAPI的には旧方式である`early_stopping_rounds`引数を使用していますが（XGBoost等とAPIを統一するためにあえてこちらを使用）、内部的には現在の推奨方式であるコールバック関数`early_stopping()`に変換しているため、UserWarningも出ません。

また[前述のように](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#training-api-%E3%82%B9%E3%82%AF%E3%83%A9%E3%83%83%E3%83%81%E5%AE%9F%E8%A3%85%E3%81%AE%E5%A0%B4%E5%90%88)コールバック関数は学習を1回実行するごとに初期化する必要がありますが、こちらも学習実行ごとに初期化するよう内部的に実装しています。

LightGBM単体でクロスバリデーションしたい際には`lightgbm.cv()`メソッドの方が使い勝手が良いですが、`cross_val_score_eval_set()`メソッドは**LightGBM以外のScikit-Learn学習器**（SVM, XGBoost等）**にもそのまま適用できるため、[後述のように](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#%E5%8F%82%E8%80%83-training-api%E3%81%A8scikit-learn-api%E3%81%AE%E5%B7%AE)APIの共通化**を図りたい際にご活用頂ければと思います。



# 【参考】 Training APIとScikit-Learn APIの差
本記事でも逐次触れましたが、LightGBMには**Training APIとScikit-Learn APIという2種類の実装方式**が存在します。

どちらも広く用いられており、LightGBMの使用法を学ぶ上で混乱の一因となっているため、両者の違いについて触れたいと思います。

[コードを見ると](https://github.com/microsoft/LightGBM/blob/master/python-package/lightgbm/sklearn.py#L752)Scikit-Learn APIも内部的にTraining APIを実行しているため、両者の学習器としての動作は同一ですが、データの入力形式や使用方法に大きな差があります。

**以下に主な差異をまとめました**

||Training API|Scikit-Learn API|
|---|---|---|
|使用するデータセットの型|`Dataset`クラスに格納する必要がある|numpy配列(ndarray)をそのまま渡せる|
|学習用メソッド名|[`train()`](https://lightgbm.readthedocs.io/en/latest/pythonapi/lightgbm.train.html#lightgbm.train)メソッド|[`fit()`](https://lightgbm.readthedocs.io/en/latest/pythonapi/lightgbm.LGBMRegressor.html#lightgbm.LGBMRegressor.fit)メソッド|
|使用する評価指標|学習時の`train()`メソッドの`params`引数中に、`metrics`として指定|学習時の`fit()`メソッドの`eval_metric`引数に渡す|
|学習の最大回数|学習用メソッド`train()`の`num_boost_round`引数に渡す|`LGBMRegressor`, `LGBMClassifier`クラス初期化時の`n_estimators`引数に渡す|
|early_stopping判定の評価用データ|学習用メソッド`train()`の`valid_sets`引数に渡す|学習用メソッド`fit()`の`eval_set`引数に渡す|
|クロスバリデーション用メソッド名|[`cv()`](https://lightgbm.readthedocs.io/en/latest/pythonapi/lightgbm.cv.html)メソッド|なし ([前述の自作メソッド](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#scikit-learn-api-cross_val_score%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%AE%E5%A0%B4%E5%90%88-1)で対応可)|

両者の使い分けですが、

**・LightGBM単体で使用したい場合 → Training APIの方が使い勝手が良い**
**・他の学習器（SVM、XGBoost等）と併用したい場合 → APIが統一できるScikit-Learn APIの方が使い勝手が良い**

のように認識するのが適切かと思います。

[mlxtend](http://rasbt.github.io/mlxtend/)等の可視化ライブラリもScikit-Learnの学習器を渡す形式が多く、そういう意味でScikit-Learn APIの方が汎用性が広いと言えます。
一方で[前述のように](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#scikit-learn-api-cross_val_score%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%AE%E5%A0%B4%E5%90%88-1)**Scikit-Learn APIはクロスバリデーションとearly_stoppingを同時に使用できない**という弱点を抱えており（今回は[自作メソッド](https://github.com/c60evaporator/seaborn-analyzer/blob/master/seaborn_analyzer/_cv_eval_set.py#L331)で対応しましたが、本家LightGBMでも[類似機能の議論](https://github.com/microsoft/LightGBM/issues/3313)が行われているようです）**クロスバリデーションを前提としたパラメータチューニング等の用途には現時点ではTraining APIの方が向いている**と言えそうです。

参考までに、[LightGBM公式](https://lightgbm.readthedocs.io/en/latest/Parameters-Tuning.html)ではパラメータチューニングの一例として[OptunaのLightGBMチューニング用クラス](https://medium.com/optuna/lightgbm-tuner-new-optuna-integration-for-hyperparameter-optimization-8b7095e99258)の使用が紹介されています（[サンプルコード](https://github.com/optuna/optuna-examples/blob/main/lightgbm/lightgbm_tuner_simple.py)）


# 【参考】 第3のearly_stopping指定方法
本記事で紹介した旧指定方法(`early_stopping_rounds`引数)、現在の推奨方法(コールバック関数`early_stopping()`)以外にも、実は第3のearly_stopping指定方法が存在します。

それは**学習実行メソッド**（`train()`）の`params`**引数に**`early_stopping_round`**という値を指定**する方法です（同様に`eval_metric`も`metric`という名前に変えて`params`引数の中に含めます）

例えば、クロスバリデーションなしのTraining API（[こちらのケース](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#training-api%E3%81%AE%E5%A0%B4%E5%90%88-2)と同様の処理）では、以下のように指定します

```python:TrainingAPI+第3の指定方法('params'引数の'early_stopping_round')での実装法
import pandas as pd
import numpy as np
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
import lightgbm as lgb
# データセット読込(カリフォルニア住宅価格)
TARGET_VARIABLE = 'price'  # 目的変数名
USE_EXPLANATORY = ['MedInc', 'AveOccup', 'Latitude', 'HouseAge']  # 説明変数名
california_housing = pd.DataFrame(np.column_stack((fetch_california_housing().data, fetch_california_housing().target)),
    columns = np.append(fetch_california_housing().feature_names, TARGET_VARIABLE))
california_housing = california_housing.sample(n=1000, random_state=42)  # データ数多いので1000にサンプリング
y = california_housing[TARGET_VARIABLE].values  # 目的変数のnumpy配列
X = california_housing[USE_EXPLANATORY].values  # 説明変数のnumpy配列
# テストデータと学習データ分割
X_train_raw, X_test, y_train_raw, y_test = train_test_split(X, y, test_size=0.25, random_state=42)
# early_stopping用の評価データをさらに分割
X_train, X_valid, y_train, y_valid = train_test_split(X_train_raw, y_train_raw, test_size=0.25, random_state=42)

###### ここからがLightGBMの実装 ######
# データをDatasetクラスに格納
dtrain = lgb.Dataset(X_train, label=y_train)  # 学習用
dvalid = lgb.Dataset(X_valid, label=y_valid)  # early_stopping用
# 使用するパラメータ
params = {'objective': 'regression',  # 最小化させるべき損失関数(2クラス分類の場合'binary'、多クラス分類の場合'multiclass')
         'metric': 'rmse',  # 学習時に使用する評価指標(2クラス分類の場合'binary_logloss'等、多クラス分類の場合'multi_logloss'等)
         'random_state': 42,  # 乱数シード
         'boosting_type': 'gbdt',  # boosting_type
         'verbose': -1,  # これを指定しないと`No further splits with positive gain, best gain: -inf`というWarningが表示される
         'early_stopping_round': 10  # ここでearly_stoppingを指定
         }
# early_stoppingを指定してLightGBM学習
gbm = lgb.train(params, dtrain,
                valid_sets=[dvalid],  # early_stoppingの評価用データ
                num_boost_round=10000  # 最大学習サイクル数。early_stopping使用時は大きな値を入力
                )

# スコア(RMSE)算出
y_pred = gbm.predict(X_test)
score = mean_squared_error(y_true=y_test, y_pred=y_pred, squared=False)
print(f'RMSE={score}')
```

同様に、クロスバリデーションなしのScikit-Learn API（[こちらのケース](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#scikit-learn-api%E3%81%AE%E5%A0%B4%E5%90%88-2)と同様の処理）では、以下のように指定します
（`LGBMRegressor`クラスのコンストラクタに`early_stopping_round`が渡される）

```python:Scikit-LearnAPI+第3の指定方法('params'引数の'early_stopping_round')での実装法
import pandas as pd
import numpy as np
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
import lightgbm as lgb
# データセット読込(カリフォルニア住宅価格)
TARGET_VARIABLE = 'price'  # 目的変数名
USE_EXPLANATORY = ['MedInc', 'AveOccup', 'Latitude', 'HouseAge']  # 説明変数名
california_housing = pd.DataFrame(np.column_stack((fetch_california_housing().data, fetch_california_housing().target)),
    columns = np.append(fetch_california_housing().feature_names, TARGET_VARIABLE))
california_housing = california_housing.sample(n=1000, random_state=42)  # データ数多いので1000にサンプリング
y = california_housing[TARGET_VARIABLE].values  # 目的変数のnumpy配列
X = california_housing[USE_EXPLANATORY].values  # 説明変数のnumpy配列
# テストデータと学習データ分割
X_train_raw, X_test, y_train_raw, y_test = train_test_split(X, y, test_size=0.25, random_state=42)
# early_stopping用の評価データをさらに分割
X_train, X_valid, y_train, y_valid = train_test_split(X_train_raw, y_train_raw, test_size=0.25, random_state=42)

###### ここからがLightGBMの実装 ######
# 使用するパラメータ
params = {'objective': 'regression',  # 最小化させるべき損失関数(2クラス分類の場合'binary'、多クラス分類の場合'multiclass')
          'metric': 'rmse',  # 学習時に使用する評価指標(2クラス分類の場合'binary_logloss'等、多クラス分類の場合'multi_logloss'等)
          'random_state': 42,  # 乱数シード
          'boosting_type': 'gbdt',  # boosting_type
          'n_estimators': 10000,  # 最大学習サイクル数。early_stopping使用時は大きな値を入力
          'verbose': -1,  # これを指定しないと`No further splits with positive gain, best gain: -inf`というWarningが表示される
          'early_stopping_round': 10  # ここでearly_stoppingを指定
          }
# early_stoppingを指定してLightGBM学習
lgbr = lgb.LGBMRegressor(**params)
lgbr.fit(X_train, y_train, 
         eval_set=[(X_valid, y_valid)]
         )

# スコア(RMSE)算出
y_pred = lgbr.predict(X_test)
score = mean_squared_error(y_true=y_test, y_pred=y_pred, squared=False)
print(f'RMSE={score}')
```

[公式のAPIリファレンス](https://lightgbm.readthedocs.io/en/latest/Parameters.html#early_stopping_round)でも補足扱いで紹介されている方法ですが、[クロスバリデーション時のコールバック関数の初期化問題](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#training-api-スクラッチ実装の場合)を回避できる方法の一つではあるので、紹介させて頂きました。

## 第3のearly_stopping指定方法 + クロスバリデーション

[先ほどのcross_val_score改メソッド](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#scikit-learn-api-cross_val_scoreメソッドの場合-1)と第3のearly_stopping指定方法を組み合わせて、Scikit-Learn APIでearly_stoppingを有効化（early_stopping用データにはテストデータを使用）してクロスバリデーションを実施する方法も実装しました。[`seaborn_analyzer.cross_val_score_eval_set()`](https://github.com/c60evaporator/seaborn-analyzer/blob/master/seaborn_analyzer/_cv_eval_set.py#L352)メソッドとして実装しています。

`validation_fraction`引数に`'cv'`と渡すと、[先ほどのcross_val_score改メソッド](https://qiita.com/c60evaporator/items/2b7a2820d575e212bcf4#scikit-learn-api-cross_val_scoreメソッドの場合-1)と同様にクロスバリデーションのテストデータをearly_stopping用データ（eval_set）に動的に渡せます。

```python:lightgbm_sklearn_api_3rd_crossvalscore_cv.py
import pandas as pd
import numpy as np
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import KFold
import lightgbm as lgb
from seaborn_analyzer import cross_val_score_eval_set

# データセット読込(カリフォルニア住宅価格)
TARGET_VARIABLE = 'price'  # 目的変数名
USE_EXPLANATORY = ['MedInc', 'AveOccup', 'Latitude', 'HouseAge']  # 説明変数名
california_housing = pd.DataFrame(np.column_stack((fetch_california_housing().data, fetch_california_housing().target)),
    columns = np.append(fetch_california_housing().feature_names, TARGET_VARIABLE))
california_housing = california_housing.sample(n=1000, random_state=42)  # データ数多いので1000にサンプリング
y = california_housing[TARGET_VARIABLE].values  # 目的変数のnumpy配列
X = california_housing[USE_EXPLANATORY].values  # 説明変数のnumpy配列

# クロスバリデーション用のScikit-Learnクラス（5分割KFold）
cv = KFold(n_splits=5, shuffle=True, random_state=42)

###### ここからがLightGBMの実装 ######
# 使用するパラメータ
params = {'objective': 'regression',  # 最小化させるべき損失関数(2クラス分類の場合'binary'、多クラス分類の場合'multiclass')
          'metric': 'rmse',  # 学習時に使用する評価指標(2クラス分類の場合'binary_logloss'等、多クラス分類の場合'multi_logloss'等)
          'random_state': 42,  # 乱数シード
          'boosting_type': 'gbdt',  # boosting_type
          'n_estimators': 10000,  # 最大学習サイクル数。early_stopping使用時は大きな値を入力
          'verbose': -1,  # これを指定しないと`No further splits with positive gain, best gain: -inf`というWarningが表示される
          'early_stopping_round': 10  # ここでearly_stoppingを指定
          }
# early_stoppingを指定してLightGBM学習
lgbr = lgb.LGBMRegressor(**params)
# クロスバリデーション内部で`fit()`メソッドに渡すパラメータ
fit_params = {'eval_set':[(X, y)]
              }
# クロスバリデーション実行
scores = cross_val_score_eval_set(
        validation_fraction='cv',  # 'cv'と指定するとテストデータを'eval_set'に渡せる
        estimator=lgbr,  # 学習器
        X=X, y=y,  # クロスバリデーション分割前のデータを渡す
        scoring='neg_root_mean_squared_error',  # RMSE（の逆数）を指定
        cv=cv, fit_params=fit_params
        )
print(f'RMSE={scores} \nRMSE mean={np.mean(scores)}')
```

```:実行結果
RMSE=[-0.73898356 -0.61500248 -0.68039521 -0.64454389 -0.69132018] 
RMSE mean=-0.6740490637662134
```

### 第3のearly_stopping指定方法 + クロスバリデーション（リークを完全に防ぐ）

直前で紹介した[`seaborn_analyzer.cross_val_score_eval_set()`](https://github.com/c60evaporator/seaborn-analyzer/blob/master/seaborn_analyzer/_cv_eval_set.py#L352)メソッドの`validation_fraction`引数に`'cv'`と渡すと、[前述のように]()early_stopping評価用データとスコア評価用のデータが同一となっており、early_stopping用のデータがリークした状態となっています（下図の上側の図が実際のデータ分割方法です）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610167/40c9559f-e22a-4e67-ab76-63a898e6d8d3.png)

これを防ぐためには、上図の下側の図のようにearly_stopping用のデータを学習データから分割することが有効です（[Scikit-learnのGradientBoostingRegressor](https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.GradientBoostingRegressor.html#sklearn.ensemble.GradientBoostingRegressor)の`validation_fraction`引数と同様の分割法）。[`seaborn_analyzer.cross_val_score_eval_set()`](https://github.com/c60evaporator/seaborn-analyzer/blob/master/seaborn_analyzer/_cv_eval_set.py#L352)メソッドでも、`validation_fraction`**引数にfloat型の値を渡す**ことで、上図の下側の図のように**リークを完全に防いだ分割法**を実現できるようにしました。学習データから`validation_fraction`引数で指定した割合だけearly_stopping用データ（eval_data）に分割されます（eval_dataに指定されたデータは学習データから除外される）

以下、具体的な実装法です（前の例の`validation_fraction`引数をfloat型に変えただけです）

```python:lightgbm_sklearn_api_3rd_crossvalscore_cv.py
import pandas as pd
import numpy as np
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import KFold
import lightgbm as lgb
from seaborn_analyzer import cross_val_score_eval_set

# データセット読込(カリフォルニア住宅価格)
TARGET_VARIABLE = 'price'  # 目的変数名
USE_EXPLANATORY = ['MedInc', 'AveOccup', 'Latitude', 'HouseAge']  # 説明変数名
california_housing = pd.DataFrame(np.column_stack((fetch_california_housing().data, fetch_california_housing().target)),
    columns = np.append(fetch_california_housing().feature_names, TARGET_VARIABLE))
california_housing = california_housing.sample(n=1000, random_state=42)  # データ数多いので1000にサンプリング
y = california_housing[TARGET_VARIABLE].values  # 目的変数のnumpy配列
X = california_housing[USE_EXPLANATORY].values  # 説明変数のnumpy配列

# クロスバリデーション用のScikit-Learnクラス（5分割KFold）
cv = KFold(n_splits=5, shuffle=True, random_state=42)

###### ここからがLightGBMの実装 ######
# 使用するパラメータ
params = {'objective': 'regression',  # 最小化させるべき損失関数(2クラス分類の場合'binary'、多クラス分類の場合'multiclass')
          'metric': 'rmse',  # 学習時に使用する評価指標(2クラス分類の場合'binary_logloss'等、多クラス分類の場合'multi_logloss'等)
          'random_state': 42,  # 乱数シード
          'boosting_type': 'gbdt',  # boosting_type
          'n_estimators': 10000,  # 最大学習サイクル数。early_stopping使用時は大きな値を入力
          'verbose': -1,  # これを指定しないと`No further splits with positive gain, best gain: -inf`というWarningが表示される
          'early_stopping_round': 10  # ここでearly_stoppingを指定
          }
# early_stoppingを指定してLightGBM学習
lgbr = lgb.LGBMRegressor(**params)
# クロスバリデーション内部で`fit()`メソッドに渡すパラメータ
fit_params = {'eval_set':[(X, y)]
              }
# クロスバリデーション実行
scores = cross_val_score_eval_set(
        validation_fraction=0.3,  # floatで指定した割合で学習データから'eval_set'を分割する（同時に学習データからeval_setの分が除外される）
        estimator=lgbr,  # 学習器
        X=X, y=y,  # クロスバリデーション分割前のデータを渡す
        scoring='neg_root_mean_squared_error',  # RMSE（の逆数）を指定
        cv=cv, fit_params=fit_params
        )
print(f'RMSE={scores} \nRMSE mean={np.mean(scores)}')
```

過学習絶対に許さないマンを目指すのであれば、現状この方法がベストに近いかと思います。

### 2023/6追記　XGBoostのScikit-learn APIとの互換性

XGBoost1.6より、XGBoostのScikit-learn APIでもでもこの第3の指定方法と同様に、`XGBRegressor`クラスのコンストラクタに`early_stopping_rounds`引数を渡すことが、[early stoppingの推奨指定方法](https://xgboost.readthedocs.io/en/stable/python/python_api.html#xgboost.XGBRegressor)となりました。

XGBoostでは本記事の第1および第2の方法である[fit()メソッドのearly_stopping_rounds引数やコールバック関数が非推奨](https://xgboost.readthedocs.io/en/stable/python/python_api.html#xgboost.XGBRegressor.fit)となっている事から、**XGBoostのScikit-learn APIとの互換性の観点では、この第3の指定方法がベスト**となると考えられます。

# サンプルコード
本記事で使用したサンプルコードは以下のGitHubリポジトリにまとめております

https://github.com/c60evaporator/lightgbm_early_stopping

|クロスバリデーション　　　　|API|旧指定方法<br>('early_stopping_rounds'<br>引数)|現在の推奨方法<br>(コールバック関数<br>'early_stopping()')|第3の方法<br>(param['early_stopping_round']引数)|
|---|---|---|---|---|
|**なし**|**Training API**|[lightgbm_training_api_old.py](https://github.com/c60evaporator/lightgbm_early_stopping/blob/main/lightgbm_training_api_old.py)|[lightgbm_training_api_new.py](https://github.com/c60evaporator/lightgbm_early_stopping/blob/main/lightgbm_training_api_new.py)|[lightgbm_training_api_3rd.py](https://github.com/c60evaporator/lightgbm_early_stopping/blob/main/lightgbm_training_api_3rd.py)|
|**なし**|**Scikit-Learn API**|[lightgbm_sklearn_api_old.py](https://github.com/c60evaporator/lightgbm_early_stopping/blob/main/lightgbm_sklearn_api_old.py)|[lightgbm_sklearn_api_new.py](https://github.com/c60evaporator/lightgbm_early_stopping/blob/main/lightgbm_sklearn_api_new.py)|[lightgbm_sklearn_api_3rd.py](https://github.com/c60evaporator/lightgbm_early_stopping/blob/main/lightgbm_sklearn_api_3rd.py)|
|**あり**|**Training API**<br>(スクラッチ実装)|[lightgbm_training_api_cv_scratch_old.py](https://github.com/c60evaporator/lightgbm_early_stopping/blob/main/lightgbm_training_api_cv_scratch_old.py)|[lightgbm_training_api_cv_scratch_new.py](https://github.com/c60evaporator/lightgbm_early_stopping/blob/main/lightgbm_training_api_cv_scratch_new.py)|
|**あり**|**Training API**<br>(`lightgbm.cv`メソッド)|[lightgbm_training_api_cv_lgbcv_old.py](https://github.com/c60evaporator/lightgbm_early_stopping/blob/main/lightgbm_training_api_cv_lgbcv_old.py)|**[lightgbm_training_api_cv_lgbcv_new.py](https://github.com/c60evaporator/lightgbm_early_stopping/blob/main/lightgbm_training_api_cv_lgbcv_new.py)**|
|**あり**|**Scikit-Learn API**<br>(スクラッチ実装)|[lightgbm_sklearn_api_cv_old.py](https://github.com/c60evaporator/lightgbm_early_stopping/blob/main/lightgbm_sklearn_api_cv_old.py)|[lightgbm_sklearn_api_cv_new.py](https://github.com/c60evaporator/lightgbm_early_stopping/blob/main/lightgbm_sklearn_api_cv_new.py)|
|**あり**|**Scikit-Learn API**<br>(`cross_val_score`メソッド)|-|**[lightgbm_sklearn_api_cv_crossvalscore.py](https://github.com/c60evaporator/lightgbm_early_stopping/blob/main/lightgbm_sklearn_api_cv_crossvalscore.py)**|[**lightgbm_sklearn_api_3rd_crossvalscore.py**](https://github.com/c60evaporator/lightgbm_early_stopping/blob/main/lightgbm_sklearn_api_3rd_crossvalscore.py)|