Skip to main content

sklears_clustering/gmm/
em_algorithm.rs

1//! EM Algorithm Implementation for Gaussian Mixture Models
2//!
3//! This module provides the core Expectation-Maximization algorithm implementation
4//! with SIMD acceleration, supporting various covariance types and initialization
5//! strategies for both classical and Bayesian GMM variants.
6
7use scirs2_core::ndarray::{Array1, Array2, ArrayView2, Axis};
8use sklears_core::{error::Result, types::Float};
9
10use super::simd_operations::*;
11use super::types_config::{CovarianceType, WeightInit};
12
13/// GMM parameters tuple: (weights, means, covariances)
14type GmmParams = (Array1<Float>, Array2<Float>, Vec<Array2<Float>>);
15
16/// Core EM Algorithm implementation with SIMD acceleration
17pub struct EMAlgorithm {
18    /// Maximum number of EM iterations
19    pub max_iter: usize,
20    /// Convergence tolerance for log-likelihood change
21    pub tol: Float,
22    /// Regularization added to covariance diagonal for numerical stability
23    pub reg_covar: Float,
24    /// Type of covariance matrix to use
25    pub covariance_type: CovarianceType,
26    /// Initialization strategy for component weights
27    pub init_params: WeightInit,
28    /// Seed for reproducible random initialization
29    pub random_state: Option<u64>,
30}
31
32impl EMAlgorithm {
33    /// Create a new EM algorithm instance
34    pub fn new(max_iter: usize, tol: Float, reg_covar: Float) -> Self {
35        Self {
36            max_iter,
37            tol,
38            reg_covar,
39            covariance_type: CovarianceType::Full,
40            init_params: WeightInit::KMeans,
41            random_state: None,
42        }
43    }
44
45    /// Set covariance type
46    pub fn covariance_type(mut self, covariance_type: CovarianceType) -> Self {
47        self.covariance_type = covariance_type;
48        self
49    }
50
51    /// Set initialization method
52    pub fn init_params(mut self, init_params: WeightInit) -> Self {
53        self.init_params = init_params;
54        self
55    }
56
57    /// Set random state
58    pub fn random_state(mut self, seed: u64) -> Self {
59        self.random_state = Some(seed);
60        self
61    }
62
63    /// Run the complete EM algorithm
64    pub fn fit(&self, x: &ArrayView2<Float>, n_components: usize) -> Result<EMResult> {
65        // Initialize parameters using SIMD-accelerated operations
66        let (mut weights, mut means, mut covariances) =
67            self.initialize_parameters(x, n_components)?;
68
69        let mut log_likelihood = Float::NEG_INFINITY;
70        let mut converged = false;
71        let mut n_iter = 0;
72
73        // Main EM algorithm loop with SIMD acceleration
74        for iteration in 0..self.max_iter {
75            n_iter = iteration + 1;
76
77            // E-step: Compute responsibilities using SIMD operations
78            let responsibilities = self.e_step_simd(x, &weights, &means, &covariances)?;
79
80            // M-step: Update parameters using SIMD operations
81            let (new_weights, new_means, new_covariances) =
82                self.m_step_simd(x, &responsibilities)?;
83
84            // Compute log-likelihood with SIMD acceleration
85            let new_log_likelihood =
86                self.compute_log_likelihood_simd(x, &new_weights, &new_means, &new_covariances)?;
87
88            // Check convergence using SIMD operations
89            if simd_check_convergence(log_likelihood, new_log_likelihood, self.tol) {
90                converged = true;
91                weights = new_weights;
92                means = new_means;
93                covariances = new_covariances;
94                log_likelihood = new_log_likelihood;
95                break;
96            }
97
98            weights = new_weights;
99            means = new_means;
100            covariances = new_covariances;
101            log_likelihood = new_log_likelihood;
102        }
103
104        // Apply final regularization using SIMD operations
105        for cov in &mut covariances {
106            simd_regularize_covariance(cov, self.reg_covar);
107        }
108
109        Ok(EMResult {
110            weights,
111            means,
112            covariances,
113            converged,
114            n_iter,
115            log_likelihood,
116        })
117    }
118
119    /// Initialize GMM parameters with SIMD-accelerated operations
120    pub fn initialize_parameters(
121        &self,
122        x: &ArrayView2<Float>,
123        n_components: usize,
124    ) -> Result<GmmParams> {
125        // Initialize weights uniformly
126        let weights = Array1::from_elem(n_components, 1.0 / n_components as Float);
127
128        // Initialize means using specified strategy
129        let means = self.initialize_means(x, n_components)?;
130
131        // Initialize covariances based on covariance type
132        let covariances = self.initialize_covariances(x, &means, n_components)?;
133
134        Ok((weights, means, covariances))
135    }
136
137    /// Initialize means using K-means++ or random strategy
138    fn initialize_means(
139        &self,
140        x: &ArrayView2<Float>,
141        n_components: usize,
142    ) -> Result<Array2<Float>> {
143        let n_samples = x.nrows();
144        let n_features = x.ncols();
145        let mut means = Array2::zeros((n_components, n_features));
146
147        match self.init_params {
148            WeightInit::KMeans => {
149                // K-means++ initialization for better convergence
150                let mut selected = Vec::new();
151
152                // Choose first center randomly or deterministically
153                let first_idx = if let Some(seed) = self.random_state {
154                    (seed as usize) % n_samples
155                } else {
156                    0
157                };
158                means.row_mut(0).assign(&x.row(first_idx));
159                selected.push(first_idx);
160
161                // Choose remaining centers with probability proportional to squared distance
162                for i in 1..n_components {
163                    let mut distances = Array1::zeros(n_samples);
164
165                    // Compute distances to nearest existing center using SIMD
166                    for (j, sample) in x.outer_iter().enumerate() {
167                        let mut min_dist = Float::INFINITY;
168                        for &sel_idx in &selected {
169                            let center = x.row(sel_idx);
170                            let dist = simd_euclidean_distance_squared(&sample, &center);
171                            if dist < min_dist {
172                                min_dist = dist;
173                            }
174                        }
175                        distances[j] = min_dist;
176                    }
177
178                    // Select center with probability proportional to squared distance
179                    let sum_dist = distances.sum();
180                    if sum_dist > 0.0 {
181                        let mut cumsum = 0.0;
182                        let target = self.generate_random_float(i as u64) * sum_dist;
183
184                        for (j, &dist) in distances.iter().enumerate() {
185                            cumsum += dist;
186                            if cumsum >= target {
187                                means.row_mut(i).assign(&x.row(j));
188                                selected.push(j);
189                                break;
190                            }
191                        }
192                    } else {
193                        // Fallback to uniform sampling
194                        let idx = i % n_samples;
195                        means.row_mut(i).assign(&x.row(idx));
196                        selected.push(idx);
197                    }
198                }
199            }
200            WeightInit::Random => {
201                // Simple random initialization
202                for i in 0..n_components {
203                    let idx = if let Some(seed) = self.random_state {
204                        ((seed as usize) + i) % n_samples
205                    } else {
206                        i * n_samples / n_components
207                    };
208                    means.row_mut(i).assign(&x.row(idx));
209                }
210            }
211        }
212
213        Ok(means)
214    }
215
216    /// Initialize covariances based on covariance type
217    fn initialize_covariances(
218        &self,
219        x: &ArrayView2<Float>,
220        _means: &Array2<Float>,
221        n_components: usize,
222    ) -> Result<Vec<Array2<Float>>> {
223        let n_features = x.ncols();
224        let mut covariances = Vec::new();
225
226        match self.covariance_type {
227            CovarianceType::Full => {
228                // Initialize with scaled identity matrices
229                let data_var = x.var_axis(Axis(0), 0.0);
230                let scale = data_var.mean().unwrap_or(1.0);
231
232                for _ in 0..n_components {
233                    let mut cov = Array2::eye(n_features) * scale;
234                    simd_regularize_covariance(&mut cov, self.reg_covar);
235                    covariances.push(cov);
236                }
237            }
238            CovarianceType::Diagonal => {
239                let data_var = x.var_axis(Axis(0), 0.0);
240
241                for _ in 0..n_components {
242                    let mut cov = Array2::zeros((n_features, n_features));
243                    for i in 0..n_features {
244                        cov[[i, i]] = data_var[i].max(self.reg_covar);
245                    }
246                    covariances.push(cov);
247                }
248            }
249            CovarianceType::Tied => {
250                // All components share the same covariance
251                let data_cov = self.compute_sample_covariance(x);
252                let mut cov = data_cov;
253                simd_regularize_covariance(&mut cov, self.reg_covar);
254
255                for _ in 0..n_components {
256                    covariances.push(cov.clone());
257                }
258            }
259            CovarianceType::Spherical => {
260                let data_var = x.var_axis(Axis(0), 0.0);
261                let avg_var = data_var.mean().unwrap_or(1.0);
262
263                for _ in 0..n_components {
264                    let mut cov = Array2::zeros((n_features, n_features));
265                    for i in 0..n_features {
266                        cov[[i, i]] = avg_var.max(self.reg_covar);
267                    }
268                    covariances.push(cov);
269                }
270            }
271        }
272
273        Ok(covariances)
274    }
275
276    /// E-step: Compute responsibilities using SIMD acceleration
277    pub fn e_step_simd(
278        &self,
279        x: &ArrayView2<Float>,
280        weights: &Array1<Float>,
281        means: &Array2<Float>,
282        covariances: &[Array2<Float>],
283    ) -> Result<Array2<Float>> {
284        let n_samples = x.nrows();
285        let n_components = weights.len();
286        let mut responsibilities = Array2::zeros((n_samples, n_components));
287
288        for (i, sample) in x.outer_iter().enumerate() {
289            let mut log_probs = Array1::zeros(n_components);
290
291            for k in 0..n_components {
292                let mean = means.row(k);
293                let cov = &covariances[k];
294
295                // Extract diagonal for SIMD multivariate normal computation
296                let inv_diag = self.extract_diagonal_inverse(cov)?;
297                let log_det = simd_log_determinant(&cov.view());
298
299                // Compute log probability density using SIMD operations
300                let log_prob = weights[k].ln()
301                    + simd_multivariate_normal_log_density(
302                        &sample,
303                        &mean,
304                        &inv_diag.view(),
305                        log_det,
306                    );
307
308                log_probs[k] = log_prob;
309            }
310
311            // Normalize using SIMD log-sum-exp trick for numerical stability
312            let log_sum = simd_log_sum_exp(&log_probs.view());
313            for k in 0..n_components {
314                responsibilities[[i, k]] = (log_probs[k] - log_sum).exp();
315            }
316        }
317
318        Ok(responsibilities)
319    }
320
321    /// M-step: Update parameters using SIMD acceleration
322    pub fn m_step_simd(
323        &self,
324        x: &ArrayView2<Float>,
325        responsibilities: &Array2<Float>,
326    ) -> Result<GmmParams> {
327        let n_samples = x.nrows();
328        let n_features = x.ncols();
329        let n_components = responsibilities.ncols();
330
331        // Compute effective number of samples for each component using SIMD
332        let nk: Array1<Float> = responsibilities.sum_axis(Axis(0));
333
334        // Update weights using SIMD acceleration
335        let weights = &nk / n_samples as Float;
336
337        // Update means using SIMD weighted sum operations
338        let mut means = Array2::zeros((n_components, n_features));
339        for k in 0..n_components {
340            if nk[k] > 1e-12 {
341                // Use SIMD-accelerated weighted sum
342                for i in 0..n_samples {
343                    means
344                        .row_mut(k)
345                        .scaled_add(responsibilities[[i, k]], &x.row(i));
346                }
347                means.row_mut(k).mapv_inplace(|x| x / nk[k]);
348            }
349        }
350
351        // Update covariances using SIMD operations based on covariance type
352        let covariances = self.update_covariances_simd(x, &means, responsibilities, &nk)?;
353
354        Ok((weights, means, covariances))
355    }
356
357    /// Update covariances with SIMD acceleration
358    fn update_covariances_simd(
359        &self,
360        x: &ArrayView2<Float>,
361        means: &Array2<Float>,
362        responsibilities: &Array2<Float>,
363        nk: &Array1<Float>,
364    ) -> Result<Vec<Array2<Float>>> {
365        let n_samples = x.nrows();
366        let n_features = x.ncols();
367        let n_components = means.nrows();
368        let mut covariances = Vec::new();
369
370        match self.covariance_type {
371            CovarianceType::Full => {
372                for k in 0..n_components {
373                    let cov = if nk[k] > 1e-12 {
374                        simd_covariance_matrix(
375                            &x.view(),
376                            &responsibilities.column(k).view(),
377                            &means.row(k).view(),
378                        )
379                    } else {
380                        Array2::eye(n_features)
381                    };
382
383                    let mut regularized_cov = cov;
384                    simd_regularize_covariance(&mut regularized_cov, self.reg_covar);
385                    covariances.push(regularized_cov);
386                }
387            }
388            CovarianceType::Diagonal => {
389                for k in 0..n_components {
390                    let diag_cov = if nk[k] > 1e-12 {
391                        simd_diagonal_covariance(
392                            &x.view(),
393                            &responsibilities.column(k).view(),
394                            &means.row(k).view(),
395                        )
396                    } else {
397                        Array1::from_elem(n_features, self.reg_covar)
398                    };
399
400                    let mut cov = Array2::zeros((n_features, n_features));
401                    for j in 0..n_features {
402                        cov[[j, j]] = diag_cov[j].max(self.reg_covar);
403                    }
404                    covariances.push(cov);
405                }
406            }
407            CovarianceType::Tied => {
408                // Compute shared covariance using SIMD operations
409                let mut shared_cov = Array2::zeros((n_features, n_features));
410
411                for k in 0..n_components {
412                    if nk[k] > 1e-12 {
413                        let component_cov = simd_covariance_matrix(
414                            &x.view(),
415                            &responsibilities.column(k).view(),
416                            &means.row(k).view(),
417                        );
418                        shared_cov.scaled_add(nk[k] / n_samples as Float, &component_cov);
419                    }
420                }
421
422                simd_regularize_covariance(&mut shared_cov, self.reg_covar);
423
424                for _ in 0..n_components {
425                    covariances.push(shared_cov.clone());
426                }
427            }
428            CovarianceType::Spherical => {
429                for k in 0..n_components {
430                    let avg_var = if nk[k] > 1e-12 {
431                        let diag_cov = simd_diagonal_covariance(
432                            &x.view(),
433                            &responsibilities.column(k).view(),
434                            &means.row(k).view(),
435                        );
436                        diag_cov
437                            .mean()
438                            .unwrap_or(self.reg_covar)
439                            .max(self.reg_covar)
440                    } else {
441                        self.reg_covar
442                    };
443
444                    let mut cov = Array2::zeros((n_features, n_features));
445                    for j in 0..n_features {
446                        cov[[j, j]] = avg_var;
447                    }
448                    covariances.push(cov);
449                }
450            }
451        }
452
453        Ok(covariances)
454    }
455
456    /// Compute log-likelihood using SIMD acceleration
457    pub fn compute_log_likelihood_simd(
458        &self,
459        x: &ArrayView2<Float>,
460        weights: &Array1<Float>,
461        means: &Array2<Float>,
462        covariances: &[Array2<Float>],
463    ) -> Result<Float> {
464        let mut log_likelihood = 0.0;
465
466        for sample in x.outer_iter() {
467            let mut sample_likelihood = 0.0;
468
469            for k in 0..weights.len() {
470                let mean = means.row(k);
471                let cov = &covariances[k];
472
473                let inv_diag = self.extract_diagonal_inverse(cov)?;
474                let log_det = simd_log_determinant(&cov.view());
475
476                let log_prob =
477                    simd_multivariate_normal_log_density(&sample, &mean, &inv_diag.view(), log_det);
478
479                sample_likelihood += weights[k] * log_prob.exp();
480            }
481
482            if sample_likelihood > 1e-12 {
483                log_likelihood += sample_likelihood.ln();
484            }
485        }
486
487        Ok(log_likelihood)
488    }
489
490    /// Extract diagonal inverse for SIMD operations
491    fn extract_diagonal_inverse(&self, cov: &Array2<Float>) -> Result<Array1<Float>> {
492        let mut inv_diag = Array1::zeros(cov.nrows());
493        for i in 0..cov.nrows() {
494            let diag_val = cov[[i, i]];
495            if diag_val <= 1e-12 {
496                return Err(sklears_core::error::SklearsError::Other(
497                    "Singular covariance matrix".to_string(),
498                ));
499            }
500            inv_diag[i] = 1.0 / diag_val;
501        }
502        Ok(inv_diag)
503    }
504
505    /// Compute sample covariance matrix
506    fn compute_sample_covariance(&self, x: &ArrayView2<Float>) -> Array2<Float> {
507        let n_features = x.ncols();
508        let mean = x.mean_axis(Axis(0)).expect("operation should succeed");
509        let mut cov = Array2::zeros((n_features, n_features));
510
511        for sample in x.outer_iter() {
512            let diff = &sample - &mean;
513            for i in 0..n_features {
514                for j in 0..n_features {
515                    cov[[i, j]] += diff[i] * diff[j];
516                }
517            }
518        }
519
520        cov /= (x.nrows() - 1) as Float;
521        cov
522    }
523
524    /// Generate pseudo-random float for deterministic randomness
525    fn generate_random_float(&self, seed_offset: u64) -> Float {
526        if let Some(base_seed) = self.random_state {
527            let seed = base_seed.wrapping_add(seed_offset);
528            // Simple linear congruential generator for deterministic randomness
529            let a = 1103515245_u64;
530            let c = 12345_u64;
531            let m = 2_u64.pow(31);
532            let next = (a.wrapping_mul(seed).wrapping_add(c)) % m;
533            next as Float / m as Float
534        } else {
535            0.5 // Default deterministic value
536        }
537    }
538}
539
540/// Result of EM algorithm execution
541#[derive(Debug, Clone)]
542pub struct EMResult {
543    /// Component mixing weights
544    pub weights: Array1<Float>,
545    /// Component means (n_components × n_features)
546    pub means: Array2<Float>,
547    /// Component covariance matrices
548    pub covariances: Vec<Array2<Float>>,
549    /// Whether the algorithm converged within `max_iter`
550    pub converged: bool,
551    /// Number of EM iterations performed
552    pub n_iter: usize,
553    /// Final log-likelihood value
554    pub log_likelihood: Float,
555}
556
557#[allow(non_snake_case)]
558#[cfg(test)]
559mod tests {
560    use super::*;
561    use scirs2_core::ndarray::array;
562
563    #[test]
564    fn test_em_algorithm() {
565        let x = array![
566            [0.0, 0.0],
567            [0.1, 0.1],
568            [0.2, 0.0],
569            [5.0, 5.0],
570            [5.1, 5.1],
571            [5.2, 5.0],
572        ];
573
574        let em = EMAlgorithm::new(100, 1e-3, 1e-6)
575            .covariance_type(CovarianceType::Diagonal)
576            .init_params(WeightInit::KMeans);
577
578        let result = em.fit(&x.view(), 2).expect("operation should succeed");
579
580        assert_eq!(result.weights.len(), 2);
581        assert_eq!(result.means.nrows(), 2);
582        assert_eq!(result.covariances.len(), 2);
583        assert!(result.n_iter > 0);
584        assert!(result.log_likelihood.is_finite());
585    }
586
587    #[test]
588    fn test_parameter_initialization() {
589        let x = array![[0.0, 0.0], [1.0, 1.0], [2.0, 2.0], [3.0, 3.0],];
590
591        let em = EMAlgorithm::new(100, 1e-3, 1e-6)
592            .covariance_type(CovarianceType::Full)
593            .init_params(WeightInit::KMeans);
594
595        let (weights, means, covariances) = em
596            .initialize_parameters(&x.view(), 2)
597            .expect("operation should succeed");
598
599        assert_eq!(weights.len(), 2);
600        assert_eq!(means.nrows(), 2);
601        assert_eq!(means.ncols(), 2);
602        assert_eq!(covariances.len(), 2);
603
604        // Check that weights sum to 1
605        assert!((weights.sum() - 1.0).abs() < 1e-10);
606
607        // Check covariance matrix properties
608        for cov in &covariances {
609            assert_eq!(cov.nrows(), 2);
610            assert_eq!(cov.ncols(), 2);
611            // Check positive definiteness (diagonal elements should be positive)
612            for i in 0..2 {
613                assert!(cov[[i, i]] > 0.0);
614            }
615        }
616    }
617
618    #[test]
619    fn test_convergence_check() {
620        let old_ll = -100.0;
621        let new_ll = -99.999;
622        let tol = 1e-3;
623
624        assert!(simd_check_convergence(old_ll, new_ll, tol));
625
626        let new_ll_no_conv = -99.0;
627        assert!(!simd_check_convergence(old_ll, new_ll_no_conv, tol));
628    }
629}