Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

ノンパラメトリック密度推定

2つの正規分布からなる混合分布があるとする

0.3N(0.25,0.01)+0.7N(0.75,0.01)0.3 \mathcal{N}(0.25, 0.01) + 0.7 \mathcal{N}(0.75, 0.01)

確率密度関数は下の図の左側のようになる。

この分布から100個のサンプルが得られたとする。ヒストグラムは右側の図のようになる。

Source
import numpy as np
from scipy.stats import norm
import matplotlib.pyplot as plt

# gen data
# 2つの正規分布の混合分布
w = 0.3
params = [dict(loc=0.25, scale=0.1), dict(loc=0.75, scale=0.1)]

def pdf(x):
    return w * norm.pdf(x, **params[0]) + (1 - w) * norm.pdf(x, **params[1])


def rvs(size):
    n = int(w * size)
    return np.append(norm.rvs(size=n, random_state=0, **params[0]), norm.rvs(size=(size - n), random_state=0, **params[1]))


fig, axes = plt.subplots(figsize=[10, 3], ncols=2)
axes[0].set(xlabel="x", ylabel="probability density", title="probability density function")
x_range = np.linspace(0, 1, 100)
axes[0].plot(x_range, [pdf(xi) for xi in x_range], color="dimgray")

axes[1].set(xlabel="x", ylabel="count", title="histogram")
n = 50
x = rvs(n)
x = np.sort(x)
axes[1].hist(x, color="dimgray")
fig.show()
<Figure size 720x216 with 2 Axes>

ヒストグラム密度推定法

ヒストグラム密度推定法(histogram density estimation method)

1つの連続変数xxが対象の場合を考える。

標準的なヒストグラムでは、xxを幅Δi\Delta_iの区間に区切り、ii番目の区間に入ったxxの観測値の数nin_iと観測値の総数NNを用いて、各区間の確率密度を

pi=niNΔip_i = \frac{n_i}{N \Delta_i}

で推定する。

Source
class HistogramEstimator:

    def __init__(self, bandwidth: float):
        self.h = bandwidth

    def fit(self, x: np.array):
        # calc bins
        range_ = max(x) - min(x)
        times = int(np.ceil(range_ / self.h))
        bins = [min(x) + self.h * i for i in range(times + 1)]
        # calc probabilities
        N = x.shape[0]
        probs = []
        deltas = []
        for i in range(1, len(bins)):
            values = x[(bins[i - 1] <= x) & (x < bins[i])]
            n = len(values)
            delta = bins[i] - bins[i - 1]
            p = n / (N * delta)
            probs.append(p)
            deltas.append((bins[i - 1], bins[i]))
        self.probs_ = probs
        self.deltas_ = deltas
        return self

    def score(self, x: np.array) -> np.array:
        p_pred = np.zeros(shape=x.shape[0])
        for (lower, upper), p in zip(self.deltas_, self.probs_):
            p_pred[(lower <= x) & (x < upper)] = p
        return p_pred

# plot
fig, axes = plt.subplots(figsize=[14, 3], ncols=3)

for ax, h in zip(axes, [0.05, 0.10, 0.2]):
    # true
    x_range = np.linspace(0, 1, 100)
    ax.plot(x_range, [pdf(xi) for xi in x_range], color="dimgray", label="True PDF")

    # estimates
    estimator = HistogramEstimator(bandwidth=h)
    estimator.fit(x)
    for (lower, upper), p in zip(estimator.deltas_, estimator.probs_):
        ax.bar(lower, p, h, color="white", edgecolor="steelblue")
        
    ax.bar(lower, p, h, color="white", edgecolor="steelblue", label="histogram method")

    ax.scatter(x, np.zeros(x.shape[0]), label="samples", color="gray", alpha=0.5)
    ax.set(xlabel="x", ylabel="probability density", title=f"Histogram method (h={h})")
    ax.legend()

fig.show()
<Figure size 1008x216 with 3 Axes>

この方法の結果の良し悪しは区間幅Δi\Delta_iに大きく依存する。幅が狭すぎても区間に含まれるサンプルが少なすぎて推定のばらつきが大きくなるし、幅が広すぎても表現力が不足して分布をうまく捉えられなくなる。

