qualia_core_db/solvers/learning/clustering/
gmm.rs1use crate::solvers::learning::LearningError;
12
13#[derive(Debug, Clone)]
15pub struct GmmModel {
16 pub weights: Vec<f64>,
18 pub means: Vec<f64>,
20 pub variances: Vec<f64>,
22 pub labels: Vec<usize>,
24 pub log_likelihood: f64,
25 pub k: usize,
26 pub p: usize,
27 pub n_iter: usize,
28 pub converged: bool,
29}
30
31const VAR_FLOOR: f64 = 1e-6;
32const LN_2PI: f64 = 1.837_877_066_409_345_6; fn log_gauss_diag(x: &[f64], mean: &[f64], var: &[f64], p: usize) -> f64 {
36 let mut s = 0.0;
37 for j in 0..p {
38 let v = var[j].max(VAR_FLOOR);
39 let d = x[j] - mean[j];
40 s += LN_2PI + v.ln() + d * d / v;
41 }
42 -0.5 * s
43}
44
45fn log_sum_exp(values: &[f64]) -> f64 {
46 let m = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
47 if m == f64::NEG_INFINITY {
48 return m;
49 }
50 m + values.iter().map(|&v| (v - m).exp()).sum::<f64>().ln()
51}
52
53pub fn fit(
56 x: &[f64],
57 n: usize,
58 p: usize,
59 k: usize,
60 max_iter: usize,
61 tol: f64,
62 seed: u64,
63) -> Result<GmmModel, LearningError> {
64 if n == 0 || p == 0 || x.len() != n * p {
65 return Err(LearningError::InvalidDimension);
66 }
67 if k == 0 || k > n {
68 return Err(LearningError::InsufficientData);
69 }
70
71 let km = super::kmeans::fit(x, n, p, k, 50, seed)?;
74 let mut means = km.centroids;
75 let mut weights = vec![1.0 / k as f64; k];
76 let mut variances = vec![0.0; k * p];
77 {
78 let mut gmean = vec![0.0; p];
80 for i in 0..n {
81 for j in 0..p {
82 gmean[j] += x[i * p + j];
83 }
84 }
85 for j in 0..p {
86 gmean[j] /= n as f64;
87 }
88 let mut gvar = vec![0.0; p];
89 for i in 0..n {
90 for j in 0..p {
91 let d = x[i * p + j] - gmean[j];
92 gvar[j] += d * d;
93 }
94 }
95 for j in 0..p {
96 gvar[j] = (gvar[j] / n as f64).max(VAR_FLOOR);
97 }
98 for c in 0..k {
99 variances[c * p..(c + 1) * p].copy_from_slice(&gvar);
100 }
101 }
102
103 let mut resp = vec![0.0; n * k]; let mut log_comp = vec![0.0; k];
105 let mut prev_ll = f64::NEG_INFINITY;
106 let mut converged = false;
107 let mut iters = 0;
108
109 for it in 1..=max_iter.max(1) {
110 iters = it;
111 let mut ll = 0.0;
113 for i in 0..n {
114 let xi = &x[i * p..(i + 1) * p];
115 for c in 0..k {
116 log_comp[c] = weights[c].max(1e-300).ln()
117 + log_gauss_diag(
118 xi,
119 &means[c * p..(c + 1) * p],
120 &variances[c * p..(c + 1) * p],
121 p,
122 );
123 }
124 let lse = log_sum_exp(&log_comp);
125 ll += lse;
126 for c in 0..k {
127 resp[i * k + c] = (log_comp[c] - lse).exp();
128 }
129 }
130
131 for c in 0..k {
133 let mut nc = 0.0;
134 for i in 0..n {
135 nc += resp[i * k + c];
136 }
137 let nc_safe = nc.max(1e-300);
138 weights[c] = nc / n as f64;
139 for j in 0..p {
141 let mut s = 0.0;
142 for i in 0..n {
143 s += resp[i * k + c] * x[i * p + j];
144 }
145 means[c * p + j] = s / nc_safe;
146 }
147 for j in 0..p {
149 let mut s = 0.0;
150 for i in 0..n {
151 let d = x[i * p + j] - means[c * p + j];
152 s += resp[i * k + c] * d * d;
153 }
154 variances[c * p + j] = (s / nc_safe).max(VAR_FLOOR);
155 }
156 }
157
158 if (ll - prev_ll).abs() < tol && it > 1 {
159 prev_ll = ll;
160 converged = true;
161 break;
162 }
163 prev_ll = ll;
164 }
165
166 if !converged {
167 return Err(LearningError::NotConverged);
168 }
169
170 let mut labels = vec![0usize; n];
172 for i in 0..n {
173 let xi = &x[i * p..(i + 1) * p];
174 for c in 0..k {
175 log_comp[c] = weights[c].max(1e-300).ln()
176 + log_gauss_diag(
177 xi,
178 &means[c * p..(c + 1) * p],
179 &variances[c * p..(c + 1) * p],
180 p,
181 );
182 }
183 let mut best = 0;
184 for c in 1..k {
185 if log_comp[c] > log_comp[best] {
186 best = c;
187 }
188 }
189 labels[i] = best;
190 }
191
192 Ok(GmmModel {
193 weights,
194 means,
195 variances,
196 labels,
197 log_likelihood: prev_ll,
198 k,
199 p,
200 n_iter: iters,
201 converged,
202 })
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 fn two_blobs() -> (Vec<f64>, usize) {
210 let mut x = Vec::new();
212 for d in 0..8 {
213 let t = (d as f64 - 3.5) * 0.2;
214 x.push(0.0 + t);
215 x.push(0.0 - t);
216 }
217 for d in 0..8 {
218 let t = (d as f64 - 3.5) * 0.2;
219 x.push(8.0 + t);
220 x.push(8.0 + t);
221 }
222 (x, 16)
223 }
224
225 #[test]
226 fn recovers_two_components() {
227 let (x, n) = two_blobs();
228 let m = fit(&x, n, 2, 2, 200, 1e-8, 1).unwrap();
229 assert!(m.converged);
230 assert!((m.weights[0] - 0.5).abs() < 0.1 && (m.weights[1] - 0.5).abs() < 0.1);
232 let l0 = m.labels[0];
234 let l1 = m.labels[8];
235 assert_ne!(l0, l1);
236 assert!((0..8).all(|i| m.labels[i] == l0));
237 assert!((8..16).all(|i| m.labels[i] == l1));
238 let near = |c: usize, tx: f64, ty: f64| {
240 (m.means[c * 2] - tx).abs() < 0.5 && (m.means[c * 2 + 1] - ty).abs() < 0.5
241 };
242 assert!(
243 (near(0, 0.0, 0.0) && near(1, 8.0, 8.0)) || (near(1, 0.0, 0.0) && near(0, 8.0, 8.0))
244 );
245 }
246
247 #[test]
248 fn log_likelihood_is_finite_and_weights_normalised() {
249 let (x, n) = two_blobs();
250 let m = fit(&x, n, 2, 2, 200, 1e-8, 3).unwrap();
251 assert!(m.log_likelihood.is_finite());
252 assert!((m.weights.iter().sum::<f64>() - 1.0).abs() < 1e-9);
253 }
254
255 #[test]
256 fn guards() {
257 assert_eq!(
258 fit(&[1.0, 2.0], 1, 2, 2, 10, 1e-6, 0).unwrap_err(),
259 LearningError::InsufficientData
260 );
261 }
262}