learning-loop-node 0.26.0__tar.gz → 0.27.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 (76) hide show
  1. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/PKG-INFO +2 -1
  2. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/README.md +1 -0
  3. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/data_classes/general.py +2 -0
  4. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/data_classes/training.py +11 -5
  5. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/trainer/rest/backdoor_controls.py +1 -2
  6. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/trainer/trainer_logic.py +17 -7
  7. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/trainer/trainer_logic_generic.py +29 -14
  8. learning_loop_node-0.27.0/learning_loop_node/trainer/validation_set.py +63 -0
  9. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node.egg-info/PKG-INFO +2 -1
  10. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node.egg-info/SOURCES.txt +1 -0
  11. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/pyproject.toml +1 -1
  12. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/__init__.py +0 -0
  13. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/annotation/__init__.py +0 -0
  14. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/annotation/annotator_logic.py +0 -0
  15. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/annotation/annotator_node.py +0 -0
  16. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/data_classes/__init__.py +0 -0
  17. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/data_classes/annotation_data.py +0 -0
  18. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/data_classes/annotations.py +0 -0
  19. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/data_classes/detections.py +0 -0
  20. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/data_classes/image_metadata.py +0 -0
  21. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/data_classes/socket_response.py +0 -0
  22. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/data_exchanger.py +0 -0
  23. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/detector/__init__.py +0 -0
  24. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/detector/categories.py +0 -0
  25. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/detector/detector_logic.py +0 -0
  26. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/detector/detector_node.py +0 -0
  27. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/detector/exceptions.py +0 -0
  28. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/detector/geometry.py +0 -0
  29. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/detector/inbox_filter/__init__.py +0 -0
  30. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/detector/inbox_filter/cam_observation_history.py +0 -0
  31. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/detector/inbox_filter/relevance_filter.py +0 -0
  32. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/detector/outbox.py +0 -0
  33. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/detector/postprocess.py +0 -0
  34. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/detector/rest/__init__.py +0 -0
  35. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/detector/rest/about.py +0 -0
  36. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/detector/rest/backdoor_controls.py +0 -0
  37. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/detector/rest/detect.py +0 -0
  38. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/detector/rest/model_version_control.py +0 -0
  39. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/detector/rest/operation_mode.py +0 -0
  40. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/detector/rest/outbox_mode.py +0 -0
  41. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/detector/rest/upload.py +0 -0
  42. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/enums/__init__.py +0 -0
  43. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/enums/annotator.py +0 -0
  44. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/enums/detector.py +0 -0
  45. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/enums/general.py +0 -0
  46. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/enums/trainer.py +0 -0
  47. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/globals.py +0 -0
  48. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/helpers/__init__.py +0 -0
  49. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/helpers/background_tasks.py +0 -0
  50. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/helpers/entrypoint.py +0 -0
  51. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/helpers/environment_reader.py +0 -0
  52. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/helpers/gdrive_downloader.py +0 -0
  53. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/helpers/log_conf.py +0 -0
  54. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/helpers/misc.py +0 -0
  55. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/helpers/run.py +0 -0
  56. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/loop_communication.py +0 -0
  57. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/node.py +0 -0
  58. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/py.typed +0 -0
  59. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/rest.py +0 -0
  60. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/trainer/__init__.py +0 -0
  61. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/trainer/batch_size.py +0 -0
  62. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/trainer/cuda.py +0 -0
  63. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/trainer/downloader.py +0 -0
  64. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/trainer/exceptions.py +0 -0
  65. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/trainer/executor.py +0 -0
  66. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/trainer/io_helpers.py +0 -0
  67. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/trainer/metrics.py +0 -0
  68. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/trainer/rest/__init__.py +0 -0
  69. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/trainer/subprocess.py +0 -0
  70. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/trainer/test_executor.py +0 -0
  71. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/trainer/trainer_node.py +0 -0
  72. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node/trainer/training_slot.py +0 -0
  73. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node.egg-info/dependency_links.txt +0 -0
  74. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node.egg-info/requires.txt +0 -0
  75. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/learning_loop_node.egg-info/top_level.txt +0 -0
  76. {learning_loop_node-0.26.0 → learning_loop_node-0.27.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: learning_loop_node
3
- Version: 0.26.0
3
+ Version: 0.27.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
@@ -349,3 +349,4 @@ After a short time the converted model should be available as well.
349
349
  - Nodes add properties to `model.json`, which contains all the information which are needed by subsequent nodes. These are typically the properties:
350
350
  - `resolution`: resolution in which the model expects images (as `int`, since the resolution is mostly square - later, `resolution_x`` resolution_y` would also be conceivable or `resolutions` to give a list of possible resolutions)
351
351
  - `categories`: list of categories with name, id, (later also type), in the order in which they are used by the model -- this is neccessary to be robust about renamings
352
+ - `conf_threshold` (optional): the minimum confidence the trainer chose for this model's detections
@@ -316,3 +316,4 @@ After a short time the converted model should be available as well.
316
316
  - Nodes add properties to `model.json`, which contains all the information which are needed by subsequent nodes. These are typically the properties:
317
317
  - `resolution`: resolution in which the model expects images (as `int`, since the resolution is mostly square - later, `resolution_x`` resolution_y` would also be conceivable or `resolutions` to give a list of possible resolutions)
318
318
  - `categories`: list of categories with name, id, (later also type), in the order in which they are used by the model -- this is neccessary to be robust about renamings
319
+ - `conf_threshold` (optional): the minimum confidence the trainer chose for this model's detections
@@ -59,6 +59,8 @@ class ModelInformation():
59
59
  default=None, metadata={"description": "The path of the parent directory of the model in the file system."})
