Scikit-Learnを用いたPythonによるカーネル密度推定

この記事は、Pythonの機械学習ライブラリ scikit-learn を用いたカーネル密度推定について紹介するものである。

カーネル密度推定(Kernel density estimation, KDE)は、与えられた確率変数の確率密度関数を推定するノンパラメトリックな方法である。発見者の名前にちなんでParzen-Rosenblatt Window法という伝統的な名前でも呼ばれます。

未知のソース分布からの確率変数の独立同分布(i.i.d)観測値 (x1,x2,…,xn)(x1,x2,…,xn)(x_1,x_2,\ldots,x_n) が与えられたとき、カーネル密度推定値は、次式で与えられます。

p(x)=1nhΣnj=1K(x-xjh)p(x)=1nhΣj=1nK(x-xjh)
p(x) = \frac{1}{nh} \Sigma_{j=1}^{n}K(\frac{x-x_j}{h})

ここで、K(a)K(a)はカーネル関数、hhhは平滑化パラメータで、帯域幅とも呼ばれます。様々なカーネルについては後述しますが、計算を理解するためだけに、簡単な例を見てみましょう。

計算例

標本点[-2,-1,0,1,2]があり、線形カーネルが次式で与えられるとする。K(a)=1-|a|hK(a)=1-|a|hK(a)= 1-93frac{|a|}{h} and h=10h=10h=10.

xj=[−2−1012]|0−xj|=[21012]|0−xjh|=[0.20.100.10.2]K(|0−xjh|)=[0.80.910.90.8]xj=[−2−1012]|0−xj|=[21012]|0−xjh|=[0. 20.100.10.2]K(|0−xjh|)=[0.80.910.90.8]





x
j

</mtd

=
</mtd

[の場合。
</mtd

のように
2</mn
</mtd

のように
1</mn
</mtd

0</mn
</mtd

1の場合
</mtd

2の場合
</mtd

]のようになります。
</mtd
</mtr


の場合。
|のようになります。
</mrow
0</mn
となります。

x
j
</msub
の場合。
|のようになります。
</mrow
</mtd

=となります。
</mtd

[の場合。
</mtd

2</mn
</mtd

1の場合
</mtd

0の場合
</mtd

1の場合
</mtd

2の場合
</mtd

]のようになります。
</mtd
</mtr


の場合。
|のようになります。
</mrow


0
となります。

x
j


h


|のようになります。
</mrow
</mtd

=となります。
</mtd

[の場合。
</mtd

0.2の場合
</mtd

0.1の場合
</mtd

0とする。
</mtd

0.1の場合
</mtd

0.2の場合
</mtd

]のようになります。
</mtd
</mtr


Kの場合
()
の場合。
|のようになります。
</mrow


0
となります。

x
j


h


|のようになります。
</mrow
)となります。
</mtd

=とする。
</mtd

[の場合。
</mtd

0.8の場合
</mtd

0.9の場合
</mtd

1</mn
</mtd

0.9の場合
</mtd

0.8の場合
</mtd

]のようになります。
</mtd
</mtr

上記をp(x)p(x)の式に代入する。

p(0)=1(5)(10)(0.8+0.9+1+0.9+0.8)=0.088p(0)=1(5)(10)(0.8+0.9+1+0.9+0.8)=0.088
p(0) = \frac{1}{(5)(10)} ( 0.8+0.9+1+0.9+0.8 ) = 0.088

Python によるカーネル密度推定

Pythonでカーネル密度推定を計算する方法はいくつかあるが、ここでは人気のある機械学習ライブラリ scikit-learn を使うことにする。以下のライブラリをコードにインポートする。

import numpy as np
import matplotlib.pyplot as plt
from sklearn.neighbors import KernelDensity
from sklearn.model_selection import GridSearchCV


合成データ

カーネル密度推定を実演するために、2種類の分布から合成データを作成した。1つは非対称対数正規分布、もう1つはガウス分布です。以下の関数は2000点のデータを返します。

def generate_data(seed=17):
    # Fix the seed to reproduce the results
    rand = np.random.RandomState(seed)
    x = []
    dat = rand.lognormal(0, 0.3, 1000)
    x = np.concatenate((x, dat))
    dat = rand.normal(3, 1, 1000)
    x = np.concatenate((x, dat))
    return x


以下のコードでは、この点を x_train に格納する。これらの点の散布図を y 軸に沿って作成するか、これらの点のヒストグラムを作成することができる。

