Skip to main content

burn_train/logger/
metric.rs

1use super::{AsyncLogger, FileLogger, InMemoryLogger, Logger};
2use crate::metric::{
3    MetricDefinition, MetricEntry, MetricId, NumericEntry,
4    store::{EpochSummary, MetricsUpdate, Split},
5};
6use std::{
7    collections::HashMap,
8    fs,
9    path::{Path, PathBuf},
10};
11
12const EPOCH_PREFIX: &str = "epoch-";
13
14/// Metric logger.
15pub trait MetricLogger: Send {
16    /// Logs an item.
17    ///
18    /// # Arguments
19    ///
20    /// * `update` - Update information for all registered metrics.
21    /// * `epoch` - Current epoch.
22    /// * `split` - Current dataset split.
23    fn log(&mut self, update: MetricsUpdate, epoch: usize, split: &Split);
24
25    /// Read the logs for an epoch.
26    fn read_numeric(
27        &mut self,
28        name: &str,
29        epoch: usize,
30        split: &Split,
31    ) -> Result<Vec<NumericEntry>, String>;
32
33    /// Logs the metric definition information (name, description, unit, etc.)
34    fn log_metric_definition(&mut self, definition: MetricDefinition);
35
36    /// Logs summary at the end of the epoch.
37    fn log_epoch_summary(&mut self, summary: EpochSummary);
38}
39
40/// The file metric logger.
41pub struct FileMetricLogger {
42    loggers: HashMap<String, AsyncLogger<String>>,
43    directory: PathBuf,
44    metric_definitions: HashMap<MetricId, MetricDefinition>,
45    is_eval: bool,
46    last_epoch: Option<usize>,
47}
48
49impl FileMetricLogger {
50    /// Create a new file metric logger.
51    ///
52    /// # Arguments
53    ///
54    /// * `directory` - The directory.
55    ///
56    /// # Returns
57    ///
58    /// The file metric logger.
59    pub fn new(directory: impl AsRef<Path>) -> Self {
60        Self {
61            loggers: HashMap::new(),
62            directory: directory.as_ref().to_path_buf(),
63            metric_definitions: HashMap::default(),
64            is_eval: false,
65            last_epoch: None,
66        }
67    }
68
69    /// Create a new file metric logger.
70    ///
71    /// # Arguments
72    ///
73    /// * `directory` - The directory.
74    ///
75    /// # Returns
76    ///
77    /// The file metric logger.
78    pub fn new_eval(directory: impl AsRef<Path>) -> Self {
79        Self {
80            loggers: HashMap::new(),
81            directory: directory.as_ref().to_path_buf(),
82            metric_definitions: HashMap::default(),
83            is_eval: true,
84            last_epoch: None,
85        }
86    }
87
88    pub(crate) fn split_exists(&self, split: &Split) -> bool {
89        self.split_dir(split).is_some()
90    }
91
92    pub(crate) fn split_dir(&self, split: &Split) -> Option<PathBuf> {
93        let split_path = match split {
94            Split::Test(Some(tag)) => self.directory.join(split.to_string()).join(tag.as_str()),
95            other => self.directory.join(other.to_string()),
96        };
97        (split_path.exists() && split_path.is_dir()).then_some(split_path)
98    }
99
100    pub(crate) fn is_epoch_dir<P: AsRef<str>>(dirname: P) -> bool {
101        dirname.as_ref().starts_with(EPOCH_PREFIX)
102    }
103
104    /// Number of epochs recorded.
105    pub(crate) fn epochs(&self) -> usize {
106        if self.is_eval {
107            log::warn!("Number of epochs not available when testing.");
108            return 0;
109        }
110
111        let mut max_epoch = 0;
112
113        // with split
114        for path in fs::read_dir(&self.directory).unwrap() {
115            let path = path.unwrap();
116
117            if fs::metadata(path.path()).unwrap().is_dir() {
118                for split_path in fs::read_dir(path.path()).unwrap() {
119                    let split_path = split_path.unwrap();
120
121                    if fs::metadata(split_path.path()).unwrap().is_dir() {
122                        let dir_name = split_path.file_name().into_string().unwrap();
123
124                        if !dir_name.starts_with(EPOCH_PREFIX) {
125                            continue;
126                        }
127
128                        let epoch = dir_name.replace(EPOCH_PREFIX, "").parse::<usize>().ok();
129
130                        if let Some(epoch) = epoch
131                            && epoch > max_epoch
132                        {
133                            max_epoch = epoch;
134                        }
135                    }
136                }
137            }
138        }
139
140        max_epoch
141    }
142
143    fn train_directory(&self, epoch: usize, split: &Split) -> PathBuf {
144        let name = format!("{EPOCH_PREFIX}{epoch}");
145
146        match split {
147            Split::Train | Split::Valid | Split::Test(None) => {
148                self.directory.join(split.to_string()).join(name)
149            }
150            Split::Test(Some(tag)) => {
151                let tag = format_tag(tag);
152                self.directory.join(split.to_string()).join(tag).join(name)
153            }
154        }
155    }
156
157    fn eval_directory(&self, split: &Split) -> PathBuf {
158        match split {
159            Split::Train | Split::Valid | Split::Test(None) => self.directory.clone(),
160            Split::Test(Some(tag)) => self.directory.join(split.to_string()).join(format_tag(tag)),
161        }
162    }
163
164    fn file_path(&self, name: &str, epoch: Option<usize>, split: &Split) -> PathBuf {
165        let directory = match epoch {
166            Some(epoch) => self.train_directory(epoch, split),
167            None => self.eval_directory(split),
168        };
169        let name = name.replace(' ', "_");
170        let name = format!("{name}.log");
171        directory.join(name)
172    }
173
174    fn create_directory(&self, epoch: Option<usize>, split: &Split) {
175        let directory = match epoch {
176            Some(epoch) => self.train_directory(epoch, split),
177            None => self.eval_directory(split),
178        };
179        std::fs::create_dir_all(directory).ok();
180    }
181}
182
183impl FileMetricLogger {
184    fn log_item(&mut self, item: &MetricEntry, epoch: Option<usize>, split: &Split) {
185        // Skip writing placeholders to disk for global-only metrics
186        if item.serialized_entry.is_not_available() {
187            return;
188        }
189
190        let name = &self.metric_definitions.get(&item.metric_id).unwrap().name;
191        let key = logger_key(name, split);
192        let value = &item.serialized_entry.serialized;
193
194        let logger = match self.loggers.get_mut(&key) {
195            Some(val) => val,
196            None => {
197                self.create_directory(epoch, split);
198
199                let file_path = self.file_path(name, epoch, split);
200                let logger = FileLogger::new(file_path);
201                let logger = AsyncLogger::new(logger);
202
203                self.loggers.insert(key.clone(), logger);
204                self.loggers
205                    .get_mut(&key)
206                    .expect("Can get the previously saved logger.")
207            }
208        };
209
210        logger.log(value.clone());
211    }
212}
213
214fn format_tag(tag: &str) -> String {
215    tag.trim().replace(' ', "-").to_lowercase()
216}
217
218impl MetricLogger for FileMetricLogger {
219    fn log(&mut self, update: MetricsUpdate, epoch: usize, split: &Split) {
220        if !self.is_eval && self.last_epoch != Some(epoch) {
221            self.loggers.clear();
222            self.last_epoch = Some(epoch);
223        }
224
225        let entries: Vec<_> = update
226            .entries
227            .iter()
228            .chain(
229                update
230                    .entries_numeric
231                    .iter()
232                    .map(|numeric_update| &numeric_update.entry),
233            )
234            .cloned()
235            .collect();
236
237        let epoch = if !self.is_eval { Some(epoch) } else { None };
238        for item in entries.iter() {
239            self.log_item(item, epoch, split);
240        }
241    }
242
243    fn read_numeric(
244        &mut self,
245        name: &str,
246        epoch: usize,
247        split: &Split,
248    ) -> Result<Vec<NumericEntry>, String> {
249        if let Some(value) = self.loggers.get(name) {
250            value.sync()
251        }
252
253        let file_path = self.file_path(name, Some(epoch), split);
254
255        let mut errors = false;
256
257        let data = std::fs::read_to_string(file_path)
258            .unwrap_or_default()
259            .split('\n')
260            .filter_map(|value| {
261                if value.is_empty() {
262                    None
263                } else {
264                    match NumericEntry::deserialize(value) {
265                        Ok(value) => Some(value),
266                        Err(err) => {
267                            log::error!("{err}");
268                            errors = true;
269                            None
270                        }
271                    }
272                }
273            })
274            .collect();
275
276        if errors {
277            Err("Parsing numeric entry errors".to_string())
278        } else {
279            Ok(data)
280        }
281    }
282
283    fn log_metric_definition(&mut self, definition: MetricDefinition) {
284        self.metric_definitions
285            .insert(definition.metric_id.clone(), definition);
286    }
287
288    fn log_epoch_summary(&mut self, _summary: EpochSummary) {
289        if !self.is_eval {
290            self.loggers.clear();
291        }
292    }
293}
294
295fn logger_key(name: &str, split: &Split) -> String {
296    match split {
297        Split::Train | Split::Valid => format!("{name}_{split}"),
298        Split::Test(tag) => {
299            if let Some(t) = tag {
300                format!("{name}_{split}_{}", *t)
301            } else {
302                format!("{name}_{split}")
303            }
304        }
305    }
306}
307
308/// In memory metric logger, useful when testing and debugging.
309#[derive(Default)]
310pub struct InMemoryMetricLogger {
311    values: HashMap<String, Vec<InMemoryLogger>>,
312    last_epoch: Option<usize>,
313    metric_definitions: HashMap<MetricId, MetricDefinition>,
314}
315
316impl InMemoryMetricLogger {
317    /// Create a new in-memory metric logger.
318    pub fn new() -> Self {
319        Self::default()
320    }
321}
322
323impl MetricLogger for InMemoryMetricLogger {
324    fn log(&mut self, update: MetricsUpdate, epoch: usize, split: &Split) {
325        if self.last_epoch != Some(epoch) {
326            self.values
327                .values_mut()
328                .for_each(|loggers| loggers.push(InMemoryLogger::default()));
329            self.last_epoch = Some(epoch);
330        }
331
332        let entries: Vec<_> = update
333            .entries
334            .iter()
335            .chain(
336                update
337                    .entries_numeric
338                    .iter()
339                    .map(|numeric_update| &numeric_update.entry),
340            )
341            .cloned()
342            .collect();
343
344        for item in entries.iter() {
345            let name = &self.metric_definitions.get(&item.metric_id).unwrap().name;
346            let key = logger_key(name, split);
347
348            if !self.values.contains_key(&key) {
349                self.values
350                    .insert(key.to_string(), vec![InMemoryLogger::default()]);
351            }
352
353            let values = self.values.get_mut(&key).unwrap();
354
355            values
356                .last_mut()
357                .unwrap()
358                .log(item.serialized_entry.serialized.clone());
359        }
360    }
361
362    fn read_numeric(
363        &mut self,
364        name: &str,
365        epoch: usize,
366        split: &Split,
367    ) -> Result<Vec<NumericEntry>, String> {
368        let key = logger_key(name, split);
369        let values = match self.values.get(&key) {
370            Some(values) => values,
371            None => return Ok(Vec::new()),
372        };
373
374        match values.get(epoch - 1) {
375            Some(logger) => Ok(logger
376                .values
377                .iter()
378                .filter_map(|value| NumericEntry::deserialize(value).ok())
379                .collect()),
380            None => Ok(Vec::new()),
381        }
382    }
383
384    fn log_metric_definition(&mut self, definition: MetricDefinition) {
385        self.metric_definitions
386            .insert(definition.metric_id.clone(), definition);
387    }
388
389    fn log_epoch_summary(&mut self, _summary: EpochSummary) {}
390}