use std::sync::Arc;
use super::state::{FormatOptions, NumericMetricState};
use super::{MetricMetadata, SerializedEntry};
use crate::metric::{
Metric, MetricAttributes, MetricName, Numeric, NumericAttributes, NumericEntry,
};
use burn_core::tensor::{Int, Tensor};
#[derive(Default, Clone)]
pub struct TopKAccuracyMetric {
name: Arc<String>,
k: usize,
state: NumericMetricState,
pad_token: Option<usize>,
}
#[derive(new)]
pub struct TopKAccuracyInput {
outputs: Tensor<2>,
targets: Tensor<1, Int>,
}
impl TopKAccuracyMetric {
pub fn new(k: usize) -> Self {
Self {
name: Arc::new(format!("Top-K Accuracy @ TopK({})", k)),
k,
..Default::default()
}
}
pub fn with_pad_token(mut self, index: usize) -> Self {
self.pad_token = Some(index);
self
}
}
impl Metric for TopKAccuracyMetric {
type Input = TopKAccuracyInput;
fn update(&mut self, input: &TopKAccuracyInput, _metadata: &MetricMetadata) -> SerializedEntry {
let [batch_size, _n_classes] = input.outputs.dims();
let targets = input.targets.clone();
let outputs = input
.outputs
.clone()
.argsort_descending(1)
.narrow(1, 0, self.k)
.reshape([batch_size, self.k]);
let (targets, num_pad) = match self.pad_token {
Some(pad_token) => {
let mask = targets.clone().equal_scalar(pad_token as i64);
let num_pad = mask.clone().int().sum().into_scalar::<f64>();
(targets.clone().mask_fill(mask, -1_i64), num_pad)
}
None => (targets.clone(), 0_f64),
};
let accuracy = targets
.reshape([batch_size, 1])
.repeat_dim(1, self.k)
.equal(outputs)
.int()
.sum()
.into_scalar::<f64>()
/ (batch_size as f64 - num_pad);
self.state.update(100.0 * accuracy, batch_size);
self.state
.compute_update(FormatOptions::new(self.name()).unit("%").precision(2))
}
fn compute(&mut self) -> SerializedEntry {
self.state
.compute_final(FormatOptions::new(self.name()).unit("%").precision(2))
}
fn clear(&mut self) {
self.state.reset()
}
fn name(&self) -> MetricName {
self.name.clone()
}
fn attributes(&self) -> MetricAttributes {
NumericAttributes {
unit: Some("%".to_string()),
higher_is_better: true,
}
.into()
}
}
impl Numeric for TopKAccuracyMetric {
fn value(&self) -> Option<NumericEntry> {
Some(self.state.current_value())
}
fn running_value(&self) -> Option<NumericEntry> {
Some(self.state.running_value())
}
fn final_value(&self) -> NumericEntry {
self.state.final_value()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_accuracy_without_padding() {
let device = Default::default();
let mut metric = TopKAccuracyMetric::new(2);
let input = TopKAccuracyInput::new(
Tensor::from_data(
[
[0.0, 0.2, 0.8], [1.0, 2.0, 0.5], [0.4, 0.1, 0.2], [0.6, 0.7, 0.2], ],
&device,
),
Tensor::from_data([2, 2, 1, 1], &device),
);
let _entry = metric.update(&input, &MetricMetadata::fake());
assert_eq!(50.0, metric.value().unwrap().current());
}
#[test]
fn test_accuracy_with_padding() {
let device = Default::default();
let mut metric = TopKAccuracyMetric::new(2).with_pad_token(3);
let input = TopKAccuracyInput::new(
Tensor::from_data(
[
[0.0, 0.2, 0.8, 0.0], [1.0, 2.0, 0.5, 0.0], [0.4, 0.1, 0.2, 0.0], [0.6, 0.7, 0.2, 0.0], [0.0, 0.1, 0.2, 5.0], [0.0, 0.1, 0.2, 0.0], [0.6, 0.0, 0.2, 0.0], ],
&device,
),
Tensor::from_data([2, 2, 1, 1, 3, 3, 3], &device),
);
let _entry = metric.update(&input, &MetricMetadata::fake());
assert_eq!(50.0, metric.value().unwrap().current());
}
#[test]
fn test_parameterized_unique_name() {
let metric_a = TopKAccuracyMetric::new(2);
let metric_b = TopKAccuracyMetric::new(1);
let metric_c = TopKAccuracyMetric::new(2);
assert_ne!(metric_a.name(), metric_b.name());
assert_eq!(metric_a.name(), metric_c.name());
}
}