learning-loop-node 0.27.2__tar.gz → 0.28.0__tar.gz

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.
Files changed (77) hide show
  1. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/PKG-INFO +1 -1
  2. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/data_classes/__init__.py +2 -1
  3. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/data_classes/training.py +24 -1
  4. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/trainer/exceptions.py +7 -0
  5. learning_loop_node-0.28.0/learning_loop_node/trainer/hyperparameters.py +180 -0
  6. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/trainer/trainer_logic_generic.py +41 -3
  7. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/trainer/trainer_node.py +8 -1
  8. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/trainer/validation_set.py +7 -0
  9. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node.egg-info/PKG-INFO +1 -1
  10. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node.egg-info/SOURCES.txt +1 -0
  11. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/pyproject.toml +1 -1
  12. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/README.md +0 -0
  13. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/__init__.py +0 -0
  14. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/annotation/__init__.py +0 -0
  15. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/annotation/annotator_logic.py +0 -0
  16. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/annotation/annotator_node.py +0 -0
  17. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/data_classes/annotation_data.py +0 -0
  18. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/data_classes/annotations.py +0 -0
  19. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/data_classes/detections.py +0 -0
  20. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/data_classes/general.py +0 -0
  21. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/data_classes/image_metadata.py +0 -0
  22. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/data_classes/socket_response.py +0 -0
  23. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/data_exchanger.py +0 -0
  24. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/detector/__init__.py +0 -0
  25. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/detector/categories.py +0 -0
  26. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/detector/detector_logic.py +0 -0
  27. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/detector/detector_node.py +0 -0
  28. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/detector/exceptions.py +0 -0
  29. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/detector/geometry.py +0 -0
  30. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/detector/inbox_filter/__init__.py +0 -0
  31. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/detector/inbox_filter/cam_observation_history.py +0 -0
  32. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/detector/inbox_filter/relevance_filter.py +0 -0
  33. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/detector/outbox.py +0 -0
  34. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/detector/postprocess.py +0 -0
  35. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/detector/rest/__init__.py +0 -0
  36. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/detector/rest/about.py +0 -0
  37. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/detector/rest/backdoor_controls.py +0 -0
  38. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/detector/rest/detect.py +0 -0
  39. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/detector/rest/model_version_control.py +0 -0
  40. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/detector/rest/operation_mode.py +0 -0
  41. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/detector/rest/outbox_mode.py +0 -0
  42. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/detector/rest/upload.py +0 -0
  43. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/enums/__init__.py +0 -0
  44. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/enums/annotator.py +0 -0
  45. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/enums/detector.py +0 -0
  46. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/enums/general.py +0 -0
  47. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/enums/trainer.py +0 -0
  48. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/globals.py +0 -0
  49. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/helpers/__init__.py +0 -0
  50. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/helpers/background_tasks.py +0 -0
  51. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/helpers/entrypoint.py +0 -0
  52. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/helpers/environment_reader.py +0 -0
  53. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/helpers/gdrive_downloader.py +0 -0
  54. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/helpers/log_conf.py +0 -0
  55. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/helpers/misc.py +0 -0
  56. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/helpers/run.py +0 -0
  57. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/loop_communication.py +0 -0
  58. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/node.py +0 -0
  59. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/py.typed +0 -0
  60. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/rest.py +0 -0
  61. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/trainer/__init__.py +0 -0
  62. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/trainer/batch_size.py +0 -0
  63. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/trainer/cuda.py +0 -0
  64. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/trainer/downloader.py +0 -0
  65. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/trainer/executor.py +0 -0
  66. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/trainer/io_helpers.py +0 -0
  67. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/trainer/metrics.py +0 -0
  68. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/trainer/rest/__init__.py +0 -0
  69. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/trainer/rest/backdoor_controls.py +0 -0
  70. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/trainer/subprocess.py +0 -0
  71. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/trainer/test_executor.py +0 -0
  72. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/trainer/trainer_logic.py +0 -0
  73. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node/trainer/training_slot.py +0 -0
  74. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node.egg-info/dependency_links.txt +0 -0
  75. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node.egg-info/requires.txt +0 -0
  76. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/learning_loop_node.egg-info/top_level.txt +0 -0
  77. {learning_loop_node-0.27.2 → learning_loop_node-0.28.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: learning_loop_node
3
- Version: 0.27.2
3
+ Version: 0.28.0
4
4
  Summary: Python Library for Nodes which connect to the Zauberzeug Learning Loop
5
5
  Author-email: Zauberzeug GmbH <info@zauberzeug.com>
6
6
  License: MIT
@@ -30,6 +30,7 @@ from .image_metadata import ImageMetadata, ImagesMetadata
30
30
  from .socket_response import SocketResponse
31
31
  from .training import (
32
32
  Errors,
33
+ Hyperparameter,
33
34
  PretrainedModel,
34
35
  Training,
35
36
  TrainingError,
@@ -45,6 +46,6 @@ __all__ = [
45
46
  'AnnotationNodeStatus', 'Category', 'Context', 'DetectorStatus', 'ErrorConfiguration',
46
47
  'ModelInformation', 'NodeState', 'NodeStatus', 'ModelVersionResponse', 'ImagesMetadata',
47
48
  'SocketResponse',
48
- 'Errors', 'PretrainedModel', 'Training',
49
+ 'Errors', 'Hyperparameter', 'PretrainedModel', 'Training',
49
50
  'TrainingError', 'TrainingOut', 'TrainingStateData', 'TrainingStatus',
50
51
  ]
@@ -3,7 +3,7 @@ import sys
3
3
  import time
4
4
  from dataclasses import dataclass, field
5
5
  from pathlib import Path
6
- from typing import Any, Dict, List, Optional
6
+ from typing import Any, Dict, List, Literal, Optional
7
7
  from uuid import uuid4
8
8
 
9
9
  from ..enums import TrainerState
@@ -22,6 +22,28 @@ class PretrainedModel():
22
22
  description: str
23
23
 
24
24
 
25
+ HyperparameterType = Literal['int', 'float', 'bool', 'str', 'pairs']
26
+
27
+
28
+ @dataclass(**KWONLY_SLOTS)
29
+ class Hyperparameter:
30
+ """One hyperparameter a trainer accepts, reported to the loop so it can offer it in the project settings.
31
+
32
+ Plain data only, so it reaches the loop as it is. ``minimum`` and ``maximum`` apply to ``int`` and ``float``,
33
+ ``choices`` to ``str``. A ``pairs`` value is text of the form ``"key:value,..."``, which ``parse_pairs`` reads.
34
+ ``notes`` help whoever sets the value. ``expert`` ones are offered behind an expansion.
35
+ Whether a value can be changed when a training is started is the project's choice, not the trainer's.
36
+ """
37
+ name: str
38
+ type: HyperparameterType
39
+ default: Any
40
+ minimum: float | None = None
41
+ maximum: float | None = None
42
+ choices: list[str] | None = None
43
+ notes: str = ''
44
+ expert: bool = False
45
+
46
+
25
47
  @dataclass(**KWONLY_SLOTS)
26
48
  class TrainingStatus():
27
49
  id: str # NOTE this must not be changed, but tests wont detect a change -> update tests!
@@ -33,6 +55,7 @@ class TrainingStatus():
33
55
  progress: Optional[float]
34
56
 
35
57
  pretrained_models: List[PretrainedModel] = field(default_factory=list)
58
+ hyperparameters: list[Hyperparameter] = field(default_factory=list)
36
59
  architecture: Optional[str] = None
37
60
  context: Optional[Context] = None
38
61
 
@@ -15,6 +15,13 @@ class TrainingRefusedError(Exception):
15
15
  """Raised when a trainer cannot take on a training it was asked to begin; nothing was set up."""
16
16
 
17
17
 
18
+ class HyperparameterMismatchError(Exception):
19
+ """Raised when a training's hyperparameters do not fit the trainer's declaration; nothing was set up.
20
+
21
+ Unlike a refusal, the training would fail on any trainer of the same declaration.
22
+ """
23
+
24
+
18
25
  class TrainingSlotTakenError(TrainingRefusedError):
19
26
  """Raised when a sibling node holds the training slot."""
20
27
 
@@ -0,0 +1,180 @@
1
+ """Resolving the hyperparameters a training was sent against the ones its trainer declares.
2
+
3
+ The loop sends only what the project sets, plus what the job overrides. Everything else comes from
4
+ the trainer's declared defaults, so after :func:`resolve` a training holds the full set it runs
5
+ with, and that is what gets reported back to the loop.
6
+ """
7
+
8
+ import contextlib
9
+ import logging
10
+ import math
11
+ from collections.abc import Mapping, Sequence
12
+ from typing import Any, get_args
13
+
14
+ from ..data_classes import Hyperparameter
15
+ from ..data_classes.training import HyperparameterType
16
+ from .batch_size import REQUESTED_BATCH_SIZE
17
+ from .exceptions import CriticalError
18
+ from .validation_set import COMPUTE_VALIDATION_SET
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ LIBRARY_NAMES = (REQUESTED_BATCH_SIZE, COMPUTE_VALIDATION_SET)
23
+ """Hyperparameters the library reads itself, so a trainer that does not declare them still uses them."""
24
+
25
+ BOOL_STRINGS = {'true': True, '1': True, 'yes': True, 'on': True,
26
+ 'false': False, '0': False, 'no': False, 'off': False}
27
+
28
+
29
+ def resolve(declared: Sequence[Hyperparameter], received: Mapping[str, Any]) -> dict[str, Any]:
30
+ """Coerce and check ``received`` against ``declared`` and fill in the declared defaults.
31
+
32
+ ``declared`` is trusted to have passed :func:`check_declaration`. Names the trainer does not declare are kept
33
+ unchanged: old project configurations still carry them, and a resolved training carries the values its trainer
34
+ reported. An unset hyperparameter with a default of ``None`` is left out, so the trainer's own fallback applies.
35
+ Resolving twice gives the same result.
36
+
37
+ :raises CriticalError: If a value cannot be coerced to its type, is out of range or not one of its choices.
38
+ """
39
+ resolved = dict(received)
40
+ for hyperparameter in declared:
41
+ value = received.get(hyperparameter.name)
42
+ if _is_unset(hyperparameter, value):
43
+ if hyperparameter.default is None:
44
+ resolved.pop(hyperparameter.name, None)
45
+ else:
46
+ resolved[hyperparameter.name] = _coerce(hyperparameter, hyperparameter.default)
47
+ else:
48
+ resolved[hyperparameter.name] = _check(hyperparameter, _coerce(hyperparameter, value))
49
+ return resolved
50
+
51
+
52
+ def check_declaration(declared: Sequence[Hyperparameter]) -> None:
53
+ """Check that ``declared`` is consistent, so that :func:`resolve` can rely on it.
54
+
55
+ A trainer's declaration does not change while it runs, so the node checks it once, when it is built. A default of
56
+ ``None`` stands for "the trainer decides" and is not checked.
57
+
58
+ :raises ValueError: If a name is declared twice, a hyperparameter contradicts itself or its default does not fit.
59
+ """
60
+ names = [h.name for h in declared]
61
+ if duplicates := sorted({name for name in names if names.count(name) > 1}):
62
+ raise ValueError(f'the trainer declares these hyperparameters twice: {", ".join(duplicates)}')
63
+ for hyperparameter in declared:
64
+ _check_one_declaration(hyperparameter)
65
+
66
+
67
+ def parse_pairs(text: str) -> list[tuple[str, str]]:
68
+ """The ``key:value`` items of a ``pairs`` hyperparameter such as ``"uuid:uuid,uuid:uuid"``.
69
+
70
+ Empty items and the spaces around keys and values are skipped, so ``''`` holds no pairs.
71
+
72
+ :raises ValueError: If an item is not a key and a value separated by a single colon.
73
+ """
74
+ pairs = []
75
+ for item in (i.strip() for i in text.split(',')):
76
+ if not item:
77
+ continue
78
+ key, _, value = (part.strip() for part in item.partition(':'))
79
+ if not key or not value or ':' in value:
80
+ raise ValueError(f'"{item}" is not of the form "key:value"')
81
+ pairs.append((key, value))
82
+ return pairs
83
+
84
+
85
+ def warn_about_undeclared(declared: Sequence[Hyperparameter], received: Mapping[str, Any]) -> None:
86
+ """Log the names in ``received`` that neither the trainer nor the library knows; they are not rejected."""
87
+ unknown = sorted(set(received) - {h.name for h in declared} - set(LIBRARY_NAMES))
88
+ if unknown:
89
+ logger.warning('these hyperparameters are not declared by this trainer: %s', ', '.join(unknown))
90
+
91
+
92
+ def _check_one_declaration(hyperparameter: Hyperparameter) -> None:
93
+ def contradiction(reason: str) -> ValueError:
94
+ return ValueError(f"the trainer declares hyperparameter '{hyperparameter.name}' {reason}")
95
+
96
+ if hyperparameter.type not in get_args(HyperparameterType):
97
+ raise contradiction(f'with the unknown type {hyperparameter.type!r}')
98
+ if hyperparameter.type not in ('int', 'float') and (hyperparameter.minimum is not None or
99
+ hyperparameter.maximum is not None):
100
+ raise contradiction(f'as {hyperparameter.type} with a minimum or maximum')
101
+ if hyperparameter.type != 'str' and hyperparameter.choices:
102
+ raise contradiction(f'as {hyperparameter.type} with choices')
103
+ for bound in (hyperparameter.minimum, hyperparameter.maximum):
104
+ if bound is not None and (isinstance(bound, bool) or not isinstance(bound, (int, float))):
105
+ raise contradiction(f'with the bound {bound!r}, which is not a number')
106
+ if (hyperparameter.minimum is not None and hyperparameter.maximum is not None and
107
+ hyperparameter.minimum > hyperparameter.maximum):
108
+ raise contradiction('with a minimum above its maximum')
109
+ if hyperparameter.default is not None:
110
+ try:
111
+ _check(hyperparameter, _coerce(hyperparameter, hyperparameter.default))
112
+ except CriticalError as e:
113
+ raise contradiction(f'with a default that does not fit: {e}') from None
114
+
115
+
116
+ def _is_unset(hyperparameter: Hyperparameter, value: Any) -> bool:
117
+ """``None``, or the empty value the loop sends for a field left blank, unless it fits as a value of its own."""
118
+ if value is None:
119
+ return True
120
+ if value != '':
121
+ return False
122
+ try:
123
+ _check(hyperparameter, _coerce(hyperparameter, value))
124
+ except CriticalError:
125
+ return True
126
+ return False
127
+
128
+
129
+ def _invalid(hyperparameter: Hyperparameter, requirement: str, value: Any) -> CriticalError:
130
+ return CriticalError(f"hyperparameter '{hyperparameter.name}' {requirement}, got {value!r}")
131
+
132
+
133
+ def _coerce(hyperparameter: Hyperparameter, value: Any) -> Any:
134
+ if hyperparameter.type == 'pairs':
135
+ if not isinstance(value, str):
136
+ raise _invalid(hyperparameter, 'must be text of key:value pairs', value)
137
+ try:
138
+ return ','.join(f'{key}:{item}' for key, item in parse_pairs(value))
139
+ except ValueError as e:
140
+ raise CriticalError(f"hyperparameter '{hyperparameter.name}': {e}") from None
141
+ if hyperparameter.type == 'str':
142
+ if isinstance(value, bool) or not isinstance(value, (str, int, float)):
143
+ raise _invalid(hyperparameter, 'must be text', value)
144
+ return str(value)
145
+ if hyperparameter.type == 'bool':
146
+ if isinstance(value, bool):
147
+ return value
148
+ if isinstance(value, str) and (word := value.strip().lower()) in BOOL_STRINGS:
149
+ return BOOL_STRINGS[word]
150
+ with contextlib.suppress(TypeError, ValueError):
151
+ number = float(value)
152
+ if math.isfinite(number):
153
+ return number != 0
154
+ raise _invalid(hyperparameter, 'must be true or false', value)
155
+ if isinstance(value, bool):
156
+ raise _invalid(hyperparameter, 'must be a number', value)
157
+ if hyperparameter.type == 'int' and isinstance(value, (int, str)):
158
+ with contextlib.suppress(ValueError):
159
+ return int(value)
160
+ try:
161
+ number = float(value)
162
+ except (TypeError, ValueError):
163
+ raise _invalid(hyperparameter, 'must be a number', value) from None
164
+ if not math.isfinite(number):
165
+ raise _invalid(hyperparameter, 'must be finite', value)
166
+ if hyperparameter.type == 'float':
167
+ return number
168
+ if not number.is_integer():
169
+ raise _invalid(hyperparameter, 'must be a whole number', value)
170
+ return int(number)
171
+
172
+
173
+ def _check(hyperparameter: Hyperparameter, value: Any) -> Any:
174
+ if hyperparameter.minimum is not None and value < hyperparameter.minimum:
175
+ raise _invalid(hyperparameter, f'must be at least {hyperparameter.minimum}', value)
176
+ if hyperparameter.maximum is not None and value > hyperparameter.maximum:
177
+ raise _invalid(hyperparameter, f'must be at most {hyperparameter.maximum}', value)
178
+ if hyperparameter.choices and value not in hyperparameter.choices:
179
+ raise _invalid(hyperparameter, f"must be one of {', '.join(hyperparameter.choices)}", value)
180
+ return value
@@ -13,6 +13,7 @@ from fastapi.encoders import jsonable_encoder
13
13
  from ..data_classes import (
14
14
  Context,
15
15
  Errors,
16
+ Hyperparameter,
16
17
  PretrainedModel,
17
18
  Training,
18
19
  TrainingOut,
@@ -26,7 +27,8 @@ from ..helpers.misc import (
26
27
  is_valid_uuid4,
27
28
  )
28
29
  from .downloader import TrainingsDownloader
29
- from .exceptions import CriticalError, NodeNeedsRestartError, TrainingRefusedError
30
+ from .exceptions import CriticalError, HyperparameterMismatchError, NodeNeedsRestartError, TrainingRefusedError
31
+ from .hyperparameters import resolve, warn_about_undeclared
30
32
  from .io_helpers import ActiveTrainingIO, EnvironmentVars, LastTrainingIO
31
33
  from .training_slot import TrainingSlot, training_slot_from_env
32
34
  from .validation_set import assign_validation_set
@@ -186,6 +188,12 @@ class TrainerLogicGeneric(ABC):
186
188
  """
187
189
  raise NotImplementedError
188
190
 
191
+ @property
192
+ @abstractmethod
193
+ def provided_hyperparameters(self) -> list[Hyperparameter]:
194
+ """Returns the hyperparameters this trainer accepts; an empty list leaves them unchecked."""
195
+ raise NotImplementedError
196
+
189
197
  # ---------------------------------------- METHODS ----------------------------------------
190
198
 
191
199
  # NOTE: Trainings are started by the Learning Loop via the begin_training event
@@ -204,6 +212,7 @@ class TrainerLogicGeneric(ABC):
204
212
  progress=self.general_progress)
205
213
 
206
214
  status.pretrained_models = self.provided_pretrained_models
215
+ status.hyperparameters = self.provided_hyperparameters
207
216
  status.architecture = self.model_architecture
208
217
 
209
218
  if self._training:
@@ -224,9 +233,18 @@ class TrainerLogicGeneric(ABC):
224
233
 
225
234
  def _init_from_last_training(self) -> None:
226
235
  """Initializes a new training object from the last training saved on disc via last_training_io.
236
+
237
+ The hyperparameters are resolved again, since the training may have been saved before this trainer declared
238
+ them. If one no longer fits, the training continues with the values it was saved with, since it already runs
239
+ with them.
227
240
  """
228
241
  self._training = self.last_training_io.load()
229
242
  assert self._training is not None and self._training.training_folder is not None, 'could not restore training folder'
243
+ try:
244
+ self._training.hyperparameters = resolve(self.provided_hyperparameters, self._training.hyperparameters)
245
+ except CriticalError as e:
246
+ logger.warning('restored training does not fit the declared hyperparameters, continuing with the saved '
247
+ 'values: %s', e)
230
248
  logger.info('restored training: \n%s', self._training)
231
249
  self._active_training_io = ActiveTrainingIO(
232
250
  self._training.training_folder, self.node.loop_communicator, self._training.context)
@@ -234,13 +252,15 @@ class TrainerLogicGeneric(ABC):
234
252
  async def begin_training(self, organization: str, project: str, training_config: Dict) -> None:
235
253
  """Called on `begin_training` event from the Learning Loop.
236
254
 
237
- The training slot is taken before anything is set up, so a refused training leaves the
238
- previous training folders in place.
255
+ The hyperparameters are resolved and the training slot is taken before anything is set up, so a refused
256
+ training leaves the previous training folders in place.
239
257
 
240
258
  :raise TrainingRefusedError: if a training is active or a sibling node holds the training slot.
259
+ :raise HyperparameterMismatchError: if a hyperparameter does not fit its declaration.
241
260
  """
242
261
  if self.training_active:
243
262
  raise TrainingRefusedError(f'training {self.training.id} is still active')
263
+ training_config = self._with_resolved_hyperparameters(training_config)
244
264
  context = Context(organization=organization, project=project)
245
265
  self.training_slot.claim(self._slot_claim(context, training_config.get('training_number')))
246
266
  try:
@@ -250,6 +270,24 @@ class TrainerLogicGeneric(ABC):
250
270
  raise
251
271
  self._begin_training_task()
252
272
 
273
+ def _with_resolved_hyperparameters(self, training_config: dict) -> dict:
274
+ """Returns ``training_config`` with its hyperparameters resolved against the declared ones.
275
+
276
+ A config without a hyperparameters dict is returned as it is, for ``Training.generate_training`` to reject.
277
+
278
+ :raise HyperparameterMismatchError: if a hyperparameter does not fit its declaration.
279
+ """
280
+ declared = self.provided_hyperparameters
281
+ received = training_config.get('hyperparameters')
282
+ if not declared or not isinstance(received, dict):
283
+ return training_config
284
+ try:
285
+ resolved = resolve(declared, received)
286
+ except CriticalError as e:
287
+ raise HyperparameterMismatchError(str(e)) from e
288
+ warn_about_undeclared(declared, received)
289
+ return {**training_config, 'hyperparameters': resolved}
290
+
253
291
  def _begin_training_task(self) -> None:
254
292
  # NOTE: Task object is used to potentially cancel the task
255
293
  self.training_task = asyncio.get_event_loop().create_task(self._run())
@@ -10,7 +10,8 @@ from socketio import AsyncClient, exceptions
10
10
  from ..data_classes import SocketResponse
11
11
  from ..enums import TrainerState
12
12
  from ..node import Node
13
- from .exceptions import TrainingRefusedError
13
+ from .exceptions import HyperparameterMismatchError, TrainingRefusedError
14
+ from .hyperparameters import check_declaration
14
15
  from .io_helpers import LastTrainingIO
15
16
  from .rest import backdoor_controls
16
17
  from .trainer_logic_generic import TrainerLogicGeneric
@@ -19,6 +20,8 @@ from .trainer_logic_generic import TrainerLogicGeneric
19
20
  class TrainerNode(Node):
20
21
 
21
22
  def __init__(self, name: str, trainer_logic: TrainerLogicGeneric, uuid: Optional[str] = None, use_backdoor_controls: bool = False):
23
+ """:raise ValueError: if the trainer declares hyperparameters that contradict themselves."""
24
+ check_declaration(trainer_logic.provided_hyperparameters)
22
25
  super().__init__(name, uuid=uuid, node_type='trainer')
23
26
  trainer_logic._node = self
24
27
  self.trainer_logic = trainer_logic
@@ -72,6 +75,10 @@ class TrainerNode(Node):
72
75
  except TrainingRefusedError as e:
73
76
  self.log.warning('refused begin_training: %s', e)
74
77
  return SocketResponse(success=False, error_msg=str(e), payload='refused')
78
+ except HyperparameterMismatchError as e:
79
+ # NOTE: not a refusal, which the loop answers by offering the same job again
80
+ self.log.warning('rejected begin_training: %s', e)
81
+ return SocketResponse.for_failure(str(e))
75
82
  return True
76
83
 
77
84
  @sio_client.event
@@ -21,6 +21,7 @@ import logging
21
21
  from collections.abc import Mapping
22
22
  from typing import Any
23
23
 
24
+ from ..data_classes import Hyperparameter
24
25
  from .exceptions import CriticalError
25
26
 
26
27
  logger = logging.getLogger(__name__)
@@ -28,6 +29,12 @@ logger = logging.getLogger(__name__)
28
29
  COMPUTE_VALIDATION_SET = 'compute_validation_set'
29
30
  """The hyperparameter that asks to split a validation set off the train images when the loop sent none."""
30
31
 
32
+ COMPUTE_VALIDATION_SET_HYPERPARAMETER = Hyperparameter(
33
+ name=COMPUTE_VALIDATION_SET, type='bool', default=False,
34
+ notes='Split about a tenth of the train images off to select the models on, so the test set is only reported on; '
35
+ 'a validation set sent by the loop is used either way')
36
+ """The declaration of `compute_validation_set`, for a trainer to list among its `provided_hyperparameters`."""
37
+
31
38
  VALIDATION_FRACTION = 0.1
32
39
 
33
40
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: learning_loop_node
3
- Version: 0.27.2
3
+ Version: 0.28.0
4
4
  Summary: Python Library for Nodes which connect to the Zauberzeug Learning Loop
5
5
  Author-email: Zauberzeug GmbH <info@zauberzeug.com>
6
6
  License: MIT
@@ -61,6 +61,7 @@ learning_loop_node/trainer/cuda.py
61
61
  learning_loop_node/trainer/downloader.py
62
62
  learning_loop_node/trainer/exceptions.py
63
63
  learning_loop_node/trainer/executor.py
64
+ learning_loop_node/trainer/hyperparameters.py
64
65
  learning_loop_node/trainer/io_helpers.py
65
66
  learning_loop_node/trainer/metrics.py
66
67
  learning_loop_node/trainer/subprocess.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "learning_loop_node"
3
- version = "0.27.2"
3
+ version = "0.28.0"
4
4
  description = "Python Library for Nodes which connect to the Zauberzeug Learning Loop"
5
5
  authors = [{ name = "Zauberzeug GmbH", email = "info@zauberzeug.com" }]
6
6
  license = { text = "MIT" }