Skip to main content

haagenti_neural/
training.rs

1//! Codebook training for neural compression
2
3use crate::{
4    Codebook, CodebookConfig, CodebookStats, LayerCodebook, LayerType, NeuralError, Result,
5};
6use serde::{Deserialize, Serialize};
7
8/// Configuration for codebook training
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct TrainingConfig {
11    /// Maximum training iterations
12    pub max_iterations: usize,
13    /// Convergence threshold (change in MSE)
14    pub convergence_threshold: f32,
15    /// Mini-batch size
16    pub batch_size: usize,
17    /// Learning rate for centroid updates
18    pub learning_rate: f32,
19    /// K-means++ initialization
20    pub use_kmeans_init: bool,
21    /// Number of random restarts
22    pub num_restarts: usize,
23    /// Seed for reproducibility
24    pub seed: Option<u64>,
25}
26
27impl Default for TrainingConfig {
28    fn default() -> Self {
29        Self {
30            max_iterations: 100,
31            convergence_threshold: 1e-5,
32            batch_size: 4096,
33            learning_rate: 0.01,
34            use_kmeans_init: true,
35            num_restarts: 3,
36            seed: None,
37        }
38    }
39}
40
41/// Codebook trainer using K-means clustering
42pub struct CodebookTrainer {
43    config: TrainingConfig,
44}
45
46impl CodebookTrainer {
47    /// Create a new trainer
48    pub fn new(config: TrainingConfig) -> Self {
49        Self { config }
50    }
51
52    /// Train a codebook on data
53    pub fn train(&self, codebook_config: CodebookConfig, data: &[f32]) -> Result<Codebook> {
54        let dim = codebook_config.centroid_dim;
55        let num_vectors = data.len() / dim;
56
57        if !data.len().is_multiple_of(dim) {
58            return Err(NeuralError::TrainingError(format!(
59                "Data length {} not divisible by centroid dim {}",
60                data.len(),
61                dim
62            )));
63        }
64
65        if num_vectors < codebook_config.num_centroids {
66            return Err(NeuralError::TrainingError(format!(
67                "Need at least {} vectors, got {}",
68                codebook_config.num_centroids, num_vectors
69            )));
70        }
71
72        let mut best_codebook = None;
73        let mut best_mse = f32::INFINITY;
74
75        for restart in 0..self.config.num_restarts {
76            let seed = self.config.seed.map(|s| s + restart as u64);
77            let result = self.train_single(&codebook_config, data, seed)?;
78
79            if result.1 < best_mse {
80                best_mse = result.1;
81                best_codebook = Some(result.0);
82            }
83        }
84
85        best_codebook.ok_or_else(|| NeuralError::TrainingError("Training failed".into()))
86    }
87
88    /// Single training run
89    fn train_single(
90        &self,
91        config: &CodebookConfig,
92        data: &[f32],
93        seed: Option<u64>,
94    ) -> Result<(Codebook, f32)> {
95        let dim = config.centroid_dim;
96        let num_vectors = data.len() / dim;
97
98        // Initialize centroids
99        let mut centroids = if self.config.use_kmeans_init {
100            self.kmeans_pp_init(config.num_centroids, data, dim, seed)
101        } else {
102            self.random_init(config.num_centroids, data, dim, seed)
103        };
104
105        let mut prev_mse = f32::INFINITY;
106        let mut iterations = 0;
107
108        // K-means iterations
109        for iter in 0..self.config.max_iterations {
110            iterations = iter + 1;
111
112            // Assignment step
113            let assignments = self.assign_clusters(&centroids, data, dim);
114
115            // Update step
116            let new_centroids =
117                self.update_centroids(&assignments, data, config.num_centroids, dim);
118            centroids = new_centroids;
119
120            // Compute MSE
121            let mse = self.compute_mse(&centroids, &assignments, data, dim);
122
123            // Check convergence
124            if (prev_mse - mse).abs() < self.config.convergence_threshold {
125                break;
126            }
127            prev_mse = mse;
128        }
129
130        // Compute final statistics
131        let assignments = self.assign_clusters(&centroids, data, dim);
132        let final_mse = self.compute_mse(&centroids, &assignments, data, dim);
133        let usage = self.compute_usage(&assignments, config.num_centroids);
134
135        let stats = CodebookStats {
136            mse: final_mse,
137            iterations,
138            usage_distribution: usage,
139            samples_seen: num_vectors,
140        };
141
142        let mut codebook = Codebook::from_centroids(config.clone(), centroids, "trained")?;
143        codebook.stats = stats;
144
145        Ok((codebook, final_mse))
146    }
147
148    /// K-means++ initialization
149    fn kmeans_pp_init(&self, k: usize, data: &[f32], dim: usize, seed: Option<u64>) -> Vec<f32> {
150        use rand::prelude::*;
151
152        let mut rng = match seed {
153            Some(s) => StdRng::seed_from_u64(s),
154            None => StdRng::from_entropy(),
155        };
156
157        let num_vectors = data.len() / dim;
158        let mut centroids = Vec::with_capacity(k * dim);
159        let mut distances = vec![f32::INFINITY; num_vectors];
160
161        // Choose first centroid randomly
162        let first_idx = rng.gen_range(0..num_vectors);
163        centroids.extend_from_slice(&data[first_idx * dim..(first_idx + 1) * dim]);
164
165        // Choose remaining centroids
166        for _ in 1..k {
167            // Update distances to nearest centroid
168            for i in 0..num_vectors {
169                let vec = &data[i * dim..(i + 1) * dim];
170                let last_centroid = &centroids[centroids.len() - dim..];
171                let dist = self.squared_distance(vec, last_centroid);
172                distances[i] = distances[i].min(dist);
173            }
174
175            // Sample proportional to squared distance
176            let total: f32 = distances.iter().sum();
177            let threshold = rng.gen::<f32>() * total;
178
179            let mut cumsum = 0.0;
180            let mut chosen = 0;
181            for (i, &d) in distances.iter().enumerate() {
182                cumsum += d;
183                if cumsum >= threshold {
184                    chosen = i;
185                    break;
186                }
187            }
188
189            centroids.extend_from_slice(&data[chosen * dim..(chosen + 1) * dim]);
190        }
191
192        centroids
193    }
194
195    /// Random initialization
196    fn random_init(&self, k: usize, data: &[f32], dim: usize, seed: Option<u64>) -> Vec<f32> {
197        use rand::prelude::*;
198
199        let mut rng = match seed {
200            Some(s) => StdRng::seed_from_u64(s),
201            None => StdRng::from_entropy(),
202        };
203
204        let num_vectors = data.len() / dim;
205        let mut centroids = Vec::with_capacity(k * dim);
206
207        let indices: Vec<usize> = (0..num_vectors).choose_multiple(&mut rng, k);
208        for idx in indices {
209            centroids.extend_from_slice(&data[idx * dim..(idx + 1) * dim]);
210        }
211
212        centroids
213    }
214
215    /// Assign each vector to nearest centroid
216    fn assign_clusters(&self, centroids: &[f32], data: &[f32], dim: usize) -> Vec<usize> {
217        let num_vectors = data.len() / dim;
218        let num_centroids = centroids.len() / dim;
219
220        (0..num_vectors)
221            .map(|i| {
222                let vec = &data[i * dim..(i + 1) * dim];
223                let mut best_idx = 0;
224                let mut best_dist = f32::INFINITY;
225
226                for j in 0..num_centroids {
227                    let centroid = &centroids[j * dim..(j + 1) * dim];
228                    let dist = self.squared_distance(vec, centroid);
229                    if dist < best_dist {
230                        best_dist = dist;
231                        best_idx = j;
232                    }
233                }
234
235                best_idx
236            })
237            .collect()
238    }
239
240    /// Update centroids based on assignments
241    fn update_centroids(
242        &self,
243        assignments: &[usize],
244        data: &[f32],
245        k: usize,
246        dim: usize,
247    ) -> Vec<f32> {
248        let mut sums = vec![0.0f32; k * dim];
249        let mut counts = vec![0usize; k];
250
251        for (i, &cluster) in assignments.iter().enumerate() {
252            counts[cluster] += 1;
253            for d in 0..dim {
254                sums[cluster * dim + d] += data[i * dim + d];
255            }
256        }
257
258        // Compute means
259        for j in 0..k {
260            if counts[j] > 0 {
261                for d in 0..dim {
262                    sums[j * dim + d] /= counts[j] as f32;
263                }
264            }
265        }
266
267        sums
268    }
269
270    /// Compute MSE
271    fn compute_mse(
272        &self,
273        centroids: &[f32],
274        assignments: &[usize],
275        data: &[f32],
276        dim: usize,
277    ) -> f32 {
278        let total_dist: f32 = assignments
279            .iter()
280            .enumerate()
281            .map(|(i, &cluster)| {
282                let vec = &data[i * dim..(i + 1) * dim];
283                let centroid = &centroids[cluster * dim..(cluster + 1) * dim];
284                self.squared_distance(vec, centroid)
285            })
286            .sum();
287
288        total_dist / assignments.len() as f32
289    }
290
291    /// Compute usage distribution
292    fn compute_usage(&self, assignments: &[usize], k: usize) -> (f32, f32, f32) {
293        let mut counts = vec![0usize; k];
294        for &cluster in assignments {
295            counts[cluster] += 1;
296        }
297
298        let min = *counts.iter().min().unwrap_or(&0) as f32;
299        let max = *counts.iter().max().unwrap_or(&0) as f32;
300        let mean = counts.iter().sum::<usize>() as f32 / k as f32;
301
302        (min, max, mean)
303    }
304
305    /// Squared L2 distance
306    fn squared_distance(&self, a: &[f32], b: &[f32]) -> f32 {
307        a.iter().zip(b.iter()).map(|(x, y)| (x - y).powi(2)).sum()
308    }
309
310    /// Train codebooks for all layer types
311    pub fn train_all(
312        &self,
313        model_id: &str,
314        data_by_layer: &[(LayerType, Vec<f32>)],
315    ) -> Result<LayerCodebook> {
316        let mut layer_codebook = LayerCodebook::new(model_id);
317
318        for (layer_type, data) in data_by_layer {
319            let config = layer_type.default_config();
320            let codebook = self.train(config, data)?;
321            layer_codebook.add(*layer_type, codebook);
322        }
323
324        Ok(layer_codebook)
325    }
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331
332    #[test]
333    fn test_trainer() {
334        let config = TrainingConfig {
335            max_iterations: 10,
336            num_restarts: 1,
337            ..Default::default()
338        };
339
340        let trainer = CodebookTrainer::new(config);
341
342        // Create simple test data
343        let codebook_config = CodebookConfig {
344            num_centroids: 4,
345            centroid_dim: 2,
346            index_bits: 2,
347            product_quantization: false,
348            pq_subspaces: 1,
349        };
350
351        // 4 clusters of 2D points
352        let data: Vec<f32> = vec![
353            // Cluster 0
354            0.0, 0.0, 0.1, 0.1, -0.1, 0.1, // Cluster 1
355            1.0, 0.0, 1.1, 0.1, 0.9, -0.1, // Cluster 2
356            0.0, 1.0, 0.1, 1.1, -0.1, 0.9, // Cluster 3
357            1.0, 1.0, 1.1, 1.1, 0.9, 0.9,
358        ];
359
360        let codebook = trainer.train(codebook_config, &data).unwrap();
361
362        assert_eq!(codebook.config.num_centroids, 4);
363        assert!(codebook.stats.mse < 0.1);
364    }
365
366    #[test]
367    fn test_kmeans_pp() {
368        let trainer = CodebookTrainer::new(TrainingConfig::default());
369
370        let data: Vec<f32> = (0..200).map(|i| (i as f32 / 10.0).sin()).collect();
371
372        let centroids = trainer.kmeans_pp_init(10, &data, 2, Some(42));
373        assert_eq!(centroids.len(), 20); // 10 centroids × 2 dimensions
374    }
375}