x_train = generate_data()[:, np.newaxis]
fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(10, 5))
plt.subplot(121)
plt.scatter(np.arange(len(x_train)), x_train, c='red')
plt.xlabel('Sample no.')
plt.ylabel('Value')
plt.title('Scatter plot')
plt.subplot(122)
plt.hist(x_train, bins=50)
plt.title('Histogram')
fig.subplots_adjust(wspace=.3)
plt.show()


Scikit-LearnのKernelDensityを使用する。

推定密度関数の形状を求めるには、互いに等距離にある点の集合を生成し、各点でカーネル密度を推定すればよい。テスト点は次式で与えられる。

x_test = np.linspace(-1, 7, 2000)[:, np.newaxis]


さて、以下のコードに示すように、 KernelDensity オブジェクトを生成し、 fit() メソッドを用いて各サンプルのスコアを求めます。KernelDensity()メソッドは、2つのデフォルトパラメータ、すなわちkernel=gaussianbandwidth=1` を使用します。

model = KernelDensity()
model.fit(x_train)
log_dens = model.score_samples(x_test)


分布の形状は、以下のように各ポイントの密度スコアをプロットすることによって見ることができます。

plt.fill(x_test, np.exp(log_dens), c='cyan')
plt.show()


Bandwidth パラメーターを理解する

前の例は、密度関数のあまり印象的な推定ではありませんでしたが、これは主にデフォルトのパラメータに起因するものです。帯域幅が密度推定にどのような影響を与えるか、さまざまな値で実験してみましょう。

bandwidths = [0.01, 0.05, 0.1, 0.5, 1, 4]
fig, ax = plt.subplots(nrows=2, ncols=3, figsize=(10, 7))
plt_ind = np.arange(6) + 231


for b, ind in zip(bandwidths, plt_ind):
    kde_model = KernelDensity(kernel='gaussian', bandwidth=b)
    kde_model.fit(x_train)
    score = kde_model.score_samples(x_test)
    plt.subplot(ind)
    plt.fill(x_test, np.exp(score), c='cyan')
    plt.title("h="+str(b))


fig.subplots_adjust(hspace=0.5, wspace=.3)
plt.show()


帯域幅を大きくすると、より滑らかな推定になることがはっきりとわかります。帯域幅の値が非常に小さいと、とげとげしい曲線になり、値が非常に大きいと、非常に一般化された滑らかな曲線になり、重要な詳細が欠落します。このパラメータにバランスの取れた値を選択することが重要です。

帯域幅パラメーターのチューニング

scikit-learnライブラリは、クロスバリデーションによってbandwidthパラメータを調整することができ、データの対数尤度を最大化するパラメータ値を返します。これを実現するための関数がGridSearchCV()であり、この関数ではbandwidth` パラメータに異なる値を指定することができる。

bandwidth = np.arange(0.05, 2, .05)
kde = KernelDensity(kernel='gaussian')
grid = GridSearchCV(kde, {'bandwidth': bandwidth})
grid.fit(x_train)


最適なモデルは, GridSearchCV オブジェクトの best_estimator_ フィールドを用いて取得することができます.

ここでは、ガウシアンカーネルを用いた最適なカーネル密度推定を見て、bandwidth の値も出力してみます。

kde = grid.best_estimator_
log_dens = kde.score_samples(x_test)
plt.fill(x_test, np.exp(log_dens), c='green')
plt.title('Optimal estimate with Gaussian kernel')
plt.show()
print("optimal bandwidth: " + "{:.2f}".format(kde.bandwidth))


optimal bandwidth: 0.15


さて、この密度推定はデータを非常にうまくモデル化しているように見えます。プロットの前半は対数正規分布と一致し、プロットの後半は正規分布を非常によくモデル化しています。

密度推定のためのさまざまなカーネル

