use crate::error::Error;
pub use crate::machine_learning::DistanceCalculationMetric;
use crate::machine_learning::spatial::KdTree;
use crate::machine_learning::validation::{
check_is_fitted, preliminary_check, validate_predict_input,
};
use crate::math::matmul::{cache_resident, gemm_chunk_rows, gemm_par_auto, gemv_par_switch};
use crate::{Deserialize, Serialize};
use ahash::AHashMap;
use ndarray::{Array1, Array2, ArrayBase, ArrayView1, ArrayView2, Axis, Data, Ix1, Ix2, s};
use rayon::prelude::{IntoParallelIterator, ParallelIterator};
use std::sync::OnceLock;
const KNN_KD_TREE_MAX_DIMS: usize = 8;
fn select_top_class<T>(scores: &AHashMap<usize, T>) -> Option<usize>
where
T: PartialOrd + Copy,
{
scores
.iter()
.max_by(|a, b| {
let (sa, sb): (T, T) = (*a.1, *b.1);
sa.partial_cmp(&sb)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| b.0.cmp(a.0)) })
.map(|(&idx, _)| idx)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum WeightingStrategy {
#[default]
Uniform,
Distance,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KNN<T> {
k: usize,
x_train: Option<Array2<f64>>,
y_train_encoded: Option<Array1<usize>>,
#[serde(bound(
serialize = "T: Serialize + Eq + std::hash::Hash",
deserialize = "T: Deserialize<'de> + Eq + std::hash::Hash"
))]
label_map: Option<(AHashMap<T, usize>, Vec<T>)>,
weighting_strategy: WeightingStrategy,
metric: DistanceCalculationMetric,
#[serde(skip)]
tree: OnceLock<Option<KdTree>>,
}
impl<T: Clone + std::hash::Hash + Eq> Default for KNN<T> {
fn default() -> Self {
KNN {
k: 5,
x_train: None,
y_train_encoded: None,
label_map: None,
weighting_strategy: WeightingStrategy::Uniform,
metric: DistanceCalculationMetric::Euclidean,
tree: OnceLock::new(),
}
}
}
impl<T: Clone + std::hash::Hash + Eq> KNN<T> {
pub fn new(k: usize) -> Result<Self, Error> {
if k == 0 {
return Err(Error::invalid_parameter("k", "must be greater than 0"));
}
Ok(KNN {
k,
x_train: None,
y_train_encoded: None,
label_map: None,
weighting_strategy: WeightingStrategy::Uniform,
metric: DistanceCalculationMetric::Euclidean,
tree: OnceLock::new(),
})
}
pub fn with_weighting_strategy(mut self, weighting_strategy: WeightingStrategy) -> Self {
self.weighting_strategy = weighting_strategy;
self
}
pub fn with_metric(mut self, metric: DistanceCalculationMetric) -> Result<Self, Error> {
if let DistanceCalculationMetric::Minkowski(p) = metric
&& (p < 1.0 || !p.is_finite())
{
return Err(Error::invalid_parameter(
"p",
format!("Minkowski p must be at least 1 and finite, got {}", p),
));
}
self.metric = metric;
Ok(self)
}
get_field!(get_k, k, usize);
get_field!(
get_weighting_strategy,
weighting_strategy,
WeightingStrategy
);
get_field!(get_metric, metric, DistanceCalculationMetric);
get_field_as_ref!(get_x_train, x_train, Option<&Array2<f64>>);
pub fn fit<S1, S2>(
&mut self,
x: &ArrayBase<S1, Ix2>,
y: &ArrayBase<S2, Ix1>,
) -> Result<&mut Self, Error>
where
S1: Data<Elem = f64>,
S2: Data<Elem = T>,
{
preliminary_check(x, None)?;
if y.len() != x.nrows() {
return Err(Error::dimension_mismatch(x.nrows(), y.len()));
}
if x.nrows() < self.k {
return Err(Error::invalid_input("The number of samples is less than k"));
}
let mut label_to_idx: AHashMap<T, usize> = AHashMap::new();
let mut idx_to_label: Vec<T> = Vec::new();
let mut next_idx = 0;
let mut encoded_labels = Vec::with_capacity(y.len());
for label in y.iter() {
let idx = if let Some(&existing_idx) = label_to_idx.get(label) {
existing_idx
} else {
let new_idx = next_idx;
label_to_idx.insert(label.clone(), new_idx);
idx_to_label.push(label.clone());
next_idx += 1;
new_idx
};
encoded_labels.push(idx);
}
self.x_train = Some(x.to_owned());
self.y_train_encoded = Some(Array1::from(encoded_labels));
self.label_map = Some((label_to_idx, idx_to_label));
self.tree = OnceLock::new();
Ok(self)
}
pub fn predict<S>(&self, x: &ArrayBase<S, Ix2>) -> Result<Array1<T>, Error>
where
S: Data<Elem = f64>,
{
check_is_fitted(
self.x_train.is_some() && self.y_train_encoded.is_some() && self.label_map.is_some(),
"KNN",
)?;
let x_train = self.x_train.as_ref().unwrap();
validate_predict_input(x, x_train.ncols())?;
let y_train_encoded = self.y_train_encoded.as_ref().unwrap();
let (_, idx_to_label) = self.label_map.as_ref().unwrap();
let tree = self.neighbor_tree(x_train.view());
let train_sq_norms = if tree.is_some() {
None
} else {
self.euclidean_train_sq_norms(x_train)
};
let encoded_results: Result<Vec<usize>, Error> = if let Some(train_sq_norms) =
train_sq_norms.as_ref()
{
if cache_resident::<f64>(x_train.nrows(), x_train.ncols()) {
(0..x.nrows())
.map(|i| {
self.predict_one(
x.row(i),
x_train.view(),
y_train_encoded,
Some((train_sq_norms, None)),
tree,
)
})
.collect()
} else {
let chunk_rows = gemm_chunk_rows(x_train.nrows());
let mut encoded = Vec::with_capacity(x.nrows());
for chunk_start in (0..x.nrows()).step_by(chunk_rows) {
let chunk_end = (chunk_start + chunk_rows).min(x.nrows());
let projections =
gemm_par_auto(&x.slice(s![chunk_start..chunk_end, ..]), &x_train.t());
for i in chunk_start..chunk_end {
encoded.push(self.predict_one(
x.row(i),
x_train.view(),
y_train_encoded,
Some((train_sq_norms, Some(projections.row(i - chunk_start)))),
tree,
)?);
}
}
Ok(encoded)
}
} else {
(0..x.nrows())
.map(|i| self.predict_one(x.row(i), x_train.view(), y_train_encoded, None, tree))
.collect()
};
encoded_results.map(|encoded_preds| {
Array1::from(
encoded_preds
.into_iter()
.map(|idx| idx_to_label[idx].clone())
.collect::<Vec<_>>(),
)
})
}
}
impl<T: Clone + std::hash::Hash + Eq + Sync + Send> KNN<T> {
pub fn predict_parallel<S>(&self, x: &ArrayBase<S, Ix2>) -> Result<Array1<T>, Error>
where
S: Data<Elem = f64> + Send + Sync,
{
check_is_fitted(
self.x_train.is_some() && self.y_train_encoded.is_some() && self.label_map.is_some(),
"KNN",
)?;
let x_train = self.x_train.as_ref().unwrap();
validate_predict_input(x, x_train.ncols())?;
let y_train_encoded = self.y_train_encoded.as_ref().unwrap();
let (_, idx_to_label) = self.label_map.as_ref().unwrap();
let tree = self.neighbor_tree(x_train.view());
let train_sq_norms = if tree.is_some() {
None
} else {
self.euclidean_train_sq_norms(x_train)
};
let encoded_results: Result<Vec<usize>, Error> = if let Some(train_sq_norms) =
train_sq_norms.as_ref()
{
if cache_resident::<f64>(x_train.nrows(), x_train.ncols()) {
(0..x.nrows())
.into_par_iter()
.map(|i| {
self.predict_one(
x.row(i),
x_train.view(),
y_train_encoded,
Some((train_sq_norms, None)),
tree,
)
})
.collect()
} else {
let chunk_rows = gemm_chunk_rows(x_train.nrows());
let mut encoded = Vec::with_capacity(x.nrows());
for chunk_start in (0..x.nrows()).step_by(chunk_rows) {
let chunk_end = (chunk_start + chunk_rows).min(x.nrows());
let projections =
gemm_par_auto(&x.slice(s![chunk_start..chunk_end, ..]), &x_train.t());
let chunk_results: Result<Vec<usize>, Error> = (chunk_start..chunk_end)
.into_par_iter()
.map(|i| {
self.predict_one(
x.row(i),
x_train.view(),
y_train_encoded,
Some((train_sq_norms, Some(projections.row(i - chunk_start)))),
tree,
)
})
.collect();
encoded.extend(chunk_results?);
}
Ok(encoded)
}
} else {
(0..x.nrows())
.into_par_iter()
.map(|i| self.predict_one(x.row(i), x_train.view(), y_train_encoded, None, tree))
.collect()
};
encoded_results.map(|encoded_preds| {
Array1::from(
encoded_preds
.into_iter()
.map(|idx| idx_to_label[idx].clone())
.collect::<Vec<_>>(),
)
})
}
}
impl<T: Clone + std::hash::Hash + Eq> KNN<T> {
fn euclidean_train_sq_norms<S>(&self, x_train: &ArrayBase<S, Ix2>) -> Option<Array1<f64>>
where
S: Data<Elem = f64>,
{
match self.metric {
DistanceCalculationMetric::Euclidean => {
Some(x_train.map_axis(Axis(1), |row| row.dot(&row)))
}
_ => None,
}
}
fn neighbor_tree(&self, x_train: ArrayView2<f64>) -> Option<&KdTree> {
self.tree
.get_or_init(|| {
if x_train.ncols() <= KNN_KD_TREE_MAX_DIMS {
Some(KdTree::build(x_train, self.metric))
} else {
None
}
})
.as_ref()
}
fn predict_one(
&self,
x: ArrayView1<f64>,
x_train: ArrayView2<f64>,
y_train_encoded: &Array1<usize>,
euclidean_fast: Option<(&Array1<f64>, Option<ArrayView1<f64>>)>,
tree: Option<&KdTree>,
) -> Result<usize, Error> {
let n_samples = x_train.nrows();
let k = self.k.min(n_samples);
let k_neighbors_owned: Vec<(f64, usize)> = if let Some(tree) = tree {
tree.k_nearest(x, k)
.into_iter()
.map(|(idx, cmp)| (self.metric.distance_from_comparable(cmp), idx))
.collect()
} else {
let mut distances: Vec<(f64, usize)> = match euclidean_fast {
Some((train_sq_norms, precomputed)) => {
let x_sq = x.dot(&x);
let projections_owned;
let projections = match precomputed {
Some(p) => p,
None => {
projections_owned = gemv_par_switch(&x_train, &x, false);
projections_owned.view()
}
};
projections
.iter()
.zip(train_sq_norms.iter())
.enumerate()
.map(|(i, (&proj, &t_sq))| {
let dist_sq = (x_sq + t_sq - 2.0 * proj).max(0.0);
(dist_sq.sqrt(), i)
})
.collect()
}
None => (0..n_samples)
.map(|i| (self.metric.distance(x, x_train.row(i)), i))
.collect(),
};
if k < distances.len() {
distances
.select_nth_unstable_by(k - 1, |a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)));
}
distances.truncate(k);
distances
};
let k_neighbors = &k_neighbors_owned[..];
let result = match self.weighting_strategy {
WeightingStrategy::Uniform => {
let mut class_counts: AHashMap<usize, usize> = AHashMap::with_capacity(k);
for &(_, idx) in k_neighbors {
*class_counts.entry(y_train_encoded[idx]).or_insert(0) += 1;
}
select_top_class(&class_counts).ok_or_else(|| {
Error::computation("No valid neighbors found for classification")
})?
}
WeightingStrategy::Distance => {
let exact_matches: AHashMap<usize, usize> = k_neighbors
.iter()
.filter(|&&(distance, _)| distance == 0.0)
.fold(AHashMap::new(), |mut acc, &(_, idx)| {
*acc.entry(y_train_encoded[idx]).or_insert(0) += 1;
acc
});
if !exact_matches.is_empty() {
select_top_class(&exact_matches).ok_or_else(|| {
Error::computation("No valid neighbors found for classification")
})?
} else {
let mut class_weights: AHashMap<usize, f64> = AHashMap::with_capacity(k);
for &(distance, idx) in k_neighbors {
*class_weights.entry(y_train_encoded[idx]).or_insert(0.0) += 1.0 / distance;
}
select_top_class(&class_weights).ok_or_else(|| {
Error::computation("No valid neighbors found for classification")
})?
}
}
};
Ok(result)
}
pub fn fit_predict<S1, S2>(
&mut self,
x_train: &ArrayBase<S1, Ix2>,
y_train: &ArrayBase<S2, Ix1>,
) -> Result<Array1<T>, Error>
where
S1: Data<Elem = f64>,
S2: Data<Elem = T>,
{
self.fit(x_train, y_train)?;
self.predict(x_train)
}
}
impl<T: Clone + std::hash::Hash + Eq + Serialize + for<'de> Deserialize<'de>> KNN<T> {
model_save_and_load_methods!(KNN<T>);
}