tuneRF関数の挙動の検証

RのrandomForestパッケージのtuneRF関数が遅いというお話があったので,調べてみた.
tuneRF関数は,ランダムフォレストを用いて予測モデルを構築する際に使用する特徴量の個数を貪欲的な方法により求める.
tuneRF関数だけでなく,Rで機械学習アルゴリズムをチューニングする方法については,id:TJOさんの記事「Rで機械学習するならチューニングもグリッドサーチ関数orオプションでお手軽に」が詳しいので,是非参照してほしい.

検証に使用するデータと環境

UCI Machine Learning RepositoryBank Marketingデータセットを使用する.このデータセットは,ポルトガルの銀行で電話でダイレクトマーケティングを実施した際に収集したデータを用いて,予測モデルのアルゴリズムに投入できるように特徴量を構築したもの.

まずは,データを取得して解凍する.

$ wget http://archive.ics.uci.edu/ml/machine-learning-databases/00222/bank.zip
$ unzip bank.zip

同じディレクトリに"bank-full.csv", "bank-names.txt", "bank.csv"という3つのファイルが解凍される."bank-full.csv"がすべてのレコードを含んでおり,ここではこのデータを使用する.

データをRに読み込んで,行頭やサイズ,各列のデータ型を確認する.

> bank <- read.csv("bank-full.csv", sep=";")
> head(bank, 3)
  age          job marital education default balance housing loan contact day
1  58   management married  tertiary      no    2143     yes   no unknown   5
2  44   technician  single secondary      no      29     yes   no unknown   5
3  33 entrepreneur married secondary      no       2     yes  yes unknown   5
  month duration campaign pdays previous poutcome  y
1   may      261        1    -1        0  unknown no
2   may      151        1    -1        0  unknown no
3   may       76        1    -1        0  unknown no
> dim(bank)
[1] 45211    17
> sapply(bank, class)
      age       job   marital education   default   balance   housing      loan 
"integer"  "factor"  "factor"  "factor"  "factor" "integer"  "factor"  "factor" 
  contact       day     month  duration  campaign     pdays  previous  poutcome 
 "factor" "integer"  "factor" "integer" "integer" "integer" "integer"  "factor" 
        y 
 "factor"

行数が45,211,列数が17であることを確認できる.最後の列(y)は,ダイレクトマーケティングに顧客が反応したかどうかを表すカテゴリ変数であり,この項目を目的変数として使用する(Yes: 反応した,No: 反応していない).残りの16個の列を説明変数と使用する.上記の結果を見ると,16個のうち9項目がカテゴリ変数であることを確認できる.

なお,以下で検証に使用する環境は次の通り.

  • OS : OS X Yosemite
  • プロセッサ : 1.7GHz Intel Core i7
  • メモリ : 8GB 1600MHz DDR3

まずは単純にtuneRF関数を実行してみる

まずは特別な設定を行わずにtuneRF関数を実行してみよう.tuneRF関数の第1引数には説明変数,第2引数には目的変数を指定する.また,doBest引数をTRUEに指定すると,評価が最も良いモデルを返すようになる.

> set.seed(123)
> system.time(tuneRF(bank %>% select(-y), bank$y, doBest=TRUE))
mtry = 4  OOB error = 9.46% 
Searching left ...
mtry = 2 	OOB error = 9.78% 
-0.03295162 0.05 
Searching right ...
mtry = 8 	OOB error = 9.48% 
-0.001402197 0.05 
   ユーザ   システム       経過  
    77.131      1.725     81.135

上記を見ると,おおよそ1分20秒程度実行に要したことを確認できる.この結果,特徴量の個数が

  • 4個のときに,Out-of-Bag誤差(OOB error)は9.46%
  • 2個のときに,Out-of-Bag誤差は9.78%
  • 8個のときに,Out-of-Bag誤差は9.48%

となり,特徴量の個数が4個のときにOut-of-Bag誤差が最少となり,この個数に設定するのが良さそうであることがわかる*1

構築する決定木の個数を増やしてみる

ntreeTry引数はデフォルトでは50となっており,50個の決定木を構築することがわかる.500個の決定木を構築するように指定してみよう.

> set.seed(123)
> system.time(tuneRF(bank %>% select(-y), bank$y, ntreeTry=500, doBest=TRUE))
mtry = 4  OOB error = 9.1% 
Searching left ...
mtry = 2 	OOB error = 9.53% 
-0.04689018 0.05 
Searching right ...
mtry = 8 	OOB error = 9.18% 
-0.008503401 0.05 
   ユーザ   システム       経過  
   237.656      2.943    244.725

構築する決定木の個数が10倍になったので,実行時間も約10倍になるのかと思うところであるが,3倍程度に抑えられていることを確認できる.

tuneRF関数の引数と仕様

tuneRF関数にはいくつかの引数を指定できる.

> args(tuneRF)
function (x, y, mtryStart = if (is.factor(y)) floor(sqrt(ncol(x))) else floor(ncol(x)/3), 
    ntreeTry = 50, stepFactor = 2, improve = 0.05, trace = TRUE, 
    plot = TRUE, doBest = FALSE, ...) 