60
60
  model_size: Optional[str] = field(default=None, metadata={
61
61
  "description": "The size of the model (i.e. the specification or variant of the model architecture)."})
62
+ conf_threshold: Optional[float] = field(default=None, metadata={
63
+ "description": "The minimum confidence the trainer chose for the detections of this model, if it chose one."})
62
64
 
63
65
  @property
64
66
  def context(self):
@@ -103,13 +103,19 @@ class Training():
103
103
  assert self.image_data is not None, 'Image data not set'
104
104
  return [image['id'] for image in self.image_data]
105
105
 
106
- def train_image_count(self) -> int:
107
- assert self.image_data is not None, 'Image data not set'
108
- return len([image for image in self.image_data if image['set'] == 'train'])
106
+ def train_images(self) -> list[dict]:
107
+ return self._images_of_set('train')
108
+
109
+ def val_images(self) -> list[dict]:
110
+ """The images to select models on: the `val` images if there are any, else the test images."""
111
+ return self._images_of_set('val') or self.test_images()
112
+
113
+ def test_images(self) -> list[dict]:
114
+ return self._images_of_set('test')
109
115
 
110
- def test_image_count(self) -> int:
116
+ def _images_of_set(self, set_name: str) -> list[dict]:
111
117
  assert self.image_data is not None, 'Image data not set'
112
- return len([image for image in self.image_data if image['set'] == 'test'])
118
+ return [image for image in self.image_data if image['set'] == set_name]
113
119
 
114
120
 
115
121
  @dataclass(**KWONLY_SLOTS)
@@ -38,8 +38,7 @@ async def reset(request: Request):
38
38
  trainer_node: 'TrainerNode' = request.app
39
39
  async with trainer_node.repeat_loop_lock:
40
40
 
41
- await trainer_node.trainer_logic.stop() # NOTE first stop may only kill running training process
42
- await trainer_node.trainer_logic.stop()
41
+ await trainer_node.trainer_logic.abort()
43
42
  trainer_node.last_training_io.delete()
44
43
  trainer_node.status.reset_all_errors()
45
44
 
@@ -25,6 +25,8 @@ class TrainerLogic(TrainerLogicGeneric):
25
25
  super().__init__(model_format)
26
26
  self._detection_progress: Optional[float] = None
27
27
  self._executor: Optional[Executor] = None
28
+ self._process_started: bool = False
29
+ self._stop_requested: bool = False
28
30
  self.start_training_task: Optional[Coroutine] = None
29
31
  self.inference_batch_size = self._environment_vars.inference_batch_size
30
32
 
@@ -47,10 +49,13 @@ class TrainerLogic(TrainerLogicGeneric):
47
49
  previous_state = TrainerState.TrainModelDownloaded
48
50
  error_key = 'run_training'
49
51
  self._executor = Executor(self.training.training_folder)
