※基礎的な集計の段階は終了しているという前提で話を進めていく
使用したパッケージの一覧
データの分割(訓練/テスト)
データを訓練用とテスト用に分割する。
strata 引数に目的変数を指定する事で目的変数の分布を変えずにデータを分割する事が可能。
library(tidyverse) library(tidymodels) # 元データを訓練用とテスト用に分割 # strata を指定する事でクラスの分布を保持したまま分割 lst.splitted <- rsample::initial_split(iris, prop = 0.75, strata = "Species") %>% { list( train = rsample::analysis(.), test = rsample::assessment(.) ) }
クロスバリデーション用データの分割
クロスバリデーション用にデータを分割する。
strata に目的変数を指定しているのは前述した rsample::initial_split と同じ理由。
# クロスバリデーション用の分割を定義 df.cv <- rsample::vfold_cv( lst.splitted$train, v = 5, strata = "Species" )
前処理の定義
事前に実施した基礎集計の結果やドメイン知識を元にデータの前処理を行う。
いわゆる特徴量エンジニアリングを行う箇所。
※ここで定義している処理はサンプルとして適当に行っているものであり、実施によって精度は下がっていると思われるw
# 前処理レシピの定義 recipe <- recipes::recipe(Species ~ ., lst.splitted$train) %>% # Sepal.Width を削除 recipes::step_rm(Sepal.Width) %>% # Sepal.Length を対数変換 recipes::step_log(Sepal.Length) %>% # 説明変数を基準化 recipes::step_center(all_predictors()) %>% recipes::step_scale(all_predictors())
モデルの定義
今回は RandomForest でモデルを定義する。
各ハイパーパラメータに parsnip::varying を指定する事で後から複数の値を設定可能。今回はグリッドサーチでパラメータを指定する(後述)。
parsnip::set_engine では指定した engine パッケージ特有のパラメータをセットする事ができる。
下記の例では ranger::ranger 関数の num.threads パラメータ(並列処理に用いるコア数)を指定している。
# モデルの定義 model <- parsnip::rand_forest( mode = "classification", mtry = parsnip::varying(), min_n = parsnip::varying(), trees = parsnip::varying() ) %>% parsnip::set_engine(engine = "ranger", num.threads = 4)
グリッドサーチの定義
3 x 3 x 3 = 27 パターンのハイパーパラメータの組み合わせを定義。
今回はグリッドサーチを行っているが dials::grid_random を用いてランダムサーチを行う事も可能。
# グリッドサーチ用の組み合わせパターンを定義 df.grid.params <- dials::grid_regular( dials::mtry %>% dials::range_set(c(1, 3)), dials::min_n %>% dials::range_set(c(2, 6)), dials::trees %>% dials::range_set(c(500, 1500)), levels = 3 )
モデルの学習と評価
処理の構造としては下記の 2 重ループとなっている。
- ハイパーパラメータ一覧のループ
- クロスバリデーションによる分割のループ
- モデルの学習
- 学習済モデルによる予測
- モデルの評価
- CV 毎の評価スコアを平均してハイパーパラメータ毎の評価スコアを算出
上記によって得られる結果を評価スコアでソートする事で最も性能の良いハイパーパラメータを特定する。
- recipes::prep
- recipes::bake
- parsnip::fit
- yardstick::metric_set
- yardstick::accuracy
- yardstick::precision
- yardstick::recall
- yardstick::f_meas
df.results <- # ハイパーパラメータをモデルに適用 # ※2020.05 時点で下記のコードは動かないmerge(df.grid.params, model) %>%# 下記のコードに改修(2020.05.19) purrr::pmap(df.grid.params, set_args, object = model) %>% # ハイパーパラメータの組み合わせごとにループ purrr::map(function(model.applied) { # クロスバリデーションの分割ごとにループ purrr::map(df.cv$splits, model = model.applied, function(df.split, model) { # 前処理済データの作成 df.train <- recipe %>% recipes::prep() %>% recipes::bake(rsample::analysis(df.split)) df.test <- recipe %>% recipes::prep() %>% recipes::bake(rsample::assessment(df.split)) model %>% # モデルの学習 { model <- (.) parsnip::fit(model, Species ~ ., df.train) } %>% # 学習済モデルによる予測 { fit <- (.) list( train = predict(fit, df.train, type = "class")[[1]], test = predict(fit, df.test, type = "class")[[1]] ) } %>% # 評価 { lst.predicted <- (.) # 評価指標の一覧を定義 metrics <- yardstick::metric_set( yardstick::accuracy, yardstick::precision, yardstick::recall, yardstick::f_meas ) # train データでモデルを評価 df.result.train <- df.train %>% dplyr::mutate( predicted = lst.predicted$train ) %>% metrics(truth = Species, estimate = predicted) %>% dplyr::select(-.estimator) %>% dplyr::mutate( .metric = stringr::str_c("train", .metric, sep = "_") ) %>% tidyr::spread(key = .metric, value = .estimate) # test データでモデルを評価 df.result.test <- df.test %>% dplyr::mutate( predicted = lst.predicted$test ) %>% metrics(truth = Species, estimate = predicted) %>% dplyr::select(-.estimator) %>% dplyr::mutate( .metric = stringr::str_c("test", .metric, sep = "_") ) %>% tidyr::spread(key = .metric, value = .estimate) dplyr::bind_cols( df.result.train, df.result.test ) } }) %>% # CV 分割全体の平均値を評価スコアとする purrr::reduce(dplyr::bind_rows) %>% dplyr::summarise_all(mean) }) %>% # 評価結果とパラメータを結合 purrr::reduce(dplyr::bind_rows) %>% dplyr::bind_cols(df.grid.params) %>% # 評価スコアの順にソート(昇順) dplyr::arrange( desc(test_accuracy) ) %>% dplyr::select( mtry, min_n, trees, train_accuracy, train_precision, train_recall, train_f_meas, test_accuracy, test_precision, test_recall, test_f_meas )
上記で得られる df.results のサンプル。
mtry | min_n | trees | train_accuracy | train_precision | train_recall | train_f_meas | test_accuracy | test_precision | test_recall | test_f_meas |
---|---|---|---|---|---|---|---|---|---|---|
2 | 2 | 500 | 1.000 | 1.000 | 1.000 | 1.000 | 0.946 | 0.949 | 0.946 | 0.946 |
2 | 4 | 500 | 0.991 | 0.991 | 0.991 | 0.991 | 0.946 | 0.949 | 0.946 | 0.946 |
3 | 4 | 500 | 0.987 | 0.987 | 0.987 | 0.987 | 0.946 | 0.949 | 0.946 | 0.946 |
2 | 6 | 500 | 0.985 | 0.985 | 0.985 | 0.985 | 0.946 | 0.949 | 0.946 | 0.946 |
3 | 6 | 500 | 0.982 | 0.983 | 0.982 | 0.982 | 0.946 | 0.949 | 0.946 | 0.946 |
ベストモデルの構築
最も性能の良い(=評価スコア最大)モデルを構築する。
この段階では訓練データの全体を用いて学習を行う事に注意。
# 訓練データに前処理を適用 df.train.baked <- recipe %>% recipes::prep() %>% recipes::bake(lst.splitted$train) # 最も性能の良いハイパーパラメータを用いたモデルを構築 best_model <- update( model, mtry = df.results[1,]$mtry, min_n = df.results[1,]$min_n, trees = df.results[1,]$trees ) %>% # 訓練データ全体を用いてモデルの学習を行う parsnip::fit(Species ~ ., df.train.baked)
テストデータによる検証
テストデータを用いた評価を行い、構築したモデルの汎化性能を検証する。
# テストデータに前処理を適用 df.test.baked <- recipe %>% recipes::prep() %>% recipes::bake(lst.splitted$test) # 汎化性能を検証 df.test.baked %>% # ベストモデルを用いて予測 dplyr::mutate( predicted = predict(best_model, df.test.baked)[[1]] ) %>% # 精度(Accuracy)を算出 yardstick::accuracy(Species, predicted)
まとめ
必要になる度に前に書いたコードを思い出したりコピペしたりでノウハウをまとめられていなかったので、今回は良い機会になった。
機械学習周りは scikit-learn 一択かなと思ってた頃もあったけど tidymodels がいい感じなのでぜひ使う人が増えてくれると嬉し。
他の人がどんな感じでやってるのかも知りたいところ。