【Rで機械学習】LSTM-RNNの予測結果を考察する。

前回、sinカーブを学習データとしたLSTM-RNNによる学習と予測の実験を行いました。

wanko-sato.hatenablog.com

その結果、比較的良好な予測結果が得られたわけなんですが、果たして本当にそれだけで満足して良いのだろうか?とふと疑問に思いました。というのも、そもそもLSTM-RNNの予測は学習されたモデルから確率分布を用いてサンプリングしてくるものなので、どうしても確率的な振る舞いが混入してきます。その確率的な振る舞いがどのようなものであるのか、きちんと把握した上でないと、予測結果の良しあしは判断できないのではないか、と考えたのです。

きっかけとなったデータ

前回の記事に書いた学習モデルを用いていくつか予測データを作ってみたところ、ちょっと気になる結果が得られました。
f:id:wanko_sato:20170611085959p:plain
x=100のあたりに外れ値が発生しています。このような外れ値が発生してもその後の予測がきちんとできている、というあたりすごいと思うのですが、一回こっきりの予測でこのような外れ値が出てくるのは果たしてどうなんだろう?と思うわけです。確率的な振る舞いをする以上、このような結果は避けられないとは思うのですが、だとしたら、こうした外れ値とどのように付き合っていったら良いか?というのが問題になってくると思うのです。

学習精度とデータ量の関係

加えて、データの量が学習精度に与える影響も気になります。
前回使用した学習データは、sinカーブ1周期を200区間に区切り、それを10周期繰り返したものを1単位として5000回繰り返したデータを使用しました。データ量としては

200×10×5000 = 10,000,000

の数値を含んだテキストデータです。ファイルサイズとしてはおよそ34MBのもので、テキストファイルとしてはほどほどの大きさです。が、果たしてこれが学習データとして十分なのか、あるいは多すぎるのか、してみたいとも思いました。

ならば実験してみよう。

というわけで、データ量が学習結果に与える影響と、そもそも学習結果から得られた予測結果がどのような性質をもっているのか、を考察するための実験を行います。

方針

  1. sinカーブ1周期を200区間に区切り、それを10周期繰り返したものを1単位としたデータを用意する
  2. 上記データを1,000回、5,000回、10,000回繰り返したデータを用意し、それぞれを学習データとして学習モデルを作成する
  3. 学習結果から0を起点とした200個の連続データを生成し、それを1,000回繰り返す

予測結果は2,3回出すだけでは正直よくわからないので、1,000回出力しています。そもそも確率的な振る舞いをするのですから、予測自体も大量に行ってみないとどのような振る舞いをするのかわからないためです。そのうえで、データを眺めながらどのような指標で良しあしを判定していくのか、考えていきます。

データを作る

repNum <- 1000

sinDataVec <- sapply(seq(1,repNum),function(x){
  curv <- 0
  sinData <- c()
  for(i in 0:2000){
    curv <- sin((2*pi/200)*i)*100
    sinData <- rbind(sinData,floor(rnorm(1,curv,20)))
  }
  sinDataU <- paste(sinData,collapse = " ")
  return(sinDataU)
})

このコードで指定の繰り返し数のデータを作成できます。作成したsinDataVecはテキストファイルに保存して、学習データとして読み込めるようにしておきます。そのあたりの詳細は前回の記事を参照してください。

学習コストの比較

学習にかかった時間等を比較してみましょう。

data time[sec] NLL Perp
1,000 48.944 4.016 55.455
5,000 245.173 3.301 27.149
10,000 295.356 3.212 24.835

学習時間をプロットすると、
f:id:wanko_sato:20170611093951p:plain
このような形で頭打ちになっているようです。
また、Perpをプロットするとf:id:wanko_sato:20170611094136p:plain
と下げ止まっており、あまり学習データを増やしてもある程度のところで頭打ちになるのじゃないか、という印象を受けます。もちろん、今回使用している学習データがかなり規則性の強いデータであり、値のバリエーションも少ないことも影響しているでしょう。おそらく言語データのように規則性があるといってもそのバリエーションが膨大なものの場合、大量のデータが必要になるであろうことは想像に難くありません。

