use crate::error::Error;
use crate::machine_learning::validation::{
preliminary_check, validate_max_iterations, validate_predict_input, validate_tolerance,
};
use crate::math::matmul::gemm_internal;
use crate::math::reduction::{DET_REDUCE_BLOCK, det_reduce, det_reduce_range};
use crate::math::squared_euclidean_distance_row;
use crate::parallel_gates::{SCAN_F64_PARALLEL_MIN_ELEMS, SUM_F64_PARALLEL_MIN_ELEMS};
use crate::{Deserialize, Serialize};
use ndarray::{Array1, Array2, ArrayBase, ArrayView1, Axis, Data, Ix2};
use ndarray_rand::rand::Rng;
use rayon::prelude::{
IndexedParallelIterator, IntoParallelIterator, IntoParallelRefMutIterator, ParallelIterator,
ParallelSlice, ParallelSliceMut,
};
use std::ops::AddAssign;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct KMeans {
n_clusters: usize,
max_iter: usize,
tol: f64,
random_state: Option<u64>,
centroids: Option<Array2<f64>>,
labels: Option<Array1<usize>>,
inertia: Option<f64>,
n_iter: Option<usize>,
}
impl Default for KMeans {
fn default() -> Self {
KMeans::new(8, 300, 1e-4).expect("Default KMeans parameters should be valid")
}
}
impl KMeans {
pub fn new(n_clusters: usize, max_iterations: usize, tolerance: f64) -> Result<Self, Error> {
if n_clusters == 0 {
return Err(Error::invalid_parameter(
"n_clusters",
"must be greater than 0",
));
}
validate_max_iterations(max_iterations)?;
validate_tolerance(tolerance)?;
Ok(KMeans {
n_clusters,
max_iter: max_iterations,
tol: tolerance,
random_state: None,
centroids: None,
labels: None,
inertia: None,
n_iter: None,
})
}
pub fn with_random_state(mut self, seed: u64) -> Self {
self.random_state = Some(seed);
self
}
get_field!(get_n_clusters, n_clusters, usize);
get_field!(get_max_iterations, max_iter, usize);
get_field!(get_tolerance, tol, f64);
get_field!(get_random_state, random_state, Option<u64>);
get_field!(get_actual_iterations, n_iter, Option<usize>);
get_field_as_ref!(get_labels, labels, Option<&Array1<usize>>);
get_field!(get_inertia, inertia, Option<f64>);
get_field_as_ref!(get_centroids, centroids, Option<&Array2<f64>>);
fn argmin_centroid(proj_row: ArrayView1<f64>, centroid_sq_norms: &Array1<f64>) -> usize {
let mut min_cluster = 0;
let mut min_val = f64::MAX;
for (cluster_idx, (&c_sq, &proj)) in
centroid_sq_norms.iter().zip(proj_row.iter()).enumerate()
{
let val = c_sq - 2.0 * proj;
if val < min_val {
min_val = val;
min_cluster = cluster_idx;
}
}
min_cluster
}
fn init_centroids<S>(&mut self, data: &ArrayBase<S, Ix2>) -> Result<(), Error>
where
S: Data<Elem = f64>,
{
let n_samples = data.shape()[0];
let n_features = data.shape()[1];
let mut centroids = Array2::<f64>::zeros((self.n_clusters, n_features));
let mut rng = crate::random::make_rng(self.random_state);
let first_center_idx = rng.random_range(0..n_samples);
centroids.row_mut(0).assign(&data.row(first_center_idx));
let init_parallel = n_samples.saturating_mul(n_features) >= SCAN_F64_PARALLEL_MIN_ELEMS;
let mut min_dists: Vec<f64> = if init_parallel {
data.outer_iter()
.into_par_iter()
.map(|sample| squared_euclidean_distance_row(&sample, ¢roids.row(0)))
.collect()
} else {
data.outer_iter()
.map(|sample| squared_euclidean_distance_row(&sample, ¢roids.row(0)))
.collect()
};
for k in 1..self.n_clusters {
if k > 1 {
let latest = centroids.row(k - 1);
let fold_min = |(sample, min_dist): (ArrayView1<f64>, &mut f64)| {
let dist = squared_euclidean_distance_row(&sample, &latest);
if dist < *min_dist {
*min_dist = dist;
}
};
if init_parallel {
data.outer_iter()
.into_par_iter()
.zip(min_dists.par_iter_mut())
.for_each(fold_min);
} else {
data.outer_iter()
.zip(min_dists.iter_mut())
.for_each(fold_min);
}
}
let distances = &min_dists;
let total_dist: f64 = det_reduce(
distances,
distances.len() >= SUM_F64_PARALLEL_MIN_ELEMS,
|block| block.iter().sum::<f64>(),
|a, b| a + b,
0.0,
);
if total_dist == 0.0 {
let random_idx = rng.random_range(0..n_samples);
centroids.row_mut(k).assign(&data.row(random_idx));
continue;
}
let mut cumulative_dist = 0.0;
let choice = rng.random::<f64>() * total_dist;
for (i, &dist) in distances.iter().enumerate() {
cumulative_dist += dist;
if dist > 0.0 && cumulative_dist >= choice {
centroids.row_mut(k).assign(&data.row(i));
break;
}
}
}
self.centroids = Some(centroids);
Ok(())
}
pub fn fit<S>(&mut self, data: &ArrayBase<S, Ix2>) -> Result<&mut Self, Error>
where
S: Data<Elem = f64>,
{
preliminary_check(data, None)?;
let n_samples = data.shape()[0];
let n_features = data.shape()[1];
if n_samples < self.n_clusters {
return Err(Error::invalid_input(
"Number of samples is less than number of clusters",
));
}
self.init_centroids(data)?;
let mut labels = Array1::<usize>::zeros(n_samples);
let mut prev_inertia: Option<f64> = None;
let mut iter_count = 0;
let mut new_centroids = Array2::<f64>::zeros((self.n_clusters, n_features));
let mut counts = vec![0usize; self.n_clusters];
#[cfg(feature = "show_progress")]
let progress_bar = {
let pb = crate::create_progress_bar(
self.max_iter as u64,
"[{elapsed_precise}] {bar:40} {pos}/{len} | Inertia: {msg}",
);
pb.set_message(format!("{:.6}", f64::INFINITY));
pb
};
for i in 0..self.max_iter {
let centroids = self.centroids.as_ref().unwrap();
let centroid_sq_norms = centroids.map_axis(Axis(1), |row| row.dot(&row));
let data_view = data.view();
let projections = gemm_internal(&data_view, ¢roids.t());
let compute_assignments = |sample_idx: usize| -> Result<(usize, f64), Error> {
let min_cluster =
Self::argmin_centroid(projections.row(sample_idx), ¢roid_sq_norms);
let dist = squared_euclidean_distance_row(
&data_view.row(sample_idx),
¢roids.row(min_cluster),
);
Ok((min_cluster, dist))
};
let scan_work = n_samples.saturating_mul(self.n_clusters + n_features);
let results: Result<Vec<(usize, f64)>, Error> =
if scan_work >= SCAN_F64_PARALLEL_MIN_ELEMS {
(0..n_samples)
.into_par_iter()
.map(compute_assignments)
.collect()
} else {
(0..n_samples).map(compute_assignments).collect()
};
let results = results?;
let n_clusters = self.n_clusters;
let accumulate_parallel =
n_samples.saturating_mul(n_features) >= SUM_F64_PARALLEL_MIN_ELEMS;
let (sums, new_counts, inertia) = det_reduce_range(
n_samples,
accumulate_parallel,
|range| {
let mut sums = Array2::<f64>::zeros((n_clusters, n_features));
let mut counts = vec![0usize; n_clusters];
let mut inertia = 0.0;
for i in range {
let (cluster, dist) = results[i];
inertia += dist;
sums.row_mut(cluster).add_assign(&data_view.row(i));
counts[cluster] += 1;
}
(sums, counts, inertia)
},
|(mut sums_a, mut counts_a, inertia_a), (sums_b, counts_b, inertia_b)| {
sums_a += &sums_b;
for (a, b) in counts_a.iter_mut().zip(counts_b) {
*a += b;
}
(sums_a, counts_a, inertia_a + inertia_b)
},
(
Array2::<f64>::zeros((n_clusters, n_features)),
vec![0usize; n_clusters],
0.0,
),
);
new_centroids.assign(&sums);
counts.copy_from_slice(&new_counts);
let label_slice = labels
.as_slice_mut()
.expect("labels are freshly allocated and contiguous");
if accumulate_parallel {
label_slice
.par_chunks_mut(DET_REDUCE_BLOCK)
.zip(results.par_chunks(DET_REDUCE_BLOCK))
.for_each(|(label_block, result_block)| {
for (label, &(cluster, _)) in label_block.iter_mut().zip(result_block) {
*label = cluster;
}
});
} else {
for (label, &(cluster, _)) in label_slice.iter_mut().zip(&results) {
*label = cluster;
}
}
#[cfg(feature = "show_progress")]
{
progress_bar.set_message(format!("{:.6}", inertia));
progress_bar.inc(1);
}
if let Some(prev) = prev_inertia
&& (prev - inertia).abs() < self.tol * prev.max(self.tol)
{
iter_count = i + 1;
self.inertia = Some(inertia);
break;
}
prev_inertia = Some(inertia);
iter_count = i + 1;
new_centroids
.outer_iter_mut()
.into_par_iter()
.enumerate()
.for_each(|(idx, mut centroid_row)| {
if counts[idx] > 0 {
let count_f = counts[idx] as f64;
centroid_row.par_mapv_inplace(|x| x / count_f);
}
});
for (cluster_idx, &count) in counts.iter().enumerate() {
if count == 0 {
let result: Result<Option<usize>, Error> = results
.iter()
.enumerate()
.try_fold(
None,
|acc, (sample_idx, &(assigned_cluster, dist))| match acc {
None => Ok(Some((sample_idx, assigned_cluster, dist))),
Some((best_idx, best_cluster, best_dist)) => {
if dist > best_dist {
Ok(Some((sample_idx, assigned_cluster, dist)))
} else {
Ok(Some((best_idx, best_cluster, best_dist)))
}
}
},
)
.map(|opt| opt.map(|(idx, _, _)| idx));
if let Some(farthest_idx) = result? {
new_centroids
.row_mut(cluster_idx)
.assign(&data.row(farthest_idx));
} else {
new_centroids
.row_mut(cluster_idx)
.assign(&self.centroids.as_ref().unwrap().row(cluster_idx));
}
}
}
let previous = self.centroids.replace(new_centroids);
new_centroids = previous.expect("centroids are initialized before the loop starts");
}
#[cfg(feature = "show_progress")]
{
let final_inertia = self.inertia.unwrap_or_else(|| prev_inertia.unwrap_or(0.0));
let convergence_status = if iter_count < self.max_iter {
"Converged"
} else {
"Max iterations"
};
progress_bar.finish_with_message(format!(
"{:.6} | {} | Iterations: {}",
final_inertia, convergence_status, iter_count
));
}
self.labels = Some(labels);
if self.inertia.is_none() {
self.inertia = prev_inertia;
}
self.n_iter = Some(iter_count);
Ok(self)
}
pub fn predict<S>(&self, data: &ArrayBase<S, Ix2>) -> Result<Array1<usize>, Error>
where
S: Data<Elem = f64>,
{
let centroids = self
.centroids
.as_ref()
.ok_or_else(|| Error::not_fitted("KMeans"))?;
validate_predict_input(data, centroids.ncols())?;
let centroid_sq_norms = centroids.map_axis(Axis(1), |row| row.dot(&row));
let projections = gemm_internal(data, ¢roids.t());
let scan_work = data.nrows().saturating_mul(centroids.nrows());
let labels: Vec<usize> = if scan_work >= SCAN_F64_PARALLEL_MIN_ELEMS {
(0..data.nrows())
.into_par_iter()
.map(|i| Self::argmin_centroid(projections.row(i), ¢roid_sq_norms))
.collect()
} else {
(0..data.nrows())
.map(|i| Self::argmin_centroid(projections.row(i), ¢roid_sq_norms))
.collect()
};
Ok(Array1::from(labels))
}
pub fn fit_predict<S>(&mut self, data: &ArrayBase<S, Ix2>) -> Result<Array1<usize>, Error>
where
S: Data<Elem = f64>,
{
self.fit(data)?;
Ok(self.labels.clone().unwrap())
}
model_save_and_load_methods!(KMeans);
}