1373 lines
48 KiB
Python
1373 lines
48 KiB
Python
|
"""Testing for K-means"""
|
||
|
import re
|
||
|
import sys
|
||
|
from io import StringIO
|
||
|
|
||
|
import numpy as np
|
||
|
import pytest
|
||
|
from scipy import sparse as sp
|
||
|
|
||
|
from sklearn.base import clone
|
||
|
from sklearn.cluster import KMeans, MiniBatchKMeans, k_means, kmeans_plusplus
|
||
|
from sklearn.cluster._k_means_common import (
|
||
|
_euclidean_dense_dense_wrapper,
|
||
|
_euclidean_sparse_dense_wrapper,
|
||
|
_inertia_dense,
|
||
|
_inertia_sparse,
|
||
|
_is_same_clustering,
|
||
|
_relocate_empty_clusters_dense,
|
||
|
_relocate_empty_clusters_sparse,
|
||
|
)
|
||
|
from sklearn.cluster._kmeans import _labels_inertia, _mini_batch_step
|
||
|
from sklearn.datasets import make_blobs
|
||
|
from sklearn.exceptions import ConvergenceWarning
|
||
|
from sklearn.metrics import pairwise_distances, pairwise_distances_argmin
|
||
|
from sklearn.metrics.cluster import v_measure_score
|
||
|
from sklearn.metrics.pairwise import euclidean_distances
|
||
|
from sklearn.utils._testing import (
|
||
|
assert_allclose,
|
||
|
assert_array_equal,
|
||
|
create_memmap_backed_data,
|
||
|
)
|
||
|
from sklearn.utils.extmath import row_norms
|
||
|
from sklearn.utils.fixes import CSR_CONTAINERS, threadpool_limits
|
||
|
|
||
|
# non centered, sparse centers to check the
|
||
|
centers = np.array(
|
||
|
[
|
||
|
[0.0, 5.0, 0.0, 0.0, 0.0],
|
||
|
[1.0, 1.0, 4.0, 0.0, 0.0],
|
||
|
[1.0, 0.0, 0.0, 5.0, 1.0],
|
||
|
]
|
||
|
)
|
||
|
n_samples = 100
|
||
|
n_clusters, n_features = centers.shape
|
||
|
X, true_labels = make_blobs(
|
||
|
n_samples=n_samples, centers=centers, cluster_std=1.0, random_state=42
|
||
|
)
|
||
|
X_as_any_csr = [container(X) for container in CSR_CONTAINERS]
|
||
|
data_containers = [np.array] + CSR_CONTAINERS
|
||
|
data_containers_ids = (
|
||
|
["dense", "sparse_matrix", "sparse_array"]
|
||
|
if len(X_as_any_csr) == 2
|
||
|
else ["dense", "sparse_matrix"]
|
||
|
)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("array_constr", data_containers, ids=data_containers_ids)
|
||
|
@pytest.mark.parametrize("algo", ["lloyd", "elkan"])
|
||
|
@pytest.mark.parametrize("dtype", [np.float32, np.float64])
|
||
|
def test_kmeans_results(array_constr, algo, dtype):
|
||
|
# Checks that KMeans works as intended on toy dataset by comparing with
|
||
|
# expected results computed by hand.
|
||
|
X = array_constr([[0, 0], [0.5, 0], [0.5, 1], [1, 1]], dtype=dtype)
|
||
|
sample_weight = [3, 1, 1, 3]
|
||
|
init_centers = np.array([[0, 0], [1, 1]], dtype=dtype)
|
||
|
|
||
|
expected_labels = [0, 0, 1, 1]
|
||
|
expected_inertia = 0.375
|
||
|
expected_centers = np.array([[0.125, 0], [0.875, 1]], dtype=dtype)
|
||
|
expected_n_iter = 2
|
||
|
|
||
|
kmeans = KMeans(n_clusters=2, n_init=1, init=init_centers, algorithm=algo)
|
||
|
kmeans.fit(X, sample_weight=sample_weight)
|
||
|
|
||
|
assert_array_equal(kmeans.labels_, expected_labels)
|
||
|
assert_allclose(kmeans.inertia_, expected_inertia)
|
||
|
assert_allclose(kmeans.cluster_centers_, expected_centers)
|
||
|
assert kmeans.n_iter_ == expected_n_iter
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("array_constr", data_containers, ids=data_containers_ids)
|
||
|
@pytest.mark.parametrize("algo", ["lloyd", "elkan"])
|
||
|
def test_kmeans_relocated_clusters(array_constr, algo):
|
||
|
# check that empty clusters are relocated as expected
|
||
|
X = array_constr([[0, 0], [0.5, 0], [0.5, 1], [1, 1]])
|
||
|
|
||
|
# second center too far from others points will be empty at first iter
|
||
|
init_centers = np.array([[0.5, 0.5], [3, 3]])
|
||
|
|
||
|
kmeans = KMeans(n_clusters=2, n_init=1, init=init_centers, algorithm=algo)
|
||
|
kmeans.fit(X)
|
||
|
|
||
|
expected_n_iter = 3
|
||
|
expected_inertia = 0.25
|
||
|
assert_allclose(kmeans.inertia_, expected_inertia)
|
||
|
assert kmeans.n_iter_ == expected_n_iter
|
||
|
|
||
|
# There are two acceptable ways of relocating clusters in this example, the output
|
||
|
# depends on how the argpartition strategy breaks ties. We accept both outputs.
|
||
|
try:
|
||
|
expected_labels = [0, 0, 1, 1]
|
||
|
expected_centers = [[0.25, 0], [0.75, 1]]
|
||
|
assert_array_equal(kmeans.labels_, expected_labels)
|
||
|
assert_allclose(kmeans.cluster_centers_, expected_centers)
|
||
|
except AssertionError:
|
||
|
expected_labels = [1, 1, 0, 0]
|
||
|
expected_centers = [[0.75, 1.0], [0.25, 0.0]]
|
||
|
assert_array_equal(kmeans.labels_, expected_labels)
|
||
|
assert_allclose(kmeans.cluster_centers_, expected_centers)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("array_constr", data_containers, ids=data_containers_ids)
|
||
|
def test_relocate_empty_clusters(array_constr):
|
||
|
# test for the _relocate_empty_clusters_(dense/sparse) helpers
|
||
|
|
||
|
# Synthetic dataset with 3 obvious clusters of different sizes
|
||
|
X = np.array([-10.0, -9.5, -9, -8.5, -8, -1, 1, 9, 9.5, 10]).reshape(-1, 1)
|
||
|
X = array_constr(X)
|
||
|
sample_weight = np.ones(10)
|
||
|
|
||
|
# centers all initialized to the first point of X
|
||
|
centers_old = np.array([-10.0, -10, -10]).reshape(-1, 1)
|
||
|
|
||
|
# With this initialization, all points will be assigned to the first center
|
||
|
# At this point a center in centers_new is the weighted sum of the points
|
||
|
# it contains if it's not empty, otherwise it is the same as before.
|
||
|
centers_new = np.array([-16.5, -10, -10]).reshape(-1, 1)
|
||
|
weight_in_clusters = np.array([10.0, 0, 0])
|
||
|
labels = np.zeros(10, dtype=np.int32)
|
||
|
|
||
|
if array_constr is np.array:
|
||
|
_relocate_empty_clusters_dense(
|
||
|
X, sample_weight, centers_old, centers_new, weight_in_clusters, labels
|
||
|
)
|
||
|
else:
|
||
|
_relocate_empty_clusters_sparse(
|
||
|
X.data,
|
||
|
X.indices,
|
||
|
X.indptr,
|
||
|
sample_weight,
|
||
|
centers_old,
|
||
|
centers_new,
|
||
|
weight_in_clusters,
|
||
|
labels,
|
||
|
)
|
||
|
|
||
|
# The relocation scheme will take the 2 points farthest from the center and
|
||
|
# assign them to the 2 empty clusters, i.e. points at 10 and at 9.9. The
|
||
|
# first center will be updated to contain the other 8 points.
|
||
|
assert_array_equal(weight_in_clusters, [8, 1, 1])
|
||
|
assert_allclose(centers_new, [[-36], [10], [9.5]])
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("distribution", ["normal", "blobs"])
|
||
|
@pytest.mark.parametrize("array_constr", data_containers, ids=data_containers_ids)
|
||
|
@pytest.mark.parametrize("tol", [1e-2, 1e-8, 1e-100, 0])
|
||
|
def test_kmeans_elkan_results(distribution, array_constr, tol, global_random_seed):
|
||
|
# Check that results are identical between lloyd and elkan algorithms
|
||
|
rnd = np.random.RandomState(global_random_seed)
|
||
|
if distribution == "normal":
|
||
|
X = rnd.normal(size=(5000, 10))
|
||
|
else:
|
||
|
X, _ = make_blobs(random_state=rnd)
|
||
|
X[X < 0] = 0
|
||
|
X = array_constr(X)
|
||
|
|
||
|
km_lloyd = KMeans(n_clusters=5, random_state=global_random_seed, n_init=1, tol=tol)
|
||
|
km_elkan = KMeans(
|
||
|
algorithm="elkan",
|
||
|
n_clusters=5,
|
||
|
random_state=global_random_seed,
|
||
|
n_init=1,
|
||
|
tol=tol,
|
||
|
)
|
||
|
|
||
|
km_lloyd.fit(X)
|
||
|
km_elkan.fit(X)
|
||
|
assert_allclose(km_elkan.cluster_centers_, km_lloyd.cluster_centers_)
|
||
|
assert_array_equal(km_elkan.labels_, km_lloyd.labels_)
|
||
|
assert km_elkan.n_iter_ == km_lloyd.n_iter_
|
||
|
assert km_elkan.inertia_ == pytest.approx(km_lloyd.inertia_, rel=1e-6)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("algorithm", ["lloyd", "elkan"])
|
||
|
def test_kmeans_convergence(algorithm, global_random_seed):
|
||
|
# Check that KMeans stops when convergence is reached when tol=0. (#16075)
|
||
|
rnd = np.random.RandomState(global_random_seed)
|
||
|
X = rnd.normal(size=(5000, 10))
|
||
|
max_iter = 300
|
||
|
|
||
|
km = KMeans(
|
||
|
algorithm=algorithm,
|
||
|
n_clusters=5,
|
||
|
random_state=global_random_seed,
|
||
|
n_init=1,
|
||
|
tol=0,
|
||
|
max_iter=max_iter,
|
||
|
).fit(X)
|
||
|
|
||
|
assert km.n_iter_ < max_iter
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("Estimator", [KMeans, MiniBatchKMeans])
|
||
|
def test_predict_sample_weight_deprecation_warning(Estimator):
|
||
|
X = np.random.rand(100, 2)
|
||
|
sample_weight = np.random.uniform(size=100)
|
||
|
kmeans = Estimator()
|
||
|
kmeans.fit(X, sample_weight=sample_weight)
|
||
|
warn_msg = (
|
||
|
"'sample_weight' was deprecated in version 1.3 and will be removed in 1.5."
|
||
|
)
|
||
|
with pytest.warns(FutureWarning, match=warn_msg):
|
||
|
kmeans.predict(X, sample_weight=sample_weight)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("X_csr", X_as_any_csr)
|
||
|
def test_minibatch_update_consistency(X_csr, global_random_seed):
|
||
|
# Check that dense and sparse minibatch update give the same results
|
||
|
rng = np.random.RandomState(global_random_seed)
|
||
|
|
||
|
centers_old = centers + rng.normal(size=centers.shape)
|
||
|
centers_old_csr = centers_old.copy()
|
||
|
|
||
|
centers_new = np.zeros_like(centers_old)
|
||
|
centers_new_csr = np.zeros_like(centers_old_csr)
|
||
|
|
||
|
weight_sums = np.zeros(centers_old.shape[0], dtype=X.dtype)
|
||
|
weight_sums_csr = np.zeros(centers_old.shape[0], dtype=X.dtype)
|
||
|
|
||
|
sample_weight = np.ones(X.shape[0], dtype=X.dtype)
|
||
|
|
||
|
# extract a small minibatch
|
||
|
X_mb = X[:10]
|
||
|
X_mb_csr = X_csr[:10]
|
||
|
sample_weight_mb = sample_weight[:10]
|
||
|
|
||
|
# step 1: compute the dense minibatch update
|
||
|
old_inertia = _mini_batch_step(
|
||
|
X_mb,
|
||
|
sample_weight_mb,
|
||
|
centers_old,
|
||
|
centers_new,
|
||
|
weight_sums,
|
||
|
np.random.RandomState(global_random_seed),
|
||
|
random_reassign=False,
|
||
|
)
|
||
|
assert old_inertia > 0.0
|
||
|
|
||
|
# compute the new inertia on the same batch to check that it decreased
|
||
|
labels, new_inertia = _labels_inertia(X_mb, sample_weight_mb, centers_new)
|
||
|
assert new_inertia > 0.0
|
||
|
assert new_inertia < old_inertia
|
||
|
|
||
|
# step 2: compute the sparse minibatch update
|
||
|
old_inertia_csr = _mini_batch_step(
|
||
|
X_mb_csr,
|
||
|
sample_weight_mb,
|
||
|
centers_old_csr,
|
||
|
centers_new_csr,
|
||
|
weight_sums_csr,
|
||
|
np.random.RandomState(global_random_seed),
|
||
|
random_reassign=False,
|
||
|
)
|
||
|
assert old_inertia_csr > 0.0
|
||
|
|
||
|
# compute the new inertia on the same batch to check that it decreased
|
||
|
labels_csr, new_inertia_csr = _labels_inertia(
|
||
|
X_mb_csr, sample_weight_mb, centers_new_csr
|
||
|
)
|
||
|
assert new_inertia_csr > 0.0
|
||
|
assert new_inertia_csr < old_inertia_csr
|
||
|
|
||
|
# step 3: check that sparse and dense updates lead to the same results
|
||
|
assert_array_equal(labels, labels_csr)
|
||
|
assert_allclose(centers_new, centers_new_csr)
|
||
|
assert_allclose(old_inertia, old_inertia_csr)
|
||
|
assert_allclose(new_inertia, new_inertia_csr)
|
||
|
|
||
|
|
||
|
def _check_fitted_model(km):
|
||
|
# check that the number of clusters centers and distinct labels match
|
||
|
# the expectation
|
||
|
centers = km.cluster_centers_
|
||
|
assert centers.shape == (n_clusters, n_features)
|
||
|
|
||
|
labels = km.labels_
|
||
|
assert np.unique(labels).shape[0] == n_clusters
|
||
|
|
||
|
# check that the labels assignment are perfect (up to a permutation)
|
||
|
assert_allclose(v_measure_score(true_labels, labels), 1.0)
|
||
|
assert km.inertia_ > 0.0
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize(
|
||
|
"input_data",
|
||
|
[X] + X_as_any_csr,
|
||
|
ids=data_containers_ids,
|
||
|
)
|
||
|
@pytest.mark.parametrize(
|
||
|
"init",
|
||
|
["random", "k-means++", centers, lambda X, k, random_state: centers],
|
||
|
ids=["random", "k-means++", "ndarray", "callable"],
|
||
|
)
|
||
|
@pytest.mark.parametrize("Estimator", [KMeans, MiniBatchKMeans])
|
||
|
def test_all_init(Estimator, input_data, init):
|
||
|
# Check KMeans and MiniBatchKMeans with all possible init.
|
||
|
n_init = 10 if isinstance(init, str) else 1
|
||
|
km = Estimator(
|
||
|
init=init, n_clusters=n_clusters, random_state=42, n_init=n_init
|
||
|
).fit(input_data)
|
||
|
_check_fitted_model(km)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize(
|
||
|
"init",
|
||
|
["random", "k-means++", centers, lambda X, k, random_state: centers],
|
||
|
ids=["random", "k-means++", "ndarray", "callable"],
|
||
|
)
|
||
|
def test_minibatch_kmeans_partial_fit_init(init):
|
||
|
# Check MiniBatchKMeans init with partial_fit
|
||
|
n_init = 10 if isinstance(init, str) else 1
|
||
|
km = MiniBatchKMeans(
|
||
|
init=init, n_clusters=n_clusters, random_state=0, n_init=n_init
|
||
|
)
|
||
|
for i in range(100):
|
||
|
# "random" init requires many batches to recover the true labels.
|
||
|
km.partial_fit(X)
|
||
|
_check_fitted_model(km)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize(
|
||
|
"init, expected_n_init",
|
||
|
[
|
||
|
("k-means++", 1),
|
||
|
("random", "default"),
|
||
|
(
|
||
|
lambda X, n_clusters, random_state: random_state.uniform(
|
||
|
size=(n_clusters, X.shape[1])
|
||
|
),
|
||
|
"default",
|
||
|
),
|
||
|
("array-like", 1),
|
||
|
],
|
||
|
)
|
||
|
@pytest.mark.parametrize("Estimator", [KMeans, MiniBatchKMeans])
|
||
|
def test_kmeans_init_auto_with_initial_centroids(Estimator, init, expected_n_init):
|
||
|
"""Check that `n_init="auto"` chooses the right number of initializations.
|
||
|
Non-regression test for #26657:
|
||
|
https://github.com/scikit-learn/scikit-learn/pull/26657
|
||
|
"""
|
||
|
n_sample, n_features, n_clusters = 100, 10, 5
|
||
|
X = np.random.randn(n_sample, n_features)
|
||
|
if init == "array-like":
|
||
|
init = np.random.randn(n_clusters, n_features)
|
||
|
if expected_n_init == "default":
|
||
|
expected_n_init = 3 if Estimator is MiniBatchKMeans else 10
|
||
|
|
||
|
kmeans = Estimator(n_clusters=n_clusters, init=init, n_init="auto").fit(X)
|
||
|
assert kmeans._n_init == expected_n_init
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("Estimator", [KMeans, MiniBatchKMeans])
|
||
|
def test_fortran_aligned_data(Estimator, global_random_seed):
|
||
|
# Check that KMeans works with fortran-aligned data.
|
||
|
X_fortran = np.asfortranarray(X)
|
||
|
centers_fortran = np.asfortranarray(centers)
|
||
|
|
||
|
km_c = Estimator(
|
||
|
n_clusters=n_clusters, init=centers, n_init=1, random_state=global_random_seed
|
||
|
).fit(X)
|
||
|
km_f = Estimator(
|
||
|
n_clusters=n_clusters,
|
||
|
init=centers_fortran,
|
||
|
n_init=1,
|
||
|
random_state=global_random_seed,
|
||
|
).fit(X_fortran)
|
||
|
assert_allclose(km_c.cluster_centers_, km_f.cluster_centers_)
|
||
|
assert_array_equal(km_c.labels_, km_f.labels_)
|
||
|
|
||
|
|
||
|
def test_minibatch_kmeans_verbose():
|
||
|
# Check verbose mode of MiniBatchKMeans for better coverage.
|
||
|
km = MiniBatchKMeans(n_clusters=n_clusters, random_state=42, verbose=1)
|
||
|
old_stdout = sys.stdout
|
||
|
sys.stdout = StringIO()
|
||
|
try:
|
||
|
km.fit(X)
|
||
|
finally:
|
||
|
sys.stdout = old_stdout
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("algorithm", ["lloyd", "elkan"])
|
||
|
@pytest.mark.parametrize("tol", [1e-2, 0])
|
||
|
def test_kmeans_verbose(algorithm, tol, capsys):
|
||
|
# Check verbose mode of KMeans for better coverage.
|
||
|
X = np.random.RandomState(0).normal(size=(5000, 10))
|
||
|
|
||
|
KMeans(
|
||
|
algorithm=algorithm,
|
||
|
n_clusters=n_clusters,
|
||
|
random_state=42,
|
||
|
init="random",
|
||
|
n_init=1,
|
||
|
tol=tol,
|
||
|
verbose=1,
|
||
|
).fit(X)
|
||
|
|
||
|
captured = capsys.readouterr()
|
||
|
|
||
|
assert re.search(r"Initialization complete", captured.out)
|
||
|
assert re.search(r"Iteration [0-9]+, inertia", captured.out)
|
||
|
|
||
|
if tol == 0:
|
||
|
assert re.search(r"strict convergence", captured.out)
|
||
|
else:
|
||
|
assert re.search(r"center shift .* within tolerance", captured.out)
|
||
|
|
||
|
|
||
|
def test_minibatch_kmeans_warning_init_size():
|
||
|
# Check that a warning is raised when init_size is smaller than n_clusters
|
||
|
with pytest.warns(
|
||
|
RuntimeWarning, match=r"init_size.* should be larger than n_clusters"
|
||
|
):
|
||
|
MiniBatchKMeans(init_size=10, n_clusters=20).fit(X)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("Estimator", [KMeans, MiniBatchKMeans])
|
||
|
def test_warning_n_init_precomputed_centers(Estimator):
|
||
|
# Check that a warning is raised when n_init > 1 and an array is passed for
|
||
|
# the init parameter.
|
||
|
with pytest.warns(
|
||
|
RuntimeWarning,
|
||
|
match="Explicit initial center position passed: performing only one init",
|
||
|
):
|
||
|
Estimator(init=centers, n_clusters=n_clusters, n_init=10).fit(X)
|
||
|
|
||
|
|
||
|
def test_minibatch_sensible_reassign(global_random_seed):
|
||
|
# check that identical initial clusters are reassigned
|
||
|
# also a regression test for when there are more desired reassignments than
|
||
|
# samples.
|
||
|
zeroed_X, true_labels = make_blobs(
|
||
|
n_samples=100, centers=5, random_state=global_random_seed
|
||
|
)
|
||
|
zeroed_X[::2, :] = 0
|
||
|
|
||
|
km = MiniBatchKMeans(
|
||
|
n_clusters=20, batch_size=10, random_state=global_random_seed, init="random"
|
||
|
).fit(zeroed_X)
|
||
|
# there should not be too many exact zero cluster centers
|
||
|
assert km.cluster_centers_.any(axis=1).sum() > 10
|
||
|
|
||
|
# do the same with batch-size > X.shape[0] (regression test)
|
||
|
km = MiniBatchKMeans(
|
||
|
n_clusters=20, batch_size=200, random_state=global_random_seed, init="random"
|
||
|
).fit(zeroed_X)
|
||
|
# there should not be too many exact zero cluster centers
|
||
|
assert km.cluster_centers_.any(axis=1).sum() > 10
|
||
|
|
||
|
# do the same with partial_fit API
|
||
|
km = MiniBatchKMeans(n_clusters=20, random_state=global_random_seed, init="random")
|
||
|
for i in range(100):
|
||
|
km.partial_fit(zeroed_X)
|
||
|
# there should not be too many exact zero cluster centers
|
||
|
assert km.cluster_centers_.any(axis=1).sum() > 10
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize(
|
||
|
"input_data",
|
||
|
[X] + X_as_any_csr,
|
||
|
ids=data_containers_ids,
|
||
|
)
|
||
|
def test_minibatch_reassign(input_data, global_random_seed):
|
||
|
# Check the reassignment part of the minibatch step with very high or very
|
||
|
# low reassignment ratio.
|
||
|
perfect_centers = np.empty((n_clusters, n_features))
|
||
|
for i in range(n_clusters):
|
||
|
perfect_centers[i] = X[true_labels == i].mean(axis=0)
|
||
|
|
||
|
sample_weight = np.ones(n_samples)
|
||
|
centers_new = np.empty_like(perfect_centers)
|
||
|
|
||
|
# Give a perfect initialization, but a large reassignment_ratio, as a
|
||
|
# result many centers should be reassigned and the model should no longer
|
||
|
# be good
|
||
|
score_before = -_labels_inertia(input_data, sample_weight, perfect_centers, 1)[1]
|
||
|
|
||
|
_mini_batch_step(
|
||
|
input_data,
|
||
|
sample_weight,
|
||
|
perfect_centers,
|
||
|
centers_new,
|
||
|
np.zeros(n_clusters),
|
||
|
np.random.RandomState(global_random_seed),
|
||
|
random_reassign=True,
|
||
|
reassignment_ratio=1,
|
||
|
)
|
||
|
|
||
|
score_after = -_labels_inertia(input_data, sample_weight, centers_new, 1)[1]
|
||
|
|
||
|
assert score_before > score_after
|
||
|
|
||
|
# Give a perfect initialization, with a small reassignment_ratio,
|
||
|
# no center should be reassigned.
|
||
|
_mini_batch_step(
|
||
|
input_data,
|
||
|
sample_weight,
|
||
|
perfect_centers,
|
||
|
centers_new,
|
||
|
np.zeros(n_clusters),
|
||
|
np.random.RandomState(global_random_seed),
|
||
|
random_reassign=True,
|
||
|
reassignment_ratio=1e-15,
|
||
|
)
|
||
|
|
||
|
assert_allclose(centers_new, perfect_centers)
|
||
|
|
||
|
|
||
|
def test_minibatch_with_many_reassignments():
|
||
|
# Test for the case that the number of clusters to reassign is bigger
|
||
|
# than the batch_size. Run the test with 100 clusters and a batch_size of
|
||
|
# 10 because it turned out that these values ensure that the number of
|
||
|
# clusters to reassign is always bigger than the batch_size.
|
||
|
MiniBatchKMeans(
|
||
|
n_clusters=100,
|
||
|
batch_size=10,
|
||
|
init_size=n_samples,
|
||
|
random_state=42,
|
||
|
verbose=True,
|
||
|
).fit(X)
|
||
|
|
||
|
|
||
|
def test_minibatch_kmeans_init_size():
|
||
|
# Check the internal _init_size attribute of MiniBatchKMeans
|
||
|
|
||
|
# default init size should be 3 * batch_size
|
||
|
km = MiniBatchKMeans(n_clusters=10, batch_size=5, n_init=1).fit(X)
|
||
|
assert km._init_size == 15
|
||
|
|
||
|
# if 3 * batch size < n_clusters, it should then be 3 * n_clusters
|
||
|
km = MiniBatchKMeans(n_clusters=10, batch_size=1, n_init=1).fit(X)
|
||
|
assert km._init_size == 30
|
||
|
|
||
|
# it should not be larger than n_samples
|
||
|
km = MiniBatchKMeans(
|
||
|
n_clusters=10, batch_size=5, n_init=1, init_size=n_samples + 1
|
||
|
).fit(X)
|
||
|
assert km._init_size == n_samples
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("tol, max_no_improvement", [(1e-4, None), (0, 10)])
|
||
|
def test_minibatch_declared_convergence(capsys, tol, max_no_improvement):
|
||
|
# Check convergence detection based on ewa batch inertia or on
|
||
|
# small center change.
|
||
|
X, _, centers = make_blobs(centers=3, random_state=0, return_centers=True)
|
||
|
|
||
|
km = MiniBatchKMeans(
|
||
|
n_clusters=3,
|
||
|
init=centers,
|
||
|
batch_size=20,
|
||
|
tol=tol,
|
||
|
random_state=0,
|
||
|
max_iter=10,
|
||
|
n_init=1,
|
||
|
verbose=1,
|
||
|
max_no_improvement=max_no_improvement,
|
||
|
)
|
||
|
|
||
|
km.fit(X)
|
||
|
assert 1 < km.n_iter_ < 10
|
||
|
|
||
|
captured = capsys.readouterr()
|
||
|
if max_no_improvement is None:
|
||
|
assert "Converged (small centers change)" in captured.out
|
||
|
if tol == 0:
|
||
|
assert "Converged (lack of improvement in inertia)" in captured.out
|
||
|
|
||
|
|
||
|
def test_minibatch_iter_steps():
|
||
|
# Check consistency of n_iter_ and n_steps_ attributes.
|
||
|
batch_size = 30
|
||
|
n_samples = X.shape[0]
|
||
|
km = MiniBatchKMeans(n_clusters=3, batch_size=batch_size, random_state=0).fit(X)
|
||
|
|
||
|
# n_iter_ is the number of started epochs
|
||
|
assert km.n_iter_ == np.ceil((km.n_steps_ * batch_size) / n_samples)
|
||
|
assert isinstance(km.n_iter_, int)
|
||
|
|
||
|
# without stopping condition, max_iter should be reached
|
||
|
km = MiniBatchKMeans(
|
||
|
n_clusters=3,
|
||
|
batch_size=batch_size,
|
||
|
random_state=0,
|
||
|
tol=0,
|
||
|
max_no_improvement=None,
|
||
|
max_iter=10,
|
||
|
).fit(X)
|
||
|
|
||
|
assert km.n_iter_ == 10
|
||
|
assert km.n_steps_ == (10 * n_samples) // batch_size
|
||
|
assert isinstance(km.n_steps_, int)
|
||
|
|
||
|
|
||
|
def test_kmeans_copyx():
|
||
|
# Check that copy_x=False returns nearly equal X after de-centering.
|
||
|
my_X = X.copy()
|
||
|
km = KMeans(copy_x=False, n_clusters=n_clusters, random_state=42)
|
||
|
km.fit(my_X)
|
||
|
_check_fitted_model(km)
|
||
|
|
||
|
# check that my_X is de-centered
|
||
|
assert_allclose(my_X, X)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("Estimator", [KMeans, MiniBatchKMeans])
|
||
|
def test_score_max_iter(Estimator, global_random_seed):
|
||
|
# Check that fitting KMeans or MiniBatchKMeans with more iterations gives
|
||
|
# better score
|
||
|
X = np.random.RandomState(global_random_seed).randn(100, 10)
|
||
|
|
||
|
km1 = Estimator(n_init=1, random_state=global_random_seed, max_iter=1)
|
||
|
s1 = km1.fit(X).score(X)
|
||
|
km2 = Estimator(n_init=1, random_state=global_random_seed, max_iter=10)
|
||
|
s2 = km2.fit(X).score(X)
|
||
|
assert s2 > s1
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("array_constr", data_containers, ids=data_containers_ids)
|
||
|
@pytest.mark.parametrize(
|
||
|
"Estimator, algorithm",
|
||
|
[(KMeans, "lloyd"), (KMeans, "elkan"), (MiniBatchKMeans, None)],
|
||
|
)
|
||
|
@pytest.mark.parametrize("max_iter", [2, 100])
|
||
|
def test_kmeans_predict(
|
||
|
Estimator, algorithm, array_constr, max_iter, global_dtype, global_random_seed
|
||
|
):
|
||
|
# Check the predict method and the equivalence between fit.predict and
|
||
|
# fit_predict.
|
||
|
X, _ = make_blobs(
|
||
|
n_samples=200, n_features=10, centers=10, random_state=global_random_seed
|
||
|
)
|
||
|
X = array_constr(X, dtype=global_dtype)
|
||
|
|
||
|
km = Estimator(
|
||
|
n_clusters=10,
|
||
|
init="random",
|
||
|
n_init=10,
|
||
|
max_iter=max_iter,
|
||
|
random_state=global_random_seed,
|
||
|
)
|
||
|
if algorithm is not None:
|
||
|
km.set_params(algorithm=algorithm)
|
||
|
km.fit(X)
|
||
|
labels = km.labels_
|
||
|
|
||
|
# re-predict labels for training set using predict
|
||
|
pred = km.predict(X)
|
||
|
assert_array_equal(pred, labels)
|
||
|
|
||
|
# re-predict labels for training set using fit_predict
|
||
|
pred = km.fit_predict(X)
|
||
|
assert_array_equal(pred, labels)
|
||
|
|
||
|
# predict centroid labels
|
||
|
pred = km.predict(km.cluster_centers_)
|
||
|
assert_array_equal(pred, np.arange(10))
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("X_csr", X_as_any_csr)
|
||
|
@pytest.mark.parametrize("Estimator", [KMeans, MiniBatchKMeans])
|
||
|
def test_dense_sparse(Estimator, X_csr, global_random_seed):
|
||
|
# Check that the results are the same for dense and sparse input.
|
||
|
sample_weight = np.random.RandomState(global_random_seed).random_sample(
|
||
|
(n_samples,)
|
||
|
)
|
||
|
km_dense = Estimator(
|
||
|
n_clusters=n_clusters, random_state=global_random_seed, n_init=1
|
||
|
)
|
||
|
km_dense.fit(X, sample_weight=sample_weight)
|
||
|
km_sparse = Estimator(
|
||
|
n_clusters=n_clusters, random_state=global_random_seed, n_init=1
|
||
|
)
|
||
|
km_sparse.fit(X_csr, sample_weight=sample_weight)
|
||
|
|
||
|
assert_array_equal(km_dense.labels_, km_sparse.labels_)
|
||
|
assert_allclose(km_dense.cluster_centers_, km_sparse.cluster_centers_)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("X_csr", X_as_any_csr)
|
||
|
@pytest.mark.parametrize(
|
||
|
"init", ["random", "k-means++", centers], ids=["random", "k-means++", "ndarray"]
|
||
|
)
|
||
|
@pytest.mark.parametrize("Estimator", [KMeans, MiniBatchKMeans])
|
||
|
def test_predict_dense_sparse(Estimator, init, X_csr):
|
||
|
# check that models trained on sparse input also works for dense input at
|
||
|
# predict time and vice versa.
|
||
|
n_init = 10 if isinstance(init, str) else 1
|
||
|
km = Estimator(n_clusters=n_clusters, init=init, n_init=n_init, random_state=0)
|
||
|
|
||
|
km.fit(X_csr)
|
||
|
assert_array_equal(km.predict(X), km.labels_)
|
||
|
|
||
|
km.fit(X)
|
||
|
assert_array_equal(km.predict(X_csr), km.labels_)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("array_constr", data_containers, ids=data_containers_ids)
|
||
|
@pytest.mark.parametrize("dtype", [np.int32, np.int64])
|
||
|
@pytest.mark.parametrize("init", ["k-means++", "ndarray"])
|
||
|
@pytest.mark.parametrize("Estimator", [KMeans, MiniBatchKMeans])
|
||
|
def test_integer_input(Estimator, array_constr, dtype, init, global_random_seed):
|
||
|
# Check that KMeans and MiniBatchKMeans work with integer input.
|
||
|
X_dense = np.array([[0, 0], [10, 10], [12, 9], [-1, 1], [2, 0], [8, 10]])
|
||
|
X = array_constr(X_dense, dtype=dtype)
|
||
|
|
||
|
n_init = 1 if init == "ndarray" else 10
|
||
|
init = X_dense[:2] if init == "ndarray" else init
|
||
|
|
||
|
km = Estimator(
|
||
|
n_clusters=2, init=init, n_init=n_init, random_state=global_random_seed
|
||
|
)
|
||
|
if Estimator is MiniBatchKMeans:
|
||
|
km.set_params(batch_size=2)
|
||
|
|
||
|
km.fit(X)
|
||
|
|
||
|
# Internally integer input should be converted to float64
|
||
|
assert km.cluster_centers_.dtype == np.float64
|
||
|
|
||
|
expected_labels = [0, 1, 1, 0, 0, 1]
|
||
|
assert_allclose(v_measure_score(km.labels_, expected_labels), 1.0)
|
||
|
|
||
|
# Same with partial_fit (#14314)
|
||
|
if Estimator is MiniBatchKMeans:
|
||
|
km = clone(km).partial_fit(X)
|
||
|
assert km.cluster_centers_.dtype == np.float64
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("Estimator", [KMeans, MiniBatchKMeans])
|
||
|
def test_transform(Estimator, global_random_seed):
|
||
|
# Check the transform method
|
||
|
km = Estimator(n_clusters=n_clusters, random_state=global_random_seed).fit(X)
|
||
|
|
||
|
# Transorfming cluster_centers_ should return the pairwise distances
|
||
|
# between centers
|
||
|
Xt = km.transform(km.cluster_centers_)
|
||
|
assert_allclose(Xt, pairwise_distances(km.cluster_centers_))
|
||
|
# In particular, diagonal must be 0
|
||
|
assert_array_equal(Xt.diagonal(), np.zeros(n_clusters))
|
||
|
|
||
|
# Transorfming X should return the pairwise distances between X and the
|
||
|
# centers
|
||
|
Xt = km.transform(X)
|
||
|
assert_allclose(Xt, pairwise_distances(X, km.cluster_centers_))
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("Estimator", [KMeans, MiniBatchKMeans])
|
||
|
def test_fit_transform(Estimator, global_random_seed):
|
||
|
# Check equivalence between fit.transform and fit_transform
|
||
|
X1 = Estimator(random_state=global_random_seed, n_init=1).fit(X).transform(X)
|
||
|
X2 = Estimator(random_state=global_random_seed, n_init=1).fit_transform(X)
|
||
|
assert_allclose(X1, X2)
|
||
|
|
||
|
|
||
|
def test_n_init(global_random_seed):
|
||
|
# Check that increasing the number of init increases the quality
|
||
|
previous_inertia = np.inf
|
||
|
for n_init in [1, 5, 10]:
|
||
|
# set max_iter=1 to avoid finding the global minimum and get the same
|
||
|
# inertia each time
|
||
|
km = KMeans(
|
||
|
n_clusters=n_clusters,
|
||
|
init="random",
|
||
|
n_init=n_init,
|
||
|
random_state=global_random_seed,
|
||
|
max_iter=1,
|
||
|
).fit(X)
|
||
|
assert km.inertia_ <= previous_inertia
|
||
|
|
||
|
|
||
|
def test_k_means_function(global_random_seed):
|
||
|
# test calling the k_means function directly
|
||
|
cluster_centers, labels, inertia = k_means(
|
||
|
X, n_clusters=n_clusters, sample_weight=None, random_state=global_random_seed
|
||
|
)
|
||
|
|
||
|
assert cluster_centers.shape == (n_clusters, n_features)
|
||
|
assert np.unique(labels).shape[0] == n_clusters
|
||
|
|
||
|
# check that the labels assignment are perfect (up to a permutation)
|
||
|
assert_allclose(v_measure_score(true_labels, labels), 1.0)
|
||
|
assert inertia > 0.0
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize(
|
||
|
"input_data",
|
||
|
[X] + X_as_any_csr,
|
||
|
ids=data_containers_ids,
|
||
|
)
|
||
|
@pytest.mark.parametrize("Estimator", [KMeans, MiniBatchKMeans])
|
||
|
def test_float_precision(Estimator, input_data, global_random_seed):
|
||
|
# Check that the results are the same for single and double precision.
|
||
|
km = Estimator(n_init=1, random_state=global_random_seed)
|
||
|
|
||
|
inertia = {}
|
||
|
Xt = {}
|
||
|
centers = {}
|
||
|
labels = {}
|
||
|
|
||
|
for dtype in [np.float64, np.float32]:
|
||
|
X = input_data.astype(dtype, copy=False)
|
||
|
km.fit(X)
|
||
|
|
||
|
inertia[dtype] = km.inertia_
|
||
|
Xt[dtype] = km.transform(X)
|
||
|
centers[dtype] = km.cluster_centers_
|
||
|
labels[dtype] = km.labels_
|
||
|
|
||
|
# dtype of cluster centers has to be the dtype of the input data
|
||
|
assert km.cluster_centers_.dtype == dtype
|
||
|
|
||
|
# same with partial_fit
|
||
|
if Estimator is MiniBatchKMeans:
|
||
|
km.partial_fit(X[0:3])
|
||
|
assert km.cluster_centers_.dtype == dtype
|
||
|
|
||
|
# compare arrays with low precision since the difference between 32 and
|
||
|
# 64 bit comes from an accumulation of rounding errors.
|
||
|
assert_allclose(inertia[np.float32], inertia[np.float64], rtol=1e-4)
|
||
|
assert_allclose(Xt[np.float32], Xt[np.float64], atol=Xt[np.float64].max() * 1e-4)
|
||
|
assert_allclose(
|
||
|
centers[np.float32], centers[np.float64], atol=centers[np.float64].max() * 1e-4
|
||
|
)
|
||
|
assert_array_equal(labels[np.float32], labels[np.float64])
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("dtype", [np.int32, np.int64, np.float32, np.float64])
|
||
|
@pytest.mark.parametrize("Estimator", [KMeans, MiniBatchKMeans])
|
||
|
def test_centers_not_mutated(Estimator, dtype):
|
||
|
# Check that KMeans and MiniBatchKMeans won't mutate the user provided
|
||
|
# init centers silently even if input data and init centers have the same
|
||
|
# type.
|
||
|
X_new_type = X.astype(dtype, copy=False)
|
||
|
centers_new_type = centers.astype(dtype, copy=False)
|
||
|
|
||
|
km = Estimator(init=centers_new_type, n_clusters=n_clusters, n_init=1)
|
||
|
km.fit(X_new_type)
|
||
|
|
||
|
assert not np.may_share_memory(km.cluster_centers_, centers_new_type)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize(
|
||
|
"input_data",
|
||
|
[X] + X_as_any_csr,
|
||
|
ids=data_containers_ids,
|
||
|
)
|
||
|
def test_kmeans_init_fitted_centers(input_data):
|
||
|
# Check that starting fitting from a local optimum shouldn't change the
|
||
|
# solution
|
||
|
km1 = KMeans(n_clusters=n_clusters).fit(input_data)
|
||
|
km2 = KMeans(n_clusters=n_clusters, init=km1.cluster_centers_, n_init=1).fit(
|
||
|
input_data
|
||
|
)
|
||
|
|
||
|
assert_allclose(km1.cluster_centers_, km2.cluster_centers_)
|
||
|
|
||
|
|
||
|
def test_kmeans_warns_less_centers_than_unique_points(global_random_seed):
|
||
|
# Check KMeans when the number of found clusters is smaller than expected
|
||
|
X = np.asarray([[0, 0], [0, 1], [1, 0], [1, 0]]) # last point is duplicated
|
||
|
km = KMeans(n_clusters=4, random_state=global_random_seed)
|
||
|
|
||
|
# KMeans should warn that fewer labels than cluster centers have been used
|
||
|
msg = (
|
||
|
r"Number of distinct clusters \(3\) found smaller than "
|
||
|
r"n_clusters \(4\). Possibly due to duplicate points in X."
|
||
|
)
|
||
|
with pytest.warns(ConvergenceWarning, match=msg):
|
||
|
km.fit(X)
|
||
|
# only three distinct points, so only three clusters
|
||
|
# can have points assigned to them
|
||
|
assert set(km.labels_) == set(range(3))
|
||
|
|
||
|
|
||
|
def _sort_centers(centers):
|
||
|
return np.sort(centers, axis=0)
|
||
|
|
||
|
|
||
|
def test_weighted_vs_repeated(global_random_seed):
|
||
|
# Check that a sample weight of N should yield the same result as an N-fold
|
||
|
# repetition of the sample. Valid only if init is precomputed, otherwise
|
||
|
# rng produces different results. Not valid for MinibatchKMeans due to rng
|
||
|
# to extract minibatches.
|
||
|
sample_weight = np.random.RandomState(global_random_seed).randint(
|
||
|
1, 5, size=n_samples
|
||
|
)
|
||
|
X_repeat = np.repeat(X, sample_weight, axis=0)
|
||
|
|
||
|
km = KMeans(
|
||
|
init=centers, n_init=1, n_clusters=n_clusters, random_state=global_random_seed
|
||
|
)
|
||
|
|
||
|
km_weighted = clone(km).fit(X, sample_weight=sample_weight)
|
||
|
repeated_labels = np.repeat(km_weighted.labels_, sample_weight)
|
||
|
km_repeated = clone(km).fit(X_repeat)
|
||
|
|
||
|
assert_array_equal(km_repeated.labels_, repeated_labels)
|
||
|
assert_allclose(km_weighted.inertia_, km_repeated.inertia_)
|
||
|
assert_allclose(
|
||
|
_sort_centers(km_weighted.cluster_centers_),
|
||
|
_sort_centers(km_repeated.cluster_centers_),
|
||
|
)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize(
|
||
|
"input_data",
|
||
|
[X] + X_as_any_csr,
|
||
|
ids=data_containers_ids,
|
||
|
)
|
||
|
@pytest.mark.parametrize("Estimator", [KMeans, MiniBatchKMeans])
|
||
|
def test_unit_weights_vs_no_weights(Estimator, input_data, global_random_seed):
|
||
|
# Check that not passing sample weights should be equivalent to passing
|
||
|
# sample weights all equal to one.
|
||
|
sample_weight = np.ones(n_samples)
|
||
|
|
||
|
km = Estimator(n_clusters=n_clusters, random_state=global_random_seed, n_init=1)
|
||
|
km_none = clone(km).fit(input_data, sample_weight=None)
|
||
|
km_ones = clone(km).fit(input_data, sample_weight=sample_weight)
|
||
|
|
||
|
assert_array_equal(km_none.labels_, km_ones.labels_)
|
||
|
assert_allclose(km_none.cluster_centers_, km_ones.cluster_centers_)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize(
|
||
|
"input_data",
|
||
|
[X] + X_as_any_csr,
|
||
|
ids=data_containers_ids,
|
||
|
)
|
||
|
@pytest.mark.parametrize("Estimator", [KMeans, MiniBatchKMeans])
|
||
|
def test_scaled_weights(Estimator, input_data, global_random_seed):
|
||
|
# Check that scaling all sample weights by a common factor
|
||
|
# shouldn't change the result
|
||
|
sample_weight = np.random.RandomState(global_random_seed).uniform(size=n_samples)
|
||
|
|
||
|
km = Estimator(n_clusters=n_clusters, random_state=global_random_seed, n_init=1)
|
||
|
km_orig = clone(km).fit(input_data, sample_weight=sample_weight)
|
||
|
km_scaled = clone(km).fit(input_data, sample_weight=0.5 * sample_weight)
|
||
|
|
||
|
assert_array_equal(km_orig.labels_, km_scaled.labels_)
|
||
|
assert_allclose(km_orig.cluster_centers_, km_scaled.cluster_centers_)
|
||
|
|
||
|
|
||
|
def test_kmeans_elkan_iter_attribute():
|
||
|
# Regression test on bad n_iter_ value. Previous bug n_iter_ was one off
|
||
|
# it's right value (#11340).
|
||
|
km = KMeans(algorithm="elkan", max_iter=1).fit(X)
|
||
|
assert km.n_iter_ == 1
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("array_constr", data_containers, ids=data_containers_ids)
|
||
|
def test_kmeans_empty_cluster_relocated(array_constr):
|
||
|
# check that empty clusters are correctly relocated when using sample
|
||
|
# weights (#13486)
|
||
|
X = array_constr([[-1], [1]])
|
||
|
sample_weight = [1.9, 0.1]
|
||
|
init = np.array([[-1], [10]])
|
||
|
|
||
|
km = KMeans(n_clusters=2, init=init, n_init=1)
|
||
|
km.fit(X, sample_weight=sample_weight)
|
||
|
|
||
|
assert len(set(km.labels_)) == 2
|
||
|
assert_allclose(km.cluster_centers_, [[-1], [1]])
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("Estimator", [KMeans, MiniBatchKMeans])
|
||
|
def test_result_equal_in_diff_n_threads(Estimator, global_random_seed):
|
||
|
# Check that KMeans/MiniBatchKMeans give the same results in parallel mode
|
||
|
# than in sequential mode.
|
||
|
rnd = np.random.RandomState(global_random_seed)
|
||
|
X = rnd.normal(size=(50, 10))
|
||
|
|
||
|
with threadpool_limits(limits=1, user_api="openmp"):
|
||
|
result_1 = (
|
||
|
Estimator(n_clusters=n_clusters, random_state=global_random_seed)
|
||
|
.fit(X)
|
||
|
.labels_
|
||
|
)
|
||
|
with threadpool_limits(limits=2, user_api="openmp"):
|
||
|
result_2 = (
|
||
|
Estimator(n_clusters=n_clusters, random_state=global_random_seed)
|
||
|
.fit(X)
|
||
|
.labels_
|
||
|
)
|
||
|
assert_array_equal(result_1, result_2)
|
||
|
|
||
|
|
||
|
def test_warning_elkan_1_cluster():
|
||
|
# Check warning messages specific to KMeans
|
||
|
with pytest.warns(
|
||
|
RuntimeWarning,
|
||
|
match="algorithm='elkan' doesn't make sense for a single cluster",
|
||
|
):
|
||
|
KMeans(n_clusters=1, algorithm="elkan").fit(X)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("array_constr", data_containers, ids=data_containers_ids)
|
||
|
@pytest.mark.parametrize("algo", ["lloyd", "elkan"])
|
||
|
def test_k_means_1_iteration(array_constr, algo, global_random_seed):
|
||
|
# check the results after a single iteration (E-step M-step E-step) by
|
||
|
# comparing against a pure python implementation.
|
||
|
X = np.random.RandomState(global_random_seed).uniform(size=(100, 5))
|
||
|
init_centers = X[:5]
|
||
|
X = array_constr(X)
|
||
|
|
||
|
def py_kmeans(X, init):
|
||
|
new_centers = init.copy()
|
||
|
labels = pairwise_distances_argmin(X, init)
|
||
|
for label in range(init.shape[0]):
|
||
|
new_centers[label] = X[labels == label].mean(axis=0)
|
||
|
labels = pairwise_distances_argmin(X, new_centers)
|
||
|
return labels, new_centers
|
||
|
|
||
|
py_labels, py_centers = py_kmeans(X, init_centers)
|
||
|
|
||
|
cy_kmeans = KMeans(
|
||
|
n_clusters=5, n_init=1, init=init_centers, algorithm=algo, max_iter=1
|
||
|
).fit(X)
|
||
|
cy_labels = cy_kmeans.labels_
|
||
|
cy_centers = cy_kmeans.cluster_centers_
|
||
|
|
||
|
assert_array_equal(py_labels, cy_labels)
|
||
|
assert_allclose(py_centers, cy_centers)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("dtype", [np.float32, np.float64])
|
||
|
@pytest.mark.parametrize("squared", [True, False])
|
||
|
def test_euclidean_distance(dtype, squared, global_random_seed):
|
||
|
# Check that the _euclidean_(dense/sparse)_dense helpers produce correct
|
||
|
# results
|
||
|
rng = np.random.RandomState(global_random_seed)
|
||
|
a_sparse = sp.random(
|
||
|
1, 100, density=0.5, format="csr", random_state=rng, dtype=dtype
|
||
|
)
|
||
|
a_dense = a_sparse.toarray().reshape(-1)
|
||
|
b = rng.randn(100).astype(dtype, copy=False)
|
||
|
b_squared_norm = (b**2).sum()
|
||
|
|
||
|
expected = ((a_dense - b) ** 2).sum()
|
||
|
expected = expected if squared else np.sqrt(expected)
|
||
|
|
||
|
distance_dense_dense = _euclidean_dense_dense_wrapper(a_dense, b, squared)
|
||
|
distance_sparse_dense = _euclidean_sparse_dense_wrapper(
|
||
|
a_sparse.data, a_sparse.indices, b, b_squared_norm, squared
|
||
|
)
|
||
|
|
||
|
rtol = 1e-4 if dtype == np.float32 else 1e-7
|
||
|
assert_allclose(distance_dense_dense, distance_sparse_dense, rtol=rtol)
|
||
|
assert_allclose(distance_dense_dense, expected, rtol=rtol)
|
||
|
assert_allclose(distance_sparse_dense, expected, rtol=rtol)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("dtype", [np.float32, np.float64])
|
||
|
def test_inertia(dtype, global_random_seed):
|
||
|
# Check that the _inertia_(dense/sparse) helpers produce correct results.
|
||
|
rng = np.random.RandomState(global_random_seed)
|
||
|
X_sparse = sp.random(
|
||
|
100, 10, density=0.5, format="csr", random_state=rng, dtype=dtype
|
||
|
)
|
||
|
X_dense = X_sparse.toarray()
|
||
|
sample_weight = rng.randn(100).astype(dtype, copy=False)
|
||
|
centers = rng.randn(5, 10).astype(dtype, copy=False)
|
||
|
labels = rng.randint(5, size=100, dtype=np.int32)
|
||
|
|
||
|
distances = ((X_dense - centers[labels]) ** 2).sum(axis=1)
|
||
|
expected = np.sum(distances * sample_weight)
|
||
|
|
||
|
inertia_dense = _inertia_dense(X_dense, sample_weight, centers, labels, n_threads=1)
|
||
|
inertia_sparse = _inertia_sparse(
|
||
|
X_sparse, sample_weight, centers, labels, n_threads=1
|
||
|
)
|
||
|
|
||
|
rtol = 1e-4 if dtype == np.float32 else 1e-6
|
||
|
assert_allclose(inertia_dense, inertia_sparse, rtol=rtol)
|
||
|
assert_allclose(inertia_dense, expected, rtol=rtol)
|
||
|
assert_allclose(inertia_sparse, expected, rtol=rtol)
|
||
|
|
||
|
# Check the single_label parameter.
|
||
|
label = 1
|
||
|
mask = labels == label
|
||
|
distances = ((X_dense[mask] - centers[label]) ** 2).sum(axis=1)
|
||
|
expected = np.sum(distances * sample_weight[mask])
|
||
|
|
||
|
inertia_dense = _inertia_dense(
|
||
|
X_dense, sample_weight, centers, labels, n_threads=1, single_label=label
|
||
|
)
|
||
|
inertia_sparse = _inertia_sparse(
|
||
|
X_sparse, sample_weight, centers, labels, n_threads=1, single_label=label
|
||
|
)
|
||
|
|
||
|
assert_allclose(inertia_dense, inertia_sparse, rtol=rtol)
|
||
|
assert_allclose(inertia_dense, expected, rtol=rtol)
|
||
|
assert_allclose(inertia_sparse, expected, rtol=rtol)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("Klass, default_n_init", [(KMeans, 10), (MiniBatchKMeans, 3)])
|
||
|
def test_n_init_auto(Klass, default_n_init):
|
||
|
est = Klass(n_init="auto", init="k-means++")
|
||
|
est.fit(X)
|
||
|
assert est._n_init == 1
|
||
|
|
||
|
est = Klass(n_init="auto", init="random")
|
||
|
est.fit(X)
|
||
|
assert est._n_init == 10 if Klass.__name__ == "KMeans" else 3
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("Estimator", [KMeans, MiniBatchKMeans])
|
||
|
def test_sample_weight_unchanged(Estimator):
|
||
|
# Check that sample_weight is not modified in place by KMeans (#17204)
|
||
|
X = np.array([[1], [2], [4]])
|
||
|
sample_weight = np.array([0.5, 0.2, 0.3])
|
||
|
Estimator(n_clusters=2, random_state=0).fit(X, sample_weight=sample_weight)
|
||
|
|
||
|
assert_array_equal(sample_weight, np.array([0.5, 0.2, 0.3]))
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("Estimator", [KMeans, MiniBatchKMeans])
|
||
|
@pytest.mark.parametrize(
|
||
|
"param, match",
|
||
|
[
|
||
|
({"n_clusters": n_samples + 1}, r"n_samples.* should be >= n_clusters"),
|
||
|
(
|
||
|
{"init": X[:2]},
|
||
|
r"The shape of the initial centers .* does not match "
|
||
|
r"the number of clusters",
|
||
|
),
|
||
|
(
|
||
|
{"init": lambda X_, k, random_state: X_[:2]},
|
||
|
r"The shape of the initial centers .* does not match "
|
||
|
r"the number of clusters",
|
||
|
),
|
||
|
(
|
||
|
{"init": X[:8, :2]},
|
||
|
r"The shape of the initial centers .* does not match "
|
||
|
r"the number of features of the data",
|
||
|
),
|
||
|
(
|
||
|
{"init": lambda X_, k, random_state: X_[:8, :2]},
|
||
|
r"The shape of the initial centers .* does not match "
|
||
|
r"the number of features of the data",
|
||
|
),
|
||
|
],
|
||
|
)
|
||
|
def test_wrong_params(Estimator, param, match):
|
||
|
# Check that error are raised with clear error message when wrong values
|
||
|
# are passed for the parameters
|
||
|
# Set n_init=1 by default to avoid warning with precomputed init
|
||
|
km = Estimator(n_init=1)
|
||
|
with pytest.raises(ValueError, match=match):
|
||
|
km.set_params(**param).fit(X)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize(
|
||
|
"param, match",
|
||
|
[
|
||
|
(
|
||
|
{"x_squared_norms": X[:2]},
|
||
|
r"The length of x_squared_norms .* should "
|
||
|
r"be equal to the length of n_samples",
|
||
|
),
|
||
|
],
|
||
|
)
|
||
|
def test_kmeans_plusplus_wrong_params(param, match):
|
||
|
with pytest.raises(ValueError, match=match):
|
||
|
kmeans_plusplus(X, n_clusters, **param)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize(
|
||
|
"input_data",
|
||
|
[X] + X_as_any_csr,
|
||
|
)
|
||
|
@pytest.mark.parametrize("dtype", [np.float64, np.float32])
|
||
|
def test_kmeans_plusplus_output(input_data, dtype, global_random_seed):
|
||
|
# Check for the correct number of seeds and all positive values
|
||
|
data = input_data.astype(dtype)
|
||
|
centers, indices = kmeans_plusplus(
|
||
|
data, n_clusters, random_state=global_random_seed
|
||
|
)
|
||
|
|
||
|
# Check there are the correct number of indices and that all indices are
|
||
|
# positive and within the number of samples
|
||
|
assert indices.shape[0] == n_clusters
|
||
|
assert (indices >= 0).all()
|
||
|
assert (indices <= data.shape[0]).all()
|
||
|
|
||
|
# Check for the correct number of seeds and that they are bound by the data
|
||
|
assert centers.shape[0] == n_clusters
|
||
|
assert (centers.max(axis=0) <= data.max(axis=0)).all()
|
||
|
assert (centers.min(axis=0) >= data.min(axis=0)).all()
|
||
|
|
||
|
# Check that indices correspond to reported centers
|
||
|
# Use X for comparison rather than data, test still works against centers
|
||
|
# calculated with sparse data.
|
||
|
assert_allclose(X[indices].astype(dtype), centers)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("x_squared_norms", [row_norms(X, squared=True), None])
|
||
|
def test_kmeans_plusplus_norms(x_squared_norms):
|
||
|
# Check that defining x_squared_norms returns the same as default=None.
|
||
|
centers, indices = kmeans_plusplus(X, n_clusters, x_squared_norms=x_squared_norms)
|
||
|
|
||
|
assert_allclose(X[indices], centers)
|
||
|
|
||
|
|
||
|
def test_kmeans_plusplus_dataorder(global_random_seed):
|
||
|
# Check that memory layout does not effect result
|
||
|
centers_c, _ = kmeans_plusplus(X, n_clusters, random_state=global_random_seed)
|
||
|
|
||
|
X_fortran = np.asfortranarray(X)
|
||
|
|
||
|
centers_fortran, _ = kmeans_plusplus(
|
||
|
X_fortran, n_clusters, random_state=global_random_seed
|
||
|
)
|
||
|
|
||
|
assert_allclose(centers_c, centers_fortran)
|
||
|
|
||
|
|
||
|
def test_is_same_clustering():
|
||
|
# Sanity check for the _is_same_clustering utility function
|
||
|
labels1 = np.array([1, 0, 0, 1, 2, 0, 2, 1], dtype=np.int32)
|
||
|
assert _is_same_clustering(labels1, labels1, 3)
|
||
|
|
||
|
# these other labels represent the same clustering since we can retrieve the first
|
||
|
# labels by simply renaming the labels: 0 -> 1, 1 -> 2, 2 -> 0.
|
||
|
labels2 = np.array([0, 2, 2, 0, 1, 2, 1, 0], dtype=np.int32)
|
||
|
assert _is_same_clustering(labels1, labels2, 3)
|
||
|
|
||
|
# these other labels do not represent the same clustering since not all ones are
|
||
|
# mapped to a same value
|
||
|
labels3 = np.array([1, 0, 0, 2, 2, 0, 2, 1], dtype=np.int32)
|
||
|
assert not _is_same_clustering(labels1, labels3, 3)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize(
|
||
|
"kwargs", ({"init": np.str_("k-means++")}, {"init": [[0, 0], [1, 1]], "n_init": 1})
|
||
|
)
|
||
|
def test_kmeans_with_array_like_or_np_scalar_init(kwargs):
|
||
|
"""Check that init works with numpy scalar strings.
|
||
|
|
||
|
Non-regression test for #21964.
|
||
|
"""
|
||
|
X = np.asarray([[0, 0], [0.5, 0], [0.5, 1], [1, 1]], dtype=np.float64)
|
||
|
|
||
|
clustering = KMeans(n_clusters=2, **kwargs)
|
||
|
# Does not raise
|
||
|
clustering.fit(X)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize(
|
||
|
"Klass, method",
|
||
|
[(KMeans, "fit"), (MiniBatchKMeans, "fit"), (MiniBatchKMeans, "partial_fit")],
|
||
|
)
|
||
|
def test_feature_names_out(Klass, method):
|
||
|
"""Check `feature_names_out` for `KMeans` and `MiniBatchKMeans`."""
|
||
|
class_name = Klass.__name__.lower()
|
||
|
kmeans = Klass()
|
||
|
getattr(kmeans, method)(X)
|
||
|
n_clusters = kmeans.cluster_centers_.shape[0]
|
||
|
|
||
|
names_out = kmeans.get_feature_names_out()
|
||
|
assert_array_equal([f"{class_name}{i}" for i in range(n_clusters)], names_out)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("csr_container", CSR_CONTAINERS + [None])
|
||
|
def test_predict_does_not_change_cluster_centers(csr_container):
|
||
|
"""Check that predict does not change cluster centers.
|
||
|
|
||
|
Non-regression test for gh-24253.
|
||
|
"""
|
||
|
X, _ = make_blobs(n_samples=200, n_features=10, centers=10, random_state=0)
|
||
|
if csr_container is not None:
|
||
|
X = csr_container(X)
|
||
|
|
||
|
kmeans = KMeans()
|
||
|
y_pred1 = kmeans.fit_predict(X)
|
||
|
# Make cluster_centers readonly
|
||
|
kmeans.cluster_centers_ = create_memmap_backed_data(kmeans.cluster_centers_)
|
||
|
kmeans.labels_ = create_memmap_backed_data(kmeans.labels_)
|
||
|
|
||
|
y_pred2 = kmeans.predict(X)
|
||
|
assert_array_equal(y_pred1, y_pred2)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("init", ["k-means++", "random"])
|
||
|
def test_sample_weight_init(init, global_random_seed):
|
||
|
"""Check that sample weight is used during init.
|
||
|
|
||
|
`_init_centroids` is shared across all classes inheriting from _BaseKMeans so
|
||
|
it's enough to check for KMeans.
|
||
|
"""
|
||
|
rng = np.random.RandomState(global_random_seed)
|
||
|
X, _ = make_blobs(
|
||
|
n_samples=200, n_features=10, centers=10, random_state=global_random_seed
|
||
|
)
|
||
|
x_squared_norms = row_norms(X, squared=True)
|
||
|
|
||
|
kmeans = KMeans()
|
||
|
clusters_weighted = kmeans._init_centroids(
|
||
|
X=X,
|
||
|
x_squared_norms=x_squared_norms,
|
||
|
init=init,
|
||
|
sample_weight=rng.uniform(size=X.shape[0]),
|
||
|
n_centroids=5,
|
||
|
random_state=np.random.RandomState(global_random_seed),
|
||
|
)
|
||
|
clusters = kmeans._init_centroids(
|
||
|
X=X,
|
||
|
x_squared_norms=x_squared_norms,
|
||
|
init=init,
|
||
|
sample_weight=np.ones(X.shape[0]),
|
||
|
n_centroids=5,
|
||
|
random_state=np.random.RandomState(global_random_seed),
|
||
|
)
|
||
|
with pytest.raises(AssertionError):
|
||
|
assert_allclose(clusters_weighted, clusters)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("init", ["k-means++", "random"])
|
||
|
def test_sample_weight_zero(init, global_random_seed):
|
||
|
"""Check that if sample weight is 0, this sample won't be chosen.
|
||
|
|
||
|
`_init_centroids` is shared across all classes inheriting from _BaseKMeans so
|
||
|
it's enough to check for KMeans.
|
||
|
"""
|
||
|
rng = np.random.RandomState(global_random_seed)
|
||
|
X, _ = make_blobs(
|
||
|
n_samples=100, n_features=5, centers=5, random_state=global_random_seed
|
||
|
)
|
||
|
sample_weight = rng.uniform(size=X.shape[0])
|
||
|
sample_weight[::2] = 0
|
||
|
x_squared_norms = row_norms(X, squared=True)
|
||
|
|
||
|
kmeans = KMeans()
|
||
|
clusters_weighted = kmeans._init_centroids(
|
||
|
X=X,
|
||
|
x_squared_norms=x_squared_norms,
|
||
|
init=init,
|
||
|
sample_weight=sample_weight,
|
||
|
n_centroids=10,
|
||
|
random_state=np.random.RandomState(global_random_seed),
|
||
|
)
|
||
|
# No center should be one of the 0 sample weight point
|
||
|
# (i.e. be at a distance=0 from it)
|
||
|
d = euclidean_distances(X[::2], clusters_weighted)
|
||
|
assert not np.any(np.isclose(d, 0))
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("array_constr", data_containers, ids=data_containers_ids)
|
||
|
@pytest.mark.parametrize("algorithm", ["lloyd", "elkan"])
|
||
|
def test_relocating_with_duplicates(algorithm, array_constr):
|
||
|
"""Check that kmeans stops when there are more centers than non-duplicate samples
|
||
|
|
||
|
Non-regression test for issue:
|
||
|
https://github.com/scikit-learn/scikit-learn/issues/28055
|
||
|
"""
|
||
|
X = np.array([[0, 0], [1, 1], [1, 1], [1, 0], [0, 1]])
|
||
|
km = KMeans(n_clusters=5, init=X, algorithm=algorithm)
|
||
|
|
||
|
msg = r"Number of distinct clusters \(4\) found smaller than n_clusters \(5\)"
|
||
|
with pytest.warns(ConvergenceWarning, match=msg):
|
||
|
km.fit(array_constr(X))
|
||
|
|
||
|
assert km.n_iter_ == 1
|