Section 8 リサンプリング

一般的に学習機の性能評価はリサンプリングを通じて行われる。リサンプリングの概要は次のようなものである。まず、データセット全体を\(D\)として、これを訓練セット\(D^{*b}\)とテストセット\(D\setminus D^{*b}\)に分割する。この種の分割を\(B\)回行う(つまり、\(b = 1,...,B\)とする)。そして、それぞれのテストセット、訓練セットの対を用いて訓練と予測を行い、性能指標\(S(D^{*b}, D\setminus D^{*b}\))を計算する。これにより\(B\)個の性能指標が得られるが、これを集約する(一般的には平均値が用いられる)。リサンプリングの方法には、クロスバリデーションやブートストラップなど様々な手法が存在する。

もしさらに詳しく知りたいのであれば、Simonによる論文(Resampling Strategies for Model Assessment and Selection | SpringerLink)を読むのは悪い選択ではないだろう。また、Berndらによる論文、Resampling methods for meta-model validation with recommendations for evolutionary computationでは、リサンプリング手法の統計的な背景に対して多くの説明がなされている。

8.1 リサンプリング手法を決める

mlrではmakeResampleDesc関数を使ってリサンプリング手法を設定する。この関数にはリサンプリング手法の名前とともに、手法に応じてその他の情報(例えば繰り返し数など)を指定する。サポートしているサンプリング手法は以下のとおりである。

  • CV: クロスバリデーション(Cross-varidation)
  • LOO: 一つ抜き法(Leave-one-out cross-varidation)
  • RepCV: Repeatedクロスバリデーション(Repeated cross-varidation)
  • Bootstrap: out-of-bagブートストラップとそのバリエーション(b632等)
  • Subsample: サブサンプリング(モンテカルロクロスバリデーションとも呼ばれる)
  • Holdout: ホールドアウト法

3-fold(3分割)クロスバリデーションの場合は

rdesc = makeResampleDesc("CV", iters = 3)
rdesc
$> Resample description: cross-validation with 3 iterations.
$> Predict: test
$> Stratification: FALSE

ホールドアウト法の場合は

rdesc = makeResampleDesc("Holdout")
rdesc
$> Resample description: holdout with 0.67 split rate.
$> Predict: test
$> Stratification: FALSE

という具合だ。

これらのリサンプルdescriptionのうち、よく使うものは予め別名が用意してある。例えばホールドアウト法はhout、クロスバリデーションはcv5cv10などよく使う分割数に対して定義してある。

hout
$> Resample description: holdout with 0.67 split rate.
$> Predict: test
$> Stratification: FALSE
cv3
$> Resample description: cross-validation with 3 iterations.
$> Predict: test
$> Stratification: FALSE

8.2 リサンプリングを実行する

resample関数は指定されたリサンプリング手法により、学習機をタスク上で評価する。

最初の例として、BostonHousingデータに対する線形回帰を3分割クロスバリデーションで評価してみよう。

\(K\)分割クロスバリデーションはデータセット\(D\)\(K\)個の(ほぼ)等しいサイズのサブセットに分割する。\(K\)回の繰り返しの\(b\)番目では、\(b\)番目のサブセットがテストに、残りが訓練に使用される。

resample関数に学習器を指定する際には、Learnerクラスのオブジェクトか学習器の名前(regr.lmなど)のいずれを渡しても良い。性能指標は指定しなければ学習器に応じたデフォルトが使用される(回帰の場合は平均二乗誤差)。

rdesc = makeResampleDesc("CV", iters = 3)

r = resample("regr.lm", bh.task, rdesc)
$> [Resample] cross-validation iter 1: mse.test.mean=20.7
$> [Resample] cross-validation iter 2: mse.test.mean=  25
$> [Resample] cross-validation iter 3: mse.test.mean=25.3
$> [Resample] Aggr. Result: mse.test.mean=23.7
r
$> Resample Result
$> Task: BostonHousing-example
$> Learner: regr.lm
$> Aggr perf: mse.test.mean=23.7
$> Runtime: 0.034143

ここでrに格納したオブジェクトはResampleResultクラスである。この中には評価結果の他に、実行時間や予測値、リサンプリング毎のフィット済みモデルなどが格納されている。

## 中身をざっと確認
names(r)
$>  [1] "learner.id"     "task.id"        "task.desc"      "measures.train"
$>  [5] "measures.test"  "aggr"           "pred"           "models"        
$>  [9] "err.msgs"       "err.dumps"      "extract"        "runtime"

