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

東京で働くデータサイエンティストのブログ

元祖「銀座で働くデータサイエンティスト」です / 道玄坂→銀座→(八重洲)→東京23区内某所

「正答率100%」になってしまう機械学習モデルの例を挙げてみる

機械学習 R 雑感

何か僕がシンガポールに出張している間に妙なニュースが流れていたようで。



(※記事そのものへのリンクは控えました)


見る人が見れば「ああこれはleakageだな」と一瞥して終わるところなんですが、そもそもleakageってどういうことなのかピンと来ない人もいるかと思いますので、以前取り上げたデータ分析題材を例にとって実演してみようと思います。お題はこちら。



何故これを選んだかというと、このテニス四大大会データには上記で話題になっていた"GeneID"に良く似た「対戦カード2選手それぞれの名前」という特徴量が含まれているからです。ということで、Rを使って適当にやってみましょう。


Disclaimer: 元ネタの論文は時間がないこともあって真面目に読んでいませんので、あくまでも「leakageがとんでもない副作用をもたらすケースの再現例」をここでは論じています。よってこの記事が元ネタの正確な解説でも何でもないただの与太話である旨悪しからずご了承下さい)


データを取ってくる



僕のGitHubアカウントに適当に前処理しておいたUCI Machine Learning Repositoryの"Tennis"というテニス四大大会の対戦データを置いてあるので、DLしてきてRのワーキングディレクトリに置いてください。その上で、以下のように前処理しておきましょう。

> d<-read.table('men.txt',sep='\t',header=T)
> d<-d[,-c(16,17,18,19,20,21,34,35,36,37,38,39)]
> d$Result<-as.factor(d$Result)


これで上記の"GeneID"変数同様にleakage要因になり得る「対戦カード2選手のそれぞれの名前」以外の要因*1は除外されたことになります。なお目的変数となる勝敗データは予めカテゴリ変数に変換してあります。


適当に基礎分析してみる


ということで、一応念のため簡単に数値型の説明変数同士の相関を図示してみます。

> image(cor(d[,-c(1,2,3)]))

f:id:TJO:20160127232147p:plain


一部に多重共線性を疑わせる説明変数のペアが見えますが、とりあえず問題はなさそうです。ここまでは普通の多変量モデルを推定する流れと同じなので、いよいよ懸案の機械学習モデルの方に取り掛かることにしましょう。


元ネタを真似てプレイヤー名を説明変数に入れたままにし、尚且つテストデータではなく学習(訓練)データを分類してみる


某友人のブログによると、元ネタではどうやらCV(交差検証)をちゃんとやっていない模様です。



ということで、既に述べたように"GeneID"を入れるのと同じ気分でテニス四大大会データセットでもPlayer1, Player2という「対戦カードのプレイヤー双方の名前」つまりIDに当たるパラメータを入れたままにして、尚且つCVもやらないという感じを再現するようにテストデータではなく学習(訓練)データをロジスティック回帰で分類してみます。

> d.glm<-glm(Result~.,d,family=binomial)
> table(d$Result,round(predict(d.glm,newdata=d[,-3],type='response'),0))
   
      0   1
  0 251   0
  1   0 240

# 完全に100%合ってますね(笑)


あははははは(汗)。確かに正答率100%になっています。これは典型的なleakageの例そのものなんですが、冒頭に引用したツイートの内容から察するに元ネタでもこれに近いことが起きていたのではないかと思われます。ちなみに、いかな訓練データに対する予測だから正答率は高くて当たり前といえども、試しに「対戦カードのプレイヤー双方の名前」を削除して同様にロジスティック回帰でやり直してみるとこうなります。

> d.glm.true<-glm(Result~.,d[,-c(1,2)],family=binomial)
> table(d$Result,round(predict(d.glm.true,newdata=d[,-c(1,2,3)],type='response'),0))
   
      0   1
  0 238  13
  1  14 226

# 27件が誤判定されている


ということで、ある意味当たり前ですがさすがに正答率100%になるなんてことはありません。つまり、CVしてないというだけでは説明がつかないようなことが起きているということが言えると思われます。なお、同様のことをSVMでやってみると以下のようになります。

