1use async_trait::async_trait;
7use std::collections::HashMap;
8
9use super::{EvalError, Evaluator, Score};
10
11pub struct Bleu {
13 max_n: usize,
14 char_level: bool,
16 smoothing: bool,
18}
19
20impl Default for Bleu {
21 fn default() -> Self {
22 Self::new()
23 }
24}
25
26impl Bleu {
27 pub fn new() -> Self {
28 Self {
29 max_n: 4,
30 char_level: false,
31 smoothing: false,
32 }
33 }
34
35 pub fn with_max_n(mut self, n: usize) -> Self {
37 self.max_n = n.max(1);
38 self
39 }
40
41 pub fn with_char_level(mut self, v: bool) -> Self {
43 self.char_level = v;
44 self
45 }
46
47 pub fn with_smoothing(mut self, v: bool) -> Self {
49 self.smoothing = v;
50 self
51 }
52}
53
54fn tokenize(s: &str, char_level: bool) -> Vec<String> {
56 if char_level {
57 s.chars()
58 .filter(|c| !c.is_whitespace())
59 .map(|c| c.to_lowercase().collect::<String>())
60 .collect()
61 } else {
62 s.split_whitespace().map(|w| w.to_lowercase()).collect()
63 }
64}
65
66fn ngrams(tokens: &[String], n: usize) -> HashMap<Vec<String>, usize> {
67 let mut m = HashMap::new();
68 if tokens.len() < n {
69 return m;
70 }
71 for i in 0..=tokens.len() - n {
72 let g: Vec<String> = tokens[i..i + n].to_vec();
73 *m.entry(g).or_insert(0) += 1;
74 }
75 m
76}
77
78#[async_trait]
79impl Evaluator for Bleu {
80 async fn eval(
81 &self,
82 _input: &str,
83 prediction: &str,
84 reference: &str,
85 ) -> Result<Score, EvalError> {
86 let pred = tokenize(prediction, self.char_level);
87 let ref_t = tokenize(reference, self.char_level);
88 let plen = pred.len();
89 let rlen = ref_t.len();
90 if plen == 0 || rlen == 0 {
91 return Ok(Score::new(0.0).with_label("empty"));
92 }
93
94 let mut log_precisions: Vec<f64> = Vec::new();
95 for n in 1..=self.max_n {
96 let pred_grams = ngrams(&pred, n);
97 let ref_grams = ngrams(&ref_t, n);
98 let mut matches = 0usize;
99 let mut total = 0usize;
100 for (g, &c) in &pred_grams {
101 total += c;
102 let r = ref_grams.get(g).copied().unwrap_or(0);
103 matches += c.min(r);
104 }
105 if total == 0 {
106 if self.smoothing {
108 continue;
109 }
110 return Ok(Score::new(0.0).with_label("no_ngram_match"));
111 }
112 let p = if matches == 0 {
113 if self.smoothing {
114 0.5 / total as f64
116 } else {
117 return Ok(Score::new(0.0).with_label("no_ngram_match"));
118 }
119 } else {
120 matches as f64 / total as f64
121 };
122 log_precisions.push(p.ln());
123 }
124
125 let geo_mean = log_precisions.iter().sum::<f64>() / log_precisions.len() as f64;
126 let bp = if plen > rlen {
128 1.0
129 } else {
130 (1.0 - rlen as f64 / plen as f64).exp()
131 };
132 let bleu = bp * geo_mean.exp();
133 Ok(Score::new(bleu.clamp(0.0, 1.0)).with_label("bleu"))
134 }
135
136 fn name(&self) -> &str {
137 "bleu"
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[tokio::test]
146 async fn test_bleu_identical() {
147 let ev = Bleu::new();
148 let s = ev
149 .eval("", "the cat sat on the mat", "the cat sat on the mat")
150 .await
151 .unwrap();
152 assert!((s.value - 1.0).abs() < 1e-9);
153 }
154
155 #[tokio::test]
156 async fn test_bleu_partial() {
157 let ev = Bleu::new();
158 let s = ev
159 .eval("", "the cat sat on the mat", "the cat sat on a mat")
160 .await
161 .unwrap();
162 assert!(s.value > 0.0 && s.value < 1.0);
163 }
164
165 #[tokio::test]
166 async fn test_bleu_no_match() {
167 let ev = Bleu::new();
168 let s = ev
169 .eval(
170 "",
171 "completely different words here",
172 "the cat sat on the mat",
173 )
174 .await
175 .unwrap();
176 assert!((s.value - 0.0).abs() < 1e-9);
177 }
178
179 #[tokio::test]
180 async fn test_bleu_empty() {
181 let ev = Bleu::new();
182 let s = ev.eval("", "", "ref").await.unwrap();
183 assert!((s.value - 0.0).abs() < 1e-9);
184 }
185
186 #[tokio::test]
187 async fn test_bleu_brevity_penalty() {
188 let ev = Bleu::new().with_max_n(1);
190 let s = ev
191 .eval("", "the cat", "the cat sat on the mat")
192 .await
193 .unwrap();
194 assert!(s.value < 1.0);
195 }
196
197 #[tokio::test]
198 async fn test_bleu_char_level_chinese() {
199 let ev = Bleu::new().with_char_level(true).with_max_n(2);
201 let s = ev.eval("", "猫坐在垫子上", "猫坐在垫子上").await.unwrap();
202 assert!((s.value - 1.0).abs() < 1e-9);
203 }
204
205 #[tokio::test]
206 async fn test_bleu_smoothing_avoids_zero() {
207 let strict = Bleu::new();
209 let s = strict.eval("", "the cat", "the cat").await.unwrap();
210 assert!((s.value - 0.0).abs() < 1e-9);
211 let smooth = Bleu::new().with_smoothing(true);
213 let s2 = smooth.eval("", "the cat", "the cat").await.unwrap();
214 assert!(s2.value > 0.0);
215 }
216}