カーネル法の概要をゆるふわに掴む(1/2)

カーネル法がよくわからなかったので「カーネル多変量解析」を読み始めました。が、難しい……。

少しずつ読みながら、内容をpython使いながら噛み砕きつつ書いてみようと思います。線形代数苦手な人向け。どこか間違ってたらごめんなさい。

この記事では、第一章カーネル法概要を見てみます。二次元に点をランダムにマッピングして、それを通る近似曲線をカーネル法で求めてみます。

このシリーズの目次はこちら

この記事のゴール

サンプルデータを用意して、それらの点を通りそうなグニャグニャした$${f(x)}$$を求めるのがゴールです

サンプルデータはこんな感じです。横軸$${x}$$で縦軸$${y}$$。

サンプルデータ

ゴールのイメージはこんな感じ。

青い線が関数、赤い点を通っている

なお、サンプルデータのコードはこんな感じで作りました(pandasとかnumpyとかのインストールはしといてください)。data.pyとして保存して実行すると一番上の画像がでます。

import numpy as np

def random_point(num, seed=1):
	np.random.seed(seed)
	return np.random.randn(num, 2)


if __name__ == '__main__':
	import matplotlib.pyplot as plt	
	x = random_point(12)
	plt.scatter(x[:, 0], x[:, 1], c='red', marker='o')
	plt.show()

カーネルを使った近似曲線(理屈)

$${x}$$の値を入れたら、いい感じに$${y}$$の値が出てくるような近似曲線$${f(x)}$$を作りたいです。その近似曲線$${f(x)}$$を作るために、$${x^{(i)}}$$の入力のときは$${y^{(i)}}$$という結果だったよというサンプルデータの集まり($${i=1,2,…,n}$$)を使います。

近似曲線$${f(x)}$$は、$${x}$$の関数が幾つか足し合わされたものになりそうですね。$${x}$$が1つしかない$${y = ax +b}$$とかだと無理そうです。たぶん$${x}$$の多項式になるかなぁと予想します。

ここで突然ですが、変数$${x}$$とベクトル$${x^{(i)}}$$との近さを示す関数$${k(x^{(i)}, x)}$$があるとします(これをカーネル関数と呼びます。中身は後で)。この関数をサンプルデータ分だけ足してつなげてみます。例えばサンプルデータ3つなら
$${f(x) = a_1k(x^{(1)},x) + a_2k(x^{(2)},x) +a_3k(x^{(3)},x)}$$
各$${k}$$の影響度を調整するために重み$${a_i}$$を各$${k}$$に掛けています。文字がいっぱい出てきていますが、$${a_i}$$も$${x^{(i)}}$$も固定値なので、これの変数は$${x}$$だけです。

この考えを3個じゃなくてn個のサンプルデータとすると
$${f(x) = \sum a_i k(x^{(i)},x)}$$
この中の変数は$${x}$$だけかつn項もあり、しかも各データ点からの近さを使って計算するのだから、$${\alpha = a_1,a_2,...a_n}$$を適切に設定することで近似曲線$${f(x)}$$が得られそうな感じがします。

ということで、「どうやったらサンプルデータを使って$${\alpha = a_1,a_2,...a_n}$$を適切に設定できるか」が論点になります。

$${\alpha}$$を適切に設定すると、各$${x^{(i)}}$$を使って計算された予想値$${f(x^{(i)})}$$と実値$${y^{(i)}}$$の差が小さくなるはずです。

例えば$${x^{(1)}}$$を使ったとき、$${y^{(1)}}$$の予想値は
$${f(x^{(1)}) = a_1 k(x^{(1)}, x^{(1)}) + a_2k(x^{(2)},x^{(1)})+....+a_nk(x^{(n)},x^{(1)})}$$
です。実際の値との誤差は$${y^{(1)}-f(x^{(1)})}$$になります。

だから「すべてのサンプルデータに対して、誤差の二乗の合計が小さくなる$${\alpha = a_1,a_2,...a_n}$$が決められたら、よい近似曲線が書けた」といえます(二乗にしたのは誤差の正負を無視するため)。

