use crate::error::{IrisError, Result};
use burn::tensor::{Int, Tensor, TensorData, backend::Backend};
pub struct KMeans<B: Backend> {
pub k: usize,
pub max_iter: usize,
pub centroids: Option<Tensor<B, 2>>,
}
impl<B: Backend> KMeans<B> {
#[must_use]
pub fn new(k: usize, max_iter: usize) -> Self {
Self {
k,
max_iter,
centroids: None,
}
}
pub fn fit(&mut self, data: &Tensor<B, 2>) -> Result<()> {
let dims = data.dims();
let n = dims[0];
let d = dims[1];
if n < self.k {
return Err(IrisError::InvalidParameter(
"Number of data points must be >= K".into(),
));
}
let device = &data.device();
let initial_centroids = data.clone().slice([0..self.k, 0..d]);
let mut centroids = initial_centroids;
for _ in 0..self.max_iter {
let p_unsqueezed = data.clone().unsqueeze_dim::<3>(1); let c_unsqueezed = centroids.clone().unsqueeze_dim::<3>(0);
let diff = p_unsqueezed.sub(c_unsqueezed); let squared_diff = diff.powf_scalar(2.0);
let dists = squared_diff.sum_dim(2).squeeze::<2>();
let assignments = dists.argmin(1).squeeze::<1>();
let assignments_data = assignments.into_data();
let assignments_vec: Vec<i32> = assignments_data.iter::<i32>().collect();
let data_data = data.clone().into_data();
let flat_data: Vec<f32> = data_data.iter::<f32>().collect();
let mut new_centroids_data = vec![0.0f32; self.k * d];
let mut counts = vec![0.0f32; self.k];
for i in 0..n {
let cluster = assignments_vec[i] as usize;
counts[cluster] += 1.0;
for j in 0..d {
new_centroids_data[cluster * d + j] += flat_data[i * d + j];
}
}
for k in 0..self.k {
let count = counts[k].max(1.0);
for j in 0..d {
new_centroids_data[k * d + j] /= count;
}
}
centroids =
Tensor::<B, 2>::from_data(TensorData::new(new_centroids_data, [self.k, d]), device);
}
self.centroids = Some(centroids);
Ok(())
}
pub fn predict(&self, data: &Tensor<B, 2>) -> Result<Tensor<B, 1, Int>> {
let centroids = self.centroids.as_ref().ok_or_else(|| {
IrisError::Generic("K-Means centroids are not initialized. Fit the model first.".into())
})?;
let p_unsqueezed = data.clone().unsqueeze_dim::<3>(1); let c_unsqueezed = centroids.clone().unsqueeze_dim::<3>(0);
let diff = p_unsqueezed.sub(c_unsqueezed); let squared_diff = diff.powf_scalar(2.0);
let dists = squared_diff.sum_dim(2).squeeze::<2>();
let assignments = dists.argmin(1).squeeze::<1>(); Ok(assignments)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::{TestBackend, test_device};
#[test]
fn test_kmeans_clustering() {
let device = test_device();
let data = Tensor::<TestBackend, 2>::from_data(
TensorData::new(vec![1.0f32, 1.0, 1.1, 1.1, 10.0, 10.0, 10.2, 10.2], [4, 2]),
&device,
);
let mut km = KMeans::new(2, 5);
km.fit(&data).unwrap();
assert!(km.centroids.is_some());
let assignments = km.predict(&data).unwrap();
assert_eq!(assignments.dims(), [4]);
}
}