incremental_rs/
minibatch_kmeans.rs1use crate::error::IncrementalError;
2use crate::IncrementalUnsupervisedEstimator;
3use ndarray::{Array1, Array2};
4use rand::Rng;
5
6#[derive(Debug)]
7pub struct MiniBatchKMeans {
8 n_clusters: usize,
9 centroids: Option<Array2<f64>>,
10 counts: Array1<f64>, n_features: Option<usize>,
12}
13
14impl MiniBatchKMeans {
15 pub fn new(n_clusters: usize) -> Self {
16 Self {
17 n_clusters,
18 centroids: None,
19 counts: Array1::zeros(n_clusters),
20 n_features: None,
21 }
22 }
23
24 fn sq_euclidean(a: &ndarray::ArrayView1<f64>, b: &ndarray::ArrayView1<f64>) -> f64 {
26 a.iter()
27 .zip(b.iter())
28 .map(|(x, y)| (x - y).powi(2))
29 .sum()
30 }
31
32 fn init_centroids(&mut self, batch_x: &Array2<f64>) -> Result<(), IncrementalError> {
34 let n_samples = batch_x.nrows();
35 let n_features = batch_x.ncols();
36
37 if n_samples < self.n_clusters {
38 return Err(IncrementalError::EmptyBatch);
39 }
40
41 let mut rng = rand::thread_rng();
42 let mut centroids = Array2::zeros((self.n_clusters, n_features));
43 let mut chosen_indices = Vec::with_capacity(self.n_clusters);
44
45 while chosen_indices.len() < self.n_clusters {
46 let idx = rng.gen_range(0..n_samples);
47 if !chosen_indices.contains(&idx) {
48 chosen_indices.push(idx);
49 }
50 }
51
52 for (c_idx, &s_idx) in chosen_indices.iter().enumerate() {
53 centroids.row_mut(c_idx).assign(&batch_x.row(s_idx));
54 }
55
56 self.centroids = Some(centroids);
57 self.n_features = Some(n_features);
58 Ok(())
59 }
60
61 fn validate_batch(&self, batch_x: &Array2<f64>) -> Result<(), IncrementalError> {
62 if batch_x.nrows() == 0 {
63 return Err(IncrementalError::EmptyBatch);
64 }
65 if batch_x.iter().any(|v| !v.is_finite()) {
66 return Err(IncrementalError::NonFiniteInput);
67 }
68 if let Some(n_f) = self.n_features {
69 if batch_x.ncols() != n_f {
70 return Err(IncrementalError::DimensionMismatch {
71 expected: n_f,
72 actual: batch_x.ncols(),
73 });
74 }
75 }
76 Ok(())
77 }
78
79 pub fn centroids(&self) -> Option<&Array2<f64>> {
81 self.centroids.as_ref()
82 }
83}
84
85impl IncrementalUnsupervisedEstimator for MiniBatchKMeans {
86 fn partial_fit(&mut self, batch_x: &Array2<f64>) -> Result<(), IncrementalError> {
87 self.validate_batch(batch_x)?;
88
89 if self.centroids.is_none() {
90 self.init_centroids(batch_x)?;
91 }
92
93 let centroids = self.centroids.as_mut().unwrap();
94
95 let mut assignments = Vec::with_capacity(batch_x.nrows());
97 for row in batch_x.rows() {
98 let mut best_cluster = 0;
99 let mut min_dist = f64::INFINITY;
100
101 for (c_idx, c_row) in centroids.rows().into_iter().enumerate() {
102 let dist = Self::sq_euclidean(&row, &c_row);
103 if dist < min_dist {
104 min_dist = dist;
105 best_cluster = c_idx;
106 }
107 }
108 assignments.push(best_cluster);
109 }
110
111 for (row_idx, &c_idx) in assignments.iter().enumerate() {
113 self.counts[c_idx] += 1.0;
114 let eta = 1.0 / self.counts[c_idx]; let x_i = batch_x.row(row_idx);
117 let mut c_i = centroids.row_mut(c_idx);
118
119 c_i.zip_mut_with(&x_i, |c_val, &x_val| {
121 *c_val = (1.0 - eta) * (*c_val) + eta * x_val;
122 });
123 }
124
125 Ok(())
126 }
127
128 fn predict_labels(&self, x: &Array2<f64>) -> Result<Array1<usize>, IncrementalError> {
129 self.validate_batch(x)?;
130
131 let centroids = match &self.centroids {
132 Some(c) => c,
133 None => return Err(IncrementalError::EmptyBatch),
134 };
135
136 let mut predictions = Vec::with_capacity(x.nrows());
137 for row in x.rows() {
138 let mut best_cluster = 0;
139 let mut min_dist = f64::INFINITY;
140
141 for (c_idx, c_row) in centroids.rows().into_iter().enumerate() {
142 let dist = Self::sq_euclidean(&row, &c_row);
143 if dist < min_dist {
144 min_dist = dist;
145 best_cluster = c_idx;
146 }
147 }
148 predictions.push(best_cluster);
149 }
150
151 Ok(Array1::from(predictions))
152 }
153}