From 86d4fc2ade4255c61ad5845067b5acb68d35dabb Mon Sep 17 00:00:00 2001 From: xmotli02 Date: Sun, 26 Jul 2020 22:42:14 +0200 Subject: [PATCH 01/13] Added basic file logger #1803 --- CHANGELOG.md | 2 + docs/source/loggers.rst | 6 + pytorch_lightning/loggers/__init__.py | 2 + pytorch_lightning/loggers/file_logger.py | 180 +++++++++++++++++++++++ tests/loggers/test_file_logger.py | 85 +++++++++++ 5 files changed, 275 insertions(+) create mode 100644 pytorch_lightning/loggers/file_logger.py create mode 100644 tests/loggers/test_file_logger.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e800c28964ff..eecb499333594 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). - Added SyncBN for DDP ([#2801](https://github.com/PyTorchLightning/pytorch-lightning/pull/2801)) +- Added FileLogger ([#2721](https://github.com/PyTorchLightning/pytorch-lightning/pull/2721)) + - Added SSIM metrics ([#2671](https://github.com/PyTorchLightning/pytorch-lightning/pull/2671)) - Added BLEU metrics ([#2535](https://github.com/PyTorchLightning/pytorch-lightning/pull/2535)) diff --git a/docs/source/loggers.rst b/docs/source/loggers.rst index 1877e9f3eff5a..2b0acaf5603f3 100644 --- a/docs/source/loggers.rst +++ b/docs/source/loggers.rst @@ -339,4 +339,10 @@ Test-tube ^^^^^^^^^ .. autoclass:: pytorch_lightning.loggers.test_tube.TestTubeLogger + :noindex: + +FileLogger +^^^^^^^^^^ + +.. autoclass:: pytorch_lightning.loggers.file_logger.FileLogger :noindex: \ No newline at end of file diff --git a/pytorch_lightning/loggers/__init__.py b/pytorch_lightning/loggers/__init__.py index daa2b99bb80c6..d2e713ba2c012 100644 --- a/pytorch_lightning/loggers/__init__.py +++ b/pytorch_lightning/loggers/__init__.py @@ -2,6 +2,8 @@ from pytorch_lightning.loggers.base import LightningLoggerBase, LoggerCollection from pytorch_lightning.loggers.tensorboard import TensorBoardLogger +from pytorch_lightning.loggers.file_logger import FileLogger + __all__ = [ 'LightningLoggerBase', diff --git a/pytorch_lightning/loggers/file_logger.py b/pytorch_lightning/loggers/file_logger.py new file mode 100644 index 0000000000000..adb9105466867 --- /dev/null +++ b/pytorch_lightning/loggers/file_logger.py @@ -0,0 +1,180 @@ +""" +File logger +----------- +""" +import io +import os +import csv +import torch + +from argparse import Namespace +from typing import Optional, Dict, Any, Union + + +from pytorch_lightning import _logger as log +from pytorch_lightning.core.saving import save_hparams_to_yaml +from pytorch_lightning.loggers.base import LightningLoggerBase +from pytorch_lightning.utilities.distributed import rank_zero_only + + +class ExperimentWriter(object): + NAME_HPARAMS_FILE = 'hparams.yaml' + NAME_METRICS_FILE = 'metrics.csv' + + def __init__(self, log_dir): + self.hparams = {} + self.metrics = [] + self.metrics_keys = ["step"] + + self.log_dir = log_dir + if not os.path.exists(log_dir): + os.makedirs(log_dir) + + self.metrics_file_path = os.path.join(self.log_dir, self.NAME_METRICS_FILE) + + def log_hparams(self, params): + self.hparams.update(params) + + def log_metrics(self, metrics_dict, step=None): + def _handle_value(value): + if isinstance(value, torch.Tensor): + return value.item() + return value + + if step is None: + step = len(self.metrics) + + new_row = dict.fromkeys(self.metrics_keys) + new_row['step'] = step + for k, v in metrics_dict.items(): + if k not in self.metrics_keys: + self.metrics_keys.append(k) + new_row[k] = _handle_value(v) + self.metrics.append(new_row) + + def save(self): + hparams_file = os.path.join(self.log_dir, self.NAME_HPARAMS_FILE) + save_hparams_to_yaml(hparams_file, self.hparams) + + if self.metrics: + with io.open(self.metrics_file_path, 'w', newline='') as f: + self.writer = csv.DictWriter(f, fieldnames=self.metrics_keys) + self.writer.writeheader() + self.writer.writerows(self.metrics) + + +class FileLogger(LightningLoggerBase): + r""" + Log to local file system in yaml and CSV format. Logs are saved to + ``os.path.join(save_dir, name, version)``. + + Example: + >>> from pytorch_lightning import Trainer + >>> from pytorch_lightning.loggers import FileLogger + >>> logger = FileLogger("logs", name="my_exp_name") + >>> trainer = Trainer(logger=logger) + + Args: + save_dir: Save directory + name: Experiment name. Defaults to ``'default'``. + version: Experiment version. If version is not specified the logger inspects the save + directory for existing versions, then automatically assigns the next available version. + """ + + def __init__(self, + save_dir: str, + name: Optional[str] = "default", + version: Optional[Union[int, str]] = None): + + super().__init__() + self._save_dir = save_dir + self._name = name or '' + self._version = version + self._experiment = None + + @property + def root_dir(self) -> str: + """ + Parent directory for all checkpoint subdirectories. + If the experiment name parameter is ``None`` or the empty string, no experiment subdirectory is used + and the checkpoint will be saved in "save_dir/version_dir" + """ + if self.name is None or len(self.name) == 0: + return self._save_dir + return os.path.join(self._save_dir, self.name) + + @property + def log_dir(self) -> str: + """ + The log directory for this run. By default, it is named + ``'version_${self.version}'`` but it can be overridden by passing a string value + for the constructor's version parameter instead of ``None`` or an int. + """ + # create a pseudo standard path ala test-tube + version = self.version if isinstance(self.version, str) else f"version_{self.version}" + log_dir = os.path.join(self.root_dir, version) + return log_dir + + @property + def experiment(self) -> ExperimentWriter: + r""" + + Actual ExperimentWriter object. To use ExperimentWriter features in your + :class:`~pytorch_lightning.core.lightning.LightningModule` do the following. + + Example:: + + self.logger.experiment.some_experiment_writer_function() + + """ + if self._experiment is not None: + return self._experiment + + os.makedirs(self.root_dir, exist_ok=True) + self._experiment = ExperimentWriter(log_dir=self.log_dir) + return self._experiment + + @rank_zero_only + def log_hyperparams(self, params: Union[Dict[str, Any], Namespace]) -> None: + params = self._convert_params(params) + self.experiment.log_hparams(params) + + @rank_zero_only + def log_metrics(self, metrics: Dict[str, float], step: Optional[int] = None) -> None: + self.experiment.log_metrics(metrics, step) + + @rank_zero_only + def save(self) -> None: + super().save() + self.experiment.save() + + @rank_zero_only + def finalize(self, status: str) -> None: + self.save() + + @property + def name(self) -> str: + return self._name + + @property + def version(self) -> int: + if self._version is None: + self._version = self._get_next_version() + return self._version + + def _get_next_version(self): + root_dir = os.path.join(self._save_dir, self.name) + + if not os.path.isdir(root_dir): + log.warning('Missing logger folder: %s', root_dir) + return 0 + + existing_versions = [] + for d in os.listdir(root_dir): + if os.path.isdir(os.path.join(root_dir, d)) and d.startswith("version_"): + existing_versions.append(int(d.split("_")[1])) + + if len(existing_versions) == 0: + return 0 + + return max(existing_versions) + 1 diff --git a/tests/loggers/test_file_logger.py b/tests/loggers/test_file_logger.py new file mode 100644 index 0000000000000..517d1238f844a --- /dev/null +++ b/tests/loggers/test_file_logger.py @@ -0,0 +1,85 @@ +from argparse import Namespace + +import pytest +import torch +import os + +from pytorch_lightning.loggers import FileLogger + + +def test_file_logger_automatic_versioning(tmpdir): + """Verify that automatic versioning works""" + + root_dir = tmpdir.mkdir("exp") + root_dir.mkdir("version_0") + root_dir.mkdir("version_1") + + logger = FileLogger(save_dir=tmpdir, name="exp") + + assert logger.version == 2 + + +def test_file_logger_manual_versioning(tmpdir): + """Verify that manual versioning works""" + + root_dir = tmpdir.mkdir("exp") + root_dir.mkdir("version_0") + root_dir.mkdir("version_1") + root_dir.mkdir("version_2") + + logger = FileLogger(save_dir=tmpdir, name="exp", version=1) + + assert logger.version == 1 + + +def test_file_logger_named_version(tmpdir): + """Verify that manual versioning works for string versions, e.g. '2020-02-05-162402' """ + + exp_name = "exp" + tmpdir.mkdir(exp_name) + expected_version = "2020-02-05-162402" + + logger = FileLogger(save_dir=tmpdir, name=exp_name, version=expected_version) + logger.log_hyperparams({"a": 1, "b": 2}) + logger.save() + assert logger.version == expected_version + assert os.listdir(tmpdir / exp_name) == [expected_version] + assert os.listdir(tmpdir / exp_name / expected_version) + + +@pytest.mark.parametrize("name", ['', None]) +def test_file_logger_no_name(tmpdir, name): + """Verify that None or empty name works""" + logger = FileLogger(save_dir=tmpdir, name=name) + logger.save() + assert logger.root_dir == tmpdir + assert os.listdir(tmpdir / 'version_0') + + +@pytest.mark.parametrize("step_idx", [10, None]) +def test_file_logger_log_metrics(tmpdir, step_idx): + logger = FileLogger(tmpdir) + metrics = { + "float": 0.3, + "int": 1, + "FloatTensor": torch.tensor(0.1), + "IntTensor": torch.tensor(1) + } + logger.log_metrics(metrics, step_idx) + logger.save() + + +def test_file_logger_log_hyperparams(tmpdir): + logger = FileLogger(tmpdir) + hparams = { + "float": 0.3, + "int": 1, + "string": "abc", + "bool": True, + "dict": {'a': {'b': 'c'}}, + "list": [1, 2, 3], + "namespace": Namespace(foo=Namespace(bar='buzz')), + "layer": torch.nn.BatchNorm1d + } + logger.log_hyperparams(hparams) + logger.save() From ef9619a22fbb49ff6269576bf4ee5fde3922f9ef Mon Sep 17 00:00:00 2001 From: xmotli02 Date: Thu, 30 Jul 2020 09:17:49 +0200 Subject: [PATCH 02/13] fixup! Added basic file logger #1803 --- pytorch_lightning/loggers/__init__.py | 3 +- .../loggers/{file_logger.py => csv.py} | 41 +++++++++++++++---- .../{test_file_logger.py => test_csv.py} | 14 +++---- 3 files changed, 42 insertions(+), 16 deletions(-) rename pytorch_lightning/loggers/{file_logger.py => csv.py} (83%) rename tests/loggers/{test_file_logger.py => test_csv.py} (84%) diff --git a/pytorch_lightning/loggers/__init__.py b/pytorch_lightning/loggers/__init__.py index d2e713ba2c012..3b46a31dbba85 100644 --- a/pytorch_lightning/loggers/__init__.py +++ b/pytorch_lightning/loggers/__init__.py @@ -2,13 +2,14 @@ from pytorch_lightning.loggers.base import LightningLoggerBase, LoggerCollection from pytorch_lightning.loggers.tensorboard import TensorBoardLogger -from pytorch_lightning.loggers.file_logger import FileLogger +from pytorch_lightning.loggers.csv import CSVLogger __all__ = [ 'LightningLoggerBase', 'LoggerCollection', 'TensorBoardLogger', + 'CSVLogger', ] try: diff --git a/pytorch_lightning/loggers/file_logger.py b/pytorch_lightning/loggers/csv.py similarity index 83% rename from pytorch_lightning/loggers/file_logger.py rename to pytorch_lightning/loggers/csv.py index adb9105466867..6b87fb5474f8e 100644 --- a/pytorch_lightning/loggers/file_logger.py +++ b/pytorch_lightning/loggers/csv.py @@ -1,6 +1,9 @@ """ -File logger ------------ +CSV logger +---------- + +CSV logger for basic experiment logging that does not require opening ports + """ import io import os @@ -14,10 +17,25 @@ from pytorch_lightning import _logger as log from pytorch_lightning.core.saving import save_hparams_to_yaml from pytorch_lightning.loggers.base import LightningLoggerBase -from pytorch_lightning.utilities.distributed import rank_zero_only +from pytorch_lightning.utilities.distributed import rank_zero_warn, rank_zero_only class ExperimentWriter(object): + r""" + Experiment writer for CSVLogger. + + Currently supports to log hyperparameters and metrics in YAML and CSV + format. Creates the directory structure: + ``` + log_dir/ + hparams.yaml + metrics.csv + ``` + + Args: + log_dir: Directory for the experiment logs + """ + NAME_HPARAMS_FILE = 'hparams.yaml' NAME_METRICS_FILE = 'metrics.csv' @@ -27,15 +45,21 @@ def __init__(self, log_dir): self.metrics_keys = ["step"] self.log_dir = log_dir - if not os.path.exists(log_dir): - os.makedirs(log_dir) + if os.path.exists(self.log_dir): + rank_zero_warn( + f"Experiment logs directory {self.log_dir} exists and is not empty. " + "Previous log files in this directory will be deleted when the new ones are saved!" + ) + os.makedirs(self.log_dir, exist_ok=True) self.metrics_file_path = os.path.join(self.log_dir, self.NAME_METRICS_FILE) def log_hparams(self, params): + """Record hparams""" self.hparams.update(params) def log_metrics(self, metrics_dict, step=None): + """Record metrics""" def _handle_value(value): if isinstance(value, torch.Tensor): return value.item() @@ -53,6 +77,7 @@ def _handle_value(value): self.metrics.append(new_row) def save(self): + """Save recorded hparams and metrics into files""" hparams_file = os.path.join(self.log_dir, self.NAME_HPARAMS_FILE) save_hparams_to_yaml(hparams_file, self.hparams) @@ -63,15 +88,15 @@ def save(self): self.writer.writerows(self.metrics) -class FileLogger(LightningLoggerBase): +class CSVLogger(LightningLoggerBase): r""" Log to local file system in yaml and CSV format. Logs are saved to ``os.path.join(save_dir, name, version)``. Example: >>> from pytorch_lightning import Trainer - >>> from pytorch_lightning.loggers import FileLogger - >>> logger = FileLogger("logs", name="my_exp_name") + >>> from pytorch_lightning.loggers import CSVLogger + >>> logger = CSVLogger("logs", name="my_exp_name") >>> trainer = Trainer(logger=logger) Args: diff --git a/tests/loggers/test_file_logger.py b/tests/loggers/test_csv.py similarity index 84% rename from tests/loggers/test_file_logger.py rename to tests/loggers/test_csv.py index 517d1238f844a..e4de63e618592 100644 --- a/tests/loggers/test_file_logger.py +++ b/tests/loggers/test_csv.py @@ -4,7 +4,7 @@ import torch import os -from pytorch_lightning.loggers import FileLogger +from pytorch_lightning.loggers import CSVLogger def test_file_logger_automatic_versioning(tmpdir): @@ -14,7 +14,7 @@ def test_file_logger_automatic_versioning(tmpdir): root_dir.mkdir("version_0") root_dir.mkdir("version_1") - logger = FileLogger(save_dir=tmpdir, name="exp") + logger = CSVLogger(save_dir=tmpdir, name="exp") assert logger.version == 2 @@ -27,7 +27,7 @@ def test_file_logger_manual_versioning(tmpdir): root_dir.mkdir("version_1") root_dir.mkdir("version_2") - logger = FileLogger(save_dir=tmpdir, name="exp", version=1) + logger = CSVLogger(save_dir=tmpdir, name="exp", version=1) assert logger.version == 1 @@ -39,7 +39,7 @@ def test_file_logger_named_version(tmpdir): tmpdir.mkdir(exp_name) expected_version = "2020-02-05-162402" - logger = FileLogger(save_dir=tmpdir, name=exp_name, version=expected_version) + logger = CSVLogger(save_dir=tmpdir, name=exp_name, version=expected_version) logger.log_hyperparams({"a": 1, "b": 2}) logger.save() assert logger.version == expected_version @@ -50,7 +50,7 @@ def test_file_logger_named_version(tmpdir): @pytest.mark.parametrize("name", ['', None]) def test_file_logger_no_name(tmpdir, name): """Verify that None or empty name works""" - logger = FileLogger(save_dir=tmpdir, name=name) + logger = CSVLogger(save_dir=tmpdir, name=name) logger.save() assert logger.root_dir == tmpdir assert os.listdir(tmpdir / 'version_0') @@ -58,7 +58,7 @@ def test_file_logger_no_name(tmpdir, name): @pytest.mark.parametrize("step_idx", [10, None]) def test_file_logger_log_metrics(tmpdir, step_idx): - logger = FileLogger(tmpdir) + logger = CSVLogger(tmpdir) metrics = { "float": 0.3, "int": 1, @@ -70,7 +70,7 @@ def test_file_logger_log_metrics(tmpdir, step_idx): def test_file_logger_log_hyperparams(tmpdir): - logger = FileLogger(tmpdir) + logger = CSVLogger(tmpdir) hparams = { "float": 0.3, "int": 1, From 06f493cce2041adb106c16a89f212d3924dc5ad9 Mon Sep 17 00:00:00 2001 From: xmotli02 Date: Thu, 30 Jul 2020 09:23:18 +0200 Subject: [PATCH 03/13] fixup! Added basic file logger #1803 --- CHANGELOG.md | 2 +- docs/source/loggers.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eecb499333594..b16281e831c22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). - Added SyncBN for DDP ([#2801](https://github.com/PyTorchLightning/pytorch-lightning/pull/2801)) -- Added FileLogger ([#2721](https://github.com/PyTorchLightning/pytorch-lightning/pull/2721)) +- Added CSVLogger ([#2721](https://github.com/PyTorchLightning/pytorch-lightning/pull/2721)) - Added SSIM metrics ([#2671](https://github.com/PyTorchLightning/pytorch-lightning/pull/2671)) diff --git a/docs/source/loggers.rst b/docs/source/loggers.rst index 2b0acaf5603f3..09ecdaf045805 100644 --- a/docs/source/loggers.rst +++ b/docs/source/loggers.rst @@ -341,8 +341,8 @@ Test-tube .. autoclass:: pytorch_lightning.loggers.test_tube.TestTubeLogger :noindex: -FileLogger +CSVLogger ^^^^^^^^^^ -.. autoclass:: pytorch_lightning.loggers.file_logger.FileLogger +.. autoclass:: pytorch_lightning.loggers.csv.CSVLogger :noindex: \ No newline at end of file From 5ae2af2b3589fdf1e077777df3adf1fcbd38a10f Mon Sep 17 00:00:00 2001 From: xmotli02 Date: Thu, 30 Jul 2020 09:37:22 +0200 Subject: [PATCH 04/13] fixup! Added basic file logger #1803 --- pytorch_lightning/loggers/csv.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pytorch_lightning/loggers/csv.py b/pytorch_lightning/loggers/csv.py index 6b87fb5474f8e..a3c1e46348d14 100644 --- a/pytorch_lightning/loggers/csv.py +++ b/pytorch_lightning/loggers/csv.py @@ -25,12 +25,7 @@ class ExperimentWriter(object): Experiment writer for CSVLogger. Currently supports to log hyperparameters and metrics in YAML and CSV - format. Creates the directory structure: - ``` - log_dir/ - hparams.yaml - metrics.csv - ``` + format, respectively. Args: log_dir: Directory for the experiment logs From bc2f233bf5fae17f70850581605fb923494f71c4 Mon Sep 17 00:00:00 2001 From: xmotli02 Date: Thu, 30 Jul 2020 09:38:27 +0200 Subject: [PATCH 05/13] fixup! Added basic file logger #1803 --- docs/source/loggers.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/loggers.rst b/docs/source/loggers.rst index 09ecdaf045805..f62961bf06ffd 100644 --- a/docs/source/loggers.rst +++ b/docs/source/loggers.rst @@ -342,7 +342,7 @@ Test-tube :noindex: CSVLogger -^^^^^^^^^^ +^^^^^^^^^ .. autoclass:: pytorch_lightning.loggers.csv.CSVLogger :noindex: \ No newline at end of file From 018baeb5ecc0dcd8ca07c55cd78463a04a5cea75 Mon Sep 17 00:00:00 2001 From: xmotli02 Date: Thu, 30 Jul 2020 10:31:10 +0200 Subject: [PATCH 06/13] fixup! Added basic file logger #1803 --- pytorch_lightning/loggers/csv.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pytorch_lightning/loggers/csv.py b/pytorch_lightning/loggers/csv.py index a3c1e46348d14..e9aa13c127cc1 100644 --- a/pytorch_lightning/loggers/csv.py +++ b/pytorch_lightning/loggers/csv.py @@ -34,7 +34,7 @@ class ExperimentWriter(object): NAME_HPARAMS_FILE = 'hparams.yaml' NAME_METRICS_FILE = 'metrics.csv' - def __init__(self, log_dir): + def __init__(self, log_dir: str) -> None: self.hparams = {} self.metrics = [] self.metrics_keys = ["step"] @@ -49,11 +49,11 @@ def __init__(self, log_dir): self.metrics_file_path = os.path.join(self.log_dir, self.NAME_METRICS_FILE) - def log_hparams(self, params): + def log_hparams(self, params: Dict[str, Any]) -> None: """Record hparams""" self.hparams.update(params) - def log_metrics(self, metrics_dict, step=None): + def log_metrics(self, metrics_dict: Dict[str, float], step: Optional[int] = None) -> None: """Record metrics""" def _handle_value(value): if isinstance(value, torch.Tensor): @@ -71,7 +71,7 @@ def _handle_value(value): new_row[k] = _handle_value(v) self.metrics.append(new_row) - def save(self): + def save(self) -> None: """Save recorded hparams and metrics into files""" hparams_file = os.path.join(self.log_dir, self.NAME_HPARAMS_FILE) save_hparams_to_yaml(hparams_file, self.hparams) @@ -135,6 +135,10 @@ def log_dir(self) -> str: log_dir = os.path.join(self.root_dir, version) return log_dir + @property + def save_dir(self) -> Optional[str]: + return self._save_dir + @property def experiment(self) -> ExperimentWriter: r""" From e7bb7921f2a47d5f98f0695f1e1f4c299f09c679 Mon Sep 17 00:00:00 2001 From: Jirka Borovec Date: Wed, 5 Aug 2020 18:40:11 +0200 Subject: [PATCH 07/13] csv --- pytorch_lightning/loggers/__init__.py | 2 +- .../loggers/{csv.py => csv_logs.py} | 28 ++++++++++--------- 2 files changed, 16 insertions(+), 14 deletions(-) rename pytorch_lightning/loggers/{csv.py => csv_logs.py} (90%) diff --git a/pytorch_lightning/loggers/__init__.py b/pytorch_lightning/loggers/__init__.py index 3b46a31dbba85..5f2f3044d0a65 100644 --- a/pytorch_lightning/loggers/__init__.py +++ b/pytorch_lightning/loggers/__init__.py @@ -2,7 +2,7 @@ from pytorch_lightning.loggers.base import LightningLoggerBase, LoggerCollection from pytorch_lightning.loggers.tensorboard import TensorBoardLogger -from pytorch_lightning.loggers.csv import CSVLogger +from pytorch_lightning.loggers.csv_logs import CSVLogger __all__ = [ diff --git a/pytorch_lightning/loggers/csv.py b/pytorch_lightning/loggers/csv_logs.py similarity index 90% rename from pytorch_lightning/loggers/csv.py rename to pytorch_lightning/loggers/csv_logs.py index e9aa13c127cc1..ad916710fa200 100644 --- a/pytorch_lightning/loggers/csv.py +++ b/pytorch_lightning/loggers/csv_logs.py @@ -37,7 +37,6 @@ class ExperimentWriter(object): def __init__(self, log_dir: str) -> None: self.hparams = {} self.metrics = [] - self.metrics_keys = ["step"] self.log_dir = log_dir if os.path.exists(self.log_dir): @@ -63,24 +62,27 @@ def _handle_value(value): if step is None: step = len(self.metrics) - new_row = dict.fromkeys(self.metrics_keys) - new_row['step'] = step - for k, v in metrics_dict.items(): - if k not in self.metrics_keys: - self.metrics_keys.append(k) - new_row[k] = _handle_value(v) - self.metrics.append(new_row) + metrics = {k: _handle_value(v) for k, v in metrics_dict.items()} + metrics['step'] = step + self.metrics.append(metrics) def save(self) -> None: """Save recorded hparams and metrics into files""" hparams_file = os.path.join(self.log_dir, self.NAME_HPARAMS_FILE) save_hparams_to_yaml(hparams_file, self.hparams) - if self.metrics: - with io.open(self.metrics_file_path, 'w', newline='') as f: - self.writer = csv.DictWriter(f, fieldnames=self.metrics_keys) - self.writer.writeheader() - self.writer.writerows(self.metrics) + if not self.metrics: + return + + last_m = {} + for m in self.metrics: + last_m.update(m) + metrics_keys = list(last_m.keys()) + + with io.open(self.metrics_file_path, 'w', newline='') as f: + self.writer = csv.DictWriter(f, fieldnames=metrics_keys) + self.writer.writeheader() + self.writer.writerows(self.metrics) class CSVLogger(LightningLoggerBase): From c2c4b7d14937482ae68bba5460fdcecf15129930 Mon Sep 17 00:00:00 2001 From: Jirka Borovec Date: Wed, 5 Aug 2020 21:19:35 +0200 Subject: [PATCH 08/13] Apply suggestions from code review --- CHANGELOG.md | 2 +- pytorch_lightning/loggers/csv_logs.py | 14 ++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b16281e831c22..bf8d002bce0e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). - Added SyncBN for DDP ([#2801](https://github.com/PyTorchLightning/pytorch-lightning/pull/2801)) -- Added CSVLogger ([#2721](https://github.com/PyTorchLightning/pytorch-lightning/pull/2721)) +- Added basic `CSVLogger` ([#2721](https://github.com/PyTorchLightning/pytorch-lightning/pull/2721)) - Added SSIM metrics ([#2671](https://github.com/PyTorchLightning/pytorch-lightning/pull/2671)) diff --git a/pytorch_lightning/loggers/csv_logs.py b/pytorch_lightning/loggers/csv_logs.py index ad916710fa200..1e395abadb293 100644 --- a/pytorch_lightning/loggers/csv_logs.py +++ b/pytorch_lightning/loggers/csv_logs.py @@ -9,11 +9,9 @@ import os import csv import torch - from argparse import Namespace from typing import Optional, Dict, Any, Union - from pytorch_lightning import _logger as log from pytorch_lightning.core.saving import save_hparams_to_yaml from pytorch_lightning.loggers.base import LightningLoggerBase @@ -41,8 +39,8 @@ def __init__(self, log_dir: str) -> None: self.log_dir = log_dir if os.path.exists(self.log_dir): rank_zero_warn( - f"Experiment logs directory {self.log_dir} exists and is not empty. " - "Previous log files in this directory will be deleted when the new ones are saved!" + f"Experiment logs directory {self.log_dir} exists and is not empty." + " Previous log files in this directory will be deleted when the new ones are saved!" ) os.makedirs(self.log_dir, exist_ok=True) @@ -121,9 +119,9 @@ def root_dir(self) -> str: If the experiment name parameter is ``None`` or the empty string, no experiment subdirectory is used and the checkpoint will be saved in "save_dir/version_dir" """ - if self.name is None or len(self.name) == 0: - return self._save_dir - return os.path.join(self._save_dir, self.name) + if not self.name: + return self.save_dir + return os.path.join(self.save_dir, self.name) @property def log_dir(self) -> str: @@ -153,7 +151,7 @@ def experiment(self) -> ExperimentWriter: self.logger.experiment.some_experiment_writer_function() """ - if self._experiment is not None: + if self._experiment: return self._experiment os.makedirs(self.root_dir, exist_ok=True) From 051f453b73cc15998ed47cc9897b753ec31c7e2b Mon Sep 17 00:00:00 2001 From: Jirka Borovec Date: Wed, 5 Aug 2020 21:23:21 +0200 Subject: [PATCH 09/13] tests --- tests/loggers/test_all.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/loggers/test_all.py b/tests/loggers/test_all.py index 5bd81d7116948..da0c9a623e4bf 100644 --- a/tests/loggers/test_all.py +++ b/tests/loggers/test_all.py @@ -5,11 +5,13 @@ import platform from unittest import mock +import cloudpickle import pytest import tests.base.develop_utils as tutils from pytorch_lightning import Trainer, Callback from pytorch_lightning.loggers import ( + CSVLogger, TensorBoardLogger, MLFlowLogger, NeptuneLogger, @@ -34,6 +36,7 @@ def _get_logger_args(logger_class, save_dir): @pytest.mark.parametrize("logger_class", [ TensorBoardLogger, + CSVLogger, CometLogger, MLFlowLogger, NeptuneLogger, @@ -85,6 +88,7 @@ def log_metrics(self, metrics, step): @pytest.mark.parametrize("logger_class", [ + CSVLogger, TensorBoardLogger, CometLogger, MLFlowLogger, @@ -148,6 +152,7 @@ def name(self): @pytest.mark.parametrize("logger_class", [ TensorBoardLogger, + CSVLogger, CometLogger, MLFlowLogger, NeptuneLogger, @@ -170,6 +175,7 @@ def test_loggers_pickle(tmpdir, monkeypatch, logger_class): # test pickling loggers pickle.dumps(logger) + cloudpickle.dumps(logger) trainer = Trainer( max_epochs=1, @@ -226,6 +232,7 @@ def on_train_batch_start(self, trainer, pl_module): @pytest.mark.skipif(platform.system() == "Windows", reason="Distributed training is not supported on Windows") @pytest.mark.parametrize("logger_class", [ TensorBoardLogger, + CSVLogger, CometLogger, MLFlowLogger, NeptuneLogger, From dafdb742e7f007d784c977863ef4f38bfbed40ad Mon Sep 17 00:00:00 2001 From: Jirka Borovec Date: Wed, 5 Aug 2020 21:29:03 +0200 Subject: [PATCH 10/13] tests --- tests/loggers/test_csv.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/loggers/test_csv.py b/tests/loggers/test_csv.py index e4de63e618592..f2e8ab85fb9e5 100644 --- a/tests/loggers/test_csv.py +++ b/tests/loggers/test_csv.py @@ -4,7 +4,9 @@ import torch import os +from pytorch_lightning.core.saving import load_hparams_from_yaml from pytorch_lightning.loggers import CSVLogger +from pytorch_lightning.loggers.csv_logs import ExperimentWriter def test_file_logger_automatic_versioning(tmpdir): @@ -68,6 +70,10 @@ def test_file_logger_log_metrics(tmpdir, step_idx): logger.log_metrics(metrics, step_idx) logger.save() + path_yaml = os.path.join(logger.log_dir, ExperimentWriter.NAME_HPARAMS_FILE) + params = load_hparams_from_yaml(path_yaml) + assert all([n in params for n in metrics]) + def test_file_logger_log_hyperparams(tmpdir): logger = CSVLogger(tmpdir) From 982cce3cbbbbcecfade118d2b07a210c99274577 Mon Sep 17 00:00:00 2001 From: Jirka Borovec Date: Wed, 5 Aug 2020 21:39:28 +0200 Subject: [PATCH 11/13] tests --- pytorch_lightning/core/saving.py | 2 +- tests/loggers/test_csv.py | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/pytorch_lightning/core/saving.py b/pytorch_lightning/core/saving.py index 5e3ef1d97236d..37c63de1804ab 100644 --- a/pytorch_lightning/core/saving.py +++ b/pytorch_lightning/core/saving.py @@ -313,7 +313,7 @@ def load_hparams_from_yaml(config_yaml: str) -> Dict[str, Any]: return {} with open(config_yaml) as fp: - tags = yaml.load(fp, Loader=yaml.SafeLoader) + tags = yaml.load(fp) return tags diff --git a/tests/loggers/test_csv.py b/tests/loggers/test_csv.py index f2e8ab85fb9e5..3bc8330075e6a 100644 --- a/tests/loggers/test_csv.py +++ b/tests/loggers/test_csv.py @@ -70,9 +70,11 @@ def test_file_logger_log_metrics(tmpdir, step_idx): logger.log_metrics(metrics, step_idx) logger.save() - path_yaml = os.path.join(logger.log_dir, ExperimentWriter.NAME_HPARAMS_FILE) - params = load_hparams_from_yaml(path_yaml) - assert all([n in params for n in metrics]) + path_csv = os.path.join(logger.log_dir, ExperimentWriter.NAME_METRICS_FILE) + with open(path_csv, 'r') as fp: + lines = fp.readlines() + assert len(lines) == 2 + assert all([n in lines[0] for n in metrics]) def test_file_logger_log_hyperparams(tmpdir): @@ -89,3 +91,7 @@ def test_file_logger_log_hyperparams(tmpdir): } logger.log_hyperparams(hparams) logger.save() + + path_yaml = os.path.join(logger.log_dir, ExperimentWriter.NAME_HPARAMS_FILE) + params = load_hparams_from_yaml(path_yaml) + assert all([n in params for n in hparams]) From f03a7f7645ce53d535f1154a24a3a0525a934b49 Mon Sep 17 00:00:00 2001 From: Jirka Borovec Date: Wed, 5 Aug 2020 22:34:51 +0200 Subject: [PATCH 12/13] miss --- tests/loggers/test_all.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/loggers/test_all.py b/tests/loggers/test_all.py index da0c9a623e4bf..7978aa8e41ace 100644 --- a/tests/loggers/test_all.py +++ b/tests/loggers/test_all.py @@ -232,7 +232,7 @@ def on_train_batch_start(self, trainer, pl_module): @pytest.mark.skipif(platform.system() == "Windows", reason="Distributed training is not supported on Windows") @pytest.mark.parametrize("logger_class", [ TensorBoardLogger, - CSVLogger, + # CSVLogger, # todo CometLogger, MLFlowLogger, NeptuneLogger, From 6a85720226fe6b99033a660e4176544bf86086ae Mon Sep 17 00:00:00 2001 From: Jirka Borovec Date: Wed, 5 Aug 2020 22:40:50 +0200 Subject: [PATCH 13/13] docs --- docs/source/loggers.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/loggers.rst b/docs/source/loggers.rst index f62961bf06ffd..e04ba1af5ca1c 100644 --- a/docs/source/loggers.rst +++ b/docs/source/loggers.rst @@ -344,5 +344,5 @@ Test-tube CSVLogger ^^^^^^^^^ -.. autoclass:: pytorch_lightning.loggers.csv.CSVLogger +.. autoclass:: pytorch_lightning.loggers.csv_logs.CSVLogger :noindex: \ No newline at end of file