読者です 読者をやめる 読者になる 読者になる

About connecting the dots.

statistics/machine learning adversaria.

stackingを試してみた

つい先日,stackingについての以下の記事が話題になっていました.

このあたり,私自身は試したことがなかったので,実際に試してみましたよというお話.

コード

Rでちゃちゃっと書きました.データをk分割して,分割サンプルごとに訓練データでロジスティック回帰予測モデル構築→予測結果を訓練データに追加→RandomForestで予測モデルを構築,までが訓練フェーズ.テストフェーズでは構築したロジスティック回帰とRandomForestを使ってテストデータの分類を行いました.

使ったのは手元にあった2000サンプル,分類クラス数は2クラスで,正例負例がそれぞれ1000サンプルでした.素性ベクトルはすべてバイナリの15個の変数になります.

library(randomForest)

# load data
data = read.delim("data/sample.tsv", sep="\t")

# stacking sample
k = 10 # cross validation split number
result = c()
for (i in 1:k) {
  print(i)
  data.splitted = cv(data, k)
  # construct predict model
  data.train = cv.training(data.splitted, 1)
  model.glm = glm(y~., data=data.train, family=binomial)
  data.train = stacking(data.train, model.glm) # commentout this line when not using stacking
  model.rf = randomForest(y~., data=data.train)
  # predict with test data
  data.test = cv.test(data.splitted, 1)
  data.test = stacking(data.test, model.glm) # commentout this line when not using stacking
  model.rf.predict = predict(model.rf, newdata=data.test, type="class")
  result = rbind(result, score(model.rf.predict, data.test$y))
}

上で説明した処理はすべて関数化して,以下のようにまとめました.また判別制度の評価をちゃんとするには交差検定も必要なので,こちらも関数にしてまとめておきました*1

# create data for k-fold cross validation
cv = function(d, k) {
  n = sample(nrow(d), nrow(d))
  d.randomized = data[n,] # randomize data
  n.residual = k-nrow(d)%%k
  d.dummy = as.data.frame(matrix(NA, nrow=n.residual, ncol=ncol(d)))
  names(d.dummy) = names(d)
  d.randomized = rbind(d.randomized, d.dummy) # append dummy for residuals
  d.splitted = split(d.randomized, 1:k)
  for (i in 1:k) {
    d.splitted[[i]] = na.omit(d.splitted[[i]])
  }
  d.splitted
}

# training data
cv.training = function(d, k) {
  d.train = as.data.frame(matrix(0, nrow=0, ncol=ncol(d[[1]])))
  names(d.train) = names(d[[1]])
  for (i in 1:length(d)) {
    if (i != k) {
      d.train = rbind(d.train, d[[i]])
    }
  }
  d.train
}

# test data
cv.test = function(d, k) {
  d[[k]]
}

# stacking with glm
stacking = function(d, m) {
  d = cbind(d, predict(m, newdata=d, type="response"))
  names(d)[length(d)] = "stacking"
  d
}

# check
score = function(p, r) {
  s = c(0, 0, 0, 0)
  for (i in 1:length(p)) {
    pi = 2-as.integer(p[[i]])
    ri = 2-as.integer(r[i])
    s[pi*2+ri+1] = s[pi*2+ri+1]+1
  }
  s
}

結果

ということで,10分割の交差検定をした結果を見ましょう.以下のように予測値と実際の結果のマトリックスで表示してみます.

# show results
m = matrix(apply(result, 2, sum), 2, 2)
dimnames(m) = list(c("res$n", "res$p"), c("pred$n", "pred$p"))
print(m)
print(m/nrow(data))

stackingなし

stacking処理を無効にするには,単にstacking()関数を読んでいる部分2箇所をコメントアウトしてあげるだけです.結果は.Precisionは850/(850+149)=83.2%で,Recallは850/(850+134)=86.3%でした.

> print(m)
       res$p res$n
pred$p   850   149
pred$n   134   867
> print(m/nrow(data))
       res$p  res$n
pred$p 0.425 0.0745
pred$n 0.067 0.4335

stackingあり

それに対してstackingはどうでしょう.Precisionは827/(827+167)=83.2%で,Recallは827/(827+152)=84.5%でした.むしろ悪くなってる...orz

> print(m)
       res$p res$n
pred$p   827   167
pred$n   152   854
> print(m/nrow(data))
        res$p  res$n
pred$p 0.4135 0.0835
pred$n 0.0760 0.4270

というわけで,とりあえず試してみましたよというお話でした.コード全体はgistにあげました.よろしければどうぞ.

*1:このあたりをまとめるにあたっては,shakezoさんの RでデータフレームをK分割する - shakezoの日記 を参考にさせていただきました.