1use core::f32;
13use std::cmp::Ordering;
14use std::collections::BinaryHeap;
15use std::ops::{AddAssign, DivAssign};
16use std::sync::Arc;
17use std::vec;
18use std::{collections::HashMap, ops::MulAssign};
19
20use arrow_array::{
21 Array, ArrayRef, FixedSizeListArray, Float32Array, PrimitiveArray, UInt32Array,
22 cast::AsArray,
23 types::{ArrowPrimitiveType, Float16Type, Float32Type, Float64Type, UInt8Type},
24};
25use arrow_array::{ArrowNumericType, UInt8Array};
26use arrow_ord::sort::sort_to_indices;
27use arrow_schema::{ArrowError, DataType};
28use bitvec::prelude::*;
29use lance_arrow::FixedSizeListArrayExt;
30use lance_core::utils::tokio::get_num_compute_intensive_cpus;
31use lance_linalg::distance::hamming::{hamming, hamming_distance_batch};
32use lance_linalg::distance::{DistanceType, Normalize, dot_distance_batch};
33use lance_linalg::kernels::{argmin_value_float, argmin_value_float_with_bias};
34use log::{info, warn};
35use num_traits::One;
36use num_traits::{AsPrimitive, Float, FromPrimitive, Num, Zero};
37use rand::prelude::*;
38use rayon::prelude::*;
39use {
40 lance_linalg::distance::{
41 Dot,
42 l2::{L2, l2_distance_batch},
43 },
44 lance_linalg::kernels::argmin_value,
45};
46
47use crate::vector::utils::SimpleIndex;
48use crate::{Error, Result};
49
50#[derive(Debug, PartialEq)]
52pub enum KMeanInit {
53 Random,
54 Incremental(Arc<FixedSizeListArray>),
55}
56
57pub struct KMeansParams {
59 pub max_iters: u32,
61
62 pub tolerance: f64,
65
66 pub redos: usize,
68
69 pub init: KMeanInit,
71
72 pub distance_type: DistanceType,
74
75 pub balance_factor: f32,
81
82 pub hierarchical_k: usize,
88
89 pub on_progress: Option<Arc<dyn Fn(u32, u32) + Send + Sync>>,
91}
92
93impl std::fmt::Debug for KMeansParams {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 f.debug_struct("KMeansParams")
96 .field("max_iters", &self.max_iters)
97 .field("tolerance", &self.tolerance)
98 .field("redos", &self.redos)
99 .field("init", &self.init)
100 .field("distance_type", &self.distance_type)
101 .field("balance_factor", &self.balance_factor)
102 .field("hierarchical_k", &self.hierarchical_k)
103 .field("on_progress", &self.on_progress.as_ref().map(|_| "..."))
104 .finish()
105 }
106}
107
108impl Default for KMeansParams {
109 fn default() -> Self {
110 Self {
111 max_iters: 50,
112 tolerance: 1e-4,
113 redos: 1,
114 init: KMeanInit::Random,
115 distance_type: DistanceType::L2,
116 balance_factor: 0.0,
117 hierarchical_k: 16,
118 on_progress: None,
119 }
120 }
121}
122
123impl KMeansParams {
124 pub fn new(
125 centroids: Option<Arc<FixedSizeListArray>>,
126 max_iters: u32,
127 redos: usize,
128 distance_type: DistanceType,
129 ) -> Self {
130 let init = match centroids {
131 Some(centroids) => KMeanInit::Incremental(centroids),
132 None => KMeanInit::Random,
133 };
134 Self {
135 max_iters,
136 redos,
137 distance_type,
138 init,
139 ..Default::default()
140 }
141 }
142
143 pub fn with_balance_factor(mut self, balance_factor: f32) -> Self {
149 self.balance_factor = balance_factor;
150 self
151 }
152
153 pub fn with_on_progress(mut self, cb: Arc<dyn Fn(u32, u32) + Send + Sync>) -> Self {
154 self.on_progress = Some(cb);
155 self
156 }
157
158 pub fn with_hierarchical_k(mut self, hierarchical_k: usize) -> Self {
163 self.hierarchical_k = hierarchical_k;
164 self
165 }
166}
167
168fn kmeans_random_init<T: ArrowPrimitiveType>(
172 data: &[T::Native],
173 dimension: usize,
174 k: usize,
175 mut rng: impl Rng,
176 distance_type: DistanceType,
177) -> KMeans {
178 assert!(data.len() >= k * dimension);
179 let chosen = (0..data.len() / dimension).choose_multiple(&mut rng, k);
180 let centroids = PrimitiveArray::<T>::from_iter_values(
181 chosen
182 .iter()
183 .flat_map(|&i| data[i * dimension..(i + 1) * dimension].iter())
184 .copied(),
185 );
186 KMeans {
187 centroids: Arc::new(centroids),
188 dimension,
189 distance_type,
190 loss: f64::MAX,
191 }
192}
193
194fn split_clusters<T: Float + MulAssign>(
197 n: usize,
198 cnts: &mut [usize],
199 centroids: &mut [T],
200 dim: usize,
201) {
202 let eps = T::from(1.0 / 1024.0).unwrap();
203 let mut rng = SmallRng::from_os_rng();
204 for i in 0..cnts.len() {
205 if cnts[i] == 0 {
206 let mut j = 0;
207 loop {
208 let p = (cnts[j] as f32 - 1.0) / (n - cnts.len()) as f32;
209 if rng.random::<f32>() < p {
210 break;
211 }
212 j += 1;
213 j %= cnts.len();
214 }
215
216 cnts[i] = cnts[j] / 2;
217 cnts[j] -= cnts[i];
218 for k in 0..dim {
219 if k % 2 == 0 {
220 centroids[i * dim + k] = centroids[j * dim + k] * (T::one() + eps);
221 centroids[j * dim + k] *= T::one() - eps;
222 } else {
223 centroids[i * dim + k] = centroids[j * dim + k] * (T::one() - eps);
224 centroids[j * dim + k] *= T::one() + eps;
225 }
226 }
227 }
228 }
229}
230
231fn compute_cluster_sizes(
233 membership: &[Option<u32>],
234 radius: &[f32],
235 losses: &[f64],
236 cluster_sizes: &mut [usize],
237) -> f32 {
238 cluster_sizes.fill(0);
239 let mut max_cluster_id = 0;
240 let mut max_cluster_size = 0;
241 membership.iter().for_each(|cluster_id| {
242 if let Some(cluster_id) = cluster_id {
243 let cluster_id = *cluster_id as usize;
244 cluster_sizes[cluster_id] += 1;
245 if cluster_sizes[cluster_id] > max_cluster_size {
246 max_cluster_size = cluster_sizes[cluster_id];
247 max_cluster_id = cluster_id;
248 }
249 }
250 });
251
252 (radius[max_cluster_id] - losses[max_cluster_id] as f32 / cluster_sizes[max_cluster_id] as f32)
253 / membership.len() as f32
254}
255
256fn compute_balance_loss(cluster_sizes: &[usize], n: usize, balance_factor: f32) -> f32 {
257 let size_loss = cluster_sizes.iter().map(|size| size.pow(2)).sum::<usize>() as f32;
258 balance_factor * (size_loss - n.pow(2) as f32 / cluster_sizes.len() as f32)
259}
260
261pub trait KMeansAlgo<T: Num> {
262 fn compute_membership_and_loss(
273 centroids: &[T],
274 data: &[T],
275 dimension: usize,
276 distance_type: DistanceType,
277 balance_factor: f32,
278 cluster_sizes: Option<&[usize]>,
279 index: Option<&SimpleIndex>,
280 ) -> (Vec<Option<u32>>, Vec<f32>, Vec<f64>) {
281 let (membership, dists) = Self::compute_membership_and_dist(
282 centroids,
283 data,
284 dimension,
285 distance_type,
286 balance_factor,
287 cluster_sizes,
288 index,
289 );
290
291 let k = centroids.len() / dimension;
292 let mut cluster_radius = vec![0.0; k];
293 let mut losses = vec![0.0; k];
294 for (cluster_id, dist) in membership.iter().zip(dists.iter()) {
295 if let (Some(cluster_id), Some(dist)) = (cluster_id, dist) {
296 let cluster_id = *cluster_id as usize;
297 cluster_radius[cluster_id] = cluster_radius[cluster_id].max(*dist);
298 losses[cluster_id] += *dist as f64;
299 }
300 }
301
302 (membership, cluster_radius, losses)
303 }
304
305 fn compute_membership_and_dist(
306 centroids: &[T],
307 data: &[T],
308 dimension: usize,
309 distance_type: DistanceType,
310 balance_factor: f32,
311 cluster_sizes: Option<&[usize]>,
312 index: Option<&SimpleIndex>,
313 ) -> (Vec<Option<u32>>, Vec<Option<f32>>);
314
315 fn to_kmeans(
317 data: &[T],
318 dimension: usize,
319 k: usize,
320 membership: &[Option<u32>],
321 cluster_sizes: &mut [usize],
322 distance_type: DistanceType,
323 loss: f64,
324 ) -> KMeans;
325}
326
327pub struct KMeansAlgoFloat<T: ArrowNumericType>
328where
329 T::Native: Float + Num,
330{
331 phantom_data: std::marker::PhantomData<T>,
332}
333
334impl<T: ArrowNumericType> KMeansAlgo<T::Native> for KMeansAlgoFloat<T>
335where
336 T::Native: Float + Dot + L2 + MulAssign + DivAssign + AddAssign + FromPrimitive + Sync,
337 PrimitiveArray<T>: From<Vec<T::Native>>,
338{
339 fn compute_membership_and_dist(
340 centroids: &[T::Native],
341 data: &[T::Native],
342 dimension: usize,
343 distance_type: DistanceType,
344 balance_factor: f32,
345 cluster_sizes: Option<&[usize]>,
346 index: Option<&SimpleIndex>,
347 ) -> (Vec<Option<u32>>, Vec<Option<f32>>) {
348 let cluster_and_dists = match index {
349 Some(index) => data
350 .par_chunks(dimension)
351 .map(|vec| {
352 let query = PrimitiveArray::<T>::from_iter_values(vec.iter().copied());
353 index
355 .search(Arc::new(query))
356 .map(|(id, dist)| Some((id, dist)))
357 .unwrap()
358 })
359 .collect::<Vec<_>>(),
360 None => match distance_type {
361 DistanceType::L2 => data
362 .par_chunks(dimension)
363 .map(|vec| {
364 argmin_value_float_with_bias(
365 l2_distance_batch(vec, centroids, dimension),
366 cluster_sizes
367 .map(|size| size.iter().map(|size| balance_factor * *size as f32)),
368 )
369 })
370 .collect::<Vec<_>>(),
371 DistanceType::Dot => data
372 .par_chunks(dimension)
373 .map(|vec| {
374 argmin_value_float_with_bias(
375 dot_distance_batch(vec, centroids, dimension),
376 cluster_sizes
377 .map(|size| size.iter().map(|size| balance_factor * *size as f32)),
378 )
379 })
380 .collect::<Vec<_>>(),
381 _ => {
382 panic!(
383 "KMeans::find_partitions: {} is not supported",
384 distance_type
385 );
386 }
387 },
388 };
389
390 cluster_and_dists.into_iter().map(Option::unzip).unzip()
391 }
392
393 fn to_kmeans(
394 data: &[T::Native],
395 dimension: usize,
396 k: usize,
397 membership: &[Option<u32>],
398 cluster_sizes: &mut [usize],
399 distance_type: DistanceType,
400 loss: f64,
401 ) -> KMeans {
402 let mut centroids = vec![T::Native::zero(); k * dimension];
403
404 let mut num_cpus = get_num_compute_intensive_cpus();
405 if k < num_cpus || k < 16 {
406 num_cpus = 1;
407 }
408 let chunk_size = k / num_cpus;
409
410 centroids
411 .par_chunks_mut(dimension * chunk_size)
412 .enumerate()
413 .with_max_len(1)
414 .for_each(|(i, centroids)| {
415 let start = i * chunk_size;
416 let end = ((i + 1) * chunk_size).min(k);
417 data.chunks(dimension)
418 .zip(membership.iter())
419 .filter_map(|(vector, cluster_id)| {
420 cluster_id.map(|cluster_id| (vector, cluster_id as usize))
421 })
422 .for_each(|(vector, cluster_id)| {
423 if start <= cluster_id && cluster_id < end {
424 let local_id = cluster_id - start;
425 let centroid =
426 &mut centroids[local_id * dimension..(local_id + 1) * dimension];
427 centroid.iter_mut().zip(vector).for_each(|(c, v)| *c += *v);
428 }
429 });
430 });
431
432 centroids
433 .par_chunks_mut(dimension)
434 .zip(cluster_sizes.par_iter())
435 .for_each(|(centroid, &cnt)| {
436 if cnt > 0 {
437 let norm = T::Native::one() / T::Native::from_usize(cnt).unwrap();
438 centroid.iter_mut().for_each(|v| *v *= norm);
439 }
440 });
441
442 let empty_clusters = cluster_sizes.iter().filter(|&cnt| *cnt == 0).count();
443 if empty_clusters as f32 / k as f32 > 0.1 {
444 if data.len() / dimension < k * 256 {
445 warn!(
446 "KMeans: more than 10% of clusters are empty: {} of {}.\nHelp: this could mean your dataset \
447 is too small to have a meaningful index ({} < {}) or has many duplicate vectors.",
448 empty_clusters,
449 k,
450 data.len() / dimension,
451 k * 256
452 );
453 } else {
454 warn!(
455 "KMeans: more than 10% of clusters are empty: {} of {}.\nHelp: this could mean your dataset \
456 has many duplicate vectors.",
457 empty_clusters, k
458 );
459 }
460 }
461
462 split_clusters(
463 data.len() / dimension,
464 cluster_sizes,
465 &mut centroids,
466 dimension,
467 );
468
469 KMeans {
470 centroids: Arc::new(PrimitiveArray::<T>::from(centroids)),
471 dimension,
472 distance_type,
473 loss,
474 }
475 }
476}
477
478struct KModeAlgo {}
479
480impl KMeansAlgo<u8> for KModeAlgo {
481 fn compute_membership_and_dist(
482 centroids: &[u8],
483 data: &[u8],
484 dimension: usize,
485 distance_type: DistanceType,
486 balance_factor: f32,
487 cluster_sizes: Option<&[usize]>,
488 _: Option<&SimpleIndex>,
489 ) -> (Vec<Option<u32>>, Vec<Option<f32>>) {
490 assert_eq!(distance_type, DistanceType::Hamming);
491 let cluster_and_dists = data
492 .par_chunks(dimension)
493 .map(|vec| {
494 argmin_value(
495 centroids
496 .chunks_exact(dimension)
497 .enumerate()
498 .map(|(id, c)| {
499 hamming(vec, c)
500 + balance_factor
501 * cluster_sizes.map(|sizes| sizes[id] as f32).unwrap_or(0.0)
502 }),
503 )
504 })
505 .collect::<Vec<_>>();
506 cluster_and_dists.into_iter().map(Option::unzip).unzip()
507 }
508
509 fn to_kmeans(
510 data: &[u8],
511 dimension: usize,
512 k: usize,
513 membership: &[Option<u32>],
514 _cluster_sizes: &mut [usize],
515 distance_type: DistanceType,
516 loss: f64,
517 ) -> KMeans {
518 assert_eq!(distance_type, DistanceType::Hamming);
519
520 let mut clusters = HashMap::<u32, Vec<usize>>::new();
521 membership.iter().enumerate().for_each(|(i, part_id)| {
522 if let Some(part_id) = part_id {
523 clusters.entry(*part_id).or_default().push(i);
524 }
525 });
526 let centroids = (0..k as u32)
527 .into_par_iter()
528 .flat_map(|part_id| {
529 if let Some(vecs) = clusters.get(&part_id) {
530 let mut ones = vec![0_u32; dimension * 8];
531 let cnt = vecs.len() as u32;
532 vecs.iter().for_each(|&i| {
533 let vec = &data[i * dimension..(i + 1) * dimension];
534 ones.iter_mut()
535 .zip(vec.view_bits::<Lsb0>())
536 .for_each(|(c, v)| {
537 if *v.as_ref() {
538 *c += 1;
539 }
540 });
541 });
542
543 let bits = ones.iter().map(|&c| c * 2 > cnt).collect::<BitVec<u8>>();
544 bits.as_raw_slice()
545 .iter()
546 .copied()
547 .map(Some)
548 .collect::<Vec<_>>()
549 } else {
550 vec![None; dimension]
551 }
552 })
553 .collect::<Vec<_>>();
554
555 KMeans {
556 centroids: Arc::new(UInt8Array::from(centroids)),
557 dimension,
558 distance_type,
559 loss,
560 }
561 }
562}
563
564#[derive(Debug, Clone)]
566pub struct KMeans {
567 pub centroids: ArrayRef,
571
572 pub dimension: usize,
574
575 pub distance_type: DistanceType,
577
578 pub loss: f64,
580}
581
582impl KMeans {
583 fn empty(dimension: usize, distance_type: DistanceType) -> Self {
584 Self {
585 centroids: arrow_array::array::new_empty_array(&DataType::Float32),
586 dimension,
587 distance_type,
588 loss: f64::MAX,
589 }
590 }
591
592 pub fn with_centroids(
595 centroids: ArrayRef,
596 dimension: usize,
597 distance_type: DistanceType,
598 loss: f64,
599 ) -> Self {
600 assert!(matches!(
601 centroids.data_type(),
602 DataType::Float16 | DataType::Float32 | DataType::Float64 | DataType::UInt8
603 ));
604 Self {
605 centroids,
606 dimension,
607 distance_type,
608 loss,
609 }
610 }
611
612 fn init_random<T: ArrowPrimitiveType>(
620 data: &[T::Native],
621 dimension: usize,
622 k: usize,
623 rng: impl Rng,
624 distance_type: DistanceType,
625 ) -> Self {
626 kmeans_random_init::<T>(data, dimension, k, rng, distance_type)
627 }
628
629 pub fn new(data: &FixedSizeListArray, k: usize, max_iters: u32) -> arrow::error::Result<Self> {
631 let params = KMeansParams {
632 max_iters,
633 distance_type: DistanceType::L2,
634 ..Default::default()
635 };
636 Self::new_with_params(data, k, ¶ms)
637 }
638
639 fn train_kmeans<T: ArrowNumericType, Algo: KMeansAlgo<T::Native>>(
640 data: &FixedSizeListArray,
641 k: usize,
642 params: &KMeansParams,
643 ) -> arrow::error::Result<Self>
644 where
645 T::Native: Num,
646 {
647 let data = if data.len() >= k * 512 {
653 data.slice(0, k * 512)
654 } else {
655 data.clone()
656 };
657
658 let n = data.len();
659 let dimension = data.value_length() as usize;
660
661 let data =
662 data.values()
663 .as_primitive_opt::<T>()
664 .ok_or(ArrowError::InvalidArgumentError(format!(
665 "KMeans: data must be {}, got: {}",
666 T::DATA_TYPE,
667 data.value_type()
668 )))?;
669
670 let mut best_kmeans = Self::empty(dimension, params.distance_type);
671 let mut cluster_sizes = vec![0; k];
672 let mut adjusted_balance_factor = f32::MAX;
673
674 let rng = SmallRng::from_os_rng();
676 for redo in 1..=params.redos {
677 let mut kmeans: Self = match ¶ms.init {
678 KMeanInit::Random => Self::init_random::<T>(
679 data.values(),
680 dimension,
681 k,
682 rng.clone(),
683 params.distance_type,
684 ),
685 KMeanInit::Incremental(centroids) => Self::with_centroids(
686 centroids.values().clone(),
687 dimension,
688 params.distance_type,
689 f64::MAX,
690 ),
691 };
692
693 let mut loss = f64::MAX;
694 for i in 1..=params.max_iters {
695 if let Some(cb) = ¶ms.on_progress {
696 cb(i, params.max_iters);
697 }
698 if i % 10 == 0 {
699 info!(
700 "KMeans training: iteration {} / {}, redo={}",
701 i, params.max_iters, redo
702 );
703 };
704
705 let index = SimpleIndex::may_train_index(
706 kmeans.centroids.clone(),
707 kmeans.dimension,
708 kmeans.distance_type,
709 )?;
710
711 let balance_factor = adjusted_balance_factor.min(params.balance_factor);
712 let (membership, radius, losses) = Algo::compute_membership_and_loss(
713 kmeans.centroids.as_primitive::<T>().values(),
714 data.values(),
715 dimension,
716 params.distance_type,
717 balance_factor,
718 Some(&cluster_sizes),
719 index.as_ref(),
720 );
721
722 adjusted_balance_factor =
723 compute_cluster_sizes(&membership, &radius, &losses, &mut cluster_sizes);
724 let balance_loss = compute_balance_loss(&cluster_sizes, n, balance_factor);
725 let last_loss = losses.iter().sum::<f64>() + balance_loss as f64;
726
727 kmeans = Algo::to_kmeans(
728 data.values(),
729 dimension,
730 k,
731 &membership,
732 &mut cluster_sizes,
733 params.distance_type,
734 last_loss,
735 );
736 if (loss - last_loss).abs() < params.tolerance * last_loss {
737 info!(
738 "KMeans training: converged at iteration {} / {}, redo={}, loss={}, last_loss={}, loss_diff={}",
739 i,
740 params.max_iters,
741 redo,
742 loss,
743 last_loss,
744 (loss - last_loss).abs() / last_loss
745 );
746 break;
747 }
748 loss = last_loss;
749 }
750 if kmeans.loss < best_kmeans.loss {
751 best_kmeans = kmeans;
752 }
753 }
754
755 Ok(best_kmeans)
756 }
757
758 fn create_array_from_indices<T: ArrowNumericType>(
760 indices: &[usize],
761 data_values: &[T::Native],
762 dimension: usize,
763 ) -> arrow::error::Result<FixedSizeListArray>
764 where
765 T::Native: Clone,
766 PrimitiveArray<T>: From<Vec<T::Native>>,
767 {
768 let mut subset_data = Vec::with_capacity(indices.len() * dimension);
769 for &idx in indices {
770 let start = idx * dimension;
771 let end = start + dimension;
772 subset_data.extend_from_slice(&data_values[start..end]);
773 }
774 let array = PrimitiveArray::<T>::from(subset_data);
775 FixedSizeListArray::try_new_from_values(array, dimension as i32)
776 }
777
778 fn train_hierarchical_kmeans<T: ArrowNumericType, Algo: KMeansAlgo<T::Native>>(
784 data: &FixedSizeListArray,
785 target_k: usize,
786 params: &KMeansParams,
787 ) -> arrow::error::Result<Self>
788 where
789 T::Native: Num,
790 PrimitiveArray<T>: From<Vec<T::Native>>,
791 {
792 #[derive(Clone, Debug)]
794 struct Cluster<N> {
795 id: usize,
796 indices: Vec<usize>,
797 centroid: Vec<N>,
798 finalized: bool,
799 }
800
801 impl<N> Eq for Cluster<N> {}
802
803 impl<N> PartialEq for Cluster<N> {
804 fn eq(&self, other: &Self) -> bool {
805 self.indices.len() == other.indices.len()
806 }
807 }
808
809 impl<N> Ord for Cluster<N> {
810 fn cmp(&self, other: &Self) -> Ordering {
811 match (self.finalized, other.finalized) {
813 (false, true) => Ordering::Greater,
814 (true, false) => Ordering::Less,
815 _ => {
816 self.indices.len().cmp(&other.indices.len())
818 }
819 }
820 }
821 }
822
823 impl<N> PartialOrd for Cluster<N> {
824 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
825 Some(self.cmp(other))
826 }
827 }
828
829 let n = data.len();
830 let dimension = data.value_length() as usize;
831
832 let data_values = data
833 .values()
834 .as_primitive_opt::<T>()
835 .ok_or(ArrowError::InvalidArgumentError(format!(
836 "KMeans: data must be {}, got: {}",
837 T::DATA_TYPE,
838 data.value_type()
839 )))?
840 .values();
841
842 let initial_k = params.hierarchical_k.min(target_k).min(n);
844 info!(
845 "Hierarchical clustering: initial k={}, target k={}",
846 initial_k, target_k
847 );
848
849 let initial_kmeans = Self::train_kmeans::<T, Algo>(data, initial_k, params)?;
850
851 let (membership, _, _) = Algo::compute_membership_and_loss(
853 initial_kmeans.centroids.as_primitive::<T>().values(),
854 data_values,
855 dimension,
856 params.distance_type,
857 0.0, None,
859 None,
860 );
861
862 let mut heap: BinaryHeap<Cluster<T::Native>> = BinaryHeap::new();
864 let mut next_cluster_id = 0;
865 let initial_centroids = initial_kmeans.centroids.as_primitive::<T>().values();
866
867 for i in 0..initial_k {
868 let mut cluster_indices = Vec::new();
869 for (idx, &cluster_id) in membership.iter().enumerate() {
870 if let Some(cid) = cluster_id
871 && cid as usize == i
872 {
873 cluster_indices.push(idx);
874 }
875 }
876
877 if !cluster_indices.is_empty() {
878 let centroid_start = i * dimension;
879 let centroid_end = centroid_start + dimension;
880 let centroid = initial_centroids[centroid_start..centroid_end].to_vec();
881
882 heap.push(Cluster {
883 id: next_cluster_id,
884 indices: cluster_indices,
885 centroid,
886 finalized: false,
887 });
888 next_cluster_id += 1;
889 }
890 }
891
892 while heap.len() < target_k {
894 let mut largest_cluster = heap.pop().ok_or(ArrowError::InvalidArgumentError(
896 "No cluster can be further split".to_string(),
897 ))?;
898
899 if largest_cluster.finalized {
901 log::warn!(
902 "Cluster {} is already finalized, no further split is possible, finish with {} clusters",
903 largest_cluster.id,
904 heap.len() + 1
905 );
906 heap.push(largest_cluster);
907 break;
908 }
909
910 if largest_cluster.indices.len() <= 1 {
912 log::warn!(
913 "Cluster {} has only 1 point, no further split is possible, finish with {} clusters",
914 largest_cluster.id,
915 heap.len() + 1
916 );
917 heap.push(largest_cluster);
918 break;
919 }
920
921 let cluster_size = largest_cluster.indices.len();
922 log::debug!(
923 "Splitting cluster {} with {} points (current total clusters: {})",
924 largest_cluster.id,
925 cluster_size,
926 heap.len() + 1 );
928
929 let remaining_k = target_k - heap.len(); let cluster_k = if cluster_size <= params.hierarchical_k {
932 2.min(remaining_k).min(cluster_size)
933 } else {
934 let suggested_k = cluster_size / params.hierarchical_k;
936 suggested_k
937 .min(remaining_k)
938 .min(params.hierarchical_k)
939 .max(2)
940 };
941
942 let sub_data = Self::create_array_from_indices::<T>(
944 &largest_cluster.indices,
945 data_values,
946 dimension,
947 )?;
948
949 let sub_kmeans = Self::train_kmeans::<T, Algo>(&sub_data, cluster_k, params)?;
951
952 let sub_data = sub_data.values().as_primitive::<T>().values();
954 let (sub_membership, _, _) = Algo::compute_membership_and_loss(
955 sub_kmeans.centroids.as_primitive::<T>().values(),
956 sub_data,
957 dimension,
958 params.distance_type,
959 0.0,
960 None,
961 None,
962 );
963
964 let approx_cluster_capacity = if cluster_k > 0 {
966 largest_cluster.indices.len().div_ceil(cluster_k)
967 } else {
968 0
969 };
970 let mut cluster_assignments: Vec<Vec<usize>> = (0..cluster_k)
971 .map(|_| Vec::with_capacity(approx_cluster_capacity))
972 .collect();
973
974 let mut first_sid: Option<u32> = None;
975 let mut all_same = true;
976 for (local_idx, &membership) in sub_membership.iter().enumerate() {
977 let Some(sub_cluster_id) = membership else {
978 continue;
979 };
980
981 if let Some(first) = first_sid {
982 if sub_cluster_id != first {
983 all_same = false;
984 }
985 } else {
986 first_sid = Some(sub_cluster_id);
987 }
988
989 let sub_cluster_id = sub_cluster_id as usize;
990 if let Some(indices) = cluster_assignments.get_mut(sub_cluster_id) {
991 indices.push(largest_cluster.indices[local_idx]);
992 } else {
993 all_same = false;
995 }
996 }
997
998 if all_same {
1000 largest_cluster.finalized = true;
1001 heap.push(largest_cluster);
1002 continue;
1003 }
1004
1005 let sub_centroids = sub_kmeans.centroids.as_primitive::<T>().values();
1007 for (i, new_cluster_indices) in cluster_assignments.into_iter().enumerate() {
1008 if new_cluster_indices.is_empty() {
1009 continue;
1010 }
1011
1012 let centroid_start = i * dimension;
1013 let centroid_end = centroid_start + dimension;
1014 let centroid = sub_centroids[centroid_start..centroid_end].to_vec();
1015
1016 heap.push(Cluster {
1017 id: next_cluster_id,
1018 indices: new_cluster_indices,
1019 centroid,
1020 finalized: false,
1021 });
1022 next_cluster_id += 1;
1023 }
1024
1025 log::debug!(
1026 "Split complete: now have {} clusters (target: {})",
1027 heap.len(),
1028 target_k
1029 );
1030 }
1031 debug_assert_eq!(heap.len(), target_k);
1032
1033 let mut all_clusters: Vec<Cluster<T::Native>> = heap.into_vec();
1035 all_clusters.sort_by_key(|c| c.id);
1037
1038 let flat_centroids: Vec<T::Native> =
1039 all_clusters.into_iter().flat_map(|c| c.centroid).collect();
1040 let centroids_array = PrimitiveArray::<T>::from(flat_centroids);
1041
1042 Ok(Self {
1043 centroids: Arc::new(centroids_array),
1044 dimension,
1045 distance_type: params.distance_type,
1046 loss: 0.0, })
1048 }
1049
1050 pub fn new_with_params(
1054 data: &FixedSizeListArray,
1055 k: usize,
1056 params: &KMeansParams,
1057 ) -> arrow::error::Result<Self> {
1058 let n = data.len();
1059 if n < k {
1060 return Err(ArrowError::InvalidArgumentError(format!(
1061 "KMeans: training does not have sufficient data points: n({}) is smaller than k({})",
1062 n, k
1063 )));
1064 }
1065
1066 if k > 256 && params.hierarchical_k > 1 {
1071 log::debug!("Using hierarchical clustering for k={}", k);
1072 return match (data.value_type(), params.distance_type) {
1073 (DataType::Float16, _) => Self::train_hierarchical_kmeans::<
1074 Float16Type,
1075 KMeansAlgoFloat<Float16Type>,
1076 >(data, k, params),
1077 (DataType::Float32, _) => Self::train_hierarchical_kmeans::<
1078 Float32Type,
1079 KMeansAlgoFloat<Float32Type>,
1080 >(data, k, params),
1081 (DataType::Float64, _) => Self::train_hierarchical_kmeans::<
1082 Float64Type,
1083 KMeansAlgoFloat<Float64Type>,
1084 >(data, k, params),
1085 (DataType::UInt8, DistanceType::Hamming) => {
1086 Self::train_hierarchical_kmeans::<UInt8Type, KModeAlgo>(data, k, params)
1087 }
1088 _ => Err(ArrowError::InvalidArgumentError(format!(
1089 "KMeans: can not train data type {} with distance type: {}",
1090 data.value_type(),
1091 params.distance_type
1092 ))),
1093 };
1094 }
1095
1096 match (data.value_type(), params.distance_type) {
1097 (DataType::Float16, _) => {
1098 Self::train_kmeans::<Float16Type, KMeansAlgoFloat<Float16Type>>(data, k, params)
1099 }
1100
1101 (DataType::Float32, _) => {
1102 Self::train_kmeans::<Float32Type, KMeansAlgoFloat<Float32Type>>(data, k, params)
1103 }
1104 (DataType::Float64, _) => {
1105 Self::train_kmeans::<Float64Type, KMeansAlgoFloat<Float64Type>>(data, k, params)
1106 }
1107 (DataType::UInt8, DistanceType::Hamming) => {
1108 Self::train_kmeans::<UInt8Type, KModeAlgo>(data, k, params)
1109 }
1110 _ => Err(ArrowError::InvalidArgumentError(format!(
1111 "KMeans: can not train data type {} with distance type: {}",
1112 data.value_type(),
1113 params.distance_type
1114 ))),
1115 }
1116 }
1117}
1118
1119pub fn kmeans_find_partitions_arrow_array(
1120 centroids: &FixedSizeListArray,
1121 query: &dyn Array,
1122 nprobes: usize,
1123 distance_type: DistanceType,
1124) -> arrow::error::Result<(UInt32Array, Float32Array)> {
1125 if centroids.value_length() as usize != query.len() {
1126 return Err(ArrowError::InvalidArgumentError(format!(
1127 "Centroids and vectors have different dimensions: {} != {}",
1128 centroids.value_length(),
1129 query.len()
1130 )));
1131 }
1132
1133 match (centroids.value_type(), query.data_type()) {
1134 (DataType::Float16, DataType::Float16) => Ok(kmeans_find_partitions(
1135 centroids.values().as_primitive::<Float16Type>().values(),
1136 query.as_primitive::<Float16Type>().values(),
1137 nprobes,
1138 distance_type,
1139 )?),
1140 (DataType::Float32, DataType::Float32) => Ok(kmeans_find_partitions(
1141 centroids.values().as_primitive::<Float32Type>().values(),
1142 query.as_primitive::<Float32Type>().values(),
1143 nprobes,
1144 distance_type,
1145 )?),
1146 (DataType::Float64, DataType::Float64) => Ok(kmeans_find_partitions(
1147 centroids.values().as_primitive::<Float64Type>().values(),
1148 query.as_primitive::<Float64Type>().values(),
1149 nprobes,
1150 distance_type,
1151 )?),
1152 (DataType::UInt8, DataType::UInt8) => Ok(kmeans_find_partitions_binary(
1153 centroids.values().as_primitive::<UInt8Type>().values(),
1154 query.as_primitive::<UInt8Type>().values(),
1155 nprobes,
1156 distance_type,
1157 )?),
1158 _ => Err(ArrowError::InvalidArgumentError(format!(
1159 "Centroids and vectors have different types: {} != {}",
1160 centroids.value_type(),
1161 query.data_type()
1162 ))),
1163 }
1164}
1165
1166pub fn kmeans_find_partitions<T: Float + L2 + Dot>(
1178 centroids: &[T],
1179 query: &[T],
1180 nprobes: usize,
1181 distance_type: DistanceType,
1182) -> arrow::error::Result<(UInt32Array, Float32Array)> {
1183 let dists: Vec<f32> = match distance_type {
1184 DistanceType::L2 => l2_distance_batch(query, centroids, query.len()).collect(),
1185 DistanceType::Dot => dot_distance_batch(query, centroids, query.len()).collect(),
1186 _ => {
1187 panic!(
1188 "KMeans::find_partitions: {} is not supported",
1189 distance_type
1190 );
1191 }
1192 };
1193
1194 let dists_arr = Float32Array::from(dists);
1196 let indices = sort_to_indices(&dists_arr, None, Some(nprobes))?;
1197 let dists = arrow::compute::take(&dists_arr, &indices, None)?
1198 .as_primitive::<Float32Type>()
1199 .clone();
1200 Ok((indices, dists))
1201}
1202
1203pub fn kmeans_find_partitions_binary(
1204 centroids: &[u8],
1205 query: &[u8],
1206 nprobes: usize,
1207 distance_type: DistanceType,
1208) -> arrow::error::Result<(UInt32Array, Float32Array)> {
1209 let dists: Vec<f32> = match distance_type {
1210 DistanceType::Hamming => hamming_distance_batch(query, centroids, query.len()).collect(),
1211 _ => {
1212 panic!(
1213 "KMeans::find_partitions: {} is not supported",
1214 distance_type
1215 );
1216 }
1217 };
1218
1219 let dists_arr = Float32Array::from(dists);
1221 let indices = sort_to_indices(&dists_arr, None, Some(nprobes))?;
1222 let dists = arrow::compute::take(&dists_arr, &indices, None)?
1223 .as_primitive::<Float32Type>()
1224 .clone();
1225 Ok((indices, dists))
1226}
1227
1228#[allow(clippy::type_complexity)]
1230pub fn compute_partitions_arrow_array(
1231 centroids: &FixedSizeListArray,
1232 vectors: &FixedSizeListArray,
1233 distance_type: DistanceType,
1234) -> arrow::error::Result<(Vec<Option<u32>>, Vec<Option<f32>>)> {
1235 if centroids.value_length() != vectors.value_length() {
1236 return Err(ArrowError::InvalidArgumentError(
1237 "Centroids and vectors have different dimensions".to_string(),
1238 ));
1239 }
1240 match (centroids.value_type(), vectors.value_type()) {
1241 (DataType::Float16, DataType::Float16) => Ok(compute_partitions_with_dists::<
1242 Float16Type,
1243 KMeansAlgoFloat<Float16Type>,
1244 >(
1245 centroids.values().as_primitive(),
1246 vectors.values().as_primitive(),
1247 centroids.value_length(),
1248 distance_type,
1249 )),
1250 (DataType::Float32, DataType::Float32) => Ok(compute_partitions_with_dists::<
1251 Float32Type,
1252 KMeansAlgoFloat<Float32Type>,
1253 >(
1254 centroids.values().as_primitive(),
1255 vectors.values().as_primitive(),
1256 centroids.value_length(),
1257 distance_type,
1258 )),
1259 (DataType::Float32, DataType::Int8) => Ok(compute_partitions_with_dists::<
1260 Float32Type,
1261 KMeansAlgoFloat<Float32Type>,
1262 >(
1263 centroids.values().as_primitive(),
1264 vectors.convert_to_floating_point()?.values().as_primitive(),
1265 centroids.value_length(),
1266 distance_type,
1267 )),
1268 (DataType::Float64, DataType::Float64) => Ok(compute_partitions_with_dists::<
1269 Float64Type,
1270 KMeansAlgoFloat<Float64Type>,
1271 >(
1272 centroids.values().as_primitive(),
1273 vectors.values().as_primitive(),
1274 centroids.value_length(),
1275 distance_type,
1276 )),
1277 (DataType::UInt8, DataType::UInt8) => {
1278 Ok(compute_partitions_with_dists::<UInt8Type, KModeAlgo>(
1279 centroids.values().as_primitive(),
1280 vectors.values().as_primitive(),
1281 centroids.value_length(),
1282 distance_type,
1283 ))
1284 }
1285 _ => Err(ArrowError::InvalidArgumentError(
1286 "Centroids and vectors have incompatible types".to_string(),
1287 )),
1288 }
1289}
1290
1291pub fn compute_partitions<T: ArrowNumericType, K: KMeansAlgo<T::Native>>(
1295 centroids: &PrimitiveArray<T>,
1296 vectors: &PrimitiveArray<T>,
1297 dimension: impl AsPrimitive<usize>,
1298 distance_type: DistanceType,
1299) -> (Vec<Option<u32>>, f64)
1300where
1301 T::Native: Num,
1302{
1303 let dimension = dimension.as_();
1304 let (membership, _, losses) = K::compute_membership_and_loss(
1305 centroids.values(),
1306 vectors.values(),
1307 dimension,
1308 distance_type,
1309 0.0,
1310 None,
1311 None,
1312 );
1313 (membership, losses.iter().sum::<f64>())
1314}
1315
1316pub fn compute_partitions_with_dists<T: ArrowNumericType, K: KMeansAlgo<T::Native>>(
1319 centroids: &PrimitiveArray<T>,
1320 vectors: &PrimitiveArray<T>,
1321 dimension: impl AsPrimitive<usize>,
1322 distance_type: DistanceType,
1323) -> (Vec<Option<u32>>, Vec<Option<f32>>)
1324where
1325 T::Native: Num,
1326{
1327 let dimension = dimension.as_();
1328 K::compute_membership_and_dist(
1329 centroids.values(),
1330 vectors.values(),
1331 dimension,
1332 distance_type,
1333 0.0,
1334 None,
1335 None,
1336 )
1337}
1338
1339#[allow(clippy::too_many_arguments)]
1352pub fn train_kmeans<T: ArrowPrimitiveType>(
1353 array: &PrimitiveArray<T>,
1354 mut params: KMeansParams,
1355 dimension: usize,
1356 k: usize,
1357 sample_rate: usize,
1358) -> Result<KMeans>
1359where
1360 T::Native: Dot + L2 + Normalize,
1361 PrimitiveArray<T>: From<Vec<T::Native>>,
1362{
1363 let num_rows = array.len() / dimension;
1364 if num_rows < k {
1365 return Err(Error::unprocessable(format!(
1366 "KMeans cannot train {k} centroids with {num_rows} vectors; choose a smaller K (< {num_rows})"
1367 )));
1368 }
1369
1370 let data = if num_rows > sample_rate * k {
1372 log::info!(
1373 "Sample {} out of {} to train kmeans of {} dim, {} clusters",
1374 sample_rate * k,
1375 array.len() / dimension,
1376 dimension,
1377 k,
1378 );
1379 let sample_size = sample_rate * k;
1380 array.slice(0, sample_size * dimension)
1381 } else {
1382 array.clone()
1383 };
1384
1385 let data = FixedSizeListArray::try_new_from_values(data, dimension as i32)?;
1386
1387 params.balance_factor /= data.len() as f32;
1388 let model = KMeans::new_with_params(&data, k, ¶ms)?;
1389 Ok(model)
1390}
1391
1392#[inline]
1393pub fn compute_partition<T: Float + L2 + Dot>(
1394 centroids: &[T],
1395 vector: &[T],
1396 distance_type: DistanceType,
1397) -> Option<u32> {
1398 match distance_type {
1399 DistanceType::L2 => {
1400 argmin_value_float(l2_distance_batch(vector, centroids, vector.len())).map(|(c, _)| c)
1401 }
1402 DistanceType::Dot => {
1403 argmin_value_float(dot_distance_batch(vector, centroids, vector.len())).map(|(c, _)| c)
1404 }
1405 _ => {
1406 panic!(
1407 "KMeans::compute_partition: distance type {} is not supported",
1408 distance_type
1409 );
1410 }
1411 }
1412}
1413
1414#[cfg(test)]
1415mod tests {
1416 use std::iter::repeat_n;
1417
1418 use arrow_array::Float16Array;
1419 use arrow_array::types::Float16Type;
1420 use half::f16;
1421 use lance_arrow::*;
1422 use lance_testing::datagen::generate_random_array;
1423
1424 use super::*;
1425 use lance_linalg::distance::l2;
1426 use lance_linalg::kernels::argmin;
1427
1428 #[test]
1429 fn test_train_with_small_dataset() {
1430 let data = Float32Array::from(vec![1.0, 2.0, 3.0, 4.0]);
1431 let data = FixedSizeListArray::try_new_from_values(data, 2).unwrap();
1432 match KMeans::new(&data, 128, 5) {
1433 Ok(_) => panic!("Should fail to train KMeans"),
1434 Err(e) => {
1435 assert!(e.to_string().contains("smaller than"));
1436 }
1437 }
1438 }
1439
1440 #[test]
1441 fn test_compute_partitions() {
1442 const DIM: usize = 256;
1443 let centroids = generate_random_array(DIM * 18);
1444 let data = generate_random_array(DIM * 20);
1445
1446 let expected = data
1447 .values()
1448 .chunks(DIM)
1449 .map(|row| {
1450 argmin(
1451 centroids
1452 .values()
1453 .chunks(DIM)
1454 .map(|centroid| l2(row, centroid)),
1455 )
1456 })
1457 .collect::<Vec<_>>();
1458 let (actual, _) = compute_partitions::<Float32Type, KMeansAlgoFloat<Float32Type>>(
1459 ¢roids,
1460 &data,
1461 DIM,
1462 DistanceType::L2,
1463 );
1464 assert_eq!(expected, actual);
1465 }
1466
1467 #[tokio::test]
1468 async fn test_compute_membership_and_loss() {
1469 const DIM: usize = 256;
1470 let centroids = generate_random_array(DIM * 18);
1471 let data = generate_random_array(DIM * 20);
1472
1473 let (membership, _, losses) = KMeansAlgoFloat::<Float32Type>::compute_membership_and_loss(
1474 centroids.as_slice(),
1475 data.values(),
1476 DIM,
1477 DistanceType::L2,
1478 0.0,
1479 None,
1480 None,
1481 );
1482 let loss = losses.iter().sum::<f64>();
1483 assert!(loss > 0.0, "loss is not zero: {}", loss);
1484 membership.iter().for_each(|cd| {
1485 assert!(cd.is_some());
1486 });
1487 }
1488
1489 #[tokio::test]
1490 async fn test_l2_with_nans() {
1491 const DIM: usize = 8;
1492 const K: usize = 32;
1493 const NUM_CENTROIDS: usize = 16 * 2048;
1494 let centroids = generate_random_array(DIM * NUM_CENTROIDS);
1495 let values = Float32Array::from_iter_values(repeat_n(f32::NAN, DIM * K));
1496
1497 compute_partitions::<Float32Type, KMeansAlgoFloat<Float32Type>>(
1498 ¢roids,
1499 &values,
1500 DIM,
1501 DistanceType::L2,
1502 )
1503 .0
1504 .iter()
1505 .for_each(|cd| {
1506 assert!(cd.is_none());
1507 });
1508 }
1509
1510 #[tokio::test]
1511 async fn test_train_l2_kmeans_with_nans() {
1512 const DIM: usize = 8;
1513 const K: usize = 32;
1514 const NUM_CENTROIDS: usize = 16 * 2048;
1515 let centroids = generate_random_array(DIM * NUM_CENTROIDS);
1516 let values = repeat_n(f32::NAN, DIM * K).collect::<Vec<_>>();
1517
1518 let (membership, _, _) = KMeansAlgoFloat::<Float32Type>::compute_membership_and_loss(
1519 centroids.as_slice(),
1520 &values,
1521 DIM,
1522 DistanceType::L2,
1523 0.0,
1524 None,
1525 None,
1526 );
1527
1528 membership.iter().for_each(|cd| assert!(cd.is_none()));
1529 }
1530
1531 #[tokio::test]
1532 async fn test_train_kmode() {
1533 const DIM: usize = 16;
1534 const K: usize = 32;
1535 const NUM_VALUES: usize = 256 * K;
1536
1537 let mut rng = SmallRng::from_os_rng();
1538 let values =
1539 UInt8Array::from_iter_values((0..NUM_VALUES * DIM).map(|_| rng.random_range(0..255)));
1540
1541 let fsl = FixedSizeListArray::try_new_from_values(values, DIM as i32).unwrap();
1542
1543 let params = KMeansParams {
1544 distance_type: DistanceType::Hamming,
1545 ..Default::default()
1546 };
1547 let kmeans = KMeans::new_with_params(&fsl, K, ¶ms).unwrap();
1548 assert_eq!(kmeans.centroids.len(), K * DIM);
1549 assert_eq!(kmeans.dimension, DIM);
1550 assert_eq!(kmeans.centroids.data_type(), &DataType::UInt8);
1551 }
1552
1553 #[tokio::test]
1554 async fn test_hierarchical_kmeans() {
1555 const DIM: usize = 64;
1556 const K: usize = 257; const NUM_VALUES: usize = 1024 * K;
1558
1559 let values = generate_random_array(NUM_VALUES * DIM);
1560 let fsl = FixedSizeListArray::try_new_from_values(values, DIM as i32).unwrap();
1561
1562 let params = KMeansParams {
1563 max_iters: 10,
1564 hierarchical_k: 16,
1565 ..Default::default()
1566 };
1567
1568 let kmeans = KMeans::new_with_params(&fsl, K, ¶ms).unwrap();
1569
1570 assert_eq!(kmeans.centroids.len(), K * DIM);
1572 assert_eq!(kmeans.dimension, DIM);
1573 assert_eq!(kmeans.centroids.data_type(), &DataType::Float32);
1574
1575 let centroids = kmeans.centroids.as_primitive::<Float32Type>().values();
1577 for val in centroids {
1578 assert!(!val.is_nan(), "Centroid should not contain NaN values");
1579 }
1580 }
1581
1582 #[tokio::test]
1583 async fn test_float16_underflow_fix() {
1584 const DIM: usize = 2;
1590 const K: usize = 2;
1591 const NUM_VALUES: usize = K * 65536; let f32_values = generate_random_array(NUM_VALUES * DIM);
1594 let f16_values = Float16Array::from_iter_values(
1595 f32_values.values().iter().map(|&v| half::f16::from_f32(v)),
1596 );
1597 let fsl = FixedSizeListArray::try_new_from_values(f16_values, DIM as i32).unwrap();
1598
1599 let params = KMeansParams {
1600 max_iters: 10,
1601 ..Default::default()
1602 };
1603
1604 let kmeans = KMeans::new_with_params(&fsl, K, ¶ms).unwrap();
1605
1606 assert_eq!(kmeans.centroids.len(), K * DIM);
1608 assert_eq!(kmeans.dimension, DIM);
1609 assert_eq!(kmeans.centroids.data_type(), &DataType::Float16);
1610
1611 let centroids = kmeans.centroids.as_primitive::<Float16Type>().values();
1614 for &val in centroids {
1615 assert!(!val.is_nan(), "Centroid should not contain NaN values");
1616 assert!(val != f16::ZERO);
1617 }
1618 }
1619}