交差検証法の説明と実装

良いモデルを選択したい!

与えられたデータに対して学習アルゴリズムを適用しようとするときに、どのようなモデルを選択するべきなのか、ということを考えなければなりません。例えば線形モデルを適用する場合は基底関数の種類や数を決めないといけないですし、過適合を防ぐために正則化項をモデルに付加している場合は正則化パラメータも調整しなければなりません。それでは、どのようにパラメータを調整したり、モデルの種類を選択すればよいのでしょうか?

そもそも私達の目標は、現在手元にあるデータセットを上手く説明できるモデルを見つけることではなく(そういうケースもあるかもしれませんが、一般的には)将来の未知なデータを予測することでしょう。これを汎化と呼び、将来データに対する予測誤差を汎化誤差と呼ぶことが出来ます。これは訓練標本に対する誤差とは異なることに注意が必要です。現在与えられている訓練標本に対する誤差を小さくするためには、パラメータの値を色々と変更し極端な値にしたり複雑なモデルを選択することでいくらでも小さく出来てしまうケースがあります。これは過適合を起こしてしまいますので好ましくありませんね。

交差検証法

とは言っても、将来データがもし現時点で与えられていれば、それをテスト標本とし、汎化誤差を求めることができますが、現時点ではそれはもちろん与えられていません(もしくはモデルを選択する上でテスト標本を選んではいけない設定を考えることが多いです)。そこで、学習データを幾つかの組に分割し、一部をモデル選択・パラメータ調整のためのデータセット(学習データ)とし、残りを汎化誤差を測るためだけに利用します(評価データ)。最もわかりやすいのは、与えられたデータを2つに分割し、片方で学習し、もう片方で誤差を評価します。これをホールドアウト検証と言います。

ホールドアウト検証では、学習データと評価データに分けてしまい、学習データの数が減ってしまいました。また、分割した2つのデータセットのうち、どちらを学習データとし、どちらを評価データとするかによって、結果が大きく変わってしまうこともあります。そこで、さらに洗練された方法として交差検証法があります。

交差検証法では、与えられているデータセットを2個以上のグループに別けます。例えば50サンプルを10サンプル毎の5グループに分けるような例が考えられます。

  • グループ1を評価用として、その他のグループで学習する
  • グループ2を評価用として、その他のグループで学習する
  • グループ3を評価用として、その他のグループで学習する
  • グループ4を評価用として、その他のグループで学習する
  • グループ5を評価用として、その他のグループで学習する

これによって、5つの誤差が計算でき、それらの平均値を今回のモデルの汎化誤差とします。このような場合を5-fold cross validationと呼びます。実際には10-foldが頻繁に用いられます。はじめの話に戻ると、それぞれのモデルに対して交差検証を行い、最も汎化誤差が小さいモデルを採用すべき、という考え方を取ります。

実装しよう!

それでは以下では具体例として、50個のサンプル標本を基に、交差検証法を行います。ここでは、ガウスカーネルモデルと{l2}ノルム正則化のモデルを利用し、ガウスカーネルのバンド幅{h}正則化パラメータ{\lambda}をどのような値に調整すればよいかを調べていきましょう。分割数は5と、4グループ(40サンプル)を用いて学習し、残りの1グループ(10サンプル)で評価します。これをすべてのグループに繰り返し行います。分割方法はランダムに行う必要があります。詳しくは実装コードを御覧ください。

各実験では、パラメータの値を変更した回数分だけ行います。パラメータを2つ調整するため、少し実装が複雑になります。それぞれのパラメータは{0.0001,0.001,0.01,0.1,1,10,100,1000,10000}の中から選択するようにしています。ですので実験数は5×8×8になり、実装ではそれらが3つの"for"に対応しています。

交差検証用のパッケージもありますが、ここでは理解を深めるために一歩一歩実装していきます。

# -*- coding: utf-8 -*-
import numpy as np
import matplotlib.pyplot as plt
import random

s = 50
N = 1000
x = np.matrix(np.linspace(-3,3,s)).T
X = np.matrix(np.linspace(-3,3,N)).T
pix = np.pi * x
y = np.cos(np.pi*x)+x+np.random.randn(s,1)

plt.plot(x,y,'bo',label='data samples')
plt.plot(X,np.cos(np.pi*X)+X,'r')

#preparing for cross validation
hhs = 2*np.power([0.0001,0.001,0.01,0.1,1,10,100,1000,10000],2)
ls = np.array([0.0001,0.001,0.01,0.1,1,10,100,1000,10000])
m = 5
randomlist = range(s)
random.shuffle(randomlist)
randomlist = np.array(randomlist) % 5
g=np.zeros([5,len(hhs),len(ls)])
xx = np.tile(np.power(x,2).T,(s,1))+np.tile(np.power(x,2),(1,s))-2*((x).dot(x.T))

#Cross Validation
for i in range(len(hhs)):
    K = np.exp(-xx/hhs[i])      
    for q in range(m):
        ki=K[randomlist!=q,:]
        kc=K[randomlist==q,:]
        yi=y[randomlist!=q].T
        yc=y[randomlist==q].T
        for j in range(len(ls)):
            n = np.sum(randomlist!=q)
            x2 = np.power(x[randomlist!=q],2)
            t = (np.linalg.inv(ki.T.dot(ki)+ls[j]*np.eye(s))).dot(ki.T.dot(yi.T))
            f = kc.dot(t)
            g[q,i,j] = np.mean(np.power((f-yc.T),2))

G = np.mean(g,0)
smallest = np.unravel_index(G.argmin(), G.shape)
h = hhs[smallest[0]]
l = ls[smallest[1]]

交差検証法で選択されたパラメータはバンド幅が{2\times 1^2}正則化パラメータは0.01となりました。それらの値を用いて、関数を図示していきましょう。

XX = np.tile(np.power(x,2).T,(N,1))+np.tile(np.power(X,2),(1,s))-2*((X).dot(x.T))
kk = np.exp(-xx/h)
KK = np.exp(-XX/h)
TT = (np.linalg.inv(kk.T.dot(kk)+l*np.eye(s))).dot(kk.T.dot(y))
F = KK.dot(TT)

#Plot the graphs
plt.cla()
plt.plot(x,y,'bo',label='data samples')
plt.plot(X,np.cos(np.pi*X)+X,'r',label='y=cos(pi*x)+x')
plt.plot(X,F,'g-',label='After cross validation')
plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.)

下記が交差検証法で得られたパラメータを用いたプロットです。赤線と緑線が近しいことが視覚的に確認でき、ある程度成功しているのではないかと考えることが出来ますね。
f:id:decompose:20160528004725p:plain

さらに勉強するには

今回の記事を作成する上では以下の書籍が非常に参考になりました。是非覗いてみてください。
『イラストで学ぶ機械学習:最小二乗法による識別モデル学習を中心に』(第4章)
『はじめてのパターン認識』(第2章)
『Introduction to Statistical Learning』(第5章)