六本木で働くデータサイエンティストのブログ

元祖「銀座で働くデータサイエンティスト」です / 道玄坂→銀座→東京→六本木

不均衡データをdownsampling + baggingで補正すると汎化性能も確保できて良さそう

弊社のランチゲストにお招きしたことのある@さんが、こんなことをツイートしておられました。

不均衡(imbalanced)データのクラス分類における補正方法については、代表的な手法であるclass weight(損失関数に対して負例のコストを負例と正例の割合に応じて割り引くもの)のやり方を以前このブログでも取り上げたことがあります。

ということで、ほんの触り程度ですがやってみようと思います。ちなみに計算負荷とか自分の手間とか色々考えて、基本的にはrandomForest {randomForest}でしかやりません。他の分類器については皆さんご自身でお試しくださいm(_ _)m


データセット


以前のデータセットだとちょっと少ないので、正例250&負例3750の合計4000点の不均衡データセットを用意しました。GitHubからcloneするなり落とすなりしてください。

適当にdというデータフレームとして読み込んでおきましょう。ついでに決定境界を描くためのグリッドデータも作っておきます。

> px <- seq(-4,4,0.05)
> py <- seq(-4,4,0.05)
> pgrid <- expand.grid(px, py)
> names(pgrid) <- names(d)[-3]

準備はこれでおしまいです。


従前通りクラス重み付けでやってみる


randomForestであればclasswt引数に正例・負例の比を与えればおしまいです。

> d.rf <- randomForest(as.factor(label)~., d, classwt=c(1, 3750/250))
> out.rf <- predict(d.rf, newdata=pgrid)
> plot(d[,-3], col=d[,3]+1, xlim=c(-4,4), ylim=c(-4,4), cex=0.5, pch=19)
> par(new=T)
> contour(px, py, array(out.rf, c(length(px), length(py))), levels=0.5, col='purple', lwd=5, drawlabels=F)

f:id:TJO:20170811154742p:plain

大体予想したような感じになっています。


downsampling + baggingでやってみる


クソコードで恐縮ですが、まずbaggingを10個でやってみました。

> outbag.rf <- c()
> for (i in 1:10){
+     set.seed(i)
+     train0 <- d[sample(3750, 250, replace=F),]
+     train1 <- d[3751:4000,]
+     train <- rbind(train0, train1)
+     model <- randomForest(as.factor(label)~., train)
+     tmp <- predict(model, newdata=pgrid)
+     outbag.rf <- cbind(outbag.rf, tmp)
+ }
> outbag.rf.grid <- apply(outbag.rf, 1, mean)-1
> plot(d[,-3], col=d[,3]+1, xlim=c(-4,4), ylim=c(-4,4), cex=0.5, pch=19)
> par(new=T)
> contour(px, py, array(out10.rf.grid, c(length(px), length(py))), levels=0.5, col='purple', lwd=5, drawlabels=F)

f:id:TJO:20170811155719p:plain

少し正例の領域が広がったようです。まだいけそうなので、baggingを50個でやってみます。

f:id:TJO:20170811160052p:plain

ギザギザしたところが減ってきました。調子に乗って100個でやってみます。

f:id:TJO:20170811160311p:plain

これぐらいで一旦打ち止めにしておこうかと思います。


発展:正例が負例の中に埋め込まれるような形になった場合


元データを生成したスクリプトは実は以下の通りです。

> set.seed(1001)
> x1 <- cbind(rnorm(1000, 1, 1), rnorm(1000, 1, 1))
> set.seed(1002)
> x2 <- cbind(rnorm(1000, -1, 1), rnorm(1000, 1, 1))
> set.seed(1003)
> x3 <- cbind(rnorm(1000, -1, 1), rnorm(1000, -1, 1))
> set.seed(4001)
> x41 <- cbind(rnorm(250, 0.5, 0.5), rnorm(250, -0.5, 0.5))
> set.seed(4002)
> x42 <- cbind(rnorm(250, 1, 0.5), rnorm(250, -0.5, 0.5))
> set.seed(4003)
> x43 <- cbind(rnorm(250, 0.5, 0.5), rnorm(250, -1, 0.5))
> set.seed(4004)
> x44 <- cbind(rnorm(250, 1, 0.5), rnorm(250, -1, 0.5))
> d <- rbind(x1,x2,x3,x41,x42,x43,x44)
> d <- data.frame(x = d[,1], y = d[,2], label=c(rep(0, 3750), rep(1, 250)))

要は第4象限の一番右下に不均衡な正例が集中するようにしたので、これまでに見たような結果になるのはむしろ当たり前なのかなと思った次第です。そこで、labelをいじってもっと全体的に見て内側に正例が集中するように変えてみます。

> d1 <- d
> d1$label <- c(rep(0,3000), rep(1,250), rep(0,750))

まずclasswtでやってみます。

> d1.rf <- randomForest(as.factor(label)~., d1, classwt=c(1, 3750/250))
> out.rf <- predict(d1.rf, newdata=pgrid)
> plot(d1[,-3], col=d1[,3]+1, xlim=c(-4,4), ylim=c(-4,4), cex=0.5, pch=19)
> par(new=T)
> contour(px, py, array(out.rf, c(length(px), length(py))), levels=0.5, col='purple', lwd=5, drawlabels=F)

f:id:TJO:20170811160906p:plain

ものすごくoverfittingしている感じすらしてしまう、妙に小ぢんまりとした決定境界になってしまいました。では、今度はdownsampling + baggingでやってみましょう。面倒なのでいきなりbaggingを100個でやります。

> outbag.rf <- c()
> for (i in 1:100){
+ set.seed(i)
+ train.tmp <- d1[d1$label==0,]
+ train0 <- train.tmp[sample(3750, 250, replace=F),]
+ train1 <- d1[3001:3250,]
+ train <- rbind(train0, train1)
+ model <- randomForest(as.factor(label)~., train)
+ tmp <- predict(model, newdata=pgrid)
+ outbag.rf <- cbind(outbag.rf, tmp)
+ }
> outbag.rf.grid <- apply(outbag.rf, 1, mean)-1
> plot(d1[,-3], col=d1[,3]+1, xlim=c(-4,4), ylim=c(-4,4), cex=0.5, pch=19)
> par(new=T)
> contour(px, py, array(outbag.rf.grid, c(length(px), length(py))), levels=0.5, col='purple', lwd=5, drawlabels=F)

f:id:TJO:20170811161356p:plain

きちんと原点(0,0)のすぐ右下の第4象限で一番近いゾーンにある程度広がりのある、そこそこ汎化性能がありげに見える決定境界が描かれているのが分かります。ただしfalse positiveが増えそうだなという感じもするので、そこは今後の課題でしょうか(そこを重視し過ぎるとoverfittingに逆戻りなので)。


感想


確かに、2通りの正例設定で試してみた感じだとclass weightよりはdownsampling + baggingの方がより汎化性能に優れた決定境界が得られそうだと思った次第です。というより、実を言うと「発展」に対してclasswtで補正した時の結果が個人的にはちょっとショックでした。こんなにoverfittingしてしまうのかー、的な。。。


そういう意味で言うと、@さんご紹介の論文が指摘するように理論的にもdownsampling + baggingの方がきちんと汎化性能を確保できるというのがこれで納得できましたということで、今回の実験は有意義な結果になったのではないかと思います。今度からは積極的に試してみます。