Skip to main content

burn_train/metric/
acc.rs

1use super::MetricMetadata;
2use super::state::{FormatOptions, NumericMetricState};
3use crate::metric::{Metric, MetricAttributes, MetricName, Numeric, SerializedEntry};
4use burn_core::tensor::{Int, Tensor, TensorReadError};
5
6/// The accuracy metric.
7#[derive(Clone)]
8pub struct AccuracyMetric {
9    name: MetricName,
10    state: NumericMetricState,
11    pad_token: Option<usize>,
12}
13
14/// The [accuracy metric](AccuracyMetric) input type.
15#[derive(new)]
16pub struct AccuracyInput {
17    outputs: Tensor<2>,
18    targets: Tensor<1, Int>,
19}
20
21impl Default for AccuracyMetric {
22    fn default() -> Self {
23        Self::new()
24    }
25}
26
27impl AccuracyMetric {
28    /// Creates the metric.
29    pub fn new() -> Self {
30        Self {
31            name: MetricName::new("Accuracy".to_string()),
32            state: Default::default(),
33            pad_token: Default::default(),
34        }
35    }
36
37    /// Sets the pad token.
38    pub fn with_pad_token(mut self, index: usize) -> Self {
39        self.pad_token = Some(index);
40        self
41    }
42}
43
44impl Metric for AccuracyMetric {
45    type Input = AccuracyInput;
46
47    fn update(
48        &mut self,
49        input: &AccuracyInput,
50        _metadata: &MetricMetadata,
51    ) -> Result<SerializedEntry, TensorReadError> {
52        let targets = input.targets.clone();
53        let outputs = input.outputs.clone();
54
55        let [batch_size, _n_classes] = outputs.dims();
56
57        let outputs = outputs.argmax(1).reshape([batch_size]);
58
59        let (num_matches, num_pad) = match self.pad_token {
60            Some(pad_token) => {
61                let mask = targets.clone().equal_scalar(pad_token as i64);
62                let matches = outputs.equal(targets).float().mask_fill(mask.clone(), 0);
63                let num_pad = mask.int().sum().try_into_scalar::<i64>()? as usize;
64
65                (matches.sum().try_into_scalar::<f64>()?, num_pad)
66            }
67            None => (
68                outputs
69                    .equal(targets)
70                    .int()
71                    .sum()
72                    .try_into_scalar::<f64>()?,
73                0,
74            ),
75        };
76        let valid_count = batch_size - num_pad;
77        let accuracy = num_matches / valid_count as f64;
78
79        self.state.update(100.0 * accuracy, valid_count);
80        Ok(self
81            .state
82            .compute_update(FormatOptions::new(self.name()).unit("%").precision(2)))
83    }
84
85    fn compute(&mut self) -> Result<SerializedEntry, TensorReadError> {
86        Ok(self
87            .state
88            .compute_final(FormatOptions::new(self.name()).unit("%").precision(2)))
89    }
90
91    fn clear(&mut self) {
92        self.state.reset()
93    }
94
95    fn name(&self) -> MetricName {
96        self.name.clone()
97    }
98
99    fn attributes(&self) -> MetricAttributes {
100        super::NumericAttributes {
101            unit: Some("%".to_string()),
102            higher_is_better: true,
103        }
104        .into()
105    }
106}
107
108impl Numeric for AccuracyMetric {
109    fn value(&self) -> Option<super::NumericEntry> {
110        Some(self.state.current_value())
111    }
112
113    fn running_value(&self) -> Option<super::NumericEntry> {
114        Some(self.state.running_value())
115    }
116
117    fn final_value(&self) -> super::NumericEntry {
118        self.state.final_value()
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use crate::{
126        ClassificationOutput,
127        metric::{Adaptor, ItemLazy},
128    };
129    use burn_core::tensor::Device;
130
131    #[test]
132    fn test_accuracy_without_padding() {
133        let device = Default::default();
134        let mut metric = AccuracyMetric::new();
135        let input = AccuracyInput::new(
136            Tensor::from_data(
137                [
138                    [0.0, 0.2, 0.8], // 2
139                    [1.0, 2.0, 0.5], // 1
140                    [0.4, 0.1, 0.2], // 0
141                    [0.6, 0.7, 0.2], // 1
142                ],
143                &device,
144            ),
145            Tensor::from_data([2, 2, 1, 1], &device),
146        );
147
148        let _entry = metric.update(&input, &MetricMetadata::fake()).unwrap();
149        assert_eq!(50.0, metric.value().unwrap().current());
150    }
151
152    #[test]
153    fn test_accuracy_with_padding() {
154        let device = Default::default();
155        let mut metric = AccuracyMetric::new().with_pad_token(3);
156        let input = AccuracyInput::new(
157            Tensor::from_data(
158                [
159                    [0.0, 0.2, 0.8, 0.0], // 2
160                    [1.0, 2.0, 0.5, 0.0], // 1
161                    [0.4, 0.1, 0.2, 0.0], // 0
162                    [0.6, 0.7, 0.2, 0.0], // 1
163                    [0.0, 0.1, 0.2, 5.0], // Predicted padding should not count
164                    [0.0, 0.1, 0.2, 0.0], // Error on padding should not count
165                    [0.6, 0.0, 0.2, 0.0], // Error on padding should not count
166                ],
167                &device,
168            ),
169            Tensor::from_data([2, 2, 1, 1, 3, 3, 3], &device),
170        );
171
172        let _entry = metric.update(&input, &MetricMetadata::fake()).unwrap();
173        assert_eq!(50.0, metric.value().unwrap().current());
174    }
175
176    #[test]
177    fn test_accuracy_epoch_aggregation_excludes_padding() {
178        let device = Default::default();
179        let mut metric = AccuracyMetric::new().with_pad_token(2);
180
181        // One valid, correct sample and three padding samples.
182        metric
183            .update(
184                &AccuracyInput::new(
185                    Tensor::from_data([[0.9, 0.1], [0.9, 0.1], [0.9, 0.1], [0.9, 0.1]], &device),
186                    Tensor::from_data([0, 2, 2, 2], &device),
187                ),
188                &MetricMetadata::fake(),
189            )
190            .unwrap();
191        // Four valid, incorrect samples.
192        metric
193            .update(
194                &AccuracyInput::new(
195                    Tensor::from_data([[0.9, 0.1]; 4], &device),
196                    Tensor::from_data([1, 1, 1, 1], &device),
197                ),
198                &MetricMetadata::fake(),
199            )
200            .unwrap();
201
202        // One correct prediction out of five valid samples.
203        assert_eq!(20.0, metric.final_value().current());
204    }
205
206    #[test]
207    fn test_fully_padded_batch_does_not_poison_epoch_accuracy() {
208        let device = Default::default();
209        let mut metric = AccuracyMetric::new().with_pad_token(2);
210
211        metric
212            .update(
213                &AccuracyInput::new(
214                    Tensor::from_data([[0.9, 0.1]; 2], &device),
215                    Tensor::from_data([2, 2], &device),
216                ),
217                &MetricMetadata::fake(),
218            )
219            .unwrap();
220        metric
221            .update(
222                &AccuracyInput::new(
223                    Tensor::from_data([[0.9, 0.1]], &device),
224                    Tensor::from_data([0], &device),
225                ),
226                &MetricMetadata::fake(),
227            )
228            .unwrap();
229
230        assert_eq!(100.0, metric.final_value().current());
231    }
232
233    #[test]
234    fn test_accuracy_after_syncing_autodiff_classification_output() {
235        let device = Device::flex().autodiff();
236        let mut metric = AccuracyMetric::new();
237        let output = ClassificationOutput::new(
238            Tensor::from_data([0.0], &device),
239            Tensor::from_data([[0.1, 0.9]], &device),
240            Tensor::from_data([1], &device),
241        )
242        .sync()
243        .unwrap();
244        let input = output.adapt();
245
246        let _entry = metric.update(&input, &MetricMetadata::fake()).unwrap();
247        assert_eq!(100.0, metric.value().unwrap().current());
248    }
249}