mod params;
pub use params::Params;
use crate::dlpack::{AsDlTensor, AsDlTensorMut, DLPackError};
use crate::error::{LibraryError, check_cuvs};
use crate::resources::Resources;
type Result<T> = std::result::Result<T, KMeansError>;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum KMeansError {
#[error(transparent)]
Library(#[from] LibraryError),
#[error(transparent)]
DLPack(#[from] DLPackError),
#[error("invalid parameter: {0}")]
Validation(String),
}
pub fn fit<X, W, C>(
res: &Resources,
params: &Params,
x: &X,
sample_weight: Option<&W>,
centroids: &mut C,
) -> Result<(f64, i32)>
where
X: AsDlTensor + ?Sized,
W: AsDlTensor + ?Sized,
C: AsDlTensorMut + ?Sized,
{
let x = x.as_dl_tensor()?;
let sample_weight = sample_weight.map(|w| w.as_dl_tensor()).transpose()?;
let centroids = centroids.as_dl_tensor_mut()?;
let mut inertia: f64 = 0.0;
let mut niter: i32 = 0;
let mut sample_weight_c = sample_weight.as_ref().map(|w| w.to_c());
let sample_weight_ptr =
sample_weight_c.as_mut().map(|w| w.as_mut_ptr()).unwrap_or(std::ptr::null_mut());
unsafe {
check_cuvs(ffi::cuvsKMeansFit(
res.handle(),
params.handle(),
x.to_c().as_mut_ptr(),
sample_weight_ptr,
centroids.to_c().as_mut_ptr(),
&mut inertia as *mut f64,
&mut niter as *mut i32,
))?;
}
Ok((inertia, niter))
}
pub fn predict<X, W, C, L>(
res: &Resources,
params: &Params,
x: &X,
sample_weight: Option<&W>,
centroids: &C,
labels: &mut L,
normalize_weight: bool,
) -> Result<f64>
where
X: AsDlTensor + ?Sized,
W: AsDlTensor + ?Sized,
C: AsDlTensor + ?Sized,
L: AsDlTensorMut + ?Sized,
{
let x = x.as_dl_tensor()?;
let sample_weight = sample_weight.map(|w| w.as_dl_tensor()).transpose()?;
let centroids = centroids.as_dl_tensor()?;
let labels = labels.as_dl_tensor_mut()?;
let mut inertia: f64 = 0.0;
let mut sample_weight_c = sample_weight.as_ref().map(|w| w.to_c());
let sample_weight_ptr =
sample_weight_c.as_mut().map(|w| w.as_mut_ptr()).unwrap_or(std::ptr::null_mut());
unsafe {
check_cuvs(ffi::cuvsKMeansPredict(
res.handle(),
params.handle(),
x.to_c().as_mut_ptr(),
sample_weight_ptr,
centroids.to_c().as_mut_ptr(),
labels.to_c().as_mut_ptr(),
normalize_weight,
&mut inertia as *mut f64,
))?;
}
Ok(inertia)
}
pub fn cluster_cost<X, C>(res: &Resources, x: &X, centroids: &C) -> Result<f64>
where
X: AsDlTensor + ?Sized,
C: AsDlTensor + ?Sized,
{
let x = x.as_dl_tensor()?;
let centroids = centroids.as_dl_tensor()?;
let mut inertia: f64 = 0.0;
unsafe {
check_cuvs(ffi::cuvsKMeansClusterCost(
res.handle(),
x.to_c().as_mut_ptr(),
centroids.to_c().as_mut_ptr(),
&mut inertia as *mut f64,
))?;
}
Ok(inertia)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::DeviceTensor;
use ndarray_rand::RandomExt;
use ndarray_rand::rand_distr::Uniform;
#[test]
fn test_kmeans() {
let res = Resources::new().unwrap();
let n_clusters = 4;
let n_datapoints = 256;
let n_features = 16;
let dataset_host = ndarray::Array::<f32, _>::random(
(n_datapoints, n_features),
Uniform::new(0., 1.0).unwrap(),
);
let dataset = DeviceTensor::from_host(&res, &dataset_host).unwrap();
let centroids_host = ndarray::Array::<f32, _>::zeros((n_clusters, n_features));
let mut centroids = DeviceTensor::from_host(&res, ¢roids_host).unwrap();
let params = Params::builder().n_clusters(n_clusters as i32).build().unwrap();
let original_inertia = cluster_cost(&res, &dataset, ¢roids).unwrap();
let (inertia, n_iter) =
fit(&res, ¶ms, &dataset, None::<&DeviceTensor<'_, f32>>, &mut centroids).unwrap();
assert!(inertia < original_inertia);
assert!(n_iter >= 1);
let mut labels_host = ndarray::Array::<i32, _>::zeros((n_clusters,));
let mut labels = DeviceTensor::<i32>::zeros(&res, &[n_clusters]).unwrap();
predict(
&res,
¶ms,
¢roids,
None::<&DeviceTensor<'_, f32>>,
¢roids,
&mut labels,
false,
)
.unwrap();
labels.copy_to_host(&res, &mut labels_host).unwrap();
assert_eq!(labels_host[[0,]], 0);
assert_eq!(labels_host[[1,]], 1);
assert_eq!(labels_host[[2,]], 2);
assert_eq!(labels_host[[3,]], 3);
}
}