また、ヒストグラム法の問題として

  1. 推定した密度が区間の縁で不連続になる

  2. 次元の呪いに弱い:次元数を上げていった場合、DD次元空間を各変数につきMM個の区間にすると、区間の総数はMDM^D個になり、各区間に含まれるデータ量が不足する

といったものがある

カーネル密度推定法

DD次元のユークリッド空間中の未知の確率密度p(x)p(\boldsymbol{x})から観測値の集合が得られていて、この集合からp(x)p(\boldsymbol{x})の値を推定したいとする。

x\boldsymbol{x}を含むある小さな領域R\mathcal{R}を考える。この領域に割り当てられた確率は

P=Rp(x)dxP = \int_{\mathcal{R}} p(\boldsymbol{x}) d\boldsymbol{x}

と表すことができる。

(参考)領域R\mathcal{R}のイメージ(1次元の場合)
glue:figure - Unknown Directive
:figwidth: 80%
:name: "fig-region"

ここでp(x)p(\boldsymbol{x})から得られたNN個の観測値からなるデータ集合があるとする。各データ店が領域R\mathcal{R}中にある確率はPPなので、R\mathcal{R}内の点の総数KKは二項分布に従う

Bin(KN,P)=N!K!(NK)!PK(1P)NKBin(K|N, P) = \frac{N!}{K!(N-K)!} P^K (1-P)^{N-K}

よって、データ点がこの領域内にある平均割合と分散は

E[K/N]=P,Var[K/N]=P(1P)NE[K/N] = P, \hspace{2em} Var[K/N] = \frac{P(1-P)}{N}

となる。

大きいNNについては、分散が小さくなって平均の周囲で鋭く尖った分布となり、

KNPK \simeq NP

となる。

R\mathcal{R}が確率密度p(x)p(\boldsymbol{x})がこの領域内でほぼ一定とみなせるほど十分に小さいものであると仮定できるのであれば、領域の体積VVを用いて

Pp(x)VP \simeq p(\boldsymbol{x}) V

となる。

これらを組み合わせて、次の密度の推定量が得られる。

p(x)=KNVp(\boldsymbol{x}) = \frac{K}{NV}

カーネル関数

確率密度を求めたいデータ点x\boldsymbol{x}を中心とする小さな超立方体を領域R\mathcal{R}とする。 この領域内にある点の数KKを数えるには、次の関数を定義しておくと便利である。

