はじめに
R のtidymodelsでlightgbmをするときの覚書を残します。いまいち分かっていない点も所々あるのですが、まあわかってないなりにまとめておくことが重要かな、と思ってとりあえず書きました。tidymodelsの勉強という側面もあるので、ステップバイステップで進めています。
パッケージの紹介
tidymodels
, lightgbm
, bonsai
を使います。
bonsai?
tidymodels
を通してツリー系のモデルを建てたい場合、bonsai
を使います。枝葉を適宜に間引くことで美しい木を育てる、という面から名付けたのでしょうか。オシャレな名前だなあと思います。
そして、Change logにnum_leaves
のチューニングが出来るようになったよ、と書いてあったので試してみようかな~となった次第です。set_engine()
で指定するとのことです。
With the newest version of each of dials, parsnip, and bonsai installed, tune this argument by marking the
num_leaves
engine argument for tuning when defining your model specification:
boost_tree() %>% set_engine("lightgbm", num_threads = x)
コード
いつものようにirisの判別をしていきます。使うライブラリは以下のものです。
library(tidyverse) #データ処理、パイプ演算子のため
library(tidymodels) #機械学習用、dialsも含まれている
library(lightgbm) #lightGBMをするため
library(bonsai) #lightGBMをtidymodelsを通して使うため
library(doParallel) #並列処理に使う
まずデータを分割します。strataは対象となる変数が均等になるように分割する指示となります。irisであれば、学習データや検証データに特定の種が多いor少ないといった偏りを防ぐため設定します。
今回はiris_trainを学習用、iris_testを検証用データをします。
set.seed(42)
data("iris")
iris_split <- initial_split(iris, prop = 0.8, strata = Species)
iris_train <- training(iris_split)
iris_test <- testing(iris_split)
次に説明変数の前処理を行います。今回のようにlightGBMではぶっちゃけ不要とのことですが、すべての数字列をノーマライズします。このrecipe()
では、ほかにも欠損値をどう扱うか、因子型に対しダミー変数化するか、などの前処理を行うことが出来ます。
iris_recipe <- recipe(Species ~ ., data = iris_split) %>%
step_normalize(all_numeric_predictors())
続いて、どのようなモデルの仕様を決めていきます。今回はlightGBMを使っていくのでboost_tree()
を使い、その中でどのパラメーターをチューニングするかを設定します。チューニングしたいパラメーターはtune()
を引数とします。
今回は、公式でbest firstだ、としている以下の3つのパラメーターに絞ります。
- num_leaves:ツリーモデルの複雑性をコントロールするメインパラメーターで、分岐の終着点の数です。max_depthとの兼ね合いで決めるとのことです。あまり大きすぎると過学習する危険性があり、2^(max_depth)より小さい値にすることが推奨されています。
- min_data_in_leaf:葉のデータの最小数を設定します。tidymodelsではmin_nで指定します。これはlightGBMのようなleaf-wise treeでの過学習を抑えるために重要なパラメーターです。大きな値にすると過学習を防ぐ(ツリーが深くなりすぎる)ことを防ぐことが出来ますが、一方で過小適合となる可能性があります。
- max_depth:ツリーの深さを制限するパラメーターです。tidymodelsではtree_depthで指定します。ここの値を明示的に設定する場合は、num_leavesも明示して2^(max_depth)以下になるように設定することが良いとのことです。
ここで、num_leavesはset_engine()
で設定します。なぜでしょうね。
lightgbm_spec <-
boost_tree(min_n = tune(),
tree_depth = tune(),
) %>%
set_engine("lightgbm", num_leaves = tune()) %>%
set_mode("classification")
つぎにworkflowを立て、モデルとデータを組み合わせます。
今回は、lightGBM決め打ちなのでworkflowを使うメリットがあまり無いように見えますが、複数のモデルで検証する場合はmap関数でまとめて処理することも可能です。bob3さんのサイトのが非常に参考になります。また、今回は後々チューニングした後に再利用するので、ワークフローはやっぱり便利です。
lightgbm_wf <- workflow() %>%
add_recipe(iris_recipe) %>%
add_model(lightgbm_spec)
続いてハイパーパラメータチューニング用のCross validation用のデータを作ります。今回はv = 4を指定したので、4分割のCross validationになります。
cv_folds <- vfold_cv(iris_train, v = 4, strata = Species)
ここで、並列処理を設定します。別にこのタイミングでなくても良いのですが、なんとなくこれから時間がかかる処理をするぞ!というタイミングで設定します。
cl <- makePSOCKcluster(parallel::detectCores() - 1)
registerDoParallel(cl)
グリッドサーチでハイパーパラメータをチューニングします。ベイズ最適でチューニングする場合は、tune_bayes()
を使うことが出来ます。grid = 10は、tune()
で指定したパラメーターを、それぞれ10個ずつチューニングするようにグリッドサーチを行います。大きい数値にすればそれだけ細かくグリッドサーチをしますが、時間がとてもかかるようになります。
res_grid <- tune_grid(lightgbm_wf, cv_folds, grid = 10)
ここから結果を確認していきます。とりあえず結果をauto_plot()
すると、グリッドサーチした結果を確認できます。roc_aucを見ると、なんとなくMinimal node sizeとTree depthを大きくすると精度が良くなるような傾向が見えます。
autoplot(res_grid)
ハイパーパラメータを確認します。今回はroc_aucが良いものを選びます。
確認すると、numl_leaves < 2^(tree_depth)になっているので、これをそのまま用います。ちなみに、このnuml_leaves < 2^(tree_depth) が満たせなかった場合、どうすればよいのかは分かりません。(明示的に指定するしかないのかな)
best_params <- select_best(res_grid, metric = "roc_auc")
best_params
# A tibble: 1 × 4
min_n tree_depth num_leaves .config
<int> <int> <int> <chr>
1 39 11 59 Preprocessor1_Model05
ここで得られたハイパーパラメータを使ってファイナライズしていきます。finalize_workflow()
をって、元のワークフローに対し、先ほど求められたパラメーターを設定します。
final_wf <- finalize_workflow(lightgbm_wf, best_params)
final_wf
══ Workflow ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
Preprocessor: Recipe
Model: boost_tree()
── Preprocessor ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
1 Recipe Step
• step_normalize()
── Model ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Boosted Tree Model Specification (classification)
Main Arguments:
min_n = 39
tree_depth = 11
Engine-Specific Arguments:
num_leaves = 59
Computational engine: lightgbm
いよいよ大詰め、先ほど作ったワークフローにlast_fit()
を繋げ、モデルを作成します。last_fit()
はsplitしたデータのみ渡すことが出来ます。学習データを入れ子的にさらに分割し、fitting用のデータとfittingしたデータを検証するためのデータに分けているのかな、と思います。ちなみに、splitしていないデータを渡すとエラーが出ます。
iris_train_split <- initial_split(iris_train, prop = 0.8, strata = Species)
turned_fit <- final_wf %>%
last_fit(iris_train_split)
最後に、最初に作った検証用データを使って、作成したモデルを評価します。
test_predictions <- predict(turned_fit %>% extract_workflow(), new_data = iris_test)
iris_test %>% select(Species) %>%
mutate(pred_Species = test_predictions$.pred_class) %>%
conf_mat(truth = Species, estimate = pred_Species)
外したのは2個だけなので、まあこんなものなのでしょうか。
Truth
Prediction setosa versicolor virginica
setosa 10 0 0
versicolor 0 9 1
virginica 0 1 9
以上です。
参考にした記事
以下の記事を主に参考にしました。本当にありがとうございます。