r$measures.testには各テストセットの性能指標が入っている。

## 各テストセットの性能指標
r$measures.test
$>   iter      mse
$> 1    1 20.74068
$> 2    2 25.00755
$> 3    3 25.25456

r$aggrには集約(aggrigate)後の性能指標が入っている。

## 集約後の性能指標
r$aggr
$> mse.test.mean 
$>       23.6676

名前mse.test.meanは、性能指標がmseであり、test.meanによりデータが集約されていることを表している。test.meanは多くの性能指標においてデフォルトの集約方法であり、その名前が示すようにテストデータの性能指標の平均値である。

mlrではどのような種類の学習器も同じようにリサンプリングを行える。以下では、分類問題の例としてSonarデータセットに対する分類木を5反復のサブサンプリングで評価してみよう。

サブサンプリングの各繰り返しでは、データセット\(D\)はランダムに訓練データとテストデータに分割される。このとき、テストデータには指定の割合のデータ数が割り当てられる。この反復が1の場合はホールドアウト法と同じである。

評価したい性能指標はリストとしてまとめて指定することもできる。以下の例では平均誤分類、偽陽性・偽陰性率、訓練時間を指定している。

rdesc = makeResampleDesc("Subsample", iter = 5, split = 4/5)
lrn = makeLearner("classif.rpart", parms = list(split = "information"))
r = resample(lrn, sonar.task, rdesc, measures = list(mmce, fpr, fnr, timetrain))
$> [Resample] subsampling iter 1: mmce.test.mean=0.19,fpr.test.mean=0.263,fnr.test.mean=0.13,timetrain.test.mean=0.015
$> [Resample] subsampling iter 2: mmce.test.mean=0.286,fpr.test.mean= 0.2,fnr.test.mean=0.333,timetrain.test.mean=0.016
$> [Resample] subsampling iter 3: mmce.test.mean=0.167,fpr.test.mean=0.263,fnr.test.mean=0.087,timetrain.test.mean=0.013
$> [Resample] subsampling iter 4: mmce.test.mean=0.286,fpr.test.mean= 0.4,fnr.test.mean=0.182,timetrain.test.mean=0.017
$> [Resample] subsampling iter 5: mmce.test.mean=0.238,fpr.test.mean=0.278,fnr.test.mean=0.208,timetrain.test.mean=0.013
$> [Resample] Aggr. Result: mmce.test.mean=0.233,fpr.test.mean=0.281,fnr.test.mean=0.188,timetrain.test.mean=0.0148
r
$> Resample Result
$> Task: Sonar-example
$> Learner: classif.rpart
$> Aggr perf: mmce.test.mean=0.233,fpr.test.mean=0.281,fnr.test.mean=0.188,timetrain.test.mean=0.0148
$> Runtime: 0.136948

もし指標を後から追加したくなったら、addRRMeasure関数を使うと良い。

addRRMeasure(r, list(ber, timepredict))
$> Resample Result
$> Task: Sonar-example
$> Learner: classif.rpart
$> Aggr perf: mmce.test.mean=0.233,fpr.test.mean=0.281,fnr.test.mean=0.188,timetrain.test.mean=0.0148,ber.test.mean=0.234,timepredict.test.mean=0.005
$> Runtime: 0.136948

デフォルトではresample関数は進捗と途中結果を表示するが、show.info=FALSEで非表示にもできる。このようなメッセージを完全に制御したかったら、Configuration - mlr tutorialを確認してもらいたい。

上記例では学習器を明示的に作成してからresampleに渡したが、代わりに学習器の名前を指定しても良い。その場合、学習器のパラメータは...引数を通じて渡すことができる。

resample("classif.rpart", parms = list(split = "information"), sonar.task, rdesc,
         measures = list(mmce, fpr, fnr, timetrain), show.info = FALSE)
$> Resample Result
$> Task: Sonar-example
$> Learner: classif.rpart
$> Aggr perf: mmce.test.mean=0.262,fpr.test.mean=0.256,fnr.test.mean=0.273,timetrain.test.mean=0.0148
$> Runtime: 0.135859

8.3 リサンプル結果へのアクセス

学習器の性能以外にも、リサンプル結果から様々な情報を得ることが出来る。例えばリサンプリングの各繰り返しに対応する予測値やフィット済みモデル等だ。以下で情報の取得の仕方をみていこう。

