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

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

H2OのRパッケージ{h2o}でお手軽にDeep Learningを実践してみる(1):まずは決定境界を描く

我らが自称ゆるふわ*1ガチ勢代表@氏がこんな記事をupしてました。



ということで、こんなに簡単にDeep LearningをR上で試せるんだったらついでに僕もやってみようと思ったのでした。ただし同じirisでやるのも芸がないので*2、そこはちょっと変えてみましたよーということで。


そうそう、Deep Learningの原理については@さんのまとめがめちゃくちゃ参考になると思うので、是非是非ご参照ください。


今回はまだ初歩の初歩しか触りませんが、それでもチューニングのところなどにこのスライドにも出てくるような様々な原理的側面がかかわってきます。


そもそもH2Oって何だっけ


元々H2Oはin-memoryプラットフォームとしてHadoop上や最近だとSpark上で動かすのを前提として配布されているデータ分析&機械学習フレームワークなんですが、何故かRパッケージも配布してるんですね*3


ということで、そのRパッケージ{h2o}をGitHubからインストールして使ってやりましょーというのが、冒頭の@氏の記事の趣旨だったわけで、このブログ記事でも同様にやってみようと思います。


とりあえず{h2o}をインストールする


ちょっと前までは色々不具合があった模様ですが、今現在はこれですんなり入るみたいです。

> install.packages("h2o", repos=(c("http://s3.amazonaws.com/h2o-release/h2o/master/1542/R", getOption("repos"))))
> library("h2o", lib.loc="C:/Program Files/R/R-3.0.2/library")
 要求されたパッケージ RCurl をロード中です 
 要求されたパッケージ bitops をロード中です 
 要求されたパッケージ rjson をロード中です 
 要求されたパッケージ statmod をロード中です 
 要求されたパッケージ survival をロード中です 
 要求されたパッケージ splines をロード中です 
 要求されたパッケージ tools をロード中です 

----------------------------------------------------------------------

Your next step is to start H2O and get a connection object (named
'localH2O', for example):
    > localH2O = h2o.init()

For H2O package documentation, ask for help:
    > ??h2o

After starting H2O, you can use the Web UI at http://localhost:54321
For more information visit http://docs.0xdata.com

----------------------------------------------------------------------


 次のパッケージを付け加えます: ‘h2o’ 

 以下のオブジェクトはマスクされています (from ‘package:base’) : 

     ifelse, max, min, strsplit, sum, tolower, toupper 


ダメならソースを落としてきてローカルから入れろみたいな話になるみたいですが、環境次第かなぁと。ちなみにご覧の通りWindows版での動作確認しかしていないので悪しからず。


いつもの多変量データで手っ取り早くh2o.deeplearningを試してみる


では、このブログでは定番の多変量データを使って試してみましょう。リンク先のGitHubリポジトリから落としてきて、Rのワーキングディレクトリに置いときます。


まず、h2oをJava VM上で起動させます。当たり前ですが、Java VMが入ってないと動かないので入れてない人は事前にインストールしておきましょう*4

> localH2O <- h2o.init(ip = "localhost", port = 54321, startH2O = TRUE, nthreads=-1)

H2O is not running yet, starting it now...

Note:  In case of errors look at the following log files:
    C:\Users\XXX\AppData\Local\Temp\RtmpghjvGo/h2o_XXX_win_started_from_r.out
    C:\Users\XXX\AppData\Local\Temp\RtmpghjvGo/h2o_XXX_win_started_from_r.err

java version "1.7.0_67"
Java(TM) SE Runtime Environment (build 1.7.0_67-b01)
Java HotSpot(TM) 64-Bit Server VM (build 24.65-b04, mixed mode)

Successfully connected to http://localhost:54321 

R is connected to H2O cluster:
    H2O cluster uptime:         1 seconds 506 milliseconds 
    H2O cluster version:        2.7.0.1542 
    H2O cluster name:           H2O_started_from_R 
    H2O cluster total nodes:    1 
    H2O cluster total memory:   7.10 GB 
    H2O cluster total cores:    8 
    H2O cluster allowed cores:  8 
    H2O cluster healthy:        TRUE 


ここでは64bit版を使ってますが、別に32bit版でも普通に動くのでご安心あれ。以下@氏がやっていたのと同じようにやっていきます。ひとつ注意点として、{h2o}の関数群はh2o.importFileなどの関数でh2oクラス(S4クラス)に変換されたものしか受け付けないので、必ず事前に変換しておきましょう。

> cfData <- h2o.importFile(localH2O, path = "conflict_sample_wo_header.txt")
# ここで処理進捗のインジケータが入るが割愛
> head(cfdata)
  C1 C2 C3 C4 C5 C6 C7 C8
