1use std::sync::Arc;
2
3use burn_core::tensor::{Bool, Tensor, TensorReadError};
4
5use crate::metric::{ClassReduction, MetricName, NumericEntry, SerializedEntry, format_float};
6
7#[derive(Clone)]
14pub struct NumericMetricState {
15 sum: f64,
16 count: usize,
17 current: f64,
18 current_count: usize,
19}
20
21#[derive(Clone)]
26pub struct PredictionAccumulatorState {
27 predictions: Vec<Tensor<2>>,
28 targets: Vec<Tensor<2, Bool>>,
29 current: Option<f64>,
30}
31
32pub struct FormatOptions {
34 name: Arc<String>,
35 unit: Option<String>,
36 precision: Option<usize>,
37}
38
39impl PredictionAccumulatorState {
40 pub fn new() -> Self {
42 Self {
43 predictions: vec![],
44 targets: vec![],
45 current: None,
46 }
47 }
48
49 pub fn accumulate(&mut self, preds: Tensor<2>, targets: Tensor<2, Bool>) {
51 self.predictions.push(preds);
52 self.targets.push(targets);
53 }
54
55 pub fn tensors(&self) -> (Tensor<2>, Tensor<2, Bool>) {
57 (
58 Tensor::cat(self.predictions.clone(), 0),
59 Tensor::cat(self.targets.clone(), 0),
60 )
61 }
62
63 pub fn compute(&mut self, value: f64, format: FormatOptions) -> SerializedEntry {
66 self.current = Some(value);
67
68 let serialized = NumericEntry::Value(value).serialize();
69
70 let formatted_value = match format.precision {
71 Some(precision) => format_float(value, precision),
72 None => format!("{value}"),
73 };
74
75 let formatted = match format.unit {
77 Some(unit) => format!("epoch {formatted_value} {unit} - batch N/A {unit}"),
78 None => format!("epoch {formatted_value} - batch N/A"),
79 };
80
81 SerializedEntry::new(formatted, serialized)
82 }
83
84 pub fn value(&self) -> Option<NumericEntry> {
86 self.current.map(NumericEntry::Value)
87 }
88
89 pub fn reset(&mut self) {
91 self.predictions.clear();
92 self.targets.clear();
93 self.current = None;
94 }
95
96 pub(crate) fn is_empty(&self) -> bool {
97 self.predictions.is_empty()
98 }
99
100 pub(crate) fn serialize_placeholder(&self, format: FormatOptions) -> SerializedEntry {
101 SerializedEntry::not_available(format.unit.as_deref())
102 }
103}
104
105impl Default for PredictionAccumulatorState {
106 fn default() -> Self {
107 Self::new()
108 }
109}
110
111impl FormatOptions {
112 pub fn new(name: MetricName) -> Self {
114 Self {
115 name: name.clone(),
116 unit: None,
117 precision: None,
118 }
119 }
120
121 pub fn unit(mut self, unit: &str) -> Self {
123 self.unit = Some(unit.to_string());
124 self
125 }
126
127 pub fn precision(mut self, precision: usize) -> Self {
129 self.precision = Some(precision);
130 self
131 }
132
133 pub fn name(&self) -> &Arc<String> {
135 &self.name
136 }
137
138 pub fn unit_value(&self) -> &Option<String> {
140 &self.unit
141 }
142
143 pub fn precision_value(&self) -> Option<usize> {
145 self.precision
146 }
147}
148
149impl NumericMetricState {
150 pub fn new() -> Self {
152 Self {
153 sum: 0.0,
154 count: 0,
155 current: f64::NAN,
156 current_count: 0,
157 }
158 }
159
160 pub fn reset(&mut self) {
162 self.sum = 0.0;
163 self.count = 0;
164 self.current = f64::NAN;
165 self.current_count = 0;
166 }
167
168 pub fn update(&mut self, value: f64, count: usize) {
175 self.current = value;
176 self.current_count = count;
177
178 if count > 0 {
179 self.sum += value * count as f64;
180 self.count += count;
181 }
182 }
183
184 pub fn compute_update(&self, format: FormatOptions) -> SerializedEntry {
186 self.compute(format, false)
187 }
188
189 pub fn compute_final(&self, format: FormatOptions) -> SerializedEntry {
191 self.compute(format, true)
192 }
193
194 fn compute(&self, format: FormatOptions, final_entry: bool) -> SerializedEntry {
195 let value_current = self.current;
196 let count = self.current_count;
197 let value_running = self.sum / self.count as f64;
198 let serialized = if final_entry {
200 NumericEntry::Final(value_running).serialize()
201 } else {
202 NumericEntry::Aggregated {
203 aggregated_value: value_current,
204 count,
205 }
206 .serialize()
207 };
208
209 let (formatted_current, formatted_running) = match format.precision {
210 Some(precision) => (
211 format_float(value_current, precision),
212 format_float(value_running, precision),
213 ),
214 None => (format!("{value_current}"), format!("{value_running}")),
215 };
216
217 let formatted = match format.unit {
219 Some(unit) => {
220 format!("epoch {formatted_running} {unit} - batch {formatted_current} {unit}")
221 }
222 None => format!("epoch {formatted_running} - batch {formatted_current}"),
223 };
224
225 SerializedEntry::new(formatted, serialized)
226 }
227
228 pub fn current_value(&self) -> NumericEntry {
230 NumericEntry::Aggregated {
231 aggregated_value: self.current,
232 count: self.current_count,
233 }
234 }
235
236 pub fn running_value(&self) -> NumericEntry {
238 NumericEntry::Aggregated {
239 aggregated_value: self.sum / self.count as f64,
240 count: self.count,
241 }
242 }
243
244 pub fn final_value(&self) -> NumericEntry {
246 NumericEntry::Final(self.sum / self.count as f64)
247 }
248}
249
250impl Default for NumericMetricState {
251 fn default() -> Self {
252 Self::new()
253 }
254}
255
256#[derive(Clone)]
264pub struct ConfusionStatsState {
265 true_positive: Option<Tensor<1>>,
267 false_positive: Option<Tensor<1>>,
268 false_negative: Option<Tensor<1>>,
269 running_count: usize,
270
271 current_tp: Option<Tensor<1>>,
273 current_fp: Option<Tensor<1>>,
274 current_fn: Option<Tensor<1>>,
275 current_count: usize,
276
277 current_value: f64,
279 running_value: f64,
280}
281
282impl Default for ConfusionStatsState {
283 fn default() -> Self {
284 Self::new()
285 }
286}
287
288impl ConfusionStatsState {
289 pub fn new() -> Self {
291 Self {
292 true_positive: None,
293 false_positive: None,
294 false_negative: None,
295 running_count: 0,
296 current_tp: None,
297 current_fp: None,
298 current_fn: None,
299 current_count: 0,
300 current_value: f64::NAN,
301 running_value: f64::NAN,
302 }
303 }
304
305 pub fn reset(&mut self) {
307 self.true_positive = None;
308 self.false_positive = None;
309 self.false_negative = None;
310 self.running_count = 0;
311 self.current_tp = None;
312 self.current_fp = None;
313 self.current_fn = None;
314 self.current_count = 0;
315 self.current_value = f64::NAN;
316 self.running_value = f64::NAN;
317 }
318
319 fn accumulate(running: &mut Option<Tensor<1>>, batch: Option<Tensor<1>>) {
320 if let Some(b) = batch {
321 *running = Some(match running.take() {
322 Some(acc) => acc + b,
323 None => b,
324 });
325 }
326 }
327
328 pub fn update(
330 &mut self,
331 tp: Option<Tensor<1>>,
332 fp: Option<Tensor<1>>,
333 fn_: Option<Tensor<1>>,
334 sample_size: usize,
335 ) {
336 Self::accumulate(&mut self.true_positive, tp.clone());
338 Self::accumulate(&mut self.false_positive, fp.clone());
339 Self::accumulate(&mut self.false_negative, fn_.clone());
340 self.running_count += sample_size;
341
342 self.current_tp = tp;
344 self.current_fp = fp;
345 self.current_fn = fn_;
346 self.current_count = sample_size;
347 }
348
349 pub fn compute_update(
355 &mut self,
356 class_reduction: ClassReduction,
357 format: FormatOptions,
358 compute_fn: impl Fn(Option<Tensor<1>>, Option<Tensor<1>>, Option<Tensor<1>>) -> Tensor<1>,
359 ) -> Result<SerializedEntry, TensorReadError> {
360 let current_tp = self.current_tp.take();
361 let current_fp = self.current_fp.take();
362 let current_fn = self.current_fn.take();
363
364 let batch_metric = compute_fn(current_tp, current_fp, current_fn);
366 self.current_value = Self::class_average(batch_metric, class_reduction)?;
367
368 let total_tp = self.true_positive.clone();
370 let total_fp = self.false_positive.clone();
371 let total_fn = self.false_negative.clone();
372
373 let epoch_metric = compute_fn(total_tp, total_fp, total_fn);
374 self.running_value = Self::class_average(epoch_metric, class_reduction)?;
375
376 let serialized = NumericEntry::Aggregated {
378 aggregated_value: self.current_value,
379 count: self.current_count,
380 }
381 .serialize();
382 Ok(self.serialized_entry(format, serialized))
383 }
384
385 pub fn compute_final(&mut self, format: FormatOptions) -> SerializedEntry {
387 self.serialized_entry(format, NumericEntry::Final(self.running_value).serialize())
388 }
389
390 fn serialized_entry(&mut self, format: FormatOptions, serialized: String) -> SerializedEntry {
391 let (formatted_current, formatted_running) = match format.precision {
392 Some(p) => (
393 format_float(self.current_value, p),
394 format_float(self.running_value, p),
395 ),
396 None => (
397 format!("{}", self.current_value),
398 format!("{}", self.running_value),
399 ),
400 };
401
402 let formatted = match format.unit {
403 Some(unit) => {
404 format!("epoch {formatted_running} {unit} - batch {formatted_current} {unit}")
405 }
406 None => format!("epoch {formatted_running} - batch {formatted_current}"),
407 };
408
409 SerializedEntry::new(formatted, serialized)
410 }
411
412 fn class_average(
413 mut metric: Tensor<1>,
414 class_reduction: ClassReduction,
415 ) -> Result<f64, TensorReadError> {
416 use ClassReduction::{Macro, Micro};
417 let avg = match class_reduction {
418 Micro => metric,
419 Macro => {
420 if metric
421 .clone()
422 .contains_nan()
423 .any()
424 .try_into_scalar::<bool>()?
425 {
426 let mask = metric.clone().is_nan();
427 metric = metric
428 .clone()
429 .select(0, mask.bool_not().argwhere().squeeze_dim(1));
430 }
431 metric.mean()
432 }
433 };
434 avg.try_into_scalar::<f64>()
435 }
436
437 pub fn current_value(&self) -> Option<NumericEntry> {
439 (!self.current_value.is_nan()).then_some(NumericEntry::Aggregated {
440 aggregated_value: self.current_value,
441 count: self.current_count,
442 })
443 }
444
445 pub fn running_value(&self) -> Option<NumericEntry> {
447 (!self.running_value.is_nan()).then_some(NumericEntry::Aggregated {
448 aggregated_value: self.running_value,
449 count: self.running_count,
450 })
451 }
452
453 pub fn final_value(&self) -> NumericEntry {
455 NumericEntry::Final(self.running_value)
458 }
459}