scikit-learn` では、異なるカーネル関数を用いてカーネル密度推定を行うことができます。

    1. kernel ='cosine':      K(a;h)∝cos(πa2h) if |a| <hk(a;h)∝cos⁡(πa2h) if |a|<hk(a;h) ‘epanechnikov’,=”” ‘exponential’,=”” ‘gaussian’,=”” ‘linear’,=”” ‘tophat’]=”” (- rac{|a|}{h})=”” ( rac{\pi=”” )=”” +=”” -=”” 0.=”” 0.1)[:,=”” 1=”” 2,=”” 2.=”” 231=”” 3.=”” 4.=”” 5.=”” 6.=”” 7))=”” <=”” <div=”” \cos=”” \exp=”” \exp(- rac{a^2}{2h^2})=”” rac{a^2}{h^2}=”” rac{|a|}{h}=”” \propto=”” ext=”” :="" ```=""kernel=”tophat” a=”” all=”” along=”” and=”” around=”” ax=”plt.subplots(nrows=2,” a}{2h})=”” below=”” building=”” c=”blue” class=”lazyload-wrapper” code=”” density=”” entire=”” estimate=”” example,=”” fig,=”” fig.subplots_adjust(hspace=”0.5,” figsize=”(10,” for=”” h=”” if=”” in=”” ind=”” is=”” k(a;h)∝1=”” k(a;h)∝1−a2h2k(a;h)∝1−a2h2k(a;h)=”” k(a;h)∝1−|a|h=”” k(a;h)∝exp(−a22h2)k(a;h)∝exp⁡(−a22h2)k(a;h)=”” k(a;h)∝exp(−|a|h)k(a;h)∝exp⁡(−|a|h)k(a;h)=”” k,=”” kde_model=”KernelDensity(kernel=k)” kde_model.fit([[0]])=”” kernels=”[‘cosine’,” means=”” model=”” ncols=”3,” next,=”” none])=”” none],=”” np.exp(score),=”” of=”” one=”” only=”” plot=”” plt.fill(np.arange(-2,=”” plt.show()=”” plt.subplot(ind)=”” plt.title(k)=”” plt_ind=”np.arange(6)” plt_ind):=”” points=”” process:=”” sample=”” score=”kde_model.score_samples(np.arange(-2,” shows=”” simple=”” the=”” them.=”” these=”” this=”” to=”” understand=”” using=”” value,=”” way=”” work=”” wspace=”.3)” y-axis.=”” zero=”” zip(kernels,=”” {=”” |a|=”” |a|<hk(a;h)=”” |a|<hk(a;h)∝1=”” |a|

Experimenting With Different Kernels

Let’s experiment with different kernels and see how they estimate the probability density function for our synthetic data.

We can use GridSearchCV(), as before, to find the optimal bandwidth value. However, for cosine, linear, and tophat kernels GridSearchCV() might give a runtime warning due to some scores resulting in -inf values. One possible way to address this issue is to write a custom scoring function for GridSearchCV().

In the code below, -inf scores for test points are omitted in the my_scores() custom scoring function and a mean value is returned. This is not necessarily the best scheme to handle -inf score values and some other strategy can be adopted, depending upon the data in question.

def my_scores(estimator, X):
    scores = estimator.score_samples(X)
    # Remove -inf
    scores = scores[scores != float('-inf')]
    # Return the mean values
    return np.mean(scores)


kernels = ['cosine', 'epanechnikov', 'exponential', 'gaussian', 'linear', 'tophat']
fig, ax = plt.subplots(nrows=2, ncols=3, figsize=(10, 7))
plt_ind = np.arange(6) + 231
h_vals = np.arange(0.05, 1, .1)


for k, ind in zip(kernels, plt_ind):
    grid = GridSearchCV(KernelDensity(kernel=k),
                        {'bandwidth': h_vals},
                        scoring=my_scores)
    grid.fit(x_train)
    kde = grid.best_estimator_
    log_dens = kde.score_samples(x_test)
    plt.subplot(ind)
    plt.fill(x_test, np.exp(log_dens), c='cyan')
    plt.title(k + " h=" + "{:.2f}".format(kde.bandwidth))


fig.subplots_adjust(hspace=.5, wspace=.3)
plt.show()


The Final Optimized Model

The above example shows how different kernels estimate the density in different ways. One final step is to set up GridSearchCV() so that it not only discovers the optimum bandwidth, but also the optimal kernel for our example data. Here is the final code that also plots the final density estimate and its tuned parameters in the plot title:

grid = GridSearchCV(KernelDensity(),
                    {'bandwidth': h_vals, 'kernel': kernels},
                    scoring=my_scores)
grid.fit(x_train)
best_kde = grid.best_estimator_
log_dens = best_kde.score_samples(x_test)
plt.fill(x_test, np.exp(log_dens), c='green')
plt.title("Best Kernel: " + best_kde.kernel+" h="+"{:.2f}".format(best_kde.bandwidth))
plt.show()


Conclusion

Kernel density estimation using scikit-learn‘s library sklearn.neighbors has been discussed in this article. The examples are given for univariate data, however it can also be applied to data with multiple dimensions.

While being an intuitive and simple way for density estimation for unknown source distributions, a data scientist should use it with caution as the curse of dimensionality can slow it down considerably.

</hk(a;h)∝cos⁡(πa2h) if |a|

タイトルとURLをコピーしました