Skip to main content

burn_train/metric/
hamming.rs

1use std::sync::Arc;
2
3use super::state::{FormatOptions, NumericMetricState};
4use super::{MetricMetadata, SerializedEntry};
5use crate::metric::{
6    Metric, MetricAttributes, MetricName, Numeric, NumericAttributes, NumericEntry,
7};
8use burn_core::tensor::{Int, Tensor, TensorReadError, activation::sigmoid};
9
10/// The hamming score, sometimes referred to as multi-label or label-based accuracy.
11#[derive(Clone)]
12pub struct HammingScore {
13    name: MetricName,
14    state: NumericMetricState,
15    threshold: f32,
16    sigmoid: bool,
17}
18
19/// The [hamming score](HammingScore) input type.
20#[derive(new)]
21pub struct HammingScoreInput {
22    outputs: Tensor<2>,
23    targets: Tensor<2, Int>,
24}
25
26impl HammingScore {
27    /// Creates the metric.
28    pub fn new() -> Self {
29        Self::default()
30    }
31
32    fn update_name(&mut self) {
33        self.name = Arc::new(format!("Hamming Score @ Threshold({})", self.threshold));
34    }
35
36    /// Sets the threshold.
37    pub fn with_threshold(mut self, threshold: f32) -> Self {
38        self.threshold = threshold;
39        self.update_name();
40        self
41    }
42
43    /// Sets the sigmoid activation function usage.
44    pub fn with_sigmoid(mut self, sigmoid: bool) -> Self {
45        self.sigmoid = sigmoid;
46        self.update_name();
47        self
48    }
49}
50
51impl Default for HammingScore {
52    /// Creates a new metric instance with default values.
53    fn default() -> Self {
54        let threshold = 0.5;
55        let name = Arc::new(format!("Hamming Score @ Threshold({})", threshold));
56
57        Self {
58            name,
59            state: NumericMetricState::default(),
60            threshold,
61            sigmoid: false,
62        }
63    }
64}
65
66impl Metric for HammingScore {
67    type Input = HammingScoreInput;
68
69    fn update(
70        &mut self,
71        input: &HammingScoreInput,
72        _metadata: &MetricMetadata,
73    ) -> Result<SerializedEntry, TensorReadError> {
74        let [batch_size, _n_classes] = input.outputs.dims();
75
76        let targets = input.targets.clone();
77
78        let mut outputs = input.outputs.clone();
79
80        if self.sigmoid {
81            outputs = sigmoid(outputs);
82        }
83
84        let score = outputs
85            .greater_scalar(self.threshold)
86            .equal(targets.bool())
87            .float()
88            .mean()
89            .try_into_scalar::<f64>()?;
90
91        self.state.update(100.0 * score, batch_size);
92        Ok(self
93            .state
94            .compute_update(FormatOptions::new(self.name()).unit("%").precision(2)))
95    }
96
97    fn compute(&mut self) -> Result<SerializedEntry, TensorReadError> {
98        Ok(self
99            .state
100            .compute_final(FormatOptions::new(self.name()).unit("%").precision(2)))
101    }
102
103    fn clear(&mut self) {
104        self.state.reset()
105    }
106
107    fn name(&self) -> MetricName {
108        self.name.clone()
109    }
110
111    fn attributes(&self) -> MetricAttributes {
112        NumericAttributes {
113            unit: Some("%".to_string()),
114            higher_is_better: true,
115        }
116        .into()
117    }
118}
119
120impl Numeric for HammingScore {
121    fn value(&self) -> Option<NumericEntry> {
122        Some(self.state.current_value())
123    }
124
125    fn running_value(&self) -> Option<NumericEntry> {
126        Some(self.state.running_value())
127    }
128
129    fn final_value(&self) -> NumericEntry {
130        self.state.final_value()
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn test_hamming_score() {
140        let device = Default::default();
141        let mut metric = HammingScore::new();
142
143        let x = Tensor::from_data(
144            [
145                [0.32, 0.52, 0.38, 0.68, 0.61], // with x > 0.5: [0, 1, 0, 1, 1]
146                [0.43, 0.31, 0.21, 0.63, 0.53], //               [0, 0, 0, 1, 1]
147                [0.44, 0.25, 0.71, 0.39, 0.73], //               [0, 0, 1, 0, 1]
148                [0.49, 0.37, 0.68, 0.39, 0.31], //               [0, 0, 1, 0, 0]
149            ],
150            &device,
151        );
152        let y = Tensor::from_data(
153            [
154                [0, 1, 0, 1, 1],
155                [0, 0, 0, 1, 1],
156                [0, 0, 1, 0, 1],
157                [0, 0, 1, 0, 0],
158            ],
159            &device,
160        );
161
162        let _entry = metric
163            .update(
164                &HammingScoreInput::new(x.clone(), y.clone()),
165                &MetricMetadata::fake(),
166            )
167            .unwrap();
168        assert_eq!(100.0, metric.value().unwrap().current());
169
170        // Invert all targets: y = (1 - y)
171        let y = y.neg().add_scalar(1);
172        let _entry = metric
173            .update(
174                &HammingScoreInput::new(x.clone(), y), // invert targets (1 - y)
175                &MetricMetadata::fake(),
176            )
177            .unwrap();
178        assert_eq!(0.0, metric.value().unwrap().current());
179
180        // Invert 5 target values -> 1 - (5/20) = 0.75
181        let y = Tensor::from_data(
182            [
183                [0, 1, 1, 0, 1],
184                [0, 0, 0, 0, 1],
185                [0, 0, 0, 0, 1],
186                [0, 1, 1, 0, 0],
187            ],
188            &device,
189        );
190        let _entry = metric
191            .update(
192                &HammingScoreInput::new(x, y), // invert targets (1 - y)
193                &MetricMetadata::fake(),
194            )
195            .unwrap();
196        assert_eq!(75.0, metric.value().unwrap().current());
197    }
198
199    #[test]
200    fn test_parameterized_unique_name() {
201        let metric_a = HammingScore::new().with_threshold(0.5);
202        let metric_b = HammingScore::new().with_threshold(0.75);
203        let metric_c = HammingScore::new().with_threshold(0.5);
204
205        assert_ne!(metric_a.name(), metric_b.name());
206        assert_eq!(metric_a.name(), metric_c.name());
207    }
208}