8.3.1 予測値

デフォルトでは、ResampleResultはリサンプリングで得た予測値を含んでいる。メモリ節約などの目的でこれを止めさせたければ、resample関数にkeep.pred = FALSEを指定する。

予測値は$predスロットに格納されている。また、getRRPredictions関数を使ってアクセスすることもできる。

r$pred
$> Resampled Prediction for:
$> Resample description: subsampling with 5 iterations and 0.80 split rate.
$> Predict: test
$> Stratification: FALSE
$> predict.type: response
$> threshold: 
$> time (mean): 0.01
$>    id truth response iter  set
$> 1 194     M        M    1 test
$> 2  59     R        R    1 test
$> 3 113     M        R    1 test
$> 4 191     M        M    1 test
$> 5  32     R        M    1 test
$> 6 170     M        M    1 test
$> ... (210 rows, 5 cols)
pred = getRRPredictions(r)
pred
$> Resampled Prediction for:
$> Resample description: subsampling with 5 iterations and 0.80 split rate.
$> Predict: test
$> Stratification: FALSE
$> predict.type: response
$> threshold: 
$> time (mean): 0.01
$>    id truth response iter  set
$> 1 194     M        M    1 test
$> 2  59     R        R    1 test
$> 3 113     M        R    1 test
$> 4 191     M        M    1 test
$> 5  32     R        M    1 test
$> 6 170     M        M    1 test
$> ... (210 rows, 5 cols)

ここで作成したpredResamplePredictionクラスのオブジェクトである。これはPredictionオブジェクトのように$dataにデータフレームとして予測値と真値(教師あり学習の場合)が格納されている。as.data.frameを使って直接$dataスロットの中身を取得できる。さらに、Predictionオブジェクトに対するゲッター関数は全て利用可能である。

head(as.data.frame(pred))
$>    id truth response iter  set
$> 1 194     M        M    1 test
$> 2  59     R        R    1 test
$> 3 113     M        R    1 test
$> 4 191     M        M    1 test
$> 5  32     R        M    1 test
$> 6 170     M        M    1 test
head(getPredictionTruth(pred))
$> [1] M R M M R M
$> Levels: M R

データフレームのitersetは繰り返し回数とデータセットの種類(訓練なのかテストなのか)を示している。

デフォルトでは予測はテストセットだけに行われるが、makeResampleDescに対し、predict = "train"を指定で訓練セットだけに、predict = "both"を指定で訓練セットとテストセットの両方に予測を行うことが出来る。後で例を見るが、b632b632+のようなブートストラップ手法ではこれらの設定が必要となる。

以下では単純なホールドアウト法の例を見よう。つまり、テストセットと訓練セットへの分割は一度だけ行い、予測は両方のデータセットを用いて行う。

rdesc = makeResampleDesc("Holdout", predict = "both")

r = resample("classif.lda", iris.task, rdesc, show.info = FALSE)
r
$> Resample Result
$> Task: iris-example
$> Learner: classif.lda
$> Aggr perf: mmce.test.mean=0.02
$> Runtime: 0.0158811
r$aggr
$> mmce.test.mean 
$>           0.02

(predict="both"の指定にかかわらず、r$aggrではテストデータに対するmmceしか計算しないことに注意してもらいたい。訓練セットに対して計算する方法はこの後で説明する。)

リサンプリング結果から予測を取り出す方法として、getRRPredictionListを使う方法もある。これは、分割されたデータセット(訓練/テスト)それぞれと、リサンプリングの繰り返し毎に分割した単位でまとめた予測結果のリストを返す。

getRRPredictionList(r)
$> $train
$> $train$`1`
$> Prediction: 100 observations
$> predict.type: response
$> threshold: 
$> time: 0.00
$>      id      truth   response
$> 2     2     setosa     setosa
$> 118 118  virginica  virginica
$> 65   65 versicolor versicolor
$> 42   42     setosa     setosa
$> 124 124  virginica  virginica
$> 34   34     setosa     setosa
$> ... (100 rows, 3 cols)
$> 
$> 
$> 
$> $test
$> $test$`1`
$> Prediction: 50 observations
$> predict.type: response
$> threshold: 
$> time: 0.00
$>      id      truth   response
$> 91   91 versicolor versicolor
$> 78   78 versicolor versicolor
$> 7     7     setosa     setosa
$> 146 146  virginica  virginica
$> 139 139  virginica  virginica
$> 9     9     setosa     setosa
$> ... (50 rows, 3 cols)

