UPDATE: 2022-12-18 12:01:42

はじめに

tidymodelsパッケージの使い方をいくつかのノートに分けてまとめている。tidymodelsパッケージは、統計モデルや機械学習モデルを構築するために必要なパッケージをコレクションしているパッケージで、非常に色んなパッケージがある。ここでは、今回はworkflowsというパッケージの使い方をまとめていく。モデルの数理的な側面や機械学習の用語などは、このノートでは扱わない。

下記の公式ドキュメントやtidymodelsパッケージに関する書籍を参考にしている。

workflowsパッケージの目的

workflowsパッケージのワークフローを利用することで、前処理、モデリング、後処理をまとめることができる。基本的には、recipeパッケージやparsnipパッケージを組み合わせて、ワークフローを作成することになる。公式ドキュメントはこちら。

workflowsパッケージの実行例

workflowsパッケージの基本的な利用方法を見る前に必要なオブジェクトを定義しておく。関数の使い方をまとめているので、ここでは動けば良い程度にレシピを作成しておく。

library(tidymodels)
library(tidyverse)
df_past <- read_csv("https://raw.githubusercontent.com/SugiAki1989/statistical_note/main/note_TidyModels00/df_past.csv")
set.seed(1989)

# rsample
df_initial <- df_past %>% initial_split(prop = 0.8, strata = "Status")
df_train <- df_initial %>% training()
df_test <- df_initial %>% testing()

# parsnip
model1 <- rand_forest(mtry = 5, trees = 1000) %>%
  set_engine("ranger", importance = "permutation") %>%
  set_mode("classification")

# recipes
recipe1 <- recipe(Status ~ ., data = df_train) %>% 
  step_impute_bag(Income, impute_with = imp_vars(Marital, Expenses)) %>% 
  step_impute_bag(Assets, impute_with = imp_vars(Marital, Expenses, Income)) %>% 
  step_impute_bag(Debt, impute_with = imp_vars(Marital, Expenses, Income)) %>% 
  step_impute_bag(Home, impute_with = imp_vars(Marital, Expenses, Income)) %>% 
  step_impute_bag(Marital, impute_with = imp_vars(Marital, Expenses, Income)) %>% 
  step_impute_bag(Job, impute_with = imp_vars(Marital, Expenses, Income))

workflow関数で、前処理、モデリングの情報をまとめておく。ワークフローには、レシピの内容、モデルの設定がまとめられていることがわかる。そのため、ワークフローを使えば、データに対して、どのような前処理を行うのか、そして、どのようなモデルを適用するのかがわかるので、訓練用、テスト用データを変えてもワークフローを使い回すことで、同じ処理を双方のデータに適用できる。

workflow1 <- workflow() %>% 
  add_recipe(recipe1) %>% 
  add_model(model1)

workflow1
## ══ Workflow ════════════════════════════════════════════════════════════════════
## Preprocessor: Recipe
## Model: rand_forest()
## 
## ── Preprocessor ────────────────────────────────────────────────────────────────
## 6 Recipe Steps
## 
## • step_impute_bag()
## • step_impute_bag()
## • step_impute_bag()
## • step_impute_bag()
## • step_impute_bag()
## • step_impute_bag()
## 
## ── Model ───────────────────────────────────────────────────────────────────────
## Random Forest Model Specification (classification)
## 
## Main Arguments:
##   mtry = 5
##   trees = 1000
## 
## Engine-Specific Arguments:
##   importance = permutation
## 
## Computational engine: ranger

訓練データでモデルを学習するときは、ワークフローオブジェクトをfit関数に渡せば良い。

model_trained_workflow <- 
  workflow1 %>% 
    fit(data = df_train)
model_trained_workflow
## ══ Workflow [trained] ══════════════════════════════════════════════════════════
## Preprocessor: Recipe
## Model: rand_forest()
## 
## ── Preprocessor ────────────────────────────────────────────────────────────────
## 6 Recipe Steps
## 
## • step_impute_bag()
## • step_impute_bag()
## • step_impute_bag()
## • step_impute_bag()
## • step_impute_bag()
## • step_impute_bag()
## 
## ── Model ───────────────────────────────────────────────────────────────────────
## Ranger result
## 
## Call:
##  ranger::ranger(x = maybe_data_frame(x), y = y, mtry = min_cols(~5,      x), num.trees = ~1000, importance = ~"permutation", num.threads = 1,      verbose = FALSE, seed = sample.int(10^5, 1), probability = TRUE) 
## 
## Type:                             Probability estimation 
## Number of trees:                  1000 
## Sample size:                      3206 
## Number of independent variables:  13 
## Mtry:                             5 
## Target node size:                 10 
## Variable importance mode:         permutation 
## Splitrule:                        gini 
## OOB prediction error (Brier s.):  0.1460313

