はじめに
タイトルの通りカーネル関数の話。内積のことを考えるとカーネル関数から作られるグラム行列は正定値でないとダメな気がしたがそんなことはなくて半正定値であればよい、それは何故か?という話。
いくつかの書籍に書いてある話なのでわざわざ記事にしなくてもよいかもしれないが、自分が抱いた疑問やその解消に至るまでの経緯などを含めて備忘録として残しておく。
なお、カーネル関数については以前書いた以下の記事でも触れているので参考までに。
カーネル関数と再生性
本題に入る前に前提となるカーネル関数の定義や対象とする空間および再生性という性質について簡単に述べておく。
まず集合上の2変数関数であって、対称性と半正定値性を有する関数をカーネル関数と呼ぶ。 これは、任意に対し行列目がのグラム行列を作ると、それが半正定値対称行列になることを意味する。
また、カーネル関数について、第2引数をで固定した1変数関数をと表記する。
そして、この1変数関数を用いて内積空間を以下のように定義する。
この空間の内積はとして、
と定義する。後述の零関数の例にもある通りの表現方法は複数存在する可能性がある(つまり、やが異なっていても同じになることがある)。それでも式(2)で定義する内積はwell-definedであり、の表し方によらず1つに定まる。
このように定義した内積空間では、任意のに対し以下のような再生性という性質を有する。
これは関数にを代入する操作が内積の形で表せることを意味し、この性質から様々な定理が導かれる。
ちなみに、このままでは完備性を有しておらずヒルベルト空間とは呼べない(と思われる*1)。このに対し完備化という操作を施し完備性を有した空間は再生核ヒルベルト空間になるが、今回の内容には絡まないのでこれ以上は触れない。
疑問点
このように定めた内積空間において
を考える(はを番目の要素に持つベクトル)。カーネル関数が半正定値ということはグラム行列が半正定値でにもなり得る。ということは、であればとなる以外のが存在することになる。
ところが、内積の定義を考えるとのはずで、それにもかかわらずがでなくとも式(4)をにできるのはなんか奇妙、というのが今回の論点。
疑問の解消
そもそもの話
考えてみればとはどういうことだろう?でというなら分かるが、それ以外に意味することはあるのだろうか?
これはベクトル空間に立ち返るとは零ベクトルであることを意味する。零ベクトルなので任意のに対して、となればよい。 関数なので表現方法が異なっていても入出力関係が変わらなければよい。すなわち、任意のに対してであればは零ベクトルと言える*2。これを達成するは何を入力してもを出力する関数、つまり零関数である。が零関数であれば、が全てである必要もない。
の証明
は自明なので のみ示す。 まず、について以下のコーシー・シュワルツの不等式が成り立つことを示す。
これはを考えると、
であり、これが任意の実数に対して成立するためには判別式が以下、すなわちとなる必要があるため、これを展開して式(5)が導かれる。
また、任意のに対しであればである。を式(3)の再生性と式(5)を用いて展開すると、
となる。そして、なのでとなりが導かれる。
具体例で検証
このことと式(4)よりとなるの組み合わせではは零関数であり、任意のについてとなる。
これをカーネル関数として2次の多項式カーネルを採用した場合を例にとって考えてみると、であれば任意のに対してが成り立つ。
ここまで見てもまだ直感と反する気がするので、一応このことをスクリプトを書いて確かめてみる。多項式カーネルなのでならとなる。そして、の零固有値に対応する固有ベクトルをとすればとなる。このを用いて作ったは果たして適当なに対してとなるのか、ということを確かめるスクリプトが以下。
import numpy as np def func(a, X, y, k): res = 0 for i in range(a.shape[0]): res += a[i] * k(y, X[i, :]) return res if __name__ == '__main__': np.random.seed(3407) d = 6 n = 30 X = np.random.randn(n, d) # 2次多項式カーネル k = lambda x, y: (1 + x @ y) ** 2 K = np.zeros((n, n)) for i in range(n): for j in range(n): K[i, j] = k(X[i, :], X[j, :]) print ("rank(K) =", np.linalg.matrix_rank(K)) # Kの零固有値に対応する固有ベクトルを抽出 # このベクトルaはa @ K @ a = 0を満たす l, U = np.linalg.eigh(K) a = U[:, np.abs(l) < 1e-12] # 入力を適当に生成 y = -50 + 100 * np.random.rand(d) for i in range(a.shape[1]): f = lambda y : func(a[:, i], X, y, k) print("f" + str(i) + "(x) =", f(y))
結果としては、
rank(K) = 28 f0(x) = -1.4438228390645236e-11 f1(x) = -1.4743761767022079e-12
演算誤差もあるので完全にとはならないが、ほぼになったといえる。また乱数のseedを変えて何度か試しても同じくらいの値になるので、とりあえず納得。
おわりに
最後に参考文献。冒頭に述べた以前書いた記事でも同様の参考文献を挙げたが今回もお世話なったので改めて記載。