Skip to main content

burn_train/metric/
base.rs

1use std::sync::Arc;
2
3use burn_core::data::dataloader::Progress;
4use burn_core::tensor::TensorReadError;
5use burn_optim::lr_scheduler::module_lr_scheduler::ModuleLearningRate;
6
7/// Metric metadata that can be used when computing metrics.
8pub struct MetricMetadata {
9    /// The current progress.
10    pub progress: Progress,
11
12    /// The current iteration.
13    pub iteration: Option<usize>,
14
15    /// The current learning rate.
16    pub lr: Option<ModuleLearningRate>,
17}
18
19impl MetricMetadata {
20    /// Fake metric metadata
21    #[cfg(test)]
22    pub fn fake() -> Self {
23        Self {
24            progress: Progress {
25                items_processed: 1,
26                items_total: 1,
27                unit: Some("items".to_string()),
28            },
29            iteration: Some(0),
30            lr: None,
31        }
32    }
33}
34
35/// Metric id that can be used to compare metrics and retrieve entries of the same metric.
36/// For now we take the name as id to make sure that the same metric has the same id across different runs.
37#[derive(Debug, Clone, new, PartialEq, Eq, Hash)]
38pub struct MetricId {
39    /// The metric id.
40    id: Arc<String>,
41}
42
43/// Metric attributes define the properties intrinsic to different types of metric.
44#[derive(Clone, Debug)]
45pub enum MetricAttributes {
46    /// Numeric attributes.
47    Numeric(NumericAttributes),
48    /// No attributes.
49    None,
50}
51
52/// Definition of a metric.
53#[derive(Clone, Debug)]
54pub struct MetricDefinition {
55    /// The metric's id.
56    pub metric_id: MetricId,
57    /// The name of the metric.
58    pub name: MetricName,
59    /// The description of the metric.
60    pub description: Option<String>,
61    /// The attributes of the metric.
62    pub attributes: MetricAttributes,
63}
64
65impl MetricDefinition {
66    /// Create a new metric definition given the metric and a unique id.
67    pub fn new<Me: Metric>(metric_id: MetricId, metric: &Me) -> Self {
68        Self {
69            metric_id,
70            name: metric.name(),
71            description: metric.description(),
72            attributes: metric.attributes(),
73        }
74    }
75}
76
77/// Metric trait.
78///
79/// # Notes
80///
81/// Implementations should define their own input type only used by the metric.
82/// This is important since some conflict may happen when the model output is adapted for each
83/// metric's input type.
84pub trait Metric: Send + Sync + Clone {
85    /// The input type of the metric.
86    type Input;
87
88    /// The parameterized name of the metric.
89    ///
90    /// This should be unique, so avoid using short generic names, prefer using the long name.
91    ///
92    /// For a metric that can exist at different parameters (e.g., top-k accuracy for different
93    /// values of k), the name should be unique for each instance.
94    fn name(&self) -> MetricName;
95
96    /// A short description of the metric.
97    fn description(&self) -> Option<String> {
98        None
99    }
100
101    /// Attributes of the metric.
102    ///
103    /// By default, metrics have no attributes.
104    fn attributes(&self) -> MetricAttributes {
105        MetricAttributes::None
106    }
107
108    /// Update the metric state and returns the current metric entry.
109    ///
110    /// # Errors
111    ///
112    /// Returns a [`TensorReadError`] when a tensor read fails. In that case, the metric state
113    /// could hold part or none of this update.
114    fn update(
115        &mut self,
116        item: &Self::Input,
117        metadata: &MetricMetadata,
118    ) -> Result<SerializedEntry, TensorReadError>;
119
120    /// Compute the final metric value.
121    ///
122    /// # Errors
123    ///
124    /// Returns a [`TensorReadError`] when a tensor read fails.
125    fn compute(&mut self) -> Result<SerializedEntry, TensorReadError>;
126
127    /// Clear the metric state.
128    fn clear(&mut self);
129}
130
131/// Type used to store metric names efficiently.
132pub type MetricName = Arc<String>;
133
134/// Adaptor are used to transform types so that they can be used by metrics.
135///
136/// This should be implemented by a model's output type for all [metric inputs](Metric::Input) that are
137/// registered with the specific learning paradigm (i.e. [SupervisedTraining](crate::SupervisedTraining)).
138pub trait Adaptor<T> {
139    /// Adapt the type to be passed to a [metric](Metric).
140    fn adapt(&self) -> T;
141}
142
143impl<T> Adaptor<()> for T {
144    fn adapt(&self) {}
145}
146
147/// Attributes that describe intrinsic properties of a numeric metric.
148#[derive(Clone, Debug)]
149pub struct NumericAttributes {
150    /// Optional unit (e.g. "%", "ms", "pixels")
151    pub unit: Option<String>,
152    /// Whether larger values are better (true) or smaller are better (false).
153    pub higher_is_better: bool,
154}
155
156impl From<NumericAttributes> for MetricAttributes {
157    fn from(attr: NumericAttributes) -> Self {
158        MetricAttributes::Numeric(attr)
159    }
160}
161
162impl Default for NumericAttributes {
163    fn default() -> Self {
164        Self {
165            unit: None,
166            higher_is_better: true,
167        }
168    }
169}
170
171/// Declare a metric to be numeric.
172///
173/// This is useful to plot the values of a metric during training.
174///
175/// Some metrics (e.g. accuracy) produce a meaningful value on every batch,
176/// while others (e.g. AUC-PR) only accumulate state per batch and are
177/// computed once all state has been accumulated.. For the latter, [`value`](Self::value)
178/// and [`running_value`](Self::running_value) return `None` until that
179/// epoch-level computation has happened, signaling that no value has been
180/// computed for the current step.
181pub trait Numeric {
182    /// Returns the numeric value of the metric, or `None` if this metric has
183    /// no meaningful value yet (e.g. it only accumulates state per batch and
184    /// computes its value once all state has been accumulated).
185    fn value(&self) -> Option<NumericEntry>;
186    /// Returns the current aggregated value of the metric over the global step,
187    /// or `None` if this metric has no meaningful value yet (e.g. it only accumulates state
188    /// per batch and computes its value once all state has been accumulated).
189    fn running_value(&self) -> Option<NumericEntry>;
190    /// Returns the final, authoritative value of the metric for the epoch.
191    /// Only meaningful after [`Metric::compute`] has been called.
192    fn final_value(&self) -> NumericEntry;
193}
194
195/// Serialized form of a metric entry.
196#[derive(Debug, Clone, new)]
197pub struct SerializedEntry {
198    /// The string to be displayed.
199    pub formatted: String,
200    /// The string to be saved.
201    pub serialized: String,
202}
203
204impl SerializedEntry {
205    /// Canonical sentinel string for metric values that have no batch-level computation.
206    pub const NOT_AVAILABLE: &'static str = "N/A";
207
208    /// Convenience helper for standard placeholder entries when no valid data is ready.
209    pub fn not_available(unit: Option<&str>) -> Self {
210        let na = Self::NOT_AVAILABLE;
211        let formatted = match unit {
212            Some(u) => format!("epoch {na} {u} - batch {na} {u}"),
213            None => format!("epoch {na} - batch {na}"),
214        };
215        Self::new(formatted, na.to_string())
216    }
217
218    /// Returns whether this entry represents an un-serializable/skipped placeholder.
219    pub fn is_not_available(&self) -> bool {
220        self.serialized == Self::NOT_AVAILABLE || self.serialized.is_empty()
221    }
222}
223
224/// Data type that contains the current state of a metric at a given time.
225#[derive(Debug, Clone)]
226pub struct MetricEntry {
227    /// Id of the entry's metric.
228    pub metric_id: MetricId,
229    /// The serialized form of the entry.
230    pub serialized_entry: SerializedEntry,
231}
232
233impl MetricEntry {
234    /// Create a new metric.
235    pub fn new(metric_id: MetricId, serialized_entry: SerializedEntry) -> Self {
236        Self {
237            metric_id,
238            serialized_entry,
239        }
240    }
241}
242
243/// Numeric metric entry.
244#[derive(Debug, Clone)]
245pub enum NumericEntry {
246    /// Single numeric value.
247    Value(f64),
248    /// Aggregated numeric (value, number of elements).
249    Aggregated {
250        /// The aggregated value of all entries.
251        aggregated_value: f64,
252        /// The number of entries present in the aggregated value.
253        count: usize,
254    },
255    /// The final, authoritative value for the epoch; computed once at epoch end
256    /// and logged in addition to the regular per-batch entries.
257    Final(f64),
258}
259
260impl NumericEntry {
261    /// Gets the current aggregated value of the metric.
262    pub fn current(&self) -> f64 {
263        match self {
264            NumericEntry::Value(val) => *val,
265            NumericEntry::Final(val) => *val,
266            NumericEntry::Aggregated {
267                aggregated_value, ..
268            } => *aggregated_value,
269        }
270    }
271
272    /// Returns a String representing the NumericEntry
273    pub fn serialize(&self) -> String {
274        match self {
275            Self::Value(v) => v.to_string(),
276            Self::Final(v) => format!("{v},final"),
277            Self::Aggregated {
278                aggregated_value,
279                count,
280            } => format!("{aggregated_value},{count}"),
281        }
282    }
283
284    /// De-serializes a string representing a NumericEntry and returns a Result containing the corresponding NumericEntry.
285    pub fn deserialize(entry: &str) -> Result<Self, String> {
286        // Check for comma separated values
287        let values = entry.split(',').collect::<Vec<_>>();
288        let num_values = values.len();
289
290        if num_values == 1 {
291            // Numeric value
292            match values[0].parse::<f64>() {
293                Ok(value) => Ok(NumericEntry::Value(value)),
294                Err(err) => Err(err.to_string()),
295            }
296        } else if num_values == 2 {
297            let (v0, v1) = (values[0], values[1]);
298            // Check if it's tagged as an explicit epoch-level final calculation
299            if v1 == "final" {
300                match v0.parse::<f64>() {
301                    Ok(value) => Ok(NumericEntry::Final(value)),
302                    Err(err) => Err(err.to_string()),
303                }
304            } else {
305                // Aggregated numeric (value, number of elements)
306                match v0.parse::<f64>() {
307                    Ok(value) => match v1.parse::<usize>() {
308                        Ok(numel) => Ok(NumericEntry::Aggregated {
309                            aggregated_value: value,
310                            count: numel,
311                        }),
312                        Err(err) => Err(err.to_string()),
313                    },
314                    Err(err) => Err(err.to_string()),
315                }
316            }
317        } else {
318            Err("Invalid number of values for numeric entry".to_string())
319        }
320    }
321
322    /// Compare this numeric metric's value with another one using the specified direction.
323    pub fn better_than(&self, other: &NumericEntry, higher_is_better: bool) -> bool {
324        (self.current() > other.current()) == higher_is_better
325    }
326}
327
328/// Format a float with the given precision. Will use scientific notation if necessary.
329pub fn format_float(float: f64, precision: usize) -> String {
330    let scientific_notation_threshold = 0.1_f64.powf(precision as f64 - 1.0);
331
332    match scientific_notation_threshold >= float {
333        true => format!("{float:.precision$e}"),
334        false => format!("{float:.precision$}"),
335    }
336}