learning-loop-node 0.28.0__tar.gz → 0.29.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.
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/PKG-INFO +14 -1
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/README.md +13 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/data_classes/__init__.py +2 -2
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/data_classes/image_metadata.py +13 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/data_exchanger.py +23 -1
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/detector/detector_node.py +60 -47
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/trainer/exceptions.py +12 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/trainer/trainer_logic.py +0 -1
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/trainer/trainer_logic_generic.py +22 -2
- learning_loop_node-0.29.0/learning_loop_node/trainer/training_results.py +187 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/trainer/validation_set.py +7 -2
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node.egg-info/PKG-INFO +14 -1
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node.egg-info/SOURCES.txt +1 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/pyproject.toml +1 -1
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/__init__.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/annotation/__init__.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/annotation/annotator_logic.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/annotation/annotator_node.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/data_classes/annotation_data.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/data_classes/annotations.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/data_classes/detections.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/data_classes/general.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/data_classes/socket_response.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/data_classes/training.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/detector/__init__.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/detector/categories.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/detector/detector_logic.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/detector/exceptions.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/detector/geometry.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/detector/inbox_filter/__init__.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/detector/inbox_filter/cam_observation_history.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/detector/inbox_filter/relevance_filter.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/detector/outbox.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/detector/postprocess.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/detector/rest/__init__.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/detector/rest/about.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/detector/rest/backdoor_controls.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/detector/rest/detect.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/detector/rest/model_version_control.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/detector/rest/operation_mode.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/detector/rest/outbox_mode.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/detector/rest/upload.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/enums/__init__.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/enums/annotator.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/enums/detector.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/enums/general.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/enums/trainer.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/globals.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/helpers/__init__.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/helpers/background_tasks.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/helpers/entrypoint.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/helpers/environment_reader.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/helpers/gdrive_downloader.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/helpers/log_conf.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/helpers/misc.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/helpers/run.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/loop_communication.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/node.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/py.typed +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/rest.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/trainer/__init__.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/trainer/batch_size.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/trainer/cuda.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/trainer/downloader.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/trainer/executor.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/trainer/hyperparameters.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/trainer/io_helpers.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/trainer/metrics.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/trainer/rest/__init__.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/trainer/rest/backdoor_controls.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/trainer/subprocess.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/trainer/test_executor.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/trainer/trainer_node.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/trainer/training_slot.py +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node.egg-info/dependency_links.txt +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node.egg-info/requires.txt +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node.egg-info/top_level.txt +0 -0
- {learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: learning_loop_node
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.29.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
|
|
@@ -129,6 +129,8 @@ The `batch_detect` endpoint receives a dictionary, with the same entries as the
|
|
|
129
129
|
|
|
130
130
|
- `images`: List of image data dictionaries, each with the same structure as the `image` entry in the `detect` endpoint
|
|
131
131
|
|
|
132
|
+
The entries `camera_id`, `tags`, `source` and `creation_date` apply to every image of the batch. An image data dictionary may also carry its own: its `tags` are added to those of the batch, while its `camera_id`, `source` and `creation_date` replace those of the batch. This way a batch can hold images of several cameras, each filtered and uploaded as its own camera's.
|
|
133
|
+
|
|
132
134
|
Sending a camera's JPEG as it is (`format: 'jpeg'`) saves the bandwidth of a raw pixel array and spares the detector a re-encode for the upload.
|
|
133
135
|
|
|
134
136
|
Example code can be found [in the rosys implementation](https://github.com/zauberzeug/rosys/blob/main/rosys/vision/detector_hardware.py).
|
|
@@ -256,6 +258,17 @@ Trainers fetch the images and anntoations from the Learning Loop to train new mo
|
|
|
256
258
|
|
|
257
259
|
- if the command line tool "jpeginfo" is installed, the downloader will drop corrupted images automatically
|
|
258
260
|
|
|
261
|
+
### Training results
|
|
262
|
+
|
|
263
|
+
Together with its model, a trainer uploads the results of the training to the Learning Loop, e.g. the
|
|
264
|
+
metrics per epoch, the evaluation curves and the confusion matrix. The trainer decides which files
|
|
265
|
+
they are, grouped into the parts `core`, `examples` and `logs`; a trainer that names none uploads
|
|
266
|
+
only its model. `core` is always uploaded, the examples and the raw training log only if the
|
|
267
|
+
training's `upload_examples` and `upload_logs` hyperparameters are true (both default to false).
|
|
268
|
+
The results are sent right after the model and never stop the training: results the Learning Loop
|
|
269
|
+
does not store, e.g. beyond the 5 GiB it keeps per training, are logged and skipped, and a part
|
|
270
|
+
that gets no answer or a server error is sent up to three times.
|
|
271
|
+
|
|
259
272
|
### Several trainers on one GPU
|
|
260
273
|
|
|
261
274
|
Trainers that share a GPU (say, one per model architecture on the same machine) coordinate through
|
|
@@ -96,6 +96,8 @@ The `batch_detect` endpoint receives a dictionary, with the same entries as the
|
|
|
96
96
|
|
|
97
97
|
- `images`: List of image data dictionaries, each with the same structure as the `image` entry in the `detect` endpoint
|
|
98
98
|
|
|
99
|
+
The entries `camera_id`, `tags`, `source` and `creation_date` apply to every image of the batch. An image data dictionary may also carry its own: its `tags` are added to those of the batch, while its `camera_id`, `source` and `creation_date` replace those of the batch. This way a batch can hold images of several cameras, each filtered and uploaded as its own camera's.
|
|
100
|
+
|
|
99
101
|
Sending a camera's JPEG as it is (`format: 'jpeg'`) saves the bandwidth of a raw pixel array and spares the detector a re-encode for the upload.
|
|
100
102
|
|
|
101
103
|
Example code can be found [in the rosys implementation](https://github.com/zauberzeug/rosys/blob/main/rosys/vision/detector_hardware.py).
|
|
@@ -223,6 +225,17 @@ Trainers fetch the images and anntoations from the Learning Loop to train new mo
|
|
|
223
225
|
|
|
224
226
|
- if the command line tool "jpeginfo" is installed, the downloader will drop corrupted images automatically
|
|
225
227
|
|
|
228
|
+
### Training results
|
|
229
|
+
|
|
230
|
+
Together with its model, a trainer uploads the results of the training to the Learning Loop, e.g. the
|
|
231
|
+
metrics per epoch, the evaluation curves and the confusion matrix. The trainer decides which files
|
|
232
|
+
they are, grouped into the parts `core`, `examples` and `logs`; a trainer that names none uploads
|
|
233
|
+
only its model. `core` is always uploaded, the examples and the raw training log only if the
|
|
234
|
+
training's `upload_examples` and `upload_logs` hyperparameters are true (both default to false).
|
|
235
|
+
The results are sent right after the model and never stop the training: results the Learning Loop
|
|
236
|
+
does not store, e.g. beyond the 5 GiB it keeps per training, are logged and skipped, and a part
|
|
237
|
+
that gets no answer or a server error is sent up to three times.
|
|
238
|
+
|
|
226
239
|
### Several trainers on one GPU
|
|
227
240
|
|
|
228
241
|
Trainers that share a GPU (say, one per model architecture on the same machine) coordinate through
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/data_classes/__init__.py
RENAMED
|
@@ -26,7 +26,7 @@ from .general import (
|
|
|
26
26
|
NodeState,
|
|
27
27
|
NodeStatus,
|
|
28
28
|
)
|
|
29
|
-
from .image_metadata import ImageMetadata, ImagesMetadata
|
|
29
|
+
from .image_metadata import CaptureMetadata, ImageMetadata, ImagesMetadata
|
|
30
30
|
from .socket_response import SocketResponse
|
|
31
31
|
from .training import (
|
|
32
32
|
Errors,
|
|
@@ -44,7 +44,7 @@ __all__ = [
|
|
|
44
44
|
'BoxDetection', 'ClassificationDetection', 'ImageMetadata', 'Observation', 'Point', 'PointDetection',
|
|
45
45
|
'SegmentationDetection', 'Shape', 'Detections',
|
|
46
46
|
'AnnotationNodeStatus', 'Category', 'Context', 'DetectorStatus', 'ErrorConfiguration',
|
|
47
|
-
'ModelInformation', 'NodeState', 'NodeStatus', 'ModelVersionResponse', 'ImagesMetadata',
|
|
47
|
+
'ModelInformation', 'NodeState', 'NodeStatus', 'ModelVersionResponse', 'ImagesMetadata', 'CaptureMetadata',
|
|
48
48
|
'SocketResponse',
|
|
49
49
|
'Errors', 'Hyperparameter', 'PretrainedModel', 'Training',
|
|
50
50
|
'TrainingError', 'TrainingOut', 'TrainingStateData', 'TrainingStatus',
|
|
@@ -56,3 +56,16 @@ class ImageMetadata():
|
|
|
56
56
|
@dataclass(**KWONLY_SLOTS)
|
|
57
57
|
class ImagesMetadata():
|
|
58
58
|
items: List[ImageMetadata] = field(default_factory=list, metadata={'description': 'List of image metadata'})
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(**KWONLY_SLOTS)
|
|
62
|
+
class CaptureMetadata:
|
|
63
|
+
"""Where an image comes from and how it is tagged."""
|
|
64
|
+
camera_id: str | None = field(default=None, metadata={
|
|
65
|
+
'description': 'Camera the image was taken with; selects the history of the relevance filter'})
|
|
66
|
+
tags: list[str] = field(default_factory=list, metadata={
|
|
67
|
+
'description': 'Tags of the image'})
|
|
68
|
+
source: str | None = field(default=None, metadata={
|
|
69
|
+
'description': 'Source of the image'})
|
|
70
|
+
creation_date: str | None = field(default=None, metadata={
|
|
71
|
+
'description': 'Creation date of the image'})
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/data_exchanger.py
RENAMED
|
@@ -14,7 +14,12 @@ import aiofiles # type: ignore
|
|
|
14
14
|
from .data_classes import Context
|
|
15
15
|
from .helpers.misc import create_resource_paths, create_task, is_valid_image, is_valid_uuid4
|
|
16
16
|
from .loop_communication import LoopCommunicator
|
|
17
|
-
from .trainer.exceptions import
|
|
17
|
+
from .trainer.exceptions import (
|
|
18
|
+
CriticalError,
|
|
19
|
+
ModelUploadFailedError,
|
|
20
|
+
TrainingResultsNotAnsweredError,
|
|
21
|
+
TrainingResultsUploadFailedError,
|
|
22
|
+
)
|
|
18
23
|
|
|
19
24
|
logger = logging.getLogger(__name__)
|
|
20
25
|
|
|
@@ -207,6 +212,23 @@ class DataExchanger():
|
|
|
207
212
|
training_number, mformat, uploaded_model)
|
|
208
213
|
return uploaded_model['id']
|
|
209
214
|
|
|
215
|
+
async def upload_training_results(self, context: Context, training_number: int, part: str, zip_path: str) -> None:
|
|
216
|
+
"""Store one part of a training's results beside its latest model; a part sent again replaces the stored one.
|
|
217
|
+
|
|
218
|
+
:raise TrainingResultsUploadFailedError: If the loop answered with a client error other than 408.
|
|
219
|
+
:raise TrainingResultsNotAnsweredError: If the loop answered 408 or a server error.
|
|
220
|
+
"""
|
|
221
|
+
path = (f'/{context.organization}/projects/{context.project}/trainings/{training_number}'
|
|
222
|
+
f'/training_results/{part}')
|
|
223
|
+
response = await self.loop_communicator.put(path, files=[zip_path], timeout=60*10)
|
|
224
|
+
if response.status_code == HTTPStatus.REQUEST_TIMEOUT or response.status_code >= 500:
|
|
225
|
+
raise TrainingResultsNotAnsweredError(f'The loop did not answer the upload of the {part} results of '
|
|
226
|
+
f'training {training_number}: {response.status_code} {response.text}')
|
|
227
|
+
if response.status_code != HTTPStatus.OK:
|
|
228
|
+
raise TrainingResultsUploadFailedError(f'The loop did not store the {part} results of training '
|
|
229
|
+
f'{training_number}: {response.status_code} {response.text}')
|
|
230
|
+
logger.info('Uploaded the %s results of training %s', part, training_number)
|
|
231
|
+
|
|
210
232
|
|
|
211
233
|
def _rejects_upload(status_code: int) -> bool:
|
|
212
234
|
"""A client error other than 408 and 429 answers the same however often the upload is sent."""
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/detector/detector_node.py
RENAMED
|
@@ -19,6 +19,7 @@ from socketio import AsyncClient
|
|
|
19
19
|
|
|
20
20
|
from ..data_classes import (
|
|
21
21
|
AboutResponse,
|
|
22
|
+
CaptureMetadata,
|
|
22
23
|
Context,
|
|
23
24
|
DetectorStatus,
|
|
24
25
|
ImageMetadata,
|
|
@@ -272,10 +273,16 @@ class DetectorNode(Node):
|
|
|
272
273
|
|
|
273
274
|
Data dict follows the schema of the detect endpoint,
|
|
274
275
|
but 'images' is a list of image dicts.
|
|
276
|
+
Each image dict may also carry its own:
|
|
277
|
+
- camera_id: replaces the camera_id of the batch
|
|
278
|
+
- tags: added to the tags of the batch
|
|
279
|
+
- source: replaces the source of the batch
|
|
280
|
+
- creation_date: replaces the creation_date of the batch
|
|
275
281
|
"""
|
|
276
282
|
try:
|
|
277
283
|
images_data = data['images']
|
|
278
284
|
images = [image_from_dict(image) for image in images_data]
|
|
285
|
+
capture_metadata = [_capture_metadata(data, image) for image in images_data]
|
|
279
286
|
except Exception:
|
|
280
287
|
self.log.exception('could not parse images from socketio')
|
|
281
288
|
return {'error': 'could not parse images from data'}
|
|
@@ -283,11 +290,8 @@ class DetectorNode(Node):
|
|
|
283
290
|
try:
|
|
284
291
|
det = await self.get_batch_detections(
|
|
285
292
|
images=images,
|
|
286
|
-
|
|
287
|
-
camera_id=data.get('camera_id', None),
|
|
288
|
-
source=data.get('source', None),
|
|
293
|
+
capture_metadata=capture_metadata,
|
|
289
294
|
autoupload=data.get('autoupload', 'filtered'),
|
|
290
|
-
creation_date=data.get('creation_date', None),
|
|
291
295
|
)
|
|
292
296
|
return jsonable_encoder(asdict(det))
|
|
293
297
|
except DetectorUnavailableError as e:
|
|
@@ -638,35 +642,17 @@ class DetectorNode(Node):
|
|
|
638
642
|
array = await _pixel_array(image)
|
|
639
643
|
async with self.detection_lock:
|
|
640
644
|
detector = _unwrap_detector(self._detector)
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
metadata.tags.extend(tags)
|
|
644
|
-
metadata.source = source
|
|
645
|
-
metadata.created = creation_date
|
|
646
|
-
|
|
647
|
-
fix_shape_detections(metadata)
|
|
648
|
-
n_bo, n_cl = len(metadata.box_detections), len(metadata.classification_detections)
|
|
649
|
-
n_po, n_se = len(metadata.point_detections), len(metadata.segmentation_detections)
|
|
650
|
-
self.log.debug('Detected: %d boxes, %d points, %d segs, %d classes', n_bo, n_po, n_se, n_cl)
|
|
645
|
+
detections = await run.io_bound(detector.logic.evaluate, array)
|
|
651
646
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
background_tasks.create(self.outbox.save(image, metadata))
|
|
656
|
-
elif autoupload == 'disabled':
|
|
657
|
-
pass
|
|
658
|
-
else:
|
|
659
|
-
self.log.error('unknown autoupload value %s', autoupload)
|
|
660
|
-
return metadata
|
|
647
|
+
capture = CaptureMetadata(camera_id=camera_id, tags=tags, source=source, creation_date=creation_date)
|
|
648
|
+
self._finish_detection(detections, image, capture, autoupload)
|
|
649
|
+
return detections
|
|
661
650
|
|
|
662
651
|
async def get_batch_detections(self,
|
|
663
652
|
images: Sequence[ImageData],
|
|
664
|
-
|
|
653
|
+
capture_metadata: Sequence[CaptureMetadata],
|
|
665
654
|
*,
|
|
666
|
-
|
|
667
|
-
source: Optional[str] = None,
|
|
668
|
-
autoupload: str = 'filtered',
|
|
669
|
-
creation_date: Optional[str] = None) -> ImagesMetadata:
|
|
655
|
+
autoupload: str = 'filtered') -> ImagesMetadata:
|
|
670
656
|
"""
|
|
671
657
|
Processing function for the detector node when a batch inference is requested via SocketIO.
|
|
672
658
|
|
|
@@ -674,35 +660,45 @@ class DetectorNode(Node):
|
|
|
674
660
|
cares about uploading to the loop and returns the detections as a list of ImageMetadata.
|
|
675
661
|
Each image is a pixel array or JPEG bytes; JPEG bytes are decoded for the inference
|
|
676
662
|
and stored as they are if the image is uploaded.
|
|
663
|
+
`capture_metadata` holds the camera, tags, source and creation date of each image.
|
|
677
664
|
Raises exception if no model is loaded.
|
|
678
665
|
"""
|
|
666
|
+
if len(capture_metadata) != len(images):
|
|
667
|
+
raise ValueError(f'got {len(capture_metadata)} capture metadata for {len(images)} images')
|
|
679
668
|
_unwrap_detector(self._detector) # fails fast, before the decode
|
|
680
669
|
arrays = await asyncio.gather(*(_pixel_array(image) for image in images))
|
|
681
670
|
async with self.detection_lock:
|
|
682
671
|
detector = _unwrap_detector(self._detector)
|
|
683
672
|
all_detections = await run.io_bound(detector.logic.batch_evaluate, arrays)
|
|
684
673
|
|
|
685
|
-
for
|
|
686
|
-
|
|
687
|
-
metadata.source = source
|
|
688
|
-
metadata.created = creation_date
|
|
689
|
-
|
|
690
|
-
for detections, image in zip(all_detections.items, images):
|
|
691
|
-
fix_shape_detections(detections)
|
|
692
|
-
n_bo, n_cl = len(detections.box_detections), len(detections.classification_detections)
|
|
693
|
-
n_po, n_se = len(detections.point_detections), len(detections.segmentation_detections)
|
|
694
|
-
self.log.debug('Detected: %d boxes, %d points, %d segs, %d classes', n_bo, n_po, n_se, n_cl)
|
|
695
|
-
|
|
696
|
-
if autoupload == 'filtered':
|
|
697
|
-
background_tasks.create(self.relevance_filter.may_upload_detections(detections, camera_id, image))
|
|
698
|
-
elif autoupload == 'all':
|
|
699
|
-
background_tasks.create(self.outbox.save(image, detections))
|
|
700
|
-
elif autoupload == 'disabled':
|
|
701
|
-
pass
|
|
702
|
-
else:
|
|
703
|
-
self.log.error('unknown autoupload value %s', autoupload)
|
|
674
|
+
for detections, image, capture in zip(all_detections.items, images, capture_metadata):
|
|
675
|
+
self._finish_detection(detections, image, capture, autoupload)
|
|
704
676
|
return all_detections
|
|
705
677
|
|
|
678
|
+
def _finish_detection(self,
|
|
679
|
+
detections: ImageMetadata,
|
|
680
|
+
image: ImageData,
|
|
681
|
+
capture: CaptureMetadata,
|
|
682
|
+
autoupload: str) -> None:
|
|
683
|
+
detections.tags.extend(capture.tags)
|
|
684
|
+
detections.source = capture.source
|
|
685
|
+
detections.created = capture.creation_date
|
|
686
|
+
|
|
687
|
+
fix_shape_detections(detections)
|
|
688
|
+
n_bo, n_cl = len(detections.box_detections), len(detections.classification_detections)
|
|
689
|
+
n_po, n_se = len(detections.point_detections), len(detections.segmentation_detections)
|
|
690
|
+
self.log.debug('Detected: %d boxes, %d points, %d segs, %d classes', n_bo, n_po, n_se, n_cl)
|
|
691
|
+
|
|
692
|
+
if autoupload == 'filtered':
|
|
693
|
+
background_tasks.create(
|
|
694
|
+
self.relevance_filter.may_upload_detections(detections, capture.camera_id, image))
|
|
695
|
+
elif autoupload == 'all':
|
|
696
|
+
background_tasks.create(self.outbox.save(image, detections))
|
|
697
|
+
elif autoupload == 'disabled':
|
|
698
|
+
pass
|
|
699
|
+
else:
|
|
700
|
+
self.log.error('unknown autoupload value %s', autoupload)
|
|
701
|
+
|
|
706
702
|
async def upload_images(
|
|
707
703
|
self, *,
|
|
708
704
|
images: Sequence[ImageData],
|
|
@@ -785,6 +781,23 @@ async def _pixel_array(image: ImageData) -> np.ndarray:
|
|
|
785
781
|
return await run.io_bound(jpg_bytes_to_numpy_array, image)
|
|
786
782
|
|
|
787
783
|
|
|
784
|
+
def _capture_metadata(batch_data: dict, image_data: dict) -> CaptureMetadata:
|
|
785
|
+
"""The values of one image dict of a batch: its own where it sets them, the batch's otherwise, and both tags."""
|
|
786
|
+
image_tags = image_data.get('tags') or []
|
|
787
|
+
if not isinstance(image_tags, list):
|
|
788
|
+
raise ValueError(f'tags of an image must be a list, got {type(image_tags).__name__}')
|
|
789
|
+
return CaptureMetadata(camera_id=_own_or_batch('camera_id', image_data, batch_data),
|
|
790
|
+
tags=[*batch_data.get('tags', []), *image_tags],
|
|
791
|
+
source=_own_or_batch('source', image_data, batch_data),
|
|
792
|
+
creation_date=_own_or_batch('creation_date', image_data, batch_data))
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def _own_or_batch(key: str, image_data: dict, batch_data: dict) -> str | None:
|
|
796
|
+
"""`None` counts as not set, so an image cannot clear a value of the batch."""
|
|
797
|
+
own = image_data.get(key)
|
|
798
|
+
return own if own is not None else batch_data.get(key)
|
|
799
|
+
|
|
800
|
+
|
|
788
801
|
def _unwrap_detector(state: '_DetectorState') -> _ActiveDetector:
|
|
789
802
|
"""Return the active detector or raise DetectorUnavailableError."""
|
|
790
803
|
match state:
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/trainer/exceptions.py
RENAMED
|
@@ -11,6 +11,18 @@ class ModelUploadFailedError(Exception):
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
class TrainingResultsUploadFailedError(Exception):
|
|
15
|
+
"""Raised when the loop answered the upload of a part of a training's results with a client error other than 408.
|
|
16
|
+
The answer is final: sending the part again would get the same answer.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TrainingResultsNotAnsweredError(Exception):
|
|
21
|
+
"""Raised when the loop gave up waiting for a part of a training's results (408) or answered with a server error;
|
|
22
|
+
the part may be sent again.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
|
|
14
26
|
class TrainingRefusedError(Exception):
|
|
15
27
|
"""Raised when a trainer cannot take on a training it was asked to begin; nothing was set up."""
|
|
16
28
|
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/trainer/trainer_logic.py
RENAMED
|
@@ -26,7 +26,6 @@ class TrainerLogic(TrainerLogicGeneric):
|
|
|
26
26
|
self._detection_progress: Optional[float] = None
|
|
27
27
|
self._executor: Optional[Executor] = None
|
|
28
28
|
self._process_started: bool = False
|
|
29
|
-
self._stop_requested: bool = False
|
|
30
29
|
self.start_training_task: Optional[Coroutine] = None
|
|
31
30
|
self.inference_batch_size = self._environment_vars.inference_batch_size
|
|
32
31
|
|
|
@@ -6,6 +6,7 @@ import sys
|
|
|
6
6
|
import time
|
|
7
7
|
from abc import ABC, abstractmethod
|
|
8
8
|
from dataclasses import asdict
|
|
9
|
+
from pathlib import Path
|
|
9
10
|
from typing import TYPE_CHECKING, Any, Callable, Coroutine, Dict, List, Optional
|
|
10
11
|
|
|
11
12
|
from fastapi.encoders import jsonable_encoder
|
|
@@ -26,6 +27,7 @@ from ..helpers.misc import (
|
|
|
26
27
|
delete_all_training_folders,
|
|
27
28
|
is_valid_uuid4,
|
|
28
29
|
)
|
|
30
|
+
from . import training_results
|
|
29
31
|
from .downloader import TrainingsDownloader
|
|
30
32
|
from .exceptions import CriticalError, HyperparameterMismatchError, NodeNeedsRestartError, TrainingRefusedError
|
|
31
33
|
from .hyperparameters import resolve, warn_about_undeclared
|
|
@@ -57,6 +59,7 @@ class TrainerLogicGeneric(ABC):
|
|
|
57
59
|
self.model_format: str = model_format
|
|
58
60
|
self.errors = Errors()
|
|
59
61
|
self._state_failures: Dict[str, int] = {} # failed attempts in a row, by error key
|
|
62
|
+
self._stop_requested: bool = False # set by a trainer that stopped its training process; not kept on restart
|
|
60
63
|
|
|
61
64
|
self.training_task: Optional[asyncio.Task] = None
|
|
62
65
|
self.shutdown_event: asyncio.Event = asyncio.Event()
|
|
@@ -416,7 +419,11 @@ class TrainerLogicGeneric(ABC):
|
|
|
416
419
|
self.last_training_io.save(self.training)
|
|
417
420
|
|
|
418
421
|
async def _prepare(self) -> None:
|
|
419
|
-
"""Downloads images to the images_folder and saves annotations to training.data.image_data.
|
|
422
|
+
"""Downloads images to the images_folder and saves annotations to training.data.image_data.
|
|
423
|
+
Moves part of the train images to `val` if `compute_validation_set` asks for it (see `validation_set`).
|
|
424
|
+
A malformed `upload_examples` or `upload_logs` ends the training here, before it has run.
|
|
425
|
+
"""
|
|
426
|
+
training_results.check_hyperparameters(self.training.hyperparameters)
|
|
420
427
|
self.node.data_exchanger.set_context(self.training.context)
|
|
421
428
|
downloader = TrainingsDownloader(self.node.data_exchanger)
|
|
422
429
|
image_data, skipped_image_count = await downloader.download_training_data(self.training.images_folder)
|
|
@@ -489,11 +496,16 @@ class TrainerLogicGeneric(ABC):
|
|
|
489
496
|
self.errors.reset(error_key)
|
|
490
497
|
|
|
491
498
|
async def _upload_model(self) -> None:
|
|
492
|
-
"""Uploads the latest model to the Learning Loop.
|
|
499
|
+
"""Uploads the latest model and then its training results to the Learning Loop.
|
|
500
|
+
The results never fail the state (see `training_results`).
|
|
493
501
|
"""
|
|
494
502
|
new_model_uuid = await self._upload_model_return_new_model_uuid(self.training.context)
|
|
495
503
|
logger.info('Successfully uploaded model and received new model id: %s', new_model_uuid)
|
|
496
504
|
self.training.model_uuid_for_detecting = new_model_uuid
|
|
505
|
+
await training_results.upload_results(self.training, model_uuid=new_model_uuid, node_name=self.node.name,
|
|
506
|
+
node_uuid=self.node.uuid, get_results=self._get_training_results,
|
|
507
|
+
stop_requested=self._stop_requested,
|
|
508
|
+
data_exchanger=self.node.data_exchanger)
|
|
497
509
|
|
|
498
510
|
async def _upload_model_return_new_model_uuid(self, context: Context) -> str:
|
|
499
511
|
"""Upload model files, usually pytorch model (.pt) hyp.yaml and the converted .wts file.
|
|
@@ -658,6 +670,14 @@ class TrainerLogicGeneric(ABC):
|
|
|
658
670
|
"""
|
|
659
671
|
raise NotImplementedError
|
|
660
672
|
|
|
673
|
+
def _get_training_results(self, training_folder: Path) -> dict[str, list[Path]]:
|
|
674
|
+
"""Called when the model is uploaded. Returns the result files of the training by part, stored beside the model.
|
|
675
|
+
The parts are `core` (always uploaded) and `examples` and `logs` (uploaded only if `upload_examples` and
|
|
676
|
+
`upload_logs` are true); see `training_results`. Directories go in as a whole. Name only results, never the
|
|
677
|
+
dataset. Default: no results, and nothing is uploaded.
|
|
678
|
+
"""
|
|
679
|
+
return {}
|
|
680
|
+
|
|
661
681
|
def _get_model_metadata(self) -> dict[str, Any]:
|
|
662
682
|
"""Return the values the trainer determined for this model, to be merged into its model.json.
|
|
663
683
|
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""The results of a training — metrics, curves, logs — uploaded to the loop beside its model.
|
|
2
|
+
|
|
3
|
+
A trainer names its result files in `_get_training_results`, grouped into the parts `core`, `examples` and
|
|
4
|
+
`logs`. `upload_results` sends them once the model is uploaded and never fails the training: a part the loop
|
|
5
|
+
answered is done, a part that got no answer or a server error is sent up to `MAX_ATTEMPTS` times, and
|
|
6
|
+
anything else that goes wrong is logged and skips the part. `core` is always uploaded; `examples` and `logs`
|
|
7
|
+
only if the training's `upload_examples` and `upload_logs` hyperparameters ask for them. Each part goes up as
|
|
8
|
+
one zip that keeps the paths inside the training folder, and `core` additionally holds `meta.json`, where
|
|
9
|
+
the results come from.
|
|
10
|
+
"""
|
|
11
|
+
import asyncio
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
import zipfile
|
|
16
|
+
from collections.abc import Callable, Mapping
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
import httpx
|
|
23
|
+
|
|
24
|
+
from ..data_classes import Training
|
|
25
|
+
from ..data_exchanger import DataExchanger
|
|
26
|
+
from .exceptions import CriticalError, TrainingResultsNotAnsweredError, TrainingResultsUploadFailedError
|
|
27
|
+
from .validation_set import bool_hyperparameter
|
|
28
|
+
|
|
29
|
+
CORE = 'core'
|
|
30
|
+
EXAMPLES = 'examples'
|
|
31
|
+
LOGS = 'logs'
|
|
32
|
+
PARTS = (CORE, EXAMPLES, LOGS)
|
|
33
|
+
|
|
34
|
+
UPLOAD_EXAMPLES = 'upload_examples'
|
|
35
|
+
UPLOAD_LOGS = 'upload_logs'
|
|
36
|
+
OPTIONAL_PARTS = {EXAMPLES: UPLOAD_EXAMPLES, LOGS: UPLOAD_LOGS}
|
|
37
|
+
"""The parts uploaded only if the hyperparameter they map to is true."""
|
|
38
|
+
|
|
39
|
+
META_FILE = 'meta.json'
|
|
40
|
+
RESULTS_FOLDER = 'training_results'
|
|
41
|
+
"""Inside the training folder: the zips and the record of the parts that are done."""
|
|
42
|
+
DONE_FILE = 'uploaded_parts.txt'
|
|
43
|
+
|
|
44
|
+
MAX_ATTEMPTS = 3
|
|
45
|
+
RETRY_DELAY = 5 # seconds before the second attempt, doubled before each further one
|
|
46
|
+
|
|
47
|
+
COMPRESSED_SUFFIXES = {'.jpg', '.jpeg', '.png', '.webp', '.gif', '.mp4', '.zip', '.gz', '.tgz', '.xz', '.7z'}
|
|
48
|
+
"""Files stored in a zip as they are; compressing them again costs time and saves almost nothing."""
|
|
49
|
+
|
|
50
|
+
logger = logging.getLogger(__name__)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def check_hyperparameters(hyperparameters: Mapping[str, Any]) -> None:
|
|
54
|
+
"""Raise a CriticalError if `upload_examples` or `upload_logs` is set to anything but a boolean."""
|
|
55
|
+
for name in OPTIONAL_PARTS.values():
|
|
56
|
+
bool_hyperparameter(hyperparameters, name)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def upload_results(training: Training, *, model_uuid: str, node_name: str, node_uuid: str,
|
|
60
|
+
get_results: Callable[[Path], Mapping[str, list[Path]]], stop_requested: bool,
|
|
61
|
+
data_exchanger: DataExchanger) -> None:
|
|
62
|
+
"""Upload the parts of `get_results(training_folder)` the training's hyperparameters ask for; never raises.
|
|
63
|
+
|
|
64
|
+
A part is recorded as done once it is answered or skipped, so a second call sends only the parts left.
|
|
65
|
+
"""
|
|
66
|
+
training_folder = Path(training.training_folder)
|
|
67
|
+
results_folder = training_folder / RESULTS_FOLDER
|
|
68
|
+
try:
|
|
69
|
+
results = get_results(training_folder)
|
|
70
|
+
parts = _parts_to_upload(results, training.hyperparameters)
|
|
71
|
+
meta_content = _meta(training, model_uuid=model_uuid, node_name=node_name, node_uuid=node_uuid,
|
|
72
|
+
stop_requested=stop_requested)
|
|
73
|
+
done = _load_done(results_folder)
|
|
74
|
+
except Exception:
|
|
75
|
+
logger.exception('Skipping the results of training %s', training.training_number)
|
|
76
|
+
return
|
|
77
|
+
for part in [p for p in parts if p not in done]:
|
|
78
|
+
try:
|
|
79
|
+
await _upload_part(training, part, results[part], meta_content if part == CORE else None, data_exchanger)
|
|
80
|
+
except TrainingResultsUploadFailedError as e:
|
|
81
|
+
logger.warning('Skipping the %s results of training %s: %s', part, training.training_number, e)
|
|
82
|
+
except Exception:
|
|
83
|
+
logger.exception('Skipping the %s results of training %s', part, training.training_number)
|
|
84
|
+
done.append(part)
|
|
85
|
+
try:
|
|
86
|
+
_save_done(results_folder, done)
|
|
87
|
+
except OSError:
|
|
88
|
+
logger.exception('Could not record the %s results of training %s as done', part, training.training_number)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _parts_to_upload(results: Mapping[str, list[Path]], hyperparameters: Mapping[str, Any]) -> list[str]:
|
|
92
|
+
"""The parts of `results` to upload, in upload order: `core` first, the optional ones only if asked for.
|
|
93
|
+
|
|
94
|
+
A part not in `PARTS`, or one whose hyperparameter is malformed, is logged and left out.
|
|
95
|
+
"""
|
|
96
|
+
parts = []
|
|
97
|
+
for part in results:
|
|
98
|
+
if part not in PARTS:
|
|
99
|
+
logger.warning('Leaving out the training results part %r, which is none of %s', part, ', '.join(PARTS))
|
|
100
|
+
continue
|
|
101
|
+
if part in OPTIONAL_PARTS:
|
|
102
|
+
try:
|
|
103
|
+
if not bool_hyperparameter(hyperparameters, OPTIONAL_PARTS[part]):
|
|
104
|
+
continue
|
|
105
|
+
except CriticalError as e:
|
|
106
|
+
logger.error('Leaving out the %s results: %s', part, e)
|
|
107
|
+
continue
|
|
108
|
+
parts.append(part)
|
|
109
|
+
return sorted(parts, key=lambda part: part != CORE)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _meta(training: Training, *, model_uuid: str, node_name: str, node_uuid: str,
|
|
113
|
+
stop_requested: bool) -> dict[str, Any]:
|
|
114
|
+
"""Where the results come from, so they explain themselves without the loop's database."""
|
|
115
|
+
return {
|
|
116
|
+
'node_name': node_name,
|
|
117
|
+
'node_uuid': node_uuid,
|
|
118
|
+
'organization': training.context.organization,
|
|
119
|
+
'project': training.context.project,
|
|
120
|
+
'training_number': training.training_number,
|
|
121
|
+
'model_uuid': model_uuid,
|
|
122
|
+
'start_time': datetime.fromtimestamp(training.start_time, timezone.utc).isoformat(),
|
|
123
|
+
'uploaded_at': datetime.now(timezone.utc).isoformat(),
|
|
124
|
+
'stop_requested': stop_requested,
|
|
125
|
+
'learning_loop_node_version': _library_version(),
|
|
126
|
+
'hyperparameters': dict(training.hyperparameters),
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
async def _upload_part(training: Training, part: str, files: list[Path], part_meta: dict[str, Any] | None,
|
|
131
|
+
data_exchanger: DataExchanger) -> None:
|
|
132
|
+
"""Zip and send one part; send it again while it gets no answer, up to `MAX_ATTEMPTS` times."""
|
|
133
|
+
training_folder = Path(training.training_folder)
|
|
134
|
+
zip_path = training_folder / RESULTS_FOLDER / f'{part}.zip'
|
|
135
|
+
await asyncio.to_thread(_write_zip, files, training_folder, zip_path, part_meta)
|
|
136
|
+
for attempt in range(1, MAX_ATTEMPTS + 1):
|
|
137
|
+
try:
|
|
138
|
+
await data_exchanger.upload_training_results(training.context, training.training_number, part,
|
|
139
|
+
str(zip_path))
|
|
140
|
+
return
|
|
141
|
+
except (TrainingResultsNotAnsweredError, httpx.TransportError):
|
|
142
|
+
if attempt == MAX_ATTEMPTS:
|
|
143
|
+
raise
|
|
144
|
+
await asyncio.sleep(RETRY_DELAY * 2 ** (attempt - 1))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _write_zip(files: list[Path], training_folder: Path, zip_path: Path, meta: dict[str, Any] | None) -> None:
|
|
148
|
+
"""Zip `files` into `zip_path`; directories go in as a whole, except anything in the folder of `zip_path`.
|
|
149
|
+
|
|
150
|
+
A path inside `training_folder` keeps its path relative to it, any other its path relative to the parent of
|
|
151
|
+
the file or directory named. Files in `COMPRESSED_SUFFIXES` are stored, everything else compressed.
|
|
152
|
+
A file that does not exist is logged and left out.
|
|
153
|
+
"""
|
|
154
|
+
results_folder = zip_path.parent
|
|
155
|
+
results_folder.mkdir(parents=True, exist_ok=True)
|
|
156
|
+
partial = zip_path.with_name(f'{zip_path.name}.partial')
|
|
157
|
+
with zipfile.ZipFile(partial, 'w', compression=zipfile.ZIP_DEFLATED) as zipf:
|
|
158
|
+
for file in files:
|
|
159
|
+
if not file.exists():
|
|
160
|
+
logger.warning('training result %s does not exist, leaving it out', file)
|
|
161
|
+
continue
|
|
162
|
+
root = training_folder if file.is_relative_to(training_folder) else file.parent
|
|
163
|
+
for path in sorted(file.rglob('*')) if file.is_dir() else [file]:
|
|
164
|
+
if path.is_file() and not path.is_relative_to(results_folder):
|
|
165
|
+
stored = path.suffix.lower() in COMPRESSED_SUFFIXES
|
|
166
|
+
zipf.write(path, str(path.relative_to(root)),
|
|
167
|
+
compress_type=zipfile.ZIP_STORED if stored else zipfile.ZIP_DEFLATED)
|
|
168
|
+
if meta is not None:
|
|
169
|
+
zipf.writestr(META_FILE, json.dumps(meta, indent=1))
|
|
170
|
+
os.replace(partial, zip_path)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _load_done(results_folder: Path) -> list[str]:
|
|
174
|
+
path = results_folder / DONE_FILE
|
|
175
|
+
return path.read_text().split() if path.exists() else []
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _save_done(results_folder: Path, done: list[str]) -> None:
|
|
179
|
+
results_folder.mkdir(parents=True, exist_ok=True)
|
|
180
|
+
(results_folder / DONE_FILE).write_text('\n'.join(done))
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _library_version() -> str | None:
|
|
184
|
+
try:
|
|
185
|
+
return version('learning_loop_node')
|
|
186
|
+
except PackageNotFoundError:
|
|
187
|
+
return None
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/trainer/validation_set.py
RENAMED
|
@@ -40,9 +40,14 @@ VALIDATION_FRACTION = 0.1
|
|
|
40
40
|
|
|
41
41
|
def compute_validation_set(hyperparameters: Mapping[str, Any]) -> bool:
|
|
42
42
|
"""Whether `compute_validation_set` is set, defaulting to false; anything but a boolean is a CriticalError."""
|
|
43
|
-
|
|
43
|
+
return bool_hyperparameter(hyperparameters, COMPUTE_VALIDATION_SET)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def bool_hyperparameter(hyperparameters: Mapping[str, Any], name: str) -> bool:
|
|
47
|
+
"""Whether the hyperparameter `name` is set, defaulting to false; anything but a boolean is a CriticalError."""
|
|
48
|
+
value = hyperparameters.get(name, False)
|
|
44
49
|
if not isinstance(value, bool):
|
|
45
|
-
raise CriticalError(f"'{
|
|
50
|
+
raise CriticalError(f"'{name}' must be true or false, got {value!r}")
|
|
46
51
|
return value
|
|
47
52
|
|
|
48
53
|
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: learning_loop_node
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.29.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
|
|
@@ -129,6 +129,8 @@ The `batch_detect` endpoint receives a dictionary, with the same entries as the
|
|
|
129
129
|
|
|
130
130
|
- `images`: List of image data dictionaries, each with the same structure as the `image` entry in the `detect` endpoint
|
|
131
131
|
|
|
132
|
+
The entries `camera_id`, `tags`, `source` and `creation_date` apply to every image of the batch. An image data dictionary may also carry its own: its `tags` are added to those of the batch, while its `camera_id`, `source` and `creation_date` replace those of the batch. This way a batch can hold images of several cameras, each filtered and uploaded as its own camera's.
|
|
133
|
+
|
|
132
134
|
Sending a camera's JPEG as it is (`format: 'jpeg'`) saves the bandwidth of a raw pixel array and spares the detector a re-encode for the upload.
|
|
133
135
|
|
|
134
136
|
Example code can be found [in the rosys implementation](https://github.com/zauberzeug/rosys/blob/main/rosys/vision/detector_hardware.py).
|
|
@@ -256,6 +258,17 @@ Trainers fetch the images and anntoations from the Learning Loop to train new mo
|
|
|
256
258
|
|
|
257
259
|
- if the command line tool "jpeginfo" is installed, the downloader will drop corrupted images automatically
|
|
258
260
|
|
|
261
|
+
### Training results
|
|
262
|
+
|
|
263
|
+
Together with its model, a trainer uploads the results of the training to the Learning Loop, e.g. the
|
|
264
|
+
metrics per epoch, the evaluation curves and the confusion matrix. The trainer decides which files
|
|
265
|
+
they are, grouped into the parts `core`, `examples` and `logs`; a trainer that names none uploads
|
|
266
|
+
only its model. `core` is always uploaded, the examples and the raw training log only if the
|
|
267
|
+
training's `upload_examples` and `upload_logs` hyperparameters are true (both default to false).
|
|
268
|
+
The results are sent right after the model and never stop the training: results the Learning Loop
|
|
269
|
+
does not store, e.g. beyond the 5 GiB it keeps per training, are logged and skipped, and a part
|
|
270
|
+
that gets no answer or a server error is sent up to three times.
|
|
271
|
+
|
|
259
272
|
### Several trainers on one GPU
|
|
260
273
|
|
|
261
274
|
Trainers that share a GPU (say, one per model architecture on the same machine) coordinate through
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node.egg-info/SOURCES.txt
RENAMED
|
@@ -69,6 +69,7 @@ learning_loop_node/trainer/test_executor.py
|
|
|
69
69
|
learning_loop_node/trainer/trainer_logic.py
|
|
70
70
|
learning_loop_node/trainer/trainer_logic_generic.py
|
|
71
71
|
learning_loop_node/trainer/trainer_node.py
|
|
72
|
+
learning_loop_node/trainer/training_results.py
|
|
72
73
|
learning_loop_node/trainer/training_slot.py
|
|
73
74
|
learning_loop_node/trainer/validation_set.py
|
|
74
75
|
learning_loop_node/trainer/rest/__init__.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "learning_loop_node"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.29.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" }
|
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/annotation/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/data_classes/general.py
RENAMED
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/data_classes/training.py
RENAMED
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/detector/__init__.py
RENAMED
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/detector/categories.py
RENAMED
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/detector/exceptions.py
RENAMED
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/detector/geometry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/detector/outbox.py
RENAMED
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/detector/postprocess.py
RENAMED
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/detector/rest/__init__.py
RENAMED
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/detector/rest/about.py
RENAMED
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/detector/rest/detect.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/detector/rest/upload.py
RENAMED
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/enums/__init__.py
RENAMED
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/enums/annotator.py
RENAMED
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/enums/detector.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/helpers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/helpers/entrypoint.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/helpers/log_conf.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/loop_communication.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/trainer/__init__.py
RENAMED
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/trainer/batch_size.py
RENAMED
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/trainer/downloader.py
RENAMED
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/trainer/executor.py
RENAMED
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/trainer/io_helpers.py
RENAMED
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/trainer/metrics.py
RENAMED
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/trainer/rest/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/trainer/subprocess.py
RENAMED
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/trainer/test_executor.py
RENAMED
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/trainer/trainer_node.py
RENAMED
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node/trainer/training_slot.py
RENAMED
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node.egg-info/requires.txt
RENAMED
|
File without changes
|
{learning_loop_node-0.28.0 → learning_loop_node-0.29.0}/learning_loop_node.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|