> library(e1071)
> d.svm<-svm(Result~.,d,kernel='linear')
> table(d$Result,predict(d.svm,newdata=d[,-3]))
   
      0   1
  0 251   0
  1   1 239
> d.svm.true<-svm(Result~.,d[,-c(1,2)],kernel='linear')
> table(d$Result,predict(d.svm.true,newdata=d[,-c(1,2,3)]))
   
      0   1
  0 238  13
  1  15 225


あえてここでは線形カーネルSVMで回してますが、試しにチューニングなしのガウシアンカーネルSVMでやるとデタラメな結果になるのでなかなか面白いです*2。興味ある方はご自分でどうぞ(笑)。


何故こうなったのか


ともあれ、ロジスティック回帰の結果はかなり異様であるということはこれで分かりました。では、何故こうなったんでしょうか? その理由は簡単で、このテニス四大大会データセットにおいては目的変数と「対戦カードのプレイヤー双方の名前」がほぼ一対一対応するからです。実際にデータを見てみると、

> nrow(d[,1:2])
[1] 491
> nrow(unique(d[,1:2]))
[1] 485

# 491件のデータのうち485件のデータが当該期間内で1回きりの対戦カードで占められている


ということで対戦カードのプレイヤー双方の名前さえ分かれば自動的に大半の対戦カードの勝敗が分かってしまうという状況になっています。そこで試しに「対戦カードのプレイヤー双方の名前」だけを説明変数として(即ちその他の有用な説明変数全てを削除して)、勝敗データを目的変数として予測するロジスティック回帰モデルを推定するとこうなります。

> d.glm.name<-glm(Result~.,d[,1:3],family=binomial)
> table(d$Result,round(predict(d.glm.name,newdata=d[,1:2],type='response'),0))
   
      0   1
  0 247   4
  1   2 238

# たったの6件しか誤判定していない


ということで、いかに「対戦カードのプレイヤー双方の名前」が無駄に訓練データに対する正答率の向上=訓練誤差の低下に猛烈に貢献しているかが分かるかと思います。そしてこれこそが、"GeneID"という変数を含んでしまった元ネタの学習モデルの「(訓練データに対する)正答率ほぼ100%」という結果をもたらした要因であろうと推測されます。


なお駄目押しということで「対戦カードのプレイヤー双方の名前」を説明変数に含めてしまった場合の偏回帰係数をチェックするとこうなっています。

> summary(d.glm)

Call:
glm(formula = Result ~ ., family = binomial, data = d)

Deviance Residuals: 
       Min          1Q      Median          3Q         Max  
-7.228e-06  -2.409e-06  -2.110e-08   2.409e-06   8.367e-06  

Coefficients: (47 not defined because of singularities)
                                Estimate Std. Error z value Pr(>|z|)
(Intercept)                    1.184e+02  4.986e+06       0        1
Player1A.Kuznetsov            -3.999e+01  1.553e+06       0        1
Player1A.Man0rino             -2.280e+01  1.667e+06       0        1
Player1A.Ramos                -1.334e+01  2.653e+06       0        1
Player1A.Seppi                 2.574e+01  1.920e+06       0        1
Player1A.Ungur                 6.009e+01  1.841e+06       0        1
Player1Adrian Man0rino        -2.082e+01  2.717e+06       0        1
Player1Adrian Ungur            1.062e+01  3.510e+06       0        1

# ... (中略) ...

