Skip to content

Commit 0804837

Browse files
committed
Make TensorFlow optional and fix event_types param
1 parent 4bd8740 commit 0804837

File tree

9 files changed

+152
-24
lines changed

9 files changed

+152
-24
lines changed

.github/workflows/test-with-tox.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ jobs:
1212
platform:
1313
- ubuntu-18.04
1414
- ubuntu-20.04 # ubuntu-latest
15+
- ubuntu-22.04
1516
- macos-10.15
1617
- macOS-11 # macos-latest
1718
- macos-12
@@ -25,7 +26,7 @@ jobs:
2526
with:
2627
python-version: ${{ matrix.python-version }}
2728
- name: install libsndfile for linux
28-
if: matrix.platform == 'ubuntu-18.04' || matrix.platform == 'ubuntu-20.04'
29+
if: matrix.platform == 'ubuntu-18.04' || matrix.platform == 'ubuntu-20.04' || matrix.platform == 'ubuntu-22.04'
2930
run: sudo apt-get install -y libsndfile1
3031
- name: Install dependencies
3132
run: |

README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,15 @@ A simple yet powerful tensorboard event log parser/reader.
2121
* Both the documentation and code have high test coverage rate.
2222
* Follows [PEP 484](https://www.python.org/dev/peps/pep-0484/) with full type hints.
2323

24-
Installation: (Requires python >= 3.7)
24+
Installation:
2525

2626
```sh
27-
pip install -U tbparse
27+
pip install tensorflow # or tensorflow-cpu
28+
pip install -U tbparse # requires Python >= 3.7
2829
```
2930

31+
**Note**: If you don't want to install TensorFlow, see [Installing without TensorFlow](https://tbparse.readthedocs.io/en/latest/pages/installation.html#installing-without-tensorflow).
32+
3033
We suggest using an additional virtual environment for parsing and plotting the tensorboard events. So no worries if your training code uses Python 3.6 or older versions.
3134

3235
Reading one or more event files with tbparse only requires 5 lines of code:
@@ -66,10 +69,11 @@ All events above are generated and plotted in [gallery-pytorch.ipynb](https://gi
6669
## Installation
6770

6871
```sh
69-
pip install -U tbparse
72+
pip install tensorflow # or tensorflow-cpu
73+
pip install -U tbparse # requires Python >= 3.7
7074
```
7175

72-
(Requires python >= 3.7)
76+
**Note**: If you don't want to install TensorFlow, see [Installing without TensorFlow](https://tbparse.readthedocs.io/en/latest/pages/installation.html#installing-without-tensorflow).
7377

7478
## Testing the Source Code
7579

docs/index.rst

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,14 @@ A simple yet powerful tensorboard event log parser/reader:
4848
* Both the documentation and code have high test coverage rate.
4949
* Follows `PEP 484 <https://www.python.org/dev/peps/pep-0484/>`_ with full type hints.
5050

51-
Installation: (Requires python >= 3.7)
51+
Installation:
5252

5353
.. code-block:: bash
5454
55-
pip install -U tbparse
55+
pip install tensorflow # or tensorflow-cpu
56+
pip install -U tbparse # requires Python >= 3.7
57+
58+
**Note**: If you don't want to install TensorFlow, see :ref:`Installing without TensorFlow <tbparse_installing-without-tensorflow>`.
5659

5760
We suggest using an additional virtual environment for parsing and plotting
5861
the tensorboard events. So no worries if your training code uses Python 3.6

docs/pages/installation.rst

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,62 @@
1+
.. _tbparse_installation:
2+
13
===================================
24
Installation
35
===================================
46

57
.. highlight:: sh
68

7-
(Requires python >= 3.7)
8-
99
Install from PyPI:
1010

1111
.. code-block:: bash
1212
13-
pip install -U tbparse
13+
pip install tensorflow # or tensorflow-cpu
14+
pip install -U tbparse # requires Python >= 3.7
15+
16+
**Note**: If you don't want to install TensorFlow, see :ref:`Installing without TensorFlow <tbparse_installing-without-tensorflow>`.
1417

1518
Install from Source:
1619

1720
.. code-block:: bash
1821
1922
git clone https://github.com/j3soon/tbparse
2023
cd tbparse
21-
pip install -e .
24+
pip install tensorflow # or tensorflow-cpu
25+
pip install -e . # requires Python >= 3.7
26+
27+
.. _tbparse_installing-without-tensorflow:
28+
29+
Installing without TensorFlow
30+
===================================
31+
32+
You can install tbparse with reduced feature set if you don't want to install TensorFlow:
33+
34+
.. code-block:: bash
35+
36+
# Don't install TensorFlow
37+
pip install -U tbparse # requires Python >= 3.7
38+
39+
Without TensorFlow, tbparse supports parsing
40+
:ref:`scalars <tbparse_parsing-scalars>`,
41+
:ref:`histograms <tbparse_parsing-histograms>`, and
42+
:ref:`hparams <tbparse_parsing-hparams>`,
43+
but doesn't support parsing
44+
:ref:`tensors <tbparse_parsing-tensors>`,
45+
:ref:`images <tbparse_parsing-images>`,
46+
:ref:`audio <tbparse_parsing-audio>`, and
47+
:ref:`text <tbparse_parsing-text>`.
48+
49+
tbparse will instruct you to install TensorFlow by raising an error if you try to parse the unsupported event types, such as:
50+
51+
ModuleNotFoundError: No module named 'tensorflow'. Please install 'tensorflow' or 'tensorflow-cpu'.
52+
53+
In addition, an error may occur if you have installed TensorFlow and TensorBoard and uninstalled TensorFlow afterwards:
54+
55+
AttributeError: module 'tensorflow' has no attribute 'io'
56+
57+
This error occurs since TensorBoard will depend on TensorFlow if TensorFlow exists in the environment.
58+
See `TensorBoard README <https://github.com/tensorflow/tensorboard#can-i-run-tensorboard-without-a-tensorflow-installation>`_
59+
for more information.
60+
61+
To resolve this issue, create a new virtual environment and install tbparse without installing TensorFlow.
62+
Or you may uninstall all packages related to TensorFlow and TensorBoard, which require much more effort.

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
python_requires=">=3.7",
3939
install_requires=[
4040
"pandas>=1.3.0",
41-
"tensorflow>=2.0.0",
41+
"tensorboard>=2.0.0",
4242
],
4343
extras_require={
4444
"testing": ["pytest", "mypy", "flake8", "pylint", "sphinx",

tbparse/summary_reader.py

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,20 @@
77
import copy
88
import os
99
from collections import defaultdict
10+
from types import ModuleType
1011
from typing import Any, Dict, List, Optional, Set, Tuple, Union, cast
1112

1213
import numpy as np
1314
import pandas as pd
14-
import tensorflow as tf
1515
from tensorboard.backend.event_processing.event_accumulator import (
1616
AUDIO, COMPRESSED_HISTOGRAMS, HISTOGRAMS, IMAGES, SCALARS,
1717
STORE_EVERYTHING_SIZE_GUIDANCE, TENSORS, AudioEvent, EventAccumulator,
1818
HistogramEvent, ImageEvent, ScalarEvent, TensorEvent)
1919
from tensorboard.plugins.hparams.plugin_data_pb2 import HParamsPluginData
20+
try:
21+
import tensorflow
22+
except ImportError:
23+
tensorflow = None
2024

2125
# pylint: disable=W0105
2226
"""
@@ -47,6 +51,7 @@
4751
}
4852

4953
ALL_EVENT_TYPES = {SCALARS, TENSORS, HISTOGRAMS, IMAGES, AUDIO, HPARAMS, TEXT}
54+
REDUCED_EVENT_TYPES = {SCALARS, HISTOGRAMS, HPARAMS}
5055
ALL_EXTRA_COLUMNS = {'dir_name', 'file_name', 'wall_time', 'min', 'max', 'num',
5156
'sum', 'sum_squares', 'width', 'height', 'content_type',
5257
'length_frames', 'sample_rate'}
@@ -98,7 +103,7 @@ def __init__(self, log_path: str, *, pivot=False, extra_columns=None,
98103
:param event_types: Specifies the event types to parse, \
99104
defaults to all event types.
100105
:type event_types: Set[{'scalars', 'tensors', 'histograms', 'images', \
101-
'audio'}]
106+
'audio', 'hparams', 'text'}]
102107
"""
103108
self._log_path: str = log_path
104109
"""Load directory location, or load file location."""
@@ -114,6 +119,8 @@ def __init__(self, log_path: str, *, pivot=False, extra_columns=None,
114119
"""Determines whether the DataFrame is stored in wide format."""
115120
self._event_types: Set[str] = (event_types or ALL_EVENT_TYPES).copy()
116121
"""Specifies the event types to parse."""
122+
if tensorflow is None:
123+
self._event_types = (event_types or REDUCED_EVENT_TYPES).copy()
117124
if not isinstance(self._event_types, set):
118125
raise ValueError(f"`event_types` should be a {set} instead of \
119126
{str(type(self._event_types))}")
@@ -148,7 +155,8 @@ def __init__(self, log_path: str, *, pivot=False, extra_columns=None,
148155
filepath = os.path.join(self.log_path, filename)
149156
r = SummaryReader(filepath,
150157
pivot=self._pivot,
151-
extra_columns=self._extra_columns)
158+
extra_columns=self._extra_columns,
159+
event_types=self._event_types)
152160
self._children[filename] = r
153161

154162
@property
@@ -250,6 +258,10 @@ def get_events(self, event_type: str) -> pd.DataFrame:
250258
"""
251259
if event_type not in ALL_EVENT_TYPES:
252260
raise ValueError(f"Unknown event_type: {event_type}")
261+
if event_type not in REDUCED_EVENT_TYPES and tensorflow is None:
262+
self._get_tensorflow() # raise error
263+
if event_type not in self._event_types:
264+
raise ValueError(f"event_type is ignored by user: {event_type}")
253265
group_columns: List[Any] = list(filter(
254266
lambda x: x in self._extra_columns, ['dir_name', 'file_name']))
255267
dfs = []
@@ -425,6 +437,13 @@ def buckets_to_histogram_dict(lst: np.ndarray) -> Dict[str, Any]:
425437
"""
426438
return SummaryReader.tensor_to_histogram(lst)
427439

440+
@staticmethod
441+
def _get_tensorflow() -> ModuleType:
442+
if tensorflow is not None:
443+
return tensorflow
444+
raise ModuleNotFoundError("No module named 'tensorflow'. " +
445+
"Please install 'tensorflow' or 'tensorflow-cpu'.")
446+
428447
@staticmethod
429448
def tensor_to_image(tensor: np.ndarray) -> Dict[str, Any]:
430449
"""Convert a tensor to image dictionary.
@@ -434,6 +453,8 @@ def tensor_to_image(tensor: np.ndarray) -> Dict[str, Any]:
434453
:return: A `{image_data_name: image_data}` dictionary.
435454
:rtype: Dict[str, Any]
436455
"""
456+
# pylint: disable=C0103
457+
tf = SummaryReader._get_tensorflow()
437458
lst = list(map(tf.image.decode_image, tensor[2:]))
438459
lst = list(map(lambda x: x.numpy(), lst))
439460
image = np.stack(lst, axis=0)
@@ -455,6 +476,8 @@ def tensor_to_audio(tensor: np.ndarray) -> Dict[str, Any]:
455476
:return: A `{audio_data_name: audio_data}` dictionary.
456477
:rtype: Dict[str, Any]
457478
"""
479+
# pylint: disable=C0103
480+
tf = SummaryReader._get_tensorflow()
458481
assert tensor[:, 1].tolist() == [b''] * tensor.shape[0]
459482
lst = list(map(tf.audio.decode_wav, tensor[:, 0]))
460483
audio_lst = list(map(lambda x: x[0].numpy(), lst))
@@ -650,6 +673,10 @@ def _get_tensor_cols(self, tag_to_events: Dict[str, TensorEvent]) -> \
650673
Dict[str, List[Any]]:
651674
"""Return a dict of lists based on the tags and TensorEvents."""
652675
cols = self._get_default_cols(tag_to_events)
676+
if len(tag_to_events) == 0:
677+
return cols
678+
# pylint: disable=C0103
679+
tf = SummaryReader._get_tensorflow()
653680
idx = 0
654681
for tag, events in tag_to_events.items():
655682
for e in events:
@@ -700,8 +727,11 @@ def _get_histogram_cols(self, tag_to_events: Dict[str, HistogramEvent]) \
700727
def _get_image_cols(self, tag_to_events: Dict[str, ImageEvent]) -> \
701728
Dict[str, List[Any]]:
702729
"""Return a dict of lists based on the tags and ImageEvent."""
703-
704730
cols = self._get_default_cols(tag_to_events)
731+
if len(tag_to_events) == 0:
732+
return cols
733+
# pylint: disable=C0103
734+
tf = SummaryReader._get_tensorflow()
705735
idx = 0
706736
for tag, events in tag_to_events.items():
707737
for e in events:
@@ -728,6 +758,10 @@ def _get_audio_cols(self, tag_to_events: Dict[str, AudioEvent]) -> \
728758
Dict[str, List[Any]]:
729759
"""Return a dict of lists based on the tags and AudioEvent."""
730760
cols = self._get_default_cols(tag_to_events)
761+
if len(tag_to_events) == 0:
762+
return cols
763+
# pylint: disable=C0103
764+
tf = SummaryReader._get_tensorflow()
731765
idx = 0
732766
for tag, events in tag_to_events.items():
733767
for e in events:
@@ -770,6 +804,10 @@ def _get_text_cols(self, tag_to_events: Dict[str, TensorEvent]) -> \
770804
Dict[str, List[Any]]:
771805
"""Return a dict of lists based on the tags and TensorEvent."""
772806
cols = self._get_default_cols(tag_to_events)
807+
if len(tag_to_events) == 0:
808+
return cols
809+
# pylint: disable=C0103
810+
tf = SummaryReader._get_tensorflow()
773811
idx = 0
774812
for tag, events in tag_to_events.items():
775813
for e in events:
@@ -894,8 +932,9 @@ def children(self) -> Dict[str, 'SummaryReader']:
894932

895933
@property
896934
def raw_tags(self) -> Dict[str, List[str]]:
897-
"""Returns a dictionary containing a list of raw tags for each raw event type.
898-
This property is only supported when `log_path` is a event file.
935+
"""Returns a dictionary containing a list of raw tags for each raw
936+
event type. This property is only supported when `log_path` is a
937+
event file.
899938
900939
:return: A `{eventType: ['list', 'of', 'tags']}` dictionary.
901940
:rtype: Dict[str, List[str]]
@@ -1013,17 +1052,17 @@ def _make_empty_dict(data) -> Dict[str, Any]:
10131052
:rtype: Dict[str, Any]
10141053
"""
10151054
return {
1016-
IMAGES: [],
1017-
AUDIO: [],
1055+
IMAGES: copy.deepcopy(data),
1056+
AUDIO: copy.deepcopy(data),
10181057
HISTOGRAMS: copy.deepcopy(data),
10191058
SCALARS: copy.deepcopy(data),
10201059
# COMPRESSED_HISTOGRAMS: [],
10211060
TENSORS: copy.deepcopy(data),
10221061
# GRAPH: [],
10231062
# META_GRAPH: [],
10241063
# RUN_METADATA: [],
1025-
HPARAMS: [],
1026-
TEXT: [],
1064+
HPARAMS: copy.deepcopy(data),
1065+
TEXT: copy.deepcopy(data),
10271066
}
10281067

10291068
def __repr__(self) -> str:

tests/test_summary_reader/test_edge_cases.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ def test_event_types(prepare, testdir):
7171
event_file = os.path.join(run_dir, event_filename)
7272
# Test default
7373
reader = SummaryReader(event_file, event_types={'tensors'})
74-
assert reader.scalars.columns.to_list() == []
74+
with pytest.raises(ValueError):
75+
reader.scalars
7576

7677
def test_get_tags(prepare, testdir):
7778
log_dir = os.path.join(testdir.tmpdir, 'run')
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import os
2+
3+
import numpy as np
4+
import pytest
5+
from tbparse import SummaryReader
6+
from torch.utils.tensorboard import SummaryWriter
7+
8+
9+
@pytest.fixture
10+
def prepare(testdir):
11+
# Ref: https://pytorch.org/docs/stable/tensorboard.html
12+
log_dir = os.path.join(testdir.tmpdir, 'run')
13+
writer = SummaryWriter(log_dir)
14+
x = range(100)
15+
for i in x:
16+
writer.add_scalar('y=2x', i * 2, i)
17+
writer.add_text('text', 'lorem ipsum', 0)
18+
writer.close()
19+
20+
def test_log_dir(prepare, testdir):
21+
log_dir = os.path.join(testdir.tmpdir, 'run')
22+
reader = SummaryReader(log_dir, pivot=True)
23+
df = reader.scalars
24+
assert df.columns.tolist() == ['step', 'y=2x']
25+
assert df['step'].to_list() == [i for i in range(100)]
26+
assert df['y=2x'].to_list() == [i*2 for i in range(100)]
27+
with pytest.raises(ModuleNotFoundError):
28+
df = reader.text
29+
with pytest.raises(ModuleNotFoundError):
30+
reader = SummaryReader(log_dir, pivot=True, event_types={'scalars', 'text'})

tox.ini

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,17 @@ allowlist_externals =
1717
make
1818

1919
commands =
20+
# Test tbparse with reduced feature set (without TensorFlow)
2021
pip install -e .[testing]
21-
pytest
22+
# May need to clean tox cache if the command below failed.
23+
pytest "{toxinidir}/tests/test_summary_reader/test_edge_cases.py" \
24+
"{toxinidir}/tests/test_summary_reader/test_histogram_torch_sample.py" \
25+
"{toxinidir}/tests/test_summary_reader/test_hparams_torch_sample.py" \
26+
"{toxinidir}/tests/test_summary_reader/test_scalar_torch_sample.py" \
27+
"{toxinidir}/tests/test_summary_reader/test_no_tensorflow.py"
28+
# Test tbparse with full feature set (with TensorFlow)
29+
pip install tensorflow
30+
pytest --ignore="{toxinidir}/tests/test_summary_reader/test_no_tensorflow.py"
2231
mypy --ignore-missing-imports tbparse
2332
flake8 tbparse
2433
pylint tbparse

0 commit comments

Comments
 (0)