テストデータでモデルの予測値を計算する場合は、ワークフローオブジェクトをpredict関数に渡せば良い。

model_predicted_workflow <- 
  model_trained_workflow %>% 
    predict(df_test)
model_predicted_workflow
## # A tibble: 802 × 1
##    .pred_class
##    <fct>      
##  1 bad        
##  2 bad        
##  3 bad        
##  4 bad        
##  5 good       
##  6 bad        
##  7 bad        
##  8 good       
##  9 bad        
## 10 good       
## # … with 792 more rows

これが基本的なworkflowパッケージの使い方である。モデルとレシピをワークフローが束ねることで、モデルの訓練からモデルの予測までを効率よく行うことができる。

先程はクロスバリデーションを行わず、とりあえずworkflowパッケージの使い方をまとめていたが、ここからはクロスバリデーションを行った場合にワークフローがどのように機能するのかまとめておく。データに分割を行い、先程と同じく、レシピとモデルを作成しておく。

set.seed(1989)
df_train_stratified_splits <- 
  vfold_cv(df_train, v = 5, strata = "Status")

model2 <- rand_forest(mtry = 5, trees = 500, min_n = 10) %>%
  set_engine("ranger", importance = "permutation") %>%
  set_mode("classification")

recipe2 <- recipe(Status ~ ., data = df_train) %>% 
  step_impute_bag(Income, impute_with = imp_vars(Marital, Expenses)) %>% 
  step_impute_bag(Assets, impute_with = imp_vars(Marital, Expenses, Income)) %>% 
  step_impute_bag(Debt, impute_with = imp_vars(Marital, Expenses, Income, Assets)) %>% 
  step_impute_bag(Home, impute_with = imp_vars(Marital, Expenses, Income, Assets, Debt)) %>% 
  step_impute_bag(Marital, impute_with = imp_vars(Marital, Expenses, Income, Assets, Debt, Home)) %>% 
  step_impute_bag(Job, impute_with = imp_vars(Marital, Expenses, Income, Assets, Debt, Home, Marital))

workflow2 <- workflow() %>% 
  add_recipe(recipe2) %>% 
  add_model(model2)

workflow2
## ══ Workflow ════════════════════════════════════════════════════════════════════
## Preprocessor: Recipe
## Model: rand_forest()
## 
## ── Preprocessor ────────────────────────────────────────────────────────────────
## 6 Recipe Steps
## 
## • step_impute_bag()
## • step_impute_bag()
## • step_impute_bag()
## • step_impute_bag()
## • step_impute_bag()
## • step_impute_bag()
## 
## ── Model ───────────────────────────────────────────────────────────────────────
## Random Forest Model Specification (classification)
## 
## Main Arguments:
##   mtry = 5
##   trees = 500
##   min_n = 10
## 
## Engine-Specific Arguments:
##   importance = permutation
## 
## Computational engine: ranger

データを分割しているので、先程のようにそのままワークフローを適用できない。tuneパッケージのfit_resamples関数を使う必要がある。tuneパッケージは次回ノートにまとめるので、ここでは特に説明はしない。また、metric_set関数はyardstickパッケージの関数なので、同じく説明は他のノートでまとめる。

処理内容としては、分割されたデータが複数あるので、それらに対してワークフローで管理しているレシピとモデル内容を使って、評価指標はaccuracyで、計算に使用した予測値は残す、ということをしている。このステップでは、クロスバリデーションしてもモデルを評価しているので、処理時間がかかる。これまでの部分は何をするかを決めていただけで、実際に手は動かしていない。

tuneパッケージを使用している事がわかるようにオブジェクトにはわかりやすいようにtunedとつかておく。

workflow_tuned <- 
workflow2 %>% 
  fit_resamples(
    resamples = df_train_stratified_splits,
    metrics = metric_set(accuracy),
    control = control_resamples(save_pred = TRUE)
  )

中身を確認すると、いくつかのカラムができており、内容は下記の通り。

  • splits: クロスバリデーションのために分割されたデータ
  • id: 分割されたデータのフォールド番号
  • .metrics: 評価指標の値
  • .notes: エラー、ワーニングなどの情報
  • .predictions: 評価指標を計算するために利用したデータの観測値とモデルの予測値
