dataeval 0.72.0__py3-none-any.whl → 0.72.2__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.
- dataeval/__init__.py +4 -4
- dataeval/detectors/__init__.py +4 -3
- dataeval/detectors/drift/__init__.py +10 -11
- dataeval/{_internal/detectors → detectors}/drift/base.py +51 -102
- dataeval/{_internal/detectors → detectors}/drift/cvm.py +9 -8
- dataeval/{_internal/detectors → detectors}/drift/ks.py +11 -10
- dataeval/{_internal/detectors → detectors}/drift/mmd.py +33 -34
- dataeval/{_internal/detectors → detectors}/drift/torch.py +15 -13
- dataeval/{_internal/detectors → detectors}/drift/uncertainty.py +12 -9
- dataeval/detectors/drift/updates.py +61 -0
- dataeval/detectors/linters/__init__.py +3 -3
- dataeval/{_internal/detectors → detectors/linters}/clusterer.py +47 -45
- dataeval/{_internal/detectors → detectors/linters}/duplicates.py +20 -10
- dataeval/{_internal/detectors → detectors/linters}/merged_stats.py +3 -1
- dataeval/{_internal/detectors → detectors/linters}/outliers.py +19 -26
- dataeval/detectors/ood/__init__.py +8 -16
- dataeval/{_internal/detectors → detectors}/ood/ae.py +9 -9
- dataeval/{_internal/detectors → detectors}/ood/aegmm.py +10 -30
- dataeval/{_internal/detectors → detectors}/ood/base.py +27 -21
- dataeval/{_internal/detectors → detectors}/ood/llr.py +27 -23
- dataeval/detectors/ood/metadata_ks_compare.py +99 -0
- dataeval/detectors/ood/metadata_least_likely.py +119 -0
- dataeval/detectors/ood/metadata_ood_mi.py +92 -0
- dataeval/{_internal/detectors → detectors}/ood/vae.py +11 -13
- dataeval/{_internal/detectors → detectors}/ood/vaegmm.py +10 -32
- dataeval/{_internal/interop.py → interop.py} +12 -7
- dataeval/metrics/__init__.py +1 -1
- dataeval/metrics/bias/__init__.py +4 -4
- dataeval/{_internal/metrics → metrics/bias}/balance.py +70 -4
- dataeval/{_internal/metrics → metrics/bias}/coverage.py +10 -8
- dataeval/{_internal/metrics → metrics/bias}/diversity.py +54 -20
- dataeval/metrics/bias/metadata.py +275 -0
- dataeval/{_internal/metrics → metrics/bias}/parity.py +21 -17
- dataeval/metrics/estimators/__init__.py +3 -3
- dataeval/{_internal/metrics → metrics/estimators}/ber.py +31 -28
- dataeval/{_internal/metrics → metrics/estimators}/divergence.py +15 -16
- dataeval/{_internal/metrics → metrics/estimators}/uap.py +8 -6
- dataeval/metrics/stats/__init__.py +7 -7
- dataeval/{_internal/metrics → metrics}/stats/base.py +66 -40
- dataeval/{_internal/metrics → metrics}/stats/boxratiostats.py +19 -15
- dataeval/{_internal/metrics → metrics}/stats/datasetstats.py +19 -17
- dataeval/{_internal/metrics → metrics}/stats/dimensionstats.py +12 -10
- dataeval/metrics/stats/hashstats.py +156 -0
- dataeval/{_internal/metrics → metrics}/stats/labelstats.py +8 -6
- dataeval/{_internal/metrics → metrics}/stats/pixelstats.py +12 -11
- dataeval/{_internal/metrics → metrics}/stats/visualstats.py +14 -13
- dataeval/{_internal/output.py → output.py} +26 -6
- dataeval/utils/__init__.py +8 -4
- dataeval/utils/image.py +71 -0
- dataeval/utils/shared.py +151 -0
- dataeval/utils/split_dataset.py +486 -0
- dataeval/utils/tensorflow/__init__.py +9 -7
- dataeval/{_internal/models/tensorflow → utils/tensorflow/_internal}/autoencoder.py +64 -68
- dataeval/{_internal/models/tensorflow/losses.py → utils/tensorflow/_internal/loss.py} +10 -9
- dataeval/{_internal/models/tensorflow → utils/tensorflow/_internal}/pixelcnn.py +18 -22
- dataeval/{_internal/models/tensorflow → utils/tensorflow/_internal}/trainer.py +3 -1
- dataeval/{_internal/models/tensorflow → utils/tensorflow/_internal}/utils.py +18 -18
- dataeval/utils/tensorflow/loss/__init__.py +6 -2
- dataeval/utils/torch/__init__.py +7 -3
- dataeval/{_internal/models/pytorch → utils/torch}/blocks.py +19 -14
- dataeval/{_internal → utils/torch}/datasets.py +49 -43
- dataeval/utils/torch/models.py +138 -0
- dataeval/{_internal/models/pytorch/autoencoder.py → utils/torch/trainer.py} +12 -141
- dataeval/{_internal → utils/torch}/utils.py +3 -1
- dataeval/workflows/__init__.py +1 -1
- dataeval/{_internal/workflows → workflows}/sufficiency.py +42 -37
- {dataeval-0.72.0.dist-info → dataeval-0.72.2.dist-info}/METADATA +7 -5
- dataeval-0.72.2.dist-info/RECORD +72 -0
- dataeval/_internal/detectors/__init__.py +0 -0
- dataeval/_internal/detectors/drift/__init__.py +0 -0
- dataeval/_internal/detectors/ood/__init__.py +0 -0
- dataeval/_internal/metrics/__init__.py +0 -0
- dataeval/_internal/metrics/stats/hashstats.py +0 -75
- dataeval/_internal/metrics/utils.py +0 -447
- dataeval/_internal/models/__init__.py +0 -0
- dataeval/_internal/models/pytorch/__init__.py +0 -0
- dataeval/_internal/models/pytorch/utils.py +0 -67
- dataeval/_internal/models/tensorflow/__init__.py +0 -0
- dataeval/_internal/workflows/__init__.py +0 -0
- dataeval/detectors/drift/kernels/__init__.py +0 -10
- dataeval/detectors/drift/updates/__init__.py +0 -7
- dataeval/utils/tensorflow/models/__init__.py +0 -9
- dataeval/utils/tensorflow/recon/__init__.py +0 -3
- dataeval/utils/torch/datasets/__init__.py +0 -12
- dataeval/utils/torch/models/__init__.py +0 -11
- dataeval/utils/torch/trainer/__init__.py +0 -7
- dataeval-0.72.0.dist-info/RECORD +0 -80
- /dataeval/{_internal/models/tensorflow → utils/tensorflow/_internal}/gmm.py +0 -0
- {dataeval-0.72.0.dist-info → dataeval-0.72.2.dist-info}/LICENSE.txt +0 -0
- {dataeval-0.72.0.dist-info → dataeval-0.72.2.dist-info}/WHEEL +0 -0
@@ -1,18 +1,20 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
__all__ = ["OutliersOutput", "Outliers"]
|
4
|
+
|
3
5
|
from dataclasses import dataclass
|
4
6
|
from typing import Generic, Iterable, Literal, Sequence, TypeVar, Union, overload
|
5
7
|
|
6
8
|
import numpy as np
|
7
9
|
from numpy.typing import ArrayLike, NDArray
|
8
10
|
|
9
|
-
from dataeval.
|
10
|
-
from dataeval.
|
11
|
-
from dataeval.
|
12
|
-
from dataeval.
|
13
|
-
from dataeval.
|
14
|
-
from dataeval.
|
15
|
-
from dataeval.
|
11
|
+
from dataeval.detectors.linters.merged_stats import combine_stats, get_dataset_step_from_idx
|
12
|
+
from dataeval.metrics.stats.base import BOX_COUNT, SOURCE_INDEX
|
13
|
+
from dataeval.metrics.stats.datasetstats import DatasetStatsOutput, datasetstats
|
14
|
+
from dataeval.metrics.stats.dimensionstats import DimensionStatsOutput
|
15
|
+
from dataeval.metrics.stats.pixelstats import PixelStatsOutput
|
16
|
+
from dataeval.metrics.stats.visualstats import VisualStatsOutput
|
17
|
+
from dataeval.output import OutputMetadata, set_metadata
|
16
18
|
|
17
19
|
IndexIssueMap = dict[int, dict[str, float]]
|
18
20
|
OutlierStatsOutput = Union[DimensionStatsOutput, PixelStatsOutput, VisualStatsOutput]
|
@@ -27,7 +29,7 @@ class OutliersOutput(Generic[TIndexIssueMap], OutputMetadata):
|
|
27
29
|
Attributes
|
28
30
|
----------
|
29
31
|
issues : dict[int, dict[str, float]] | list[dict[int, dict[str, float]]]
|
30
|
-
Indices of image
|
32
|
+
Indices of image Outliers with their associated issue type and calculated values.
|
31
33
|
|
32
34
|
- For a single dataset, a dictionary containing the indices of outliers and
|
33
35
|
a dictionary showing the issues and calculated values for the given index.
|
@@ -37,7 +39,7 @@ class OutliersOutput(Generic[TIndexIssueMap], OutputMetadata):
|
|
37
39
|
|
38
40
|
issues: TIndexIssueMap
|
39
41
|
|
40
|
-
def __len__(self):
|
42
|
+
def __len__(self) -> int:
|
41
43
|
if isinstance(self.issues, dict):
|
42
44
|
return len(self.issues)
|
43
45
|
else:
|
@@ -69,7 +71,7 @@ def _get_outlier_mask(
|
|
69
71
|
|
70
72
|
class Outliers:
|
71
73
|
r"""
|
72
|
-
Calculates statistical
|
74
|
+
Calculates statistical Outliers of a dataset using various statistical tests applied to each image
|
73
75
|
|
74
76
|
Parameters
|
75
77
|
----------
|
@@ -86,7 +88,7 @@ class Outliers:
|
|
86
88
|
|
87
89
|
See Also
|
88
90
|
--------
|
89
|
-
Duplicates
|
91
|
+
:term:`Duplicates`
|
90
92
|
|
91
93
|
Note
|
92
94
|
----
|
@@ -157,12 +159,12 @@ class Outliers:
|
|
157
159
|
@overload
|
158
160
|
def from_stats(self, stats: Sequence[OutlierStatsOutput]) -> OutliersOutput[list[IndexIssueMap]]: ...
|
159
161
|
|
160
|
-
@set_metadata(
|
162
|
+
@set_metadata(["outlier_method", "outlier_threshold"])
|
161
163
|
def from_stats(
|
162
164
|
self, stats: OutlierStatsOutput | DatasetStatsOutput | Sequence[OutlierStatsOutput]
|
163
|
-
) -> OutliersOutput:
|
165
|
+
) -> OutliersOutput[IndexIssueMap] | OutliersOutput[list[IndexIssueMap]]:
|
164
166
|
"""
|
165
|
-
Returns indices of
|
167
|
+
Returns indices of Outliers with the issues identified for each
|
166
168
|
|
167
169
|
Parameters
|
168
170
|
----------
|
@@ -195,7 +197,7 @@ class Outliers:
|
|
195
197
|
{}
|
196
198
|
""" # noqa: E501
|
197
199
|
if isinstance(stats, DatasetStatsOutput):
|
198
|
-
outliers = self._get_outliers({k: v for o in stats.
|
200
|
+
outliers = self._get_outliers({k: v for o in stats._outputs() for k, v in o.dict().items()})
|
199
201
|
return OutliersOutput(outliers)
|
200
202
|
|
201
203
|
if isinstance(stats, (DimensionStatsOutput, PixelStatsOutput, VisualStatsOutput)):
|
@@ -226,19 +228,10 @@ class Outliers:
|
|
226
228
|
|
227
229
|
return OutliersOutput(output_list)
|
228
230
|
|
229
|
-
@set_metadata(
|
230
|
-
"dataeval.detectors",
|
231
|
-
[
|
232
|
-
"use_dimension",
|
233
|
-
"use_pixel",
|
234
|
-
"use_visual",
|
235
|
-
"outlier_method",
|
236
|
-
"outlier_threshold",
|
237
|
-
],
|
238
|
-
)
|
231
|
+
@set_metadata(["use_dimension", "use_pixel", "use_visual", "outlier_method", "outlier_threshold"])
|
239
232
|
def evaluate(self, data: Iterable[ArrayLike]) -> OutliersOutput[IndexIssueMap]:
|
240
233
|
"""
|
241
|
-
Returns indices of
|
234
|
+
Returns indices of Outliers with the issues identified for each
|
242
235
|
|
243
236
|
Parameters
|
244
237
|
----------
|
@@ -1,23 +1,15 @@
|
|
1
1
|
"""
|
2
|
-
Out-of-distribution detectors identify data that is different from the data used to train a particular model.
|
2
|
+
Out-of-distribution (OOD)` detectors identify data that is different from the data used to train a particular model.
|
3
3
|
"""
|
4
4
|
|
5
5
|
from dataeval import _IS_TENSORFLOW_AVAILABLE
|
6
6
|
|
7
7
|
if _IS_TENSORFLOW_AVAILABLE: # pragma: no cover
|
8
|
-
from dataeval.
|
9
|
-
from dataeval.
|
10
|
-
from dataeval.
|
11
|
-
from dataeval.
|
12
|
-
from dataeval.
|
13
|
-
from dataeval.
|
8
|
+
from dataeval.detectors.ood.ae import OOD_AE
|
9
|
+
from dataeval.detectors.ood.aegmm import OOD_AEGMM
|
10
|
+
from dataeval.detectors.ood.base import OODOutput, OODScoreOutput
|
11
|
+
from dataeval.detectors.ood.llr import OOD_LLR
|
12
|
+
from dataeval.detectors.ood.vae import OOD_VAE
|
13
|
+
from dataeval.detectors.ood.vaegmm import OOD_VAEGMM
|
14
14
|
|
15
|
-
__all__ = [
|
16
|
-
"OOD_AE",
|
17
|
-
"OOD_AEGMM",
|
18
|
-
"OOD_LLR",
|
19
|
-
"OOD_VAE",
|
20
|
-
"OOD_VAEGMM",
|
21
|
-
"OODOutput",
|
22
|
-
"OODScoreOutput",
|
23
|
-
]
|
15
|
+
__all__ = ["OOD_AE", "OOD_AEGMM", "OOD_LLR", "OOD_VAE", "OOD_VAEGMM", "OODOutput", "OODScoreOutput"]
|
@@ -8,6 +8,8 @@ Licensed under Apache Software License (Apache 2.0)
|
|
8
8
|
|
9
9
|
from __future__ import annotations
|
10
10
|
|
11
|
+
__all__ = ["OOD_AE"]
|
12
|
+
|
11
13
|
from typing import Callable
|
12
14
|
|
13
15
|
import numpy as np
|
@@ -15,21 +17,20 @@ import tensorflow as tf
|
|
15
17
|
import tf_keras as keras
|
16
18
|
from numpy.typing import ArrayLike
|
17
19
|
|
18
|
-
from dataeval.
|
19
|
-
from dataeval.
|
20
|
-
from dataeval.
|
21
|
-
from dataeval.
|
22
|
-
from dataeval._internal.output import set_metadata
|
20
|
+
from dataeval.detectors.ood.base import OODBase, OODScoreOutput
|
21
|
+
from dataeval.interop import as_numpy
|
22
|
+
from dataeval.utils.tensorflow._internal.autoencoder import AE
|
23
|
+
from dataeval.utils.tensorflow._internal.utils import predict_batch
|
23
24
|
|
24
25
|
|
25
26
|
class OOD_AE(OODBase):
|
26
27
|
"""
|
27
|
-
Autoencoder
|
28
|
+
Autoencoder-based :term:`out of distribution<Out-of-distribution (OOD)>` detector.
|
28
29
|
|
29
30
|
Parameters
|
30
31
|
----------
|
31
32
|
model : AE
|
32
|
-
|
33
|
+
An :term:`autoencoder<Autoencoder>` model.
|
33
34
|
"""
|
34
35
|
|
35
36
|
def __init__(self, model: AE) -> None:
|
@@ -49,8 +50,7 @@ class OOD_AE(OODBase):
|
|
49
50
|
loss_fn = keras.losses.MeanSquaredError()
|
50
51
|
super().fit(as_numpy(x_ref), threshold_perc, loss_fn, optimizer, epochs, batch_size, verbose)
|
51
52
|
|
52
|
-
|
53
|
-
def score(self, X: ArrayLike, batch_size: int = int(1e10)) -> OODScoreOutput:
|
53
|
+
def _score(self, X: ArrayLike, batch_size: int = int(1e10)) -> OODScoreOutput:
|
54
54
|
self._validate(X := as_numpy(X))
|
55
55
|
|
56
56
|
# reconstruct instances
|
@@ -8,19 +8,20 @@ Licensed under Apache Software License (Apache 2.0)
|
|
8
8
|
|
9
9
|
from __future__ import annotations
|
10
10
|
|
11
|
+
__all__ = ["OOD_AEGMM"]
|
12
|
+
|
11
13
|
from typing import Callable
|
12
14
|
|
13
15
|
import tensorflow as tf
|
14
16
|
import tf_keras as keras
|
15
17
|
from numpy.typing import ArrayLike
|
16
18
|
|
17
|
-
from dataeval.
|
18
|
-
from dataeval.
|
19
|
-
from dataeval.
|
20
|
-
from dataeval.
|
21
|
-
from dataeval.
|
22
|
-
from dataeval.
|
23
|
-
from dataeval._internal.output import set_metadata
|
19
|
+
from dataeval.detectors.ood.base import OODGMMBase, OODScoreOutput
|
20
|
+
from dataeval.interop import to_numpy
|
21
|
+
from dataeval.utils.tensorflow._internal.autoencoder import AEGMM
|
22
|
+
from dataeval.utils.tensorflow._internal.gmm import gmm_energy
|
23
|
+
from dataeval.utils.tensorflow._internal.loss import LossGMM
|
24
|
+
from dataeval.utils.tensorflow._internal.utils import predict_batch
|
24
25
|
|
25
26
|
|
26
27
|
class OOD_AEGMM(OODGMMBase):
|
@@ -30,7 +31,7 @@ class OOD_AEGMM(OODGMMBase):
|
|
30
31
|
Parameters
|
31
32
|
----------
|
32
33
|
model : AEGMM
|
33
|
-
|
34
|
+
An AEGMM model.
|
34
35
|
"""
|
35
36
|
|
36
37
|
def __init__(self, model: AEGMM) -> None:
|
@@ -50,28 +51,7 @@ class OOD_AEGMM(OODGMMBase):
|
|
50
51
|
loss_fn = LossGMM()
|
51
52
|
super().fit(x_ref, threshold_perc, loss_fn, optimizer, epochs, batch_size, verbose)
|
52
53
|
|
53
|
-
|
54
|
-
def score(self, X: ArrayLike, batch_size: int = int(1e10)) -> OODScoreOutput:
|
55
|
-
"""
|
56
|
-
Compute the out-of-distribution (OOD) score for a given dataset.
|
57
|
-
|
58
|
-
Parameters
|
59
|
-
----------
|
60
|
-
X : ArrayLike
|
61
|
-
Input data to score.
|
62
|
-
batch_size : int, default 1e10
|
63
|
-
Number of instances to process in each batch.
|
64
|
-
Use a smaller batch size if your dataset is large or if you encounter memory issues.
|
65
|
-
|
66
|
-
Returns
|
67
|
-
-------
|
68
|
-
OODScoreOutput
|
69
|
-
An object containing the instance-level OOD score.
|
70
|
-
|
71
|
-
Note
|
72
|
-
----
|
73
|
-
This model does not produce a feature level score like the OOD_AE or OOD_VAE models.
|
74
|
-
"""
|
54
|
+
def _score(self, X: ArrayLike, batch_size: int = int(1e10)) -> OODScoreOutput:
|
75
55
|
self._validate(X := to_numpy(X))
|
76
56
|
_, z, _ = predict_batch(X, self.model, batch_size=batch_size)
|
77
57
|
energy, _ = gmm_energy(z, self.gmm_params, return_mean=False)
|
@@ -8,6 +8,8 @@ Licensed under Apache Software License (Apache 2.0)
|
|
8
8
|
|
9
9
|
from __future__ import annotations
|
10
10
|
|
11
|
+
__all__ = ["OODOutput", "OODScoreOutput"]
|
12
|
+
|
11
13
|
from abc import ABC, abstractmethod
|
12
14
|
from dataclasses import dataclass
|
13
15
|
from typing import Callable, Literal, cast
|
@@ -17,10 +19,10 @@ import tensorflow as tf
|
|
17
19
|
import tf_keras as keras
|
18
20
|
from numpy.typing import ArrayLike, NDArray
|
19
21
|
|
20
|
-
from dataeval.
|
21
|
-
from dataeval.
|
22
|
-
from dataeval.
|
23
|
-
from dataeval._internal.
|
22
|
+
from dataeval.interop import to_numpy
|
23
|
+
from dataeval.output import OutputMetadata, set_metadata
|
24
|
+
from dataeval.utils.tensorflow._internal.gmm import GaussianMixtureModelParams, gmm_params
|
25
|
+
from dataeval.utils.tensorflow._internal.trainer import trainer
|
24
26
|
|
25
27
|
|
26
28
|
@dataclass(frozen=True)
|
@@ -32,7 +34,7 @@ class OODOutput(OutputMetadata):
|
|
32
34
|
Attributes
|
33
35
|
----------
|
34
36
|
is_ood : NDArray
|
35
|
-
Array of images that are detected as
|
37
|
+
Array of images that are detected as :term:Out-of-Distribution (OOD)`
|
36
38
|
instance_score : NDArray
|
37
39
|
Instance score of the evaluated dataset
|
38
40
|
feature_score : NDArray | None
|
@@ -61,7 +63,7 @@ class OODScoreOutput(OutputMetadata):
|
|
61
63
|
instance_score: NDArray[np.float32]
|
62
64
|
feature_score: NDArray[np.float32] | None = None
|
63
65
|
|
64
|
-
def get(self, ood_type: Literal["instance", "feature"]) -> NDArray:
|
66
|
+
def get(self, ood_type: Literal["instance", "feature"]) -> NDArray[np.float32]:
|
65
67
|
"""
|
66
68
|
Returns either the instance or feature score
|
67
69
|
|
@@ -107,9 +109,12 @@ class OODBase(ABC):
|
|
107
109
|
self._validate(X)
|
108
110
|
|
109
111
|
@abstractmethod
|
112
|
+
def _score(self, X: ArrayLike, batch_size: int = int(1e10)) -> OODScoreOutput: ...
|
113
|
+
|
114
|
+
@set_metadata()
|
110
115
|
def score(self, X: ArrayLike, batch_size: int = int(1e10)) -> OODScoreOutput:
|
111
116
|
"""
|
112
|
-
Compute the out-of-distribution (OOD) scores for a given dataset.
|
117
|
+
Compute the :term:`out of distribution<Out-of-distribution (OOD)>` scores for a given dataset.
|
113
118
|
|
114
119
|
Parameters
|
115
120
|
----------
|
@@ -124,6 +129,7 @@ class OODBase(ABC):
|
|
124
129
|
OODScoreOutput
|
125
130
|
An object containing the instance-level and feature-level OOD scores.
|
126
131
|
"""
|
132
|
+
return self._score(X, batch_size)
|
127
133
|
|
128
134
|
def _threshold_score(self, ood_type: Literal["feature", "instance"] = "instance") -> np.floating:
|
129
135
|
return np.percentile(self._ref_score.get(ood_type), self._threshold_perc)
|
@@ -131,12 +137,12 @@ class OODBase(ABC):
|
|
131
137
|
def fit(
|
132
138
|
self,
|
133
139
|
x_ref: ArrayLike,
|
134
|
-
threshold_perc: float
|
135
|
-
loss_fn: Callable[..., tf.Tensor]
|
136
|
-
optimizer: keras.optimizers.Optimizer
|
137
|
-
epochs: int
|
138
|
-
batch_size: int
|
139
|
-
verbose: bool
|
140
|
+
threshold_perc: float,
|
141
|
+
loss_fn: Callable[..., tf.Tensor],
|
142
|
+
optimizer: keras.optimizers.Optimizer,
|
143
|
+
epochs: int,
|
144
|
+
batch_size: int,
|
145
|
+
verbose: bool,
|
140
146
|
) -> None:
|
141
147
|
"""
|
142
148
|
Train the model and infer the threshold value.
|
@@ -174,7 +180,7 @@ class OODBase(ABC):
|
|
174
180
|
self._ref_score = self.score(x_ref, batch_size)
|
175
181
|
self._threshold_perc = threshold_perc
|
176
182
|
|
177
|
-
@set_metadata(
|
183
|
+
@set_metadata()
|
178
184
|
def predict(
|
179
185
|
self,
|
180
186
|
X: ArrayLike,
|
@@ -182,7 +188,7 @@ class OODBase(ABC):
|
|
182
188
|
ood_type: Literal["feature", "instance"] = "instance",
|
183
189
|
) -> OODOutput:
|
184
190
|
"""
|
185
|
-
Predict whether instances are out-of-distribution or not.
|
191
|
+
Predict whether instances are :term:`out of distribution<Out-of-distribution (OOD)>` or not.
|
186
192
|
|
187
193
|
Parameters
|
188
194
|
----------
|
@@ -218,12 +224,12 @@ class OODGMMBase(OODBase):
|
|
218
224
|
def fit(
|
219
225
|
self,
|
220
226
|
x_ref: ArrayLike,
|
221
|
-
threshold_perc: float
|
222
|
-
loss_fn: Callable[..., tf.Tensor]
|
223
|
-
optimizer: keras.optimizers.Optimizer
|
224
|
-
epochs: int
|
225
|
-
batch_size: int
|
226
|
-
verbose: bool
|
227
|
+
threshold_perc: float,
|
228
|
+
loss_fn: Callable[..., tf.Tensor],
|
229
|
+
optimizer: keras.optimizers.Optimizer,
|
230
|
+
epochs: int,
|
231
|
+
batch_size: int,
|
232
|
+
verbose: bool,
|
227
233
|
) -> None:
|
228
234
|
# Train the model
|
229
235
|
trainer(
|
@@ -8,6 +8,8 @@ Licensed under Apache Software License (Apache 2.0)
|
|
8
8
|
|
9
9
|
from __future__ import annotations
|
10
10
|
|
11
|
+
__all__ = ["OOD_LLR"]
|
12
|
+
|
11
13
|
from functools import partial
|
12
14
|
from typing import Callable
|
13
15
|
|
@@ -18,15 +20,14 @@ from numpy.typing import ArrayLike, NDArray
|
|
18
20
|
from tf_keras.layers import Input
|
19
21
|
from tf_keras.models import Model
|
20
22
|
|
21
|
-
from dataeval.
|
22
|
-
from dataeval.
|
23
|
-
from dataeval.
|
24
|
-
from dataeval.
|
25
|
-
from dataeval.
|
26
|
-
from dataeval._internal.output import set_metadata
|
23
|
+
from dataeval.detectors.ood.base import OODBase, OODScoreOutput
|
24
|
+
from dataeval.interop import to_numpy
|
25
|
+
from dataeval.utils.tensorflow._internal.pixelcnn import PixelCNN
|
26
|
+
from dataeval.utils.tensorflow._internal.trainer import trainer
|
27
|
+
from dataeval.utils.tensorflow._internal.utils import predict_batch
|
27
28
|
|
28
29
|
|
29
|
-
def
|
30
|
+
def _build_model(
|
30
31
|
dist: PixelCNN, input_shape: tuple | None = None, filepath: str | None = None
|
31
32
|
) -> tuple[keras.Model, PixelCNN]:
|
32
33
|
"""
|
@@ -35,7 +36,7 @@ def build_model(
|
|
35
36
|
Parameters
|
36
37
|
----------
|
37
38
|
dist
|
38
|
-
TensorFlow distribution.
|
39
|
+
:term:`TensorFlow` distribution.
|
39
40
|
input_shape
|
40
41
|
Input shape of the model.
|
41
42
|
filepath
|
@@ -54,11 +55,11 @@ def build_model(
|
|
54
55
|
return model, dist
|
55
56
|
|
56
57
|
|
57
|
-
def
|
58
|
+
def _mutate_categorical(
|
58
59
|
X: NDArray,
|
59
60
|
rate: float,
|
60
61
|
seed: int = 0,
|
61
|
-
feature_range: tuple = (0, 255),
|
62
|
+
feature_range: tuple[int, int] = (0, 255),
|
62
63
|
) -> tf.Tensor:
|
63
64
|
"""
|
64
65
|
Randomly change integer feature values to values within a set range
|
@@ -113,17 +114,17 @@ class OOD_LLR(OODBase):
|
|
113
114
|
log_prob: Callable | None = None,
|
114
115
|
sequential: bool = False,
|
115
116
|
) -> None:
|
116
|
-
self.dist_s = model
|
117
|
-
self.dist_b = (
|
117
|
+
self.dist_s: PixelCNN = model
|
118
|
+
self.dist_b: PixelCNN = (
|
118
119
|
model.copy()
|
119
120
|
if hasattr(model, "copy")
|
120
121
|
else keras.models.clone_model(model)
|
121
122
|
if model_background is None
|
122
123
|
else model_background
|
123
124
|
)
|
124
|
-
self.has_log_prob = hasattr(model, "log_prob")
|
125
|
-
self.sequential = sequential
|
126
|
-
self.log_prob = log_prob
|
125
|
+
self.has_log_prob: bool = hasattr(model, "log_prob")
|
126
|
+
self.sequential: bool = sequential
|
127
|
+
self.log_prob: Callable | None = log_prob
|
127
128
|
|
128
129
|
self._ref_score: OODScoreOutput
|
129
130
|
self._threshold_perc: float
|
@@ -138,8 +139,12 @@ class OOD_LLR(OODBase):
|
|
138
139
|
epochs: int = 20,
|
139
140
|
batch_size: int = 64,
|
140
141
|
verbose: bool = True,
|
141
|
-
mutate_fn: Callable =
|
142
|
-
mutate_fn_kwargs: dict
|
142
|
+
mutate_fn: Callable = _mutate_categorical,
|
143
|
+
mutate_fn_kwargs: dict[str, float | int | tuple[int, int]] = {
|
144
|
+
"rate": 0.2,
|
145
|
+
"seed": 0,
|
146
|
+
"feature_range": (0, 255),
|
147
|
+
},
|
143
148
|
mutate_batch_size: int = int(1e10),
|
144
149
|
) -> None:
|
145
150
|
"""
|
@@ -200,11 +205,11 @@ class OOD_LLR(OODBase):
|
|
200
205
|
|
201
206
|
if use_build:
|
202
207
|
# build and train semantic model
|
203
|
-
self.model_s =
|
208
|
+
self.model_s: keras.Model = _build_model(self.dist_s, input_shape)[0]
|
204
209
|
self.model_s.compile(optimizer=optimizer_s)
|
205
210
|
self.model_s.fit(X, **kwargs)
|
206
211
|
# build and train background model
|
207
|
-
self.model_b =
|
212
|
+
self.model_b: keras.Model = _build_model(self.dist_b, input_shape)[0]
|
208
213
|
self.model_b.compile(optimizer=optimizer_b)
|
209
214
|
self.model_b.fit(X_back, **kwargs)
|
210
215
|
else:
|
@@ -230,7 +235,7 @@ class OOD_LLR(OODBase):
|
|
230
235
|
batch_size: int = int(1e10),
|
231
236
|
) -> NDArray:
|
232
237
|
"""
|
233
|
-
Compute log probability of a batch of instances under the generative model
|
238
|
+
Compute log probability of a batch of instances under the :term:`generative model<Generative Model>`.
|
234
239
|
"""
|
235
240
|
logp_fn = partial(dist.log_prob, return_per_feature=return_per_feature)
|
236
241
|
# TODO: TBD: can this be any of the other types from predict_batch? i.e. tf.Tensor or tuple
|
@@ -269,7 +274,7 @@ class OOD_LLR(OODBase):
|
|
269
274
|
return_per_feature
|
270
275
|
Return likelihood ratio per feature.
|
271
276
|
batch_size
|
272
|
-
Batch size for the generative model evaluations.
|
277
|
+
Batch size for the :term:`generative model<Generative Model>` evaluations.
|
273
278
|
|
274
279
|
Returns
|
275
280
|
-------
|
@@ -280,8 +285,7 @@ class OOD_LLR(OODBase):
|
|
280
285
|
logp_b = logp_fn(self.dist_b, X, return_per_feature=return_per_feature, batch_size=batch_size)
|
281
286
|
return logp_s - logp_b
|
282
287
|
|
283
|
-
|
284
|
-
def score(
|
288
|
+
def _score(
|
285
289
|
self,
|
286
290
|
X: ArrayLike,
|
287
291
|
batch_size: int = int(1e10),
|
@@ -0,0 +1,99 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import numbers
|
4
|
+
import warnings
|
5
|
+
from typing import Any, Mapping
|
6
|
+
|
7
|
+
import numpy as np
|
8
|
+
from numpy.typing import NDArray
|
9
|
+
from scipy.stats import iqr, ks_2samp
|
10
|
+
from scipy.stats import wasserstein_distance as emd
|
11
|
+
|
12
|
+
|
13
|
+
def meta_distribution_compare(
|
14
|
+
md0: Mapping[str, list[Any] | NDArray[Any]], md1: Mapping[str, list[Any] | NDArray[Any]]
|
15
|
+
) -> dict[str, dict[str, float]]:
|
16
|
+
"""Measures the featurewise distance between two metadata distributions, and computes a p-value to evaluate its
|
17
|
+
significance.
|
18
|
+
|
19
|
+
Uses the Earth Mover's Distance and the Kolmogorov-Smirnov two-sample test, featurewise.
|
20
|
+
|
21
|
+
Parameters
|
22
|
+
----------
|
23
|
+
md0 : Mapping[str, list[Any] | NDArray[Any]]
|
24
|
+
A set of arrays of values, indexed by metadata feature names, with one value per data example per feature.
|
25
|
+
md1 : Mapping[str, list[Any] | NDArray[Any]]
|
26
|
+
Another set of arrays of values, indexed by metadata feature names, with one value per data example per
|
27
|
+
feature.
|
28
|
+
|
29
|
+
Returns
|
30
|
+
-------
|
31
|
+
dict[str, KstestResult]
|
32
|
+
A dictionary with keys corresponding to metadata feature names, and values that are KstestResult objects, as
|
33
|
+
defined by scipy.stats.ks_2samp. These values also have two additional attributes: shift_magnitude and
|
34
|
+
statistic_location. The first is the Earth Mover's Distance normalized by the interquartile range (IQR) of
|
35
|
+
the reference, while the second is the value at which the KS statistic has its maximum, measured in
|
36
|
+
IQR-normalized units relative to the median of the reference distribution.
|
37
|
+
|
38
|
+
Examples
|
39
|
+
--------
|
40
|
+
Imagine we have 3 data examples, and that the corresponding metadata contains 2 features called time and
|
41
|
+
altitude.
|
42
|
+
|
43
|
+
>>> import numpy
|
44
|
+
>>> md0 = {"time": [1.2, 3.4, 5.6], "altitude": [235, 6789, 101112]}
|
45
|
+
>>> md1 = {"time": [7.8, 9.10, 11.12], "altitude": [532, 9876, 211101]}
|
46
|
+
>>> md_out = meta_distribution_compare(md0, md1)
|
47
|
+
>>> for k, v in md_out.items():
|
48
|
+
>>> print(k)
|
49
|
+
>>> for kv in v:
|
50
|
+
>>> print("\t", f"{kv}: {v[kv]:.3f}")
|
51
|
+
time
|
52
|
+
statistic_location: 0.444
|
53
|
+
shift_magnitude: 2.700
|
54
|
+
pvalue: 0.000
|
55
|
+
altitude
|
56
|
+
statistic_location: 0.478
|
57
|
+
shift_magnitude: 0.749
|
58
|
+
pvalue: 0.944
|
59
|
+
"""
|
60
|
+
|
61
|
+
if (metadata_keys := md0.keys()) != md1.keys():
|
62
|
+
raise ValueError(f"Both sets of metadata keys must be identical: {list(md0)}, {list(md1)}")
|
63
|
+
|
64
|
+
mdc_dict = {} # output dict
|
65
|
+
for k in metadata_keys:
|
66
|
+
mdc_dict.update({k: {}})
|
67
|
+
|
68
|
+
x0, x1 = list(md0[k]), list(md1[k])
|
69
|
+
|
70
|
+
allx = x0 + x1 # "+" sign concatenates lists.
|
71
|
+
|
72
|
+
if not all(isinstance(allxi, numbers.Number) for allxi in allx): # NB: np.nan *is* a number in this context.
|
73
|
+
continue # non-numeric features will return an empty dict for feature k
|
74
|
+
|
75
|
+
# from Numerical Recipes in C, 3rd ed. p. 737. If too few points, warn and keep going.
|
76
|
+
if np.sqrt(((N := len(x0)) * (M := len(x1))) / (N + M)) < 4:
|
77
|
+
warnings.warn(
|
78
|
+
f"Sample sizes of {N}, {M} for feature {k} will yield unreliable p-values from the KS test.",
|
79
|
+
UserWarning,
|
80
|
+
)
|
81
|
+
|
82
|
+
xmin, xmax = min(allx), max(allx)
|
83
|
+
if xmin == xmax: # only one value in this feature, so fill in the obvious results for feature k
|
84
|
+
mdc_dict[k].update({"statistic_location": 0.0, "shift_magnitude": 0.0, "pvalue": 1.0})
|
85
|
+
continue
|
86
|
+
|
87
|
+
ks_result = ks_2samp(x0, x1, method="asymp")
|
88
|
+
dev = ks_result.statistic_location - xmin # pyright: ignore (KSresult type)
|
89
|
+
loc = dev / (xmax - xmin) if xmax > xmin else dev
|
90
|
+
|
91
|
+
dX = iqr(x0) # preferred value of dX, which is the scale of the the md0 values for feature k
|
92
|
+
dX = (max(x0) - min(x0)) / 2.0 if dX == 0 else dX # reasonable alternative value of dX, when iqr is zero.
|
93
|
+
dX = 1.0 if dX == 0 else dX # if dX is *still* zero, just avoid division by zero this way
|
94
|
+
|
95
|
+
drift = emd(x0, x1) / dX
|
96
|
+
|
97
|
+
mdc_dict[k].update({"statistic_location": loc, "shift_magnitude": drift, "pvalue": ks_result.pvalue}) # pyright: ignore
|
98
|
+
|
99
|
+
return mdc_dict
|