Player1B.Becker               -6.231e+01  1.989e+06       0        1
Player1B.Kavcic                1.176e+01  2.618e+06       0        1
Player2Wayne Odesnik                  NA         NA      NA       NA
Player2X.Malisse                      NA         NA      NA       NA
Player2Xavier Malisse          3.738e+00  1.531e+06       0        1
Player2Y-H.Lu                         NA         NA      NA       NA
Player2Y-T.Wang                       NA         NA      NA       NA
Player2Yen-Hsun Lu             2.906e+01  2.002e+06       0        1
Player2Ze Zhang                       NA         NA      NA       NA
FSP.1                         -3.950e-01  2.790e+04       0        1
FSW.1                          5.260e-01  2.748e+04       0        1
SSP.1                                 NA         NA      NA       NA
SSW.1                         -3.849e-01  2.887e+04       0        1
ACE.1                          6.963e-01  2.728e+04       0        1
DBF.1                         -5.881e-01  6.988e+04       0        1
WNR.1                          7.539e-02  2.272e+04       0        1
UFE.1                          2.598e-01  1.165e+04       0        1
BPC.1                          4.117e+00  9.451e+04       0        1
BPW.1                          1.484e+00  4.416e+04       0        1
NPA.1                         -1.009e-01  3.825e+04       0        1
NPW.1                         -1.488e-02  4.079e+04       0        1
FSP.2                         -9.991e-01  4.887e+04       0        1
FSW.2                         -5.386e-01  2.570e+04       0        1
SSP.2                                 NA         NA      NA       NA
SSW.2                          1.611e-02  4.715e+04       0        1
ACE.2                         -7.294e-01  2.171e+04       0        1
DBF.2                         -4.960e-01  9.111e+04       0        1
WNR.2                         -3.902e-01  8.420e+03       0        1
UFE.2                          2.237e-01  1.491e+04       0        1
BPC.2                         -6.438e+00  9.798e+04       0        1
BPW.2                          1.426e-01  4.375e+04       0        1
NPA.2                         -1.442e-01  6.894e+04       0        1
NPW.2                          4.440e-01  7.039e+04       0        1

(Dispersion parameter for binomial family taken to be 1)

    Null deviance: 6.8042e+02  on 490  degrees of freedom
Residual deviance: 3.9260e-09  on  74  degrees of freedom
AIC: 834

Number of Fisher Scoring iterations: 25


物の見事にランク落ちを起こしまくってあちこちNAだらけ&p値が1とかいう結果になっていて、モデル自体が完全にめちゃくちゃです*3。ということで、偏回帰係数や推定パラメータさえチェックすればすぐ分かることかなと思う次第です*4


もちろん、このようなID的な変数がきちんと説明変数として機能するケースは幾らでも世の中ありますが、それは「個々のIDごとに十分な数の繰り返し測定を含む場合」です。つまり、今回のテニス四大大会のデータで言えば「同一カードでの対戦が5回とか10回とか当該期間内にある場合」なら説明変数に含めても良いわけです。けれども、個々のIDがほぼ1回ずつしか出てこないような場合には使ってはいけないものだと言って良いでしょう。


最後に、まともにやったらどういう結果になるかは過去記事で紹介済みなのでそちらをお読み下さい。



まずleakageにつながるプレイヤー名を説明変数から削除し、ごくごく手抜きながらもCVとしてholdoutを用意した上で訓練データで学習モデルを作った上でholdoutに対するテスト正答率を見るというやり方をした上で、最終的に到達し得るテスト正答率が93.6%程度とかそんな感じです。やっぱり100%なんて結果は普通はお目にかかれないものかと*5


教訓

  • そもそもCV(交差検証)はどれほど簡単でもいいのでやりましょう
  • データセットの各行を表すIDのような変数はleakageに直結しやすいので繰り返しの有無の確認を含めて扱いには気をつけましょう


ほとんど当たり前の話ですが、ぶっちゃけ昔の現場で全力で「CVなしで訓練誤差だけ低いの見せてドヤ顔」という事例も見たことありますし、超絶スーパー分類器が出来上がってやったー!と思ったものの念のため確認したらやっぱりleakageがあってぬか喜びと判明してガックリなんていうのは他ならぬ僕自身が一度だけやらかしたことがあります。皆様気をつけましょう。。。

*1:leakageになり得るもの

*2:特に後半でやっているような「プレイヤー名だけを説明変数にした場合」とか

*3:そもそも本来まともに機能する説明変数であるはずの他の測定指標系までもが全てp値が1になるなど影響は甚大

*4:SVMだとパラメータ推定結果は出しようがないので分からないかもしれませんが

*5:MNISTみたいに方法論が確立しているような題材ではまた別ですが