feat: add Camera Work AI package
This commit is contained in:
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,41 @@
|
||||
# Fast inner loop for DBSCAN.
|
||||
|
||||
# Authors: The scikit-learn developers
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
from libcpp.vector cimport vector
|
||||
|
||||
from ..utils._typedefs cimport uint8_t, intp_t
|
||||
|
||||
|
||||
def dbscan_inner(const uint8_t[::1] is_core,
|
||||
object[:] neighborhoods,
|
||||
intp_t[::1] labels):
|
||||
cdef intp_t i, label_num = 0, v
|
||||
cdef intp_t[:] neighb
|
||||
cdef vector[intp_t] stack
|
||||
|
||||
for i in range(labels.shape[0]):
|
||||
if labels[i] != -1 or not is_core[i]:
|
||||
continue
|
||||
|
||||
# Depth-first search starting from i, ending at the non-core points.
|
||||
# This is very similar to the classic algorithm for computing connected
|
||||
# components, the difference being that we label non-core points as
|
||||
# part of a cluster (component), but don't expand their neighborhoods.
|
||||
while True:
|
||||
if labels[i] == -1:
|
||||
labels[i] = label_num
|
||||
if is_core[i]:
|
||||
neighb = neighborhoods[i]
|
||||
for i in range(neighb.shape[0]):
|
||||
v = neighb[i]
|
||||
if labels[v] == -1:
|
||||
stack.push_back(v)
|
||||
|
||||
if stack.size() == 0:
|
||||
break
|
||||
i = stack.back()
|
||||
stack.pop_back()
|
||||
|
||||
label_num += 1
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,274 @@
|
||||
# Minimum spanning tree single linkage implementation for hdbscan
|
||||
|
||||
# Authors: The scikit-learn developers
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
|
||||
# 1. Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
|
||||
# 3. Neither the name of the copyright holder nor the names of its contributors
|
||||
# may be used to endorse or promote products derived from this software without
|
||||
# specific prior written permission.
|
||||
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
cimport numpy as cnp
|
||||
from libc.float cimport DBL_MAX
|
||||
|
||||
import numpy as np
|
||||
from ...metrics._dist_metrics cimport DistanceMetric64
|
||||
from ...cluster._hierarchical_fast cimport UnionFind
|
||||
from ...cluster._hdbscan._tree cimport HIERARCHY_t
|
||||
from ...cluster._hdbscan._tree import HIERARCHY_dtype
|
||||
from ...utils._typedefs cimport intp_t, float64_t, int64_t, uint8_t
|
||||
|
||||
cnp.import_array()
|
||||
|
||||
cdef extern from "numpy/arrayobject.h":
|
||||
intp_t * PyArray_SHAPE(cnp.PyArrayObject *)
|
||||
|
||||
# Numpy structured dtype representing a single ordered edge in Prim's algorithm
|
||||
MST_edge_dtype = np.dtype([
|
||||
("current_node", np.int64),
|
||||
("next_node", np.int64),
|
||||
("distance", np.float64),
|
||||
])
|
||||
|
||||
# Packed shouldn't make a difference since they're all 8-byte quantities,
|
||||
# but it's included just to be safe.
|
||||
ctypedef packed struct MST_edge_t:
|
||||
int64_t current_node
|
||||
int64_t next_node
|
||||
float64_t distance
|
||||
|
||||
cpdef cnp.ndarray[MST_edge_t, ndim=1, mode='c'] mst_from_mutual_reachability(
|
||||
cnp.ndarray[float64_t, ndim=2] mutual_reachability
|
||||
):
|
||||
"""Compute the Minimum Spanning Tree (MST) representation of the mutual-
|
||||
reachability graph using Prim's algorithm.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mutual_reachability : ndarray of shape (n_samples, n_samples)
|
||||
Array of mutual-reachabilities between samples.
|
||||
|
||||
Returns
|
||||
-------
|
||||
mst : ndarray of shape (n_samples - 1,), dtype=MST_edge_dtype
|
||||
The MST representation of the mutual-reachability graph. The MST is
|
||||
represented as a collection of edges.
|
||||
"""
|
||||
cdef:
|
||||
# Note: we utilize ndarray's over memory-views to make use of numpy
|
||||
# binary indexing and sub-selection below.
|
||||
cnp.ndarray[int64_t, ndim=1, mode='c'] current_labels
|
||||
cnp.ndarray[float64_t, ndim=1, mode='c'] min_reachability, left, right
|
||||
cnp.ndarray[MST_edge_t, ndim=1, mode='c'] mst
|
||||
|
||||
cnp.ndarray[uint8_t, mode='c'] label_filter
|
||||
|
||||
int64_t n_samples = PyArray_SHAPE(<cnp.PyArrayObject*> mutual_reachability)[0]
|
||||
int64_t current_node, new_node_index, new_node, i
|
||||
|
||||
mst = np.empty(n_samples - 1, dtype=MST_edge_dtype)
|
||||
current_labels = np.arange(n_samples, dtype=np.int64)
|
||||
current_node = 0
|
||||
min_reachability = np.full(n_samples, fill_value=np.inf, dtype=np.float64)
|
||||
for i in range(0, n_samples - 1):
|
||||
label_filter = current_labels != current_node
|
||||
current_labels = current_labels[label_filter]
|
||||
left = min_reachability[label_filter]
|
||||
right = mutual_reachability[current_node][current_labels]
|
||||
min_reachability = np.minimum(left, right)
|
||||
|
||||
new_node_index = np.argmin(min_reachability)
|
||||
new_node = current_labels[new_node_index]
|
||||
mst[i].current_node = current_node
|
||||
mst[i].next_node = new_node
|
||||
mst[i].distance = min_reachability[new_node_index]
|
||||
current_node = new_node
|
||||
|
||||
return mst
|
||||
|
||||
|
||||
cpdef cnp.ndarray[MST_edge_t, ndim=1, mode='c'] mst_from_data_matrix(
|
||||
const float64_t[:, ::1] raw_data,
|
||||
const float64_t[::1] core_distances,
|
||||
DistanceMetric64 dist_metric,
|
||||
float64_t alpha=1.0
|
||||
):
|
||||
"""Compute the Minimum Spanning Tree (MST) representation of the mutual-
|
||||
reachability graph generated from the provided `raw_data` and
|
||||
`core_distances` using Prim's algorithm.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
raw_data : ndarray of shape (n_samples, n_features)
|
||||
Input array of data samples.
|
||||
|
||||
core_distances : ndarray of shape (n_samples,)
|
||||
An array containing the core-distance calculated for each corresponding
|
||||
sample.
|
||||
|
||||
dist_metric : DistanceMetric
|
||||
The distance metric to use when calculating pairwise distances for
|
||||
determining mutual-reachability.
|
||||
|
||||
Returns
|
||||
-------
|
||||
mst : ndarray of shape (n_samples - 1,), dtype=MST_edge_dtype
|
||||
The MST representation of the mutual-reachability graph. The MST is
|
||||
represented as a collection of edges.
|
||||
"""
|
||||
|
||||
cdef:
|
||||
uint8_t[::1] in_tree
|
||||
float64_t[::1] min_reachability
|
||||
int64_t[::1] current_sources
|
||||
cnp.ndarray[MST_edge_t, ndim=1, mode='c'] mst
|
||||
|
||||
int64_t current_node, source_node, new_node, next_node_source
|
||||
int64_t i, j, n_samples, num_features
|
||||
|
||||
float64_t current_node_core_dist, new_reachability, mutual_reachability_distance
|
||||
float64_t next_node_min_reach, pair_distance, next_node_core_dist
|
||||
|
||||
n_samples = raw_data.shape[0]
|
||||
num_features = raw_data.shape[1]
|
||||
|
||||
mst = np.empty(n_samples - 1, dtype=MST_edge_dtype)
|
||||
|
||||
in_tree = np.zeros(n_samples, dtype=np.uint8)
|
||||
min_reachability = np.full(n_samples, fill_value=np.inf, dtype=np.float64)
|
||||
current_sources = np.ones(n_samples, dtype=np.int64)
|
||||
|
||||
current_node = 0
|
||||
|
||||
# The following loop dynamically updates minimum reachability node-by-node,
|
||||
# avoiding unnecessary computation where possible.
|
||||
for i in range(0, n_samples - 1):
|
||||
|
||||
in_tree[current_node] = 1
|
||||
|
||||
current_node_core_dist = core_distances[current_node]
|
||||
|
||||
new_reachability = DBL_MAX
|
||||
source_node = 0
|
||||
new_node = 0
|
||||
|
||||
for j in range(n_samples):
|
||||
if in_tree[j]:
|
||||
continue
|
||||
|
||||
next_node_min_reach = min_reachability[j]
|
||||
next_node_source = current_sources[j]
|
||||
|
||||
pair_distance = dist_metric.dist(
|
||||
&raw_data[current_node, 0],
|
||||
&raw_data[j, 0],
|
||||
num_features
|
||||
)
|
||||
|
||||
pair_distance /= alpha
|
||||
|
||||
next_node_core_dist = core_distances[j]
|
||||
mutual_reachability_distance = max(
|
||||
current_node_core_dist,
|
||||
next_node_core_dist,
|
||||
pair_distance
|
||||
)
|
||||
|
||||
# If MRD(i, j) is smaller than node j's min_reachability, we update
|
||||
# node j's min_reachability for future reference.
|
||||
if mutual_reachability_distance < next_node_min_reach:
|
||||
min_reachability[j] = mutual_reachability_distance
|
||||
current_sources[j] = current_node
|
||||
|
||||
# If MRD(i, j) is also smaller than node i's current
|
||||
# min_reachability, we update and set their edge as the current
|
||||
# MST edge candidate.
|
||||
if mutual_reachability_distance < new_reachability:
|
||||
new_reachability = mutual_reachability_distance
|
||||
source_node = current_node
|
||||
new_node = j
|
||||
|
||||
# If the node j is closer to another node already in the tree, we
|
||||
# make their edge the current MST candidate edge.
|
||||
elif next_node_min_reach < new_reachability:
|
||||
new_reachability = next_node_min_reach
|
||||
source_node = next_node_source
|
||||
new_node = j
|
||||
|
||||
mst[i].current_node = source_node
|
||||
mst[i].next_node = new_node
|
||||
mst[i].distance = new_reachability
|
||||
current_node = new_node
|
||||
|
||||
return mst
|
||||
|
||||
cpdef cnp.ndarray[HIERARCHY_t, ndim=1, mode="c"] make_single_linkage(const MST_edge_t[::1] mst):
|
||||
"""Construct a single-linkage tree from an MST.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mst : ndarray of shape (n_samples - 1,), dtype=MST_edge_dtype
|
||||
The MST representation of the mutual-reachability graph. The MST is
|
||||
represented as a collection of edges.
|
||||
|
||||
Returns
|
||||
-------
|
||||
single_linkage : ndarray of shape (n_samples - 1,), dtype=HIERARCHY_dtype
|
||||
The single-linkage tree tree (dendrogram) built from the MST. Each
|
||||
of the array represents the following:
|
||||
|
||||
- left node/cluster
|
||||
- right node/cluster
|
||||
- distance
|
||||
- new cluster size
|
||||
"""
|
||||
cdef:
|
||||
cnp.ndarray[HIERARCHY_t, ndim=1, mode="c"] single_linkage
|
||||
|
||||
# Note mst.shape[0] is one fewer than the number of samples
|
||||
int64_t n_samples = mst.shape[0] + 1
|
||||
intp_t current_node_cluster, next_node_cluster
|
||||
int64_t current_node, next_node, i
|
||||
float64_t distance
|
||||
UnionFind U = UnionFind(n_samples)
|
||||
|
||||
single_linkage = np.zeros(n_samples - 1, dtype=HIERARCHY_dtype)
|
||||
|
||||
for i in range(n_samples - 1):
|
||||
|
||||
current_node = mst[i].current_node
|
||||
next_node = mst[i].next_node
|
||||
distance = mst[i].distance
|
||||
|
||||
current_node_cluster = U.fast_find(current_node)
|
||||
next_node_cluster = U.fast_find(next_node)
|
||||
|
||||
single_linkage[i].left_node = current_node_cluster
|
||||
single_linkage[i].right_node = next_node_cluster
|
||||
single_linkage[i].value = distance
|
||||
single_linkage[i].cluster_size = U.size[current_node_cluster] + U.size[next_node_cluster]
|
||||
|
||||
U.union(current_node_cluster, next_node_cluster)
|
||||
|
||||
return single_linkage
|
||||
BIN
Binary file not shown.
CameraAI~/Tools~/CWCameraWorker/_internal/sklearn/cluster/_hdbscan/_reachability.cp310-win_amd64.pyd
LFS
BIN
Binary file not shown.
@@ -0,0 +1,210 @@
|
||||
# mutual reachability distance computations
|
||||
|
||||
# Authors: The scikit-learn developers
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
|
||||
# 1. Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
|
||||
# 3. Neither the name of the copyright holder nor the names of its contributors
|
||||
# may be used to endorse or promote products derived from this software without
|
||||
# specific prior written permission.
|
||||
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
cimport numpy as cnp
|
||||
|
||||
import numpy as np
|
||||
from scipy.sparse import issparse
|
||||
from cython cimport floating, integral
|
||||
from libc.math cimport isfinite, INFINITY
|
||||
from ...utils._typedefs cimport intp_t
|
||||
cnp.import_array()
|
||||
|
||||
|
||||
def mutual_reachability_graph(
|
||||
distance_matrix, min_samples=5, max_distance=0.0
|
||||
):
|
||||
"""Compute the weighted adjacency matrix of the mutual reachability graph.
|
||||
|
||||
The mutual reachability distance used to build the graph is defined as::
|
||||
|
||||
max(d_core(x_p), d_core(x_q), d(x_p, x_q))
|
||||
|
||||
and the core distance `d_core` is defined as the distance between a point
|
||||
`x_p` and its k-th nearest neighbor.
|
||||
|
||||
Note that all computations are done in-place.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
distance_matrix : {ndarray, sparse matrix} of shape (n_samples, n_samples)
|
||||
Array of distances between samples. If sparse, the array must be in
|
||||
`CSR` format.
|
||||
|
||||
min_samples : int, default=5
|
||||
The parameter `k` used to calculate the distance between a point
|
||||
`x_p` and its k-th nearest neighbor.
|
||||
|
||||
max_distance : float, default=0.0
|
||||
The distance which `np.inf` is replaced with. When the true mutual-
|
||||
reachability distance is measured to be infinite, it is instead
|
||||
truncated to `max_dist`. Only used when `distance_matrix` is a sparse
|
||||
matrix.
|
||||
|
||||
Returns
|
||||
-------
|
||||
mututal_reachability_graph: {ndarray, sparse matrix} of shape \
|
||||
(n_samples, n_samples)
|
||||
Weighted adjacency matrix of the mutual reachability graph.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Campello, R. J., Moulavi, D., & Sander, J. (2013, April).
|
||||
Density-based clustering based on hierarchical density estimates.
|
||||
In Pacific-Asia Conference on Knowledge Discovery and Data Mining
|
||||
(pp. 160-172). Springer Berlin Heidelberg.
|
||||
"""
|
||||
further_neighbor_idx = min_samples - 1
|
||||
if issparse(distance_matrix):
|
||||
if distance_matrix.format != "csr":
|
||||
raise ValueError(
|
||||
"Only sparse CSR matrices are supported for `distance_matrix`."
|
||||
)
|
||||
_sparse_mutual_reachability_graph(
|
||||
distance_matrix.data,
|
||||
distance_matrix.indices,
|
||||
distance_matrix.indptr,
|
||||
distance_matrix.shape[0],
|
||||
further_neighbor_idx=further_neighbor_idx,
|
||||
max_distance=max_distance,
|
||||
)
|
||||
else:
|
||||
_dense_mutual_reachability_graph(
|
||||
distance_matrix, further_neighbor_idx=further_neighbor_idx
|
||||
)
|
||||
return distance_matrix
|
||||
|
||||
|
||||
def _dense_mutual_reachability_graph(
|
||||
floating[:, :] distance_matrix,
|
||||
intp_t further_neighbor_idx,
|
||||
):
|
||||
"""Dense implementation of mutual reachability graph.
|
||||
|
||||
The computation is done in-place, i.e. the distance matrix is modified
|
||||
directly.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
distance_matrix : ndarray of shape (n_samples, n_samples)
|
||||
Array of distances between samples.
|
||||
|
||||
further_neighbor_idx : int
|
||||
The index of the furthest neighbor to use to define the core distances.
|
||||
"""
|
||||
cdef:
|
||||
intp_t i, j, n_samples = distance_matrix.shape[0]
|
||||
floating mutual_reachability_distance
|
||||
floating[::1] core_distances
|
||||
|
||||
# We assume that the distance matrix is symmetric. We choose to sort every
|
||||
# row to have the same implementation than the sparse case that requires
|
||||
# CSR matrix.
|
||||
core_distances = np.ascontiguousarray(
|
||||
np.partition(
|
||||
distance_matrix, further_neighbor_idx, axis=1
|
||||
)[:, further_neighbor_idx]
|
||||
)
|
||||
|
||||
with nogil:
|
||||
# TODO: Update w/ prange with thread count based on
|
||||
# _openmp_effective_n_threads
|
||||
for i in range(n_samples):
|
||||
for j in range(n_samples):
|
||||
mutual_reachability_distance = max(
|
||||
core_distances[i],
|
||||
core_distances[j],
|
||||
distance_matrix[i, j],
|
||||
)
|
||||
distance_matrix[i, j] = mutual_reachability_distance
|
||||
|
||||
|
||||
def _sparse_mutual_reachability_graph(
|
||||
cnp.ndarray[floating, ndim=1, mode="c"] data,
|
||||
cnp.ndarray[integral, ndim=1, mode="c"] indices,
|
||||
cnp.ndarray[integral, ndim=1, mode="c"] indptr,
|
||||
intp_t n_samples,
|
||||
intp_t further_neighbor_idx,
|
||||
floating max_distance,
|
||||
):
|
||||
"""Sparse implementation of mutual reachability graph.
|
||||
|
||||
The computation is done in-place, i.e. the distance matrix is modified
|
||||
directly. This implementation only accepts `CSR` format sparse matrices.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
distance_matrix : sparse matrix of shape (n_samples, n_samples)
|
||||
Sparse matrix of distances between samples. The sparse format should
|
||||
be `CSR`.
|
||||
|
||||
further_neighbor_idx : int
|
||||
The index of the furthest neighbor to use to define the core distances.
|
||||
|
||||
max_distance : float
|
||||
The distance which `np.inf` is replaced with. When the true mutual-
|
||||
reachability distance is measured to be infinite, it is instead
|
||||
truncated to `max_dist`. Only used when `distance_matrix` is a sparse
|
||||
matrix.
|
||||
"""
|
||||
cdef:
|
||||
integral i, col_ind, row_ind
|
||||
floating mutual_reachability_distance
|
||||
floating[:] core_distances
|
||||
floating[:] row_data
|
||||
|
||||
if floating is float:
|
||||
dtype = np.float32
|
||||
else:
|
||||
dtype = np.float64
|
||||
|
||||
core_distances = np.empty(n_samples, dtype=dtype)
|
||||
|
||||
for i in range(n_samples):
|
||||
row_data = data[indptr[i]:indptr[i + 1]]
|
||||
if further_neighbor_idx < row_data.size:
|
||||
core_distances[i] = np.partition(
|
||||
row_data, further_neighbor_idx
|
||||
)[further_neighbor_idx]
|
||||
else:
|
||||
core_distances[i] = INFINITY
|
||||
|
||||
with nogil:
|
||||
for row_ind in range(n_samples):
|
||||
for i in range(indptr[row_ind], indptr[row_ind + 1]):
|
||||
col_ind = indices[i]
|
||||
mutual_reachability_distance = max(
|
||||
core_distances[row_ind], core_distances[col_ind], data[i]
|
||||
)
|
||||
if isfinite(mutual_reachability_distance):
|
||||
data[i] = mutual_reachability_distance
|
||||
elif max_distance > 0:
|
||||
data[i] = max_distance
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,49 @@
|
||||
# Copyright (c) 2015, Leland McInnes
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
|
||||
# 1. Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
|
||||
# 3. Neither the name of the copyright holder nor the names of its contributors
|
||||
# may be used to endorse or promote products derived from this software without
|
||||
# specific prior written permission.
|
||||
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
from ...utils._typedefs cimport intp_t, float64_t, uint8_t
|
||||
cimport numpy as cnp
|
||||
|
||||
# This corresponds to the scipy.cluster.hierarchy format
|
||||
ctypedef packed struct HIERARCHY_t:
|
||||
intp_t left_node
|
||||
intp_t right_node
|
||||
float64_t value
|
||||
intp_t cluster_size
|
||||
|
||||
# Effectively an edgelist encoding a parent/child pair, along with a value and
|
||||
# the corresponding cluster_size in each row providing a tree structure.
|
||||
ctypedef packed struct CONDENSED_t:
|
||||
intp_t parent
|
||||
intp_t child
|
||||
float64_t value
|
||||
intp_t cluster_size
|
||||
|
||||
cdef extern from "numpy/arrayobject.h":
|
||||
intp_t * PyArray_SHAPE(cnp.PyArrayObject *)
|
||||
@@ -0,0 +1,799 @@
|
||||
# Tree handling (condensing, finding stable clusters) for hdbscan
|
||||
|
||||
# Authors: The scikit-learn developers
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
|
||||
# 1. Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
|
||||
# 3. Neither the name of the copyright holder nor the names of its contributors
|
||||
# may be used to endorse or promote products derived from this software without
|
||||
# specific prior written permission.
|
||||
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
cimport numpy as cnp
|
||||
from libc.math cimport isinf
|
||||
import cython
|
||||
|
||||
import numpy as np
|
||||
|
||||
cnp.import_array()
|
||||
|
||||
cdef extern from "numpy/arrayobject.h":
|
||||
intp_t * PyArray_SHAPE(cnp.PyArrayObject *)
|
||||
|
||||
cdef cnp.float64_t INFTY = np.inf
|
||||
cdef cnp.intp_t NOISE = -1
|
||||
|
||||
HIERARCHY_dtype = np.dtype([
|
||||
("left_node", np.intp),
|
||||
("right_node", np.intp),
|
||||
("value", np.float64),
|
||||
("cluster_size", np.intp),
|
||||
])
|
||||
|
||||
CONDENSED_dtype = np.dtype([
|
||||
("parent", np.intp),
|
||||
("child", np.intp),
|
||||
("value", np.float64),
|
||||
("cluster_size", np.intp),
|
||||
])
|
||||
|
||||
cpdef tuple tree_to_labels(
|
||||
const HIERARCHY_t[::1] single_linkage_tree,
|
||||
cnp.intp_t min_cluster_size=10,
|
||||
cluster_selection_method="eom",
|
||||
bint allow_single_cluster=False,
|
||||
cnp.float64_t cluster_selection_epsilon=0.0,
|
||||
max_cluster_size=None,
|
||||
):
|
||||
cdef:
|
||||
cnp.ndarray[CONDENSED_t, ndim=1, mode='c'] condensed_tree
|
||||
cnp.ndarray[cnp.intp_t, ndim=1, mode='c'] labels
|
||||
cnp.ndarray[cnp.float64_t, ndim=1, mode='c'] probabilities
|
||||
|
||||
condensed_tree = _condense_tree(single_linkage_tree, min_cluster_size)
|
||||
labels, probabilities = _get_clusters(
|
||||
condensed_tree,
|
||||
_compute_stability(condensed_tree),
|
||||
cluster_selection_method,
|
||||
allow_single_cluster,
|
||||
cluster_selection_epsilon,
|
||||
max_cluster_size,
|
||||
)
|
||||
|
||||
return (labels, probabilities)
|
||||
|
||||
cdef list bfs_from_hierarchy(
|
||||
const HIERARCHY_t[::1] hierarchy,
|
||||
cnp.intp_t bfs_root
|
||||
):
|
||||
"""
|
||||
Perform a breadth first search on a tree in scipy hclust format.
|
||||
"""
|
||||
|
||||
cdef list process_queue, next_queue, result
|
||||
cdef cnp.intp_t n_samples = hierarchy.shape[0] + 1
|
||||
cdef cnp.intp_t node
|
||||
process_queue = [bfs_root]
|
||||
result = []
|
||||
|
||||
while process_queue:
|
||||
result.extend(process_queue)
|
||||
# By construction, node i is formed by the union of nodes
|
||||
# hierarchy[i - n_samples, 0] and hierarchy[i - n_samples, 1]
|
||||
process_queue = [
|
||||
x - n_samples
|
||||
for x in process_queue
|
||||
if x >= n_samples
|
||||
]
|
||||
if process_queue:
|
||||
next_queue = []
|
||||
for node in process_queue:
|
||||
next_queue.extend(
|
||||
[
|
||||
hierarchy[node].left_node,
|
||||
hierarchy[node].right_node,
|
||||
]
|
||||
)
|
||||
process_queue = next_queue
|
||||
return result
|
||||
|
||||
|
||||
cpdef cnp.ndarray[CONDENSED_t, ndim=1, mode='c'] _condense_tree(
|
||||
const HIERARCHY_t[::1] hierarchy,
|
||||
cnp.intp_t min_cluster_size=10
|
||||
):
|
||||
"""Condense a tree according to a minimum cluster size. This is akin
|
||||
to the runt pruning procedure of Stuetzle. The result is a much simpler
|
||||
tree that is easier to visualize. We include extra information on the
|
||||
lambda value at which individual points depart clusters for later
|
||||
analysis and computation.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
hierarchy : ndarray of shape (n_samples,), dtype=HIERARCHY_dtype
|
||||
A single linkage hierarchy in scipy.cluster.hierarchy format.
|
||||
|
||||
min_cluster_size : int, optional (default 10)
|
||||
The minimum size of clusters to consider. Clusters smaller than this
|
||||
are pruned from the tree.
|
||||
|
||||
Returns
|
||||
-------
|
||||
condensed_tree : ndarray of shape (n_samples,), dtype=CONDENSED_dtype
|
||||
Effectively an edgelist encoding a parent/child pair, along with a
|
||||
value and the corresponding cluster_size in each row providing a tree
|
||||
structure.
|
||||
"""
|
||||
|
||||
cdef:
|
||||
cnp.intp_t root = 2 * hierarchy.shape[0]
|
||||
cnp.intp_t n_samples = hierarchy.shape[0] + 1
|
||||
cnp.intp_t next_label = n_samples + 1
|
||||
list result_list, node_list = bfs_from_hierarchy(hierarchy, root)
|
||||
|
||||
cnp.intp_t[::1] relabel
|
||||
cnp.uint8_t[::1] ignore
|
||||
|
||||
cnp.intp_t node, sub_node, left, right
|
||||
cnp.float64_t lambda_value, distance
|
||||
cnp.intp_t left_count, right_count
|
||||
HIERARCHY_t children
|
||||
|
||||
relabel = np.empty(root + 1, dtype=np.intp)
|
||||
relabel[root] = n_samples
|
||||
result_list = []
|
||||
ignore = np.zeros(len(node_list), dtype=bool)
|
||||
|
||||
for node in node_list:
|
||||
if ignore[node] or node < n_samples:
|
||||
continue
|
||||
|
||||
children = hierarchy[node - n_samples]
|
||||
left = children.left_node
|
||||
right = children.right_node
|
||||
distance = children.value
|
||||
if distance > 0.0:
|
||||
lambda_value = 1.0 / distance
|
||||
else:
|
||||
lambda_value = INFTY
|
||||
|
||||
if left >= n_samples:
|
||||
left_count = hierarchy[left - n_samples].cluster_size
|
||||
else:
|
||||
left_count = 1
|
||||
|
||||
if right >= n_samples:
|
||||
right_count = hierarchy[right - n_samples].cluster_size
|
||||
else:
|
||||
right_count = 1
|
||||
|
||||
if left_count >= min_cluster_size and right_count >= min_cluster_size:
|
||||
relabel[left] = next_label
|
||||
next_label += 1
|
||||
result_list.append(
|
||||
(relabel[node], relabel[left], lambda_value, left_count)
|
||||
)
|
||||
|
||||
relabel[right] = next_label
|
||||
next_label += 1
|
||||
result_list.append(
|
||||
(relabel[node], relabel[right], lambda_value, right_count)
|
||||
)
|
||||
|
||||
elif left_count < min_cluster_size and right_count < min_cluster_size:
|
||||
for sub_node in bfs_from_hierarchy(hierarchy, left):
|
||||
if sub_node < n_samples:
|
||||
result_list.append(
|
||||
(relabel[node], sub_node, lambda_value, 1)
|
||||
)
|
||||
ignore[sub_node] = True
|
||||
|
||||
for sub_node in bfs_from_hierarchy(hierarchy, right):
|
||||
if sub_node < n_samples:
|
||||
result_list.append(
|
||||
(relabel[node], sub_node, lambda_value, 1)
|
||||
)
|
||||
ignore[sub_node] = True
|
||||
|
||||
elif left_count < min_cluster_size:
|
||||
relabel[right] = relabel[node]
|
||||
for sub_node in bfs_from_hierarchy(hierarchy, left):
|
||||
if sub_node < n_samples:
|
||||
result_list.append(
|
||||
(relabel[node], sub_node, lambda_value, 1)
|
||||
)
|
||||
ignore[sub_node] = True
|
||||
|
||||
else:
|
||||
relabel[left] = relabel[node]
|
||||
for sub_node in bfs_from_hierarchy(hierarchy, right):
|
||||
if sub_node < n_samples:
|
||||
result_list.append(
|
||||
(relabel[node], sub_node, lambda_value, 1)
|
||||
)
|
||||
ignore[sub_node] = True
|
||||
|
||||
return np.array(result_list, dtype=CONDENSED_dtype)
|
||||
|
||||
|
||||
cdef dict _compute_stability(
|
||||
cnp.ndarray[CONDENSED_t, ndim=1, mode='c'] condensed_tree
|
||||
):
|
||||
|
||||
cdef:
|
||||
cnp.float64_t[::1] result, births
|
||||
cnp.intp_t[:] parents = condensed_tree['parent']
|
||||
|
||||
cnp.intp_t parent, cluster_size, result_index, idx
|
||||
cnp.float64_t lambda_val
|
||||
CONDENSED_t condensed_node
|
||||
cnp.intp_t largest_child = condensed_tree['child'].max()
|
||||
cnp.intp_t smallest_cluster = np.min(parents)
|
||||
cnp.intp_t num_clusters = np.max(parents) - smallest_cluster + 1
|
||||
dict stability_dict = {}
|
||||
|
||||
largest_child = max(largest_child, smallest_cluster)
|
||||
births = np.full(largest_child + 1, np.nan, dtype=np.float64)
|
||||
|
||||
for idx in range(PyArray_SHAPE(<cnp.PyArrayObject*> condensed_tree)[0]):
|
||||
condensed_node = condensed_tree[idx]
|
||||
births[condensed_node.child] = condensed_node.value
|
||||
|
||||
births[smallest_cluster] = 0.0
|
||||
|
||||
result = np.zeros(num_clusters, dtype=np.float64)
|
||||
for idx in range(PyArray_SHAPE(<cnp.PyArrayObject*> condensed_tree)[0]):
|
||||
condensed_node = condensed_tree[idx]
|
||||
parent = condensed_node.parent
|
||||
lambda_val = condensed_node.value
|
||||
cluster_size = condensed_node.cluster_size
|
||||
|
||||
result_index = parent - smallest_cluster
|
||||
result[result_index] += (lambda_val - births[parent]) * cluster_size
|
||||
|
||||
for idx in range(num_clusters):
|
||||
stability_dict[idx + smallest_cluster] = result[idx]
|
||||
|
||||
return stability_dict
|
||||
|
||||
|
||||
cdef list bfs_from_cluster_tree(
|
||||
cnp.ndarray[CONDENSED_t, ndim=1, mode='c'] condensed_tree,
|
||||
cnp.intp_t bfs_root
|
||||
):
|
||||
|
||||
cdef:
|
||||
list result = []
|
||||
cnp.ndarray[cnp.intp_t, ndim=1] process_queue = (
|
||||
np.array([bfs_root], dtype=np.intp)
|
||||
)
|
||||
cnp.ndarray[cnp.intp_t, ndim=1] children = condensed_tree['child']
|
||||
cnp.intp_t[:] parents = condensed_tree['parent']
|
||||
|
||||
while len(process_queue) > 0:
|
||||
result.extend(process_queue.tolist())
|
||||
process_queue = children[np.isin(parents, process_queue)]
|
||||
|
||||
return result
|
||||
|
||||
|
||||
cdef cnp.float64_t[::1] max_lambdas(cnp.ndarray[CONDENSED_t, ndim=1, mode='c'] condensed_tree):
|
||||
|
||||
cdef:
|
||||
cnp.intp_t parent, current_parent, idx
|
||||
cnp.float64_t lambda_val, max_lambda
|
||||
cnp.float64_t[::1] deaths
|
||||
cnp.intp_t largest_parent = condensed_tree['parent'].max()
|
||||
|
||||
deaths = np.zeros(largest_parent + 1, dtype=np.float64)
|
||||
current_parent = condensed_tree[0].parent
|
||||
max_lambda = condensed_tree[0].value
|
||||
|
||||
for idx in range(1, PyArray_SHAPE(<cnp.PyArrayObject*> condensed_tree)[0]):
|
||||
parent = condensed_tree[idx].parent
|
||||
lambda_val = condensed_tree[idx].value
|
||||
|
||||
if parent == current_parent:
|
||||
max_lambda = max(max_lambda, lambda_val)
|
||||
else:
|
||||
deaths[current_parent] = max_lambda
|
||||
current_parent = parent
|
||||
max_lambda = lambda_val
|
||||
|
||||
deaths[current_parent] = max_lambda # value for last parent
|
||||
return deaths
|
||||
|
||||
|
||||
@cython.final
|
||||
cdef class TreeUnionFind:
|
||||
|
||||
cdef cnp.intp_t[:, ::1] data
|
||||
cdef cnp.uint8_t[::1] is_component
|
||||
|
||||
def __init__(self, size):
|
||||
cdef cnp.intp_t idx
|
||||
self.data = np.zeros((size, 2), dtype=np.intp)
|
||||
for idx in range(size):
|
||||
self.data[idx, 0] = idx
|
||||
self.is_component = np.ones(size, dtype=np.uint8)
|
||||
|
||||
cdef void union(self, cnp.intp_t x, cnp.intp_t y):
|
||||
cdef cnp.intp_t x_root = self.find(x)
|
||||
cdef cnp.intp_t y_root = self.find(y)
|
||||
|
||||
if self.data[x_root, 1] < self.data[y_root, 1]:
|
||||
self.data[x_root, 0] = y_root
|
||||
elif self.data[x_root, 1] > self.data[y_root, 1]:
|
||||
self.data[y_root, 0] = x_root
|
||||
else:
|
||||
self.data[y_root, 0] = x_root
|
||||
self.data[x_root, 1] += 1
|
||||
return
|
||||
|
||||
cdef cnp.intp_t find(self, cnp.intp_t x):
|
||||
if self.data[x, 0] != x:
|
||||
self.data[x, 0] = self.find(self.data[x, 0])
|
||||
self.is_component[x] = False
|
||||
return self.data[x, 0]
|
||||
|
||||
|
||||
cpdef cnp.ndarray[cnp.intp_t, ndim=1, mode='c'] labelling_at_cut(
|
||||
const HIERARCHY_t[::1] linkage,
|
||||
cnp.float64_t cut,
|
||||
cnp.intp_t min_cluster_size
|
||||
):
|
||||
"""Given a single linkage tree and a cut value, return the
|
||||
vector of cluster labels at that cut value. This is useful
|
||||
for Robust Single Linkage, and extracting DBSCAN results
|
||||
from a single HDBSCAN run.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
linkage : ndarray of shape (n_samples,), dtype=HIERARCHY_dtype
|
||||
The single linkage tree in scipy.cluster.hierarchy format.
|
||||
|
||||
cut : double
|
||||
The cut value at which to find clusters.
|
||||
|
||||
min_cluster_size : int
|
||||
The minimum cluster size; clusters below this size at
|
||||
the cut will be considered noise.
|
||||
|
||||
Returns
|
||||
-------
|
||||
labels : ndarray of shape (n_samples,)
|
||||
The cluster labels for each point in the data set;
|
||||
a label of -1 denotes a noise assignment.
|
||||
"""
|
||||
|
||||
cdef:
|
||||
cnp.intp_t n, cluster, root, n_samples, cluster_label
|
||||
cnp.intp_t[::1] unique_labels, cluster_size
|
||||
cnp.ndarray[cnp.intp_t, ndim=1, mode='c'] result
|
||||
TreeUnionFind union_find
|
||||
dict cluster_label_map
|
||||
HIERARCHY_t node
|
||||
|
||||
root = 2 * linkage.shape[0]
|
||||
n_samples = root // 2 + 1
|
||||
result = np.empty(n_samples, dtype=np.intp)
|
||||
union_find = TreeUnionFind(root + 1)
|
||||
|
||||
cluster = n_samples
|
||||
for node in linkage:
|
||||
if node.value < cut:
|
||||
union_find.union(node.left_node, cluster)
|
||||
union_find.union(node.right_node, cluster)
|
||||
cluster += 1
|
||||
|
||||
cluster_size = np.zeros(cluster, dtype=np.intp)
|
||||
for n in range(n_samples):
|
||||
cluster = union_find.find(n)
|
||||
cluster_size[cluster] += 1
|
||||
result[n] = cluster
|
||||
|
||||
cluster_label_map = {-1: NOISE}
|
||||
cluster_label = 0
|
||||
unique_labels = np.unique(result)
|
||||
|
||||
for cluster in unique_labels:
|
||||
if cluster_size[cluster] < min_cluster_size:
|
||||
cluster_label_map[cluster] = NOISE
|
||||
else:
|
||||
cluster_label_map[cluster] = cluster_label
|
||||
cluster_label += 1
|
||||
|
||||
for n in range(n_samples):
|
||||
result[n] = cluster_label_map[result[n]]
|
||||
|
||||
return result
|
||||
|
||||
|
||||
cpdef cnp.ndarray[cnp.intp_t, ndim=1, mode='c'] _do_labelling(
|
||||
cnp.ndarray[CONDENSED_t, ndim=1, mode='c'] condensed_tree,
|
||||
set clusters,
|
||||
dict cluster_label_map,
|
||||
cnp.intp_t allow_single_cluster,
|
||||
cnp.float64_t cluster_selection_epsilon
|
||||
):
|
||||
"""Given a condensed tree, clusters and a labeling map for the clusters,
|
||||
return an array containing the labels of each point based on cluster
|
||||
membership. Note that this is where points may be marked as noisy
|
||||
outliers. The determination of some points as noise is in large, single-
|
||||
cluster datasets is controlled by the `allow_single_cluster` and
|
||||
`cluster_selection_epsilon` parameters.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
condensed_tree : ndarray of shape (n_samples,), dtype=CONDENSED_dtype
|
||||
Effectively an edgelist encoding a parent/child pair, along with a
|
||||
value and the corresponding cluster_size in each row providing a tree
|
||||
structure.
|
||||
|
||||
clusters : set
|
||||
The set of nodes corresponding to identified clusters. These node
|
||||
values should be the same as those present in `condensed_tree`.
|
||||
|
||||
cluster_label_map : dict
|
||||
A mapping from the node values present in `clusters` to the labels
|
||||
which will be returned.
|
||||
|
||||
Returns
|
||||
-------
|
||||
labels : ndarray of shape (n_samples,)
|
||||
The cluster labels for each point in the data set;
|
||||
a label of -1 denotes a noise assignment.
|
||||
"""
|
||||
|
||||
cdef:
|
||||
cnp.intp_t root_cluster
|
||||
cnp.ndarray[cnp.intp_t, ndim=1, mode='c'] result
|
||||
cnp.ndarray[cnp.intp_t, ndim=1] parent_array, child_array
|
||||
cnp.ndarray[cnp.float64_t, ndim=1] lambda_array
|
||||
TreeUnionFind union_find
|
||||
cnp.intp_t n, parent, child, cluster
|
||||
cnp.float64_t threshold
|
||||
|
||||
child_array = condensed_tree['child']
|
||||
parent_array = condensed_tree['parent']
|
||||
lambda_array = condensed_tree['value']
|
||||
|
||||
root_cluster = np.min(parent_array)
|
||||
result = np.empty(root_cluster, dtype=np.intp)
|
||||
union_find = TreeUnionFind(np.max(parent_array) + 1)
|
||||
|
||||
for n in range(PyArray_SHAPE(<cnp.PyArrayObject*> condensed_tree)[0]):
|
||||
child = child_array[n]
|
||||
parent = parent_array[n]
|
||||
if child not in clusters:
|
||||
union_find.union(parent, child)
|
||||
|
||||
for n in range(root_cluster):
|
||||
cluster = union_find.find(n)
|
||||
label = NOISE
|
||||
if cluster != root_cluster:
|
||||
label = cluster_label_map[cluster]
|
||||
elif len(clusters) == 1 and allow_single_cluster:
|
||||
# There can only be one edge with this particular child hence this
|
||||
# expression extracts a unique, scalar lambda value.
|
||||
parent_lambda = lambda_array[child_array == n]
|
||||
if cluster_selection_epsilon != 0.0:
|
||||
threshold = 1 / cluster_selection_epsilon
|
||||
else:
|
||||
# The threshold should be calculated per-sample based on the
|
||||
# largest lambda of any simbling node.
|
||||
threshold = lambda_array[parent_array == cluster].max()
|
||||
if parent_lambda >= threshold:
|
||||
label = cluster_label_map[cluster]
|
||||
|
||||
result[n] = label
|
||||
|
||||
return result
|
||||
|
||||
|
||||
cdef cnp.ndarray[cnp.float64_t, ndim=1, mode='c'] get_probabilities(
|
||||
cnp.ndarray[CONDENSED_t, ndim=1, mode='c'] condensed_tree,
|
||||
dict cluster_map,
|
||||
cnp.intp_t[::1] labels
|
||||
):
|
||||
|
||||
cdef:
|
||||
cnp.ndarray[cnp.float64_t, ndim=1, mode='c'] result
|
||||
cnp.float64_t[:] lambda_array
|
||||
cnp.float64_t[::1] deaths
|
||||
cnp.intp_t[:] child_array, parent_array
|
||||
cnp.intp_t root_cluster, n, point, cluster_num, cluster
|
||||
cnp.float64_t max_lambda, lambda_val
|
||||
|
||||
child_array = condensed_tree['child']
|
||||
parent_array = condensed_tree['parent']
|
||||
lambda_array = condensed_tree['value']
|
||||
|
||||
result = np.zeros(labels.shape[0])
|
||||
deaths = max_lambdas(condensed_tree)
|
||||
root_cluster = np.min(parent_array)
|
||||
|
||||
for n in range(PyArray_SHAPE(<cnp.PyArrayObject*> condensed_tree)[0]):
|
||||
point = child_array[n]
|
||||
if point >= root_cluster:
|
||||
continue
|
||||
|
||||
cluster_num = labels[point]
|
||||
if cluster_num == -1:
|
||||
continue
|
||||
|
||||
cluster = cluster_map[cluster_num]
|
||||
max_lambda = deaths[cluster]
|
||||
if max_lambda == 0.0 or isinf(lambda_array[n]):
|
||||
result[point] = 1.0
|
||||
else:
|
||||
lambda_val = min(lambda_array[n], max_lambda)
|
||||
result[point] = lambda_val / max_lambda
|
||||
|
||||
return result
|
||||
|
||||
|
||||
cpdef list recurse_leaf_dfs(
|
||||
cnp.ndarray[CONDENSED_t, ndim=1, mode='c'] cluster_tree,
|
||||
cnp.intp_t current_node
|
||||
):
|
||||
cdef cnp.intp_t[:] children
|
||||
cdef cnp.intp_t child
|
||||
|
||||
children = cluster_tree[cluster_tree['parent'] == current_node]['child']
|
||||
if children.shape[0] == 0:
|
||||
return [current_node,]
|
||||
else:
|
||||
return sum([recurse_leaf_dfs(cluster_tree, child) for child in children], [])
|
||||
|
||||
|
||||
cpdef list get_cluster_tree_leaves(cnp.ndarray[CONDENSED_t, ndim=1, mode='c'] cluster_tree):
|
||||
cdef cnp.intp_t root
|
||||
if PyArray_SHAPE(<cnp.PyArrayObject*> cluster_tree)[0] == 0:
|
||||
return []
|
||||
root = cluster_tree['parent'].min()
|
||||
return recurse_leaf_dfs(cluster_tree, root)
|
||||
|
||||
cdef cnp.intp_t traverse_upwards(
|
||||
cnp.ndarray[CONDENSED_t, ndim=1, mode='c'] cluster_tree,
|
||||
cnp.float64_t cluster_selection_epsilon,
|
||||
cnp.intp_t leaf,
|
||||
cnp.intp_t allow_single_cluster
|
||||
):
|
||||
cdef cnp.intp_t root, parent
|
||||
cdef cnp.float64_t parent_eps
|
||||
|
||||
root = cluster_tree['parent'].min()
|
||||
parent = cluster_tree[cluster_tree['child'] == leaf]['parent']
|
||||
if parent == root:
|
||||
if allow_single_cluster:
|
||||
return parent
|
||||
else:
|
||||
return leaf # return node closest to root
|
||||
|
||||
parent_eps = 1 / cluster_tree[cluster_tree['child'] == parent]['value']
|
||||
if parent_eps > cluster_selection_epsilon:
|
||||
return parent
|
||||
else:
|
||||
return traverse_upwards(
|
||||
cluster_tree,
|
||||
cluster_selection_epsilon,
|
||||
parent,
|
||||
allow_single_cluster
|
||||
)
|
||||
|
||||
cdef set epsilon_search(
|
||||
set leaves,
|
||||
cnp.ndarray[CONDENSED_t, ndim=1, mode='c'] cluster_tree,
|
||||
cnp.float64_t cluster_selection_epsilon,
|
||||
cnp.intp_t allow_single_cluster
|
||||
):
|
||||
cdef:
|
||||
list selected_clusters = list()
|
||||
list processed = list()
|
||||
cnp.intp_t leaf, epsilon_child, sub_node
|
||||
cnp.float64_t eps
|
||||
cnp.uint8_t[:] leaf_nodes
|
||||
cnp.ndarray[cnp.intp_t, ndim=1] children = cluster_tree['child']
|
||||
cnp.ndarray[cnp.float64_t, ndim=1] distances = cluster_tree['value']
|
||||
|
||||
for leaf in leaves:
|
||||
leaf_nodes = children == leaf
|
||||
eps = 1 / distances[leaf_nodes][0]
|
||||
if eps < cluster_selection_epsilon:
|
||||
if leaf not in processed:
|
||||
epsilon_child = traverse_upwards(
|
||||
cluster_tree,
|
||||
cluster_selection_epsilon,
|
||||
leaf,
|
||||
allow_single_cluster
|
||||
)
|
||||
selected_clusters.append(epsilon_child)
|
||||
|
||||
for sub_node in bfs_from_cluster_tree(cluster_tree, epsilon_child):
|
||||
if sub_node != epsilon_child:
|
||||
processed.append(sub_node)
|
||||
else:
|
||||
selected_clusters.append(leaf)
|
||||
|
||||
return set(selected_clusters)
|
||||
|
||||
|
||||
@cython.wraparound(True)
|
||||
cdef tuple _get_clusters(
|
||||
cnp.ndarray[CONDENSED_t, ndim=1, mode='c'] condensed_tree,
|
||||
dict stability,
|
||||
cluster_selection_method='eom',
|
||||
cnp.uint8_t allow_single_cluster=False,
|
||||
cnp.float64_t cluster_selection_epsilon=0.0,
|
||||
max_cluster_size=None
|
||||
):
|
||||
"""Given a tree and stability dict, produce the cluster labels
|
||||
(and probabilities) for a flat clustering based on the chosen
|
||||
cluster selection method.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
condensed_tree : ndarray of shape (n_samples,), dtype=CONDENSED_dtype
|
||||
Effectively an edgelist encoding a parent/child pair, along with a
|
||||
value and the corresponding cluster_size in each row providing a tree
|
||||
structure.
|
||||
|
||||
stability : dict
|
||||
A dictionary mapping cluster_ids to stability values
|
||||
|
||||
cluster_selection_method : string, optional (default 'eom')
|
||||
The method of selecting clusters. The default is the
|
||||
Excess of Mass algorithm specified by 'eom'. The alternate
|
||||
option is 'leaf'.
|
||||
|
||||
allow_single_cluster : boolean, optional (default False)
|
||||
Whether to allow a single cluster to be selected by the
|
||||
Excess of Mass algorithm.
|
||||
|
||||
cluster_selection_epsilon: double, optional (default 0.0)
|
||||
A distance threshold for cluster splits.
|
||||
|
||||
max_cluster_size: int, default=None
|
||||
The maximum size for clusters located by the EOM clusterer. Can
|
||||
be overridden by the cluster_selection_epsilon parameter in
|
||||
rare cases.
|
||||
|
||||
Returns
|
||||
-------
|
||||
labels : ndarray of shape (n_samples,)
|
||||
An integer array of cluster labels, with -1 denoting noise.
|
||||
|
||||
probabilities : ndarray (n_samples,)
|
||||
The cluster membership strength of each sample.
|
||||
|
||||
stabilities : ndarray (n_clusters,)
|
||||
The cluster coherence strengths of each cluster.
|
||||
"""
|
||||
cdef:
|
||||
list node_list
|
||||
cnp.ndarray[CONDENSED_t, ndim=1, mode='c'] cluster_tree
|
||||
cnp.uint8_t[::1] child_selection
|
||||
cnp.ndarray[cnp.intp_t, ndim=1, mode='c'] labels
|
||||
dict is_cluster, cluster_sizes
|
||||
cnp.float64_t subtree_stability
|
||||
cnp.intp_t node, sub_node, cluster, n_samples
|
||||
cnp.ndarray[cnp.float64_t, ndim=1, mode='c'] probs
|
||||
|
||||
# Assume clusters are ordered by numeric id equivalent to
|
||||
# a topological sort of the tree; This is valid given the
|
||||
# current implementation above, so don't change that ... or
|
||||
# if you do, change this accordingly!
|
||||
if allow_single_cluster:
|
||||
node_list = sorted(stability.keys(), reverse=True)
|
||||
else:
|
||||
node_list = sorted(stability.keys(), reverse=True)[:-1]
|
||||
# (exclude root)
|
||||
|
||||
cluster_tree = condensed_tree[condensed_tree['cluster_size'] > 1]
|
||||
is_cluster = {cluster: True for cluster in node_list}
|
||||
n_samples = np.max(condensed_tree[condensed_tree['cluster_size'] == 1]['child']) + 1
|
||||
|
||||
if max_cluster_size is None:
|
||||
max_cluster_size = n_samples + 1 # Set to a value that will never be triggered
|
||||
cluster_sizes = {
|
||||
child: cluster_size for child, cluster_size
|
||||
in zip(cluster_tree['child'], cluster_tree['cluster_size'])
|
||||
}
|
||||
if allow_single_cluster:
|
||||
# Compute cluster size for the root node
|
||||
cluster_sizes[node_list[-1]] = np.sum(
|
||||
cluster_tree[cluster_tree['parent'] == node_list[-1]]['cluster_size'])
|
||||
|
||||
if cluster_selection_method == 'eom':
|
||||
for node in node_list:
|
||||
child_selection = (cluster_tree['parent'] == node)
|
||||
subtree_stability = np.sum([
|
||||
stability[child] for
|
||||
child in cluster_tree['child'][child_selection]])
|
||||
if subtree_stability > stability[node] or cluster_sizes[node] > max_cluster_size:
|
||||
is_cluster[node] = False
|
||||
stability[node] = subtree_stability
|
||||
else:
|
||||
for sub_node in bfs_from_cluster_tree(cluster_tree, node):
|
||||
if sub_node != node:
|
||||
is_cluster[sub_node] = False
|
||||
|
||||
if cluster_selection_epsilon != 0.0 and PyArray_SHAPE(<cnp.PyArrayObject*> cluster_tree)[0] > 0:
|
||||
eom_clusters = [c for c in is_cluster if is_cluster[c]]
|
||||
selected_clusters = []
|
||||
# first check if eom_clusters only has root node, which skips epsilon check.
|
||||
if (len(eom_clusters) == 1 and eom_clusters[0] == cluster_tree['parent'].min()):
|
||||
if allow_single_cluster:
|
||||
selected_clusters = eom_clusters
|
||||
else:
|
||||
selected_clusters = epsilon_search(
|
||||
set(eom_clusters),
|
||||
cluster_tree,
|
||||
cluster_selection_epsilon,
|
||||
allow_single_cluster
|
||||
)
|
||||
for c in is_cluster:
|
||||
if c in selected_clusters:
|
||||
is_cluster[c] = True
|
||||
else:
|
||||
is_cluster[c] = False
|
||||
|
||||
elif cluster_selection_method == 'leaf':
|
||||
leaves = set(get_cluster_tree_leaves(cluster_tree))
|
||||
if len(leaves) == 0:
|
||||
for c in is_cluster:
|
||||
is_cluster[c] = False
|
||||
is_cluster[condensed_tree['parent'].min()] = True
|
||||
|
||||
if cluster_selection_epsilon != 0.0:
|
||||
selected_clusters = epsilon_search(
|
||||
leaves,
|
||||
cluster_tree,
|
||||
cluster_selection_epsilon,
|
||||
allow_single_cluster
|
||||
)
|
||||
else:
|
||||
selected_clusters = leaves
|
||||
|
||||
for c in is_cluster:
|
||||
if c in selected_clusters:
|
||||
is_cluster[c] = True
|
||||
else:
|
||||
is_cluster[c] = False
|
||||
|
||||
clusters = set([c for c in is_cluster if is_cluster[c]])
|
||||
cluster_map = {c: n for n, c in enumerate(sorted(list(clusters)))}
|
||||
reverse_cluster_map = {n: c for c, n in cluster_map.items()}
|
||||
|
||||
labels = _do_labelling(
|
||||
condensed_tree,
|
||||
clusters,
|
||||
cluster_map,
|
||||
allow_single_cluster,
|
||||
cluster_selection_epsilon
|
||||
)
|
||||
probs = get_probabilities(condensed_tree, reverse_cluster_map, labels)
|
||||
|
||||
return (labels, probs)
|
||||
@@ -0,0 +1,15 @@
|
||||
cluster_hdbscan_extension_metadata = {
|
||||
'_linkage': {'sources': [cython_gen.process('_linkage.pyx'), metrics_cython_tree]},
|
||||
'_reachability': {'sources': [cython_gen.process('_reachability.pyx')]},
|
||||
'_tree': {'sources': [cython_gen.process('_tree.pyx')]}
|
||||
}
|
||||
|
||||
foreach ext_name, ext_dict : cluster_hdbscan_extension_metadata
|
||||
py.extension_module(
|
||||
ext_name,
|
||||
ext_dict.get('sources'),
|
||||
dependencies: [np_dep],
|
||||
subdir: 'sklearn/cluster/_hdbscan',
|
||||
install: true
|
||||
)
|
||||
endforeach
|
||||
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
@@ -0,0 +1,9 @@
|
||||
from ..utils._typedefs cimport intp_t
|
||||
|
||||
cdef class UnionFind:
|
||||
cdef intp_t next_label
|
||||
cdef intp_t[:] parent
|
||||
cdef intp_t[:] size
|
||||
|
||||
cdef void union(self, intp_t m, intp_t n) noexcept
|
||||
cdef intp_t fast_find(self, intp_t n) noexcept
|
||||
@@ -0,0 +1,507 @@
|
||||
# Authors: The scikit-learn developers
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
import numpy as np
|
||||
cimport cython
|
||||
|
||||
from ..metrics._dist_metrics cimport DistanceMetric64
|
||||
from ..utils._fast_dict cimport IntFloatDict
|
||||
from ..utils._typedefs cimport float64_t, intp_t, uint8_t
|
||||
|
||||
# C++
|
||||
from cython.operator cimport dereference as deref, preincrement as inc
|
||||
from libcpp.map cimport map as cpp_map
|
||||
from libc.math cimport fmax, INFINITY
|
||||
|
||||
|
||||
###############################################################################
|
||||
# Utilities for computing the ward momentum
|
||||
|
||||
def compute_ward_dist(
|
||||
const float64_t[::1] m_1,
|
||||
const float64_t[:, ::1] m_2,
|
||||
const intp_t[::1] coord_row,
|
||||
const intp_t[::1] coord_col,
|
||||
float64_t[::1] res
|
||||
):
|
||||
cdef intp_t size_max = coord_row.shape[0]
|
||||
cdef intp_t n_features = m_2.shape[1]
|
||||
cdef intp_t i, j, row, col
|
||||
cdef float64_t pa, n
|
||||
|
||||
for i in range(size_max):
|
||||
row = coord_row[i]
|
||||
col = coord_col[i]
|
||||
n = (m_1[row] * m_1[col]) / (m_1[row] + m_1[col])
|
||||
pa = 0.
|
||||
for j in range(n_features):
|
||||
pa += (m_2[row, j] / m_1[row] - m_2[col, j] / m_1[col]) ** 2
|
||||
res[i] = pa * n
|
||||
|
||||
|
||||
###############################################################################
|
||||
# Utilities for cutting and exploring a hierarchical tree
|
||||
|
||||
def _hc_get_descendent(intp_t node, children, intp_t n_leaves):
|
||||
"""
|
||||
Function returning all the descendent leaves of a set of nodes in the tree.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
node : integer
|
||||
The node for which we want the descendents.
|
||||
|
||||
children : list of pairs, length n_nodes
|
||||
The children of each non-leaf node. Values less than `n_samples` refer
|
||||
to leaves of the tree. A greater value `i` indicates a node with
|
||||
children `children[i - n_samples]`.
|
||||
|
||||
n_leaves : integer
|
||||
Number of leaves.
|
||||
|
||||
Returns
|
||||
-------
|
||||
descendent : list of int
|
||||
"""
|
||||
ind = [node]
|
||||
if node < n_leaves:
|
||||
return ind
|
||||
descendent = []
|
||||
|
||||
# It is actually faster to do the accounting of the number of
|
||||
# elements is the list ourselves: len is a lengthy operation on a
|
||||
# chained list
|
||||
cdef intp_t i, n_indices = 1
|
||||
|
||||
while n_indices:
|
||||
i = ind.pop()
|
||||
if i < n_leaves:
|
||||
descendent.append(i)
|
||||
n_indices -= 1
|
||||
else:
|
||||
ind.extend(children[i - n_leaves])
|
||||
n_indices += 1
|
||||
return descendent
|
||||
|
||||
|
||||
def hc_get_heads(intp_t[:] parents, copy=True):
|
||||
"""Returns the heads of the forest, as defined by parents.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
parents : array of integers
|
||||
The parent structure defining the forest (ensemble of trees)
|
||||
copy : boolean
|
||||
If copy is False, the input 'parents' array is modified inplace
|
||||
|
||||
Returns
|
||||
-------
|
||||
heads : array of integers of same shape as parents
|
||||
The indices in the 'parents' of the tree heads
|
||||
|
||||
"""
|
||||
cdef intp_t parent, node0, node, size
|
||||
if copy:
|
||||
parents = np.copy(parents)
|
||||
size = parents.size
|
||||
|
||||
# Start from the top of the tree and go down
|
||||
for node0 in range(size - 1, -1, -1):
|
||||
node = node0
|
||||
parent = parents[node]
|
||||
while parent != node:
|
||||
parents[node0] = parent
|
||||
node = parent
|
||||
parent = parents[node]
|
||||
return parents
|
||||
|
||||
|
||||
def _get_parents(
|
||||
nodes,
|
||||
heads,
|
||||
const intp_t[:] parents,
|
||||
uint8_t[::1] not_visited
|
||||
):
|
||||
"""Returns the heads of the given nodes, as defined by parents.
|
||||
|
||||
Modifies 'heads' and 'not_visited' in-place.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
nodes : list of integers
|
||||
The nodes to start from
|
||||
heads : list of integers
|
||||
A list to hold the results (modified inplace)
|
||||
parents : array of integers
|
||||
The parent structure defining the tree
|
||||
not_visited
|
||||
The tree nodes to consider (modified inplace)
|
||||
|
||||
"""
|
||||
cdef intp_t parent, node
|
||||
|
||||
for node in nodes:
|
||||
parent = parents[node]
|
||||
while parent != node:
|
||||
node = parent
|
||||
parent = parents[node]
|
||||
if not_visited[node]:
|
||||
not_visited[node] = 0
|
||||
heads.append(node)
|
||||
|
||||
|
||||
###############################################################################
|
||||
# merge strategies implemented on IntFloatDicts
|
||||
|
||||
# These are used in the hierarchical clustering code, to implement
|
||||
# merging between two clusters, defined as a dict containing node number
|
||||
# as keys and edge weights as values.
|
||||
|
||||
|
||||
def max_merge(
|
||||
IntFloatDict a,
|
||||
IntFloatDict b,
|
||||
const intp_t[:] mask,
|
||||
intp_t n_a,
|
||||
intp_t n_b
|
||||
):
|
||||
"""Merge two IntFloatDicts with the max strategy: when the same key is
|
||||
present in the two dicts, the max of the two values is used.
|
||||
|
||||
Parameters
|
||||
==========
|
||||
a, b : IntFloatDict object
|
||||
The IntFloatDicts to merge
|
||||
mask : ndarray array of dtype integer and of dimension 1
|
||||
a mask for keys to ignore: if not mask[key] the corresponding key
|
||||
is skipped in the output dictionary
|
||||
n_a, n_b : float
|
||||
n_a and n_b are weights for a and b for the merge strategy.
|
||||
They are not used in the case of a max merge.
|
||||
|
||||
Returns
|
||||
=======
|
||||
out : IntFloatDict object
|
||||
The IntFloatDict resulting from the merge
|
||||
"""
|
||||
cdef IntFloatDict out_obj = IntFloatDict.__new__(IntFloatDict)
|
||||
cdef cpp_map[intp_t, float64_t].iterator a_it = a.my_map.begin()
|
||||
cdef cpp_map[intp_t, float64_t].iterator a_end = a.my_map.end()
|
||||
cdef intp_t key
|
||||
cdef float64_t value
|
||||
# First copy a into out
|
||||
while a_it != a_end:
|
||||
key = deref(a_it).first
|
||||
if mask[key]:
|
||||
out_obj.my_map[key] = deref(a_it).second
|
||||
inc(a_it)
|
||||
|
||||
# Then merge b into out
|
||||
cdef cpp_map[intp_t, float64_t].iterator out_it = out_obj.my_map.begin()
|
||||
cdef cpp_map[intp_t, float64_t].iterator out_end = out_obj.my_map.end()
|
||||
cdef cpp_map[intp_t, float64_t].iterator b_it = b.my_map.begin()
|
||||
cdef cpp_map[intp_t, float64_t].iterator b_end = b.my_map.end()
|
||||
while b_it != b_end:
|
||||
key = deref(b_it).first
|
||||
value = deref(b_it).second
|
||||
if mask[key]:
|
||||
out_it = out_obj.my_map.find(key)
|
||||
if out_it == out_end:
|
||||
# Key not found
|
||||
out_obj.my_map[key] = value
|
||||
else:
|
||||
deref(out_it).second = fmax(deref(out_it).second, value)
|
||||
inc(b_it)
|
||||
return out_obj
|
||||
|
||||
|
||||
def average_merge(
|
||||
IntFloatDict a,
|
||||
IntFloatDict b,
|
||||
const intp_t[:] mask,
|
||||
intp_t n_a,
|
||||
intp_t n_b
|
||||
):
|
||||
"""Merge two IntFloatDicts with the average strategy: when the
|
||||
same key is present in the two dicts, the weighted average of the two
|
||||
values is used.
|
||||
|
||||
Parameters
|
||||
==========
|
||||
a, b : IntFloatDict object
|
||||
The IntFloatDicts to merge
|
||||
mask : ndarray array of dtype integer and of dimension 1
|
||||
a mask for keys to ignore: if not mask[key] the corresponding key
|
||||
is skipped in the output dictionary
|
||||
n_a, n_b : float
|
||||
n_a and n_b are weights for a and b for the merge strategy.
|
||||
They are used for a weighted mean.
|
||||
|
||||
Returns
|
||||
=======
|
||||
out : IntFloatDict object
|
||||
The IntFloatDict resulting from the merge
|
||||
"""
|
||||
cdef IntFloatDict out_obj = IntFloatDict.__new__(IntFloatDict)
|
||||
cdef cpp_map[intp_t, float64_t].iterator a_it = a.my_map.begin()
|
||||
cdef cpp_map[intp_t, float64_t].iterator a_end = a.my_map.end()
|
||||
cdef intp_t key
|
||||
cdef float64_t value
|
||||
cdef float64_t n_out = <float64_t> (n_a + n_b)
|
||||
# First copy a into out
|
||||
while a_it != a_end:
|
||||
key = deref(a_it).first
|
||||
if mask[key]:
|
||||
out_obj.my_map[key] = deref(a_it).second
|
||||
inc(a_it)
|
||||
|
||||
# Then merge b into out
|
||||
cdef cpp_map[intp_t, float64_t].iterator out_it = out_obj.my_map.begin()
|
||||
cdef cpp_map[intp_t, float64_t].iterator out_end = out_obj.my_map.end()
|
||||
cdef cpp_map[intp_t, float64_t].iterator b_it = b.my_map.begin()
|
||||
cdef cpp_map[intp_t, float64_t].iterator b_end = b.my_map.end()
|
||||
while b_it != b_end:
|
||||
key = deref(b_it).first
|
||||
value = deref(b_it).second
|
||||
if mask[key]:
|
||||
out_it = out_obj.my_map.find(key)
|
||||
if out_it == out_end:
|
||||
# Key not found
|
||||
out_obj.my_map[key] = value
|
||||
else:
|
||||
deref(out_it).second = (n_a * deref(out_it).second
|
||||
+ n_b * value) / n_out
|
||||
inc(b_it)
|
||||
return out_obj
|
||||
|
||||
|
||||
###############################################################################
|
||||
# An edge object for fast comparisons
|
||||
|
||||
cdef class WeightedEdge:
|
||||
cdef public intp_t a
|
||||
cdef public intp_t b
|
||||
cdef public float64_t weight
|
||||
|
||||
def __init__(self, float64_t weight, intp_t a, intp_t b):
|
||||
self.weight = weight
|
||||
self.a = a
|
||||
self.b = b
|
||||
|
||||
def __richcmp__(self, WeightedEdge other, int op):
|
||||
"""Cython-specific comparison method.
|
||||
|
||||
op is the comparison code::
|
||||
< 0
|
||||
== 2
|
||||
> 4
|
||||
<= 1
|
||||
!= 3
|
||||
>= 5
|
||||
"""
|
||||
if op == 0:
|
||||
return self.weight < other.weight
|
||||
elif op == 1:
|
||||
return self.weight <= other.weight
|
||||
elif op == 2:
|
||||
return self.weight == other.weight
|
||||
elif op == 3:
|
||||
return self.weight != other.weight
|
||||
elif op == 4:
|
||||
return self.weight > other.weight
|
||||
elif op == 5:
|
||||
return self.weight >= other.weight
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(weight=%f, a=%i, b=%i)" % (self.__class__.__name__,
|
||||
self.weight,
|
||||
self.a, self.b)
|
||||
|
||||
|
||||
################################################################################
|
||||
# Efficient labelling/conversion of MSTs to single linkage hierarchies
|
||||
|
||||
cdef class UnionFind(object):
|
||||
|
||||
def __init__(self, N):
|
||||
self.parent = np.full(2 * N - 1, -1., dtype=np.intp, order='C')
|
||||
self.next_label = N
|
||||
self.size = np.hstack((np.ones(N, dtype=np.intp),
|
||||
np.zeros(N - 1, dtype=np.intp)))
|
||||
|
||||
cdef void union(self, intp_t m, intp_t n) noexcept:
|
||||
self.parent[m] = self.next_label
|
||||
self.parent[n] = self.next_label
|
||||
self.size[self.next_label] = self.size[m] + self.size[n]
|
||||
self.next_label += 1
|
||||
return
|
||||
|
||||
@cython.wraparound(True)
|
||||
cdef intp_t fast_find(self, intp_t n) noexcept:
|
||||
cdef intp_t p
|
||||
p = n
|
||||
# find the highest node in the linkage graph so far
|
||||
while self.parent[n] != -1:
|
||||
n = self.parent[n]
|
||||
# provide a shortcut up to the highest node
|
||||
while self.parent[p] != n:
|
||||
p, self.parent[p] = self.parent[p], n
|
||||
return n
|
||||
|
||||
|
||||
def _single_linkage_label(const float64_t[:, :] L):
|
||||
"""
|
||||
Convert an linkage array or MST to a tree by labelling clusters at merges.
|
||||
This is done by using a Union find structure to keep track of merges
|
||||
efficiently. This is the private version of the function that assumes that
|
||||
``L`` has been properly validated. See ``single_linkage_label`` for the
|
||||
user facing version of this function.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
L: array of shape (n_samples - 1, 3)
|
||||
The linkage array or MST where each row specifies two samples
|
||||
to be merged and a distance or weight at which the merge occurs. This
|
||||
array is assumed to be sorted by the distance/weight.
|
||||
|
||||
Returns
|
||||
-------
|
||||
A tree in the format used by scipy.cluster.hierarchy.
|
||||
"""
|
||||
|
||||
cdef float64_t[:, ::1] result_arr
|
||||
|
||||
cdef intp_t left, left_cluster, right, right_cluster, index
|
||||
cdef float64_t delta
|
||||
|
||||
result_arr = np.zeros((L.shape[0], 4), dtype=np.float64)
|
||||
U = UnionFind(L.shape[0] + 1)
|
||||
|
||||
for index in range(L.shape[0]):
|
||||
|
||||
left = <intp_t> L[index, 0]
|
||||
right = <intp_t> L[index, 1]
|
||||
delta = L[index, 2]
|
||||
|
||||
left_cluster = U.fast_find(left)
|
||||
right_cluster = U.fast_find(right)
|
||||
|
||||
result_arr[index][0] = left_cluster
|
||||
result_arr[index][1] = right_cluster
|
||||
result_arr[index][2] = delta
|
||||
result_arr[index][3] = U.size[left_cluster] + U.size[right_cluster]
|
||||
|
||||
U.union(left_cluster, right_cluster)
|
||||
|
||||
return np.asarray(result_arr)
|
||||
|
||||
|
||||
@cython.wraparound(True)
|
||||
def single_linkage_label(L):
|
||||
"""
|
||||
Convert an linkage array or MST to a tree by labelling clusters at merges.
|
||||
This is done by using a Union find structure to keep track of merges
|
||||
efficiently.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
L: array of shape (n_samples - 1, 3)
|
||||
The linkage array or MST where each row specifies two samples
|
||||
to be merged and a distance or weight at which the merge occurs. This
|
||||
array is assumed to be sorted by the distance/weight.
|
||||
|
||||
Returns
|
||||
-------
|
||||
A tree in the format used by scipy.cluster.hierarchy.
|
||||
"""
|
||||
# Validate L
|
||||
if L[:, :2].min() < 0 or L[:, :2].max() >= 2 * L.shape[0] + 1:
|
||||
raise ValueError("Input MST array is not a validly formatted MST array")
|
||||
|
||||
is_sorted = lambda x: np.all(x[:-1] <= x[1:])
|
||||
if not is_sorted(L[:, 2]):
|
||||
raise ValueError("Input MST array must be sorted by weight")
|
||||
|
||||
return _single_linkage_label(L)
|
||||
|
||||
|
||||
# Implements MST-LINKAGE-CORE from https://arxiv.org/abs/1109.2378
|
||||
def mst_linkage_core(
|
||||
const float64_t [:, ::1] raw_data,
|
||||
DistanceMetric64 dist_metric):
|
||||
"""
|
||||
Compute the necessary elements of a minimum spanning
|
||||
tree for computation of single linkage clustering. This
|
||||
represents the MST-LINKAGE-CORE algorithm (Figure 6) from
|
||||
:arxiv:`Daniel Mullner, "Modern hierarchical, agglomerative clustering
|
||||
algorithms" <1109.2378>`.
|
||||
|
||||
In contrast to the scipy implementation is never computes
|
||||
a full distance matrix, generating distances only as they
|
||||
are needed and releasing them when no longer needed.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
raw_data: array of shape (n_samples, n_features)
|
||||
The array of feature data to be clustered. Must be C-aligned
|
||||
|
||||
dist_metric: DistanceMetric64
|
||||
A DistanceMetric64 object conforming to the API from
|
||||
``sklearn.metrics._dist_metrics.pxd`` that will be
|
||||
used to compute distances.
|
||||
|
||||
Returns
|
||||
-------
|
||||
mst_core_data: array of shape (n_samples, 3)
|
||||
An array providing information from which one
|
||||
can either compute an MST, or the linkage hierarchy
|
||||
very efficiently. See :arxiv:`Daniel Mullner, "Modern hierarchical,
|
||||
agglomerative clustering algorithms" <1109.2378>` algorithm
|
||||
MST-LINKAGE-CORE for more details.
|
||||
"""
|
||||
cdef:
|
||||
intp_t n_samples = raw_data.shape[0]
|
||||
uint8_t[:] in_tree = np.zeros(n_samples, dtype=bool)
|
||||
float64_t[:, ::1] result = np.zeros((n_samples - 1, 3))
|
||||
|
||||
intp_t current_node = 0
|
||||
intp_t new_node
|
||||
intp_t i
|
||||
intp_t j
|
||||
intp_t num_features = raw_data.shape[1]
|
||||
|
||||
float64_t right_value
|
||||
float64_t left_value
|
||||
float64_t new_distance
|
||||
|
||||
float64_t[:] current_distances = np.full(n_samples, INFINITY)
|
||||
|
||||
for i in range(n_samples - 1):
|
||||
|
||||
in_tree[current_node] = 1
|
||||
|
||||
new_distance = INFINITY
|
||||
new_node = 0
|
||||
|
||||
for j in range(n_samples):
|
||||
if in_tree[j]:
|
||||
continue
|
||||
|
||||
right_value = current_distances[j]
|
||||
left_value = dist_metric.dist(&raw_data[current_node, 0],
|
||||
&raw_data[j, 0],
|
||||
num_features)
|
||||
|
||||
if left_value < right_value:
|
||||
current_distances[j] = left_value
|
||||
|
||||
if current_distances[j] < new_distance:
|
||||
new_distance = current_distances[j]
|
||||
new_node = j
|
||||
|
||||
result[i, 0] = current_node
|
||||
result[i, 1] = new_node
|
||||
result[i, 2] = new_distance
|
||||
current_node = new_node
|
||||
|
||||
return np.array(result)
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,48 @@
|
||||
from cython cimport floating
|
||||
|
||||
|
||||
cdef floating _euclidean_dense_dense(
|
||||
const floating*,
|
||||
const floating*,
|
||||
int,
|
||||
bint
|
||||
) noexcept nogil
|
||||
|
||||
cdef floating _euclidean_sparse_dense(
|
||||
const floating[::1],
|
||||
const int[::1],
|
||||
const floating[::1],
|
||||
floating,
|
||||
bint
|
||||
) noexcept nogil
|
||||
|
||||
cpdef void _relocate_empty_clusters_dense(
|
||||
const floating[:, ::1],
|
||||
const floating[::1],
|
||||
const floating[:, ::1],
|
||||
floating[:, ::1],
|
||||
floating[::1],
|
||||
const int[::1]
|
||||
)
|
||||
|
||||
cpdef void _relocate_empty_clusters_sparse(
|
||||
const floating[::1],
|
||||
const int[::1],
|
||||
const int[::1],
|
||||
const floating[::1],
|
||||
const floating[:, ::1],
|
||||
floating[:, ::1],
|
||||
floating[::1],
|
||||
const int[::1]
|
||||
)
|
||||
|
||||
cdef void _average_centers(
|
||||
floating[:, ::1],
|
||||
const floating[::1]
|
||||
)
|
||||
|
||||
cdef void _center_shift(
|
||||
const floating[:, ::1],
|
||||
const floating[:, ::1],
|
||||
floating[::1]
|
||||
)
|
||||
@@ -0,0 +1,328 @@
|
||||
# Authors: The scikit-learn developers
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
import numpy as np
|
||||
from cython cimport floating
|
||||
from cython.parallel cimport prange
|
||||
from libc.math cimport sqrt
|
||||
|
||||
from ..utils.extmath import row_norms
|
||||
|
||||
|
||||
# Number of samples per data chunk defined as a global constant.
|
||||
CHUNK_SIZE = 256
|
||||
|
||||
|
||||
cdef floating _euclidean_dense_dense(
|
||||
const floating* a, # IN
|
||||
const floating* b, # IN
|
||||
int n_features,
|
||||
bint squared
|
||||
) noexcept nogil:
|
||||
"""Euclidean distance between a dense and b dense"""
|
||||
cdef:
|
||||
int i
|
||||
int n = n_features // 4
|
||||
int rem = n_features % 4
|
||||
floating result = 0
|
||||
|
||||
# We manually unroll the loop for better cache optimization.
|
||||
for i in range(n):
|
||||
result += (
|
||||
(a[0] - b[0]) * (a[0] - b[0]) +
|
||||
(a[1] - b[1]) * (a[1] - b[1]) +
|
||||
(a[2] - b[2]) * (a[2] - b[2]) +
|
||||
(a[3] - b[3]) * (a[3] - b[3])
|
||||
)
|
||||
a += 4
|
||||
b += 4
|
||||
|
||||
for i in range(rem):
|
||||
result += (a[i] - b[i]) * (a[i] - b[i])
|
||||
|
||||
return result if squared else sqrt(result)
|
||||
|
||||
|
||||
def _euclidean_dense_dense_wrapper(
|
||||
const floating[::1] a,
|
||||
const floating[::1] b,
|
||||
bint squared
|
||||
):
|
||||
"""Wrapper of _euclidean_dense_dense for testing purpose"""
|
||||
return _euclidean_dense_dense(&a[0], &b[0], a.shape[0], squared)
|
||||
|
||||
|
||||
cdef floating _euclidean_sparse_dense(
|
||||
const floating[::1] a_data, # IN
|
||||
const int[::1] a_indices, # IN
|
||||
const floating[::1] b, # IN
|
||||
floating b_squared_norm,
|
||||
bint squared
|
||||
) noexcept nogil:
|
||||
"""Euclidean distance between a sparse and b dense"""
|
||||
cdef:
|
||||
int nnz = a_indices.shape[0]
|
||||
int i
|
||||
floating tmp, bi
|
||||
floating result = 0.0
|
||||
|
||||
for i in range(nnz):
|
||||
bi = b[a_indices[i]]
|
||||
tmp = a_data[i] - bi
|
||||
result += tmp * tmp - bi * bi
|
||||
|
||||
result += b_squared_norm
|
||||
|
||||
if result < 0:
|
||||
result = 0.0
|
||||
|
||||
return result if squared else sqrt(result)
|
||||
|
||||
|
||||
def _euclidean_sparse_dense_wrapper(
|
||||
const floating[::1] a_data,
|
||||
const int[::1] a_indices,
|
||||
const floating[::1] b,
|
||||
floating b_squared_norm,
|
||||
bint squared
|
||||
):
|
||||
"""Wrapper of _euclidean_sparse_dense for testing purpose"""
|
||||
return _euclidean_sparse_dense(
|
||||
a_data, a_indices, b, b_squared_norm, squared)
|
||||
|
||||
|
||||
cpdef floating _inertia_dense(
|
||||
const floating[:, ::1] X, # IN
|
||||
const floating[::1] sample_weight, # IN
|
||||
const floating[:, ::1] centers, # IN
|
||||
const int[::1] labels, # IN
|
||||
int n_threads,
|
||||
int single_label=-1,
|
||||
):
|
||||
"""Compute inertia for dense input data
|
||||
|
||||
Sum of squared distance between each sample and its assigned center.
|
||||
|
||||
If single_label is >= 0, the inertia is computed only for that label.
|
||||
"""
|
||||
cdef:
|
||||
int n_samples = X.shape[0]
|
||||
int n_features = X.shape[1]
|
||||
int i, j
|
||||
|
||||
floating sq_dist = 0.0
|
||||
floating inertia = 0.0
|
||||
|
||||
for i in prange(n_samples, nogil=True, num_threads=n_threads,
|
||||
schedule='static'):
|
||||
j = labels[i]
|
||||
if single_label < 0 or single_label == j:
|
||||
sq_dist = _euclidean_dense_dense(&X[i, 0], ¢ers[j, 0],
|
||||
n_features, True)
|
||||
inertia += sq_dist * sample_weight[i]
|
||||
|
||||
return inertia
|
||||
|
||||
|
||||
cpdef floating _inertia_sparse(
|
||||
X, # IN
|
||||
const floating[::1] sample_weight, # IN
|
||||
const floating[:, ::1] centers, # IN
|
||||
const int[::1] labels, # IN
|
||||
int n_threads,
|
||||
int single_label=-1,
|
||||
):
|
||||
"""Compute inertia for sparse input data
|
||||
|
||||
Sum of squared distance between each sample and its assigned center.
|
||||
|
||||
If single_label is >= 0, the inertia is computed only for that label.
|
||||
"""
|
||||
cdef:
|
||||
floating[::1] X_data = X.data
|
||||
int[::1] X_indices = X.indices
|
||||
int[::1] X_indptr = X.indptr
|
||||
|
||||
int n_samples = X.shape[0]
|
||||
int i, j
|
||||
|
||||
floating sq_dist = 0.0
|
||||
floating inertia = 0.0
|
||||
|
||||
floating[::1] centers_squared_norms = row_norms(centers, squared=True)
|
||||
|
||||
for i in prange(n_samples, nogil=True, num_threads=n_threads,
|
||||
schedule='static'):
|
||||
j = labels[i]
|
||||
if single_label < 0 or single_label == j:
|
||||
sq_dist = _euclidean_sparse_dense(
|
||||
X_data[X_indptr[i]: X_indptr[i + 1]],
|
||||
X_indices[X_indptr[i]: X_indptr[i + 1]],
|
||||
centers[j], centers_squared_norms[j], True)
|
||||
inertia += sq_dist * sample_weight[i]
|
||||
|
||||
return inertia
|
||||
|
||||
|
||||
cpdef void _relocate_empty_clusters_dense(
|
||||
const floating[:, ::1] X, # IN
|
||||
const floating[::1] sample_weight, # IN
|
||||
const floating[:, ::1] centers_old, # IN
|
||||
floating[:, ::1] centers_new, # INOUT
|
||||
floating[::1] weight_in_clusters, # INOUT
|
||||
const int[::1] labels # IN
|
||||
):
|
||||
"""Relocate centers which have no sample assigned to them."""
|
||||
cdef:
|
||||
int[::1] empty_clusters = np.where(np.equal(weight_in_clusters, 0))[0].astype(np.int32)
|
||||
int n_empty = empty_clusters.shape[0]
|
||||
|
||||
if n_empty == 0:
|
||||
return
|
||||
|
||||
cdef:
|
||||
int n_features = X.shape[1]
|
||||
|
||||
floating[::1] distances = ((np.asarray(X) - np.asarray(centers_old)[labels])**2).sum(axis=1)
|
||||
int[::1] far_from_centers = np.argpartition(distances, -n_empty)[:-n_empty-1:-1].astype(np.int32)
|
||||
|
||||
int new_cluster_id, old_cluster_id, far_idx, idx, k
|
||||
floating weight
|
||||
|
||||
if np.max(distances) == 0:
|
||||
# Happens when there are more clusters than non-duplicate samples. Relocating
|
||||
# is pointless in this case.
|
||||
return
|
||||
|
||||
for idx in range(n_empty):
|
||||
|
||||
new_cluster_id = empty_clusters[idx]
|
||||
|
||||
far_idx = far_from_centers[idx]
|
||||
weight = sample_weight[far_idx]
|
||||
|
||||
old_cluster_id = labels[far_idx]
|
||||
|
||||
for k in range(n_features):
|
||||
centers_new[old_cluster_id, k] -= X[far_idx, k] * weight
|
||||
centers_new[new_cluster_id, k] = X[far_idx, k] * weight
|
||||
|
||||
weight_in_clusters[new_cluster_id] = weight
|
||||
weight_in_clusters[old_cluster_id] -= weight
|
||||
|
||||
|
||||
cpdef void _relocate_empty_clusters_sparse(
|
||||
const floating[::1] X_data, # IN
|
||||
const int[::1] X_indices, # IN
|
||||
const int[::1] X_indptr, # IN
|
||||
const floating[::1] sample_weight, # IN
|
||||
const floating[:, ::1] centers_old, # IN
|
||||
floating[:, ::1] centers_new, # INOUT
|
||||
floating[::1] weight_in_clusters, # INOUT
|
||||
const int[::1] labels # IN
|
||||
):
|
||||
"""Relocate centers which have no sample assigned to them."""
|
||||
cdef:
|
||||
int[::1] empty_clusters = np.where(np.equal(weight_in_clusters, 0))[0].astype(np.int32)
|
||||
int n_empty = empty_clusters.shape[0]
|
||||
|
||||
if n_empty == 0:
|
||||
return
|
||||
|
||||
cdef:
|
||||
int n_samples = X_indptr.shape[0] - 1
|
||||
int i, j, k
|
||||
|
||||
floating[::1] distances = np.zeros(n_samples, dtype=X_data.base.dtype)
|
||||
floating[::1] centers_squared_norms = row_norms(centers_old, squared=True)
|
||||
|
||||
for i in range(n_samples):
|
||||
j = labels[i]
|
||||
distances[i] = _euclidean_sparse_dense(
|
||||
X_data[X_indptr[i]: X_indptr[i + 1]],
|
||||
X_indices[X_indptr[i]: X_indptr[i + 1]],
|
||||
centers_old[j], centers_squared_norms[j], True)
|
||||
|
||||
if np.max(distances) == 0:
|
||||
# Happens when there are more clusters than non-duplicate samples. Relocating
|
||||
# is pointless in this case.
|
||||
return
|
||||
|
||||
cdef:
|
||||
int[::1] far_from_centers = np.argpartition(distances, -n_empty)[:-n_empty-1:-1].astype(np.int32)
|
||||
|
||||
int new_cluster_id, old_cluster_id, far_idx, idx
|
||||
floating weight
|
||||
|
||||
for idx in range(n_empty):
|
||||
|
||||
new_cluster_id = empty_clusters[idx]
|
||||
|
||||
far_idx = far_from_centers[idx]
|
||||
weight = sample_weight[far_idx]
|
||||
|
||||
old_cluster_id = labels[far_idx]
|
||||
|
||||
for k in range(X_indptr[far_idx], X_indptr[far_idx + 1]):
|
||||
centers_new[old_cluster_id, X_indices[k]] -= X_data[k] * weight
|
||||
centers_new[new_cluster_id, X_indices[k]] = X_data[k] * weight
|
||||
|
||||
weight_in_clusters[new_cluster_id] = weight
|
||||
weight_in_clusters[old_cluster_id] -= weight
|
||||
|
||||
|
||||
cdef void _average_centers(
|
||||
floating[:, ::1] centers, # INOUT
|
||||
const floating[::1] weight_in_clusters # IN
|
||||
):
|
||||
"""Average new centers wrt weights."""
|
||||
cdef:
|
||||
int n_clusters = centers.shape[0]
|
||||
int n_features = centers.shape[1]
|
||||
int j, k
|
||||
floating alpha
|
||||
int argmax_weight = np.argmax(weight_in_clusters)
|
||||
|
||||
for j in range(n_clusters):
|
||||
if weight_in_clusters[j] > 0:
|
||||
alpha = 1.0 / weight_in_clusters[j]
|
||||
for k in range(n_features):
|
||||
centers[j, k] *= alpha
|
||||
else:
|
||||
# For convenience, we avoid setting empty clusters at the origin but place
|
||||
# them at the location of the biggest cluster.
|
||||
for k in range(n_features):
|
||||
centers[j, k] = centers[argmax_weight, k]
|
||||
|
||||
|
||||
cdef void _center_shift(
|
||||
const floating[:, ::1] centers_old, # IN
|
||||
const floating[:, ::1] centers_new, # IN
|
||||
floating[::1] center_shift # OUT
|
||||
):
|
||||
"""Compute shift between old and new centers."""
|
||||
cdef:
|
||||
int n_clusters = centers_old.shape[0]
|
||||
int n_features = centers_old.shape[1]
|
||||
int j
|
||||
|
||||
for j in range(n_clusters):
|
||||
center_shift[j] = _euclidean_dense_dense(
|
||||
¢ers_new[j, 0], ¢ers_old[j, 0], n_features, False)
|
||||
|
||||
|
||||
def _is_same_clustering(
|
||||
const int[::1] labels1,
|
||||
const int[::1] labels2,
|
||||
n_clusters
|
||||
):
|
||||
"""Check if two arrays of labels are the same up to a permutation of the labels"""
|
||||
cdef int[::1] mapping = np.full(fill_value=-1, shape=(n_clusters,), dtype=np.int32)
|
||||
cdef int i
|
||||
|
||||
for i in range(labels1.shape[0]):
|
||||
if mapping[labels1[i]] == -1:
|
||||
mapping[labels1[i]] = labels2[i]
|
||||
elif mapping[labels1[i]] != labels2[i]:
|
||||
return False
|
||||
return True
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,686 @@
|
||||
# Authors: The scikit-learn developers
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
from cython cimport floating
|
||||
from cython.parallel import prange, parallel
|
||||
from libc.stdlib cimport calloc, free
|
||||
from libc.string cimport memset
|
||||
|
||||
from ..utils._openmp_helpers cimport omp_lock_t
|
||||
from ..utils._openmp_helpers cimport omp_init_lock
|
||||
from ..utils._openmp_helpers cimport omp_destroy_lock
|
||||
from ..utils._openmp_helpers cimport omp_set_lock
|
||||
from ..utils._openmp_helpers cimport omp_unset_lock
|
||||
from ..utils.extmath import row_norms
|
||||
from ._k_means_common import CHUNK_SIZE
|
||||
from ._k_means_common cimport _relocate_empty_clusters_dense
|
||||
from ._k_means_common cimport _relocate_empty_clusters_sparse
|
||||
from ._k_means_common cimport _euclidean_dense_dense
|
||||
from ._k_means_common cimport _euclidean_sparse_dense
|
||||
from ._k_means_common cimport _average_centers
|
||||
from ._k_means_common cimport _center_shift
|
||||
|
||||
|
||||
def init_bounds_dense(
|
||||
const floating[:, ::1] X, # IN
|
||||
const floating[:, ::1] centers, # IN
|
||||
const floating[:, ::1] center_half_distances, # IN
|
||||
int[::1] labels, # OUT
|
||||
floating[::1] upper_bounds, # OUT
|
||||
floating[:, ::1] lower_bounds, # OUT
|
||||
int n_threads):
|
||||
"""Initialize upper and lower bounds for each sample for dense input data.
|
||||
|
||||
Given X, centers and the pairwise distances divided by 2.0 between the
|
||||
centers this calculates the upper bounds and lower bounds for each sample.
|
||||
The upper bound for each sample is set to the distance between the sample
|
||||
and the closest center.
|
||||
|
||||
The lower bound for each sample is a one-dimensional array of n_clusters.
|
||||
For each sample i assume that the previously assigned cluster is c1 and the
|
||||
previous closest distance is dist, for a new cluster c2, the
|
||||
lower_bound[i][c2] is set to distance between the sample and this new
|
||||
cluster, if and only if dist > center_half_distances[c1][c2]. This prevents
|
||||
computation of unnecessary distances for each sample to the clusters that
|
||||
it is unlikely to be assigned to.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : ndarray of shape (n_samples, n_features), dtype=floating
|
||||
The input data.
|
||||
|
||||
centers : ndarray of shape (n_clusters, n_features), dtype=floating
|
||||
The cluster centers.
|
||||
|
||||
center_half_distances : ndarray of shape (n_clusters, n_clusters), \
|
||||
dtype=floating
|
||||
The half of the distance between any 2 clusters centers.
|
||||
|
||||
labels : ndarray of shape(n_samples), dtype=int
|
||||
The label for each sample. This array is modified in place.
|
||||
|
||||
upper_bounds : ndarray of shape(n_samples,), dtype=floating
|
||||
The upper bound on the distance between each sample and its closest
|
||||
cluster center. This array is modified in place.
|
||||
|
||||
lower_bounds : ndarray, of shape(n_samples, n_clusters), dtype=floating
|
||||
The lower bound on the distance between each sample and each cluster
|
||||
center. This array is modified in place.
|
||||
|
||||
n_threads : int
|
||||
The number of threads to be used by openmp.
|
||||
"""
|
||||
cdef:
|
||||
int n_samples = X.shape[0]
|
||||
int n_clusters = centers.shape[0]
|
||||
int n_features = X.shape[1]
|
||||
|
||||
floating min_dist, dist
|
||||
int best_cluster, i, j
|
||||
|
||||
for i in prange(
|
||||
n_samples, num_threads=n_threads, schedule='static', nogil=True
|
||||
):
|
||||
best_cluster = 0
|
||||
min_dist = _euclidean_dense_dense(&X[i, 0], ¢ers[0, 0],
|
||||
n_features, False)
|
||||
lower_bounds[i, 0] = min_dist
|
||||
for j in range(1, n_clusters):
|
||||
if min_dist > center_half_distances[best_cluster, j]:
|
||||
dist = _euclidean_dense_dense(&X[i, 0], ¢ers[j, 0],
|
||||
n_features, False)
|
||||
lower_bounds[i, j] = dist
|
||||
if dist < min_dist:
|
||||
min_dist = dist
|
||||
best_cluster = j
|
||||
labels[i] = best_cluster
|
||||
upper_bounds[i] = min_dist
|
||||
|
||||
|
||||
def init_bounds_sparse(
|
||||
X, # IN
|
||||
const floating[:, ::1] centers, # IN
|
||||
const floating[:, ::1] center_half_distances, # IN
|
||||
int[::1] labels, # OUT
|
||||
floating[::1] upper_bounds, # OUT
|
||||
floating[:, ::1] lower_bounds, # OUT
|
||||
int n_threads):
|
||||
"""Initialize upper and lower bounds for each sample for sparse input data.
|
||||
|
||||
Given X, centers and the pairwise distances divided by 2.0 between the
|
||||
centers this calculates the upper bounds and lower bounds for each sample.
|
||||
The upper bound for each sample is set to the distance between the sample
|
||||
and the closest center.
|
||||
|
||||
The lower bound for each sample is a one-dimensional array of n_clusters.
|
||||
For each sample i assume that the previously assigned cluster is c1 and the
|
||||
previous closest distance is dist, for a new cluster c2, the
|
||||
lower_bound[i][c2] is set to distance between the sample and this new
|
||||
cluster, if and only if dist > center_half_distances[c1][c2]. This prevents
|
||||
computation of unnecessary distances for each sample to the clusters that
|
||||
it is unlikely to be assigned to.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : sparse matrix of shape (n_samples, n_features), dtype=floating
|
||||
The input data. Must be in CSR format.
|
||||
|
||||
centers : ndarray of shape (n_clusters, n_features), dtype=floating
|
||||
The cluster centers.
|
||||
|
||||
center_half_distances : ndarray of shape (n_clusters, n_clusters), \
|
||||
dtype=floating
|
||||
The half of the distance between any 2 clusters centers.
|
||||
|
||||
labels : ndarray of shape(n_samples), dtype=int
|
||||
The label for each sample. This array is modified in place.
|
||||
|
||||
upper_bounds : ndarray of shape(n_samples,), dtype=floating
|
||||
The upper bound on the distance between each sample and its closest
|
||||
cluster center. This array is modified in place.
|
||||
|
||||
lower_bounds : ndarray of shape(n_samples, n_clusters), dtype=floating
|
||||
The lower bound on the distance between each sample and each cluster
|
||||
center. This array is modified in place.
|
||||
|
||||
n_threads : int
|
||||
The number of threads to be used by openmp.
|
||||
"""
|
||||
cdef:
|
||||
int n_samples = X.shape[0]
|
||||
int n_clusters = centers.shape[0]
|
||||
|
||||
floating[::1] X_data = X.data
|
||||
int[::1] X_indices = X.indices
|
||||
int[::1] X_indptr = X.indptr
|
||||
|
||||
floating min_dist, dist
|
||||
int best_cluster, i, j
|
||||
|
||||
floating[::1] centers_squared_norms = row_norms(centers, squared=True)
|
||||
|
||||
for i in prange(
|
||||
n_samples, num_threads=n_threads, schedule='static', nogil=True
|
||||
):
|
||||
best_cluster = 0
|
||||
min_dist = _euclidean_sparse_dense(
|
||||
X_data[X_indptr[i]: X_indptr[i + 1]],
|
||||
X_indices[X_indptr[i]: X_indptr[i + 1]],
|
||||
centers[0], centers_squared_norms[0], False)
|
||||
|
||||
lower_bounds[i, 0] = min_dist
|
||||
for j in range(1, n_clusters):
|
||||
if min_dist > center_half_distances[best_cluster, j]:
|
||||
dist = _euclidean_sparse_dense(
|
||||
X_data[X_indptr[i]: X_indptr[i + 1]],
|
||||
X_indices[X_indptr[i]: X_indptr[i + 1]],
|
||||
centers[j], centers_squared_norms[j], False)
|
||||
lower_bounds[i, j] = dist
|
||||
if dist < min_dist:
|
||||
min_dist = dist
|
||||
best_cluster = j
|
||||
labels[i] = best_cluster
|
||||
upper_bounds[i] = min_dist
|
||||
|
||||
|
||||
def elkan_iter_chunked_dense(
|
||||
const floating[:, ::1] X, # IN
|
||||
const floating[::1] sample_weight, # IN
|
||||
const floating[:, ::1] centers_old, # IN
|
||||
floating[:, ::1] centers_new, # OUT
|
||||
floating[::1] weight_in_clusters, # OUT
|
||||
const floating[:, ::1] center_half_distances, # IN
|
||||
const floating[::1] distance_next_center, # IN
|
||||
floating[::1] upper_bounds, # INOUT
|
||||
floating[:, ::1] lower_bounds, # INOUT
|
||||
int[::1] labels, # INOUT
|
||||
floating[::1] center_shift, # OUT
|
||||
int n_threads,
|
||||
bint update_centers=True):
|
||||
"""Single iteration of K-means Elkan algorithm with dense input.
|
||||
|
||||
Update labels and centers (inplace), for one iteration, distributed
|
||||
over data chunks.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : ndarray of shape (n_samples, n_features), dtype=floating
|
||||
The observations to cluster.
|
||||
|
||||
sample_weight : ndarray of shape (n_samples,), dtype=floating
|
||||
The weights for each observation in X.
|
||||
|
||||
centers_old : ndarray of shape (n_clusters, n_features), dtype=floating
|
||||
Centers before previous iteration, placeholder for the centers after
|
||||
previous iteration.
|
||||
|
||||
centers_new : ndarray of shape (n_clusters, n_features), dtype=floating
|
||||
Centers after previous iteration, placeholder for the new centers
|
||||
computed during this iteration.
|
||||
|
||||
weight_in_clusters : ndarray of shape (n_clusters,), dtype=floating
|
||||
Placeholder for the sums of the weights of every observation assigned
|
||||
to each center.
|
||||
|
||||
center_half_distances : ndarray of shape (n_clusters, n_clusters), \
|
||||
dtype=floating
|
||||
Half pairwise distances between centers.
|
||||
|
||||
distance_next_center : ndarray of shape (n_clusters,), dtype=floating
|
||||
Distance between each center its closest center.
|
||||
|
||||
upper_bounds : ndarray of shape (n_samples,), dtype=floating
|
||||
Upper bound for the distance between each sample and its center,
|
||||
updated inplace.
|
||||
|
||||
lower_bounds : ndarray of shape (n_samples, n_clusters), dtype=floating
|
||||
Lower bound for the distance between each sample and each center,
|
||||
updated inplace.
|
||||
|
||||
labels : ndarray of shape (n_samples,), dtype=int
|
||||
labels assignment.
|
||||
|
||||
center_shift : ndarray of shape (n_clusters,), dtype=floating
|
||||
Distance between old and new centers.
|
||||
|
||||
n_threads : int
|
||||
The number of threads to be used by openmp.
|
||||
|
||||
update_centers : bool
|
||||
- If True, the labels and the new centers will be computed, i.e. runs
|
||||
the E-step and the M-step of the algorithm.
|
||||
- If False, only the labels will be computed, i.e runs the E-step of
|
||||
the algorithm. This is useful especially when calling predict on a
|
||||
fitted model.
|
||||
"""
|
||||
cdef:
|
||||
int n_samples = X.shape[0]
|
||||
int n_features = X.shape[1]
|
||||
int n_clusters = centers_new.shape[0]
|
||||
|
||||
if n_samples == 0:
|
||||
# An empty array was passed, do nothing and return early (before
|
||||
# attempting to compute n_chunks). This can typically happen when
|
||||
# calling the prediction function of a bisecting k-means model with a
|
||||
# large fraction of outliers.
|
||||
return
|
||||
|
||||
cdef:
|
||||
# hard-coded number of samples per chunk. Splitting in chunks is
|
||||
# necessary to get parallelism. Chunk size chosen to be same as lloyd's
|
||||
int n_samples_chunk = CHUNK_SIZE if n_samples > CHUNK_SIZE else n_samples
|
||||
int n_chunks = n_samples // n_samples_chunk
|
||||
int n_samples_rem = n_samples % n_samples_chunk
|
||||
int chunk_idx
|
||||
int start, end
|
||||
|
||||
int i, j, k
|
||||
|
||||
floating *centers_new_chunk
|
||||
floating *weight_in_clusters_chunk
|
||||
|
||||
omp_lock_t lock
|
||||
|
||||
# count remainder chunk in total number of chunks
|
||||
n_chunks += n_samples != n_chunks * n_samples_chunk
|
||||
|
||||
# number of threads should not be bigger than number of chunks
|
||||
n_threads = min(n_threads, n_chunks)
|
||||
|
||||
if update_centers:
|
||||
memset(¢ers_new[0, 0], 0, n_clusters * n_features * sizeof(floating))
|
||||
memset(&weight_in_clusters[0], 0, n_clusters * sizeof(floating))
|
||||
omp_init_lock(&lock)
|
||||
|
||||
with nogil, parallel(num_threads=n_threads):
|
||||
# thread local buffers
|
||||
centers_new_chunk = <floating*> calloc(n_clusters * n_features, sizeof(floating))
|
||||
weight_in_clusters_chunk = <floating*> calloc(n_clusters, sizeof(floating))
|
||||
|
||||
for chunk_idx in prange(n_chunks, schedule='static'):
|
||||
start = chunk_idx * n_samples_chunk
|
||||
if chunk_idx == n_chunks - 1 and n_samples_rem > 0:
|
||||
end = start + n_samples_rem
|
||||
else:
|
||||
end = start + n_samples_chunk
|
||||
|
||||
_update_chunk_dense(
|
||||
X[start: end],
|
||||
sample_weight[start: end],
|
||||
centers_old,
|
||||
center_half_distances,
|
||||
distance_next_center,
|
||||
labels[start: end],
|
||||
upper_bounds[start: end],
|
||||
lower_bounds[start: end],
|
||||
centers_new_chunk,
|
||||
weight_in_clusters_chunk,
|
||||
update_centers)
|
||||
|
||||
# reduction from local buffers.
|
||||
if update_centers:
|
||||
# The lock is necessary to avoid race conditions when aggregating
|
||||
# info from different thread-local buffers.
|
||||
omp_set_lock(&lock)
|
||||
for j in range(n_clusters):
|
||||
weight_in_clusters[j] += weight_in_clusters_chunk[j]
|
||||
for k in range(n_features):
|
||||
centers_new[j, k] += centers_new_chunk[j * n_features + k]
|
||||
omp_unset_lock(&lock)
|
||||
|
||||
free(centers_new_chunk)
|
||||
free(weight_in_clusters_chunk)
|
||||
|
||||
if update_centers:
|
||||
omp_destroy_lock(&lock)
|
||||
_relocate_empty_clusters_dense(X, sample_weight, centers_old,
|
||||
centers_new, weight_in_clusters, labels)
|
||||
|
||||
_average_centers(centers_new, weight_in_clusters)
|
||||
_center_shift(centers_old, centers_new, center_shift)
|
||||
|
||||
# update lower and upper bounds
|
||||
for i in range(n_samples):
|
||||
upper_bounds[i] += center_shift[labels[i]]
|
||||
|
||||
for j in range(n_clusters):
|
||||
lower_bounds[i, j] -= center_shift[j]
|
||||
if lower_bounds[i, j] < 0:
|
||||
lower_bounds[i, j] = 0
|
||||
|
||||
|
||||
cdef void _update_chunk_dense(
|
||||
const floating[:, ::1] X, # IN
|
||||
const floating[::1] sample_weight, # IN
|
||||
const floating[:, ::1] centers_old, # IN
|
||||
const floating[:, ::1] center_half_distances, # IN
|
||||
const floating[::1] distance_next_center, # IN
|
||||
int[::1] labels, # INOUT
|
||||
floating[::1] upper_bounds, # INOUT
|
||||
floating[:, ::1] lower_bounds, # INOUT
|
||||
floating *centers_new, # OUT
|
||||
floating *weight_in_clusters, # OUT
|
||||
bint update_centers) noexcept nogil:
|
||||
"""K-means combined EM step for one dense data chunk.
|
||||
|
||||
Compute the partial contribution of a single data chunk to the labels and
|
||||
centers.
|
||||
"""
|
||||
cdef:
|
||||
int n_samples = labels.shape[0]
|
||||
int n_clusters = centers_old.shape[0]
|
||||
int n_features = centers_old.shape[1]
|
||||
|
||||
floating upper_bound, distance
|
||||
int i, j, k, label
|
||||
|
||||
for i in range(n_samples):
|
||||
upper_bound = upper_bounds[i]
|
||||
bounds_tight = 0
|
||||
label = labels[i]
|
||||
|
||||
# Next center is not far away from the currently assigned center.
|
||||
# Sample might need to be assigned to another center.
|
||||
if not distance_next_center[label] >= upper_bound:
|
||||
|
||||
for j in range(n_clusters):
|
||||
|
||||
# If this holds, then center_index is a good candidate for the
|
||||
# sample to be relabelled, and we need to confirm this by
|
||||
# recomputing the upper and lower bounds.
|
||||
if (
|
||||
j != label
|
||||
and (upper_bound > lower_bounds[i, j])
|
||||
and (upper_bound > center_half_distances[label, j])
|
||||
):
|
||||
|
||||
# Recompute upper bound by calculating the actual distance
|
||||
# between the sample and its current assigned center.
|
||||
if not bounds_tight:
|
||||
upper_bound = _euclidean_dense_dense(
|
||||
&X[i, 0], ¢ers_old[label, 0], n_features, False)
|
||||
lower_bounds[i, label] = upper_bound
|
||||
bounds_tight = 1
|
||||
|
||||
# If the condition still holds, then compute the actual
|
||||
# distance between the sample and center. If this is less
|
||||
# than the previous distance, reassign label.
|
||||
if (
|
||||
upper_bound > lower_bounds[i, j]
|
||||
or (upper_bound > center_half_distances[label, j])
|
||||
):
|
||||
|
||||
distance = _euclidean_dense_dense(
|
||||
&X[i, 0], ¢ers_old[j, 0], n_features, False)
|
||||
lower_bounds[i, j] = distance
|
||||
if distance < upper_bound:
|
||||
label = j
|
||||
upper_bound = distance
|
||||
|
||||
labels[i] = label
|
||||
upper_bounds[i] = upper_bound
|
||||
|
||||
if update_centers:
|
||||
weight_in_clusters[label] += sample_weight[i]
|
||||
for k in range(n_features):
|
||||
centers_new[label * n_features + k] += X[i, k] * sample_weight[i]
|
||||
|
||||
|
||||
def elkan_iter_chunked_sparse(
|
||||
X, # IN
|
||||
const floating[::1] sample_weight, # IN
|
||||
const floating[:, ::1] centers_old, # IN
|
||||
floating[:, ::1] centers_new, # OUT
|
||||
floating[::1] weight_in_clusters, # OUT
|
||||
const floating[:, ::1] center_half_distances, # IN
|
||||
const floating[::1] distance_next_center, # IN
|
||||
floating[::1] upper_bounds, # INOUT
|
||||
floating[:, ::1] lower_bounds, # INOUT
|
||||
int[::1] labels, # INOUT
|
||||
floating[::1] center_shift, # OUT
|
||||
int n_threads,
|
||||
bint update_centers=True):
|
||||
"""Single iteration of K-means Elkan algorithm with sparse input.
|
||||
|
||||
Update labels and centers (inplace), for one iteration, distributed
|
||||
over data chunks.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : sparse matrix of shape (n_samples, n_features)
|
||||
The observations to cluster. Must be in CSR format.
|
||||
|
||||
sample_weight : ndarray of shape (n_samples,), dtype=floating
|
||||
The weights for each observation in X.
|
||||
|
||||
centers_old : ndarray of shape (n_clusters, n_features), dtype=floating
|
||||
Centers before previous iteration, placeholder for the centers after
|
||||
previous iteration.
|
||||
|
||||
centers_new : ndarray of shape (n_clusters, n_features), dtype=floating
|
||||
Centers after previous iteration, placeholder for the new centers
|
||||
computed during this iteration.
|
||||
|
||||
weight_in_clusters : ndarray of shape (n_clusters,), dtype=floating
|
||||
Placeholder for the sums of the weights of every observation assigned
|
||||
to each center.
|
||||
|
||||
center_half_distances : ndarray of shape (n_clusters, n_clusters), \
|
||||
dtype=floating
|
||||
Half pairwise distances between centers.
|
||||
|
||||
distance_next_center : ndarray of shape (n_clusters,), dtype=floating
|
||||
Distance between each center its closest center.
|
||||
|
||||
upper_bounds : ndarray of shape (n_samples,), dtype=floating
|
||||
Upper bound for the distance between each sample and its center,
|
||||
updated inplace.
|
||||
|
||||
lower_bounds : ndarray of shape (n_samples, n_clusters), dtype=floating
|
||||
Lower bound for the distance between each sample and each center,
|
||||
updated inplace.
|
||||
|
||||
labels : ndarray of shape (n_samples,), dtype=int
|
||||
labels assignment.
|
||||
|
||||
center_shift : ndarray of shape (n_clusters,), dtype=floating
|
||||
Distance between old and new centers.
|
||||
|
||||
n_threads : int
|
||||
The number of threads to be used by openmp.
|
||||
|
||||
update_centers : bool
|
||||
- If True, the labels and the new centers will be computed, i.e. runs
|
||||
the E-step and the M-step of the algorithm.
|
||||
- If False, only the labels will be computed, i.e runs the E-step of
|
||||
the algorithm. This is useful especially when calling predict on a
|
||||
fitted model.
|
||||
"""
|
||||
cdef:
|
||||
int n_samples = X.shape[0]
|
||||
int n_features = X.shape[1]
|
||||
int n_clusters = centers_new.shape[0]
|
||||
|
||||
if n_samples == 0:
|
||||
# An empty array was passed, do nothing and return early (before
|
||||
# attempting to compute n_chunks). This can typically happen when
|
||||
# calling the prediction function of a bisecting k-means model with a
|
||||
# large fraction of outliers.
|
||||
return
|
||||
|
||||
cdef:
|
||||
floating[::1] X_data = X.data
|
||||
int[::1] X_indices = X.indices
|
||||
int[::1] X_indptr = X.indptr
|
||||
|
||||
# hard-coded number of samples per chunk. Splitting in chunks is
|
||||
# necessary to get parallelism. Chunk size chosen to be same as lloyd's
|
||||
int n_samples_chunk = CHUNK_SIZE if n_samples > CHUNK_SIZE else n_samples
|
||||
int n_chunks = n_samples // n_samples_chunk
|
||||
int n_samples_rem = n_samples % n_samples_chunk
|
||||
int chunk_idx
|
||||
int start, end
|
||||
|
||||
int i, j, k
|
||||
|
||||
floating[::1] centers_squared_norms = row_norms(centers_old, squared=True)
|
||||
|
||||
floating *centers_new_chunk
|
||||
floating *weight_in_clusters_chunk
|
||||
|
||||
omp_lock_t lock
|
||||
|
||||
# count remainder chunk in total number of chunks
|
||||
n_chunks += n_samples != n_chunks * n_samples_chunk
|
||||
|
||||
# number of threads should not be bigger than number of chunks
|
||||
n_threads = min(n_threads, n_chunks)
|
||||
|
||||
if update_centers:
|
||||
memset(¢ers_new[0, 0], 0, n_clusters * n_features * sizeof(floating))
|
||||
memset(&weight_in_clusters[0], 0, n_clusters * sizeof(floating))
|
||||
omp_init_lock(&lock)
|
||||
|
||||
with nogil, parallel(num_threads=n_threads):
|
||||
# thread local buffers
|
||||
centers_new_chunk = <floating*> calloc(n_clusters * n_features, sizeof(floating))
|
||||
weight_in_clusters_chunk = <floating*> calloc(n_clusters, sizeof(floating))
|
||||
|
||||
for chunk_idx in prange(n_chunks, schedule='static'):
|
||||
start = chunk_idx * n_samples_chunk
|
||||
if chunk_idx == n_chunks - 1 and n_samples_rem > 0:
|
||||
end = start + n_samples_rem
|
||||
else:
|
||||
end = start + n_samples_chunk
|
||||
|
||||
_update_chunk_sparse(
|
||||
X_data[X_indptr[start]: X_indptr[end]],
|
||||
X_indices[X_indptr[start]: X_indptr[end]],
|
||||
X_indptr[start: end+1],
|
||||
sample_weight[start: end],
|
||||
centers_old,
|
||||
centers_squared_norms,
|
||||
center_half_distances,
|
||||
distance_next_center,
|
||||
labels[start: end],
|
||||
upper_bounds[start: end],
|
||||
lower_bounds[start: end],
|
||||
centers_new_chunk,
|
||||
weight_in_clusters_chunk,
|
||||
update_centers)
|
||||
|
||||
# reduction from local buffers.
|
||||
if update_centers:
|
||||
# The lock is necessary to avoid race conditions when aggregating
|
||||
# info from different thread-local buffers.
|
||||
omp_set_lock(&lock)
|
||||
for j in range(n_clusters):
|
||||
weight_in_clusters[j] += weight_in_clusters_chunk[j]
|
||||
for k in range(n_features):
|
||||
centers_new[j, k] += centers_new_chunk[j * n_features + k]
|
||||
omp_unset_lock(&lock)
|
||||
|
||||
free(centers_new_chunk)
|
||||
free(weight_in_clusters_chunk)
|
||||
|
||||
if update_centers:
|
||||
omp_destroy_lock(&lock)
|
||||
_relocate_empty_clusters_sparse(
|
||||
X_data, X_indices, X_indptr, sample_weight,
|
||||
centers_old, centers_new, weight_in_clusters, labels)
|
||||
|
||||
_average_centers(centers_new, weight_in_clusters)
|
||||
_center_shift(centers_old, centers_new, center_shift)
|
||||
|
||||
# update lower and upper bounds
|
||||
for i in range(n_samples):
|
||||
upper_bounds[i] += center_shift[labels[i]]
|
||||
|
||||
for j in range(n_clusters):
|
||||
lower_bounds[i, j] -= center_shift[j]
|
||||
if lower_bounds[i, j] < 0:
|
||||
lower_bounds[i, j] = 0
|
||||
|
||||
|
||||
cdef void _update_chunk_sparse(
|
||||
const floating[::1] X_data, # IN
|
||||
const int[::1] X_indices, # IN
|
||||
const int[::1] X_indptr, # IN
|
||||
const floating[::1] sample_weight, # IN
|
||||
const floating[:, ::1] centers_old, # IN
|
||||
const floating[::1] centers_squared_norms, # IN
|
||||
const floating[:, ::1] center_half_distances, # IN
|
||||
const floating[::1] distance_next_center, # IN
|
||||
int[::1] labels, # INOUT
|
||||
floating[::1] upper_bounds, # INOUT
|
||||
floating[:, ::1] lower_bounds, # INOUT
|
||||
floating *centers_new, # OUT
|
||||
floating *weight_in_clusters, # OUT
|
||||
bint update_centers) noexcept nogil:
|
||||
"""K-means combined EM step for one sparse data chunk.
|
||||
|
||||
Compute the partial contribution of a single data chunk to the labels and
|
||||
centers.
|
||||
"""
|
||||
cdef:
|
||||
int n_samples = labels.shape[0]
|
||||
int n_clusters = centers_old.shape[0]
|
||||
int n_features = centers_old.shape[1]
|
||||
|
||||
floating upper_bound, distance
|
||||
int i, j, k, label
|
||||
int s = X_indptr[0]
|
||||
|
||||
for i in range(n_samples):
|
||||
upper_bound = upper_bounds[i]
|
||||
bounds_tight = 0
|
||||
label = labels[i]
|
||||
|
||||
# Next center is not far away from the currently assigned center.
|
||||
# Sample might need to be assigned to another center.
|
||||
if not distance_next_center[label] >= upper_bound:
|
||||
|
||||
for j in range(n_clusters):
|
||||
|
||||
# If this holds, then center_index is a good candidate for the
|
||||
# sample to be relabelled, and we need to confirm this by
|
||||
# recomputing the upper and lower bounds.
|
||||
if (
|
||||
j != label
|
||||
and (upper_bound > lower_bounds[i, j])
|
||||
and (upper_bound > center_half_distances[label, j])
|
||||
):
|
||||
|
||||
# Recompute upper bound by calculating the actual distance
|
||||
# between the sample and its current assigned center.
|
||||
if not bounds_tight:
|
||||
upper_bound = _euclidean_sparse_dense(
|
||||
X_data[X_indptr[i] - s: X_indptr[i + 1] - s],
|
||||
X_indices[X_indptr[i] - s: X_indptr[i + 1] - s],
|
||||
centers_old[label], centers_squared_norms[label], False)
|
||||
lower_bounds[i, label] = upper_bound
|
||||
bounds_tight = 1
|
||||
|
||||
# If the condition still holds, then compute the actual
|
||||
# distance between the sample and center. If this is less
|
||||
# than the previous distance, reassign label.
|
||||
if (
|
||||
upper_bound > lower_bounds[i, j]
|
||||
or (upper_bound > center_half_distances[label, j])
|
||||
):
|
||||
distance = _euclidean_sparse_dense(
|
||||
X_data[X_indptr[i] - s: X_indptr[i + 1] - s],
|
||||
X_indices[X_indptr[i] - s: X_indptr[i + 1] - s],
|
||||
centers_old[j], centers_squared_norms[j], False)
|
||||
lower_bounds[i, j] = distance
|
||||
if distance < upper_bound:
|
||||
label = j
|
||||
upper_bound = distance
|
||||
|
||||
labels[i] = label
|
||||
upper_bounds[i] = upper_bound
|
||||
|
||||
if update_centers:
|
||||
weight_in_clusters[label] += sample_weight[i]
|
||||
for k in range(X_indptr[i] - s, X_indptr[i + 1] - s):
|
||||
centers_new[label * n_features + X_indices[k]] += X_data[k] * sample_weight[i]
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,420 @@
|
||||
# Licence: BSD 3 clause
|
||||
|
||||
from cython cimport floating
|
||||
from cython.parallel import prange, parallel
|
||||
from libc.stdlib cimport malloc, calloc, free
|
||||
from libc.string cimport memset
|
||||
from libc.float cimport DBL_MAX, FLT_MAX
|
||||
|
||||
from ..utils._openmp_helpers cimport omp_lock_t
|
||||
from ..utils._openmp_helpers cimport omp_init_lock
|
||||
from ..utils._openmp_helpers cimport omp_destroy_lock
|
||||
from ..utils._openmp_helpers cimport omp_set_lock
|
||||
from ..utils._openmp_helpers cimport omp_unset_lock
|
||||
from ..utils.extmath import row_norms
|
||||
from ..utils._cython_blas cimport _gemm
|
||||
from ..utils._cython_blas cimport RowMajor, Trans, NoTrans
|
||||
from ._k_means_common import CHUNK_SIZE
|
||||
from ._k_means_common cimport _relocate_empty_clusters_dense
|
||||
from ._k_means_common cimport _relocate_empty_clusters_sparse
|
||||
from ._k_means_common cimport _average_centers, _center_shift
|
||||
|
||||
|
||||
def lloyd_iter_chunked_dense(
|
||||
const floating[:, ::1] X, # IN
|
||||
const floating[::1] sample_weight, # IN
|
||||
const floating[:, ::1] centers_old, # IN
|
||||
floating[:, ::1] centers_new, # OUT
|
||||
floating[::1] weight_in_clusters, # OUT
|
||||
int[::1] labels, # OUT
|
||||
floating[::1] center_shift, # OUT
|
||||
int n_threads,
|
||||
bint update_centers=True):
|
||||
"""Single iteration of K-means lloyd algorithm with dense input.
|
||||
|
||||
Update labels and centers (inplace), for one iteration, distributed
|
||||
over data chunks.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : ndarray of shape (n_samples, n_features), dtype=floating
|
||||
The observations to cluster.
|
||||
|
||||
sample_weight : ndarray of shape (n_samples,), dtype=floating
|
||||
The weights for each observation in X.
|
||||
|
||||
centers_old : ndarray of shape (n_clusters, n_features), dtype=floating
|
||||
Centers before previous iteration, placeholder for the centers after
|
||||
previous iteration.
|
||||
|
||||
centers_new : ndarray of shape (n_clusters, n_features), dtype=floating
|
||||
Centers after previous iteration, placeholder for the new centers
|
||||
computed during this iteration. `centers_new` can be `None` if
|
||||
`update_centers` is False.
|
||||
|
||||
weight_in_clusters : ndarray of shape (n_clusters,), dtype=floating
|
||||
Placeholder for the sums of the weights of every observation assigned
|
||||
to each center. `weight_in_clusters` can be `None` if `update_centers`
|
||||
is False.
|
||||
|
||||
labels : ndarray of shape (n_samples,), dtype=int
|
||||
labels assignment.
|
||||
|
||||
center_shift : ndarray of shape (n_clusters,), dtype=floating
|
||||
Distance between old and new centers.
|
||||
|
||||
n_threads : int
|
||||
The number of threads to be used by openmp.
|
||||
|
||||
update_centers : bool
|
||||
- If True, the labels and the new centers will be computed, i.e. runs
|
||||
the E-step and the M-step of the algorithm.
|
||||
- If False, only the labels will be computed, i.e runs the E-step of
|
||||
the algorithm. This is useful especially when calling predict on a
|
||||
fitted model.
|
||||
"""
|
||||
cdef:
|
||||
int n_samples = X.shape[0]
|
||||
int n_features = X.shape[1]
|
||||
int n_clusters = centers_old.shape[0]
|
||||
|
||||
if n_samples == 0:
|
||||
# An empty array was passed, do nothing and return early (before
|
||||
# attempting to compute n_chunks). This can typically happen when
|
||||
# calling the prediction function of a bisecting k-means model with a
|
||||
# large fraction of outliers.
|
||||
return
|
||||
|
||||
cdef:
|
||||
# hard-coded number of samples per chunk. Appeared to be close to
|
||||
# optimal in all situations.
|
||||
int n_samples_chunk = CHUNK_SIZE if n_samples > CHUNK_SIZE else n_samples
|
||||
int n_chunks = n_samples // n_samples_chunk
|
||||
int n_samples_rem = n_samples % n_samples_chunk
|
||||
int chunk_idx
|
||||
int start, end
|
||||
|
||||
int j, k
|
||||
|
||||
floating[::1] centers_squared_norms = row_norms(centers_old, squared=True)
|
||||
|
||||
floating *centers_new_chunk
|
||||
floating *weight_in_clusters_chunk
|
||||
floating *pairwise_distances_chunk
|
||||
|
||||
omp_lock_t lock
|
||||
|
||||
# count remainder chunk in total number of chunks
|
||||
n_chunks += n_samples != n_chunks * n_samples_chunk
|
||||
|
||||
# number of threads should not be bigger than number of chunks
|
||||
n_threads = min(n_threads, n_chunks)
|
||||
|
||||
if update_centers:
|
||||
memset(¢ers_new[0, 0], 0, n_clusters * n_features * sizeof(floating))
|
||||
memset(&weight_in_clusters[0], 0, n_clusters * sizeof(floating))
|
||||
omp_init_lock(&lock)
|
||||
|
||||
with nogil, parallel(num_threads=n_threads):
|
||||
# thread local buffers
|
||||
centers_new_chunk = <floating*> calloc(n_clusters * n_features, sizeof(floating))
|
||||
weight_in_clusters_chunk = <floating*> calloc(n_clusters, sizeof(floating))
|
||||
pairwise_distances_chunk = <floating*> malloc(n_samples_chunk * n_clusters * sizeof(floating))
|
||||
|
||||
for chunk_idx in prange(n_chunks, schedule='static'):
|
||||
start = chunk_idx * n_samples_chunk
|
||||
if chunk_idx == n_chunks - 1 and n_samples_rem > 0:
|
||||
end = start + n_samples_rem
|
||||
else:
|
||||
end = start + n_samples_chunk
|
||||
|
||||
_update_chunk_dense(
|
||||
X[start: end],
|
||||
sample_weight[start: end],
|
||||
centers_old,
|
||||
centers_squared_norms,
|
||||
labels[start: end],
|
||||
centers_new_chunk,
|
||||
weight_in_clusters_chunk,
|
||||
pairwise_distances_chunk,
|
||||
update_centers)
|
||||
|
||||
# reduction from local buffers.
|
||||
if update_centers:
|
||||
# The lock is necessary to avoid race conditions when aggregating
|
||||
# info from different thread-local buffers.
|
||||
omp_set_lock(&lock)
|
||||
for j in range(n_clusters):
|
||||
weight_in_clusters[j] += weight_in_clusters_chunk[j]
|
||||
for k in range(n_features):
|
||||
centers_new[j, k] += centers_new_chunk[j * n_features + k]
|
||||
|
||||
omp_unset_lock(&lock)
|
||||
|
||||
free(centers_new_chunk)
|
||||
free(weight_in_clusters_chunk)
|
||||
free(pairwise_distances_chunk)
|
||||
|
||||
if update_centers:
|
||||
omp_destroy_lock(&lock)
|
||||
_relocate_empty_clusters_dense(
|
||||
X, sample_weight, centers_old, centers_new, weight_in_clusters, labels
|
||||
)
|
||||
|
||||
_average_centers(centers_new, weight_in_clusters)
|
||||
_center_shift(centers_old, centers_new, center_shift)
|
||||
|
||||
|
||||
cdef void _update_chunk_dense(
|
||||
const floating[:, ::1] X, # IN
|
||||
const floating[::1] sample_weight, # IN
|
||||
const floating[:, ::1] centers_old, # IN
|
||||
const floating[::1] centers_squared_norms, # IN
|
||||
int[::1] labels, # OUT
|
||||
floating *centers_new, # OUT
|
||||
floating *weight_in_clusters, # OUT
|
||||
floating *pairwise_distances, # OUT
|
||||
bint update_centers) noexcept nogil:
|
||||
"""K-means combined EM step for one dense data chunk.
|
||||
|
||||
Compute the partial contribution of a single data chunk to the labels and
|
||||
centers.
|
||||
"""
|
||||
cdef:
|
||||
int n_samples = labels.shape[0]
|
||||
int n_clusters = centers_old.shape[0]
|
||||
int n_features = centers_old.shape[1]
|
||||
|
||||
floating sq_dist, min_sq_dist
|
||||
int i, j, k, label
|
||||
|
||||
# Instead of computing the full pairwise squared distances matrix,
|
||||
# ||X - C||² = ||X||² - 2 X.C^T + ||C||², we only need to store
|
||||
# the - 2 X.C^T + ||C||² term since the argmin for a given sample only
|
||||
# depends on the centers.
|
||||
# pairwise_distances = ||C||²
|
||||
for i in range(n_samples):
|
||||
for j in range(n_clusters):
|
||||
pairwise_distances[i * n_clusters + j] = centers_squared_norms[j]
|
||||
|
||||
# pairwise_distances += -2 * X.dot(C.T)
|
||||
_gemm(RowMajor, NoTrans, Trans, n_samples, n_clusters, n_features,
|
||||
-2.0, &X[0, 0], n_features, ¢ers_old[0, 0], n_features,
|
||||
1.0, pairwise_distances, n_clusters)
|
||||
|
||||
for i in range(n_samples):
|
||||
min_sq_dist = pairwise_distances[i * n_clusters]
|
||||
label = 0
|
||||
for j in range(1, n_clusters):
|
||||
sq_dist = pairwise_distances[i * n_clusters + j]
|
||||
if sq_dist < min_sq_dist:
|
||||
min_sq_dist = sq_dist
|
||||
label = j
|
||||
labels[i] = label
|
||||
|
||||
if update_centers:
|
||||
weight_in_clusters[label] += sample_weight[i]
|
||||
for k in range(n_features):
|
||||
centers_new[label * n_features + k] += X[i, k] * sample_weight[i]
|
||||
|
||||
|
||||
def lloyd_iter_chunked_sparse(
|
||||
X, # IN
|
||||
const floating[::1] sample_weight, # IN
|
||||
const floating[:, ::1] centers_old, # IN
|
||||
floating[:, ::1] centers_new, # OUT
|
||||
floating[::1] weight_in_clusters, # OUT
|
||||
int[::1] labels, # OUT
|
||||
floating[::1] center_shift, # OUT
|
||||
int n_threads,
|
||||
bint update_centers=True):
|
||||
"""Single iteration of K-means lloyd algorithm with sparse input.
|
||||
|
||||
Update labels and centers (inplace), for one iteration, distributed
|
||||
over data chunks.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : sparse matrix of shape (n_samples, n_features), dtype=floating
|
||||
The observations to cluster. Must be in CSR format.
|
||||
|
||||
sample_weight : ndarray of shape (n_samples,), dtype=floating
|
||||
The weights for each observation in X.
|
||||
|
||||
centers_old : ndarray of shape (n_clusters, n_features), dtype=floating
|
||||
Centers before previous iteration, placeholder for the centers after
|
||||
previous iteration.
|
||||
|
||||
centers_new : ndarray of shape (n_clusters, n_features), dtype=floating
|
||||
Centers after previous iteration, placeholder for the new centers
|
||||
computed during this iteration. `centers_new` can be `None` if
|
||||
`update_centers` is False.
|
||||
|
||||
weight_in_clusters : ndarray of shape (n_clusters,), dtype=floating
|
||||
Placeholder for the sums of the weights of every observation assigned
|
||||
to each center. `weight_in_clusters` can be `None` if `update_centers`
|
||||
is False.
|
||||
|
||||
labels : ndarray of shape (n_samples,), dtype=int
|
||||
labels assignment.
|
||||
|
||||
center_shift : ndarray of shape (n_clusters,), dtype=floating
|
||||
Distance between old and new centers.
|
||||
|
||||
n_threads : int
|
||||
The number of threads to be used by openmp.
|
||||
|
||||
update_centers : bool
|
||||
- If True, the labels and the new centers will be computed, i.e. runs
|
||||
the E-step and the M-step of the algorithm.
|
||||
- If False, only the labels will be computed, i.e runs the E-step of
|
||||
the algorithm. This is useful especially when calling predict on a
|
||||
fitted model.
|
||||
"""
|
||||
cdef:
|
||||
int n_samples = X.shape[0]
|
||||
int n_features = X.shape[1]
|
||||
int n_clusters = centers_old.shape[0]
|
||||
|
||||
if n_samples == 0:
|
||||
# An empty array was passed, do nothing and return early (before
|
||||
# attempting to compute n_chunks). This can typically happen when
|
||||
# calling the prediction function of a bisecting k-means model with a
|
||||
# large fraction of outliers.
|
||||
return
|
||||
|
||||
cdef:
|
||||
# Choose same as for dense. Does not have the same impact since with
|
||||
# sparse data the pairwise distances matrix is not precomputed.
|
||||
# However, splitting in chunks is necessary to get parallelism.
|
||||
int n_samples_chunk = CHUNK_SIZE if n_samples > CHUNK_SIZE else n_samples
|
||||
int n_chunks = n_samples // n_samples_chunk
|
||||
int n_samples_rem = n_samples % n_samples_chunk
|
||||
int chunk_idx
|
||||
int start = 0, end = 0
|
||||
|
||||
int j, k
|
||||
|
||||
floating[::1] X_data = X.data
|
||||
int[::1] X_indices = X.indices
|
||||
int[::1] X_indptr = X.indptr
|
||||
|
||||
floating[::1] centers_squared_norms = row_norms(centers_old, squared=True)
|
||||
|
||||
floating *centers_new_chunk
|
||||
floating *weight_in_clusters_chunk
|
||||
|
||||
omp_lock_t lock
|
||||
|
||||
# count remainder chunk in total number of chunks
|
||||
n_chunks += n_samples != n_chunks * n_samples_chunk
|
||||
|
||||
# number of threads should not be bigger than number of chunks
|
||||
n_threads = min(n_threads, n_chunks)
|
||||
|
||||
if update_centers:
|
||||
memset(¢ers_new[0, 0], 0, n_clusters * n_features * sizeof(floating))
|
||||
memset(&weight_in_clusters[0], 0, n_clusters * sizeof(floating))
|
||||
omp_init_lock(&lock)
|
||||
|
||||
with nogil, parallel(num_threads=n_threads):
|
||||
# thread local buffers
|
||||
centers_new_chunk = <floating*> calloc(n_clusters * n_features, sizeof(floating))
|
||||
weight_in_clusters_chunk = <floating*> calloc(n_clusters, sizeof(floating))
|
||||
|
||||
for chunk_idx in prange(n_chunks, schedule='static'):
|
||||
start = chunk_idx * n_samples_chunk
|
||||
if chunk_idx == n_chunks - 1 and n_samples_rem > 0:
|
||||
end = start + n_samples_rem
|
||||
else:
|
||||
end = start + n_samples_chunk
|
||||
|
||||
_update_chunk_sparse(
|
||||
X_data[X_indptr[start]: X_indptr[end]],
|
||||
X_indices[X_indptr[start]: X_indptr[end]],
|
||||
X_indptr[start: end+1],
|
||||
sample_weight[start: end],
|
||||
centers_old,
|
||||
centers_squared_norms,
|
||||
labels[start: end],
|
||||
centers_new_chunk,
|
||||
weight_in_clusters_chunk,
|
||||
update_centers)
|
||||
|
||||
# reduction from local buffers.
|
||||
if update_centers:
|
||||
# The lock is necessary to avoid race conditions when aggregating
|
||||
# info from different thread-local buffers.
|
||||
omp_set_lock(&lock)
|
||||
for j in range(n_clusters):
|
||||
weight_in_clusters[j] += weight_in_clusters_chunk[j]
|
||||
for k in range(n_features):
|
||||
centers_new[j, k] += centers_new_chunk[j * n_features + k]
|
||||
omp_unset_lock(&lock)
|
||||
|
||||
free(centers_new_chunk)
|
||||
free(weight_in_clusters_chunk)
|
||||
|
||||
if update_centers:
|
||||
omp_destroy_lock(&lock)
|
||||
_relocate_empty_clusters_sparse(
|
||||
X_data, X_indices, X_indptr, sample_weight,
|
||||
centers_old, centers_new, weight_in_clusters, labels)
|
||||
|
||||
_average_centers(centers_new, weight_in_clusters)
|
||||
_center_shift(centers_old, centers_new, center_shift)
|
||||
|
||||
|
||||
cdef void _update_chunk_sparse(
|
||||
const floating[::1] X_data, # IN
|
||||
const int[::1] X_indices, # IN
|
||||
const int[::1] X_indptr, # IN
|
||||
const floating[::1] sample_weight, # IN
|
||||
const floating[:, ::1] centers_old, # IN
|
||||
const floating[::1] centers_squared_norms, # IN
|
||||
int[::1] labels, # OUT
|
||||
floating *centers_new, # OUT
|
||||
floating *weight_in_clusters, # OUT
|
||||
bint update_centers) noexcept nogil:
|
||||
"""K-means combined EM step for one sparse data chunk.
|
||||
|
||||
Compute the partial contribution of a single data chunk to the labels and
|
||||
centers.
|
||||
"""
|
||||
cdef:
|
||||
int n_samples = labels.shape[0]
|
||||
int n_clusters = centers_old.shape[0]
|
||||
int n_features = centers_old.shape[1]
|
||||
|
||||
floating sq_dist, min_sq_dist
|
||||
int i, j, k, label
|
||||
floating max_floating = FLT_MAX if floating is float else DBL_MAX
|
||||
int s = X_indptr[0]
|
||||
|
||||
# XXX Precompute the pairwise distances matrix is not worth for sparse
|
||||
# currently. Should be tested when BLAS (sparse x dense) matrix
|
||||
# multiplication is available.
|
||||
for i in range(n_samples):
|
||||
min_sq_dist = max_floating
|
||||
label = 0
|
||||
|
||||
for j in range(n_clusters):
|
||||
sq_dist = 0.0
|
||||
for k in range(X_indptr[i] - s, X_indptr[i + 1] - s):
|
||||
sq_dist += centers_old[j, X_indices[k]] * X_data[k]
|
||||
|
||||
# Instead of computing the full squared distance with each cluster,
|
||||
# ||X - C||² = ||X||² - 2 X.C^T + ||C||², we only need to compute
|
||||
# the - 2 X.C^T + ||C||² term since the argmin for a given sample
|
||||
# only depends on the centers C.
|
||||
sq_dist = centers_squared_norms[j] -2 * sq_dist
|
||||
if sq_dist < min_sq_dist:
|
||||
min_sq_dist = sq_dist
|
||||
label = j
|
||||
|
||||
labels[i] = label
|
||||
|
||||
if update_centers:
|
||||
weight_in_clusters[label] += sample_weight[i]
|
||||
for k in range(X_indptr[i] - s, X_indptr[i + 1] - s):
|
||||
centers_new[label * n_features + X_indices[k]] += X_data[k] * sample_weight[i]
|
||||
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
@@ -0,0 +1,218 @@
|
||||
from cython cimport floating
|
||||
from cython.parallel cimport parallel, prange
|
||||
from libc.stdlib cimport malloc, free
|
||||
|
||||
|
||||
def _minibatch_update_dense(
|
||||
const floating[:, ::1] X, # IN
|
||||
const floating[::1] sample_weight, # IN
|
||||
const floating[:, ::1] centers_old, # IN
|
||||
floating[:, ::1] centers_new, # OUT
|
||||
floating[::1] weight_sums, # INOUT
|
||||
const int[::1] labels, # IN
|
||||
int n_threads):
|
||||
"""Update of the centers for dense MiniBatchKMeans.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : ndarray of shape (n_samples, n_features), dtype=floating
|
||||
The observations to cluster.
|
||||
|
||||
sample_weight : ndarray of shape (n_samples,), dtype=floating
|
||||
The weights for each observation in X.
|
||||
|
||||
centers_old : ndarray of shape (n_clusters, n_features), dtype=floating
|
||||
Centers before previous iteration, placeholder for the centers after
|
||||
previous iteration.
|
||||
|
||||
centers_new : ndarray of shape (n_clusters, n_features), dtype=floating
|
||||
Centers after previous iteration, placeholder for the new centers
|
||||
computed during this iteration.
|
||||
|
||||
weight_sums : ndarray of shape (n_clusters,), dtype=floating
|
||||
Current sums of the accumulated weights for each center.
|
||||
|
||||
labels : ndarray of shape (n_samples,), dtype=int
|
||||
labels assignment.
|
||||
|
||||
n_threads : int
|
||||
The number of threads to be used by openmp.
|
||||
"""
|
||||
cdef:
|
||||
int n_samples = X.shape[0]
|
||||
int n_clusters = centers_old.shape[0]
|
||||
int cluster_idx
|
||||
|
||||
int *indices
|
||||
|
||||
with nogil, parallel(num_threads=n_threads):
|
||||
indices = <int*> malloc(n_samples * sizeof(int))
|
||||
|
||||
for cluster_idx in prange(n_clusters, schedule="static"):
|
||||
update_center_dense(cluster_idx, X, sample_weight,
|
||||
centers_old, centers_new, weight_sums, labels,
|
||||
indices)
|
||||
|
||||
free(indices)
|
||||
|
||||
|
||||
cdef void update_center_dense(
|
||||
int cluster_idx,
|
||||
const floating[:, ::1] X, # IN
|
||||
const floating[::1] sample_weight, # IN
|
||||
const floating[:, ::1] centers_old, # IN
|
||||
floating[:, ::1] centers_new, # OUT
|
||||
floating[::1] weight_sums, # INOUT
|
||||
const int[::1] labels, # IN
|
||||
int *indices) noexcept nogil: # TMP
|
||||
"""Update of a single center for dense MinibatchKMeans"""
|
||||
cdef:
|
||||
int n_samples = sample_weight.shape[0]
|
||||
int n_features = centers_old.shape[1]
|
||||
floating alpha
|
||||
int n_indices
|
||||
int k, sample_idx, feature_idx
|
||||
|
||||
floating wsum = 0
|
||||
|
||||
# indices = np.where(labels == cluster_idx)[0]
|
||||
k = 0
|
||||
for sample_idx in range(n_samples):
|
||||
if labels[sample_idx] == cluster_idx:
|
||||
indices[k] = sample_idx
|
||||
wsum += sample_weight[sample_idx]
|
||||
k += 1
|
||||
n_indices = k
|
||||
|
||||
if wsum > 0:
|
||||
# Undo the previous count-based scaling for this cluster center
|
||||
for feature_idx in range(n_features):
|
||||
centers_new[cluster_idx, feature_idx] = centers_old[cluster_idx, feature_idx] * weight_sums[cluster_idx]
|
||||
|
||||
# Update cluster with new point members
|
||||
for k in range(n_indices):
|
||||
sample_idx = indices[k]
|
||||
for feature_idx in range(n_features):
|
||||
centers_new[cluster_idx, feature_idx] += X[sample_idx, feature_idx] * sample_weight[sample_idx]
|
||||
|
||||
# Update the count statistics for this center
|
||||
weight_sums[cluster_idx] += wsum
|
||||
|
||||
# Rescale to compute mean of all points (old and new)
|
||||
alpha = 1 / weight_sums[cluster_idx]
|
||||
for feature_idx in range(n_features):
|
||||
centers_new[cluster_idx, feature_idx] *= alpha
|
||||
else:
|
||||
# No sample was assigned to this cluster in this batch of data
|
||||
for feature_idx in range(n_features):
|
||||
centers_new[cluster_idx, feature_idx] = centers_old[cluster_idx, feature_idx]
|
||||
|
||||
|
||||
def _minibatch_update_sparse(
|
||||
X, # IN
|
||||
const floating[::1] sample_weight, # IN
|
||||
const floating[:, ::1] centers_old, # IN
|
||||
floating[:, ::1] centers_new, # OUT
|
||||
floating[::1] weight_sums, # INOUT
|
||||
const int[::1] labels, # IN
|
||||
int n_threads):
|
||||
"""Update of the centers for sparse MiniBatchKMeans.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : sparse matrix of shape (n_samples, n_features), dtype=floating
|
||||
The observations to cluster. Must be in CSR format.
|
||||
|
||||
sample_weight : ndarray of shape (n_samples,), dtype=floating
|
||||
The weights for each observation in X.
|
||||
|
||||
centers_old : ndarray of shape (n_clusters, n_features), dtype=floating
|
||||
Centers before previous iteration, placeholder for the centers after
|
||||
previous iteration.
|
||||
|
||||
centers_new : ndarray of shape (n_clusters, n_features), dtype=floating
|
||||
Centers after previous iteration, placeholder for the new centers
|
||||
computed during this iteration.
|
||||
|
||||
weight_sums : ndarray of shape (n_clusters,), dtype=floating
|
||||
Current sums of the accumulated weights for each center.
|
||||
|
||||
labels : ndarray of shape (n_samples,), dtype=int
|
||||
labels assignment.
|
||||
|
||||
n_threads : int
|
||||
The number of threads to be used by openmp.
|
||||
"""
|
||||
cdef:
|
||||
floating[::1] X_data = X.data
|
||||
int[::1] X_indices = X.indices
|
||||
int[::1] X_indptr = X.indptr
|
||||
int n_samples = X.shape[0]
|
||||
int n_clusters = centers_old.shape[0]
|
||||
int cluster_idx
|
||||
|
||||
int *indices
|
||||
|
||||
with nogil, parallel(num_threads=n_threads):
|
||||
indices = <int*> malloc(n_samples * sizeof(int))
|
||||
|
||||
for cluster_idx in prange(n_clusters, schedule="static"):
|
||||
update_center_sparse(cluster_idx, X_data, X_indices, X_indptr,
|
||||
sample_weight, centers_old, centers_new,
|
||||
weight_sums, labels, indices)
|
||||
|
||||
free(indices)
|
||||
|
||||
|
||||
cdef void update_center_sparse(
|
||||
int cluster_idx,
|
||||
const floating[::1] X_data, # IN
|
||||
const int[::1] X_indices, # IN
|
||||
const int[::1] X_indptr, # IN
|
||||
const floating[::1] sample_weight, # IN
|
||||
const floating[:, ::1] centers_old, # IN
|
||||
floating[:, ::1] centers_new, # OUT
|
||||
floating[::1] weight_sums, # INOUT
|
||||
const int[::1] labels, # IN
|
||||
int *indices) noexcept nogil: # TMP
|
||||
"""Update of a single center for sparse MinibatchKMeans"""
|
||||
cdef:
|
||||
int n_samples = sample_weight.shape[0]
|
||||
int n_features = centers_old.shape[1]
|
||||
floating alpha
|
||||
int n_indices
|
||||
int k, sample_idx, feature_idx
|
||||
|
||||
floating wsum = 0
|
||||
|
||||
# indices = np.where(labels == cluster_idx)[0]
|
||||
k = 0
|
||||
for sample_idx in range(n_samples):
|
||||
if labels[sample_idx] == cluster_idx:
|
||||
indices[k] = sample_idx
|
||||
wsum += sample_weight[sample_idx]
|
||||
k += 1
|
||||
n_indices = k
|
||||
|
||||
if wsum > 0:
|
||||
# Undo the previous count-based scaling for this cluster center:
|
||||
for feature_idx in range(n_features):
|
||||
centers_new[cluster_idx, feature_idx] = centers_old[cluster_idx, feature_idx] * weight_sums[cluster_idx]
|
||||
|
||||
# Update cluster with new point members
|
||||
for k in range(n_indices):
|
||||
sample_idx = indices[k]
|
||||
for feature_idx in range(X_indptr[sample_idx], X_indptr[sample_idx + 1]):
|
||||
centers_new[cluster_idx, X_indices[feature_idx]] += X_data[feature_idx] * sample_weight[sample_idx]
|
||||
|
||||
# Update the count statistics for this center
|
||||
weight_sums[cluster_idx] += wsum
|
||||
|
||||
# Rescale to compute mean of all points (old and new)
|
||||
alpha = 1 / weight_sums[cluster_idx]
|
||||
for feature_idx in range(n_features):
|
||||
centers_new[cluster_idx, feature_idx] *= alpha
|
||||
else:
|
||||
# No sample was assigned to this cluster in this batch of data
|
||||
for feature_idx in range(n_features):
|
||||
centers_new[cluster_idx, feature_idx] = centers_old[cluster_idx, feature_idx]
|
||||
@@ -0,0 +1,26 @@
|
||||
cluster_extension_metadata = {
|
||||
'_dbscan_inner':
|
||||
{'sources': [cython_gen_cpp.process('_dbscan_inner.pyx')]},
|
||||
'_hierarchical_fast':
|
||||
{'sources': [cython_gen_cpp.process('_hierarchical_fast.pyx'), metrics_cython_tree]},
|
||||
'_k_means_common':
|
||||
{'sources': [cython_gen.process('_k_means_common.pyx')], 'dependencies': [openmp_dep]},
|
||||
'_k_means_lloyd':
|
||||
{'sources': [cython_gen.process('_k_means_lloyd.pyx')], 'dependencies': [openmp_dep]},
|
||||
'_k_means_elkan':
|
||||
{'sources': [cython_gen.process('_k_means_elkan.pyx')], 'dependencies': [openmp_dep]},
|
||||
'_k_means_minibatch':
|
||||
{'sources': [cython_gen.process('_k_means_minibatch.pyx')], 'dependencies': [openmp_dep]},
|
||||
}
|
||||
|
||||
foreach ext_name, ext_dict : cluster_extension_metadata
|
||||
py.extension_module(
|
||||
ext_name,
|
||||
[ext_dict.get('sources'), utils_cython_tree],
|
||||
dependencies: [np_dep] + ext_dict.get('dependencies', []),
|
||||
subdir: 'sklearn/cluster',
|
||||
install: true
|
||||
)
|
||||
endforeach
|
||||
|
||||
subdir('_hdbscan')
|
||||
Reference in New Issue
Block a user