予測結果をとりまとめる

mxnetを使った学習のコードは前回と同じものを使います。
予測の部分を、1,000回繰り返し用に少し書き換えます。

predRep <- lapply(seq(1,1000),function(x){
  infer.model <- mx.lstm.inference(num.lstm.layer=num.lstm.layer,
                                   input.size=vocab,
                                   num.hidden=num.hidden,
                                   num.embed=num.embed,
                                   num.label=vocab,
                                   arg.params=model$arg.params,
                                   ctx=mx.cpu())
  
  # create sentense
  start <- "0"
  seq.len <- 200
  random.sample <- TRUE
  
  last.id <- dic[[start]]
  out <- "0 "
  for (i in (1:(seq.len-1))) {
    input <- c(last.id-1)
    ret <- mx.lstm.forward(infer.model, input, FALSE)
    infer.model <- ret$model
    prob <- ret$prob
    last.id <- make.output(prob, random.sample)
    out <- paste0(out, lookup.table[[last.id]], sep=" ")
  }
  
  res <- paste0(out)
  resVec <- as.numeric(unlist(strsplit(res," ")))
  return(resVec)
})

単純にlapplyで1,000回繰り返し、リストにしているだけです。ここは結構時間がかかるので、出力したリストはsaveRDSで保存しておくと安全です。
で、さらに各条件の結果を一つに取りまとめておくとさらに便利です。

fileVec <- c(file.path(fileDir,"predRes_1000by200_start0.rds"),
             file.path(fileDir,"predRes_5000by200_start0.rds"),
             file.path(fileDir,"predRes_10000by200_start0.rds")))

としてRDSファイルのパスをベクトルにして、

predDataList <- lapply(fileVec,function(x){
  out <- readRDS(x)
  predRep_rev <- lapply(seq(1,1000),function(x){
    corout <- cor(x=seq(1,50),y=out[[x]][1:50])
    if(corout<0){
      outd <- out[[x]] * (-1)
    } else {
      outd <- out[[x]]
    }
    return(outd)
  })
  return(predRep_rev)
})

とリストにしてあげればOKです。ちなみにここで、sinカーブの最初の部分が右上がりの場合はそのまま、右下がりの場合は位相を反転する、という処理を加えています。というのも、初期値が0の場合、右上がりで始まる場合と右下がりで始まる場合がほぼ等確率で発生するためです。

これでデータの準備が整ったので、予測結果がどんな感じなのか、見ていきましょう。

予測結果のプロット

予測結果をひとつのグラフにプロットするには

for(i in 1:1000){
  if(i==1){
    plot(predDataList[[5]][[i]],ylim=c(-120,120),col="#00000010")
  }else{
    par(new=T)
    plot(predDataList[[5]][[i]],ylim=c(-120,120),col="#00000010")
  }
}

これでいけます。さすがにこれだけの量をプロットするのはちょっと時間がかかるので、しばしお待ちください。


・・・・・

はい、でました。
1単位を1,000回繰り返したものを学習データとした場合
f:id:wanko_sato:20170611095138p:plain
なんかこう、全然まとまりのないプロットになりましたね。予測結果がかなりばらついており、いまいちうまくいっていない感じがします。

続いて5,000回繰り返しの場合
f:id:wanko_sato:20170611095257p:plain
お、割ときれいにいっているようです。sinカーブが太い線になるのは、もともとのデータが分散5の正規分布に従っているためですので、予想通りです。ところどころおかしなデータがありますが、1,000回のうちの数回であれば無視しても構わない範囲だろうと思います。

最後に10,000回繰り返した場合
f:id:wanko_sato:20170611095519p:plain
5,000回の場合と大きく変わらなそうです。学習コストのところで示したように、5,000回から10,000回にデータを増やしてもそこまで学習コストが変化していなかったことから、予測精度としてもそうそう大きく変化しなかった、ということでしょうか。