誤差を一気に計算するため、すべてのサンプルデータの予想値を一気に計算する行列を考えます。ここで行列出てきちゃいますが、よくみると上の予想値の式を縦に並べただけです。

$${K=\begin{pmatrix} k(x^{(1)},x^{(1)}) & k(x^{(2)},x^{(1)}) & ... & k(x^{(n)}, x^{(1)})\\k(x^{(1)},x^{(2)}) & k(x^{(2)},x^{(2)}) & ... & k(x^{(n)}, x^{(2)})\\ ... & ... & ... & ...\\k(x^{(1)},x^{(n)}) & k(x^{(2)},x^{(n)}) & ... & k(x^{(n)}, x^{(n)}) \end{pmatrix}}$$

$${K\alpha=\begin{pmatrix} k(x^{(1)},x^{(1)}) & k(x^{(2)},x^{(1)}) & ... & k(x^{(n)}, x^{(1)})\\k(x^{(1)},x^{(2)}) & k(x^{(2)},x^{(2)}) & ... & k(x^{(n)}, x^{(2)})\\ ... & ... & ... & ...\\k(x^{(1)},x^{(n)}) & k(x^{(2)},x^{(n)}) & ... & k(x^{(n)}, x^{(n)}) \end{pmatrix}\begin{pmatrix} a_1 \\ a_2 \\ ...\\ a_n \end{pmatrix} = \begin{pmatrix} y^{(pred)}_1 \\ y^{(pred)}_2 \\ ...\\ y^{(pred)}_n \end{pmatrix}}$$

ので、誤差は$${(y-K\alpha)}$$、二乗した誤差の合計は$${(y-K\alpha)^T(y-K\alpha)}$$です。この誤差合計を最小にする$${\alpha}$$を見つけたいわけです。

$${\alpha}$$で偏微分と式変形すると以下が得られます(説明略)。
$${\alpha=K^{-1}y}$$