8.3.2 訓練済みモデルの抽出

リサンプリング毎に学習器は訓練セットにフィットさせられる。標準では、WrappedModelResampleResultオブジェクトには含まれておらず、$modelsスロットは空だ。これを保持するためには、resample関数を呼び出す際に引数models = TRUEを指定する必要がある。以下に生存時間分析の例を見よう。

## 3分割クロスバリデーション
rdesc = makeResampleDesc("CV", iters = 3)

r = resample("surv.coxph", lung.task, rdesc, show.info = FALSE, models = TRUE)
r$models
$> [[1]]
$> Model for learner.id=surv.coxph; learner.class=surv.coxph
$> Trained on: task.id = lung-example; obs = 111; features = 8
$> Hyperparameters: 
$> 
$> [[2]]
$> Model for learner.id=surv.coxph; learner.class=surv.coxph
$> Trained on: task.id = lung-example; obs = 111; features = 8
$> Hyperparameters: 
$> 
$> [[3]]
$> Model for learner.id=surv.coxph; learner.class=surv.coxph
$> Trained on: task.id = lung-example; obs = 112; features = 8
$> Hyperparameters:

8.3.3 他の抽出方法

完全なフィット済みモデルを保持しようとすると、リサンプリングの繰り返し数が多かったりオブジェクトが大きかったりする場合にメモリの消費量が大きくなってしまう。モデルの全ての情報を保持する代わりに、resample関数のextract引数に指定することで必要な情報だけを保持することができる。引数extractに対しては、リサンプリング毎の各WrapedModelオブジェクトに適用するための関数を渡す必要がある。

以下では、mtcarsデータセットをk=3のk-meansでクラスタリングし、クラスター中心だけを保持する例を紹介する。

rdesc = makeResampleDesc("CV", iter = 3)

r = resample("cluster.kmeans", mtcars.task, rdesc, show.info = FALSE,
             centers = 3, extract = function(x){getLearnerModel(x)$centers})
r$extract
$> [[1]]
$>        mpg      cyl     disp       hp     drat       wt     qsec        vs
$> 1 24.33333 4.666667 119.9111 105.4444 3.972222 2.388667 17.98556 0.6666667
$> 2 17.31667 7.333333 271.4000 150.8333 2.968333 3.629167 18.25500 0.3333333
$> 3 14.53333 8.000000 386.8333 229.0000 3.423333 4.131500 16.33500 0.0000000
$>          am     gear     carb
$> 1 0.7777778 4.222222 2.555556
$> 2 0.0000000 3.000000 2.166667
$> 3 0.1666667 3.333333 3.666667
$> 
$> [[2]]
$>        mpg      cyl     disp        hp     drat       wt     qsec   vs
$> 1 24.69167 4.666667 121.1333  93.83333 4.018333 2.560833 18.68167 0.75
$> 2 14.76667 8.000000 437.3333 203.33333 3.080000 4.813333 17.48333 0.00
$> 3 15.35714 8.000000 328.8286 227.71429 3.438571 3.543571 16.09571 0.00
$>          am     gear     carb
$> 1 0.7500000 4.083333 2.500000
$> 2 0.0000000 3.000000 3.333333
$> 3 0.2857143 3.571429 3.857143
$> 
$> [[3]]
$>       mpg cyl    disp      hp    drat       wt     qsec    vs    am  gear
$> 1 19.5000   6 195.640 114.200 3.51600 3.286000 18.77600 0.800 0.200 3.600
$> 2 14.9250   8 350.825 198.750 3.07500 4.105500 17.07750 0.000 0.125 3.250
$> 3 26.3375   4 110.675  83.625 4.04625 2.324125 19.13875 0.875 0.625 4.125
$>    carb
$> 1 2.800
$> 2 3.500
$> 3 1.625

他の例として、フィット済みの回帰木から変数の重要度をgetFeatureImportanceを使って抽出してみよう(より詳しい内容はFeature Selection - mlr tutorialを確認してもらいたい)。