予測結果の分散

sinカーブが帯状になったのを確認できたので、次に各点でどれくらいデータがばらついているのかを確認します。

varList <- lapply(seq(1,length(predDataList)),function(i){
  sapply(seq(1,200),function(j){
    var(sapply(seq(1,1000),function(k){ predDataList[[i]][[k]][j] }))
  })
})

とすると、各条件、各x軸の点における分散が計算できます。これをプロットするとこんな感じ。

plot(varList[[1]],col="#FF0000",ylim=c(0,5000))
par(new=T)
plot(varList[[2]],col="#00FF00",ylim=c(0,1000))
par(new=T)
plot(varList[[3]],col="#0000FF",ylim=c(0,1000))

f:id:wanko_sato:20170611100005p:plain
赤が1,000回、緑が5,000回、青が10,000回です。赤が突出して大きくなっているので、各点のデータがかなりばらついているのがわかります。赤のデータに引っ張られてほかの二つが比較しにくいので、赤を除いたプロットにするとこんな感じになります。
f:id:wanko_sato:20170611100202p:plain
こうしてみると、青(10,000回)の方がややばらつきが少なくなっているようです。学習精度の向上がある程度のところで頭打ちになるとはいえ、やはりデータが増えることによって少しは精度が向上しているのでしょう。後はコストと求める精度の兼ね合い、というところでしょうか。

予測結果の中央値

さて、予測結果の大まかな振る舞いがなんとなくわかってきました。1,000回予測させるとsinカーブは帯状の太い線になります。これはもともとの学習データが分散5の正規分布に従う、ばらつきを持ったデータだからです。ですから、そのばらつきを正しく反映した予測結果であろう、ということが、以上で示したプロットからもわかります。
であるならば、この予測値の、各x点における中央値を求めたら、もしかしたらきれいなsinカーブになるのじゃなかろうか?と思ってやってみたのが以下のプロットです。
※平均ではなく中央値なのは、最初に示したように外れ値がそこそこ頻繁に発生するためです。

sinCurv <- c()

for(i in 0:199){
  curv <- sin((2*pi/200)*i)*100
  sinCurv <- rbind(sinCurv,floor(curv))
}

として比較用のsinカーブのデータを作成しておき、

medianList <- lapply(seq(1,length(predDataList)),function(i){
  sapply(seq(1,200),function(j){
    floor(median(sapply(seq(1,1000),function(k){ predDataList[[i]][[k]][j] })))
  })
})

として各条件における各x点の中央値を求めておきます。そして、次のようにしてプロットします。

plot(medianList[[1]],ylim=c(-120,120))
par(new=T)
plot(sinCurv,ylim=c(-120,120),col="#FF0000")

早速結果を見てみましょう。

繰り返し1,000回の場合
f:id:wanko_sato:20170611101517p:plain
黒が予測値、赤が通常のsinカーブです。こうしてみると予測値がかなりがたがたになっていて、かつsinカーブからも大きくずれていることがわかります。

では、繰り返し5,000回の場合
f:id:wanko_sato:20170611101621p:plain
ずいぶんきれいに重なりました。とはいえ、後半にいくにつれてずれが目立っているようです。

最後に繰り返し10,000回の場合
f:id:wanko_sato:20170611101712p:plain

ほぼ一致。
これはすごい。
正直、自分でやっててびっくりしました。

今後

ということはですよ。
初期値を与え、その次に予測される値を1,000回発生させ、その中央値なり最頻値なりを採用し、それを初期値として与えて・・・ということを繰り返したら、学習データがばらついていてもきれいなsinカーブが描ける、ということでしょうか?
あるいは、その発想を言語データに発展させれば、学習データが多少不十分でもそれなりの結果が得られたりする、ということなのでしょうか?

すごく気になってきたぞ。
今度やってみよう。