ここで、$${K}$$はカーネル関数$${k(x, x')}$$とサンプルデータ$${x^{(i)}(i=1,2,…,n)}$$があれば作れます。よって$${K^{-1}}$$も(正則なら)作れます。$${y}$$はサンプルデータの答えなので与えられています。よって$${\alpha}$$が求められてしまうのです。

カーネルを使った近似曲線(実装)

では、一番最初に表示したサンプルデータに対してカーネル法を実装してみます。基本上の理論をそのままコードにするだけ。

その前に。いままで棚上げになっていたカーネル関数$${k(x, x')}$$を下記とします。ガウスカーネルと呼ぶそうです。第一章の時点では詳細はわかりません。いったんこのまま計算します。
$${k(x, x') = exp(-\beta||x-x'||^2)}$$
$${\beta}$$は適当に決めるパラメータなので、$${k(x^{(i)}, x)}$$は$${x}$$だけを変数とする関数です。

これを頭のサンプルデータにあうように実装するとこんな感じ。

import numpy as np

def gaus(x1, x2, beta=1):
	return np.exp(-1 * beta * ((x1 - x2) ** 2))

上記をkernel_model.pyファイルの頭に書いて、この下にKernelModelクラスを実装していきます。

class KernelModel():
	def fit(self, x, y):
		pass

	def predict(self, x):
		pass

fit関数はn個のサンプルデータ$${x^{(1)}, x^{(2)},….,x^{(n)}}$$と$${y^{(1)}, y^{(2)},….,y^{(n)})}$$を受けて、$${\alpha}$$を計算し、近次関数$${f(x)}$$を作ります。一方、predict関数は予想したい$${x'}$$を受けて、予想値$${y'}$$を返します。

fit関数で$${\alpha}$$を計算するには、まず$${K}$$を計算します。この例ではサンプルデータが12個なので、カーネル関数を12 x 12回計算して$${K}$$を作ります。

#KernelModel内
	def _K(self, x):
		dim = len(x)
		matrix = []
		for i in range(dim):
			for j in range(dim):
				matrix.append(self.k_func_(x[j], x[i]))
		matrix = np.array(matrix)
		matrix = matrix.reshape(dim, -1)
		return matrix

$${\alpha}$$は$${\alpha=K^{-1}y}$$なので、

#KernelModel内
	def _alpha(self, x, y):
		k = self._K(x)
		return np.linalg.inv(k).dot(y)

fit関数はこれで終わりです。

#KernelModel内
	def fit(self, x, y):
		self.x = x
		self.k_func_ = lambda x1, x2: gaus(x1, x2, beta=1)
		self.alpha_ = self._alpha(x, y)

xをself.xに保存しているのはpredictでも使うため。predictは求めていた$${f(x)}$$です。xを与えると、そのxと各サンプルデータの近さ$${k(x^{(i)}, x)}$$に重み$${a_i}$$を掛けて足し合わせることで$${y}$$を導きます。

#KernelModel内
	def predict(self, x):
		pred_y = 0
		for i in range(len(self.x)):
			pred_y += self.alpha_[i] * self.k_func_(self.x[i], x)
		return pred_y

つまり全体像はこんな感じ。

import numpy as np

def gaus(x1, x2, beta=1):
	return np.exp(-1 * beta * ((x1 - x2) ** 2))

class KernelModel():
	def fit(self, x, y):
		self.x = x
		self.k_func_ = lambda x1, x2: gaus(x1, x2, beta=1)
		self.alpha_ = self._alpha(x, y)

	def predict(self, x):
		pred_y = 0
		for i in range(len(self.x)):
			pred_y += self.alpha_[i] * self.k_func_(self.x[i], x)
		return pred_y

	def _alpha(self, x, y):
		k = self._K(x)
		return np.linalg.inv(k).dot(y)

	def _K(self, x):
		dim = len(x)
		matrix = []
		for i in range(dim):
			for j in range(dim):
				matrix.append(self.k_func_(x[j], x[i]))
		matrix = np.array(matrix)
		matrix = matrix.reshape(dim, -1)
		return matrix

試しに以下を実行すると、結構正解しています($${f(x^{(1)})}$$の予想値を計算して、$${y^{(1)}}$$と比較している)。

import data
d = data.random_point(12)
x = d[:, 0]
y = d[:, 1]
m = KernelModel()
m.fit(x, y)
print("real: ", x[0], " predict: ", m.predict(x[0]))
real:  -0.6117564136500754  predict:  -0.6117560863494873

結果

このpredict関数を表示してみましょう。

サンプルデータ点とf(x)

全ての点を通るように関数が作成されています。きれい。コードはこんな感じ。devideの数だけ$${(x, f(x))}$$を計算して並べてます。

import kernel_model
import data
import numpy as np
import matplotlib.pyplot as plt

d = data.random_point(12)
model = kernel_model.KernelModel()
model.fit(x=d[:, 0], y=d[:, 1])

x_min = -2
x_max = 2
devide = 500
p = np.linspace(x_min, x_max, devide)
plt.plot(p, [model.predict(p[k]) for k in range(devide)])
plt.scatter(d[:, 0], d[:, 1], c='red', marker='o')
plt.ylim(-1010)
plt.show()

ただ、この結果、過学習っぽいです。表示範囲伸ばしてみるとこんな感じ。他の点を予想しようとすると使えなさそうですね。

上の図の表示範囲を広げた

$${f(x)}$$がサンプルデータに合うように複雑な関数になりすぎているのです。この問題を解消するために、サンプルデータずばりは通らないけど、かなり近くを通るし自然な感じの$${f(x)}$$を作ります。

長くなったので、いったんここまで。次はその関数を作って、関数のパラメータをいじったりします。まだ第一章が終わらない……。
続き:https://note.com/tanuki_112/n/n233008df0639


ちなみにpythonのコード書きながら機械学習学ぶならこれがおすすめ。「カーネル多変量解析」にはコードはないので。


この記事が気に入ったらサポートをしてみませんか?