52
+ self._process_started = False
53
+ self._stop_requested = False
50
54
  self.training.training_state = TrainerState.TrainingRunning
51
55
 
52
56
  try:
53
57
  await self._start_training()
58
+ self._process_started = True
54
59
  last_sync_time = datetime.now()
55
60
 
56
61
  while True:
@@ -77,11 +82,17 @@ class TrainerLogic(TrainerLogicGeneric):
77
82
  # if self.executor.return_code != 0:
78
83
  # raise TrainingError(cause=f'Executor returned with error code: {self.executor.return_code}')
79
84
 
80
- except TrainingError:
85
+ except TrainingError as e:
81
86
  logging.exception('Exception in trainer_logic._train')
87
+ if self._stop_requested:
88
+ # ending the process may be what failed it; a stopped training must not start over, and the
89
+ # wind-down uploads the model it has (or ends with a CriticalError if there is none)
90
+ logging.warning('Training process failed after a stop (%s), winding down with its model', e.cause)
91
+ return
82
92
  self.training.training_state = previous_state
83
93
  raise
84
94
  finally:
95
+ self._process_started = False
85
96
  # shield so the subprocess is stopped even when the task is cancelled (e.g. on abort())
86
97
  if self._executor and self._executor.is_running():
87
98
  await asyncio.shield(self.executor.stop_and_wait())
@@ -147,15 +158,14 @@ class TrainerLogic(TrainerLogicGeneric):
147
158
 
148
159
  # ---------------------------------------- OVERWRITTEN METHODS ----------------------------------------
149
160
 
150
- async def stop(self) -> None:
151
- """If executor is running, stop it. Else cancel training task."""
152
- print('===============> stop received in trainer_logic.', flush=True)
153
- if not self.training_active:
161
+ async def _stop_running_training(self) -> None:
162
+ """End the training process; before it has started, abort."""
163
+ if not self._process_started:
164
+ await super()._stop_running_training()
154
165
  return
166
+ self._stop_requested = True
155
167
  if self._executor and self._executor.is_running():
156
168
  await self.executor.stop_and_wait()
157
- return
158
- await super().stop()
159
169
 
160
170
  # ---------------------------------------- ABSTRACT METHODS ----------------------------------------
161
171
 
@@ -29,6 +29,7 @@ from .downloader import TrainingsDownloader
29
29
  from .exceptions import CriticalError, NodeNeedsRestartError, TrainingRefusedError
30
30
  from .io_helpers import ActiveTrainingIO, EnvironmentVars, LastTrainingIO
31
31
  from .training_slot import TrainingSlot, training_slot_from_env
32
+ from .validation_set import assign_validation_set
32
33
 
33
34
  if TYPE_CHECKING:
34
35
  from .trainer_node import TrainerNode
@@ -328,7 +329,7 @@ class TrainerLogicGeneric(ABC):
328
329
  async def _perform_state(self, error_key: str, state_during: TrainerState, state_after: TrainerState, action: Callable[[], Coroutine], reset_early=False):
