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.
Files changed (90) hide show
  1. dataeval/__init__.py +4 -4
  2. dataeval/detectors/__init__.py +4 -3
  3. dataeval/detectors/drift/__init__.py +10 -11
  4. dataeval/{_internal/detectors → detectors}/drift/base.py +51 -102
  5. dataeval/{_internal/detectors → detectors}/drift/cvm.py +9 -8
  6. dataeval/{_internal/detectors → detectors}/drift/ks.py +11 -10
  7. dataeval/{_internal/detectors → detectors}/drift/mmd.py +33 -34
  8. dataeval/{_internal/detectors → detectors}/drift/torch.py +15 -13
  9. dataeval/{_internal/detectors → detectors}/drift/uncertainty.py +12 -9
  10. dataeval/detectors/drift/updates.py +61 -0
  11. dataeval/detectors/linters/__init__.py +3 -3
  12. dataeval/{_internal/detectors → detectors/linters}/clusterer.py +47 -45
  13. dataeval/{_internal/detectors → detectors/linters}/duplicates.py +20 -10
  14. dataeval/{_internal/detectors → detectors/linters}/merged_stats.py +3 -1
  15. dataeval/{_internal/detectors → detectors/linters}/outliers.py +19 -26
  16. dataeval/detectors/ood/__init__.py +8 -16
  17. dataeval/{_internal/detectors → detectors}/ood/ae.py +9 -9
  18. dataeval/{_internal/detectors → detectors}/ood/aegmm.py +10 -30
  19. dataeval/{_internal/detectors → detectors}/ood/base.py +27 -21
  20. dataeval/{_internal/detectors → detectors}/ood/llr.py +27 -23
  21. dataeval/detectors/ood/metadata_ks_compare.py +99 -0
  22. dataeval/detectors/ood/metadata_least_likely.py +119 -0
  23. dataeval/detectors/ood/metadata_ood_mi.py +92 -0
  24. dataeval/{_internal/detectors → detectors}/ood/vae.py +11 -13
  25. dataeval/{_internal/detectors → detectors}/ood/vaegmm.py +10 -32
  26. dataeval/{_internal/interop.py → interop.py} +12 -7
  27. dataeval/metrics/__init__.py +1 -1
  28. dataeval/metrics/bias/__init__.py +4 -4
  29. dataeval/{_internal/metrics → metrics/bias}/balance.py +70 -4
  30. dataeval/{_internal/metrics → metrics/bias}/coverage.py +10 -8
  31. dataeval/{_internal/metrics → metrics/bias}/diversity.py +54 -20
  32. dataeval/metrics/bias/metadata.py +275 -0
  33. dataeval/{_internal/metrics → metrics/bias}/parity.py +21 -17
  34. dataeval/metrics/estimators/__init__.py +3 -3
  35. dataeval/{_internal/metrics → metrics/estimators}/ber.py +31 -28
  36. dataeval/{_internal/metrics → metrics/estimators}/divergence.py +15 -16
  37. dataeval/{_internal/metrics → metrics/estimators}/uap.py +8 -6
  38. dataeval/metrics/stats/__init__.py +7 -7
  39. dataeval/{_internal/metrics → metrics}/stats/base.py +66 -40
  40. dataeval/{_internal/metrics → metrics}/stats/boxratiostats.py +19 -15
  41. dataeval/{_internal/metrics → metrics}/stats/datasetstats.py +19 -17
  42. dataeval/{_internal/metrics → metrics}/stats/dimensionstats.py +12 -10
  43. dataeval/metrics/stats/hashstats.py +156 -0
  44. dataeval/{_internal/metrics → metrics}/stats/labelstats.py +8 -6
  45. dataeval/{_internal/metrics → metrics}/stats/pixelstats.py +12 -11
  46. dataeval/{_internal/metrics → metrics}/stats/visualstats.py +14 -13
  47. dataeval/{_internal/output.py → output.py} +26 -6
  48. dataeval/utils/__init__.py +8 -4
  49. dataeval/utils/image.py +71 -0
  50. dataeval/utils/shared.py +151 -0
  51. dataeval/utils/split_dataset.py +486 -0
  52. dataeval/utils/tensorflow/__init__.py +9 -7
  53. dataeval/{_internal/models/tensorflow → utils/tensorflow/_internal}/autoencoder.py +64 -68
  54. dataeval/{_internal/models/tensorflow/losses.py → utils/tensorflow/_internal/loss.py} +10 -9
  55. dataeval/{_internal/models/tensorflow → utils/tensorflow/_internal}/pixelcnn.py +18 -22
  56. dataeval/{_internal/models/tensorflow → utils/tensorflow/_internal}/trainer.py +3 -1
  57. dataeval/{_internal/models/tensorflow → utils/tensorflow/_internal}/utils.py +18 -18
  58. dataeval/utils/tensorflow/loss/__init__.py +6 -2
  59. dataeval/utils/torch/__init__.py +7 -3
  60. dataeval/{_internal/models/pytorch → utils/torch}/blocks.py +19 -14
  61. dataeval/{_internal → utils/torch}/datasets.py +49 -43
  62. dataeval/utils/torch/models.py +138 -0
  63. dataeval/{_internal/models/pytorch/autoencoder.py → utils/torch/trainer.py} +12 -141
  64. dataeval/{_internal → utils/torch}/utils.py +3 -1
  65. dataeval/workflows/__init__.py +1 -1
  66. dataeval/{_internal/workflows → workflows}/sufficiency.py +42 -37
  67. {dataeval-0.72.0.dist-info → dataeval-0.72.2.dist-info}/METADATA +7 -5
  68. dataeval-0.72.2.dist-info/RECORD +72 -0
  69. dataeval/_internal/detectors/__init__.py +0 -0
  70. dataeval/_internal/detectors/drift/__init__.py +0 -0
  71. dataeval/_internal/detectors/ood/__init__.py +0 -0
  72. dataeval/_internal/metrics/__init__.py +0 -0
  73. dataeval/_internal/metrics/stats/hashstats.py +0 -75
  74. dataeval/_internal/metrics/utils.py +0 -447
  75. dataeval/_internal/models/__init__.py +0 -0
  76. dataeval/_internal/models/pytorch/__init__.py +0 -0
  77. dataeval/_internal/models/pytorch/utils.py +0 -67
  78. dataeval/_internal/models/tensorflow/__init__.py +0 -0
  79. dataeval/_internal/workflows/__init__.py +0 -0
  80. dataeval/detectors/drift/kernels/__init__.py +0 -10
  81. dataeval/detectors/drift/updates/__init__.py +0 -7
  82. dataeval/utils/tensorflow/models/__init__.py +0 -9
  83. dataeval/utils/tensorflow/recon/__init__.py +0 -3
  84. dataeval/utils/torch/datasets/__init__.py +0 -12
  85. dataeval/utils/torch/models/__init__.py +0 -11
  86. dataeval/utils/torch/trainer/__init__.py +0 -7
  87. dataeval-0.72.0.dist-info/RECORD +0 -80
  88. /dataeval/{_internal/models/tensorflow → utils/tensorflow/_internal}/gmm.py +0 -0
  89. {dataeval-0.72.0.dist-info → dataeval-0.72.2.dist-info}/LICENSE.txt +0 -0
  90. {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._internal.detectors.merged_stats import combine_stats, get_dataset_step_from_idx
10
- from dataeval._internal.metrics.stats.base import BOX_COUNT, SOURCE_INDEX
11
- from dataeval._internal.metrics.stats.datasetstats import DatasetStatsOutput, datasetstats
12
- from dataeval._internal.metrics.stats.dimensionstats import DimensionStatsOutput
13
- from dataeval._internal.metrics.stats.pixelstats import PixelStatsOutput
14
- from dataeval._internal.metrics.stats.visualstats import VisualStatsOutput
15
- from dataeval._internal.output import OutputMetadata, set_metadata
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 outliers with their associated issue type and calculated values.
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 outliers of a dataset using various statistical tests applied to each image
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("dataeval.detectors", ["outlier_method", "outlier_threshold"])
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 outliers with the issues identified for each
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.outputs() for k, v in o.dict().items()})
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 outliers with the issues identified for each
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._internal.detectors.ood.ae import OOD_AE
9
- from dataeval._internal.detectors.ood.aegmm import OOD_AEGMM
10
- from dataeval._internal.detectors.ood.base import OODOutput, OODScoreOutput
11
- from dataeval._internal.detectors.ood.llr import OOD_LLR
12
- from dataeval._internal.detectors.ood.vae import OOD_VAE
13
- from dataeval._internal.detectors.ood.vaegmm import OOD_VAEGMM
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._internal.detectors.ood.base import OODBase, OODScoreOutput
19
- from dataeval._internal.interop import as_numpy
20
- from dataeval._internal.models.tensorflow.autoencoder import AE
21
- from dataeval._internal.models.tensorflow.utils import predict_batch
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 based out-of-distribution detector.
28
+ Autoencoder-based :term:`out of distribution<Out-of-distribution (OOD)>` detector.
28
29
 