r = resample("regr.rpart", bh.task, rdesc, show.info = FALSE, extract = getFeatureImportance)
r$extract
$> [[1]]
$> FeatureImportance:
$> Task: BostonHousing-example
$> 
$> Learner: regr.rpart
$> Measure: NA
$> Contrast: NA
$> Aggregation: function (x)  x
$> Replace: NA
$> Number of Monte-Carlo iterations: NA
$> Local: FALSE
$>       crim       zn    indus chas      nox       rm      age      dis
$> 1 3399.046 1192.183 4051.922    0 2303.742 15941.78 2269.408 2636.903
$>        rad      tax  ptratio b    lstat
$> 1 830.9189 1045.856 2626.595 0 11415.22
$> 
$> [[2]]
$> FeatureImportance:
$> Task: BostonHousing-example
$> 
$> Learner: regr.rpart
$> Measure: NA
$> Contrast: NA
$> Aggregation: function (x)  x
$> Replace: NA
$> Number of Monte-Carlo iterations: NA
$> Local: FALSE
$>      crim       zn    indus     chas      nox       rm      age      dis
$> 1 2122.88 579.2956 4377.805 188.1338 3020.542 14975.56 3097.911 3183.877
$>        rad      tax  ptratio b    lstat
$> 1 657.2457 2952.654 2856.316 0 10148.19
$> 
$> [[3]]
$> FeatureImportance:
$> Task: BostonHousing-example
$> 
$> Learner: regr.rpart
$> Measure: NA
$> Contrast: NA
$> Aggregation: function (x)  x
$> Replace: NA
$> Number of Monte-Carlo iterations: NA
$> Local: FALSE
$>       crim      zn    indus     chas      nox       rm      age     dis
$> 1 2588.126 1981.96 3616.241 365.7574 3703.958 16503.61 3212.413 4428.26
$>        rad      tax  ptratio        b    lstat
$> 1 639.1926 2930.041 2398.208 747.6475 11787.72

8.4 階層化とブロック化

  • カテゴリー変数に対する階層化とは、訓練セットとテストセット内で各値の比率が変わらないようにすることを指す。階層化が可能なのは目的変数がカテゴリーである場合(教師あり学習における分類や生存時間分析)や、説明変数がカテゴリーである場合に限られる。
  • ブロック化とは、観測値の一部分をブロックとして扱い、リサンプリングの間にブロックが分割されないように扱うことを指す。つまり、ブロック全体は訓練セットかテストセットのいずれかにまとまって属すことになる。

8.4.1 目的変数の階層化

分類においては、元のデータと同じ比率で各クラスの値が含まれていることが望ましい。これはクラス間の観測数が不均衡であったり、データセットの大きさが小さい場合に有効である。さもなければ、観測数が少ないクラスのデータが訓練セットに含まれないということが起こりうる。これは分類性能の低下やモデルのクラッシュにつながる。階層化リサンプリングを行うためには、makeResampleDesc実行時にstratify = TRUEを指定する。

rdesc = makeResampleDesc("CV", iters = 3, stratify = TRUE)

r = resample("classif.lda", iris.task, rdesc, show.info = FALSE)
r
$> Resample Result
$> Task: iris-example
$> Learner: classif.lda
$> Aggr perf: mmce.test.mean=0.0199
$> Runtime: 0.0313618

階層化を生存時間分析に対して行う場合は、打ち切りの割合が制御される。

8.4.2 説明変数の階層化

説明変数の階層化が必要な場合もある。この場合は、stratify.cols引数に対して階層化したい因子型変数を指定する。

rdesc = makeResampleDesc("CV", iter = 3, stratify.cols = "chas")

r = resample("regr.rpart", bh.task, rdesc, show.info = FALSE)
r
$> Resample Result
$> Task: BostonHousing-example
$> Learner: regr.rpart
$> Aggr perf: mse.test.mean=  23
$> Runtime: 0.045078

8.4.3 ブロック化

いくつかの観測値が互いに関連しており、これらが訓練データとテストデータに分割されるのが望ましくない場合には、タスク作成時にその情報をblocking引数に因子型ベクトルを与えることで指定する。

## それぞれ30の観測値からなる5つのブロックを指定する例
task = makeClassifTask(data = iris, target = "Species", blocking = factor(rep(1:5, each = 30)))
task
$> Supervised task: iris
$> Type: classif
$> Target: Species
$> Observations: 150
$> Features:
$> numerics  factors  ordered 
$>        4        0        0 
$> Missings: FALSE
$> Has weights: FALSE
$> Has blocking: TRUE
$> Classes: 3
$>     setosa versicolor  virginica 
$>         50         50         50 
$> Positive class: NA