329
330
  '''
330
331
  Perform a training state and handle errors.
331
- - If the loop sends a StopTraining event, this will raise a CancelledError.
332
+ - If the training is aborted, the CancelledError moves it to ReadyForCleanup; on shutdown it is re-raised.
332
333
  - States can raise a CriticalError indicating that there is no point in retrying the state.
333
334
  - If any other error occurs, the error is stored in the errors object and the state is reset to the previous state.
334
335
  The state is retried after a wait until the training is aborted.
@@ -377,11 +378,11 @@ class TrainerLogicGeneric(ABC):
377
378
  self.last_training_io.save(self.training)
378
379
 
379
380
  async def _prepare(self) -> None:
380
- """Downloads images to the images_folder and saves annotations to training.data.image_data.
381
- """
381
+ """Downloads images to the images_folder and saves annotations to training.data.image_data."""
382
382
  self.node.data_exchanger.set_context(self.training.context)
383
383
  downloader = TrainingsDownloader(self.node.data_exchanger)
384
384
  image_data, skipped_image_count = await downloader.download_training_data(self.training.images_folder)
385
+ assign_validation_set(image_data, self.training.hyperparameters)
385
386
 
386
387
  self.training.image_data = image_data
387
388
  self.training.skipped_image_count = skipped_image_count
@@ -421,11 +422,12 @@ class TrainerLogicGeneric(ABC):
421
422
  try:
422
423
  new_best_model = self._get_new_best_training_state()
423
424
  if new_best_model:
425
+ sets = [image['set'] for image in self.training.image_data or []]
424
426
  new_training = TrainingOut(trainer_id=self.node.uuid,
425
427
  trainer_name=self.node.name,
426
428
  confusion_matrix=new_best_model.confusion_matrix,
427
- train_image_count=self.training.train_image_count(),
428
- test_image_count=self.training.test_image_count(),
429
+ train_image_count=sets.count('train') + sets.count('val'),
430
+ test_image_count=sets.count('test'),
429
431
  hyperparameters=self.hyperparameters_for_state_sync,
430
432
  best_epoch=new_best_model.epoch)
431
433
  await asyncio.sleep(0.1) # NOTE needed for tests.
@@ -475,7 +477,7 @@ class TrainerLogicGeneric(ABC):
475
477
  already_uploaded_formats = self.active_training_io.load_model_upload_progress()
476
478
 
477
479
  for file_format in [f for f in files if f not in already_uploaded_formats]:
478
- _files = files[file_format] + [self._dump_categories_to_json()]
480
+ _files = files[file_format] + [self._dump_model_json()]
479
481
  assert len([f for f in _files if 'model.json' in f]) == 1, "model.json must be included exactly once"
480
482
 
481
483
  model_uuid = await self.node.data_exchanger.upload_model_get_uuid(context, _files, self.training.training_number, file_format)
@@ -490,10 +492,11 @@ class TrainerLogicGeneric(ABC):
490
492
  except FileNotFoundError as e:
491
493
  raise CriticalError('The model is uploaded, but the loop never answered with its id') from e
492
494
 
493
- def _dump_categories_to_json(self) -> str:
494
- """Dumps the categories to a json file and returns the path to the file.
495
+ def _dump_model_json(self) -> str:
496
+ """Dumps the categories and the trainer's model metadata to a json file and returns the path to the file.
495
497
  """
496
- content = {'categories': [asdict(c) for c in self._training.categories], } if self._training else None
498
+ content = {'categories': [asdict(c) for c in self._training.categories],
499
+ **self._get_model_metadata()} if self._training else None
497
500
  json_path = '/tmp/model.json'
498
501
  with open(json_path, 'w') as f:
499
502
  json.dump(content, f)
@@ -515,12 +518,17 @@ class TrainerLogicGeneric(ABC):
515
518
 
516
519
  async def on_shutdown(self) -> None:
517
520
  self.shutdown_event.set()
518
- await self.stop()
519
- await self.stop()
521
+ await self.abort()
520
522
 
521
- async def stop(self):
522
- """Stop the training. Default is to abort.
523
- """
523
+ async def stop(self) -> None:
524
+ """Stop a running training but keep its model. In every other state a stop does nothing."""
525
+ if not self.training_active or self.training.training_state != TrainerState.TrainingRunning:
526
+ logger.info('Received stop in state %s. This is ignored.', self.training.training_state if self.training_active else None)
527
+ return
528
+ await self._stop_running_training()
529
+
530
+ async def _stop_running_training(self) -> None:
531
+ """End the running training so it winds down with its model. This base class cannot, so it aborts."""
524
532
  await self.abort()
525
533
 
526
534
  async def abort(self):
@@ -606,6 +614,13 @@ class TrainerLogicGeneric(ABC):
606
614
  """
607
615
  raise NotImplementedError
608
616
 
617
+ def _get_model_metadata(self) -> dict[str, Any]:
618
+ """Called when the files of `_get_latest_model_files` are uploaded.
619
+ Returns values the trainer determined for this model, merged into its model.json.
620
+ The keys must be fields of `ModelInformation` (e.g. `conf_threshold`) so detectors can read them.
621
+ """
622
+ return {}
623
+
609
624
  @abstractmethod
610
625
  async def _clear_training_data(self, training_folder: str) -> None:
611
626
  """Called after a training has finished. Deletes all data that is not needed anymore after a training run.
@@ -0,0 +1,63 @@
1
+ """Which images a training selects its models on.
2
+
3
+ A node selects models (best checkpoint, early stopping, confidence threshold) on
4
+ `Training.val_images()` and reads `Training.test_images()` only for the counts it reports. It never
5
+ decides between the two itself: `val_images()` always holds a set to select on, which is the test
6
+ images unless the training has `val` images of its own. A node may notice that both hold the same
7
+ images and spare itself a second pass, but nothing more.
8
+
9
+ Where the `val` images come from, in this order:
10
+
11
+ 1. The loop sent a dedicated validation set (`set == 'val'`); it is used as it is.
12
+ 2. The `compute_validation_set` hyperparameter is true: `assign_validation_set` moves about a tenth of the
13
+ train images to `val`, chosen by a hash of the image id, so an image stays on its side when new
14
+ images arrive or a training is continued.
15
+ 3. Otherwise there are no `val` images and `val_images()` falls back to the test images, so the
16
+ model is selected on the test set.
17
+ """
18
+
19
+ import hashlib
20
+ import logging
21
+ from collections.abc import Mapping
22
+ from typing import Any
23
+
24
+ from .exceptions import CriticalError
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ COMPUTE_VALIDATION_SET = 'compute_validation_set'
29
+ """The hyperparameter that asks to split a validation set off the train images when the loop sent none."""
30
+
31
+ VALIDATION_FRACTION = 0.1
32
+
33
+
34
+ def compute_validation_set(hyperparameters: Mapping[str, Any]) -> bool:
35
+ """Whether `compute_validation_set` is set, defaulting to false; anything but a boolean is a CriticalError."""
36
+ value = hyperparameters.get(COMPUTE_VALIDATION_SET, False)
37
+ if not isinstance(value, bool):
38
+ raise CriticalError(f"'{COMPUTE_VALIDATION_SET}' must be true or false, got {value!r}")
39
+ return value
40
+
41
+
42
+ def assign_validation_set(image_data: list[dict], hyperparameters: Mapping[str, Any]) -> None:
43
+ """Move part of the train images to `val` if `compute_validation_set` asks for it and the loop sent no `val` images.
44
+ """
45
+ if any(image['set'] == 'val' for image in image_data):
46
+ logger.info('Selecting models on the validation set of the loop')
47
+ return
48
+ if not compute_validation_set(hyperparameters):
49
+ return
50
+
51
+ train_images = [image for image in image_data if image['set'] == 'train']
52
+ if len(train_images) < 2:
53
+ raise CriticalError(f'{len(train_images)} training image(s) are too few to split off a validation set, '
54
+ 'add more training images')
55
+ val_images = [image for image in train_images if _position(image) < VALIDATION_FRACTION] \
56
+ or [min(train_images, key=_position)]
57
+ for image in val_images:
58
+ image['set'] = 'val'
59
+ logger.info('Split %d of %d training images off as validation set', len(val_images), len(train_images))
60
+
61
+
62
+ def _position(image: dict) -> float:
63
+ return int(hashlib.sha256(image['id'].encode()).hexdigest()[:8], 16) / 16**8
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: learning_loop_node
3
- Version: 0.26.0
3
+ Version: 0.27.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
@@ -349,3 +349,4 @@ After a short time the converted model should be available as well.
349
349
  - Nodes add properties to `model.json`, which contains all the information which are needed by subsequent nodes. These are typically the properties:
350
350
  - `resolution`: resolution in which the model expects images (as `int`, since the resolution is mostly square - later, `resolution_x`` resolution_y` would also be conceivable or `resolutions` to give a list of possible resolutions)
351
351
  - `categories`: list of categories with name, id, (later also type), in the order in which they are used by the model -- this is neccessary to be robust about renamings
352
+ - `conf_threshold` (optional): the minimum confidence the trainer chose for this model's detections
@@ -69,5 +69,6 @@ learning_loop_node/trainer/trainer_logic.py
69
69
  learning_loop_node/trainer/trainer_logic_generic.py
70
70
  learning_loop_node/trainer/trainer_node.py
71
71
  learning_loop_node/trainer/training_slot.py
72
+ learning_loop_node/trainer/validation_set.py
72
73
  learning_loop_node/trainer/rest/__init__.py
73
74
  learning_loop_node/trainer/rest/backdoor_controls.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "learning_loop_node"
3
- version = "0.26.0"
3
+ version = "0.27.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" }