2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

R言語Advent Calendar 2024

Day 8

tidymodelsでlightgbmをやるときの覚書

Last updated at Posted at 2024-12-15

はじめに

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_leavesset_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)

iris_lightgbm.png

ハイパーパラメータを確認します。今回は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

以上です。

参考にした記事

以下の記事を主に参考にしました。本当にありがとうございます。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?