8.5 リサンプリングの詳細とリサンプルのインスタンス

既に説明したように、リサンプリング手法はmakeResampleDesc関数を使って指定する。

rdesc = makeResampleDesc("CV", iter = 3)
rdesc
$> Resample description: cross-validation with 3 iterations.
$> Predict: test
$> Stratification: FALSE
str(rdesc)
$> List of 4
$>  $ id      : chr "cross-validation"
$>  $ iters   : int 3
$>  $ predict : chr "test"
$>  $ stratify: logi FALSE
$>  - attr(*, "class")= chr [1:2] "CVDesc" "ResampleDesc"

上記rdescResampleDescクラス(resample descriptionの略)を継承しており、原則として、リサンプリング手法に関する必要な情報(繰り返し数、訓練セットとテストセットの比率、階層化したい変数など)を全て含んでいる。

makeResampleInstance関数は、データセットに含まれるデータ数を直接指定するか、タスクを指定することで、ResampleDescに従って訓練セットとテストセットの概要を生成する。

## taskに基づくリサンプルインスタンスの生成
rin = makeResampleInstance(rdesc, iris.task)
rin
$> Resample instance for 150 cases.
$> Resample description: cross-validation with 3 iterations.
$> Predict: test
$> Stratification: FALSE
str(rin)
$> List of 5
$>  $ desc      :List of 4
$>   ..$ id      : chr "cross-validation"
$>   ..$ iters   : int 3
$>   ..$ predict : chr "test"
$>   ..$ stratify: logi FALSE
$>   ..- attr(*, "class")= chr [1:2] "CVDesc" "ResampleDesc"
$>  $ size      : int 150
$>  $ train.inds:List of 3
$>   ..$ : int [1:100] 104 35 40 114 123 13 9 31 146 16 ...
$>   ..$ : int [1:100] 104 55 35 114 13 8 17 97 78 111 ...
$>   ..$ : int [1:100] 55 40 123 9 8 31 146 16 17 128 ...
$>  $ test.inds :List of 3
$>   ..$ : int [1:50] 6 8 12 15 17 24 26 29 33 36 ...
$>   ..$ : int [1:50] 2 4 9 14 16 18 20 22 23 31 ...
$>   ..$ : int [1:50] 1 3 5 7 10 11 13 19 21 25 ...
$>  $ group     : Factor w/ 0 levels: 
$>  - attr(*, "class")= chr "ResampleInstance"
## データセットのサイズを指定してリサンプルインスタンスを生成する例
rin = makeResampleInstance(rdesc, size = nrow(iris))
str(rin)
$> List of 5
$>  $ desc      :List of 4
$>   ..$ id      : chr "cross-validation"
$>   ..$ iters   : int 3
$>   ..$ predict : chr "test"
$>   ..$ stratify: logi FALSE
$>   ..- attr(*, "class")= chr [1:2] "CVDesc" "ResampleDesc"
$>  $ size      : int 150
$>  $ train.inds:List of 3
$>   ..$ : int [1:100] 119 64 54 128 100 34 63 110 22 56 ...
$>   ..$ : int [1:100] 119 128 140 47 100 34 115 139 95 58 ...
$>   ..$ : int [1:100] 64 54 140 47 63 110 22 56 115 139 ...
$>  $ test.inds :List of 3
$>   ..$ : int [1:50] 1 4 8 11 12 17 18 19 24 26 ...
$>   ..$ : int [1:50] 3 6 7 10 14 20 22 23 25 28 ...
$>   ..$ : int [1:50] 2 5 9 13 15 16 21 27 29 31 ...
$>  $ group     : Factor w/ 0 levels: 
$>  - attr(*, "class")= chr "ResampleInstance"

ここでrinResampleInstanceクラスを継承しており、訓練セットとテストセットのインデックスをリストとして含んでいる。

ResampleDescresampleに渡されると、インスタンスの生成は内部的に行われる。もちろん、ResampleInstanceを直接渡すこともできる。

リサンプルの詳細(resample description)とリサンプルのインスタンス、そしてリサンプル関数と分割するのは、複雑にしすぎているのではと感じるかもしれないが、幾つかの利点がある。

  • リサンプルインスタンスを用いると、同じ訓練セットとテストセットを用いて学習器の性能比較を行うことが容易になる。これは、既に実施した性能比較試験に対し、他の手法を追加したい場合などに特に便利である。また、後で結果を再現するためにデータとリサンプルインスタンスをセットで保管しておくこともできる。