workflow_tuned
## # Resampling results
## # 5-fold cross-validation using stratification 
## # A tibble: 5 × 5
##   splits             id    .metrics         .notes           .predictions      
##   <list>             <chr> <list>           <list>           <list>            
## 1 <split [2564/642]> Fold1 <tibble [1 × 4]> <tibble [0 × 3]> <tibble [642 × 4]>
## 2 <split [2564/642]> Fold2 <tibble [1 × 4]> <tibble [0 × 3]> <tibble [642 × 4]>
## 3 <split [2565/641]> Fold3 <tibble [1 × 4]> <tibble [0 × 3]> <tibble [641 × 4]>
## 4 <split [2565/641]> Fold4 <tibble [1 × 4]> <tibble [0 × 3]> <tibble [641 × 4]>
## 5 <split [2566/640]> Fold5 <tibble [1 × 4]> <tibble [0 × 3]> <tibble [640 × 4]>

splitsの中身は、訓練データの分割情報が記録されている。

workflow_tuned %>% 
  pluck("splits", 1)
## <Analysis/Assess/Total>
## <2564/642/3206>

.metricsの中身は、モデルの分割データに対する評価情報が記録されている。この例だと、accuracyが0.79ということになる。

workflow_tuned %>% 
  pluck(".metrics", 1)
## # A tibble: 1 × 4
##   .metric  .estimator .estimate .config             
##   <chr>    <chr>          <dbl> <chr>               
## 1 accuracy binary         0.793 Preprocessor1_Model1

.predictionsの中身は、予測値.pred_class、行番号.row、観測値Status、何番目のフォールドのモデルなのか.configが記録されている。

workflow_tuned %>% 
  pluck(".predictions", 1)
## # A tibble: 642 × 4
##    .pred_class  .row Status .config             
##    <fct>       <int> <fct>  <chr>               
##  1 bad             2 bad    Preprocessor1_Model1
##  2 bad            11 bad    Preprocessor1_Model1
##  3 bad            14 bad    Preprocessor1_Model1
##  4 bad            18 bad    Preprocessor1_Model1
##  5 good           19 bad    Preprocessor1_Model1
##  6 good           24 bad    Preprocessor1_Model1
##  7 good           26 bad    Preprocessor1_Model1
##  8 good           30 bad    Preprocessor1_Model1
##  9 good           35 bad    Preprocessor1_Model1
## 10 bad            43 bad    Preprocessor1_Model1
## # … with 632 more rows

collect_metrics関数を使うことで、クロスバリデーションの結果を集計して表示してくれる。今回であれば、データを5つに分割しており、その各フォールドの結果がまとめられている。

workflow_tuned %>%
  collect_metrics()
## # A tibble: 1 × 6
##   .metric  .estimator  mean     n std_err .config             
##   <chr>    <chr>      <dbl> <int>   <dbl> <chr>               
## 1 accuracy binary     0.785     5 0.00556 Preprocessor1_Model1

今回はパラメタチューニングを行っていないが、ここではパラメタチューニングの結果からこのモデルが良いモデルとなったと仮定する。ワークフローを使って学習データを再学習し、モデルを構築する。そして、モデルの予測値を計算する。

model_trained_workflow2 <- 
  workflow2 %>% 
    fit(df_train)

model_predicted_workflow2 <- 
  model_trained_workflow2 %>% 
    predict(df_test)

テストの観測値とモデルの予測値を計算したいのであれば、下記の通り、yardstickパッケージのaccuracy関数に渡せば良い。df_testStatusの型が文字型なので、予測値の因子型にそろえてから評価している。

tibble(
  obs = factor(df_test$Status, c(levels(model_predicted_workflow2$.pred_class))),
  pred = model_predicted_workflow2$.pred_class
  ) %>% 
  yardstick::accuracy(truth = obs, estimate = pred)
## # A tibble: 1 × 3
##   .metric  .estimator .estimate
##   <chr>    <chr>          <dbl>
## 1 accuracy binary         0.791

下記はおまけ。最終的なモデルの変数重要度を可視化している。

library(vip)

imp_model <- model2 %>%
  finalize_model(select_best(workflow_tuned)) %>%
  set_engine("ranger", importance = "permutation")

workflow() %>%
  add_recipe(recipe2) %>%
  add_model(imp_model) %>%
  fit(df_train) %>%
  extract_fit_parsnip() %>%
  vip(aesthetics = list(alpha = 0.8, fill = "#006E4F")) + theme_bw()