1use super::state::FormatOptions;
2use super::{MetricMetadata, NumericEntry, SerializedEntry, format_float};
3use crate::metric::{Metric, MetricAttributes, MetricName, Numeric, NumericAttributes};
4use burn_core::tensor::{Int, Tensor, TensorReadError};
5
6#[derive(Clone)]
13struct PerplexityState {
14 sum_nll: f64,
16 total_tokens: usize,
18 current: f64,
20 current_tokens: usize,
21}
22
23impl PerplexityState {
24 fn new() -> Self {
25 Self {
26 sum_nll: 0.0,
27 total_tokens: 0,
28 current: f64::NAN,
29 current_tokens: 0,
30 }
31 }
32
33 fn reset(&mut self) {
34 self.sum_nll = 0.0;
35 self.total_tokens = 0;
36 self.current = f64::NAN;
37 self.current_tokens = 0;
38 }
39
40 fn update(&mut self, sum_log_prob: f64, effective_tokens: usize) {
42 let batch_nll = -sum_log_prob;
45
46 self.sum_nll += batch_nll;
48 self.total_tokens += effective_tokens;
49
50 let batch_perplexity = if effective_tokens > 0 {
52 (batch_nll / effective_tokens as f64).exp()
53 } else {
54 f64::INFINITY
55 };
56 self.current = batch_perplexity;
57 self.current_tokens = effective_tokens;
58 }
59
60 pub fn compute_update(&self, format: FormatOptions) -> SerializedEntry {
62 self.compute(format, false)
63 }
64
65 pub fn compute_final(&self, format: FormatOptions) -> SerializedEntry {
67 self.compute(format, true)
68 }
69
70 fn compute(&self, format: FormatOptions, final_entry: bool) -> SerializedEntry {
71 let batch_perplexity = self.current;
72
73 let epoch_perplexity = if self.total_tokens > 0 {
75 (self.sum_nll / self.total_tokens as f64).exp()
76 } else {
77 f64::INFINITY
78 };
79
80 let (formatted_current, formatted_running) = match format.precision_value() {
82 Some(precision) => (
83 format_float(batch_perplexity, precision),
84 format_float(epoch_perplexity, precision),
85 ),
86 None => (format!("{batch_perplexity}"), format!("{epoch_perplexity}")),
87 };
88
89 let formatted = match format.unit_value() {
90 Some(unit) => {
91 format!("epoch {formatted_running} {unit} - batch {formatted_current} {unit}")
92 }
93 None => format!("epoch {formatted_running} - batch {formatted_current}"),
94 };
95
96 let serialized = if final_entry {
98 NumericEntry::Final(epoch_perplexity).serialize()
99 } else {
100 NumericEntry::Aggregated {
101 aggregated_value: batch_perplexity,
102 count: self.current_tokens,
103 }
104 .serialize()
105 };
106
107 SerializedEntry::new(formatted, serialized)
108 }
109
110 fn current_value(&self) -> Option<NumericEntry> {
111 Some(NumericEntry::Aggregated {
112 aggregated_value: self.current,
114 count: self.current_tokens,
115 })
116 }
117
118 fn running_value(&self) -> Option<NumericEntry> {
119 Some(NumericEntry::Aggregated {
120 aggregated_value: entry_value(self.sum_nll, self.total_tokens),
121 count: self.total_tokens,
122 })
123 }
124
125 fn final_value(&self) -> NumericEntry {
126 NumericEntry::Final(entry_value(self.sum_nll, self.total_tokens))
127 }
128}
129
130fn entry_value(value: f64, count: usize) -> f64 {
131 if count > 0 {
132 (value / count as f64).exp()
133 } else {
134 f64::INFINITY
135 }
136}
137
138#[derive(Clone)]
157pub struct PerplexityMetric {
158 name: MetricName,
159 state: PerplexityState,
160 pad_token: Option<usize>,
161}
162
163#[derive(new)]
165pub struct PerplexityInput {
166 outputs: Tensor<2>,
168 targets: Tensor<1, Int>,
170}
171
172impl Default for PerplexityMetric {
173 fn default() -> Self {
174 Self::new()
175 }
176}
177
178impl PerplexityMetric {
179 pub fn new() -> Self {
181 Self {
182 name: MetricName::new("Perplexity".to_string()),
183 state: PerplexityState::new(),
184 pad_token: Default::default(),
185 }
186 }
187
188 pub fn with_pad_token(mut self, index: usize) -> Self {
194 self.pad_token = Some(index);
195 self
196 }
197}
198
199impl Metric for PerplexityMetric {
200 type Input = PerplexityInput;
201
202 fn update(
203 &mut self,
204 input: &PerplexityInput,
205 _metadata: &MetricMetadata,
206 ) -> Result<SerializedEntry, TensorReadError> {
207 let targets = input.targets.clone();
208 let outputs = input.outputs.clone();
209
210 let [total_tokens, _vocab_size] = outputs.dims();
211
212 let log_probs = burn_core::tensor::activation::log_softmax(outputs, 1);
214
215 let target_log_probs = log_probs
217 .gather(1, targets.clone().unsqueeze_dim(1))
218 .squeeze_dim(1);
219
220 let (sum_log_prob, effective_tokens) = match self.pad_token {
221 Some(pad_token) => {
222 let mask = targets.clone().not_equal_scalar(pad_token as i64);
224
225 let masked_log_probs = target_log_probs.mask_fill(mask.clone().bool_not(), 0.0);
227
228 let sum_log_prob = masked_log_probs.sum().try_into_scalar::<f64>()?;
230 let effective_tokens = mask.int().sum().try_into_scalar::<i64>()? as usize;
231
232 (sum_log_prob, effective_tokens)
233 }
234 None => {
235 let sum_log_prob = target_log_probs.sum().try_into_scalar::<f64>()?;
237 (sum_log_prob, total_tokens)
238 }
239 };
240
241 self.state.update(sum_log_prob, effective_tokens);
244 Ok(self
245 .state
246 .compute_update(FormatOptions::new(self.name()).precision(2)))
247 }
248
249 fn compute(&mut self) -> Result<SerializedEntry, TensorReadError> {
250 Ok(self
251 .state
252 .compute_final(FormatOptions::new(self.name()).precision(2)))
253 }
254
255 fn clear(&mut self) {
256 self.state.reset()
257 }
258
259 fn name(&self) -> MetricName {
260 self.name.clone()
261 }
262
263 fn attributes(&self) -> MetricAttributes {
264 NumericAttributes {
265 unit: None,
266 higher_is_better: false,
267 }
268 .into()
269 }
270}
271
272impl Numeric for PerplexityMetric {
273 fn value(&self) -> Option<NumericEntry> {
274 self.state.current_value()
275 }
276
277 fn running_value(&self) -> Option<NumericEntry> {
278 self.state.running_value()
279 }
280
281 fn final_value(&self) -> NumericEntry {
282 self.state.final_value()
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 #[test]
291 fn test_perplexity_perfect_prediction() {
292 let device = Default::default();
293 let mut metric = PerplexityMetric::new();
294
295 let input = PerplexityInput::new(
297 Tensor::from_data(
298 [
299 [10.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 10.0], ],
303 &device,
304 ),
305 Tensor::from_data([0, 1, 2], &device),
306 );
307
308 let _entry = metric.update(&input, &MetricMetadata::fake()).unwrap();
309 let perplexity = metric.value().unwrap().current();
310
311 assert!(
313 perplexity < 1.1,
314 "Perfect predictions should have low perplexity, got {}",
315 perplexity
316 );
317 }
318
319 #[test]
320 fn test_perplexity_uniform_prediction() {
321 let device = Default::default();
322 let mut metric = PerplexityMetric::new();
323
324 let input = PerplexityInput::new(
326 Tensor::from_data(
327 [
328 [0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0], ],
332 &device,
333 ),
334 Tensor::from_data([0, 1, 2], &device),
335 );
336
337 let _entry = metric.update(&input, &MetricMetadata::fake()).unwrap();
338 let perplexity = metric.value().unwrap().current();
339
340 assert!(
342 (perplexity - 3.0).abs() < 0.1,
343 "Uniform distribution perplexity should be ~3.0, got {}",
344 perplexity
345 );
346 }
347
348 #[test]
349 fn test_perplexity_with_padding() {
350 let device = Default::default();
351 let mut metric = PerplexityMetric::new().with_pad_token(3);
352
353 let input = PerplexityInput::new(
354 Tensor::from_data(
355 [
356 [10.0, 0.0, 0.0, 0.0], [0.0, 10.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0], [0.0, 0.0, 0.0, 1.0], ],
361 &device,
362 ),
363 Tensor::from_data([0, 1, 3, 3], &device), );
365
366 let _entry = metric.update(&input, &MetricMetadata::fake()).unwrap();
367 let perplexity = metric.value().unwrap().current();
368
369 assert!(
371 perplexity < 1.1,
372 "Good predictions with padding should have low perplexity, got {}",
373 perplexity
374 );
375 }
376
377 #[test]
378 fn test_perplexity_wrong_prediction() {
379 let device = Default::default();
380 let mut metric = PerplexityMetric::new();
381
382 let input = PerplexityInput::new(
384 Tensor::from_data(
385 [
386 [0.0, 10.0, 0.0], [10.0, 0.0, 0.0], [0.0, 0.0, 10.0], ],
390 &device,
391 ),
392 Tensor::from_data([0, 1, 0], &device),
393 );
394
395 let _entry = metric.update(&input, &MetricMetadata::fake()).unwrap();
396 let perplexity = metric.value().unwrap().current();
397
398 assert!(
400 perplexity > 10.0,
401 "Wrong predictions should have high perplexity, got {}",
402 perplexity
403 );
404 }
405
406 #[test]
407 fn test_perplexity_multi_batch_aggregation() {
408 let device = Default::default();
409 let mut metric = PerplexityMetric::new();
410
411 let input1 = PerplexityInput::new(
413 Tensor::from_data(
414 [
415 [0.0, 0.0, 0.0], [0.0, 0.0, 0.0], ],
418 &device,
419 ),
420 Tensor::from_data([0, 1], &device),
421 );
422
423 let input2 = PerplexityInput::new(
425 Tensor::from_data(
426 [
427 [0.0, 0.0, 0.0], ],
429 &device,
430 ),
431 Tensor::from_data([2], &device),
432 );
433
434 let _entry1 = metric.update(&input1, &MetricMetadata::fake()).unwrap();
436 let _entry2 = metric.update(&input2, &MetricMetadata::fake()).unwrap();
437
438 let aggregated_perplexity = metric.value().unwrap().current();
439
440 assert!(
445 (aggregated_perplexity - 3.0).abs() < 0.1,
446 "Multi-batch aggregated perplexity should be ~3.0, got {}",
447 aggregated_perplexity
448 );
449
450 let mut single_batch_metric = PerplexityMetric::new();
452 let single_input = PerplexityInput::new(
453 Tensor::from_data([[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]], &device),
454 Tensor::from_data([0, 1, 2], &device),
455 );
456
457 let _single_entry = single_batch_metric
458 .update(&single_input, &MetricMetadata::fake())
459 .unwrap();
460 let single_batch_perplexity = single_batch_metric.value().unwrap().current();
461
462 assert!(
464 (aggregated_perplexity - single_batch_perplexity).abs() < 0.01,
465 "Multi-batch ({}) and single-batch ({}) perplexity should match",
466 aggregated_perplexity,
467 single_batch_perplexity
468 );
469 }
470
471 #[test]
472 fn test_perplexity_global_aggregation_end_to_end() {
473 let device = Default::default();
474 let mut metric = PerplexityMetric::new();
475
476 let input_batch1 = PerplexityInput::new(
479 Tensor::from_data([[5.0, 0.0, 0.0]], &device),
480 Tensor::from_data([0], &device),
481 );
482 let _ = metric
483 .update(&input_batch1, &MetricMetadata::fake())
484 .unwrap();
485
486 let batch1_current = metric.value().unwrap().current();
487 let batch1_running = metric.running_value().unwrap().current();
488 assert_eq!(batch1_current, batch1_running);
489
490 let input_batch2 = PerplexityInput::new(
493 Tensor::from_data(
494 [
495 [0.0, 0.0, 0.0],
496 [0.0, 0.0, 0.0],
497 [0.0, 0.0, 0.0],
498 [0.0, 0.0, 0.0],
499 [0.0, 0.0, 0.0],
500 ],
501 &device,
502 ),
503 Tensor::from_data([0, 1, 2, 0, 1], &device),
504 );
505 let _ = metric
506 .update(&input_batch2, &MetricMetadata::fake())
507 .unwrap();
508
509 let batch2_current = metric.value().unwrap().current();
511 assert!((batch2_current - 3.0).abs() < 0.1);
512
513 let _serialized_compute = metric.compute().unwrap();
515 let final_ppl = metric.final_value().current();
516
517 let expected_global_ppl = 2.5036;
521
522 assert!(
523 (final_ppl - expected_global_ppl).abs() < 1e-3,
524 "Expected final aggregated perplexity ~{}, got {}",
525 expected_global_ppl,
526 final_ppl
527 );
528 }
529}