Dropout

データ分析が好きです。マーケティングとか広告とか因果推論とか機械学習の解釈性に興味があります。

tidymodelsによるtidyな機械学習(その1:データ分割と前処理から学習と性能評価まで)

目次

※この記事をベースにした2019年12月7日に行われたJapan.R 2019での発表資料は以下になります。 tidymodelsによるtidyな機械学習 - Speaker Deck

はじめに

本記事ではtidymodelsを用いたtidyな機械学習フローを紹介したいと思います。

tidyverseはデータハンドリングと可視化のためのメタパッケージでしたが、tidymodelstydyverseにフィットするやり方で統計モデリング/機械学習をするためのメタパッケージになります。

tidymodels配下のパッケージは量が多く使い所が限られているパッケージも多いため、一度に全ては紹介できません。 ですので、今回は典型的な

  1. 訓練データとテストデータの分割
  2. 特徴量エンジニアリング
  3. モデルの学習
  4. モデルの精度評価

という機械学習フローを想定し、各パッケージの機能と連携をコンパクトにまとめたいと思います。

各パッケージの用途は以下になります。

  • rsample:訓練データ/テストデータの分割
  • recipe:特徴量エンジニアリング
  • parsnip:異なるパッケージのアルゴリズムを同じシンタックスで扱えるようにするラッパー
  • yardstic:モデルの精度評価

tidyな機械学習フロー

tidyversetidymodelsを読み込みます。

library(tidyverse)
library(tidymodels)

訓練データとテストデータの分割

まず第一に大元のデータを訓練データとテストデータに分割しましょう。

データはdiamondsを使って、ダイヤの価格を予測することにします。

set.seed(42)

df_split = initial_split(diamonds,  p = 0.8)

df_train = training(df_split)
df_test  = testing(df_split)

データ分割の役割はrsampleが担っています。

initial_split()で分割するデータと、訓練データが占める割合を指定します。

training()で訓練データを、testing()でテストデータを抽出できます。

特徴量エンジニアリング

次に特徴量エンジニアリングのためのレシピを作成します。作成したレシピを実際にデータに適用することで、処理済みのデータが作成されます。

どのような特徴量エンジニアリングが必要かは機械学習モデルに依存しますが、今回はRandom Forestを用いるための最低限の前処理を行います。

rec = recipe(price ~ carat + color + cut + clarity + depth, data = df_train) %>% 
  step_log(price) %>% 
  step_ordinalscore(all_nominal())

> rec
Data Recipe

Inputs:

      role #variables
   outcome          1
 predictor          9

Operations:

Log transformation on price
Scoring for all_nominal()

特徴量エンジニアリングのための関数はrecipeにまとめられています。

まずはrecipe()で用いる特徴量とデータを指定し、そこに処理をパイプで重ねていきます。