rdesc = makeResampleDesc("CV", iter = 3)
rin = makeResampleInstance(rdesc, task = iris.task)

## 同じインスタンスを使い、2種類の学習器で性能指標を計算する
r.lda = resample("classif.lda", iris.task, rin, show.info = FALSE)
r.rpart = resample("classif.rpart", iris.task, rin, show.info = FALSE)
c("lda" = r.lda$aggr, "rpart" = r.rpart$aggr)
$>   lda.mmce.test.mean rpart.mmce.test.mean 
$>           0.04000000           0.06666667
  • 新しいリサンプリング手法を追加したければ、ResampleDescおよびResampleInstanceクラスのインスタンスを作成すればよく、resample関数やそれ以上のメソッドに触る必要はない。

通常、makeResampleInstanceを呼び出したときの訓練セットとテストセットのインデックスはランダムに割り当てられる。主にホールドアウト法においては、これを完全にマニュアルで行わなければならない場面がある。これはmakeFixedHoldoutInstance関数を使うと実現できる。

rin = makeFixedHoldoutInstance(train.inds = 1:100, test.inds = 101:150, size = 150)
rin
$> Resample instance for 150 cases.
$> Resample description: holdout with 0.67 split rate.
$> Predict: test
$> Stratification: FALSE

8.6 性能指標の集約

リサンプリングそれぞれに対して性能指標を計算したら、それを集計する必要がある。

大半のリサンプリング手法(ホールドアウト法、クロスバリデーション、サブサンプリングなど)では、性能指標はテストデータのみで計算され、平均によって集約される。

mlrにおける性能指標を表現するMeasureクラスのオブジェクトは、$aggrスロットに対応するデフォルトの集約手法を格納している。大半はtest.meanである。例外の一つは平均二乗誤差平方根(rmse)である。

## 一般的な集約手法
mmce$aggr
$> Aggregation function: test.mean
## 具体的な計算方法
mmce$aggr$fun
$> function (task, perf.test, perf.train, measure, group, pred) 
$> mean(perf.test)
$> <bytecode: 0x7f824614ebc8>
$> <environment: namespace:mlr>
## rmseの場合
rmse$aggr
$> Aggregation function: test.rmse
## test.rmseの具体的な計算方法
rmse$aggr$fun
$> function (task, perf.test, perf.train, measure, group, pred) 
$> sqrt(mean(perf.test^2))
$> <bytecode: 0x7f8260ba6eb0>
$> <environment: namespace:mlr>

setAggrigation関数を使うと、集約方法を変更することも出来る。利用可能な集約手法の一覧はaggregations function | R Documentationを確認してほしい。

8.6.1 例: 一つの指標に複数の集約方法

test.mediantest.mintest.maxはそれぞれテストセットから求めた性能指標を中央値、最小値、最大値で集約する。

mseTestMedian = setAggregation(mse, test.median)
mseTestMin = setAggregation(mse, test.min)
mseTestMax = setAggregation(mse, test.max)
rdesc = makeResampleDesc("CV", iter = 3)
r = resample("regr.lm", bh.task, rdesc, show.info = FALSE, 
             measures = list(mse, mseTestMedian, mseTestMin, mseTestMax))
r
$> Resample Result
$> Task: BostonHousing-example
$> Learner: regr.lm
$> Aggr perf: mse.test.mean=25.7,mse.test.median=25.5,mse.test.min=18.5,mse.test.max=33.1
$> Runtime: 0.0360069
r$aggr
$>   mse.test.mean mse.test.median    mse.test.min    mse.test.max 
$>        25.69050        25.50319        18.46241        33.10591

8.6.2 例: 訓練セットの誤差を計算する

平均誤分類率を訓練セットとテストセットに対して計算する例を示す。makeResampleDesc実行時にpredict = "both"を指定しておく必要があることに注意してもらいたい。

mmceTrainMean = setAggregation(mmce, train.mean)
rdesc = makeResampleDesc("CV", iters = 3, predict = "both")
r = resample("classif.rpart", iris.task, rdesc, measures = list(mmce, mmceTrainMean))
$> [Resample] cross-validation iter 1: mmce.train.mean=0.03,mmce.test.mean=0.18
$> [Resample] cross-validation iter 2: mmce.train.mean=0.04,mmce.test.mean=0.04
$> [Resample] cross-validation iter 3: mmce.train.mean=0.03,mmce.test.mean=0.06
$> [Resample] Aggr. Result: mmce.test.mean=0.0933,mmce.train.mean=0.0333

