use super::{DataSet, Loss};
use crate::{Eval, EvalMut, Graph, GraphEvaluator, Op, Tree};
use std::fmt::Debug;
#[derive(Clone, Default)]
pub struct Accuracy<'a> {
name: Option<String>,
data_set: Option<&'a DataSet<f32>>,
loss_fn: Option<Loss>,
}
impl<'a> Accuracy<'a> {
pub fn named(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn on(mut self, data_set: &'a DataSet<f32>) -> Self {
self.data_set = Some(data_set);
self
}
pub fn loss(mut self, loss_fn: Loss) -> Self {
self.loss_fn = Some(loss_fn);
self
}
pub fn calc(&self, eval: &mut impl EvalMut<[f32], Vec<f32>>) -> AccuracyResult {
let data_set = self
.data_set
.expect("DataSet reference must be provided for accuracy calculation");
let loss_fn = self
.loss_fn
.expect("Loss function must be provided for accuracy calculation");
self.calc_internal(eval, data_set, loss_fn)
}
pub fn calc_internal(
&self,
eval: &mut impl EvalMut<[f32], Vec<f32>>,
data_set: &DataSet<f32>,
loss_fn: Loss,
) -> AccuracyResult {
let mut outputs = Vec::new();
let mut total_samples = 0.0;
let mut correct_predictions = 0.0;
let mut is_regression = true;
let mut mae = 0.0;
let mut mse = 0.0;
let mut min_output = f32::MAX;
let mut max_output = f32::MIN;
let mut ss_total = 0.0;
let mut ss_residual = 0.0;
let mut y_mean = 0.0;
let mut tp = 0.0;
let mut fp = 0.0;
let mut fn_ = 0.0;
let loss = loss_fn.calc(data_set, eval);
let total_values = data_set.len();
if total_values > 0 {
y_mean = data_set.iter().map(|row| row.output()[0]).sum::<f32>() / total_values as f32;
}
for row in data_set.iter() {
let output = eval.eval_mut(row.input());
outputs.push(output.clone());
if output.len() == 1 {
is_regression = true;
let y_true = row.output()[0];
let y_pred = output[0];
mae += (y_true - y_pred).abs();
mse += (y_true - y_pred).powi(2);
ss_residual += (y_true - y_pred).powi(2);
ss_total += (y_true - y_mean).powi(2);
min_output = min_output.min(y_true);
max_output = max_output.max(y_true);
total_samples += 1.0;
} else {
is_regression = false;
if let Some((max_idx, _)) = output
.iter()
.enumerate()
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
{
if let Some(target) = row.output().iter().position(|&x| x == 1.0) {
total_samples += 1.0;
if max_idx == target {
correct_predictions += 1.0;
tp += 1.0;
} else {
fp += 1.0;
}
} else {
fn_ += 1.0;
}
}
}
}
let accuracy = if is_regression {
if total_samples > 0.0 && (max_output - min_output) > 0.0 {
1.0 - (mae / total_samples) / (max_output - min_output)
} else {
0.0
}
} else if total_samples > 0.0 {
correct_predictions / total_samples
} else {
0.0
};
let (precision, recall, f1_score) = if is_regression {
(0.0, 0.0, 0.0) } else {
let precision = if tp + fp > 0.0 { tp / (tp + fp) } else { 0.0 };
let recall = if tp + fn_ > 0.0 { tp / (tp + fn_) } else { 0.0 };
let f1_score = if precision + recall > 0.0 {
2.0 * (precision * recall) / (precision + recall)
} else {
0.0
};
(precision, recall, f1_score)
};
let rmse = if total_samples > 0.0 {
(mse / total_samples).sqrt()
} else {
0.0
};
let r_squared = if ss_total > 0.0 {
1.0 - (ss_residual / ss_total)
} else {
0.0
};
AccuracyResult {
name: match &self.name {
Some(name) => name.clone(),
None => {
if is_regression {
"Regression Accuracy".to_string()
} else {
"Classification Accuracy".to_string()
}
}
},
accuracy,
precision,
recall,
f1_score,
rmse,
r_squared,
loss,
loss_fn,
sample_count: data_set.len(),
is_regression,
}
}
}
pub struct AccuracyResult {
name: String,
accuracy: f32,
precision: f32, recall: f32, f1_score: f32, rmse: f32, r_squared: f32, sample_count: usize,
loss: f32,
loss_fn: Loss,
is_regression: bool,
}
impl AccuracyResult {
pub fn name(&self) -> &str {
&self.name
}
pub fn accuracy(&self) -> f32 {
self.accuracy
}
pub fn precision(&self) -> f32 {
self.precision
}
pub fn recall(&self) -> f32 {
self.recall
}
pub fn f1_score(&self) -> f32 {
self.f1_score
}
pub fn rmse(&self) -> f32 {
self.rmse
}
pub fn r_squared(&self) -> f32 {
self.r_squared
}
pub fn sample_count(&self) -> usize {
self.sample_count
}
pub fn loss(&self) -> f32 {
self.loss
}
pub fn loss_fn(&self) -> Loss {
self.loss_fn
}
}
impl Debug for AccuracyResult {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
if self.is_regression {
write!(
f,
"{:?} {{\n\tN: {:?} \n\tAccuracy: {:.2}%\n\tR² Score: {:.5}\n\tRMSE: {:.5}\n\tLoss ({:?}): {:.5}\n}}",
self.name,
self.sample_count,
self.accuracy * 100.0,
self.r_squared,
self.rmse,
self.loss_fn,
self.loss
)
} else {
write!(
f,
"{:?} {{\n\tN: {:?} \n\tAccuracy: {:.2}%\n\tPrecision: {:.2}%\n\tRecall: {:.2}%\n\tF1 Score: {:.2}%\n\tLoss ({:?}): {:.5}\n}}",
self.name,
self.sample_count,
self.accuracy * 100.0,
self.precision * 100.0,
self.recall * 100.0,
self.f1_score * 100.0,
self.loss_fn,
self.loss
)
}
}
}
impl Eval<Graph<Op<f32>>, Option<AccuracyResult>> for Accuracy<'_> {
fn eval(&self, graph: &Graph<Op<f32>>) -> Option<AccuracyResult> {
let mut evaluator = GraphEvaluator::new(graph);
Some(self.calc(&mut evaluator))
}
}
impl Eval<Tree<Op<f32>>, Option<AccuracyResult>> for Accuracy<'_> {
fn eval(&self, tree: &Tree<Op<f32>>) -> Option<AccuracyResult> {
Some(self.calc(&mut tree.clone()))
}
}
impl Eval<Vec<Tree<Op<f32>>>, Option<AccuracyResult>> for Accuracy<'_> {
fn eval(&self, trees: &Vec<Tree<Op<f32>>>) -> Option<AccuracyResult> {
let mut cloned_trees = trees.clone();
Some(self.calc(&mut cloned_trees))
}
}