Skip to main content

burn_train/metric/
wer.rs

1use super::cer::edit_distance;
2use super::state::{FormatOptions, NumericMetricState};
3use super::{MetricMetadata, SerializedEntry};
4use crate::metric::{
5    Metric, MetricAttributes, MetricName, Numeric, NumericAttributes, NumericEntry,
6};
7use burn_core::tensor::{Int, Tensor, TensorReadError};
8use std::sync::Arc;
9
10// The edit_distance function remains the same as it calculates the Levenshtein distance
11// between two sequences. The "units" within the sequences will now be treated as words.
12/// The word error rate (WER) metric, similar to the CER, is defined as the edit distance (e.g. Levenshtein distance) between the predicted
13/// and reference word sequences, divided by the total number of words in the reference. Here, the "units" within the sequences are words.
14///
15#[derive(Clone)]
16pub struct WordErrorRate {
17    name: MetricName,
18    state: NumericMetricState,
19    pad_token: Option<usize>,
20}
21
22/// The [word error rate metric](WordErrorRate) input type.
23#[derive(new)]
24pub struct WerInput {
25    /// The predicted token sequences (as a 2-D tensor of token indices).
26    pub outputs: Tensor<2, Int>,
27    /// The target token sequences (as a 2-D tensor of token indices).
28    pub targets: Tensor<2, Int>,
29}
30impl Default for WordErrorRate {
31    fn default() -> Self {
32        Self::new()
33    }
34}
35
36impl WordErrorRate {
37    /// Creates the metric.
38    pub fn new() -> Self {
39        Self {
40            name: Arc::new("WER".to_string()),
41            state: NumericMetricState::default(),
42            pad_token: None,
43        }
44    }
45
46    /// Sets the pad token.
47    pub fn with_pad_token(mut self, index: usize) -> Self {
48        self.pad_token = Some(index);
49        self
50    }
51}
52
53impl Metric for WordErrorRate {
54    type Input = WerInput;
55
56    fn update(
57        &mut self,
58        input: &WerInput,
59        _metadata: &MetricMetadata,
60    ) -> Result<SerializedEntry, TensorReadError> {
61        let outputs = input.outputs.clone();
62        let targets = input.targets.clone();
63        let [batch_size, seq_len] = targets.dims();
64
65        let outputs_data = outputs.try_into_vec_as::<i32>()?;
66        let targets_data = targets.try_into_vec_as::<i32>()?;
67
68        let pad_token = self.pad_token.map(|p| p as i32);
69
70        let mut total_edit_distance = 0.0;
71        let mut total_target_length = 0usize;
72
73        // Process each sequence in the batch
74        for i in 0..batch_size {
75            let start = i * seq_len;
76            let end = (i + 1) * seq_len;
77            let output_seq = &outputs_data[start..end];
78            let target_seq = &targets_data[start..end];
79
80            // Handle padding and map elements to i32.
81            // These sequences now represent "words" (token IDs).
82            let (ed, target_len) = match pad_token {
83                Some(pad) => {
84                    let output_seq_no_pad = output_seq
85                        .iter()
86                        .take_while(|&&x| x != pad)
87                        .copied()
88                        .collect::<Vec<_>>();
89
90                    let target_seq_no_pad = target_seq
91                        .iter()
92                        .take_while(|&&x| x != pad)
93                        .copied()
94                        .collect::<Vec<_>>();
95
96                    (
97                        edit_distance(&target_seq_no_pad, &output_seq_no_pad),
98                        target_seq_no_pad.len(),
99                    )
100                }
101                None => (edit_distance(target_seq, output_seq), target_seq.len()),
102            };
103
104            total_edit_distance += ed as f64;
105            total_target_length += target_len;
106        }
107
108        // Compute current WER value as a percentage
109        let value = if total_target_length > 0 {
110            100.0 * total_edit_distance / total_target_length as f64
111        } else {
112            0.0
113        };
114
115        self.state.update(value, total_target_length);
116        Ok(self
117            .state
118            .compute_update(FormatOptions::new(self.name()).unit("%").precision(2)))
119    }
120
121    fn compute(&mut self) -> Result<SerializedEntry, TensorReadError> {
122        Ok(self
123            .state
124            .compute_final(FormatOptions::new(self.name()).unit("%").precision(2)))
125    }
126
127    fn name(&self) -> MetricName {
128        self.name.clone()
129    }
130
131    fn clear(&mut self) {
132        self.state.reset();
133    }
134
135    fn attributes(&self) -> MetricAttributes {
136        NumericAttributes {
137            unit: Some("%".to_string()),
138            higher_is_better: false,
139        }
140        .into()
141    }
142}
143
144impl Numeric for WordErrorRate {
145    fn value(&self) -> Option<NumericEntry> {
146        Some(self.state.current_value())
147    }
148
149    fn running_value(&self) -> Option<NumericEntry> {
150        Some(self.state.running_value())
151    }
152
153    fn final_value(&self) -> NumericEntry {
154        self.state.final_value()
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    /// Perfect match => WER = 0 %.
163    #[test]
164    fn test_wer_without_padding() {
165        let device = Default::default();
166        let mut metric = WordErrorRate::new();
167
168        // Batch size = 2, sequence length = 2
169        let preds = Tensor::from_data([[1, 2], [3, 4]], &device);
170        let tgts = Tensor::from_data([[1, 2], [3, 4]], &device);
171
172        metric
173            .update(&WerInput::new(preds, tgts), &MetricMetadata::fake())
174            .unwrap();
175
176        assert_eq!(0.0, metric.value().unwrap().current());
177    }
178
179    /// Two word edits in four target words => 50 %.
180    #[test]
181    fn test_wer_without_padding_two_errors() {
182        let device = Default::default();
183        let mut metric = WordErrorRate::new();
184
185        // One substitution in each sequence.
186        // Sequence 1: target [1, 3], pred [1, 2] -> 1 error (3 vs 2)
187        // Sequence 2: target [3, 4], pred [3, 5] -> 1 error (4 vs 5)
188        let preds = Tensor::from_data([[1, 2], [3, 5]], &device);
189        let tgts = Tensor::from_data([[1, 3], [3, 4]], &device);
190
191        metric
192            .update(&WerInput::new(preds, tgts), &MetricMetadata::fake())
193            .unwrap();
194
195        // Total errors = 2, Total target words = 4. WER = (2/4) * 100 = 50 %
196        assert_eq!(50.0, metric.value().unwrap().current());
197    }
198
199    /// Same scenario as above, but with right-padding (token 9) ignored.
200    #[test]
201    fn test_wer_with_padding() {
202        let device = Default::default();
203        let pad = 9_i64;
204        let mut metric = WordErrorRate::new().with_pad_token(pad as usize);
205
206        // Each row has three columns, last one is the pad token.
207        // Target sequences after removing pad: [1, 3] and [3, 4] (total length 4)
208        // Predicted sequences after removing pad: [1, 2] and [3, 5]
209        let preds = Tensor::from_data([[1, 2, pad], [3, 5, pad]], &device);
210        let tgts = Tensor::from_data([[1, 3, pad], [3, 4, pad]], &device);
211
212        metric
213            .update(&WerInput::new(preds, tgts), &MetricMetadata::fake())
214            .unwrap();
215        assert_eq!(50.0, metric.value().unwrap().current());
216    }
217
218    /// `clear()` must reset the running statistics to NaN.
219    #[test]
220    fn test_clear_resets_state() {
221        let device = Default::default();
222        let mut metric = WordErrorRate::new();
223
224        let preds = Tensor::from_data([[1, 2]], &device);
225        let tgts = Tensor::from_data([[1, 3]], &device); // one error
226
227        metric
228            .update(
229                &WerInput::new(preds.clone(), tgts.clone()),
230                &MetricMetadata::fake(),
231            )
232            .unwrap();
233        assert!(metric.value().unwrap().current() > 0.0);
234
235        metric.clear();
236        assert!(metric.value().unwrap().current().is_nan());
237    }
238}