blob: 8d03e8fd5b54e04f7138b71848d255964972f78d [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use crate::blas::sgemm_a_bt;
use crate::distance::{fvec_l2sqr, fvec_norm_l2sqr};
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
pub struct KMeansConfig {
pub niter: usize,
pub nredo: usize,
pub max_points_per_centroid: usize,
pub seed: u64,
/// Balance factor: penalizes large clusters to produce more uniform partitions.
/// 0.0 = standard k-means. Higher values = more balanced.
/// Typical value: 0.1 for IVF construction.
pub balance_factor: f32,
}
impl Default for KMeansConfig {
fn default() -> Self {
KMeansConfig {
niter: 25,
nredo: 1,
max_points_per_centroid: 256,
seed: 1234,
balance_factor: 0.0,
}
}
}
const EPS: f32 = 1.0 / 1024.0;
/// Threshold above which hierarchical k-means is used.
const HIERARCHICAL_THRESHOLD: usize = 256;
pub fn kmeans_train(config: &KMeansConfig, data: &[f32], n: usize, d: usize, k: usize) -> Vec<f32> {
if k > HIERARCHICAL_THRESHOLD && n > k {
kmeans_train_hierarchical(config, data, n, d, k)
} else {
kmeans_train_with_init(config, data, n, d, k, None)
}
}
/// Hierarchical k-means for large k (> 256).
/// Starts with initial_k clusters and iteratively splits the largest until target k is reached.
fn kmeans_train_hierarchical(
config: &KMeansConfig,
data: &[f32],
n: usize,
d: usize,
target_k: usize,
) -> Vec<f32> {
use std::cmp::Ordering;
use std::collections::BinaryHeap;
#[derive(Clone)]
struct Cluster {
centroid: Vec<f32>,
indices: Vec<usize>,
}
impl Eq for Cluster {}
impl PartialEq for Cluster {
fn eq(&self, other: &Self) -> bool {
self.indices.len() == other.indices.len()
}
}
impl Ord for Cluster {
fn cmp(&self, other: &Self) -> Ordering {
self.indices.len().cmp(&other.indices.len())
}
}
impl PartialOrd for Cluster {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
let mut rng = StdRng::seed_from_u64(config.seed);
// Subsample for training
let max_n = target_k * config.max_points_per_centroid;
let (train_data, train_n) = if n > max_n {
let sub = subsample(data, n, d, max_n, &mut rng);
(sub, max_n)
} else {
(data.to_vec(), n)
};
// Step 1: Train initial_k clusters
let initial_k = 16.min(target_k);
let initial_config = KMeansConfig {
niter: config.niter,
seed: config.seed,
..KMeansConfig::default()
};
let initial_centroids =
kmeans_train_with_init(&initial_config, &train_data, train_n, d, initial_k, None);
// Assign all points to initial clusters
let mut assignments = vec![0usize; train_n];
assign_clusters_fast(
&train_data,
train_n,
d,
&initial_centroids,
initial_k,
&mut assignments,
0.0,
);
// Build initial clusters
let mut heap: BinaryHeap<Cluster> = BinaryHeap::new();
for c in 0..initial_k {
let indices: Vec<usize> = (0..train_n).filter(|&i| assignments[i] == c).collect();
let centroid = initial_centroids[c * d..(c + 1) * d].to_vec();
heap.push(Cluster { centroid, indices });
}
// Step 2: Iteratively split the largest cluster
let mut finalized: Vec<Vec<f32>> = Vec::new();
let split_k = 2; // Split into 2 each time
while finalized.len() + heap.len() < target_k {
let largest = match heap.pop() {
Some(c) => c,
None => break,
};
if largest.indices.len() < split_k * 2 {
finalized.push(largest.centroid);
continue;
}
// Extract sub-data for this cluster
let sub_n = largest.indices.len();
let mut sub_data = vec![0.0f32; sub_n * d];
for (new_idx, &orig_idx) in largest.indices.iter().enumerate() {
sub_data[new_idx * d..(new_idx + 1) * d]
.copy_from_slice(&train_data[orig_idx * d..(orig_idx + 1) * d]);
}
// Run k-means to split
let sub_config = KMeansConfig {
niter: 10,
seed: config.seed + finalized.len() as u64,
..KMeansConfig::default()
};
let sub_centroids = kmeans_train_with_init(&sub_config, &sub_data, sub_n, d, split_k, None);
// Reassign points in this cluster
let mut sub_assignments = vec![0usize; sub_n];
assign_clusters_fast(
&sub_data,
sub_n,
d,
&sub_centroids,
split_k,
&mut sub_assignments,
0.0,
);
for sc in 0..split_k {
let sub_indices: Vec<usize> = (0..sub_n)
.filter(|&i| sub_assignments[i] == sc)
.map(|i| largest.indices[i])
.collect();
let centroid = sub_centroids[sc * d..(sc + 1) * d].to_vec();
if !sub_indices.is_empty() {
heap.push(Cluster {
centroid,
indices: sub_indices,
});
}
}
}
// Collect all centroids
let mut result = Vec::with_capacity(target_k * d);
for c in finalized {
result.extend_from_slice(&c);
}
while let Some(cluster) = heap.pop() {
result.extend_from_slice(&cluster.centroid);
if result.len() >= target_k * d {
break;
}
}
// Pad if needed
result.resize(target_k * d, 0.0);
result
}
pub fn kmeans_train_with_init(
config: &KMeansConfig,
data: &[f32],
n: usize,
d: usize,
k: usize,
initial_centroids: Option<&[f32]>,
) -> Vec<f32> {
if n == 0 || k == 0 {
return vec![0.0; k * d];
}
let mut rng = StdRng::seed_from_u64(config.seed);
let max_n = k * config.max_points_per_centroid;
let (train_data, train_n) = if n > max_n {
let sub = subsample(data, n, d, max_n, &mut rng);
(sub, max_n)
} else {
(data.to_vec(), n)
};
if train_n <= k {
let mut centroids = vec![0.0f32; k * d];
for i in 0..k {
let src = i % train_n;
centroids[i * d..(i + 1) * d].copy_from_slice(&train_data[src * d..(src + 1) * d]);
}
return centroids;
}
let mut best_centroids = vec![0.0f32; k * d];
let mut best_obj = f32::MAX;
let nredo = if initial_centroids.is_some() {
1
} else {
config.nredo
};
for redo in 0..nredo {
let mut centroids = if redo == 0 {
if let Some(init) = initial_centroids {
init.to_vec()
} else {
kmeans_plusplus_init(&train_data, train_n, d, k, &mut rng)
}
} else {
kmeans_plusplus_init(&train_data, train_n, d, k, &mut rng)
};
let mut assignments = vec![0usize; train_n];
let mut prev_obj = f32::MAX;
for _iter in 0..config.niter {
let obj = assign_clusters_fast(
&train_data,
train_n,
d,
&centroids,
k,
&mut assignments,
config.balance_factor,
);
update_centroids(
&train_data,
train_n,
d,
&mut centroids,
k,
&assignments,
&mut rng,
);
if prev_obj < f32::MAX {
let rel_change = (prev_obj - obj).abs() / prev_obj.max(1e-10);
if rel_change < 1e-6 {
break;
}
}
prev_obj = obj;
}
if prev_obj < best_obj {
best_obj = prev_obj;
best_centroids.copy_from_slice(&centroids);
}
}
best_centroids
}
fn kmeans_plusplus_init(data: &[f32], n: usize, d: usize, k: usize, rng: &mut StdRng) -> Vec<f32> {
let mut centroids = vec![0.0f32; k * d];
let first = rng.gen_range(0..n);
centroids[..d].copy_from_slice(&data[first * d..(first + 1) * d]);
let mut min_dists = vec![f32::MAX; n];
for c in 1..k {
let prev = &centroids[(c - 1) * d..c * d];
let mut total = 0.0f32;
for i in 0..n {
let dist = fvec_l2sqr(&data[i * d..(i + 1) * d], prev);
if dist < min_dists[i] {
min_dists[i] = dist;
}
total += min_dists[i];
}
let target = rng.gen::<f32>() * total;
let mut cumulative = 0.0f32;
let mut selected = n - 1;
for i in 0..n {
cumulative += min_dists[i];
if cumulative >= target {
selected = i;
break;
}
}
centroids[c * d..(c + 1) * d].copy_from_slice(&data[selected * d..(selected + 1) * d]);
}
centroids
}
/// Fast assignment using sgemm: ||x-c||² = ||x||² + ||c||² - 2·x·cᵀ.
/// Supports balance_factor to penalize large clusters.
fn assign_clusters_fast(
data: &[f32],
n: usize,
d: usize,
centroids: &[f32],
k: usize,
assignments: &mut [usize],
balance_factor: f32,
) -> f32 {
// Cap ip_matrix size to ~16MB. Chunk if n*k would be too large.
const MAX_MATRIX_ELEMS: usize = 4 * 1024 * 1024; // 16MB / 4 bytes
if n * k > MAX_MATRIX_ELEMS {
let chunk_n = MAX_MATRIX_ELEMS / k;
let mut total_obj = 0.0f32;
let mut offset = 0;
while offset < n {
let cn = (n - offset).min(chunk_n);
total_obj += assign_clusters_fast(
&data[offset * d..(offset + cn) * d],
cn,
d,
centroids,
k,
&mut assignments[offset..offset + cn],
balance_factor,
);
offset += cn;
}
return total_obj;
}
let x_norms: Vec<f32> = (0..n)
.map(|i| fvec_norm_l2sqr(&data[i * d..(i + 1) * d]))
.collect();
let c_norms: Vec<f32> = (0..k)
.map(|c| fvec_norm_l2sqr(&centroids[c * d..(c + 1) * d]))
.collect();
let mut ip_matrix = vec![0.0f32; n * k];
sgemm_a_bt(n, k, d, 1.0, data, centroids, 0.0, &mut ip_matrix);
// Compute cluster sizes for balance penalty
let mut cluster_sizes = vec![0u32; k];
if balance_factor > 0.0 {
for &a in assignments.iter() {
if a < k {
cluster_sizes[a] += 1;
}
}
}
let mut total_obj = 0.0f32;
for i in 0..n {
let mut best = 0;
let mut best_dist = f32::MAX;
let row = i * k;
for c in 0..k {
let mut dist = x_norms[i] + c_norms[c] - 2.0 * ip_matrix[row + c];
// Balance penalty: prefer smaller clusters
if balance_factor > 0.0 && cluster_sizes[c] > 0 {
dist += balance_factor * (cluster_sizes[c] as f32).ln();
}
if dist < best_dist {
best_dist = dist;
best = c;
}
}
assignments[i] = best;
total_obj += best_dist;
}
total_obj
}
fn update_centroids(
data: &[f32],
n: usize,
d: usize,
centroids: &mut [f32],
k: usize,
assignments: &[usize],
rng: &mut StdRng,
) {
let mut counts = vec![0usize; k];
let mut sums = vec![0.0f32; k * d];
for i in 0..n {
let c = assignments[i];
counts[c] += 1;
for j in 0..d {
sums[c * d + j] += data[i * d + j];
}
}
for c in 0..k {
if counts[c] > 0 {
let inv = 1.0 / counts[c] as f32;
for j in 0..d {
centroids[c * d + j] = sums[c * d + j] * inv;
}
}
}
for c in 0..k {
if counts[c] > 0 {
continue;
}
let donor = counts
.iter()
.enumerate()
.max_by_key(|(_, &cnt)| cnt)
.map(|(idx, _)| idx)
.unwrap_or(0);
if counts[donor] <= 1 {
let idx = rng.gen_range(0..n);
centroids[c * d..(c + 1) * d].copy_from_slice(&data[idx * d..(idx + 1) * d]);
continue;
}
let donor_copy: Vec<f32> = centroids[donor * d..(donor + 1) * d].to_vec();
centroids[c * d..(c + 1) * d].copy_from_slice(&donor_copy);
for j in 0..d {
if j.is_multiple_of(2) {
centroids[c * d + j] *= 1.0 + EPS;
centroids[donor * d + j] *= 1.0 - EPS;
} else {
centroids[c * d + j] *= 1.0 - EPS;
centroids[donor * d + j] *= 1.0 + EPS;
}
}
counts[c] = counts[donor] / 2;
counts[donor] -= counts[c];
}
}
pub fn find_nearest(point: &[f32], centroids: &[f32], k: usize, d: usize) -> usize {
let mut best = 0;
let mut best_dist = f32::MAX;
for c in 0..k {
let dist = fvec_l2sqr(point, &centroids[c * d..(c + 1) * d]);
if dist < best_dist {
best_dist = dist;
best = c;
}
}
best
}
pub(crate) fn find_nearest_batch(
data: &[f32],
n: usize,
centroids: &[f32],
k: usize,
d: usize,
) -> Vec<usize> {
if n == 0 {
return Vec::new();
}
if n == 1 {
return vec![find_nearest(&data[..d], centroids, k, d)];
}
let mut assignments = vec![0usize; n];
assign_clusters_fast(data, n, d, centroids, k, &mut assignments, 0.0);
assignments
}
pub fn find_topk(
point: &[f32],
centroids: &[f32],
k: usize,
d: usize,
nprobe: usize,
) -> (Vec<usize>, Vec<f32>) {
let nprobe = nprobe.min(k);
if nprobe == 0 {
return (Vec::new(), Vec::new());
}
let mut dists: Vec<(f32, usize)> = (0..k)
.map(|c| (fvec_l2sqr(point, &centroids[c * d..(c + 1) * d]), c))
.collect();
select_topk_prefix(&mut dists, nprobe);
let indices: Vec<usize> = dists[..nprobe].iter().map(|&(_, i)| i).collect();
let distances: Vec<f32> = dists[..nprobe].iter().map(|&(d, _)| d).collect();
(indices, distances)
}
/// Batch find top-nprobe nearest centroids for multiple queries using sgemm.
/// Returns (all_indices, all_distances) each of length nq * nprobe.
pub fn find_topk_batch(
queries: &[f32],
nq: usize,
centroids: &[f32],
k: usize,
d: usize,
nprobe: usize,
) -> (Vec<Vec<usize>>, Vec<Vec<f32>>) {
let nprobe = nprobe.min(k);
if nprobe == 0 {
return (vec![Vec::new(); nq], vec![Vec::new(); nq]);
}
if nq == 1 {
let (indices, distances) = find_topk(&queries[..d], centroids, k, d, nprobe);
return (vec![indices], vec![distances]);
}
// Precompute norms
let q_norms: Vec<f32> = (0..nq)
.map(|i| fvec_norm_l2sqr(&queries[i * d..(i + 1) * d]))
.collect();
let c_norms: Vec<f32> = (0..k)
.map(|c| fvec_norm_l2sqr(&centroids[c * d..(c + 1) * d]))
.collect();
// Batch inner products: ip[nq × k] = queries[nq × d] · centroids[k × d]^T
let mut ip_matrix = vec![0.0f32; nq * k];
sgemm_a_bt(nq, k, d, 1.0, queries, centroids, 0.0, &mut ip_matrix);
// Extract top-nprobe per query
let mut all_indices = Vec::with_capacity(nq);
let mut all_distances = Vec::with_capacity(nq);
for qi in 0..nq {
let row = qi * k;
let mut dists: Vec<(f32, usize)> = (0..k)
.map(|c| {
let dist = q_norms[qi] + c_norms[c] - 2.0 * ip_matrix[row + c];
(dist.max(0.0), c)
})
.collect();
select_topk_prefix(&mut dists, nprobe);
all_indices.push(dists[..nprobe].iter().map(|&(_, i)| i).collect());
all_distances.push(dists[..nprobe].iter().map(|&(d, _)| d).collect());
}
(all_indices, all_distances)
}
fn select_topk_prefix(dists: &mut [(f32, usize)], nprobe: usize) {
debug_assert!(nprobe > 0 && nprobe <= dists.len());
if nprobe < dists.len() {
dists.select_nth_unstable_by(nprobe - 1, compare_distance_then_index);
}
dists[..nprobe].sort_by(compare_distance_then_index);
}
fn compare_distance_then_index(left: &(f32, usize), right: &(f32, usize)) -> std::cmp::Ordering {
left.0
.total_cmp(&right.0)
.then_with(|| left.1.cmp(&right.1))
}
// --- Streaming Coreset K-means ---
/// Streaming k-means trainer for very large datasets.
/// Processes data in chunks, compresses each chunk into a weighted coreset,
/// then trains final centroids on the accumulated coreset.
pub struct StreamingKMeans {
pub d: usize,
pub k: usize,
pub chunk_size: usize,
config: KMeansConfig,
/// Accumulated coreset: (centroids, weights)
coreset_centroids: Vec<f32>,
coreset_weights: Vec<f32>,
}
impl StreamingKMeans {
/// Create a streaming k-means trainer.
/// chunk_size: number of vectors per chunk (e.g., k * 256)
pub fn new(d: usize, k: usize, chunk_size: usize, config: KMeansConfig) -> Self {
StreamingKMeans {
d,
k,
chunk_size,
config,
coreset_centroids: Vec::new(),
coreset_weights: Vec::new(),
}
}
/// Feed a chunk of training data. Can be called multiple times.
/// Each chunk is compressed into k weighted centroids (coreset).
pub fn add_chunk(&mut self, data: &[f32], n: usize) {
let d = self.d;
let chunk_k = self.k.min(n);
if chunk_k == 0 || n == 0 {
return;
}
// Train k-means on this chunk
let chunk_config = KMeansConfig {
niter: 15,
seed: self.config.seed + self.coreset_weights.len() as u64,
..KMeansConfig::default()
};
let centroids = kmeans_train_with_init(&chunk_config, data, n, d, chunk_k, None);
// Assign points to centroids to compute weights
let mut assignments = vec![0usize; n];
assign_clusters_fast(data, n, d, &centroids, chunk_k, &mut assignments, 0.0);
let mut weights = vec![0.0f32; chunk_k];
for &a in &assignments {
weights[a] += 1.0;
}
// Append to coreset
self.coreset_centroids.extend_from_slice(&centroids);
self.coreset_weights.extend_from_slice(&weights);
}
/// Finalize: train final centroids on the accumulated weighted coreset.
pub fn finalize(&self) -> Vec<f32> {
let d = self.d;
let coreset_n = self.coreset_weights.len();
if coreset_n == 0 {
return vec![0.0f32; self.k * d];
}
if coreset_n <= self.k {
let mut result = self.coreset_centroids.clone();
result.resize(self.k * d, 0.0);
return result;
}
// Weighted k-means on coreset
weighted_kmeans_train(
&self.config,
&self.coreset_centroids,
&self.coreset_weights,
coreset_n,
d,
self.k,
)
}
/// Total vectors processed so far.
pub fn total_weight(&self) -> f32 {
self.coreset_weights.iter().sum()
}
}
/// Weighted k-means: each point has a weight that affects centroid computation.
fn weighted_kmeans_train(
config: &KMeansConfig,
data: &[f32],
weights: &[f32],
n: usize,
d: usize,
k: usize,
) -> Vec<f32> {
let mut rng = StdRng::seed_from_u64(config.seed);
if n <= k {
let mut centroids = vec![0.0f32; k * d];
for i in 0..k {
let src = i % n;
centroids[i * d..(i + 1) * d].copy_from_slice(&data[src * d..(src + 1) * d]);
}
return centroids;
}
let mut centroids = kmeans_plusplus_init(data, n, d, k, &mut rng);
let mut assignments = vec![0usize; n];
for _iter in 0..config.niter {
// Assign (unweighted distance)
assign_clusters_fast(data, n, d, &centroids, k, &mut assignments, 0.0);
// Update with weights
let mut sums = vec![0.0f32; k * d];
let mut total_weights = vec![0.0f32; k];
for i in 0..n {
let c = assignments[i];
let w = weights[i];
total_weights[c] += w;
for j in 0..d {
sums[c * d + j] += w * data[i * d + j];
}
}
for c in 0..k {
if total_weights[c] > 0.0 {
let inv = 1.0 / total_weights[c];
for j in 0..d {
centroids[c * d + j] = sums[c * d + j] * inv;
}
} else {
// Reinit empty cluster
let idx = rng.gen_range(0..n);
centroids[c * d..(c + 1) * d].copy_from_slice(&data[idx * d..(idx + 1) * d]);
}
}
}
centroids
}
fn subsample(data: &[f32], n: usize, d: usize, target_n: usize, rng: &mut StdRng) -> Vec<f32> {
let mut indices: Vec<usize> = (0..n).collect();
for i in 0..target_n {
let j = rng.gen_range(i..n);
indices.swap(i, j);
}
let mut result = vec![0.0f32; target_n * d];
for (out_i, &src_i) in indices[..target_n].iter().enumerate() {
result[out_i * d..(out_i + 1) * d].copy_from_slice(&data[src_i * d..(src_i + 1) * d]);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_two_clusters() {
let mut data = Vec::new();
for _ in 0..50 {
data.push(0.1);
data.push(0.1);
}
for _ in 0..50 {
data.push(10.1);
data.push(10.1);
}
let config = KMeansConfig::default();
let centroids = kmeans_train(&config, &data, 100, 2, 2);
let c0 = if centroids[0] < 5.0 {
&centroids[0..2]
} else {
&centroids[2..4]
};
let c1 = if centroids[0] < 5.0 {
&centroids[2..4]
} else {
&centroids[0..2]
};
assert!(c0[0] < 2.0 && c0[1] < 2.0);
assert!(c1[0] > 8.0 && c1[1] > 8.0);
}
#[test]
fn test_find_topk() {
let centroids = [0.0, 0.0, 10.0, 0.0, 5.0, 5.0];
let query = [1.0, 1.0];
let (indices, _) = find_topk(&query, &centroids, 3, 2, 2);
assert_eq!(indices[0], 0);
}
#[test]
fn test_find_topk_batch_matches_full_sort_with_ties() {
let d = 2;
let k = 32;
let nprobe = 5;
let centroids: Vec<f32> = (0..k)
.flat_map(|i| [i as f32 % 4.0, (i / 4) as f32])
.collect();
let queries = vec![0.5, 0.5, 2.5, 3.5, 1.5, 1.5];
let (actual_indices, actual_distances) =
find_topk_batch(&queries, 3, &centroids, k, d, nprobe);
for qi in 0..3 {
let query = &queries[qi * d..(qi + 1) * d];
let mut expected: Vec<(f32, usize)> = (0..k)
.map(|ci| (fvec_l2sqr(query, &centroids[ci * d..(ci + 1) * d]), ci))
.collect();
expected.sort_by(compare_distance_then_index);
assert_eq!(
actual_indices[qi],
expected[..nprobe]
.iter()
.map(|&(_, index)| index)
.collect::<Vec<_>>()
);
assert_eq!(
actual_distances[qi],
expected[..nprobe]
.iter()
.map(|&(distance, _)| distance)
.collect::<Vec<_>>()
);
}
}
#[test]
fn test_find_nearest_batch_matches_scalar() {
let d = 5;
let k = 4;
let n = 17;
let centroids: Vec<f32> = (0..k * d).map(|i| i as f32 * 0.25 - 2.0).collect();
let data: Vec<f32> = (0..n * d)
.map(|i| ((i * 13 % 29) as f32) * 0.1 - 1.0)
.collect();
let batch = find_nearest_batch(&data, n, &centroids, k, d);
let scalar: Vec<usize> = (0..n)
.map(|i| find_nearest(&data[i * d..(i + 1) * d], &centroids, k, d))
.collect();
assert_eq!(batch, scalar);
}
#[test]
fn test_hot_start_converges_faster() {
let mut rng = StdRng::seed_from_u64(42);
let n = 500;
let d = 4;
let k = 4;
let data: Vec<f32> = (0..n * d).map(|_| rng.gen::<f32>() * 10.0).collect();
let config = KMeansConfig {
niter: 25,
..KMeansConfig::default()
};
let centroids = kmeans_train(&config, &data, n, d, k);
// Hot-start with previous centroids should converge in fewer iterations
let config2 = KMeansConfig {
niter: 3,
..KMeansConfig::default()
};
let centroids2 = kmeans_train_with_init(&config2, &data, n, d, k, Some(&centroids));
// Should be very close to the original since it started from converged state
let mut total_diff = 0.0f32;
for i in 0..k * d {
total_diff += (centroids[i] - centroids2[i]).abs();
}
assert!(
total_diff < 1.0,
"Hot-start centroids drifted too much: {}",
total_diff
);
}
#[test]
fn test_streaming_coreset_kmeans() {
let n = 5000;
let d = 4;
let k = 10;
let chunk_size = 1000;
let mut rng = StdRng::seed_from_u64(42);
// Generate clustered data
let mut data = Vec::new();
for cluster in 0..k {
let cx = cluster as f32 * 20.0;
let cy = cluster as f32 * 20.0;
for _ in 0..n / k {
data.push(cx + rng.gen::<f32>() * 2.0);
data.push(cy + rng.gen::<f32>() * 2.0);
data.push(rng.gen::<f32>());
data.push(rng.gen::<f32>());
}
}
let config = KMeansConfig::default();
let mut streaming = StreamingKMeans::new(d, k, chunk_size, config);
// Feed data in chunks
for chunk_start in (0..n).step_by(chunk_size) {
let chunk_end = (chunk_start + chunk_size).min(n);
let chunk_n = chunk_end - chunk_start;
streaming.add_chunk(&data[chunk_start * d..chunk_end * d], chunk_n);
}
assert!((streaming.total_weight() - n as f32).abs() < 1.0);
let centroids = streaming.finalize();
assert_eq!(centroids.len(), k * d);
// Centroids should be diverse
let first = &centroids[0..d];
let mut diverse = false;
for i in 1..k {
if fvec_l2sqr(&centroids[i * d..(i + 1) * d], first) > 1.0 {
diverse = true;
break;
}
}
assert!(diverse, "Streaming centroids are not diverse");
}
#[test]
fn test_hierarchical_kmeans() {
let n = 2000;
let d = 4;
let k = 300; // > 256, triggers hierarchical
let mut rng = StdRng::seed_from_u64(42);
let data: Vec<f32> = (0..n * d).map(|_| rng.gen::<f32>() * 100.0).collect();
let config = KMeansConfig::default();
let centroids = kmeans_train(&config, &data, n, d, k);
assert_eq!(centroids.len(), k * d);
// All centroids should be finite
for &v in &centroids {
assert!(v.is_finite(), "Non-finite centroid value: {}", v);
}
// Centroids should be diverse (not all the same)
let first = &centroids[0..d];
let mut all_same = true;
for i in 1..k {
if &centroids[i * d..(i + 1) * d] != first {
all_same = false;
break;
}
}
assert!(!all_same, "All centroids are identical");
}
}