NULL
引数 説明 データ型
x 説明変数 data.frame
y 目的変数 factor or numeric
mtryStart 選択する特徴量の個数の初期値 integer
ntreeTry 構築する決定木の個数 integer
stepFactor 特徴量の個数を減少または増加させるファクター integer
improve モデルの評価指標の改善率 numeric
trace チューニングの過程をトレースして画面に表示するかどうかを指定するフラグ logical
doBest 評価が最も良いモデルを返すかどうかを指定 logical

また,tuneRF関数自体もそれほど長くないので,中身を読んでみる.

tuneRF <- function(x, y, mtryStart=if(is.factor(y)) floor(sqrt(ncol(x))) else
                   floor(ncol(x)/3), ntreeTry=50, stepFactor=2,
                   improve=0.05, trace=TRUE, plot=TRUE, doBest=FALSE, ...) {
  if (improve < 0) stop ("improve must be non-negative.")
  # yが因子かどうかを判定
  classRF <- is.factor(y)
  errorOld <- if (classRF) {
    # yが因子の場合はクラス分類を実行し,誤分類率により評価
    randomForest(x, y, mtry=mtryStart, ntree=ntreeTry,
                 keep.forest=FALSE, ...)$err.rate[ntreeTry,1]
  } else {
    # yが因子でない場合は回帰を実行し,平均二乗平均誤差により評価
    randomForest(x, y, mtry=mtryStart, ntree=ntreeTry,
                 keep.forest=FALSE, ...)$mse[ntreeTry]
  }
  if (errorOld < 0) stop("Initial setting gave 0 error and no room for improvement.")
  # traceがTRUEに指定された場合は,チューニング過程を画面に出力
  if (trace) {
    cat("mtry =", mtryStart, " OOB error =",
        if (classRF) paste(100*round(errorOld, 4), "%", sep="") else
        errorOld, "\n")
  }

  oobError <- list()
  oobError[[1]] <- errorOld
  names(oobError)[1] <- mtryStart

  # 以下,特徴量の個数を増減させて予測モデルを構築し結果を評価(left: 個数を削減,right: 個数を増加)
  for (direction in c("left", "right")) {
    if (trace) cat("Searching", direction, "...\n")
    Improve <- 1.1*improve
    mtryBest <- mtryStart
    mtryCur <- mtryStart
    while (Improve >= improve) {
      mtryOld <- mtryCur
      mtryCur <- if (direction == "left") {
        # 個数の削減は,現在の個数をstepFactor引数で割ったものを新しい個数とする
        max(1, ceiling(mtryCur / stepFactor))
      } else {
        # 個数の増加は,現在の個数にstepFactor引数を掛けたものを新しい個数とする
        min(ncol(x), floor(mtryCur * stepFactor))
      }
      if (mtryCur == mtryOld) break
      errorCur <- if (classRF) {
        # yが因子の場合はクラス分類を実行し,誤分類率により評価
        randomForest(x, y, mtry=mtryCur, ntree=ntreeTry,
                     keep.forest=FALSE, ...)$err.rate[ntreeTry,"OOB"]
      } else {
        # yが因子でない場合は回帰を実行し,平均二乗平均誤差により評価
        randomForest(x, y, mtry=mtryCur, ntree=ntreeTry,
                     keep.forest=FALSE, ...)$mse[ntreeTry]
      }
      if (trace) {
        cat("mtry =",mtryCur, "\tOOB error =",
            if (classRF) paste(100*round(errorCur, 4), "%", sep="") else
            errorCur, "\n")
      }
      oobError[[as.character(mtryCur)]] <- errorCur
      # 予測精度(誤差)の改善率の算出
      Improve <- 1 - errorCur/errorOld
      cat(Improve, improve, "\n")
      if (Improve > improve) {
        errorOld <- errorCur
        mtryBest <- mtryCur
      }
    }
  }
  mtry <- sort(as.numeric(names(oobError)))
  res <- unlist(oobError[as.character(mtry)])
  res <- cbind(mtry=mtry, OOBError=res)

  # plotがTRUEに指定された場合は,横軸を特徴量の個数,縦軸を評価値とする折れ線グラフをプロット
  if (plot) {
    plot(res, xlab=expression(m[try]), ylab="OOB Error", type="o", log="x",
         xaxt="n")
    axis(1, at=res[,"mtry"])
  }

  # doBestがTRUEに指定された場合は,評価が最も良いモデルを返り値とする
  if (doBest)
    res <- randomForest(x, y, mtry=res[which.min(res[,2]), 1], ...)

  res
}

以上のtuneRF関数の実装を読むと,特徴量の個数と評価結果を保持するノードを木構造で表現していくことがわかる.
そのため,この処理の高速化の方針として,

  • 木の左側の計算と右側の計算を並列化する
  • 個々のrandomForest関数で決定木を構築する処理を並列化する

の2つが考えられる.これ以外の方法は,randomForest関数,およびその内部のC++Fortranのコードを改変することになる.
なお,前者は,木が深い時に特に有効であり,後者は木の深さに関係なく,ある程度の個数の決定木を構築するならば有効な手段だと思われる.

*1:ランダムフォレストはOut-of-Bag誤差が求まるのでクロスバリデーション不要とも言われるが,実際はクロスバリデーションなども組み合わせて,もう少し精緻に検証することが望ましいと思われる.