1  0  1  0  1  0  0  0 No
2  0  0  1  1  0  0  1 No
3  1  1  0  1  0  0  0 No
4  0  1  1  1  0  0  1 No
5  1  1  1  1  0  0  0 No
6  0  0  0  1  0  0  1 No


見た目は同じかもですが、普通にEnvironmentタグを見るとH2oParsedDataクラスである旨表示されているのが分かるはずです。以下、同様に続けます。

> res.err.dl<-rep(0,100)
# 判定結果を格納する空ベクトルを作る

> numlist<-sample(3000,100,replace=F)
# CVのためにどのインデックスから抜いてくるかをランダムに指定する

> for(i in 1:100){
+ cf.train <- cfData[-numlist[i],]
+ cf.test <- cfData[numlist[i],]
+ # 学習データとテストデータに分割
+ res.dl <- h2o.deeplearning(x = 1:7, y = 8, data = cf.train, activation = "Tanh",hidden=rep(20,2))
+ pred.dl <- h2o.predict(object=res.dl,newdata=cf.test[,-8])
+ # Deep Learningによる学習と予測
+ pred.dl.df <- as.data.frame(pred.dl)
+ test.dl.df <- as.data.frame(cf.test)
+ # 予測結果の整形
+ res.err.dl[i] <- ifelse(as.character(pred.dl.df[1,1])==as.character(test.dl.df[1,8]),0,1)
+ # 結果の格納-正解なら0, 不正解なら1の値が入る
+ }
> sum(res.err.dl)
[1] 5
# この和が不正解の回数(100回中)


ということで、H2Oによって実装したDeep LearningのCV errorは5%。なおactivationを"Tanh"ではなく"TanhWithDropout"にすると10%になりました*5。ちなみに最初3000行全部に対してCVしたんですが、途中でJava VMが凍ったので諦めて100サンプル抽出に切り替えました。。。


なお、svm{e1071}で同じことをやった結果がこちら。

> library(e1071)
> d<-read.table("conflict_sample.txt", header=TRUE, quote="\"")
> res.err.svm<-rep(0,100)
> numlist<-sample(3000,100,replace=F)
> for(i in 1:100){
+ cf.train <- d[-numlist[i],]
+ cf.test <- d[numlist[i],]
+ res.svm <- svm(cv~.,cf.train)
+ pred.svm <- predict(res.svm,newdata=cf.test[,-8])
+ res.err.svm[i] <- ifelse(pred.svm==cf.test[,8], 0, 1)
+ }
> sum(res.err.svm)
[1] 9


svm{e1071}でのCV errorは9%。ということでDeep Learningの方が優秀。。。と言いたいところなんですが、実はh2o.deeplearningのパラメータチューニングが結構大変で、意外とCV errorが15%ぐらいと高止まりし続けてたのを何とか手作業でチューニングして改善していたのでした。。。これは手ごわいかも。


なお、単一の試行でベタっと比べてconfusion matrix書いてみた結果が以下。

> d.dl.pred<-h2o.predict(object=d.dl,newdata=cfData[,-8])
> d.dl.pred.df<-as.data.frame(d.dl.pred)
> table(d$cv,as.character(d.dl.pred.df[,1]))
     
        No  Yes
  No  1439   61
  Yes  131 1369
# h2o.deeplearningの結果

> d.svm<-svm(cv~.,d)
> d.svm.pred<-predict(d.svm,newdata=d[,-8])
> table(d$cv,d.svm.pred)
     d.svm.pred
        No  Yes
  No  1402   98
  Yes   80 1420
# svm{e1071}の結果


Deep Learningの結果は毎回多少変わる*6のでこれだけ見ても何とも言えないんですが、思ったほど分が良くないかもしれません。。。


h2o.deeplearningでXORデータの決定境界を描いてみる


これまでもパッケージユーザーのための機械学習シリーズで、典型的な2次元XORデータに対する決定境界を見ることでそれぞれのアルゴリズムの特徴や個性を見てみようということをやってきたので、今回もやってみようと思います。


XORデータは複雑パターンの方を使います。リンク先のGitHubリポジトリから落としてきて、Rのワーキングディレクトリに置いときましょう。


まずは比較のために先にsvm{e1071}でやってみます。

> xorc <- read.table("xor_complex.txt", header=TRUE, quote="\"")
> xorc$label<-xorc$label-1
> xorc$label<-as.factor(xorc$label)
> px<-seq(-4,4,0.03)
> py<-seq(-4,4,0.03)
> pgrid<-expand.grid(px,py)
> names(pgrid)<-names(xorc)[-3]
> xorc.svm<-svm(label~.,xorc)
> xorc.svm.out<-predict(xorc.svm,newdata=pgrid)
> plot(xorc[,-3],pch=19,col=c(rep('blue',50),rep('red',50)),cex=3,xlim=c(-4,4),ylim=c(-4,4),main='SVM')
> par(new=T)
> contour(px,py,array(xorc.svm.out,dim=c(length(px),length(py))),xlim=c(-4,4),ylim=c(-4,4),col="purple",lwd=3,drawlabels=F)

