use linalg::vector::Vector;
use linalg::matrix::Matrix;
use linalg::utils;
use learning::UnSupModel;
use learning::toolkit::rand_utils;
#[derive(Clone, Copy, Debug)]
pub enum CovOption {
Full,
Regularized(f64),
Diagonal,
}
#[derive(Debug)]
pub struct GaussianMixtureModel {
comp_count: usize,
mix_weights: Vector<f64>,
model_means: Option<Matrix<f64>>,
model_covars: Option<Vec<Matrix<f64>>>,
log_lik: f64,
max_iters: usize,
pub cov_option: CovOption,
}
impl UnSupModel<Matrix<f64>, Matrix<f64>> for GaussianMixtureModel {
fn train(&mut self, inputs: &Matrix<f64>) {
let k = self.comp_count;
let mut cov_vec = Vec::with_capacity(k);
for _ in 0..k {
cov_vec.push(Matrix::identity(inputs.cols()));
}
self.model_covars = Some(cov_vec);
let random_rows: Vec<usize> =
rand_utils::reservoir_sample(&(0..inputs.rows()).collect::<Vec<usize>>(), k);
self.model_means = Some(inputs.select_rows(&random_rows));
for _ in 0..self.max_iters {
let log_lik_0 = self.log_lik;
let (weights, log_lik_1) = self.membership_weights(inputs);
if (log_lik_1 - log_lik_0).abs() < 1e-15 {
break;
}
self.log_lik = log_lik_1;
self.update_params(inputs, weights);
}
}
fn predict(&self, inputs: &Matrix<f64>) -> Matrix<f64> {
if let (&Some(_), &Some(_)) = (&self.model_means, &self.model_covars) {
self.membership_weights(inputs).0
} else {
panic!("Model has not been trained.");
}
}
}
impl GaussianMixtureModel {
pub fn new(k: usize) -> GaussianMixtureModel {
GaussianMixtureModel {
comp_count: k,
mix_weights: Vector::ones(k) / (k as f64),
model_means: None,
model_covars: None,
log_lik: 0f64,
max_iters: 100,
cov_option: CovOption::Full,
}
}
pub fn with_weights(k: usize, mixture_weights: Vector<f64>) -> GaussianMixtureModel {
assert!(mixture_weights.size() == k,
"Mixture weights must have length k.");
assert!(!mixture_weights.data().iter().any(|&x| x < 0f64),
"Mixture weights must have only non-negative entries.");
let sum = mixture_weights.sum();
let normalized_weights = mixture_weights / sum;
GaussianMixtureModel {
comp_count: k,
mix_weights: normalized_weights,
model_means: None,
model_covars: None,
log_lik: 0f64,
max_iters: 100,
cov_option: CovOption::Full,
}
}
pub fn means(&self) -> &Option<Matrix<f64>> {
&self.model_means
}
pub fn covariances(&self) -> &Option<Vec<Matrix<f64>>> {
&self.model_covars
}
pub fn mixture_weights(&self) -> &Vector<f64> {
&self.mix_weights
}
pub fn set_max_iters(&mut self, iters: usize) {
self.max_iters = iters;
}
fn membership_weights(&self, inputs: &Matrix<f64>) -> (Matrix<f64>, f64) {
let n = inputs.rows();
let mut member_weights_data = Vec::with_capacity(n * self.comp_count);
let mut cov_sqrt_dets = Vec::with_capacity(self.comp_count);
let mut cov_invs = Vec::with_capacity(self.comp_count);
if let Some(ref covars) = self.model_covars {
for cov in covars {
let covar_det = cov.det();
let covar_inv = cov.inverse();
cov_sqrt_dets.push(covar_det.sqrt());
cov_invs.push(covar_inv);
}
}
let mut log_lik = 0f64;
if let Some(ref means) = self.model_means {
for i in 0..n {
let mut pdfs = Vec::with_capacity(self.comp_count);
let x_i = inputs.select_rows(&[i]);
for j in 0..self.comp_count {
let mu_j = means.select_rows(&[j]);
let diff = &x_i - mu_j;
let pdf = (&diff * &cov_invs[j] * diff.transpose() * -0.5).into_vec()[0]
.exp() / cov_sqrt_dets[j];
pdfs.push(pdf);
}
let weighted_pdf_sum = utils::dot(&pdfs, self.mix_weights.data());
for (idx, pdf) in pdfs.iter().enumerate() {
member_weights_data.push(self.mix_weights[idx] * pdf / (weighted_pdf_sum));
}
log_lik += weighted_pdf_sum.ln();
}
}
(Matrix::new(n, self.comp_count, member_weights_data), log_lik)
}
fn update_params(&mut self, inputs: &Matrix<f64>, membership_weights: Matrix<f64>) {
let n = membership_weights.rows();
let d = inputs.cols();
let sum_weights = membership_weights.sum_rows();
self.mix_weights = &sum_weights / (n as f64);
let mut new_means = membership_weights.transpose() * inputs;
for (idx, mean) in new_means.mut_data().chunks_mut(d).enumerate() {
for m in mean {
*m /= sum_weights[idx];
}
}
let mut new_covs = Vec::with_capacity(self.comp_count);
for k in 0..self.comp_count {
let mut cov_mat = Matrix::zeros(d, d);
for i in 0..n {
let diff = inputs.select_rows(&[i]) - new_means.select_rows(&[k]);
cov_mat = cov_mat + self.compute_cov(diff, membership_weights[[i, k]]);
}
new_covs.push(cov_mat / sum_weights[k]);
}
self.model_means = Some(new_means);
self.model_covars = Some(new_covs);
}
fn compute_cov(&self, diff: Matrix<f64>, weight: f64) -> Matrix<f64> {
match self.cov_option {
CovOption::Full => (diff.transpose() * diff) * weight,
CovOption::Regularized(eps) => (diff.transpose() * diff) * weight + eps,
CovOption::Diagonal => Matrix::from_diag(&diff.elemul(&diff).into_vec()) * weight,
}
}
}
#[cfg(test)]
mod tests {
use super::GaussianMixtureModel;
#[test]
fn test_means_none() {
let model = GaussianMixtureModel::new(5);
assert_eq!(model.means(), &None);
}
#[test]
fn test_covars_none() {
let model = GaussianMixtureModel::new(5);
assert_eq!(model.covariances(), &None);
}
#[test]
#[should_panic]
fn test_negative_mixtures() {
use linalg::vector::Vector;
let mix_weights = Vector::new(vec![-0.25, 0.75, 0.5]);
let _ = GaussianMixtureModel::with_weights(3, mix_weights);
}
#[test]
#[should_panic]
fn test_wrong_length_mixtures() {
use linalg::vector::Vector;
let mix_weights = Vector::new(vec![0.1, 0.25, 0.75, 0.5]);
let _ = GaussianMixtureModel::with_weights(3, mix_weights);
}
}