smqtk_classifier 0.20.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- smqtk_classifier/__init__.py +9 -0
- smqtk_classifier/_defaults.py +8 -0
- smqtk_classifier/classification_element_factory.py +116 -0
- smqtk_classifier/classify_descriptor_collection.py +298 -0
- smqtk_classifier/exceptions.py +29 -0
- smqtk_classifier/impls/__init__.py +0 -0
- smqtk_classifier/impls/classification_element/__init__.py +0 -0
- smqtk_classifier/impls/classification_element/file.py +125 -0
- smqtk_classifier/impls/classification_element/memory.py +77 -0
- smqtk_classifier/impls/classification_element/postgres.py +263 -0
- smqtk_classifier/impls/classify_descriptor/__init__.py +0 -0
- smqtk_classifier/impls/classify_descriptor/classify_index_label_descriptor.py +63 -0
- smqtk_classifier/impls/classify_descriptor_supervised/__init__.py +0 -0
- smqtk_classifier/impls/classify_descriptor_supervised/libsvm.py +420 -0
- smqtk_classifier/impls/classify_descriptor_supervised/sklearn_logistic_regression.py +84 -0
- smqtk_classifier/impls/classify_descriptor_supervised/sklearn_svm.py +232 -0
- smqtk_classifier/interfaces/__init__.py +0 -0
- smqtk_classifier/interfaces/classification_element.py +264 -0
- smqtk_classifier/interfaces/classify_descriptor.py +399 -0
- smqtk_classifier/interfaces/classify_descriptor_supervised.py +75 -0
- smqtk_classifier/interfaces/classify_image.py +60 -0
- smqtk_classifier/interfaces/classify_image_supervised.py +73 -0
- smqtk_classifier/py.typed +0 -0
- smqtk_classifier-0.20.0.dist-info/LICENSE.txt +27 -0
- smqtk_classifier-0.20.0.dist-info/METADATA +55 -0
- smqtk_classifier-0.20.0.dist-info/RECORD +28 -0
- smqtk_classifier-0.20.0.dist-info/WHEEL +4 -0
- smqtk_classifier-0.20.0.dist-info/entry_points.txt +9 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from .interfaces.classification_element import ClassificationElement # noqa: F401
|
|
2
|
+
from .interfaces.classify_descriptor import ClassifyDescriptor # noqa: F401
|
|
3
|
+
from .interfaces.classify_descriptor_supervised import ClassifyDescriptorSupervised # noqa: F401
|
|
4
|
+
from .interfaces.classify_image import ClassifyImage # noqa: F401
|
|
5
|
+
from .interfaces.classify_image_supervised import ClassifyImageSupervised # noqa: F401
|
|
6
|
+
|
|
7
|
+
from .classification_element_factory import ClassificationElementFactory # noqa: F401
|
|
8
|
+
from .classify_descriptor_collection import ClassifyDescriptorCollection # noqa: F401
|
|
9
|
+
from .classify_descriptor_collection import ClassifierCollection # noqa: F401
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from smqtk_classifier.classification_element_factory import ClassificationElementFactory
|
|
2
|
+
from smqtk_classifier.impls.classification_element.memory import MemoryClassificationElement
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
# Default classifier element factory for interfaces.
|
|
6
|
+
DFLT_CLASSIFIER_FACTORY = ClassificationElementFactory(
|
|
7
|
+
MemoryClassificationElement, {}
|
|
8
|
+
)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from typing import Any, Dict, Hashable, Type, TypeVar
|
|
2
|
+
|
|
3
|
+
from smqtk_core import Configurable
|
|
4
|
+
from smqtk_core.configuration import (
|
|
5
|
+
cls_conf_from_config_dict,
|
|
6
|
+
cls_conf_to_config_dict,
|
|
7
|
+
make_default_config
|
|
8
|
+
)
|
|
9
|
+
from smqtk_core.dict import merge_dict
|
|
10
|
+
|
|
11
|
+
from smqtk_classifier.interfaces.classification_element import ClassificationElement
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
C = TypeVar("C", bound=ClassificationElement)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ClassificationElementFactory (Configurable):
|
|
18
|
+
"""
|
|
19
|
+
Factory class for producing ClassificationElement instances of a specified
|
|
20
|
+
type and configuration.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
# noinspection PyShadowingBuiltins
|
|
24
|
+
def __init__(self, type: Type[C], type_config: Dict[str, Any]):
|
|
25
|
+
"""
|
|
26
|
+
Initialize the factory to produce ClassificationElement instances of the
|
|
27
|
+
given type from the given configuration.
|
|
28
|
+
|
|
29
|
+
:param type: Python implementation type of the ClassifierElement to
|
|
30
|
+
produce
|
|
31
|
+
:param type_config: Configuration dictionary that will be passed
|
|
32
|
+
``from_config`` class method of given ``type``.
|
|
33
|
+
"""
|
|
34
|
+
self.type = type
|
|
35
|
+
self.type_config = type_config
|
|
36
|
+
|
|
37
|
+
#
|
|
38
|
+
# Class methods
|
|
39
|
+
#
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def get_default_config(cls) -> Dict[str, Any]:
|
|
43
|
+
"""
|
|
44
|
+
Generate and return a default configuration dictionary for this class.
|
|
45
|
+
This will be primarily used for generating what the configuration
|
|
46
|
+
dictionary would look like for this class without instantiating it.
|
|
47
|
+
|
|
48
|
+
It is not be guaranteed that the configuration dictionary returned
|
|
49
|
+
from this method is valid for construction of an instance of this class.
|
|
50
|
+
|
|
51
|
+
:return: Default configuration dictionary for the class.
|
|
52
|
+
:rtype: dict
|
|
53
|
+
|
|
54
|
+
"""
|
|
55
|
+
return make_default_config(ClassificationElement.get_impls())
|
|
56
|
+
|
|
57
|
+
# We likely do not expect subclasses for this type base, thus it is OK to
|
|
58
|
+
# use direct type reference in Type and return annotations.
|
|
59
|
+
@classmethod
|
|
60
|
+
def from_config(
|
|
61
|
+
cls: Type["ClassificationElementFactory"],
|
|
62
|
+
config_dict: Dict,
|
|
63
|
+
merge_default: bool = True
|
|
64
|
+
) -> "ClassificationElementFactory":
|
|
65
|
+
"""
|
|
66
|
+
Instantiate a new instance of this class given the configuration
|
|
67
|
+
JSON-compliant dictionary encapsulating initialization arguments.
|
|
68
|
+
|
|
69
|
+
This method should not be called via super unless and instance of the
|
|
70
|
+
class is desired.
|
|
71
|
+
|
|
72
|
+
:param config_dict: JSON compliant dictionary encapsulating
|
|
73
|
+
a configuration.
|
|
74
|
+
:param merge_default: Merge the given configuration on top of the
|
|
75
|
+
default provided by ``get_default_config``.
|
|
76
|
+
|
|
77
|
+
:return: Constructed instance from the provided config.
|
|
78
|
+
"""
|
|
79
|
+
if merge_default:
|
|
80
|
+
config_dict = merge_dict(cls.get_default_config(), config_dict)
|
|
81
|
+
|
|
82
|
+
ce_type, ce_conf = cls_conf_from_config_dict(
|
|
83
|
+
config_dict, ClassificationElement.get_impls()
|
|
84
|
+
)
|
|
85
|
+
return ClassificationElementFactory(ce_type, ce_conf)
|
|
86
|
+
|
|
87
|
+
def get_config(self) -> Dict[str, Any]:
|
|
88
|
+
return cls_conf_to_config_dict(self.type, self.type_config)
|
|
89
|
+
|
|
90
|
+
# noinspection PyShadowingBuiltins
|
|
91
|
+
def new_classification(self, type: str, uuid: Hashable) -> ClassificationElement:
|
|
92
|
+
"""
|
|
93
|
+
Create a new ClassificationElement instance of the configured
|
|
94
|
+
implementation.
|
|
95
|
+
|
|
96
|
+
:param type: Type of classifier. This is usually the name of the
|
|
97
|
+
classifier that generated this result.
|
|
98
|
+
:param uuid: UUID to associate with the classification.
|
|
99
|
+
|
|
100
|
+
:return: New ClassificationElement instance.
|
|
101
|
+
"""
|
|
102
|
+
return self.type.from_config(self.type_config, type, uuid)
|
|
103
|
+
|
|
104
|
+
# noinspection PyShadowingBuiltins
|
|
105
|
+
def __call__(self, type: str, uuid: Hashable) -> ClassificationElement:
|
|
106
|
+
"""
|
|
107
|
+
Create a new ClassificationElement instance of the configured
|
|
108
|
+
implementation.
|
|
109
|
+
|
|
110
|
+
:param type: Type of classifier. This is usually the name of the
|
|
111
|
+
classifier that generated this result.
|
|
112
|
+
:param uuid: UUID to associate with the classification.
|
|
113
|
+
|
|
114
|
+
:return: New ClassificationElement instance.
|
|
115
|
+
"""
|
|
116
|
+
return self.new_classification(type, uuid)
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
from typing import Any, Dict, Hashable, Iterable, List, Optional, Mapping, Sequence, Set, Type
|
|
3
|
+
from types import TracebackType
|
|
4
|
+
from warnings import warn
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from smqtk_core.configuration import (
|
|
8
|
+
Configurable,
|
|
9
|
+
make_default_config,
|
|
10
|
+
from_config_dict,
|
|
11
|
+
to_config_dict
|
|
12
|
+
)
|
|
13
|
+
from smqtk_core.dict import merge_dict
|
|
14
|
+
from smqtk_descriptors import DescriptorElement
|
|
15
|
+
|
|
16
|
+
from smqtk_classifier.classification_element_factory import ClassificationElementFactory
|
|
17
|
+
from smqtk_classifier.exceptions import MissingLabelError
|
|
18
|
+
from smqtk_classifier.interfaces.classification_element import ClassificationElement
|
|
19
|
+
|
|
20
|
+
from ._defaults import DFLT_CLASSIFIER_FACTORY
|
|
21
|
+
from .interfaces.classify_descriptor import ClassifyDescriptor
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ClassifyDescriptorCollection (Configurable):
|
|
25
|
+
"""
|
|
26
|
+
A collection of descriptively-labeled classifier instances for the purpose
|
|
27
|
+
of applying all stored classifiers to one or more input descriptor
|
|
28
|
+
elements.
|
|
29
|
+
|
|
30
|
+
TODO: [optionally?] map a classification element factory per classifier.
|
|
31
|
+
|
|
32
|
+
:param classifiers: Optional dictionary of semantic label keys and
|
|
33
|
+
Classifier instance values.
|
|
34
|
+
:param labeled_classifiers: Key-word arguments may be provided where
|
|
35
|
+
the key used is considered the semantic label of the provided
|
|
36
|
+
Classifier instance.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
EXAMPLE_KEY = '__example_label__'
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
classifiers: Mapping[str, ClassifyDescriptor] | None = None,
|
|
44
|
+
**labeled_classifiers: ClassifyDescriptor
|
|
45
|
+
):
|
|
46
|
+
self._label_to_classifier_lock = threading.RLock()
|
|
47
|
+
self._label_to_classifier = {}
|
|
48
|
+
|
|
49
|
+
# Go though classifiers map and key-word arguments, check that values
|
|
50
|
+
# are actually classifiers.
|
|
51
|
+
if classifiers is not None:
|
|
52
|
+
for label, classifier in classifiers.items():
|
|
53
|
+
if not isinstance(classifier, ClassifyDescriptor):
|
|
54
|
+
raise ValueError("Found a non-Classifier instance value "
|
|
55
|
+
"for key '%s'" % label)
|
|
56
|
+
self._label_to_classifier[label] = classifier
|
|
57
|
+
|
|
58
|
+
for label, classifier in labeled_classifiers.items():
|
|
59
|
+
if not isinstance(classifier, ClassifyDescriptor):
|
|
60
|
+
raise ValueError("Found a non-Classifier instance value "
|
|
61
|
+
"for key '%s'" % label)
|
|
62
|
+
elif label in self._label_to_classifier:
|
|
63
|
+
raise ValueError("Duplicate classifier label '%s' provided "
|
|
64
|
+
"in key-word arguments." % label)
|
|
65
|
+
self._label_to_classifier[label] = classifier
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def get_default_config(cls) -> Dict[str, Any]:
|
|
69
|
+
c = super(ClassifyDescriptorCollection, cls).get_default_config()
|
|
70
|
+
|
|
71
|
+
# We list the label-classifier mapping on one level, so remove the
|
|
72
|
+
# nested map parameter that can optionally be used in the constructor.
|
|
73
|
+
del c['classifiers']
|
|
74
|
+
|
|
75
|
+
# Add slot of a list of classifier plugin specifications
|
|
76
|
+
c[cls.EXAMPLE_KEY] = make_default_config(ClassifyDescriptor.get_impls())
|
|
77
|
+
|
|
78
|
+
return c
|
|
79
|
+
|
|
80
|
+
# We likely do not expect subclasses for this type base, thus it is OK to
|
|
81
|
+
# use direct type reference in Type and return annotations.
|
|
82
|
+
@classmethod
|
|
83
|
+
def from_config(
|
|
84
|
+
cls,
|
|
85
|
+
config_dict: Dict,
|
|
86
|
+
merge_default: bool = True
|
|
87
|
+
) -> "ClassifyDescriptorCollection":
|
|
88
|
+
if merge_default:
|
|
89
|
+
config_dict = merge_dict(cls.get_default_config(), config_dict)
|
|
90
|
+
|
|
91
|
+
classifier_map = {}
|
|
92
|
+
|
|
93
|
+
# Copying list of keys so we can update the dictionary as we loop.
|
|
94
|
+
for label in list(config_dict.keys()):
|
|
95
|
+
# Skip the example section.
|
|
96
|
+
if label == cls.EXAMPLE_KEY:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
classifier_config = config_dict[label]
|
|
100
|
+
classifier = from_config_dict(classifier_config,
|
|
101
|
+
ClassifyDescriptor.get_impls())
|
|
102
|
+
classifier_map[label] = classifier
|
|
103
|
+
|
|
104
|
+
return cls(classifiers=classifier_map)
|
|
105
|
+
|
|
106
|
+
def get_config(self) -> Dict[str, Any]:
|
|
107
|
+
with self._label_to_classifier_lock:
|
|
108
|
+
c = dict((label, to_config_dict(classifier))
|
|
109
|
+
for label, classifier
|
|
110
|
+
in self._label_to_classifier.items())
|
|
111
|
+
return c
|
|
112
|
+
|
|
113
|
+
def __enter__(self) -> "ClassifyDescriptorCollection":
|
|
114
|
+
"""
|
|
115
|
+
:rtype: IqrSession
|
|
116
|
+
"""
|
|
117
|
+
self._label_to_classifier_lock.acquire()
|
|
118
|
+
return self
|
|
119
|
+
|
|
120
|
+
def __exit__(
|
|
121
|
+
self,
|
|
122
|
+
exc_type: Optional[Type],
|
|
123
|
+
exc_val: Optional[BaseException],
|
|
124
|
+
exc_tb: Optional[TracebackType]
|
|
125
|
+
) -> None:
|
|
126
|
+
self._label_to_classifier_lock.release()
|
|
127
|
+
|
|
128
|
+
def size(self) -> int:
|
|
129
|
+
with self._label_to_classifier_lock:
|
|
130
|
+
return len(self._label_to_classifier)
|
|
131
|
+
|
|
132
|
+
__len__ = size
|
|
133
|
+
|
|
134
|
+
def labels(self) -> Set[str]:
|
|
135
|
+
"""
|
|
136
|
+
:return: Set of labels for currently collected classifiers.
|
|
137
|
+
"""
|
|
138
|
+
with self._label_to_classifier_lock:
|
|
139
|
+
return set(self._label_to_classifier.keys())
|
|
140
|
+
|
|
141
|
+
def add_classifier(self, label: str, classifier: ClassifyDescriptor) -> "ClassifyDescriptorCollection":
|
|
142
|
+
"""
|
|
143
|
+
Add a classifier instance with associated descriptive label to this
|
|
144
|
+
collection.
|
|
145
|
+
|
|
146
|
+
:param label: String descriptive label for the classifier.
|
|
147
|
+
:param classifier: Classifier instance to collect.
|
|
148
|
+
|
|
149
|
+
:raises ValueError: Classifier provided is not actually a classifier
|
|
150
|
+
instance, or if the label provided already exists in this
|
|
151
|
+
collection.
|
|
152
|
+
|
|
153
|
+
:return: Self.
|
|
154
|
+
"""
|
|
155
|
+
if not isinstance(classifier, ClassifyDescriptor):
|
|
156
|
+
raise ValueError("Not given a Classifier instance (given type"
|
|
157
|
+
" %s)." % type(classifier))
|
|
158
|
+
with self._label_to_classifier_lock:
|
|
159
|
+
if label in self._label_to_classifier:
|
|
160
|
+
raise ValueError("Duplicate label provided: '%s'" % label)
|
|
161
|
+
self._label_to_classifier[label] = classifier
|
|
162
|
+
return self
|
|
163
|
+
|
|
164
|
+
def get_classifier(self, label: str) -> ClassifyDescriptor:
|
|
165
|
+
"""
|
|
166
|
+
Get the classifier instance for a given label.
|
|
167
|
+
|
|
168
|
+
:param label: Label of the classifier to get.
|
|
169
|
+
|
|
170
|
+
:raises KeyError: No classifier for the given label.
|
|
171
|
+
|
|
172
|
+
:return: Classifier instance.
|
|
173
|
+
"""
|
|
174
|
+
with self._label_to_classifier_lock:
|
|
175
|
+
return self._label_to_classifier[label]
|
|
176
|
+
|
|
177
|
+
def remove_classifier(self, label: str) -> "ClassifyDescriptorCollection":
|
|
178
|
+
"""
|
|
179
|
+
Remove a label-classifier pair from this collection.
|
|
180
|
+
|
|
181
|
+
:param label: Label of the classifier to remove.
|
|
182
|
+
|
|
183
|
+
:raises KeyError: The given label does not reference a classifier in
|
|
184
|
+
this collection.
|
|
185
|
+
|
|
186
|
+
:return: Self.
|
|
187
|
+
"""
|
|
188
|
+
with self._label_to_classifier_lock:
|
|
189
|
+
del self._label_to_classifier[label]
|
|
190
|
+
return self
|
|
191
|
+
|
|
192
|
+
def labels_to_classifiers(self, labels: Optional[Iterable[str]] = None) -> Dict[str, ClassifyDescriptor]:
|
|
193
|
+
"""
|
|
194
|
+
Get a shallow copy mapping of classifiers for the labels given, or for
|
|
195
|
+
all classifiers if no labels were explicitly given.
|
|
196
|
+
|
|
197
|
+
This method is thread-safe and the returned dictionary is separate from
|
|
198
|
+
this class's control. However, the classifier instances are still
|
|
199
|
+
shared.
|
|
200
|
+
|
|
201
|
+
:param None | typing.Iterable[str] labels:
|
|
202
|
+
One or more labels of stored classifiers to retrieve.
|
|
203
|
+
If None, we will consider all stored classifiers.
|
|
204
|
+
|
|
205
|
+
:raises MissingLabelError: Thrown when one or more labels provided do
|
|
206
|
+
not associate to any currently stored classifiers.
|
|
207
|
+
|
|
208
|
+
:return: Dictionary mapping string labels to Classifier instances.
|
|
209
|
+
"""
|
|
210
|
+
with self._label_to_classifier_lock:
|
|
211
|
+
if labels is not None:
|
|
212
|
+
labels = list(labels)
|
|
213
|
+
# If we're missing some of the requested labels, complain
|
|
214
|
+
missing_labels = set(labels) - self.labels()
|
|
215
|
+
if missing_labels:
|
|
216
|
+
raise MissingLabelError(missing_labels)
|
|
217
|
+
label2classifier = {label: self._label_to_classifier[label]
|
|
218
|
+
for label in labels}
|
|
219
|
+
else:
|
|
220
|
+
label2classifier = dict(self._label_to_classifier)
|
|
221
|
+
return label2classifier
|
|
222
|
+
|
|
223
|
+
def classify(
|
|
224
|
+
self,
|
|
225
|
+
descriptor: DescriptorElement,
|
|
226
|
+
labels: Optional[Iterable[str]] = None,
|
|
227
|
+
factory: ClassificationElementFactory = DFLT_CLASSIFIER_FACTORY,
|
|
228
|
+
overwrite: bool = False
|
|
229
|
+
) -> Dict[str, ClassificationElement]:
|
|
230
|
+
"""
|
|
231
|
+
Apply all stored classifiers to the given descriptor element.
|
|
232
|
+
|
|
233
|
+
We return a dictionary mapping the label of a stored classifier to the
|
|
234
|
+
classifier element result produced by that classifier via the
|
|
235
|
+
provided classification element factory.
|
|
236
|
+
|
|
237
|
+
:param descriptor: Descriptor element to classify.
|
|
238
|
+
:param labels: One or more labels of stored classifiers to use for
|
|
239
|
+
classifying the given descriptor. If None, use all stored
|
|
240
|
+
classifiers.
|
|
241
|
+
:param factory: Classification element factory.
|
|
242
|
+
:param overwrite: Force re-computation of the classification of the
|
|
243
|
+
input descriptor.
|
|
244
|
+
|
|
245
|
+
:raises smqtk.exceptions.MissingLabelError: Some or all of the
|
|
246
|
+
requested labels are missing.
|
|
247
|
+
|
|
248
|
+
:return: Result dictionary of classifier labels to classification
|
|
249
|
+
elements.
|
|
250
|
+
"""
|
|
251
|
+
d_classifications = {}
|
|
252
|
+
label2classifier = self.labels_to_classifiers(labels)
|
|
253
|
+
for label, classifier in label2classifier.items():
|
|
254
|
+
d_classifications[label] = classifier.classify_one_element(
|
|
255
|
+
descriptor, factory=factory, overwrite=overwrite
|
|
256
|
+
)
|
|
257
|
+
return d_classifications
|
|
258
|
+
|
|
259
|
+
def classify_arrays(
|
|
260
|
+
self,
|
|
261
|
+
array_seq: Sequence[np.ndarray],
|
|
262
|
+
labels: Sequence[str] | None = None
|
|
263
|
+
) -> Dict[str, List[Dict[Hashable, float]]]:
|
|
264
|
+
"""
|
|
265
|
+
Apply all stored classifiers to the given iterable or matrix of numpy
|
|
266
|
+
arrays.
|
|
267
|
+
|
|
268
|
+
We return a dictionary mapping the label of a stored classifier to the
|
|
269
|
+
class-confidence map result produced by that classifier's
|
|
270
|
+
`classify_arrays` method.
|
|
271
|
+
|
|
272
|
+
:param Sequence[numpy.ndarray] array_seq:
|
|
273
|
+
Sequence of descriptor vectors, as numpy arrays, to be classified.
|
|
274
|
+
:param Sequence[str] labels:
|
|
275
|
+
One or more labels of stored classifiers to use for classifying the
|
|
276
|
+
given descriptors. If None (the default), use all stored
|
|
277
|
+
classifiers.
|
|
278
|
+
|
|
279
|
+
:raises MissingLabelError: Thrown when one or more labels provided do
|
|
280
|
+
not associate to any currently stored classifiers.
|
|
281
|
+
|
|
282
|
+
:return: Dictionary of result predictions for each classifier.
|
|
283
|
+
:rtype: dict[str, list[dict[collections.abc.Hashable, float]]]
|
|
284
|
+
"""
|
|
285
|
+
label2classifier = self.labels_to_classifiers(labels)
|
|
286
|
+
label2pred = {}
|
|
287
|
+
for label, classifier in label2classifier.items():
|
|
288
|
+
label2pred[label] = list(classifier.classify_arrays(array_seq))
|
|
289
|
+
return label2pred
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class ClassifierCollection(ClassifyDescriptorCollection):
|
|
293
|
+
|
|
294
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
295
|
+
warn("ClassifierCollection was renamed to "
|
|
296
|
+
"ClassifyDescriptorCollection", category=DeprecationWarning,
|
|
297
|
+
stacklevel=2)
|
|
298
|
+
super().__init__(*args, **kwargs)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from typing import Set
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class NoClassificationError (Exception):
|
|
5
|
+
"""
|
|
6
|
+
When a ClassificationElement has no mapping yet set, but an operation
|
|
7
|
+
required it.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MissingLabelError(Exception):
|
|
12
|
+
"""
|
|
13
|
+
Raised by ClassifyDescriptorCollection.classify when requested classifier
|
|
14
|
+
labels are missing from collection.
|
|
15
|
+
"""
|
|
16
|
+
def __init__(self, labels: Set[str]):
|
|
17
|
+
"""
|
|
18
|
+
:param labels: The labels missing from the collection
|
|
19
|
+
"""
|
|
20
|
+
super(MissingLabelError, self).__init__(labels)
|
|
21
|
+
self.labels = labels
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ExistingModelError(Exception):
|
|
25
|
+
"""
|
|
26
|
+
Raised by ClassifyDescriptorSupervised and ClassifyImageSupervised when
|
|
27
|
+
a model already exists in an instance to prevent overwriting and existing
|
|
28
|
+
model
|
|
29
|
+
"""
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import pickle
|
|
2
|
+
import os.path as osp
|
|
3
|
+
from typing import Any, Dict, Hashable, Optional
|
|
4
|
+
|
|
5
|
+
from smqtk_dataprovider.utils.file import safe_create_dir
|
|
6
|
+
from smqtk_dataprovider.utils.string import partition_string
|
|
7
|
+
|
|
8
|
+
from smqtk_classifier.exceptions import NoClassificationError
|
|
9
|
+
from smqtk_classifier.interfaces.classification_element import (
|
|
10
|
+
ClassificationElement,
|
|
11
|
+
CLASSIFICATION_DICT_T,
|
|
12
|
+
CLASSIFICATION_MAP_T
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FileClassificationElement (ClassificationElement): # lgtm [py/missing-equals]
|
|
17
|
+
"""
|
|
18
|
+
Create a classification element from a cached file representation.
|
|
19
|
+
|
|
20
|
+
:param type_name: Type of classification. This is usually the name of
|
|
21
|
+
the classifier that generated this result.
|
|
22
|
+
:type type_name: str
|
|
23
|
+
|
|
24
|
+
:param uuid: uuid for this classification
|
|
25
|
+
:type uuid: collections.abc.Hashable
|
|
26
|
+
|
|
27
|
+
:param save_dir: Directory to save this element's contents. If this path
|
|
28
|
+
is relative, we interpret as relative to the current working
|
|
29
|
+
directory.
|
|
30
|
+
:type save_dir: str | unicode
|
|
31
|
+
|
|
32
|
+
:param subdir_split: If a positive integer, this will cause us to store
|
|
33
|
+
the vector file in a subdirectory under the ``save_dir`` that was
|
|
34
|
+
specified. The integer value specifies the number of splits that we
|
|
35
|
+
will make in the stringification of this descriptor's UUID. If
|
|
36
|
+
there happen to be dashes in this stringification, we will
|
|
37
|
+
remove them (as would happen if given an uuid.UUID instance as
|
|
38
|
+
the uuid element).
|
|
39
|
+
:type subdir_split: None | int
|
|
40
|
+
|
|
41
|
+
:param pickle_protocol: Pickling protocol to use. We will use -1 by
|
|
42
|
+
default (latest version, probably binary).
|
|
43
|
+
:type pickle_protocol: int
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
__slots__ = ('save_dir', 'pickle_protocol', 'subdir_split', 'filepath')
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
type_name: str,
|
|
51
|
+
uuid: Hashable,
|
|
52
|
+
save_dir: str,
|
|
53
|
+
subdir_split: Optional[int] = None,
|
|
54
|
+
pickle_protocol: int = -1
|
|
55
|
+
):
|
|
56
|
+
super(FileClassificationElement, self).__init__(type_name, uuid)
|
|
57
|
+
|
|
58
|
+
# TODO: Remove absolute path conversion (allow relative)
|
|
59
|
+
self.save_dir = osp.abspath(osp.expanduser(save_dir))
|
|
60
|
+
self.pickle_protocol = pickle_protocol
|
|
61
|
+
|
|
62
|
+
# Saving components
|
|
63
|
+
self.subdir_split = subdir_split
|
|
64
|
+
if subdir_split and int(subdir_split) > 0:
|
|
65
|
+
self.subdir_split = subdir_split = int(subdir_split)
|
|
66
|
+
# Using all but the last split segment. This is so we don't create
|
|
67
|
+
# a whole bunch of directories with a single element in them.
|
|
68
|
+
save_dir = osp.join(self.save_dir,
|
|
69
|
+
*partition_string(str(uuid).replace('-', ''),
|
|
70
|
+
subdir_split)[:subdir_split-1]
|
|
71
|
+
)
|
|
72
|
+
else:
|
|
73
|
+
save_dir = self.save_dir
|
|
74
|
+
|
|
75
|
+
self.filepath = osp.join(save_dir,
|
|
76
|
+
"%s.%s.classification.pickle"
|
|
77
|
+
% (self.type_name, str(self.uuid)))
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def is_usable(cls) -> bool:
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
def get_config(self) -> Dict[str, Any]:
|
|
84
|
+
return {
|
|
85
|
+
"save_dir": self.save_dir,
|
|
86
|
+
'subdir_split': self.subdir_split,
|
|
87
|
+
"pickle_protocol": self.pickle_protocol,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
def __getstate__(self) -> Any:
|
|
91
|
+
return (
|
|
92
|
+
super(FileClassificationElement, self).__getstate__(),
|
|
93
|
+
self.save_dir,
|
|
94
|
+
self.pickle_protocol,
|
|
95
|
+
self.subdir_split,
|
|
96
|
+
self.filepath,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def __setstate__(self, state: Any) -> None:
|
|
100
|
+
super(FileClassificationElement, self).__setstate__(state[0])
|
|
101
|
+
self.save_dir, \
|
|
102
|
+
self.pickle_protocol, \
|
|
103
|
+
self.subdir_split, \
|
|
104
|
+
self.filepath \
|
|
105
|
+
= state[1:]
|
|
106
|
+
|
|
107
|
+
def has_classifications(self) -> bool:
|
|
108
|
+
return osp.isfile(self.filepath)
|
|
109
|
+
|
|
110
|
+
def get_classification(self) -> CLASSIFICATION_DICT_T:
|
|
111
|
+
if not self.has_classifications():
|
|
112
|
+
raise NoClassificationError("No classification values.")
|
|
113
|
+
with open(self.filepath, 'rb') as f:
|
|
114
|
+
return pickle.load(f)
|
|
115
|
+
|
|
116
|
+
def set_classification(
|
|
117
|
+
self,
|
|
118
|
+
m: Optional[CLASSIFICATION_MAP_T] = None,
|
|
119
|
+
**kwds: float
|
|
120
|
+
) -> CLASSIFICATION_DICT_T:
|
|
121
|
+
m = super(FileClassificationElement, self).set_classification(m, **kwds)
|
|
122
|
+
safe_create_dir(osp.dirname(self.filepath))
|
|
123
|
+
with open(self.filepath, 'wb') as f:
|
|
124
|
+
pickle.dump(m, f, self.pickle_protocol)
|
|
125
|
+
return m
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from threading import RLock
|
|
2
|
+
from typing import Any, Dict, Hashable, Optional
|
|
3
|
+
|
|
4
|
+
from smqtk_classifier.exceptions import NoClassificationError
|
|
5
|
+
from smqtk_classifier.interfaces.classification_element import (
|
|
6
|
+
ClassificationElement, CLASSIFICATION_MAP_T, CLASSIFICATION_DICT_T
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MemoryClassificationElement (ClassificationElement): # lgtm [py/missing-equals]
|
|
11
|
+
"""
|
|
12
|
+
In-memory representation of classification results. This is represented
|
|
13
|
+
with a python dictionary.
|
|
14
|
+
|
|
15
|
+
:param type_name: Name of the type of classifier this classification
|
|
16
|
+
was generated by.
|
|
17
|
+
:type type_name: str
|
|
18
|
+
|
|
19
|
+
:param uuid: Unique ID reference of the classification
|
|
20
|
+
:type uuid: collections.abc.Hashable
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
__slots__ = ('_c', '_c_lock')
|
|
24
|
+
|
|
25
|
+
def __init__(self, type_name: str, uuid: Hashable):
|
|
26
|
+
super(MemoryClassificationElement, self).__init__(type_name, uuid)
|
|
27
|
+
|
|
28
|
+
# dictionary of classification labels and values
|
|
29
|
+
self._c: Optional[CLASSIFICATION_DICT_T] = None
|
|
30
|
+
# Cannot be pickled. New lock initialized upon pickle/unpickle
|
|
31
|
+
self._c_lock = RLock()
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def is_usable(cls) -> bool:
|
|
35
|
+
# No external dependencies
|
|
36
|
+
return True
|
|
37
|
+
|
|
38
|
+
def get_config(self) -> Dict[str, Any]:
|
|
39
|
+
return {}
|
|
40
|
+
|
|
41
|
+
def __getstate__(self) -> Any:
|
|
42
|
+
state = {
|
|
43
|
+
'parent': super(MemoryClassificationElement, self).__getstate__(),
|
|
44
|
+
}
|
|
45
|
+
with self._c_lock:
|
|
46
|
+
state['c'] = self._c
|
|
47
|
+
return state
|
|
48
|
+
|
|
49
|
+
def __setstate__(self, state: Any) -> None:
|
|
50
|
+
super(MemoryClassificationElement, self).__setstate__(state['parent'])
|
|
51
|
+
if not hasattr(self, '_c_lock') or self._c_lock is None:
|
|
52
|
+
self._c_lock = RLock()
|
|
53
|
+
with self._c_lock:
|
|
54
|
+
#: :type: None | dict[collections.abc.Hashable, float]
|
|
55
|
+
self._c = state['c']
|
|
56
|
+
|
|
57
|
+
def has_classifications(self) -> bool:
|
|
58
|
+
with self._c_lock:
|
|
59
|
+
return bool(self._c)
|
|
60
|
+
|
|
61
|
+
def get_classification(self) -> CLASSIFICATION_DICT_T:
|
|
62
|
+
with self._c_lock:
|
|
63
|
+
if self._c:
|
|
64
|
+
return self._c
|
|
65
|
+
else:
|
|
66
|
+
raise NoClassificationError("No classification labels/values")
|
|
67
|
+
|
|
68
|
+
def set_classification(
|
|
69
|
+
self,
|
|
70
|
+
m: Optional[CLASSIFICATION_MAP_T] = None,
|
|
71
|
+
**kwds: float
|
|
72
|
+
) -> CLASSIFICATION_DICT_T:
|
|
73
|
+
m = super(MemoryClassificationElement, self)\
|
|
74
|
+
.set_classification(m, **kwds)
|
|
75
|
+
with self._c_lock:
|
|
76
|
+
self._c = m
|
|
77
|
+
return m
|