f:id:TJO:20141023180034p:plain


まぁ大体こんな感じでしょう。では、h2o.deeplearningでやってみるとどうなるんでしょうか?

> write.table(xorc,file='xor_complex_wo_header.txt',quote=F,col.names=F,row.names=F,sep='\t')
> xorcData<-h2o.importFile(localH2O,path="xor_complex_wo_header.txt")
# 一旦ファイルに落としてからh2oクラスに変換する

> write.table(pgrid,file='pgrid.txt',quote=F,col.names=F,row.names=F,sep='\t')
> pgData<-h2o.importFile(localH2O,path="pgrid.txt")
# pgridも一旦ファイルに落としてからh2oクラスに変換する

> xorc.dl<-h2o.deeplearning(x=1:2,y=3,data=xorcData,activation="Tanh",hidden=rep(3,2))
# 隠れレイヤー引数hiddenを基本的にはいじる

> xorc.dl.out<-h2o.predict(object=xorc.dl,newdata=pgData)
# グリッドに対して予測する
> xorc.dl.out.df<-as.data.frame(xorc.dl.out)
# 予測結果をデータフレームに直す

> plot(xorc[,-3],pch=19,col=c(rep('blue',50),rep('red',50)),cex=3,xlim=c(-4,4),ylim=c(-4,4),main='Deep Learning rep(3,2)')
> par(new=T)
> contour(px,py,array(xorc.dl.out.df[,1],dim=c(length(px),length(py))),xlim=c(-4,4),ylim=c(-4,4),col="purple",lwd=3,drawlabels=F)
# 描画する

f:id:TJO:20141023180652p:plain


何だこれwww ちなみにh2o.deeplearningは同じパラメータでも計算するたびに結果が変わるということで、試しに連続で同じ計算をするとこうなります。


f:id:TJO:20141023180727p:plain


訳分からんww 色々いじってみた結果、良さそうだったのがこちら。


f:id:TJO:20141023181425p:plain


なお、今までのところ僕がチューニングでいじったh2o.deeplearningの引数は以下の通り。

  • activation: "Tanh" / "TanhWithDropout"が鉄板ですが、例えば"Rectifier" / "RectifierWithDropout", "Maxout" / "MaxoutWithDropout"も選べます
  • hidden: 隠れレイヤーのユニット数を、レイヤーの数だけベクトルで並べます。c(3,2)なら3ユニットの隠れレイヤーが2層に当たります
  • autoencoder: これがTRUEだとautoencoderが入ります。デフォルトではFALSEっぽい
  • epochs: 繰り返し回数。デフォルトだと1回っぽいので、基本増やす必要がある気が
  • hidden_dropout_ratios: Dropoutの比率を隠れレイヤーごとに指定できます。デフォルトだと全て0.5


この辺については次回以降触れようかと思います。ってかそもそものDeep Learningの原理への勉強も理解も足りないので。。。


ちょっと思ったこと


で、3000×7および100×2という2つのデータセットに対してやってみた結果なんですが、h2o.deeplearningの挙動を見た感じだとめちゃくちゃ不安定なんですよね。というか、チューニング次第でいかようにも決定境界が好き放題変わってしまうというイメージ。。。これは正直ちょっと意外でした。けれどもよくよく考えたらそれでも当然なのかなぁと。


理由はいくつか考えられて、まずそもそもそんな小さなサンプルサイズのデータセットに使うものじゃないんじゃないか?ということ。むしろ3000×7とか100×2みたいなスモールデータにDeep Learningを使うのが間違ってるだろ!ということなんでしょうが、言い換えると「とてもじゃないが精度と汎化性能の両立が従来の分類器では達成しようがないような巨大データに対してDeep Learningを用いるべき」ということなのかなぁと。いかようにも決定境界をパラメータチューニングで操れるとなれば、ガチのビッグデータには使えるんじゃないかと。もっともそれってNN系が元々持ってる性質そのものじゃないかという気もしますが。


そしてもう一つ、Deep Learningが画像認識に使われることが多いのを鑑みるに、「画像データのようにそもそも特徴量自体がいくつもの階層(レイヤー)から成っていて、個々の階層に属する素性ごとに学習してそれぞれで最適化するというプロセスを積み重ねるような学習が必要なデータにこそ、Deep Learningは有効なんじゃないか」ということを思ったのでした。隠れレイヤーごとにチューニングをしていくことで*7、精度も汎化性能も上がるという、まさに喧伝されている通りの利点が発揮できるという。


