drytorch 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- drytorch/__init__.py +165 -0
- drytorch/contrib/__init__.py +1 -0
- drytorch/contrib/optuna.py +265 -0
- drytorch/contrib/torcheval.py +88 -0
- drytorch/contrib/torchmetrics.py +118 -0
- drytorch/core/__init__.py +1 -0
- drytorch/core/exceptions.py +1286 -0
- drytorch/core/experimenting.py +762 -0
- drytorch/core/log_events.py +387 -0
- drytorch/core/protocols.py +508 -0
- drytorch/core/registering.py +227 -0
- drytorch/core/tracking.py +447 -0
- drytorch/lib/__init__.py +1 -0
- drytorch/lib/accumulators.py +163 -0
- drytorch/lib/checkpoints.py +416 -0
- drytorch/lib/evaluations.py +96 -0
- drytorch/lib/gradient_ops.py +646 -0
- drytorch/lib/hooks.py +476 -0
- drytorch/lib/learning.py +163 -0
- drytorch/lib/loading.py +473 -0
- drytorch/lib/models.py +530 -0
- drytorch/lib/monitoring.py +809 -0
- drytorch/lib/objectives.py +1027 -0
- drytorch/lib/runners.py +417 -0
- drytorch/lib/schedulers.py +502 -0
- drytorch/lib/training.py +759 -0
- drytorch/py.typed +0 -0
- drytorch/trackers/__init__.py +1 -0
- drytorch/trackers/base_classes.py +839 -0
- drytorch/trackers/csv.py +309 -0
- drytorch/trackers/hydra.py +120 -0
- drytorch/trackers/logging.py +433 -0
- drytorch/trackers/matplotlib.py +275 -0
- drytorch/trackers/plotly.py +98 -0
- drytorch/trackers/sqlalchemy.py +715 -0
- drytorch/trackers/tensorboard.py +180 -0
- drytorch/trackers/tqdm.py +300 -0
- drytorch/trackers/wandb.py +231 -0
- drytorch/trackers/yaml.py +235 -0
- drytorch/utils/__init__.py +1 -0
- drytorch/utils/apply_ops.py +153 -0
- drytorch/utils/averages.py +86 -0
- drytorch/utils/local_ops.py +151 -0
- drytorch/utils/repr_utils.py +575 -0
- drytorch-0.1.0.dist-info/METADATA +170 -0
- drytorch-0.1.0.dist-info/RECORD +48 -0
- drytorch-0.1.0.dist-info/WHEEL +4 -0
- drytorch-0.1.0.dist-info/licenses/LICENSE +21 -0
drytorch/__init__.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Package initialization and default tracker setup.
|
|
2
|
+
|
|
3
|
+
It automatically initializes some trackers with sets of settings (modes) that
|
|
4
|
+
work well together. The mode can be set as an environmental variable
|
|
5
|
+
DRYTORCH_INIT_MODE before loading the package or explicitly reset after.
|
|
6
|
+
The DRYTORCH_STRICT environment variable turns reproducibility warnings into
|
|
7
|
+
errors when set to 1, requiring a readable commit.
|
|
8
|
+
|
|
9
|
+
Available modes:
|
|
10
|
+
1) standard: log to stderr, preferring tqdm over the built-in logger.
|
|
11
|
+
2) hydra: log to stdout and accommodate default Hydra settings.
|
|
12
|
+
3) minimal: reduce output and avoid dumping metadata.
|
|
13
|
+
4) none: no tracker is added to the default ones.
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
INIT_MODE: the mode the trackers will be initialized with at the start.
|
|
17
|
+
If DRYTORCH_INIT_MODE is not present, it defaults to "standard".
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import importlib.metadata
|
|
21
|
+
import logging
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
import warnings
|
|
25
|
+
|
|
26
|
+
from typing import Literal, TypeGuard
|
|
27
|
+
|
|
28
|
+
from drytorch.core import exceptions
|
|
29
|
+
from drytorch.core.exceptions import FailedOptionalImportWarning
|
|
30
|
+
from drytorch.core.experimenting import Experiment
|
|
31
|
+
from drytorch.core.tracking import (
|
|
32
|
+
Tracker,
|
|
33
|
+
extend_default_trackers,
|
|
34
|
+
remove_all_default_trackers,
|
|
35
|
+
)
|
|
36
|
+
from drytorch.lib.evaluations import Evaluation, Test, Validation
|
|
37
|
+
from drytorch.lib.learning import LearningSchema
|
|
38
|
+
from drytorch.lib.loading import DataLoader
|
|
39
|
+
from drytorch.lib.models import Model
|
|
40
|
+
from drytorch.lib.objectives import Loss, Metric
|
|
41
|
+
from drytorch.lib.training import Trainer
|
|
42
|
+
from drytorch.trackers import logging as builtin_logging
|
|
43
|
+
from drytorch.trackers.logging import INFO_LEVELS
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
__version__ = importlib.metadata.version('drytorch')
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
'DataLoader',
|
|
50
|
+
'Evaluation',
|
|
51
|
+
'Experiment',
|
|
52
|
+
'LearningSchema',
|
|
53
|
+
'Loss',
|
|
54
|
+
'Metric',
|
|
55
|
+
'Model',
|
|
56
|
+
'Test',
|
|
57
|
+
'Trainer',
|
|
58
|
+
'Validation',
|
|
59
|
+
'extend_default_trackers',
|
|
60
|
+
'init_trackers',
|
|
61
|
+
'remove_all_default_trackers',
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
_InitMode = Literal['standard', 'hydra', 'minimal']
|
|
65
|
+
|
|
66
|
+
logger = logging.getLogger('drytorch')
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def init_trackers(mode: _InitMode = 'standard') -> None:
|
|
70
|
+
"""Initialize trackers used by default during the experiment.
|
|
71
|
+
|
|
72
|
+
Three initializations are available:
|
|
73
|
+
1) standard: log to stderr, preferring tqdm over the built-in logger.
|
|
74
|
+
2) hydra: log to stdout and accommodate default Hydra settings.
|
|
75
|
+
3) minimal: reduce output and avoid dumping metadata.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
mode: one of the suggested initialization modes.
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
ValueError if mode is not available.
|
|
82
|
+
"""
|
|
83
|
+
remove_all_default_trackers()
|
|
84
|
+
tracker_list: list[Tracker] = [builtin_logging.BuiltinLogger()]
|
|
85
|
+
is_tqdm_installed = _add_tqdm(tracker_list, mode=mode)
|
|
86
|
+
if mode == 'hydra':
|
|
87
|
+
# hydra logs to stdout by default
|
|
88
|
+
builtin_logging.enable_default_handler(sys.stdout)
|
|
89
|
+
builtin_logging.enable_propagation()
|
|
90
|
+
_add_yaml(tracker_list)
|
|
91
|
+
verbosity = builtin_logging.INFO_LEVELS.metrics
|
|
92
|
+
|
|
93
|
+
elif mode == 'minimal':
|
|
94
|
+
if is_tqdm_installed:
|
|
95
|
+
verbosity = builtin_logging.INFO_LEVELS.training
|
|
96
|
+
else:
|
|
97
|
+
verbosity = builtin_logging.INFO_LEVELS.epoch
|
|
98
|
+
builtin_logging.set_formatter('progress')
|
|
99
|
+
|
|
100
|
+
elif mode == 'standard':
|
|
101
|
+
_add_yaml(tracker_list)
|
|
102
|
+
if is_tqdm_installed:
|
|
103
|
+
verbosity = builtin_logging.INFO_LEVELS.epoch
|
|
104
|
+
else:
|
|
105
|
+
verbosity = builtin_logging.INFO_LEVELS.metrics
|
|
106
|
+
|
|
107
|
+
else:
|
|
108
|
+
raise ValueError('Mode {mode} not available.')
|
|
109
|
+
|
|
110
|
+
extend_default_trackers(tracker_list)
|
|
111
|
+
builtin_logging.set_verbosity(verbosity)
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _add_tqdm(tracker_list: list[Tracker], mode: _InitMode) -> bool:
|
|
116
|
+
try:
|
|
117
|
+
from drytorch.trackers import tqdm
|
|
118
|
+
except (ImportError, ModuleNotFoundError):
|
|
119
|
+
warnings.warn(FailedOptionalImportWarning('tqdm'), stacklevel=2)
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
if mode == 'hydra':
|
|
123
|
+
# progress bar disappears leaving only log metrics.
|
|
124
|
+
tqdm_logger = tqdm.TqdmLogger(leave=False)
|
|
125
|
+
elif mode == 'minimal':
|
|
126
|
+
# double bar replaces most logs.
|
|
127
|
+
tqdm_logger = tqdm.TqdmLogger(enable_training_bar=True)
|
|
128
|
+
else:
|
|
129
|
+
# console metrics from the progress bar
|
|
130
|
+
tqdm_logger = tqdm.TqdmLogger()
|
|
131
|
+
|
|
132
|
+
tracker_list.append(tqdm_logger)
|
|
133
|
+
return True
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _add_yaml(tracker_list: list[Tracker]) -> bool:
|
|
137
|
+
try:
|
|
138
|
+
from drytorch.trackers import yaml
|
|
139
|
+
except (ImportError, ModuleNotFoundError):
|
|
140
|
+
warnings.warn(FailedOptionalImportWarning('yaml'), stacklevel=2)
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
tracker_list.append(yaml.MetadataDumper())
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _check_mode_is_valid(
|
|
148
|
+
mode: str,
|
|
149
|
+
) -> TypeGuard[Literal['standard', 'hydra', 'minimal']]:
|
|
150
|
+
return mode in ('standard', 'hydra', 'minimal')
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
INIT_MODE = os.getenv('DRYTORCH_INIT_MODE', 'standard')
|
|
154
|
+
if _check_mode_is_valid(INIT_MODE):
|
|
155
|
+
logger.log(INFO_LEVELS.internal, 'Initializing %s mode.', INIT_MODE)
|
|
156
|
+
init_trackers(INIT_MODE)
|
|
157
|
+
elif INIT_MODE != 'none':
|
|
158
|
+
raise ValueError(f'DRYTORCH_INIT_MODE: {INIT_MODE} not a valid setting.')
|
|
159
|
+
|
|
160
|
+
warnings.filterwarnings('ignore', category=exceptions.GitUnavailableWarning)
|
|
161
|
+
_strict = os.getenv('DRYTORCH_STRICT', '0')
|
|
162
|
+
if _strict == '1':
|
|
163
|
+
warnings.filterwarnings('error', category=exceptions.ReproducibilityWarning)
|
|
164
|
+
elif _strict != '0':
|
|
165
|
+
raise ValueError(f'DRYTORCH_STRICT: {_strict} not a valid setting.')
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Extensions and contributions."""
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""Support for optuna."""
|
|
2
|
+
|
|
3
|
+
import collections
|
|
4
|
+
import string
|
|
5
|
+
|
|
6
|
+
from collections.abc import Callable, Sequence
|
|
7
|
+
from typing import Any, Final, TypeVar
|
|
8
|
+
|
|
9
|
+
import omegaconf
|
|
10
|
+
import optuna
|
|
11
|
+
|
|
12
|
+
from typing_extensions import override
|
|
13
|
+
|
|
14
|
+
from drytorch.core import exceptions
|
|
15
|
+
from drytorch.core import protocols as p
|
|
16
|
+
from drytorch.lib import hooks, monitoring
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
'OptunaError',
|
|
21
|
+
'TrialCallback',
|
|
22
|
+
'get_final_value',
|
|
23
|
+
'suggest_overrides',
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
_Target = TypeVar('_Target', bound=p.TargetType)
|
|
27
|
+
_Output = TypeVar('_Output', bound=p.OutputType)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class OptunaError(exceptions.DryTorchError):
|
|
31
|
+
"""Base class for Optuna errors."""
|
|
32
|
+
|
|
33
|
+
_template = 'Optuna: {}'
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TrialCallback(hooks.TrainerHook):
|
|
37
|
+
"""Report a metric to an optuna trial and prune the training.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
reported: the values reported at each epoch.
|
|
41
|
+
trial: ``optuna`` trial.
|
|
42
|
+
watch: what the callback reports.
|
|
43
|
+
when: decides whether the hook runs when the trainer calls it.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
reported: Final[dict[int, float]]
|
|
47
|
+
trial: Final[optuna.Trial]
|
|
48
|
+
watch: Final[monitoring.Watch]
|
|
49
|
+
_monitor: monitoring.Monitor | None
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
trial: optuna.Trial,
|
|
54
|
+
*,
|
|
55
|
+
watch: monitoring.Watch | None = None,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Initialize.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
trial: ``optuna`` trial.
|
|
61
|
+
watch: what to report. Default reports the loss of the trainer.
|
|
62
|
+
"""
|
|
63
|
+
super().__init__()
|
|
64
|
+
self.reported = {}
|
|
65
|
+
self.trial = trial
|
|
66
|
+
self.watch = monitoring.Watch() if watch is None else watch
|
|
67
|
+
self._monitor = None
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def monitor(self) -> monitoring.Monitor:
|
|
72
|
+
"""The record of the reported metric.
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
ResultNotAvailableError: if no trainer bound the callback yet.
|
|
76
|
+
"""
|
|
77
|
+
if self._monitor is None:
|
|
78
|
+
raise exceptions.ResultNotAvailableError()
|
|
79
|
+
|
|
80
|
+
return self._monitor
|
|
81
|
+
|
|
82
|
+
@override
|
|
83
|
+
def bind_trainer(self, trainer: p.TrainerProtocol[Any, Any, Any]) -> None:
|
|
84
|
+
"""Create the record of the metric for the trainer."""
|
|
85
|
+
super().bind_trainer(trainer)
|
|
86
|
+
self._monitor = self.watch.resolve(trainer)
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
@override
|
|
90
|
+
def run(self) -> None:
|
|
91
|
+
"""Report the value to the trial and prune the training.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
optuna.TrialPruned: if the trial should be pruned.
|
|
95
|
+
"""
|
|
96
|
+
epoch = self.trainer.model.epoch
|
|
97
|
+
self.monitor.record(epoch)
|
|
98
|
+
if epoch not in self.monitor.values:
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
value = self.monitor.value
|
|
102
|
+
self.trial.report(value, epoch)
|
|
103
|
+
self.reported[epoch] = value
|
|
104
|
+
if self.trial.should_prune():
|
|
105
|
+
msg = f'Optuna pruning while monitoring {self.monitor.metric_name}'
|
|
106
|
+
self.trainer.terminate_training(msg)
|
|
107
|
+
raise optuna.TrialPruned()
|
|
108
|
+
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def suggest_overrides(
|
|
113
|
+
tune_cfg: omegaconf.DictConfig,
|
|
114
|
+
trial: optuna.Trial,
|
|
115
|
+
*,
|
|
116
|
+
use_full_name: bool = False,
|
|
117
|
+
) -> list[str]:
|
|
118
|
+
"""Suggest values for a trial from structured configurations.
|
|
119
|
+
|
|
120
|
+
This function helps integrate optuna into hydra by specifying trial
|
|
121
|
+
parameters present in the hydra run configuration.
|
|
122
|
+
|
|
123
|
+
The configuration file (loadable with hydra) should follow this structure:
|
|
124
|
+
|
|
125
|
+
.. code-block:: yaml
|
|
126
|
+
|
|
127
|
+
tune:
|
|
128
|
+
params:
|
|
129
|
+
param_name:
|
|
130
|
+
suggest: "suggest_float" # or other optuna suggest method
|
|
131
|
+
settings:
|
|
132
|
+
low: 0.0
|
|
133
|
+
high: 1.0
|
|
134
|
+
list_param:
|
|
135
|
+
suggest: "suggest_list"
|
|
136
|
+
settings:
|
|
137
|
+
min_length: 1
|
|
138
|
+
max_length: 5
|
|
139
|
+
suggest: "suggest_float" # method for sampling list elements
|
|
140
|
+
settings:
|
|
141
|
+
low: 0.0
|
|
142
|
+
high: 1.0
|
|
143
|
+
overrides: [] # additional static overrides
|
|
144
|
+
|
|
145
|
+
For 'suggest_list' configurations, the settings must specify:
|
|
146
|
+
- min_length and max_length: bounds for the size of the list.
|
|
147
|
+
- nested suggest and settings: used to sample each list element.
|
|
148
|
+
|
|
149
|
+
The resulting values can be used with hydra.initialize and hydra.compose.
|
|
150
|
+
Example usage:
|
|
151
|
+
|
|
152
|
+
.. code-block:: python
|
|
153
|
+
|
|
154
|
+
import hydra
|
|
155
|
+
|
|
156
|
+
with hydra.initialize(version_base=None, config_path='path/to/config'):
|
|
157
|
+
overrides = suggest_overrides(tune_cfg, trial)
|
|
158
|
+
dict_cfg = hydra.compose(config_name='config', overrides=overrides)
|
|
159
|
+
|
|
160
|
+
Here, "your_hydra_config" is the name of the configuration file that
|
|
161
|
+
includes the configuration parameters to override.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
tune_cfg: a structure specifying how to sample new parameter values.
|
|
165
|
+
trial: the optuna trial related to the sampled parameters.
|
|
166
|
+
use_full_name: use the fully qualified setting name. Default to a
|
|
167
|
+
human-readable name.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
A list of strings for hydra configuration overrides.
|
|
171
|
+
|
|
172
|
+
Raises:
|
|
173
|
+
OptunaError: if the suggested configuration is invalid.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
def _get_short_name(name: str) -> str:
|
|
177
|
+
*prefix_parts, suffix = name.rsplit('.', maxsplit=2)
|
|
178
|
+
if suffix.isdigit() and prefix_parts:
|
|
179
|
+
suffix = f'{prefix_parts[-1]} {suffix}'
|
|
180
|
+
|
|
181
|
+
return string.capwords(suffix.replace('_', ' '))
|
|
182
|
+
|
|
183
|
+
short_names = {k: _get_short_name(k) for k in tune_cfg.tune.params}
|
|
184
|
+
name_counts = collections.Counter(short_names.values())
|
|
185
|
+
|
|
186
|
+
all_overrides: list[str] = [*tune_cfg.overrides]
|
|
187
|
+
for setting_name, param_value in tune_cfg.tune.params.items():
|
|
188
|
+
param_name = short_names[setting_name]
|
|
189
|
+
if use_full_name or name_counts[param_name] > 1:
|
|
190
|
+
param_name = setting_name
|
|
191
|
+
|
|
192
|
+
if param_value.suggest == 'suggest_list':
|
|
193
|
+
new_value: list[Any] = []
|
|
194
|
+
for i in range(
|
|
195
|
+
trial.suggest_int(
|
|
196
|
+
name=f'{param_name} #',
|
|
197
|
+
low=param_value.settings.min_length,
|
|
198
|
+
high=param_value.settings.max_length,
|
|
199
|
+
)
|
|
200
|
+
):
|
|
201
|
+
try:
|
|
202
|
+
bound_suggest = getattr(trial, param_value.settings.suggest)
|
|
203
|
+
except AttributeError as ae:
|
|
204
|
+
msg = f'Invalid suggest configuration: {ae}.'
|
|
205
|
+
raise OptunaError(msg) from ae
|
|
206
|
+
new_value.append(
|
|
207
|
+
bound_suggest(
|
|
208
|
+
f'{param_name} {i}', **param_value.settings.settings
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
else:
|
|
212
|
+
try:
|
|
213
|
+
bound_suggest = getattr(trial, param_value.suggest)
|
|
214
|
+
except AttributeError as ae:
|
|
215
|
+
msg = f'Invalid suggest configuration: {ae}.'
|
|
216
|
+
raise OptunaError(msg) from ae
|
|
217
|
+
|
|
218
|
+
new_value = bound_suggest(param_name, **param_value.settings)
|
|
219
|
+
all_overrides.append(f'{setting_name}={new_value}')
|
|
220
|
+
|
|
221
|
+
return all_overrides
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def get_final_value(
|
|
225
|
+
trial: optuna.Trial,
|
|
226
|
+
*,
|
|
227
|
+
filter_fn: Callable[[Sequence[float]], float] | None = None,
|
|
228
|
+
) -> float:
|
|
229
|
+
"""Calculates a trial's final value from its intermediate reported values.
|
|
230
|
+
|
|
231
|
+
This function aggregates the intermediate values reported during trial
|
|
232
|
+
optimization using trial.report().
|
|
233
|
+
|
|
234
|
+
Important: This function will not work with trials created using study.ask()
|
|
235
|
+
as these don't populate the intermediate values in the corresponding
|
|
236
|
+
FrozenTrial.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
trial: the completed ``optuna`` trial to evaluate.
|
|
240
|
+
filter_fn: function to aggregate the trial's intermediate values.
|
|
241
|
+
Defaults to min or max depending on the study direction.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
The aggregated final value for the trial.
|
|
245
|
+
|
|
246
|
+
Raises:
|
|
247
|
+
OptunaError: if the trial has no reported values, or if there's
|
|
248
|
+
a trial number mismatch.
|
|
249
|
+
"""
|
|
250
|
+
current_study = trial.study
|
|
251
|
+
if filter_fn is None:
|
|
252
|
+
filter_fn = min if current_study.direction.name == 'MINIMIZE' else max
|
|
253
|
+
|
|
254
|
+
for frozen_trial in reversed(current_study.trials):
|
|
255
|
+
if frozen_trial.number == trial.number:
|
|
256
|
+
break
|
|
257
|
+
else:
|
|
258
|
+
raise OptunaError('trial number mismatch.')
|
|
259
|
+
|
|
260
|
+
reported_values = list(frozen_trial.intermediate_values.values())
|
|
261
|
+
if not reported_values:
|
|
262
|
+
msg = 'trial has no reported values. Did you use study.optimize?'
|
|
263
|
+
raise OptunaError(msg)
|
|
264
|
+
|
|
265
|
+
return filter_fn(reported_values)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Support for torcheval syncing recommended for distributed training."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Final, TypeVar
|
|
6
|
+
|
|
7
|
+
import torch
|
|
8
|
+
|
|
9
|
+
from torcheval import metrics
|
|
10
|
+
from torcheval.metrics import toolkit
|
|
11
|
+
|
|
12
|
+
from drytorch.core import exceptions
|
|
13
|
+
from drytorch.core import protocols as p
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
'from_torcheval',
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_Tensor = torch.Tensor
|
|
22
|
+
|
|
23
|
+
_ComputeReturn = TypeVar('_ComputeReturn', bound=_Tensor | dict[str, _Tensor])
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def from_torcheval(
|
|
27
|
+
torch_eval: metrics.Metric[_ComputeReturn],
|
|
28
|
+
/,
|
|
29
|
+
*,
|
|
30
|
+
higher_is_better: bool | None = None,
|
|
31
|
+
name: str | None = None,
|
|
32
|
+
) -> p.SyncedObjectiveProtocol[_Tensor, _Tensor]:
|
|
33
|
+
"""Returns a wrapper of a Metric from torcheval with a sync method.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
torch_eval: the torcheval metric to wrap.
|
|
37
|
+
higher_is_better: whether higher values are better.
|
|
38
|
+
name: optional name for the metric. Defaults to the class name.
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
exceptions.ComputedMetricsTypeError: If the metric computes to an
|
|
42
|
+
invalid type.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
class _TorchEvalWithSync:
|
|
46
|
+
higher_is_better: Final[bool | None]
|
|
47
|
+
metric: Final[metrics.Metric[_ComputeReturn]]
|
|
48
|
+
name: Final[str]
|
|
49
|
+
_synced_value: _ComputeReturn | None
|
|
50
|
+
|
|
51
|
+
def __init__(self, _metric: metrics.Metric[_ComputeReturn]) -> None:
|
|
52
|
+
self.higher_is_better = higher_is_better
|
|
53
|
+
self.metric = _metric
|
|
54
|
+
name_str = name if name is not None else _metric.__class__.__name__
|
|
55
|
+
self.name = name_str
|
|
56
|
+
self._synced_value = None
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
def compute(self) -> dict[str, _Tensor]:
|
|
60
|
+
if self._synced_value is None:
|
|
61
|
+
val = self.metric.compute()
|
|
62
|
+
else:
|
|
63
|
+
val = self._synced_value
|
|
64
|
+
|
|
65
|
+
if isinstance(val, dict):
|
|
66
|
+
return val
|
|
67
|
+
|
|
68
|
+
if not isinstance(val, _Tensor):
|
|
69
|
+
raise exceptions.ComputedMetricsTypeError(type(val))
|
|
70
|
+
|
|
71
|
+
return {self.name: val}
|
|
72
|
+
|
|
73
|
+
def reset(self) -> None:
|
|
74
|
+
self.metric.reset()
|
|
75
|
+
self._synced_value = None
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
def sync(self) -> None:
|
|
79
|
+
"""Use torcheval toolkit to synchronize and compute metrics."""
|
|
80
|
+
self._synced_value = toolkit.sync_and_compute(self.metric)
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
def update(self, outputs: _Tensor, targets: _Tensor) -> None:
|
|
84
|
+
self.metric.update(outputs, targets)
|
|
85
|
+
self._synced_value = None
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
return _TorchEvalWithSync(torch_eval)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Utilities to ensure compatibility with torchmetrics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Final
|
|
6
|
+
|
|
7
|
+
import torch
|
|
8
|
+
|
|
9
|
+
from drytorch.core import exceptions
|
|
10
|
+
from drytorch.core import protocols as p
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
'from_torchmetrics',
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from torchmetrics import metric
|
|
19
|
+
|
|
20
|
+
_Tensor = torch.Tensor
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def from_torchmetrics(
|
|
24
|
+
torch_metric: metric.CompositionalMetric,
|
|
25
|
+
/,
|
|
26
|
+
) -> p.SyncedLossProtocol[_Tensor, _Tensor]:
|
|
27
|
+
"""Returns a wrapper of a CompositionalMetric for integration.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
torch_metric: the composed torchmetrics metric to wrap.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
The wrapped metric, usable as a loss.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
RepeatedMetricsError: if two components of the same class are
|
|
37
|
+
present in the composition. Subclass the metric so each
|
|
38
|
+
configuration has its own class name.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
class _TorchMetricCompositionalMetric:
|
|
42
|
+
"""Wrapper of Compositional Metric reporting internal calculations.
|
|
43
|
+
|
|
44
|
+
Note that this CompositionalMetric can be used as loss.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
higher_is_better: Final[bool | None]
|
|
48
|
+
metric: Final[metric.CompositionalMetric]
|
|
49
|
+
name = 'Loss'
|
|
50
|
+
_leaf_metrics: list[metric.Metric]
|
|
51
|
+
|
|
52
|
+
def __init__(self, _metric: metric.CompositionalMetric) -> None:
|
|
53
|
+
self.higher_is_better = _metric.higher_is_better
|
|
54
|
+
self.metric = _metric
|
|
55
|
+
self.metric.sync_on_compute = False
|
|
56
|
+
self.metric.dist_sync_on_step = False
|
|
57
|
+
self._leaf_metrics = _get_leaf_metrics(self.metric)
|
|
58
|
+
_disable_leaf_syncing(self._leaf_metrics)
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
def compute(self) -> dict[str, _Tensor]:
|
|
62
|
+
"""Output a dictionary of metric values for each component."""
|
|
63
|
+
dict_output = dict[str, _Tensor]({self.name: self.metric.compute()})
|
|
64
|
+
for m in self._leaf_metrics:
|
|
65
|
+
if isinstance(value := m.compute(), _Tensor):
|
|
66
|
+
dict_output[m.__class__.__name__] = value
|
|
67
|
+
|
|
68
|
+
return dict_output
|
|
69
|
+
|
|
70
|
+
def forward(self, outputs: _Tensor, targets: _Tensor) -> _Tensor:
|
|
71
|
+
return self.metric(outputs, targets)
|
|
72
|
+
|
|
73
|
+
def reset(self) -> None:
|
|
74
|
+
self.metric.reset()
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
def sync(self) -> None:
|
|
78
|
+
for m in self._leaf_metrics:
|
|
79
|
+
m.sync()
|
|
80
|
+
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
def update(self, outputs: _Tensor, targets: _Tensor) -> None:
|
|
84
|
+
self.metric.update(outputs, targets)
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
return _TorchMetricCompositionalMetric(torch_metric)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _get_leaf_metrics(
|
|
91
|
+
comp_metric: metric.CompositionalMetric,
|
|
92
|
+
) -> list[metric.Metric]:
|
|
93
|
+
leaves: list[metric.Metric] = []
|
|
94
|
+
stack: list[metric.Metric | float | int | _Tensor | None] = [comp_metric]
|
|
95
|
+
names_seen: set[str] = set()
|
|
96
|
+
while stack:
|
|
97
|
+
node = stack.pop()
|
|
98
|
+
if isinstance(node, comp_metric.__class__):
|
|
99
|
+
stack.extend([node.metric_b, node.metric_a])
|
|
100
|
+
elif isinstance(node, float | int | _Tensor) or node is None:
|
|
101
|
+
continue
|
|
102
|
+
else:
|
|
103
|
+
leaves.append(node)
|
|
104
|
+
name = node.__class__.__name__
|
|
105
|
+
if name in names_seen:
|
|
106
|
+
raise exceptions.RepeatedMetricsError([name])
|
|
107
|
+
|
|
108
|
+
names_seen.add(name)
|
|
109
|
+
|
|
110
|
+
return leaves
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _disable_leaf_syncing(leaf_metrics: list[metric.Metric]) -> None:
|
|
114
|
+
for m in leaf_metrics:
|
|
115
|
+
m.sync_on_compute = False
|
|
116
|
+
m.dist_sync_on_step = False
|
|
117
|
+
|
|
118
|
+
return
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core modules setting up and defining the framework."""
|