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}