まぁ、この辺も勉強すれば理解が進むのかな&間違ってるところを正していけるのかなぁと。。。*8


最後に


Java VMが動きっぱなしだとメモリを食うので、シャットダウンしておきましょう。

> h2o.shutdown(localH2O)
Are you sure you want to shutdown the H2O instance running at http://localhost:54321 (Y/N)? Y
[1] TRUE


ということで、暇を見て色々やってみようかなと思ってます*9


追記


activation = "Rectifier"にして隠れ層いじったらこうなりました。今のところこれがベストかな?まぁ、隠れ層が1層しかないので当たり前っちゃ当たり前なんですが。。。*10

> xorc.dl<-h2o.deeplearning(x=1:2,y=3,data=xorcData,activation="Rectifier",hidden=rep(3,1),epochs = 1000,autoencoder=F)
> xorc.dl.out<-h2o.predict(object=xorc.dl,newdata=pgData)
> xorc.dl.out.df<-as.data.frame(xorc.dl.out)
> plot(xorc[,-3],pch=19,col=c(rep('blue',50),rep('red',50)),cex=3,xlim=c(-4,4),ylim=c(-4,4),main='Deep Learning rep(3,1)')
> par(new=T)
> contour(px,py,array(xorc.dl.out.df[,1],dim=c(length(px),length(py))),xlim=c(-4,4),ylim=c(-4,4),col="purple",lwd=3,drawlabels=T,levels=0.5)

f:id:TJO:20141024163108p:plain


でもこれでようやくSVMのデフォルトバージョンとかNNのやや汎化強めバージョンとかと同じくらいなんですよね。。。Deep Learning難しい。ちなみにactivation = "Tanh"に戻して、hiddenをrep(3,3)にして何度か試したら以下のように。

> xorc.dl<-h2o.deeplearning(x=1:2,y=3,data=xorcData,activation="Tanh",hidden=rep(3,3),epochs = 1000,autoencoder=F)===| 100%
> xorc.dl.out<-h2o.predict(object=xorc.dl,newdata=pgData)
> xorc.dl.out.df<-as.data.frame(xorc.dl.out)
> plot(xorc[,-3],pch=19,col=c(rep('blue',50),rep('red',50)),cex=3,xlim=c(-4,4),ylim=c(-4,4),main='Deep Learning rep(3,3)')
> par(new=T)
> contour(px,py,array(xorc.dl.out.df[,1],dim=c(length(px),length(py))),xlim=c(-4,4),ylim=c(-4,4),col="purple",lwd=3,drawlabels=T,levels=0.5)

f:id:TJO:20141024172242p:plain


これでも変な汎化の効いたSVMぐらいかなぁ。。。とりあえずDropoutの効く例がやってみたい。さらに、balance_classes関数で不均衡データ対策のために勝手に非対称ラベルを水増ししてバランスしてくれるらしく、先日のデータで試してみた結果がこちら。

> xub.dl<-h2o.deeplearning(x=1:2,y=3,data=ubData,activation="Tanh",hidden=rep(3,7),epochs=1000,autoencoder=F,balance_classes = T)
> xub.dl.out<-h2o.predict(object=xub.dl,newdata=pgData)
> xub.dl.out.df<-as.data.frame(xub.dl.out)
> plot(xub[,-3],pch=19,col=c(rep('blue',600),rep('red',20)),cex=3,xlim=c(-4,4),ylim=c(-4,4),main='Deep Learning rep(3,7)')
> par(new=T)
> contour(px,py,array(xub.dl.out.df[,1],dim=c(length(px),length(py))),xlim=c(-4,4),ylim=c(-4,4),col="purple",lwd=3,drawlabels=T,levels=0.5)

f:id:TJO:20141024174638p:plain


孤立した正例まで勝手に分類してくれてます。なお3ユニット7層なのは単に遊び過ぎただけです(笑)。

*1:どこがゆるふわなのかと顔を合わせるたびに小一時間

*2:でも多分元記事の続編で違うデータでもやるんじゃないかという雰囲気ががが

*3:CRANアーカイブからは削除されちゃったみたいですが

*4:Javaの入れ方は適当にググればすぐ分かるはず

*5:Dropoutはこの場面では効かないらしいですよ。。。

*6:DropOutなどの影響がある

*7:そこでディープラーニング土方が登場するんですね分かります

*8:なのでツッコミand/orマサカリ大歓迎

*9:絶対月1回程度にペースダウンする自信がある

*10:それはDeep Learningではない