関数は全てstep_*()で統一されていて、今回用いたstep_log()は指定した変数の対数をとる関数で、step_ordinalscore()は順序のあるカテゴリカル変数を順序(1, 2, 3..)に変換します。 (tree系のモデルではなく線形モデルを用いる場合はstep_dummy()でダミー変数にできます。 *1

all_nominal()は全てのカテゴリカル変数を指定しています。他にもたとえばall_numeric()は全ての連続変数を指定できますし、starts_with()などのtidyverseではお馴染みの関数で指定することもできます。詳細は?selectionsをご確認下さい。

レシピができたので実際にデータに適用しましょう。

rec_preped = rec %>% 
  prep(df_train)

df_train_baked = rec_preped %>% 
  juice() 

> df_train_baked
# A tibble: 43,153 x 10
   carat   cut color clarity depth table     x     y     z price
   <dbl> <dbl> <dbl>   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
 1 0.23      5     2       2  61.5    55  3.95  3.98  2.43  5.79
 2 0.21      4     2       3  59.8    61  3.89  3.84  2.31  5.79
 3 0.23      2     2       5  56.9    65  4.05  4.07  2.31  5.79
 4 0.290     4     6       4  62.4    58  4.2   4.23  2.63  5.81
 5 0.31      2     7       2  63.3    58  4.34  4.35  2.75  5.81
 6 0.24      3     7       6  62.8    57  3.94  3.96  2.48  5.82
 7 0.24      3     6       7  62.3    57  3.95  3.98  2.47  5.82
 8 0.26      3     5       3  61.9    55  4.07  4.11  2.53  5.82
 9 0.22      1     2       4  65.1    61  3.87  3.78  2.49  5.82
10 0.23      3     5       5  59.4    61  4     4.05  2.39  5.82
# ... with 43,143 more rows

prep()に訓練データを指定し、訓練データの変換を行います。

さらにjuice()で変換されたデータを抽出することができます。

priceに対数がとられてカテゴリカル変数が(cut, color, clarity)が順序になっているのがわかるかと思います。

次に同じレシピを用いてテストデータも変換します。

df_test_beked = rec_preped %>% 
  bake(df_test)

> df_test_baked
# A tibble: 10,787 x 10
   carat   cut color clarity depth table price     x     y     z
   <dbl> <dbl> <dbl>   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
 1  0.22     4     3       3  60.4    61  5.83  3.88  3.84  2.33
 2  0.3      3     7       3  62.7    59  5.86  4.21  4.27  2.66
 3  0.23     3     2       4  63.8    55  5.86  3.85  3.92  2.48
 4  0.23     3     1       4  60.5    61  5.88  3.96  3.97  2.4 
 5  0.23     3     2       5  59.5    58  6.00  4.01  4.06  2.4 
 6  0.23     2     3       5  58.2    59  6.00  4.06  4.08  2.37
 7  0.33     5     6       2  61.8    55  6.00  4.49  4.51  2.78
 8  0.33     5     7       3  61.1    56  6.00  4.49  4.55  2.76
 9  0.3      3     6       3  62.6    57  6.00  4.25  4.28  2.67
10  0.3      4     1       3  62.6    59  6.31  4.23  4.27  2.66
# ... with 10,777 more rows

レシピを新しいデータに適用する場合はbake()を使います*2

モデルの学習

recipeで訓練データ/テストデータをモデルに投入できる形に変換できたので、次はparsnipで学習モデルを定義しましょう。

Rで機械学習を行う際、学習アルゴリズムごとにパッケージが分かれていて、それ故シンタックスが統一されておらず、各々のシンタックスを覚えるのが大変でした(たとえば入力はformulaなのかmatrixなのかなど)。

これを統一的なシンタックスで扱えるようにするメタパッケージとして、たとえばcaretがありましたが、最近よりtidyverseに馴染むパッケージとしてparsnipが開発されました*3。 ちなみに開発者はcaretと同じくMax Kuhnです。

rf = rand_forest(mode = "regression", 
                 trees = 50,
                 min_n = 10,
                 mtry = 3) %>% 
  set_engine("ranger", num.threads = parallel::detectCores(), seed = 42)

> rf
Random Forest Model Specification (regression)

Main Arguments:
  mtry = 3
  trees = 50
  min_n = 10

Engine-Specific Arguments:
  num.threads = parallel::detectCores()
  seed = 42

Computational engine: ranger 

rand_forest()は学習アルゴリズムとしてRandom Forestを使用します。今回は回帰問題なのでmode=regressionとしています。 rand_forest()で指定できるハイパーパラメータは3つあり、それぞれ

  • trees:作成する決定木の数。
  • min_n:ノードに最低限含まれるサンプル数。
  • mtry:各ノードで分割に用いる特徴量の数。

となります。今回は適当に指定しました。

さらにset_engine()でRandom Forestに用いるパッケージを指定でき、ここではranger を指定しています。 set_engine()内でranger固有のパラメータを指定でき、コア数とシードを指定しました。

モデルが定義できたので、学習を行います。これはfit()にモデル式と入力データを指定するだけで完結します。

fitted = rf %>% 
  fit(price ~ ., data = df_train_baked)

> fitted
parsnip model object

Ranger result

Call:
 ranger::ranger(formula = formula, data = data, mtry = ~3, num.trees = ~50,      min.node.size = ~10, num.threads = ~parallel::detectCores(),      seed = ~42, verbose = FALSE) 

Type:                             Regression 
Number of trees:                  50 
Sample size:                      43153 
Number of independent variables:  9 
Mtry:                             3 
Target node size:                 10 
Variable importance mode:         none 
Splitrule:                        variance 
OOB prediction error (MSE):       0.009112244 
R squared (OOB):                  0.9911622 

モデルの精度評価

学習が済んだので、最後に予測精度を評価しましょう。 テストデータへの予測はpredict() で行います。

df_result = df_test_baked %>% 
  select(price) %>% 
  bind_cols(predict(fitted, df_test_baked))
  
> df_result
# A tibble: 10,787 x 2
   price .pred
   <dbl> <dbl>
 1  5.83  5.95
 2  5.86  5.98
 3  5.86  5.97
 4  5.88  5.97
 5  6.00  6.00
 6  6.00  5.99
 7  6.00  6.14
 8  6.00  6.14
 9  6.00  6.03
10  6.31  6.33
# ... with 10,777 more rows

予測結果に対してyardsticの評価指標関数を適用することでモデルの性能が評価できます。 yardsticには様々な評価指標関数が含まれており、個別で指定することもできますが、matrics()は回帰モデルの結果に対してRMSE、MAE、R2をまとめて計算してくれます。

> df_result %>% 
+   metrics(price, .pred)
# A tibble: 3 x 3
  .metric .estimator .estimate
  <chr>   <chr>          <dbl>
1 rmse    standard      0.0910
2 rsq     standard      0.992 
3 mae     standard      0.0671

まとめ

本記事ではtidymodelsによる

  1. rsampleで訓練データ/テストデータの分割し、
  2. recipeで特徴量エンジニアリングを行い、
  3. parsnipでモデルを定義・学習し、
  4. yardsticで性能を評価

する、tidyな機械学習フローを紹介しました。

各パッケージにはまだまだ多くの機能があり、今回紹介できたのは最低限の機能のみです。

次回はtidymodelsで交差検証を行い、ハイパーパラメータをチューニングする方法をご紹介できればと思っています。

参考文献

*1: recipeの他のstep関数やselectorは以下をご確認下さい。Function reference • recipes

*2:今回tree系のモデルなので標準化はやっていませんが、たとえば標準化を行う場合は訓練データの平均と分散を用いてテストデータの標準化が行われます。

*3:parsnipが対応しているパッケージは以下を御覧ください:List of Models • parsnip