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

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

NN Playgroundに出てくる「渦巻きサンプルの二値分類」を学習器を変えながら試してみる(追記あり)

A Neural Network PlaygroundというDeep Learningのパラメータを変えながらその挙動を見て学べる(楽しめる)サイトがTensorFlowプロジェクトからの提供であるんですが、そのサンプルデータセットの4番目に「渦巻きサンプル」(別名「スイスロール」)があります。以下の画像の右下のやつです。

f:id:TJO:20170826161104p:plain

これってDeep NNでやろうとすると意外としちめんどくさいんですが、確か杉山先生のイラスト機械学習本だとガウシアンカーネルSVMでサクッと分類できるみたいに書かれていた気がいたしまして。

ということで、ちょうど今ネタ切れなのもあるのでちょっとした余興としてこのデータの二値分類をやってみようと思います。なおスペースの都合上試行錯誤した部分自体は割愛して、どんなパラメータにしたらどんな風にダメだったかを簡単にコメントするに留めておきますので、悪しからずご了承ください。。。


データセット


一応、それっぽいデータセットを適当に生成してGitHubに置いときました。今回もRでやるので、dという名前でもつけてデータフレームとして読み込んでおいてください。

f:id:TJO:20170826161648p:plain

適当にプロットするとこんな感じになります。ついでに決定境界を描くためのグリッドも作っておきます。

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

SVMでやってみる


杉山本だと簡単にできるみたいなイメージで書かれているんですが、やってみると意外とうまくいかなかったです(笑)。LIBSVM由来の{e1071}のsvmだとガウシアンカーネル選択時のgammaがデフォルト値だと1/ncol(x)になるんですが、これだと汎化が強過ぎて全く渦巻き模様にフィットしない謎の決定境界を描きます。そこで、少しキツく過学習気味になるように、gamma = 8としてみました。

> library(e1071)
> d.svm <- svm(as.factor(label)~., d, gamma=8)
> out.svm <- predict(d.svm, newdata=pgrid)
> plot(d[,-3], pch=19, col=d[,3]+1, cex=2, xlim=c(-4,4), ylim=c(-4,4))
> par(new=T)
> contour(px, py, array(out.svm, c(length(px),length(py))), levels=0.5, drawlabels = F, col='purple', lwd=3, xlim=c(-4,4), ylim=c(-4,4))

f:id:TJO:20170826162013p:plain

決定境界自体は全然渦巻いてくれていませんが(汗)、とりあえず学習サンプルを綺麗に囲むように決定境界が描かれているのが分かるかと思います。


ランダムフォレストでやってみる


一般にこの手の曲線的な決定境界が求められるシチュエーションではランダムフォレストのような樹木モデル由来の学習器は良くないと予想されるんですが、それでも物は試しにやってみましょう。

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

f:id:TJO:20170826162407p:plain

おい、もうちょっとどうにかなんねーのかよ(笑)。案の定、樹木モデル特有のカクカクした決定境界がモロに過学習的な傾向を示しています。やはりこの手のデータセットには樹木モデル系の学習器はダメなようです。よってxgboostもやろうかと思っていましたが、割愛することにします。


DNNでやってみる


ということで、NN playgroundと同様にDNNでやってみます。ここではRで回すということで、MXnetでやることにします(やってみたいという方はKeras in Rでも良いと思いますが)。そう言えば今気付いたんですが、{mxnet}のバージョンが0.10.1のままのようです。。。0.11.1で最適化エンジンや損失関数の定義が増えているはずなので、最新バージョンだと違う結果が出せるかもしれません。


そして結果より先に書いておきますが、これめちゃくちゃ試行錯誤するのに時間かかりました(汗)。ぶっちゃけ、learning rateをわずかにいじっただけで全くデタラメな決定境界を描くの連続で、いつまで経ってもうまく学習しない(ほんの0.001変えただけでtraining accuracyが0.5とかひどいと0.39とかに張り付いて変わらないみたいな状況も見られた)。これは実はNN Playgroundでも明確に見られる現象で、そもそも学習中もなかなか渦巻き状の学習サンプルにきちんとモデルが追随しないんですよね。なので正直以下の結果を出したところで心が折れました。。。

> library(mxnet)
> train.x <- data.matrix(d[,-3])
> train.y <- d[,3]
> test <- data.matrix(pgrid)
> data <- mx.symbol.Variable("data")
> fc1 <- mx.symbol.FullyConnected(data, name="fc1", num_hidden=5)
> act1 <- mx.symbol.Activation(fc1, name="tanh1", act_type="tanh")
> fc2 <- mx.symbol.FullyConnected(act1, name="fc2", num_hidden=5)
> act2 <- mx.symbol.Activation(fc2, name="tanh2", act_type="tanh")
> fc3 <- mx.symbol.FullyConnected(act2, name="fc3", num_hidden=5)
> act3 <- mx.symbol.Activation(fc3, name="tanh3", act_type="tanh")
> fc4 <- mx.symbol.FullyConnected(act3, name="fc4", num_hidden=2)
> softmax <- mx.symbol.SoftmaxOutput(fc4, name="softmax")
> devices <- mx.cpu()
> mx.set.seed(71)
> model <- mx.model.FeedForward.create(softmax, X=train.x, y=train.y, ctx=devices, num.round=2000, array.batch.size=10, learning.rate=0.0008, momentum=0.99,  eval.metric=mx.metric.accuracy, initializer=mx.init.uniform(0.5), array.layout = "rowmajor", epoch.end.callback=mx.callback.log.train.metric(100))
> preds <- predict(model, test, array.layout = "rowmajor")
> pred.label <- max.col(t(preds)) - 1
> plot(d[,-3], pch=19, col=d[,3]+1, cex=2, xlim=c(-4,4), ylim=c(-4,4))
> par(new=T)
> contour(px, py, array(pred.label, c(length(px),length(py))), levels=0.5, drawlabels = F, col='purple', lwd=3, xlim=c(-4,4), ylim=c(-4,4))