29
30
  Parameters
30
31
  ----------
31
32
  model : AE
32
- An Autoencoder model.
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
- @set_metadata("dataeval.detectors")
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._internal.detectors.ood.base import OODGMMBase, OODScoreOutput
18
- from dataeval._internal.interop import to_numpy
19
- from dataeval._internal.models.tensorflow.autoencoder import AEGMM
20
- from dataeval._internal.models.tensorflow.gmm import gmm_energy
21
- from dataeval._internal.models.tensorflow.losses import LossGMM
22
- from dataeval._internal.models.tensorflow.utils import predict_batch
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
- An AEGMM model.
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
- @set_metadata("dataeval.detectors")
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._internal.interop import to_numpy
21
- from dataeval._internal.models.tensorflow.gmm import GaussianMixtureModelParams, gmm_params
22
- from dataeval._internal.models.tensorflow.trainer import trainer
23
- from dataeval._internal.output import OutputMetadata, set_metadata
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 out of distribution
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 = 100.0,
135
- loss_fn: Callable[..., tf.Tensor] | None = None,
136
- optimizer: keras.optimizers.Optimizer = keras.optimizers.Adam,
137
- epochs: int = 20,
138
- batch_size: int = 64,
139
- verbose: bool = True,
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("dataeval.detectors")
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 = 100.0,
222
- loss_fn: Callable[..., tf.Tensor] | None = None,
223
- optimizer: keras.optimizers.Optimizer = keras.optimizers.Adam,
224
- epochs: int = 20,
225
- batch_size: int = 64,
226
- verbose: bool = True,
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._internal.detectors.ood.base import OODBase, OODScoreOutput
22
- from dataeval._internal.interop import to_numpy
23
- from dataeval._internal.models.tensorflow.pixelcnn import PixelCNN
24
- from dataeval._internal.models.tensorflow.trainer import trainer
25
- from dataeval._internal.models.tensorflow.utils import predict_batch
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 build_model(
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 mutate_categorical(
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 = mutate_categorical,
142
- mutate_fn_kwargs: dict = {"rate": 0.2, "seed": 0, "feature_range": (0, 255)},
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 = build_model(self.dist_s, input_shape)[0]
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 = build_model(self.dist_b, input_shape)[0]
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
- @set_metadata("dataeval.detectors")
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