Files
2026-07-13 13:30:25 +08:00

223 lines
6.3 KiB
Python
Executable File

import numpy as np
from prml.clustering import KMeans
from prml.rv.rv import RandomVariable
class MultivariateGaussianMixture(RandomVariable):
"""
p(x|mu, L, pi(coef))
= sum_k pi_k N(x|mu_k, L_k^-1)
"""
def __init__(self,
n_components,
mu=None,
cov=None,
tau=None,
coef=None):
"""
construct mixture of Gaussians
Parameters
----------
n_components : int
number of gaussian component
mu : (n_components, ndim) np.ndarray
mean parameter of each gaussian component
cov : (n_components, ndim, ndim) np.ndarray
variance parameter of each gaussian component
tau : (n_components, ndim, ndim) np.ndarray
precision parameter of each gaussian component
coef : (n_components,) np.ndarray
mixing coefficients
"""
super().__init__()
assert isinstance(n_components, int)
self.n_components = n_components
self.mu = mu
if cov is not None and tau is not None:
raise ValueError("Cannot assign both cov and tau at a time")
elif cov is not None:
self.cov = cov
elif tau is not None:
self.tau = tau
else:
self.cov = None
self.tau = None
self.coef = coef
@property
def mu(self):
return self.parameter["mu"]
@mu.setter
def mu(self, mu):
if isinstance(mu, np.ndarray):
assert mu.ndim == 2
assert np.size(mu, 0) == self.n_components
self.ndim = np.size(mu, 1)
self.parameter["mu"] = mu
elif mu is None:
self.parameter["mu"] = None
else:
raise TypeError("mu must be either np.ndarray or None")
@property
def cov(self):
return self.parameter["cov"]
@cov.setter
def cov(self, cov):
if isinstance(cov, np.ndarray):
assert cov.shape == (self.n_components, self.ndim, self.ndim)
self._tau = np.linalg.inv(cov)
self.parameter["cov"] = cov
elif cov is None:
self.parameter["cov"] = None
self._tau = None
else:
raise TypeError("cov must be either np.ndarray or None")
@property
def tau(self):
return self._tau
@tau.setter
def tau(self, tau):
if isinstance(tau, np.ndarray):
assert tau.shape == (self.n_components, self.ndim, self.ndim)
self.parameter["cov"] = np.linalg.inv(tau)
self._tau = tau
elif tau is None:
self.parameter["cov"] = None
self._tau = None
else:
raise TypeError("tau must be either np.ndarray or None")
@property
def coef(self):
return self.parameter["coef"]
@coef.setter
def coef(self, coef):
if isinstance(coef, np.ndarray):
assert coef.ndim == 1
if np.isnan(coef).any():
self.parameter["coef"] = np.ones(self.n_components) / self.n_components
elif not np.allclose(coef.sum(), 1):
raise ValueError(f"sum of coef must be equal to 1 {coef}")
self.parameter["coef"] = coef
elif coef is None:
self.parameter["coef"] = None
else:
raise TypeError("coef must be either np.ndarray or None")
@property
def shape(self):
if hasattr(self.mu, "shape"):
return self.mu.shape[1:]
else:
return None
def _gauss(self, X):
d = X[:, None, :] - self.mu
D_sq = np.sum(np.einsum('nki,kij->nkj', d, self.cov) * d, -1)
return (
np.exp(-0.5 * D_sq)
/ np.sqrt(
np.linalg.det(self.cov) * (2 * np.pi) ** self.ndim
)
)
def _fit(self, X):
cov = np.cov(X.T)
kmeans = KMeans(self.n_components)
kmeans.fit(X)
self.mu = kmeans.centers
self.cov = np.array([cov for _ in range(self.n_components)])
self.coef = np.ones(self.n_components) / self.n_components
params = np.hstack(
(self.mu.ravel(),
self.cov.ravel(),
self.coef.ravel())
)
while True:
stats = self._expectation(X)
self._maximization(X, stats)
new_params = np.hstack(
(self.mu.ravel(),
self.cov.ravel(),
self.coef.ravel())
)
if np.allclose(params, new_params):
break
else:
params = new_params
def _expectation(self, X):
resps = self.coef * self._gauss(X)
resps /= resps.sum(axis=-1, keepdims=True)
return resps
def _maximization(self, X, resps):
Nk = np.sum(resps, axis=0)
self.coef = Nk / len(X)
self.mu = (X.T @ resps / Nk).T
d = X[:, None, :] - self.mu
self.cov = np.einsum(
'nki,nkj->kij', d, d * resps[:, :, None]) / Nk[:, None, None]
def joint_proba(self, X):
"""
calculate joint probability p(X, Z)
Parameters
----------
X : (sample_size, n_features) ndarray
input data
Returns
-------
joint_prob : (sample_size, n_components) ndarray
joint probability of input and component
"""
return self.coef * self._gauss(X)
def _pdf(self, X):
joint_prob = self.coef * self._gauss(X)
return np.sum(joint_prob, axis=-1)
def classify(self, X):
"""
classify input
max_z p(z|x, theta)
Parameters
----------
X : (sample_size, ndim) ndarray
input
Returns
-------
output : (sample_size,) ndarray
corresponding cluster index
"""
return np.argmax(self.classify_proba(X), axis=1)
def classify_proba(self, X):
"""
posterior probability of cluster
p(z|x,theta)
Parameters
----------
X : (sample_size, ndim) ndarray
input
Returns
-------
output : (sample_size, n_components) ndarray
posterior probability of cluster
"""
return self._expectation(X)