Reference
Fuzzy C-means Model
Attributes:
| Name | Type | Description |
|---|---|---|
n_clusters |
int |
The number of clusters to form as well as the number |
max_iter |
int |
Maximum number of iterations of the fuzzy C-means |
m |
float |
Degree of fuzziness: \(m \in (1, \infty)\). |
error |
float |
Relative tolerance with regards to Frobenius norm of |
random_state |
Optional[int] |
Determines random number generation for |
init |
str |
Initialization of the partition. |
n_init |
int |
Number of independent runs, each from a different |
trained |
bool |
Variable to store whether or not the model has been |
Returns:
| Type | Description |
|---|---|
FCM |
A FCM model. |
Exceptions:
| Type | Description |
|---|---|
ReferenceError |
If called without the model being trained |
Source code in fcmeans/main.py
class FCM(BaseModel):
r"""Fuzzy C-means Model
Attributes:
n_clusters (int): The number of clusters to form as well as the number
of centroids to generate by the fuzzy C-means.
max_iter (int): Maximum number of iterations of the fuzzy C-means
algorithm for a single run.
m (float): Degree of fuzziness: $m \in (1, \infty)$.
error (float): Relative tolerance with regards to Frobenius norm of
the difference
in the cluster centers of two consecutive iterations to declare
convergence.
random_state (Optional[int]): Determines random number generation for
centroid initialization.
Use an int to make the randomness deterministic.
init (str): Initialization of the partition. `"random"` draws a
random fuzzy partition; `"k-means++"` seeds the centers with the
k-means++ rule (each new center is drawn with probability
proportional to its squared distance to the closest chosen one) and
derives the partition from them. Ignored by `FCMedoids`, which
always seeds with k-means++.
n_init (int): Number of independent runs, each from a different
initialization. The run with the lowest objective function is kept.
The first run uses `random_state` itself, so `n_init > 1` is never
worse than `n_init = 1` for the same `random_state`.
trained (bool): Variable to store whether or not the model has been
trained.
Returns:
FCM: A FCM model.
Raises:
ReferenceError: If called without the model being trained
"""
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
n_clusters: int = Field(5, ge=1)
max_iter: int = Field(150, ge=1, le=1000)
m: float = Field(2.0, ge=1.0)
error: float = Field(1e-5, ge=1e-9)
random_state: Optional[int] = None
init: Literal["random", "k-means++"] = "random"
n_init: int = Field(default=1, ge=1)
trained: bool = False
verbose: Optional[bool] = False
distance: Optional[Union[DistanceOptions, Callable]] = (
DistanceOptions.euclidean
)
distance_params: Optional[dict] = {}
def _init_u(self, X: NDArray) -> None:
"""Initialize the fuzzy partition matrix `u`."""
self.rng = np.random.default_rng(self.random_state)
if self.init == "k-means++":
self._centers = self._seed_centers(X)
self.u = FCM._memberships(
FCM._dist(
X, self._centers, self.distance, self.distance_params
),
self.m,
)
return
u = self.rng.uniform(size=(X.shape[0], self.n_clusters))
self.u = u / u.sum(axis=1, keepdims=True)
def _seed_centers(self, X: NDArray) -> NDArray:
"""Pick `n_clusters` samples with the k-means++ rule."""
n = X.shape[0]
chosen = [self.rng.integers(n)]
closest = self._sq_dist_to(X, chosen[0])
for _ in range(1, self.n_clusters):
total = closest.sum()
chosen.append(
self.rng.choice(n, p=closest / total if total > 0 else None)
)
closest = np.minimum(closest, self._sq_dist_to(X, chosen[-1]))
return X[chosen]
def _sq_dist_to(self, X: NDArray, i: int) -> NDArray:
"""Squared distance from every sample to sample `i`."""
d = FCM._dist(X, X[[i]], self.distance, self.distance_params)
return d[:, 0] ** 2
def _update_centers(self, X: NDArray) -> None:
"""Update `_centers` from the current partition matrix `u`."""
self._centers = FCM._next_centers(X, self.u, self.m)
def _distances(self, X: NDArray) -> NDArray:
"""Distance from each sample in X to each center."""
return FCM._dist(X, self._centers, self.distance, self.distance_params)
def _update_u(self, X: NDArray) -> None:
"""Update `u` from the current centers."""
self.u = self.soft_predict(X)
def _objective(self, X: NDArray) -> float:
"""Objective value (or a monotone transform) to compare runs."""
return float((self.u**self.m * self._distances(X) ** 2).sum())
def _fit_once(self, X: NDArray) -> None:
"""Run the algorithm once, from a single initialization."""
self._init_u(X)
for _ in tqdm.tqdm(
range(self.max_iter), desc="Training", disable=not self.verbose
):
u_old = self.u.copy()
self._update_centers(X)
self._update_u(X)
# Stopping rule
if np.linalg.norm(self.u - u_old) < self.error:
break
self.trained = True
@validate_call(config=dict(arbitrary_types_allowed=True))
def fit(self, X: NDArray) -> None:
"""Train the fuzzy-c-means model
With `n_init > 1` the model is trained `n_init` times and the run
with the lowest objective function is kept.
Args:
X (NDArray): Training instances to cluster.
"""
if self.n_init == 1:
self._fit_once(X)
return
seed = self.random_state
extra = np.random.default_rng(seed).integers(2**32, size=self.n_init)
seeds = [seed, *map(int, extra[1:])]
best: tuple[float, dict, dict] = (np.inf, {}, {})
try:
for self.random_state in seeds:
self._fit_once(X)
obj = np.nan_to_num(self._objective(X), nan=np.inf)
if obj < best[0] or not best[1]:
# snapshot of every fitted attribute of this run
best = (
obj,
dict(self.__dict__),
dict(self.model_extra or {}),
)
self.__dict__.update(best[1])
for name, value in best[2].items():
setattr(self, name, value)
finally:
self.random_state = seed
@validate_call(config=dict(arbitrary_types_allowed=True))
def soft_predict(self, X: NDArray) -> NDArray:
"""Soft predict of FCM
Args:
X (NDArray): New data to predict.
Returns:
NDArray: Fuzzy partition array, returned as an array with
n_samples rows and n_clusters columns.
"""
return FCM._memberships(self._distances(X), self.m)
@validate_call(config=dict(arbitrary_types_allowed=True))
def predict(self, X: NDArray) -> NDArray:
"""Predict the closest cluster each sample in X belongs to.
Args:
X (NDArray): New data to predict.
Raises:
ReferenceError: If it called without the model being trained.
Returns:
NDArray: Index of the cluster each sample belongs to.
"""
self._require_trained()
X = np.expand_dims(X, axis=0) if len(X.shape) == 1 else X
return self.soft_predict(X).argmax(axis=-1)
def _require_trained(self) -> None:
if not self.trained:
raise ReferenceError(
"You need to train the model. Run `.fit()` method to this."
)
@staticmethod
def _memberships(d: NDArray, m: float) -> NDArray:
"""Fuzzy partition from the sample-to-center distances `d`."""
with np.errstate(divide="ignore", invalid="ignore"):
temp = d ** (2 / (m - 1))
u = 1.0 / (temp * (1.0 / temp).sum(axis=1, keepdims=True))
# a sample on a center belongs only to it (evenly split on ties)
zero = d == 0
rows = zero.any(axis=1)
u[rows] = zero[rows] / zero[rows].sum(axis=1, keepdims=True)
return u
@staticmethod
def _dist(
A: NDArray,
B: NDArray,
distance: Optional[Union[DistanceOptions, Callable]] = (
DistanceOptions.euclidean
),
distance_params: Optional[dict] = {},
) -> NDArray:
"""Compute the distance between two matrices"""
if callable(distance):
return distance(A, B, distance_params)
elif distance == "minkowski":
if isinstance(distance_params, dict):
p = distance_params.get("p", 1.0)
else:
p = 1.0
return FCM._minkowski(A, B, p)
elif distance == "cosine":
return FCM._cosine(A, B)
else:
return FCM._euclidean(A, B)
@staticmethod
def _euclidean(A: NDArray, B: NDArray) -> NDArray:
"""Compute the euclidean distance between two matrices"""
return np.sqrt(np.einsum("ijk->ij", (A[:, None, :] - B) ** 2))
@staticmethod
def _minkowski(A: NDArray, B: NDArray, p: float) -> NDArray:
"""Compute the minkowski distance between two matrices"""
return np.einsum("ijk->ij", np.abs(A[:, None, :] - B) ** p) ** (1 / p)
@staticmethod
def _cosine_similarity(A: NDArray, B: NDArray) -> NDArray:
"""Compute the cosine similarity between two matrices"""
p1 = np.sqrt(np.sum(A**2, axis=1))[:, np.newaxis]
p2 = np.sqrt(np.sum(B**2, axis=1))[np.newaxis, :]
return np.dot(A, B.T) / (p1 * p2)
@staticmethod
def _cosine(A: NDArray, B: NDArray) -> NDArray:
"""Compute the cosine distance between two matrices"""
return np.abs(1 - FCM._cosine_similarity(A, B))
@staticmethod
def _next_centers(X: NDArray, u: NDArray, m: float):
"""Update cluster centers"""
um = u**m
return (X.T @ um / np.sum(um, axis=0)).T
@property
def centers(self) -> NDArray:
self._require_trained()
return self._centers
@property
def partition_coefficient(self) -> float:
"""Partition coefficient
Equation 12a of
[this paper](https://doi.org/10.1016/0098-3004(84)90020-7).
"""
self._require_trained()
return np.sum(np.mean(self.u**2, axis=0))
@property
def partition_entropy_coefficient(self):
self._require_trained()
return -np.sum(np.mean(self.u * np.log2(self.u), axis=0))
__class_vars__
special
The names of the class variables defined on the model.
__private_attributes__
special
Metadata about the private attributes of the model.
__pydantic_complete__
special
Whether model building is completed, or if there are still undefined fields.
__pydantic_computed_fields__
special
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
__pydantic_custom_init__
special
Whether the model has a custom __init__ method.
__pydantic_decorators__
special
Metadata containing the decorators defined on the model.
This replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
__pydantic_extra_info__
special
A wrapper around the __pydantic_extra__ annotation, if explicitly annotated on a model.
This is a private attribute, not meant to be used outside Pydantic.
__pydantic_fields__
special
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
This replaces Model.__fields__ from Pydantic V1.
__pydantic_generic_metadata__
special
A dictionary containing metadata about generic Pydantic models.
The origin and args items map to the [__origin__][genericalias.__origin__]
and [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],
and the parameter item maps to the __parameter__ attribute of generic classes.
__pydantic_parent_namespace__
special
Parent namespace of the model, used for automatic rebuilding of models.
__pydantic_post_init__
special
The name of the post-init method for the model, if defined.
__pydantic_setattr_handlers__
special
__setattr__ handlers. Memoizing the handlers leads to a dramatic performance improvement in __setattr__
__signature__
special
The synthesized __init__ [Signature][inspect.Signature] of the model.
model_config
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
partition_coefficient: float
property
readonly
Partition coefficient
Equation 12a of this paper.
fit(self, X)
Train the fuzzy-c-means model
With n_init > 1 the model is trained n_init times and the run
with the lowest objective function is kept.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X |
NDArray |
Training instances to cluster. |
required |
Source code in fcmeans/main.py
@validate_call(config=dict(arbitrary_types_allowed=True))
def fit(self, X: NDArray) -> None:
"""Train the fuzzy-c-means model
With `n_init > 1` the model is trained `n_init` times and the run
with the lowest objective function is kept.
Args:
X (NDArray): Training instances to cluster.
"""
if self.n_init == 1:
self._fit_once(X)
return
seed = self.random_state
extra = np.random.default_rng(seed).integers(2**32, size=self.n_init)
seeds = [seed, *map(int, extra[1:])]
best: tuple[float, dict, dict] = (np.inf, {}, {})
try:
for self.random_state in seeds:
self._fit_once(X)
obj = np.nan_to_num(self._objective(X), nan=np.inf)
if obj < best[0] or not best[1]:
# snapshot of every fitted attribute of this run
best = (
obj,
dict(self.__dict__),
dict(self.model_extra or {}),
)
self.__dict__.update(best[1])
for name, value in best[2].items():
setattr(self, name, value)
finally:
self.random_state = seed
predict(self, X)
Predict the closest cluster each sample in X belongs to.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X |
NDArray |
New data to predict. |
required |
Exceptions:
| Type | Description |
|---|---|
ReferenceError |
If it called without the model being trained. |
Returns:
| Type | Description |
|---|---|
NDArray |
Index of the cluster each sample belongs to. |
Source code in fcmeans/main.py
@validate_call(config=dict(arbitrary_types_allowed=True))
def predict(self, X: NDArray) -> NDArray:
"""Predict the closest cluster each sample in X belongs to.
Args:
X (NDArray): New data to predict.
Raises:
ReferenceError: If it called without the model being trained.
Returns:
NDArray: Index of the cluster each sample belongs to.
"""
self._require_trained()
X = np.expand_dims(X, axis=0) if len(X.shape) == 1 else X
return self.soft_predict(X).argmax(axis=-1)
soft_predict(self, X)
Soft predict of FCM
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X |
NDArray |
New data to predict. |
required |
Returns:
| Type | Description |
|---|---|
NDArray |
Fuzzy partition array, returned as an array with n_samples rows and n_clusters columns. |
Source code in fcmeans/main.py
@validate_call(config=dict(arbitrary_types_allowed=True))
def soft_predict(self, X: NDArray) -> NDArray:
"""Soft predict of FCM
Args:
X (NDArray): New data to predict.
Returns:
NDArray: Fuzzy partition array, returned as an array with
n_samples rows and n_clusters columns.
"""
return FCM._memberships(self._distances(X), self.m)
Possibilistic C-means Model
Relaxes the FCM constraint \(\sum_i u_{ij} = 1\), so \(u_{ij}\) is the typicality of sample \(j\) with respect to cluster \(i\) rather than a share of membership. Outliers get low typicality in every cluster.
The model is initialized with a FCM run: its partition gives the starting centers and the scale parameters \(\eta_i\) (Krishnapuram and Keller, 1993), which are kept fixed afterwards.
Membership update:
Accepts the same hyperparameters as FCM.
Attributes:
| Name | Type | Description |
|---|---|---|
eta |
NDArray |
Scale parameter of each cluster, set by |
Exceptions:
| Type | Description |
|---|---|
ReferenceError |
If called without the model being trained. |
NotImplementedError |
If |
Source code in fcmeans/pcm.py
class PCM(FCM):
r"""Possibilistic C-means Model
Relaxes the FCM constraint $\sum_i u_{ij} = 1$, so $u_{ij}$ is the
*typicality* of sample $j$ with respect to cluster $i$ rather than a
share of membership. Outliers get low typicality in every cluster.
The model is initialized with a FCM run: its partition gives the
starting centers and the scale parameters $\eta_i$ (Krishnapuram and
Keller, 1993), which are kept fixed afterwards.
Membership update:
$$u_{ij} = \frac{1}{1 + (d_{ij}^2 / \eta_i)^{1/(m-1)}}$$
Accepts the same hyperparameters as [FCM][fcmeans.FCM].
Attributes:
eta (NDArray): Scale parameter of each cluster, set by `fit`.
Raises:
ReferenceError: If called without the model being trained.
NotImplementedError: If `partition_coefficient` or
`partition_entropy_coefficient` is requested; both assume rows of
`u` sum to one.
"""
def _init_u(self, X: NDArray) -> None:
"""Initialize `u`, `_centers` and `eta` from a FCM run."""
fcm = FCM(
n_clusters=self.n_clusters,
max_iter=self.max_iter,
m=self.m,
error=self.error,
random_state=self.random_state,
init=self.init,
distance=self.distance,
distance_params=self.distance_params,
)
fcm.fit(X)
self.rng = fcm.rng
self.u = fcm.u
self._centers = fcm.centers
um = self.u**self.m
d2 = FCM._dist(X, self._centers, self.distance, self.distance_params)
self.eta = (um * d2**2).sum(axis=0) / um.sum(axis=0)
def _objective(self, X: NDArray) -> float:
"""PCM objective: weighted distances plus the `eta` penalty."""
d = FCM._dist(X, self._centers, self.distance, self.distance_params)
um = self.u**self.m
return float(
(um * d**2).sum() + (self.eta * (1 - self.u) ** self.m).sum()
)
@validate_call(config=dict(arbitrary_types_allowed=True))
def soft_predict(self, X: NDArray) -> NDArray:
"""Typicality of each sample to each cluster
Args:
X (NDArray): New data to predict.
Returns:
NDArray: Typicality array with n_samples rows and n_clusters
columns. Rows do not sum to one.
"""
d2 = (
FCM._dist(X, self._centers, self.distance, self.distance_params)
** 2
)
return 1.0 / (1.0 + (d2 / self.eta) ** (1 / (self.m - 1)))
@property
def partition_coefficient(self) -> float:
"""Not defined for possibilistic partitions."""
raise NotImplementedError("Rows of `u` do not sum to one in PCM.")
@property
def partition_entropy_coefficient(self) -> float:
"""Not defined for possibilistic partitions."""
raise NotImplementedError("Rows of `u` do not sum to one in PCM.")
__class_vars__
special
The names of the class variables defined on the model.
__private_attributes__
special
Metadata about the private attributes of the model.
__pydantic_complete__
special
Whether model building is completed, or if there are still undefined fields.
__pydantic_computed_fields__
special
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
__pydantic_custom_init__
special
Whether the model has a custom __init__ method.
__pydantic_decorators__
special
Metadata containing the decorators defined on the model.
This replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
__pydantic_extra_info__
special
A wrapper around the __pydantic_extra__ annotation, if explicitly annotated on a model.
This is a private attribute, not meant to be used outside Pydantic.
__pydantic_fields__
special
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
This replaces Model.__fields__ from Pydantic V1.
__pydantic_generic_metadata__
special
A dictionary containing metadata about generic Pydantic models.
The origin and args items map to the [__origin__][genericalias.__origin__]
and [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],
and the parameter item maps to the __parameter__ attribute of generic classes.
__pydantic_parent_namespace__
special
Parent namespace of the model, used for automatic rebuilding of models.
__pydantic_post_init__
special
The name of the post-init method for the model, if defined.
__pydantic_setattr_handlers__
special
__setattr__ handlers. Memoizing the handlers leads to a dramatic performance improvement in __setattr__
__signature__
special
The synthesized __init__ [Signature][inspect.Signature] of the model.
model_config
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
partition_coefficient: float
property
readonly
Not defined for possibilistic partitions.
partition_entropy_coefficient: float
property
readonly
Not defined for possibilistic partitions.
soft_predict(self, X)
Typicality of each sample to each cluster
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X |
NDArray |
New data to predict. |
required |
Returns:
| Type | Description |
|---|---|
NDArray |
Typicality array with n_samples rows and n_clusters columns. Rows do not sum to one. |
Source code in fcmeans/pcm.py
@validate_call(config=dict(arbitrary_types_allowed=True))
def soft_predict(self, X: NDArray) -> NDArray:
"""Typicality of each sample to each cluster
Args:
X (NDArray): New data to predict.
Returns:
NDArray: Typicality array with n_samples rows and n_clusters
columns. Rows do not sum to one.
"""
d2 = (
FCM._dist(X, self._centers, self.distance, self.distance_params)
** 2
)
return 1.0 / (1.0 + (d2 / self.eta) ** (1 / (self.m - 1)))
Fuzzy-possibilistic C-means Model
Mixed model of Pal, Pal and Bezdek (1997). Each sample has a fuzzy membership \(u_{ij}\) (as in FCM, summing to one over the clusters) and a typicality \(t_{ij}\) (summing to one over the samples of each cluster). Centers are computed from both:
Accepts the same hyperparameters as FCM, plus eta.
soft_predict and predict use the fuzzy memberships only, since
typicality is normalized over the training samples and is not defined
for new data.
Attributes:
| Name | Type | Description |
|---|---|---|
eta |
float |
Typicality exponent: \(\eta \in (1, \infty)\). |
t |
NDArray |
Typicality matrix of the training data, with n_samples |
Exceptions:
| Type | Description |
|---|---|
ReferenceError |
If called without the model being trained. |
Source code in fcmeans/fpcm.py
class FPCM(FCM):
r"""Fuzzy-possibilistic C-means Model
Mixed model of Pal, Pal and Bezdek (1997). Each sample has a fuzzy
membership $u_{ij}$ (as in FCM, summing to one over the clusters) and a
typicality $t_{ij}$ (summing to one over the samples of each cluster).
Centers are computed from both:
$$v_i = \frac{\sum_j (u_{ij}^m + t_{ij}^\eta) x_j}
{\sum_j (u_{ij}^m + t_{ij}^\eta)}$$
Accepts the same hyperparameters as [FCM][fcmeans.FCM], plus `eta`.
`soft_predict` and `predict` use the fuzzy memberships only, since
typicality is normalized over the training samples and is not defined
for new data.
Attributes:
eta (float): Typicality exponent: $\eta \in (1, \infty)$.
t (NDArray): Typicality matrix of the training data, with n_samples
rows and n_clusters columns, set by `fit`.
Raises:
ReferenceError: If called without the model being trained.
"""
eta: float = Field(2.0, gt=1.0)
def _init_u(self, X: NDArray) -> None:
"""Initialize `u` randomly and derive `t` from it."""
super()._init_u(X)
self.t = self.u / self.u.sum(axis=0, keepdims=True)
def _objective(self, X: NDArray) -> float:
"""Fuzzy and typicality weighted sum of squared distances."""
d = FCM._dist(X, self._centers, self.distance, self.distance_params)
return float(((self.u**self.m + self.t**self.eta) * d**2).sum())
def _update_centers(self, X: NDArray) -> None:
"""Update `_centers` from the fuzzy and typicality weights."""
w = self.u**self.m + self.t**self.eta
self._centers = (X.T @ w / w.sum(axis=0)).T
def _update_u(self, X: NDArray) -> None:
"""Update `u` and `t` from the current centers."""
super()._update_u(X)
d = FCM._dist(X, self._centers, self.distance, self.distance_params)
w = d ** (-2 / (self.eta - 1))
self.t = w / w.sum(axis=0, keepdims=True)
__class_vars__
special
The names of the class variables defined on the model.
__private_attributes__
special
Metadata about the private attributes of the model.
__pydantic_complete__
special
Whether model building is completed, or if there are still undefined fields.
__pydantic_computed_fields__
special
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
__pydantic_custom_init__
special
Whether the model has a custom __init__ method.
__pydantic_decorators__
special
Metadata containing the decorators defined on the model.
This replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
__pydantic_extra_info__
special
A wrapper around the __pydantic_extra__ annotation, if explicitly annotated on a model.
This is a private attribute, not meant to be used outside Pydantic.
__pydantic_fields__
special
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
This replaces Model.__fields__ from Pydantic V1.
__pydantic_generic_metadata__
special
A dictionary containing metadata about generic Pydantic models.
The origin and args items map to the [__origin__][genericalias.__origin__]
and [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],
and the parameter item maps to the __parameter__ attribute of generic classes.
__pydantic_parent_namespace__
special
Parent namespace of the model, used for automatic rebuilding of models.
__pydantic_post_init__
special
The name of the post-init method for the model, if defined.
__pydantic_setattr_handlers__
special
__setattr__ handlers. Memoizing the handlers leads to a dramatic performance improvement in __setattr__
__signature__
special
The synthesized __init__ [Signature][inspect.Signature] of the model.
model_config
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
Gustafson-Kessel Model
FCM with an adaptive distance: each cluster \(i\) has its own norm matrix \(A_i\), so clusters can be ellipsoids of different orientations instead of spheres (Gustafson and Kessel, 1978). With the cluster volume fixed at one:
where \(p\) is the number of features. Memberships and centers are updated as in FCM.
Accepts the same hyperparameters as FCM, except that
distance cannot be changed: the adaptive norm replaces it.
Attributes:
| Name | Type | Description |
|---|---|---|
reg |
float |
Regularization of the covariance matrices. Each \(F_i\) |
norm_matrices |
NDArray |
Norm matrices \(A_i\) with shape |
Exceptions:
| Type | Description |
|---|---|
ReferenceError |
If called without the model being trained. |
ValidationError |
If |
Source code in fcmeans/gk.py
class GK(FCM):
r"""Gustafson-Kessel Model
FCM with an adaptive distance: each cluster $i$ has its own norm matrix
$A_i$, so clusters can be ellipsoids of different orientations instead of
spheres (Gustafson and Kessel, 1978). With the cluster volume fixed at
one:
$$F_i = \frac{\sum_j u_{ij}^m (x_j - v_i)(x_j - v_i)^\top}
{\sum_j u_{ij}^m}
\qquad
A_i = \det(F_i)^{1/p} F_i^{-1}
\qquad
d_{ij}^2 = (x_j - v_i)^\top A_i (x_j - v_i)$$
where $p$ is the number of features. Memberships and centers are updated
as in FCM.
Accepts the same hyperparameters as [FCM][fcmeans.FCM], except that
`distance` cannot be changed: the adaptive norm replaces it.
Attributes:
reg (float): Regularization of the covariance matrices. Each $F_i$
gets `reg * trace(F_i) / p` added to its diagonal, which keeps it
invertible when a cluster is degenerate (constant or collinear
features, fewer samples than features).
norm_matrices (NDArray): Norm matrices $A_i$ with shape
(n_clusters, n_features, n_features), set by `fit`.
Raises:
ReferenceError: If called without the model being trained.
ValidationError: If `distance` is not the default.
"""
reg: float = Field(1e-6, ge=0.0)
@field_validator("distance")
@classmethod
def _default_distance_only(cls, v):
if v != DistanceOptions.euclidean:
raise ValueError(
"GK uses its own adaptive norm; `distance` is fixed."
)
return v
def _covariances(self, X: NDArray) -> NDArray:
"""Regularized fuzzy covariance matrix of each cluster."""
diff = X[:, None, :] - self._centers
um = self.u**self.m
p = X.shape[1]
F = np.einsum("nc,ncp,ncq->cpq", um, diff, diff)
F /= um.sum(axis=0)[:, None, None]
F += (
self.reg * np.trace(F, axis1=1, axis2=2)[:, None, None] / p
) * np.eye(p)
return F
def _update_centers(self, X: NDArray) -> None:
"""Update `_centers` and the cluster shape (see `_update_shape`)."""
super()._update_centers(X)
self._update_shape(X)
def _update_shape(self, X: NDArray) -> None:
"""Update the norm matrices `A_i` from the current centers."""
F = self._covariances(X)
p = X.shape[1]
_, logdet = np.linalg.slogdet(F)
scale = np.exp(logdet / p)[:, None, None]
self.norm_matrices = scale * np.linalg.inv(F)
def _distances(self, X: NDArray) -> NDArray:
"""Distance from each sample to each center under its own norm."""
diff = X[:, None, :] - self._centers
return np.sqrt(
np.einsum("ncp,cpq,ncq->nc", diff, self.norm_matrices, diff)
)
__class_vars__
special
The names of the class variables defined on the model.
__private_attributes__
special
Metadata about the private attributes of the model.
__pydantic_complete__
special
Whether model building is completed, or if there are still undefined fields.
__pydantic_computed_fields__
special
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
__pydantic_custom_init__
special
Whether the model has a custom __init__ method.
__pydantic_decorators__
special
Metadata containing the decorators defined on the model.
This replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
__pydantic_extra_info__
special
A wrapper around the __pydantic_extra__ annotation, if explicitly annotated on a model.
This is a private attribute, not meant to be used outside Pydantic.
__pydantic_fields__
special
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
This replaces Model.__fields__ from Pydantic V1.
__pydantic_generic_metadata__
special
A dictionary containing metadata about generic Pydantic models.
The origin and args items map to the [__origin__][genericalias.__origin__]
and [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],
and the parameter item maps to the __parameter__ attribute of generic classes.
__pydantic_parent_namespace__
special
Parent namespace of the model, used for automatic rebuilding of models.
__pydantic_post_init__
special
The name of the post-init method for the model, if defined.
__pydantic_setattr_handlers__
special
__setattr__ handlers. Memoizing the handlers leads to a dramatic performance improvement in __setattr__
__signature__
special
The synthesized __init__ [Signature][inspect.Signature] of the model.
model_config
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
Gath-Geva Model
Fuzzy maximum likelihood clustering (Gath and Geva, 1989). Like GK, each cluster has its own covariance matrix \(F_i\), and it also has a prior probability \(\alpha_i\), so clusters can differ in shape, size and density. The squared distance is the inverse of a Gaussian density:
with \(F_i\) the fuzzy covariance matrix of GK. Memberships are computed from \(d_{ij}^2\) as in FCM, in the log domain so that samples far from every cluster do not overflow.
The exponential distance makes GG very sensitive to its starting point, so the model is initialized with a GK run (same hyperparameters) and the GG iterations start from its partition.
Accepts the same hyperparameters as GK (distance cannot
be changed).
Attributes:
| Name | Type | Description |
|---|---|---|
covariances |
NDArray |
Covariance matrices \(F_i\) with shape |
priors |
NDArray |
Prior probabilities \(\alpha_i\), summing to one, set |
Exceptions:
| Type | Description |
|---|---|
ReferenceError |
If called without the model being trained. |
ValidationError |
If |
Source code in fcmeans/gg.py
class GG(GK):
r"""Gath-Geva Model
Fuzzy maximum likelihood clustering (Gath and Geva, 1989). Like GK, each
cluster has its own covariance matrix $F_i$, and it also has a prior
probability $\alpha_i$, so clusters can differ in shape, size and
density. The squared distance is the inverse of a Gaussian density:
$$\alpha_i = \frac{\sum_j u_{ij}^m}{\sum_{l,j} u_{lj}^m}
\qquad
d_{ij}^2 = \frac{\sqrt{\det F_i}}{\alpha_i}
\exp\left(\tfrac{1}{2} (x_j - v_i)^\top F_i^{-1} (x_j - v_i)\right)$$
with $F_i$ the fuzzy covariance matrix of [GK][fcmeans.GK]. Memberships
are computed from $d_{ij}^2$ as in FCM, in the log domain so that samples
far from every cluster do not overflow.
The exponential distance makes GG very sensitive to its starting point,
so the model is initialized with a GK run (same hyperparameters) and the
GG iterations start from its partition.
Accepts the same hyperparameters as [GK][fcmeans.GK] (`distance` cannot
be changed).
Attributes:
covariances (NDArray): Covariance matrices $F_i$ with shape
(n_clusters, n_features, n_features), set by `fit`.
priors (NDArray): Prior probabilities $\alpha_i$, summing to one, set
by `fit`.
Raises:
ReferenceError: If called without the model being trained.
ValidationError: If `distance` is not the default.
"""
def _init_u(self, X: NDArray) -> None:
"""Initialize `u` from a GK run."""
gk = GK(
n_clusters=self.n_clusters,
max_iter=self.max_iter,
m=self.m,
error=self.error,
random_state=self.random_state,
init=self.init,
reg=self.reg,
)
gk.fit(X)
self.rng = gk.rng
self.u = gk.u
def _update_shape(self, X: NDArray) -> None:
"""Update covariances, priors and their determinant and inverse."""
um = self.u**self.m
self.covariances = self._covariances(X)
self.priors = um.sum(axis=0) / um.sum()
self._logdet = np.linalg.slogdet(self.covariances)[1]
self._inv_cov = np.linalg.inv(self.covariances)
def _objective(self, X: NDArray) -> float:
"""Log of the GG objective (the objective itself can overflow)."""
diff = X[:, None, :] - self._centers
mahalanobis = np.einsum("ncp,cpq,ncq->nc", diff, self._inv_cov, diff)
log_d2 = 0.5 * self._logdet - np.log(self.priors) + 0.5 * mahalanobis
with np.errstate(divide="ignore"):
a = self.m * np.log(self.u) + log_d2
top = a.max()
return float(top + np.log(np.exp(a - top).sum()))
@validate_call(config=dict(arbitrary_types_allowed=True))
def soft_predict(self, X: NDArray) -> NDArray:
"""Soft predict of GG
Args:
X (NDArray): New data to predict.
Returns:
NDArray: Fuzzy partition array, returned as an array with
n_samples rows and n_clusters columns.
"""
diff = X[:, None, :] - self._centers
mahalanobis = np.einsum("ncp,cpq,ncq->nc", diff, self._inv_cov, diff)
log_d2 = 0.5 * self._logdet - np.log(self.priors) + 0.5 * mahalanobis
z = -log_d2 / (self.m - 1)
z -= z.max(axis=1, keepdims=True)
e = np.exp(z)
return e / e.sum(axis=1, keepdims=True)
__class_vars__
special
The names of the class variables defined on the model.
__private_attributes__
special
Metadata about the private attributes of the model.
__pydantic_complete__
special
Whether model building is completed, or if there are still undefined fields.
__pydantic_computed_fields__
special
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
__pydantic_custom_init__
special
Whether the model has a custom __init__ method.
__pydantic_decorators__
special
Metadata containing the decorators defined on the model.
This replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
__pydantic_extra_info__
special
A wrapper around the __pydantic_extra__ annotation, if explicitly annotated on a model.
This is a private attribute, not meant to be used outside Pydantic.
__pydantic_fields__
special
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
This replaces Model.__fields__ from Pydantic V1.
__pydantic_generic_metadata__
special
A dictionary containing metadata about generic Pydantic models.
The origin and args items map to the [__origin__][genericalias.__origin__]
and [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],
and the parameter item maps to the __parameter__ attribute of generic classes.
__pydantic_parent_namespace__
special
Parent namespace of the model, used for automatic rebuilding of models.
__pydantic_post_init__
special
The name of the post-init method for the model, if defined.
__pydantic_setattr_handlers__
special
__setattr__ handlers. Memoizing the handlers leads to a dramatic performance improvement in __setattr__
__signature__
special
The synthesized __init__ [Signature][inspect.Signature] of the model.
model_config
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
soft_predict(self, X)
Soft predict of GG
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X |
NDArray |
New data to predict. |
required |
Returns:
| Type | Description |
|---|---|
NDArray |
Fuzzy partition array, returned as an array with n_samples rows and n_clusters columns. |
Source code in fcmeans/gg.py
@validate_call(config=dict(arbitrary_types_allowed=True))
def soft_predict(self, X: NDArray) -> NDArray:
"""Soft predict of GG
Args:
X (NDArray): New data to predict.
Returns:
NDArray: Fuzzy partition array, returned as an array with
n_samples rows and n_clusters columns.
"""
diff = X[:, None, :] - self._centers
mahalanobis = np.einsum("ncp,cpq,ncq->nc", diff, self._inv_cov, diff)
log_d2 = 0.5 * self._logdet - np.log(self.priors) + 0.5 * mahalanobis
z = -log_d2 / (self.m - 1)
z -= z.max(axis=1, keepdims=True)
e = np.exp(z)
return e / e.sum(axis=1, keepdims=True)
Fuzzy C-medoids Model
FCM whose prototypes are restricted to be samples of the data set, the medoids (Krishnapuram, Joshi, Nasraoui and Yi, 2001). It minimizes the FCM objective with \(v_i \in X\):
Memberships are updated as in FCM. Each medoid is the sample that minimizes the membership-weighted cost of its cluster:
Since the search only compares distances, any distance of
FCM works, including a custom callable. The initial
medoids are seeded with k-means++ on the distance matrix, using
random_state, and the model has converged when the medoids stop
changing.
The distance between all pairs of training samples is computed once, so
fit needs O(n²) memory.
Accepts the same hyperparameters as FCM.
Attributes:
| Name | Type | Description |
|---|---|---|
medoid_indices |
NDArray |
Indices in the training data of the |
Exceptions:
| Type | Description |
|---|---|
ReferenceError |
If called without the model being trained. |
ValueError |
If |
Source code in fcmeans/fcmedoids.py
class FCMedoids(FCM):
r"""Fuzzy C-medoids Model
FCM whose prototypes are restricted to be samples of the data set, the
*medoids* (Krishnapuram, Joshi, Nasraoui and Yi, 2001). It minimizes the
FCM objective with $v_i \in X$:
$$J = \sum_{i=1}^{c} \sum_{j=1}^{n} u_{ij}^m d^2(x_j, v_i)$$
Memberships are updated as in FCM. Each medoid is the sample that
minimizes the membership-weighted cost of its cluster:
$$v_i = \operatorname*{arg\,min}_{x_k \in X}
\sum_{j=1}^{n} u_{ij}^m d^2(x_j, x_k)$$
Since the search only compares distances, any `distance` of
[FCM][fcmeans.FCM] works, including a custom callable. The initial
medoids are seeded with k-means++ on the distance matrix, using
`random_state`, and the model has converged when the medoids stop
changing.
The distance between all pairs of training samples is computed once, so
`fit` needs O(n²) memory.
Accepts the same hyperparameters as [FCM][fcmeans.FCM].
Attributes:
medoid_indices (NDArray): Indices in the training data of the
medoids, set by `fit`. `centers` is the training data at these
indices.
Raises:
ReferenceError: If called without the model being trained.
ValueError: If `n_clusters` is greater than the number of samples.
"""
def _init_u(self, X: NDArray) -> None:
"""Draw the initial medoids and derive `u` from them."""
n = X.shape[0]
if self.n_clusters > n:
raise ValueError(
f"n_clusters ({self.n_clusters}) cannot exceed the number "
f"of samples ({n})."
)
self.rng = np.random.default_rng(self.random_state)
# ponytail: full n x n matrix, O(n^2) memory. Chunk or restrict
# candidate medoids if it matters.
chunks = np.array_split(X, max(1, n // 256))
self._d2 = np.vstack(
[
FCM._dist(c, X, self.distance, self.distance_params) ** 2
for c in chunks
]
)
# k-means++ seeding on the distance matrix: each new medoid is drawn
# with probability proportional to its cost to the closest chosen one
chosen = [self.rng.integers(n)]
closest = self._d2[:, chosen[0]]
for _ in range(1, self.n_clusters):
total = closest.sum()
chosen.append(
self.rng.choice(n, p=closest / total if total > 0 else None)
)
closest = np.minimum(closest, self._d2[:, chosen[-1]])
self.medoid_indices = np.array(chosen)
self._centers = X[self.medoid_indices]
self.u = self.soft_predict(X)
def _update_centers(self, X: NDArray) -> None:
"""Update the medoids from the current partition matrix `u`."""
cost = (self.u**self.m).T @ self._d2
self.medoid_indices = cost.argmin(axis=1)
self._centers = X[self.medoid_indices]
@validate_call(config=dict(arbitrary_types_allowed=True))
def fit(self, X: NDArray) -> None:
"""Train the fuzzy c-medoids model
Args:
X (NDArray): Training instances to cluster.
"""
try:
super().fit(X)
finally:
self.__dict__.pop("_d2", None)
__class_vars__
special
The names of the class variables defined on the model.
__private_attributes__
special
Metadata about the private attributes of the model.
__pydantic_complete__
special
Whether model building is completed, or if there are still undefined fields.
__pydantic_computed_fields__
special
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
__pydantic_custom_init__
special
Whether the model has a custom __init__ method.
__pydantic_decorators__
special
Metadata containing the decorators defined on the model.
This replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
__pydantic_extra_info__
special
A wrapper around the __pydantic_extra__ annotation, if explicitly annotated on a model.
This is a private attribute, not meant to be used outside Pydantic.
__pydantic_fields__
special
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
This replaces Model.__fields__ from Pydantic V1.
__pydantic_generic_metadata__
special
A dictionary containing metadata about generic Pydantic models.
The origin and args items map to the [__origin__][genericalias.__origin__]
and [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],
and the parameter item maps to the __parameter__ attribute of generic classes.
__pydantic_parent_namespace__
special
Parent namespace of the model, used for automatic rebuilding of models.
__pydantic_post_init__
special
The name of the post-init method for the model, if defined.
__pydantic_setattr_handlers__
special
__setattr__ handlers. Memoizing the handlers leads to a dramatic performance improvement in __setattr__
__signature__
special
The synthesized __init__ [Signature][inspect.Signature] of the model.
model_config
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
fit(self, X)
Train the fuzzy c-medoids model
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X |
NDArray |
Training instances to cluster. |
required |
Source code in fcmeans/fcmedoids.py
@validate_call(config=dict(arbitrary_types_allowed=True))
def fit(self, X: NDArray) -> None:
"""Train the fuzzy c-medoids model
Args:
X (NDArray): Training instances to cluster.
"""
try:
super().fit(X)
finally:
self.__dict__.pop("_d2", None)
Kernel Fuzzy C-means Model
FCM with the distance measured in the feature space of a Gaussian kernel \(K(x, v) = \exp(-\gamma \|x - v\|^2)\), while the prototypes stay in the input space (Wu, Xie and Yu, 2003):
Memberships are updated as in FCM with this distance. Centers are the kernel-weighted means
so samples far from a center, such as outliers, get almost no weight.
Accepts the same hyperparameters as FCM, plus gamma;
distance cannot be changed.
Attributes:
| Name | Type | Description |
|---|---|---|
gamma |
Optional[float] |
Kernel coefficient, greater than zero. If |
gamma_ |
float |
Value of |
Exceptions:
| Type | Description |
|---|---|
ReferenceError |
If called without the model being trained. |
ValidationError |
If |
Source code in fcmeans/kfcm.py
class KFCM(FCM):
r"""Kernel Fuzzy C-means Model
FCM with the distance measured in the feature space of a Gaussian kernel
$K(x, v) = \exp(-\gamma \|x - v\|^2)$, while the prototypes stay in the
input space (Wu, Xie and Yu, 2003):
$$d^2(x, v) = \|\phi(x) - \phi(v)\|^2 = 2 (1 - K(x, v))$$
Memberships are updated as in FCM with this distance. Centers are the
kernel-weighted means
$$v_i = \frac{\sum_j u_{ij}^m K(x_j, v_i) x_j}
{\sum_j u_{ij}^m K(x_j, v_i)}$$
so samples far from a center, such as outliers, get almost no weight.
Accepts the same hyperparameters as [FCM][fcmeans.FCM], plus `gamma`;
`distance` cannot be changed.
Attributes:
gamma (Optional[float]): Kernel coefficient, greater than zero. If
None, `1 / (n_features * X.var())` is used.
gamma_ (float): Value of `gamma` used by the fitted model.
Raises:
ReferenceError: If called without the model being trained.
ValidationError: If `distance` is not the default or `gamma` is not
positive.
"""
gamma: Optional[float] = Field(None, gt=0.0)
@field_validator("distance")
@classmethod
def _default_distance_only(cls, v):
if v != DistanceOptions.euclidean:
raise ValueError(
"KFCM uses the Gaussian kernel distance; `distance` is fixed."
)
return v
def _init_u(self, X: NDArray) -> None:
"""Initialize `u`, the centers and `gamma_` from a FCM run."""
fcm = FCM(
n_clusters=self.n_clusters,
max_iter=self.max_iter,
m=self.m,
error=self.error,
random_state=self.random_state,
init=self.init,
)
fcm.fit(X)
self.rng = fcm.rng
self.u = fcm.u
self._centers = fcm.centers
var = X.var()
self.gamma_ = self.gamma or (
1.0 / (X.shape[1] * var) if var > 0 else 1.0
)
def _kernel(self, X: NDArray) -> NDArray:
"""Gaussian kernel between the samples and the centers."""
return np.exp(-self.gamma_ * self._sq_dist(X))
def _sq_dist(self, X: NDArray) -> NDArray:
"""Squared euclidean distance between samples and centers."""
return np.einsum("ijk->ij", (X[:, None, :] - self._centers) ** 2)
def _update_centers(self, X: NDArray) -> None:
"""Update `_centers` as kernel-weighted means."""
w = self.u**self.m * self._kernel(X)
den = w.sum(axis=0)[:, None]
# a center that no sample reaches (all weights 0) stays where it is
self._centers = np.divide(
(X.T @ w).T, den, out=self._centers.copy(), where=den > 0
)
def _distances(self, X: NDArray) -> NDArray:
"""Distance in feature space, from `1 - K` computed without loss."""
return np.sqrt(-2.0 * np.expm1(-self.gamma_ * self._sq_dist(X)))
__class_vars__
special
The names of the class variables defined on the model.
__private_attributes__
special
Metadata about the private attributes of the model.
__pydantic_complete__
special
Whether model building is completed, or if there are still undefined fields.
__pydantic_computed_fields__
special
A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
__pydantic_custom_init__
special
Whether the model has a custom __init__ method.
__pydantic_decorators__
special
Metadata containing the decorators defined on the model.
This replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
__pydantic_extra_info__
special
A wrapper around the __pydantic_extra__ annotation, if explicitly annotated on a model.
This is a private attribute, not meant to be used outside Pydantic.
__pydantic_fields__
special
A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
This replaces Model.__fields__ from Pydantic V1.
__pydantic_generic_metadata__
special
A dictionary containing metadata about generic Pydantic models.
The origin and args items map to the [__origin__][genericalias.__origin__]
and [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias],
and the parameter item maps to the __parameter__ attribute of generic classes.
__pydantic_parent_namespace__
special
Parent namespace of the model, used for automatic rebuilding of models.
__pydantic_post_init__
special
The name of the post-init method for the model, if defined.
__pydantic_setattr_handlers__
special
__setattr__ handlers. Memoizing the handlers leads to a dramatic performance improvement in __setattr__
__signature__
special
The synthesized __init__ [Signature][inspect.Signature] of the model.
model_config
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
Cluster validity indices for fuzzy partitions.
All indices take the data X (n x d), the membership matrix u (n x c) and
the cluster centers (c x d) of a trained model, and use the euclidean
distance. They need at least two clusters.
davies_bouldin(X, u, centers, m=2.0)
Fuzzy Davies-Bouldin index (lower is better).
The crisp scatter of each cluster is replaced by a membership weighted one, \(S_i = \sqrt{\sum_k u_{ik}^m \lVert x_k - v_i \rVert^2 / \sum_k u_{ik}^m}\), and
Davies, D. L., and D. W. Bouldin. "A cluster separation measure." IEEE TPAMI 1.2 (1979): 224-227.
Source code in fcmeans/validation.py
def davies_bouldin(
X: NDArray, u: NDArray, centers: NDArray, m: float = 2.0
) -> float:
r"""Fuzzy Davies-Bouldin index (lower is better).
The crisp scatter of each cluster is replaced by a membership weighted
one, $S_i = \sqrt{\sum_k u_{ik}^m \lVert x_k - v_i \rVert^2 /
\sum_k u_{ik}^m}$, and
$$
DB = \frac{1}{c} \sum_i \max_{j \neq i}
\frac{S_i + S_j}{\lVert v_i - v_j \rVert}
$$
Davies, D. L., and D. W. Bouldin. "[A cluster separation
measure.](https://doi.org/10.1109/TPAMI.1979.4766909)" _IEEE TPAMI_ 1.2
(1979): 224-227.
"""
d2c = _sq_center_dist(centers)
um = u**m
scatter = np.sqrt(np.sum(um * _sq_dist(X, centers), axis=0) / um.sum(0))
with np.errstate(divide="ignore"):
ratio = (scatter[:, None] + scatter) / np.sqrt(d2c)
np.fill_diagonal(ratio, -np.inf)
return float(ratio.max(axis=1).mean())
fukuyama_sugeno(X, u, centers, m=2.0)
Fukuyama-Sugeno index (lower is better).
where \(\bar{v}\) is the mean of the centers.
Fukuyama, Y., and M. Sugeno. "A new method of choosing the number of clusters for the fuzzy c-means method." Proc. 5th Fuzzy Systems Symposium (1989): 247-250.
Source code in fcmeans/validation.py
def fukuyama_sugeno(
X: NDArray, u: NDArray, centers: NDArray, m: float = 2.0
) -> float:
r"""Fukuyama-Sugeno index (lower is better).
$$
FS = \sum_{i,k} u_{ik}^m \left(\lVert x_k - v_i \rVert^2 -
\lVert v_i - \bar{v} \rVert^2\right)
$$
where $\bar{v}$ is the mean of the centers.
Fukuyama, Y., and M. Sugeno. "A new method of choosing the number of
clusters for the fuzzy c-means method." _Proc. 5th Fuzzy Systems
Symposium_ (1989): 247-250.
"""
_sq_center_dist(centers)
spread = np.sum((centers - centers.mean(axis=0)) ** 2, axis=1)
return float(np.sum(u**m * (_sq_dist(X, centers) - spread)))
fuzzy_silhouette(X, u, centers, m=2.0, alpha=1.0)
Fuzzy silhouette (higher is better).
The silhouette \(s_k\) of each sample is computed on the crisp partition
given by u.argmax(axis=1) and averaged with weights
\((u_{pk} - u_{qk})^\alpha\), where \(u_{pk}\) and \(u_{qk}\) are the two
largest memberships of sample \(k\). Samples with no clear cluster weigh
less. m is unused and only kept for a common signature.
Builds the n x n distance matrix, so it needs O(n^2) memory.
Campello, R. J. G. B., and E. R. Hruschka. "A fuzzy extension of the silhouette width criterion for cluster analysis." Fuzzy Sets and Systems 157.21 (2006): 2858-2875.
Source code in fcmeans/validation.py
def fuzzy_silhouette(
X: NDArray,
u: NDArray,
centers: NDArray,
m: float = 2.0,
alpha: float = 1.0,
) -> float:
r"""Fuzzy silhouette (higher is better).
The silhouette $s_k$ of each sample is computed on the crisp partition
given by `u.argmax(axis=1)` and averaged with weights
$(u_{pk} - u_{qk})^\alpha$, where $u_{pk}$ and $u_{qk}$ are the two
largest memberships of sample $k$. Samples with no clear cluster weigh
less. `m` is unused and only kept for a common signature.
Builds the n x n distance matrix, so it needs O(n^2) memory.
Campello, R. J. G. B., and E. R. Hruschka. "[A fuzzy extension of the
silhouette width criterion for cluster
analysis.](https://doi.org/10.1016/j.fss.2006.07.006)" _Fuzzy Sets and
Systems_ 157.21 (2006): 2858-2875.
"""
_sq_center_dist(centers)
n, c = u.shape
labels = u.argmax(axis=1)
onehot = np.eye(c)[labels]
counts = onehot.sum(axis=0)
if (counts > 0).sum() < 2:
raise ValueError(
"The partition needs at least two non-empty clusters."
)
sq = np.sum(X**2, axis=1)
D = np.sqrt(np.maximum(sq[:, None] + sq - 2 * X @ X.T, 0.0))
sums = D @ onehot # total distance from each sample to each cluster
own = np.arange(n), labels
with np.errstate(divide="ignore", invalid="ignore"):
a = sums[own] / (counts[labels] - 1)
mean_to = sums / counts
mean_to[own] = np.inf
b = mean_to.min(axis=1)
with np.errstate(divide="ignore", invalid="ignore"):
s = (b - a) / np.maximum(a, b)
s[(counts[labels] == 1) | ~np.isfinite(s)] = 0.0 # singleton or all equal
top2 = np.sort(u, axis=1)[:, -2:]
w = (top2[:, 1] - top2[:, 0]) ** alpha
return float(np.sum(w * s) / np.sum(w))
select_n_clusters(X, n_clusters=range(2, 11), index='xie_beni', model=<class 'fcmeans.main.FCM'>, **params)
Fit model for each number of clusters and score it with index.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X |
NDArray |
Data to cluster. |
required |
n_clusters |
Iterable[int] |
Numbers of clusters to try, each >= 2. |
range(2, 11) |
index |
str |
Name of a function in |
'xie_beni' |
model |
type[FCM] |
|
<class 'fcmeans.main.FCM'> |
**params |
Passed to |
{} |
Returns:
| Type | Description |
|---|---|
tuple[int, dict[int, float]] |
The best number of clusters and the score of each one. |
Source code in fcmeans/validation.py
def select_n_clusters(
X: NDArray,
n_clusters: Iterable[int] = range(2, 11),
index: str = "xie_beni",
model: type[FCM] = FCM,
**params,
) -> tuple[int, dict[int, float]]:
"""Fit `model` for each number of clusters and score it with `index`.
Args:
X (NDArray): Data to cluster.
n_clusters (Iterable[int]): Numbers of clusters to try, each >= 2.
index (str): Name of a function in `INDICES`, such as `xie_beni`.
model (type[FCM]): `FCM` or a variant whose `u` rows sum to one.
**params: Passed to `model`, for example `m` or `random_state`.
Returns:
tuple[int, dict[int, float]]: The best number of clusters and the
score of each one.
"""
if index not in INDICES:
raise ValueError(
f"Unknown index {index!r}, use one of {list(INDICES)}"
)
func, higher_is_better = INDICES[index]
scores = {}
for c in n_clusters:
fcm = model(n_clusters=c, **params)
fcm.fit(X)
scores[c] = func(X, fcm.u, fcm.centers, fcm.m)
pick = max if higher_is_better else min
return pick(scores, key=scores.__getitem__), scores
xie_beni(X, u, centers, m=2.0)
Xie-Beni index (lower is better).
Xie, X. L., and G. Beni. "A validity measure for fuzzy clustering." IEEE TPAMI 13.8 (1991): 841-847.
Source code in fcmeans/validation.py
def xie_beni(
X: NDArray, u: NDArray, centers: NDArray, m: float = 2.0
) -> float:
r"""Xie-Beni index (lower is better).
$$
XB = \frac{\sum_{i,k} u_{ik}^m \lVert x_k - v_i \rVert^2}
{n \min_{i \neq j} \lVert v_i - v_j \rVert^2}
$$
Xie, X. L., and G. Beni. "[A validity measure for fuzzy
clustering.](https://doi.org/10.1109/34.85677)" _IEEE TPAMI_ 13.8
(1991): 841-847.
"""
d2c = _sq_center_dist(centers)
compactness = np.sum(u**m * _sq_dist(X, centers))
return float(compactness / (len(X) * d2c.min()))