カーネル密度推定を使った教師あり学習
この記事は機械学習の初心者が書いています。
予めご了承ください。
この記事では、着想の背景を数式を用いて説明します。
確率密度と確率
カーネル密度推定はカーネル関数で確率密度関数を推定することでした。
それでは確率密度関数とは、一体なんだったでしょうか?
確率密度関数は出やすさの分布を表しています。
事象Aについて、値xで確率密度が高いとは事象Aが起きたとき、そのときの値がxである確率が相対的に高いということです。
「事象A」を「ラベル0」と置き換えてみましょう。
「値xでは、ラベル0である確率密度が高い」とは「あるデータがラベル0だったとき、そのデータが値xである確率が相対的に高い」という意味になります。
ここで、確率密度≠確率であることに注意しなければいけません。
確率密度は「特定の事象に対する相対的な出やすさ」であり、他の事象の確率密度と比較できる保障はありません。
そもそも連続データにおいて、特定の値が出る確率は定義されていません。
ある程度の幅がないと確率は計算できないのです。
P(X=x)=0 \\ P(X \leqq x)=p
確率密度関数を積分すると、その区間(2次元以上なら領域)における確率を求めることができます。
つまり特定の値「の近く」の確率は計算できるわけです、おそらく。
値xの確率密度が高ければ値x「の近く」が出る確率は高く、値xの確率密度が低ければ値x「の近く」が出る確率も低くなります。
一つ例をあげて考えてみたいと思います。
値xでは、ラベル0の確率密度よりラベル1の確率密度のほうが高いです。
このとき、
- ラベルが0のときに値x「の近く」が出る確率
- ラベルが1のときに値x「の近く」が出る確率
どちらが高いでしょうか?
厳密ではありませんが、直感的に後者のほうが高いと思われます(雑)。
今後この直感が正しいものとして議論を進めます。
「ラベルが0」を「y=0」、「ラベルが1」を「y=1」と書き直しましょう。
すると、ここまでの話は
値xにおけるラベル0の確率密度 \leqq 値xにおけるラベル1の確率密度 \\
\Rightarrow y=0のときに値xが出る確率 \leqq y=1のときに値xが出る確率 \\
\Rightarrow P(x|y=0) \leqq P(x|y=1)
とまとめられます。
ただし、正確には値x「の近く」です。
条件付き確率とベイズの定理
値xについて、
P(y=0|x) \leqq P(y=1|x)
が成り立てば、ラベル0ではなくラベル1を割り当てるのが妥当だと言えます。
したがって「値xを取り出す」という条件のもとで「ラベル0 or ラベル1である」確率、条件付き確率を求めることができたら優勝です。
この不等式をベイズの定理を用いて書き直してみます。
P(y=0|x) \leqq P(y=1|x) \\ \Leftrightarrow
\frac{P(x|y=0)P(y=0)}{P(x)} \leqq \frac{P(x|y=1)P(y=1)}{P(x)}
分母のP(x)は共通していて、0以上です。
結局、
P(x|y=0)P(y=0) \leqq P(x|y=1)P(y=1)
を示すことができれば
P(y=0|x) \leqq P(y=1|x)
が成り立つことが言えます。
いまカーネル密度推定を使って
P(x|y=0) \leqq P(x|y=1)
であることがわかっています。
したがってP(y=0)とP(y=1)の値を知ることができれば決着がつきます。
ラベル比の推定
P(y=0)は「あるデータを選んだところ、それがラベル0である確率」と解釈できます。
母集団のP(y=0)とP(y=1)を求めることは容易ではありません。
そこで、教師データのラベルの構成比をP(y=0)とP(y=1)の推定値にします。
100件の教師データのうち、40件がラベル0、60件がラベル1なら
P(y=0)=0.4\\ P(y=1)=0.6
と推定します。
実は、私が過去の記事で実装した分類器ではP(y=0)とP(y=1)の影響を無視していました。
「各ラベルの比が等しい」という強い仮定を置いていたのです。
再実装
これまでの説明をもとに、もう一度オブジェクト指向の分類器を実装します。
import numpy as np
class GKDEClassifier(object):
def __init__(self, bw_method="scotts_factor", weights="None"):
# カーネルのバンド幅
self.bw_method = bw_method
# カーネルのウェイト
self.weights = weights
def fit(self, X, y):
# yのラベル数
self.y_num = len(np.unique(y))
# ラベル比の計算
self.label, y_count = np.unique(y, return_counts=True)
self.y_rate = y_count/y_count.sum()
# 推定した確率密度関数を格納するリスト
self.kernel_ = []
# 確率密度関数を格納
for i in range(self.y_num):
kernel = gaussian_kde(X[y==self.label[i]].T)
self.kernel_.append(kernel)
return self
def predict(self, X):
# 予測ラベルを格納するリスト
pred = []
# テストデータのラベル別確率を格納するndarray
self.p_ = np.empty([self.y_num, len(X)])
# ラベル別確率を格納
for i in range(self.y_num):
self.p_[i] = self.kernel_[i].evaluate(X.T)
# ラベル比をかける
for j in range(self.y_num):
self.p_[j] = self.p_[j] * self.y_rate[j]
# 予測ラベルの割り振り
for k in range(len(X)):
pred.append(self.label[np.argmax(self.p_.T[k])])
return pred
以下、追加した箇所と修正した箇所について説明します。
ラベル比の計算
self.label, y_count = np.unique(y, return_counts=True)
self.y_rate = y_count/y_count.sum()
fitメソッドに教師データのラベル比を計算する部分を追加しました。
ラベルごとの登場回数y_countを合計値で割ることで、y_rateの合計値を1にしています。
合計値で割らずにy_countをそのまま利用しても、結果は変わりません。
さらにラベルの内訳(0や1、あるいは文字列)をリストlabelとして出力します。(重要)
確率密度関数の計算
新しいコードがこちら。
種々のラベルに合致するよう修正しました。
for i in range(self.y_num):
kernel = gaussian_kde(X[y==self.label[i]].T)
self.kernel_.append(kernel)
これが今までのコードです↓↓
kernel = gaussian_kde(X[y==i].T)
もともとは「ラベルがiのデータ」を指定していましたが、新たに「i種類目のラベルのデータ」を指定するように変更しました。
出力したlabelから指定することで、非負整数ではないラベル(文字列など)に対応させます。
ラベル比を反映
for j in range(self.y_num):
self.p_[j] = self.p_[j] * self.y_rate[j]
predictメソッドに確率密度にラベル比をかけ合わせる部分を追加しました。
予測ラベルの割り振り
新しいコードがこちら。
for k in range(len(X)):
pred.append(self.label[np.argmax(self.p_.T[k])])
リストlabelから割り当てラベルを指定することで、非負整数以外のラベルにも対応させました。
その他
predictメソッド内のごちゃごちゃしていた部分をnumpyを使って書き直しました。
先にndarrayを作成してから結果を代入することで、可読性と計算スピードの向上を図っています。
最後に
自分の脳内をなんとか最後まで書き終えることができました。
お付き合いいただき、ありがとうございました。
記事を読んだ方に少しでも面白さを感じていただければ幸いです。
(2020/8/5 修正)