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#[derive(Clone)]
8pub struct AccuracyMetric {
9 name: MetricName,
10 state: NumericMetricState,
11 pad_token: Option<usize>,
12}
13
14#[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 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 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], [1.0, 2.0, 0.5], [0.4, 0.1, 0.2], [0.6, 0.7, 0.2], ],
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], [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], ],
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 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 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 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}