8.6.3 例: ブートストラップ

out-of-bagブートストラップ推定では、まず元のデータセット\(D\)から重複ありの抽出によって\(D^{*1}, ...,D^{*B}\)\(B\)個の新しいデータセット(要素数は元のデータセットと同じ)を作成する。そして、\(b\)回目の繰り返しでは、\(D^{*b}\)を訓練セットに使い、使われなかった要素\(D\setminus D^{*b}\)をテストセットに用いて各繰り返しに対する推定値を計算し、最終的に\(B\)個の推定値を得る。

out-of-bagブートストラップの変種であるb632b632+では、訓練セットのパフォーマンスとOOBサンプルのパフォーマンスの凸結合を計算するため、訓練セットに対する予測と適切な集計方法を必要とする。

## ブートストラップをリサンプリング手法に選び、予測は訓練セットとテストセットの両方に行う
rdesc = makeResampleDesc("Bootstrap", predict = "both", iters = 10)

## b632およびb632+専用の集計手法を設定する
mmceB632 = setAggregation(mmce, b632)
mmceB632plus = setAggregation(mmce, b632plus)

r = resample("classif.rpart", iris.task, rdesc, measures = list(mmce, mmceB632, mmceB632plus),
             show.info = FALSE)
r$measures.train
$>    iter        mmce        mmce        mmce
$> 1     1 0.026666667 0.026666667 0.026666667
$> 2     2 0.020000000 0.020000000 0.020000000
$> 3     3 0.026666667 0.026666667 0.026666667
$> 4     4 0.026666667 0.026666667 0.026666667
$> 5     5 0.013333333 0.013333333 0.013333333
$> 6     6 0.020000000 0.020000000 0.020000000
$> 7     7 0.020000000 0.020000000 0.020000000
$> 8     8 0.020000000 0.020000000 0.020000000
$> 9     9 0.006666667 0.006666667 0.006666667
$> 10   10 0.020000000 0.020000000 0.020000000
r$aggr
$> mmce.test.mean      mmce.b632  mmce.b632plus 
$>     0.07758371     0.05639290     0.05790858

8.7 便利な関数

これまでに説明した方法は柔軟ではあるが、学習器を少し試してみたい場合にはタイプ数が多くて面倒である。mlrには様々な略記法が用意してあるが、リサンプリング手法についても同様である。ホールドアウトやクロスバリデーション、ブートストラップ(b632)等のよく使うリサンプリング手法にはそれぞれ特有の関数が用意してある。

crossval("classif.lda", iris.task, iters = 3, measures = list(mmce, ber))
$> [Resample] cross-validation iter 1: mmce.test.mean=0.02,ber.test.mean=0.0222
$> [Resample] cross-validation iter 2: mmce.test.mean=0.04,ber.test.mean=0.0333
$> [Resample] cross-validation iter 3: mmce.test.mean=0.02,ber.test.mean=0.0167
$> [Resample] Aggr. Result: mmce.test.mean=0.0267,ber.test.mean=0.0241
$> Resample Result
$> Task: iris-example
$> Learner: classif.lda
$> Aggr perf: mmce.test.mean=0.0267,ber.test.mean=0.0241
$> Runtime: 0.035866
bootstrapB632plus("regr.lm", bh.task, iters = 3, measures = list(mse, mae))
$> [Resample] OOB bootstrapping iter 1: mse.b632plus=18.6,mae.b632plus=3.06,mse.b632plus=30.8,mae.b632plus=3.42
$> [Resample] OOB bootstrapping iter 2: mse.b632plus=23.2,mae.b632plus=3.49,mse.b632plus=17.3,mae.b632plus=3.06
$> [Resample] OOB bootstrapping iter 3: mse.b632plus=18.9,mae.b632plus=2.96,mse.b632plus=27.4,mae.b632plus=3.58
$> [Resample] Aggr. Result: mse.b632plus=23.5,mae.b632plus=3.29
$> Resample Result
$> Task: BostonHousing-example
$> Learner: regr.lm
$> Aggr perf: mse.b632plus=23.5,mae.b632plus=3.29
$> Runtime: 0.0782452