f:id:TJO:20170826172337p:plain

ということで、DNNのご利益が全く感じられない結果になりました(泣)。ちなみにNN Playgroundでやってもうまくいくことってあまり多くなくて、しかもそこでうまくいったパラメータ設定をMXnetに突っ込んでも再現しないorz どういうことですか。。。


なお、上2つにかかった時間はたったの10分。DNNでここまでチューニングするのにかかった時間は2時間半でした。Train accuracyも0.8ちょっとが限界で、それ以上は上がらず。


最後に


僕はよくこういう喩えを使うことが多いのですが。

  • 従来型の機械学習は、自動車に喩えればセダン。特に何も工夫しなくても簡単に街中を走れるが、スピードを出そうとすると限界がある
  • Deep Learningは、自動車に喩えればF1レーシングカー。果てしなく速いスピードを出すことが出来るが、チューニングが難しい

この喩えを地で行くような余興の結果になりました。。。Deep Learningも適材適所、ということで。


追記1


@さんがいつもNN Playgroundのデモで使っているという設定を参考に、以下のように変えてみました。

> data <- mx.symbol.Variable("data")
> fc1 <- mx.symbol.FullyConnected(data, name="fc1", num_hidden=8)
> act1 <- mx.symbol.Activation(fc1, name="relu1", act_type="relu")
> fc2 <- mx.symbol.FullyConnected(act1, name="fc2", num_hidden=8)
> act2 <- mx.symbol.Activation(fc2, name="relu2", act_type="relu")
> fc3 <- mx.symbol.FullyConnected(act2, name="fc3", num_hidden=5)
> act3 <- mx.symbol.Activation(fc3, name="relu3", act_type="relu")
> fc4 <- mx.symbol.FullyConnected(act3, name="fc4", num_hidden=2)
> softmax <- mx.symbol.SoftmaxOutput(fc4, name="softmax")
> devices <- mx.cpu()
> mx.set.seed(71)
> model <- mx.model.FeedForward.create(softmax, X=train.x, y=train.y, ctx=devices, num.round=3000, array.batch.size=10, learning.rate=0.0008, momentum=0.99,  eval.metric=mx.metric.accuracy, initializer=mx.init.uniform(0.5), optimizer='sgd', array.layout = "rowmajor", epoch.end.callback=mx.callback.log.train.metric(100))
> preds <- predict(model, test, array.layout = "rowmajor")
> pred.label <- max.col(t(preds)) - 1
> plot(d[,-3], pch=19, col=c(rep(1,100),rep(2,100)), cex=2, xlim=c(-4,4), ylim=c(-4,4))
> par(new=T)
> contour(px, py, array(pred.label, c(length(px),length(py))), levels=0.5, drawlabels = F, col='purple', lwd=3, xlim=c(-4,4), ylim=c(-4,4))

f:id:TJO:20170828130534p:plain

多少スイスロールっぽくはなりました。が、unit数が8とかって結構このシチュエーションだと色々迷う感じですよね。。。


追記2


何か腹が立ってきたので(笑)、{keras}つまりKeras in Rでやってみました。

> library(keras)
> model <- keras_model_sequential()
  model %>%
  layer_dense(units=8, input_shape=2) %>%
  layer_activation(activation='relu') %>%
  layer_dense(units=8) %>%
  layer_activation(activation='relu') %>%
  layer_dense(units=5) %>%
  layer_activation(activation='relu') %>%
  layer_dense(units=1, activation='sigmoid')
  model %>% compile(
  loss='binary_crossentropy',
  optimizer=optimizer_sgd(lr=0.03),
  metric=c('accuracy')
  )
> model %>% fit(train.x, train.y, epoch=1500, batch_size=10)
> pred_class <- model %>% predict(test, batch_size=10)
> pred_class <- round(pred_class,0)
> plot(d[,-3], pch=19, col=c(rep(1,100),rep(2,100)), cex=2, xlim=c(-4,4), ylim=c(-4,4))
> par(new=T)
> contour(px, py, array(pred_class, c(length(px),length(py))), levels=0.5, drawlabels = F, col='purple', lwd=3, xlim=c(-4,4), ylim=c(-4,4))

f:id:TJO:20170828145323p:plain

スイスロールにはなるんですが、やっぱり学習が収束し切れてないですねぇ。。。


追記3


f:id:TJO:20170830150459p:plain

一応、NN Playgroundでうまくいった時の設定を貼っておきます。本当はこれでうまくいくはずなんですが。。。(汗)