k(u)={1,ui12, if i=1,,D0,otherwisek(\boldsymbol{u}) = \begin{cases} 1, & |u_i| \leq \frac{1}{2}, & \text{ if } i=1,\cdots, D\\ 0, & \text{otherwise} \end{cases}

これは原点を中心とする単位立方体を表す。 関数k(u)k(\boldsymbol{u})カーネル関数(kernel function)のひとつであり、今回の用途ではParzen窓(parzen window)とも呼ばれる。

Source
def k(u):
    return 1 * all([abs(ui) <= (1/2) for ui in u])

u1 = np.linspace(-1, 1, 51)
u2 = np.linspace(-1, 1, 51)
u1, u2 = np.meshgrid(u1, u2)
k_ = np.array([k([_u1, _u2]) for _u1, _u2 in zip(u1.flatten(), u2.flatten())]).reshape(u1.shape)

fig, ax = plt.subplots(dpi=100, subplot_kw={"projection": "3d"})
ax.plot_surface(u1, u2, k_, rstride=1, cstride=1, linewidth=1, antialiased=True, alpha=0.7)
ax.set(xlabel="u1", ylabel="u2", zlabel="k", title="k(u)", zlim=[0, 2])
fig.show()
<Figure size 600x400 with 1 Axes>

k((xxn)/h)k((\boldsymbol{x} - \boldsymbol{x}_n)/h)x\boldsymbol{x}を中心とする一辺がhhの立方体の内部に、データ点xn\boldsymbol{x}_nがあれば1に、そうでなければ0となる。

例えばx=(2,3),h=2\boldsymbol{x} = (2, 3), h=2の場合は次の図のようになる

Source
_x = np.array([2, 3])
h = 2

x1 = np.linspace(-1, 5, 51)
x2 = np.linspace(0, 5, 51)
x1, x2 = np.meshgrid(x1, x2)
k_ = np.array([k( ( _x - np.array([_x1, _x2]) ) / h )
               for _x1, _x2 in zip(x1.flatten(), x2.flatten())]).reshape(u1.shape)

fig, ax = plt.subplots(dpi=100, subplot_kw={"projection": "3d"})

ax.scatter(_x[0], _x[1], color="orange")
ax.text(_x[0] * 1.1, _x[1] * 1.1, 0, f"x=({_x[0]}, {_x[1]})", color="orange")

ax.plot_surface(x1, x2, k_, rstride=1, cstride=1, linewidth=1, antialiased=True, alpha=0.4)
ax.set(xlabel="x1", ylabel="x2", zlabel="k", title="k([x - x_n] / h)", zlim=[0, 2.5])
fig.show()
<Figure size 600x400 with 1 Axes>

この立方体内部の総点数は

K=n=1Nk(xxnh)K = \sum^N_{n=1} k \left( \frac{\boldsymbol{x} - \boldsymbol{x}_n}{h} \right)

となる。

さきほどのp(x)p(\boldsymbol{x})の推定量

p(x)=KNVp(\boldsymbol{x}) = \frac{K}{NV}

に代入すると

p(x)=1NVn=1Nk(xxnh)p(\boldsymbol{x}) = \frac{1}{NV} \sum^N_{n=1} k \left( \frac{\boldsymbol{x} - \boldsymbol{x}_n}{h} \right)

一辺がhhDD次元立方体の体積がV=hDV=h^Dであることを用いると

p(x)=1N1hDn=1Nk(xxnh)p(\boldsymbol{x}) = \frac{1}{N} \frac{1}{h^D} \sum^N_{n=1} k \left( \frac{\boldsymbol{x} - \boldsymbol{x}_n}{h} \right)

となる。

このカーネルを使用した推定結果は次の図のようになる。 立方体を重ねるような推定を行うため平滑性がなく、ギザギザした密度関数が推定されている。

Source
class KernelEstimator:

    def __init__(self, bandwidth: float):
        self.h = bandwidth

    def fit(self, X: np.array):
        self.X_train_ = X
        self.N_ = X.shape[0]
        self.D_ = X.shape[1]
        return self

    def score(self, X: np.array) -> np.array:
        p_preds = []
        for x in X:
            U = (x - self.X_train_) / self.h
            K = [k(u) for u in U]
            p_pred = (1 / self.N_) * (1 / self.h ** self.D_) * np.sum( K )
            p_preds.append(p_pred)
        return np.array(p_preds)

# plot
fig, axes = plt.subplots(figsize=[14, 3], ncols=3)

for ax, h in zip(axes, [0.05, 0.10, 0.20]):
    # true
    x_range = np.linspace(0, 1, 100)
    ax.plot(x_range, [pdf(xi) for xi in x_range], color="dimgray", label="True PDF")

    # estimates
    X_train = x.reshape(-1, 1)
    X_test = x_range.reshape(-1, 1)

    estimator = KernelEstimator(bandwidth=h)
    estimator.fit(X_train)
    p = estimator.score(X_test)
    ax.plot(x_range, p, label="estimated PDF")

    ax.scatter(x, np.zeros(x.shape[0]), label="samples", color="gray", alpha=0.5)
    ax.set(xlabel="x", ylabel="probability density", title=f"Kernel method (h={h})")
    ax.legend()

fig.show()
<Figure size 1008x216 with 3 Axes>

ガウシアンカーネル

ガウス分布(正規分布)をカーネル関数に用いることで滑らかな密度推定を行う。

p(x)=1Nn=1N1(2πh2)D/2exp{xxn22h2}p(\boldsymbol{x}) = \frac{1}{N} \sum^N_{n=1} \frac{1}{(2 \pi h^2)^{D/2}} \exp \left\{ -\frac{ ||\boldsymbol{x} - \boldsymbol{x}_n||^2 }{2 h^2} \right\}
Source
from scipy.stats import norm

class GaussianKernelEstimator:

    def __init__(self, bandwidth: float):
        self.h = bandwidth

    def fit(self, X: np.array):
        self.X_train_ = X
        self.N_ = X.shape[0]
        self.D_ = X.shape[1]
        return self

    def score(self, X: np.array) -> np.array:
        p_preds = []
        for x in X:
            p_pred = norm.pdf(x=x, loc=self.X_train_, scale=self.h).mean()
            p_preds.append(p_pred)
        return np.array(p_preds)

# plot
fig, axes = plt.subplots(figsize=[14, 3], ncols=3)

for ax, h in zip(axes, [0.02, 0.05, 0.10]):
    # true
    x_range = np.linspace(0, 1, 100)
    ax.plot(x_range, [pdf(xi) for xi in x_range], color="dimgray", label="True PDF")

    # estimates
    X_train = x.reshape(-1, 1)
    X_test = x_range.reshape(-1, 1)

    estimator = GaussianKernelEstimator(bandwidth=h)
    estimator.fit(X_train)
    p = estimator.score(X_test)
    ax.plot(x_range, p, label="estimated PDF")

    ax.scatter(x, np.zeros(x.shape[0]), label="samples", color="gray", alpha=0.5)
    ax.set(xlabel="x", ylabel="probability density", title=f"Gaussian Kernel method (h={h})")
    ax.legend()

fig.show()
<Figure size 1008x216 with 3 Axes>
Source
# 数式からガウスカーネルを実装したけど合わなかったやつ
def gauss(x, x_n, h, D):
    y = (1 / (2 * np.pi * h **2)**(D/2)) * np.exp(-(np.linalg.norm(x - x_n)**2 / 2 * h ** 2))
    return np.mean(y)

class GaussianKernelEstimator:

    def __init__(self, bandwidth: float):
        self.h = bandwidth

    def fit(self, X: np.array):
        self.X_train_ = X
        self.N_ = X.shape[0]
        self.D_ = X.shape[1]
        return self

    def score(self, X: np.array) -> np.array:
        p_preds = []
        for x in X:
            p_pred = gauss(x, x_n=self.X_train_, h=self.h, D=self.D_)
            p_preds.append(p_pred)
        return np.array(p_preds)

# plot
fig, axes = plt.subplots(figsize=[14, 3], ncols=3)

for ax, h in zip(axes, [0.05, 0.10, 0.20]):
    # true
    x_range = np.linspace(0, 1, 100)
    ax.plot(x_range, [pdf(xi) for xi in x_range], color="dimgray", label="True PDF")

    # estimates
    X_train = x.reshape(-1, 1)
    X_test = x_range.reshape(-1, 1)

    estimator = GaussianKernelEstimator(bandwidth=h)
    estimator.fit(X_train)
    p = estimator.score(X_test)
    ax.plot(x_range, p, label="estimated PDF")

    ax.scatter(x, np.zeros(x.shape[0]), label="samples", color="gray", alpha=0.5)
    ax.set(xlabel="x", ylabel="probability density", title=f"Kernel method (h={h})")
    ax.legend()

fig.show()
Output
<Figure size 1008x216 with 3 Axes>

(参考)Scikit-learn実装

2.8. Density Estimation — scikit-learn 1.2.2 documentation

(※kernel='tophat'は立方体カーネルに近いk(u;h)1 if u<hk(u; h) \propto 1 \text{ if } u < h というもの)

Source
from sklearn.neighbors import KernelDensity

fig, axes = plt.subplots(figsize=[14, 3], ncols=3)
for ax, h in zip(axes, [0.02, 0.05, 0.10]):
    # true
    x_range = np.linspace(0, 1, 100)
    ax.plot(x_range, [pdf(xi) for xi in x_range], color="dimgray", label="True PDF")
    ax.scatter(x, np.zeros(x.shape[0]), label="samples", color="gray", alpha=0.5)

    # estimates
    X_train = x.reshape(-1, 1)
    X_test = x_range.reshape(-1, 1)
    
    for method in ["gaussian", "tophat"]:
        kde = KernelDensity(kernel=method, bandwidth=h).fit(X_train)
        log_likelihood = kde.score_samples(X_test)
        ax.plot(x_range, np.exp(log_likelihood), label=f"kernel='{method}'")
    ax.set(xlabel="x", ylabel="probability density", title=f"Kernel method (h={h})")
    ax.legend()
fig.show()
<Figure size 1008x216 with 3 Axes>