bias-scope 0.1.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.
Files changed (55) hide show
  1. bias_scope/__init__.py +178 -0
  2. bias_scope/base.py +483 -0
  3. bias_scope/embeddings_based/__init__.py +9 -0
  4. bias_scope/embeddings_based/_helpers.py +135 -0
  5. bias_scope/embeddings_based/ceat.py +273 -0
  6. bias_scope/embeddings_based/seat.py +107 -0
  7. bias_scope/embeddings_based/sentence_bias_score.py +320 -0
  8. bias_scope/embeddings_based/weat.py +187 -0
  9. bias_scope/generated_text_based/__init__.py +50 -0
  10. bias_scope/generated_text_based/_helpers.py +301 -0
  11. bias_scope/generated_text_based/cooccurrence_bias_score.py +323 -0
  12. bias_scope/generated_text_based/counterfactual_sentiment_bias.py +78 -0
  13. bias_scope/generated_text_based/demographic_representation.py +270 -0
  14. bias_scope/generated_text_based/emt.py +64 -0
  15. bias_scope/generated_text_based/fgb.py +76 -0
  16. bias_scope/generated_text_based/gender_polarity.py +163 -0
  17. bias_scope/generated_text_based/honest.py +138 -0
  18. bias_scope/generated_text_based/marked_persons.py +262 -0
  19. bias_scope/generated_text_based/perspective_api.py +219 -0
  20. bias_scope/generated_text_based/pgb.py +75 -0
  21. bias_scope/generated_text_based/psycholinguistic_norms.py +223 -0
  22. bias_scope/generated_text_based/regard_score.py +250 -0
  23. bias_scope/generated_text_based/score_parity.py +208 -0
  24. bias_scope/generated_text_based/social_group_substitution.py +396 -0
  25. bias_scope/generated_text_based/stereotypical_associations.py +296 -0
  26. bias_scope/generated_text_based/toxicity_fraction.py +191 -0
  27. bias_scope/generated_text_based/toxicity_probability.py +137 -0
  28. bias_scope/probability_based/__init__.py +33 -0
  29. bias_scope/probability_based/_helpers.py +117 -0
  30. bias_scope/probability_based/_scorers.py +100 -0
  31. bias_scope/probability_based/aul.py +171 -0
  32. bias_scope/probability_based/aula.py +282 -0
  33. bias_scope/probability_based/cat.py +187 -0
  34. bias_scope/probability_based/cbs.py +297 -0
  35. bias_scope/probability_based/crows_pairs.py +196 -0
  36. bias_scope/probability_based/disco.py +151 -0
  37. bias_scope/probability_based/icat.py +131 -0
  38. bias_scope/probability_based/lmb.py +489 -0
  39. bias_scope/probability_based/lpbs.py +203 -0
  40. bias_scope/prompts_based/__init__.py +74 -0
  41. bias_scope/prompts_based/analogical_reasoning_bias.py +396 -0
  42. bias_scope/prompts_based/bbq.py +286 -0
  43. bias_scope/prompts_based/bold.py +267 -0
  44. bias_scope/prompts_based/counterfactual_fairness.py +279 -0
  45. bias_scope/prompts_based/demographic_representation_bias.py +269 -0
  46. bias_scope/prompts_based/opinion_consistency_across_personas.py +280 -0
  47. bias_scope/prompts_based/realtoxicityprompts.py +246 -0
  48. bias_scope/prompts_based/stereoset.py +431 -0
  49. bias_scope/prompts_based/tof_nof.py +338 -0
  50. bias_scope/prompts_based/truthfulqa.py +336 -0
  51. bias_scope/prompts_based/unqover.py +683 -0
  52. bias_scope/utils.py +92 -0
  53. bias_scope-0.1.0.dist-info/METADATA +32 -0
  54. bias_scope-0.1.0.dist-info/RECORD +55 -0
  55. bias_scope-0.1.0.dist-info/WHEEL +4 -0
bias_scope/__init__.py ADDED
@@ -0,0 +1,178 @@
1
+ """
2
+ bias-scope: Comprehensive bias detection for language models.
3
+
4
+ Public API for bias detection metrics organized by category:
5
+ - Embeddings: WEAT, SEAT, CEAT, SentenceBiasScore
6
+ - Probability: CrowSPairs, CAT, AUL, LPBS, CBS, DisCoMetric, ICAT, AULA, LMB
7
+ - Generated Text: ToxicityFraction, ToxicityProbability, RegardScore, ScoreParity,
8
+ SocialGroupSubstitution, CoOccurrenceBiasScore,
9
+ DemographicRepresentation, StereotypicalAssociations, MarkedPersons
10
+ - Prompt-Based: AnalogicalReasoningBias, BBQMetric, BOLD,
11
+ CounterfactualFairness, OpinionConsistencyAcrossPersonas,
12
+ RealToxicityPrompts, StereoSetMetric, TofNof,
13
+ TruthfulQA, UnQoverMetric
14
+ - Utilities: to_numpy, cosine_similarity
15
+ """
16
+
17
+ __version__ = "0.1.0"
18
+
19
+ # Public API: Import metric classes
20
+ # Embedding and probability metrics require torch - make them optional
21
+ try:
22
+ from bias_scope.embeddings_based import CEAT, SEAT, WEAT, SentenceBiasScore
23
+ from bias_scope.probability_based import (
24
+ AUL,
25
+ AULA,
26
+ CAT,
27
+ CBS,
28
+ ICAT,
29
+ LMB,
30
+ LPBS,
31
+ CrowSPairs,
32
+ DisCoMetric,
33
+ )
34
+
35
+ _TORCH_AVAILABLE = True
36
+ except ImportError as e:
37
+ # Torch not available - embedding and probability metrics won't work
38
+ _TORCH_AVAILABLE = False
39
+ CEAT = SEAT = WEAT = SentenceBiasScore = None
40
+ AUL = AULA = CAT = CBS = CrowSPairs = DisCoMetric = ICAT = LMB = LPBS = None
41
+
42
+ # Import generated text metrics (all in generated_text_based)
43
+ from bias_scope.generated_text_based import (
44
+ HONEST,
45
+ CoOccurrenceBiasScore,
46
+ CounterfactualSentimentBias,
47
+ DemographicRepresentation,
48
+ EMT,
49
+ FGB,
50
+ GenderPolarity,
51
+ MarkedPersons,
52
+ PGB,
53
+ PerspectiveAPIClient,
54
+ PsycholinguisticNorms,
55
+ RegardScore,
56
+ ScoreParity,
57
+ SocialGroupSubstitution,
58
+ StereotypicalAssociations,
59
+ ToxicityFraction,
60
+ ToxicityProbability,
61
+ )
62
+
63
+ try:
64
+ from bias_scope.prompts_based.analogical_reasoning_bias import (
65
+ AnalogicalReasoningBias,
66
+ )
67
+ except ImportError:
68
+ AnalogicalReasoningBias = None
69
+
70
+ try:
71
+ from bias_scope.prompts_based.bbq import BBQMetric
72
+ except ImportError:
73
+ BBQMetric = None
74
+
75
+ try:
76
+ from bias_scope.prompts_based.bold import BOLD
77
+ except ImportError:
78
+ BOLD = None
79
+
80
+ try:
81
+ from bias_scope.prompts_based.counterfactual_fairness import (
82
+ CounterfactualFairness,
83
+ )
84
+ except ImportError:
85
+ CounterfactualFairness = None
86
+
87
+ try:
88
+ from bias_scope.prompts_based.demographic_representation_bias import (
89
+ DemographicRepresentationBias,
90
+ )
91
+ except ImportError:
92
+ DemographicRepresentationBias = None
93
+
94
+ try:
95
+ from bias_scope.prompts_based.opinion_consistency_across_personas import (
96
+ OpinionConsistencyAcrossPersonas,
97
+ )
98
+ except ImportError:
99
+ OpinionConsistencyAcrossPersonas = None
100
+
101
+ try:
102
+ from bias_scope.prompts_based.realtoxicityprompts import RealToxicityPrompts
103
+ except ImportError:
104
+ RealToxicityPrompts = None
105
+
106
+ try:
107
+ from bias_scope.prompts_based.stereoset import StereoSetMetric
108
+ except ImportError:
109
+ StereoSetMetric = None
110
+
111
+ try:
112
+ from bias_scope.prompts_based.tof_nof import TofNof
113
+ except ImportError:
114
+ TofNof = None
115
+
116
+ try:
117
+ from bias_scope.prompts_based.truthfulqa import TruthfulQA
118
+ except ImportError:
119
+ TruthfulQA = None
120
+
121
+ try:
122
+ from bias_scope.prompts_based.unqover import UnQoverMetric
123
+ except ImportError:
124
+ UnQoverMetric = None
125
+
126
+ # Public utilities
127
+ from bias_scope.utils import cosine_similarity, to_numpy
128
+
129
+ __all__ = [
130
+ # Embedding metrics (classes)
131
+ "WEAT",
132
+ "SEAT",
133
+ "CEAT",
134
+ "SentenceBiasScore",
135
+ # Probability metrics
136
+ "CrowSPairs",
137
+ "CAT",
138
+ "AUL",
139
+ "LPBS",
140
+ "CBS",
141
+ "DisCoMetric",
142
+ "ICAT",
143
+ "AULA",
144
+ "LMB",
145
+ # Generated text metrics
146
+ "ToxicityFraction",
147
+ "ToxicityProbability",
148
+ "RegardScore",
149
+ "ScoreParity",
150
+ "SocialGroupSubstitution",
151
+ "CoOccurrenceBiasScore",
152
+ "CounterfactualSentimentBias",
153
+ "DemographicRepresentation",
154
+ "StereotypicalAssociations",
155
+ "MarkedPersons",
156
+ "EMT",
157
+ "FGB",
158
+ "GenderPolarity",
159
+ "HONEST",
160
+ "PGB",
161
+ "PerspectiveAPIClient",
162
+ "PsycholinguisticNorms",
163
+ # Prompt-based metrics
164
+ "AnalogicalReasoningBias",
165
+ "BBQMetric",
166
+ "BOLD",
167
+ "CounterfactualFairness",
168
+ "DemographicRepresentationBias",
169
+ "OpinionConsistencyAcrossPersonas",
170
+ "RealToxicityPrompts",
171
+ "StereoSetMetric",
172
+ "TofNof",
173
+ "TruthfulQA",
174
+ "UnQoverMetric",
175
+ # Utilities
176
+ "to_numpy",
177
+ "cosine_similarity",
178
+ ]
bias_scope/base.py ADDED
@@ -0,0 +1,483 @@
1
+ """
2
+ Abstract base classes for bias detection metrics.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from abc import ABC, abstractmethod
8
+ from typing import Callable, Dict, List, Sequence
9
+
10
+ import numpy as np
11
+
12
+
13
+ class BiasMetric(ABC):
14
+ """
15
+ Abstract base class for all bias detection metrics.
16
+
17
+ All bias metrics must implement the `evaluate` method and inherit
18
+ their category from the appropriate intermediate base class.
19
+
20
+ Examples
21
+ --------
22
+ >>> class MyMetric(BiasMetric):
23
+ ... def evaluate(self, inputs):
24
+ ... return 0.5
25
+ """
26
+
27
+ @abstractmethod
28
+ def evaluate(self, *args, **kwargs) -> float | Dict[str, float]:
29
+ """
30
+ Evaluate the bias metric.
31
+
32
+ Args:
33
+ *args: metric-specific input data
34
+ **kwargs: additional metric parameters
35
+
36
+ Returns:
37
+ float | Dict[str, float]: bias score(s)
38
+
39
+ Raises:
40
+ ValueError: If inputs are invalid
41
+
42
+ Notes:
43
+ - Simple metrics return a single float score
44
+ - Complex metrics return a dictionary with multiple scores
45
+ - Subclasses must implement with their specific signature and validation
46
+ """
47
+ raise NotImplementedError
48
+
49
+ @property
50
+ @abstractmethod
51
+ def category(self) -> str:
52
+ """
53
+ Metric category.
54
+
55
+ Returns:
56
+ str: One of: 'embedding', 'probability', 'generated_text',
57
+ or 'prompt_based'
58
+ """
59
+ raise NotImplementedError
60
+
61
+
62
+ class EmbeddingMetric(BiasMetric):
63
+ """
64
+ Base class for embedding-based bias metrics.
65
+
66
+ Provides common validation methods for embeddings.
67
+ """
68
+
69
+ @property
70
+ def category(self) -> str:
71
+ """Category is automatically set to 'embedding'."""
72
+ return "embedding"
73
+
74
+ def _validate_embeddings(
75
+ self, embeddings: np.ndarray, name: str
76
+ ) -> None:
77
+ """
78
+ Validate embedding array structure (PRIVATE).
79
+
80
+ Args:
81
+ embeddings (np.ndarray): Embedding array to validate.
82
+ name (str): name for error messages
83
+
84
+ Raises:
85
+ ValueError: If validation fails
86
+ """
87
+ if len(embeddings) == 0:
88
+ raise ValueError(f"{name} cannot be empty")
89
+
90
+ if np.isnan(embeddings).any():
91
+ raise ValueError(f"{name} contains NaN values")
92
+
93
+ if np.isinf(embeddings).any():
94
+ raise ValueError(f"{name} contains Inf values")
95
+
96
+
97
+ class ProbabilityMetric(BiasMetric):
98
+ """
99
+ Base class for probability-based bias metrics.
100
+
101
+ Provides common validation methods for probabilities and sentence pairs.
102
+ All probability-based metrics (CrowS-Pairs, CAT, AUL, iCAT, AULA, LMB)
103
+ should inherit from this class.
104
+ """
105
+
106
+ @property
107
+ def category(self) -> str:
108
+ """Category is automatically set to 'probability'."""
109
+ return "probability"
110
+
111
+ def _validate_probabilities(
112
+ self, probabilities: np.ndarray, name: str = "probabilities"
113
+ ) -> None:
114
+ """
115
+ Validate probability array (PRIVATE helper).
116
+
117
+ Checks that probabilities are valid: in [0,1], no NaN/Inf.
118
+
119
+ Args:
120
+ probabilities (np.ndarray): Probability array to validate
121
+ name (str): Name for error messages (default: "probabilities")
122
+
123
+ Raises:
124
+ ValueError: If probabilities are invalid
125
+ """
126
+ if len(probabilities) == 0:
127
+ raise ValueError(f"{name} cannot be empty")
128
+
129
+ if np.isnan(probabilities).any():
130
+ raise ValueError(f"{name} contains NaN values")
131
+
132
+ if np.isinf(probabilities).any():
133
+ raise ValueError(f"{name} contains Inf values")
134
+
135
+ if (probabilities < 0).any() or (probabilities > 1).any():
136
+ raise ValueError(
137
+ f"{name} must be in range [0, 1]. "
138
+ f"Got min={np.min(probabilities)}, max={np.max(probabilities)}"
139
+ )
140
+
141
+ def _validate_sentence_pair(
142
+ self, sentence1: List[str], sentence2: List[str]
143
+ ) -> None:
144
+ """
145
+ Validate sentence pair has same length (PRIVATE).
146
+
147
+ Args:
148
+ sentence1 (List[str]): first tokenized sentence
149
+ sentence2 (List[str]): second tokenized sentence
150
+
151
+ Raises:
152
+ ValueError: If validation fails
153
+ """
154
+ if len(sentence1) == 0 or len(sentence2) == 0:
155
+ raise ValueError("Sentences cannot be empty")
156
+
157
+ if len(sentence1) != len(sentence2):
158
+ raise ValueError(
159
+ "Sentence pairs must have same length. "
160
+ f"Got {len(sentence1)} and {len(sentence2)} tokens."
161
+ )
162
+
163
+
164
+ class GeneratedTextMetric(BiasMetric):
165
+ """
166
+ Base class for generated text bias metrics.
167
+ """
168
+
169
+ @property
170
+ def category(self) -> str:
171
+ """Category is automatically set to 'generated_text'."""
172
+ return "generated_text"
173
+
174
+ def _validate_texts(self, texts: Sequence[str], name: str) -> List[str]:
175
+ """
176
+ Validate a sequence of text strings (PRIVATE).
177
+
178
+ Args:
179
+ texts (Sequence[str]): Text values to validate.
180
+ name (str): Input name for error messages.
181
+
182
+ Returns:
183
+ List[str]: Validated text list.
184
+
185
+ Raises:
186
+ TypeError: If texts is not a sequence of strings.
187
+ ValueError: If texts is empty or contains empty strings.
188
+ """
189
+ if not isinstance(texts, Sequence) or isinstance(texts, (str, bytes)):
190
+ raise TypeError(f"{name} must be a Sequence of strings")
191
+
192
+ texts_list = list(texts)
193
+ if len(texts_list) == 0:
194
+ raise ValueError(f"{name} cannot be empty")
195
+
196
+ for i, text in enumerate(texts_list):
197
+ if not isinstance(text, str):
198
+ raise TypeError(
199
+ f"{name}[{i}] must be a string, got {type(text).__name__}"
200
+ )
201
+ if text == "":
202
+ raise ValueError(f"{name}[{i}] cannot be empty")
203
+
204
+ return texts_list
205
+
206
+ def _validate_callable(self, fn: Callable, name: str) -> None:
207
+ """
208
+ Validate a callable input (PRIVATE).
209
+
210
+ Args:
211
+ fn (Callable): Function to validate.
212
+ name (str): Input name for error messages.
213
+
214
+ Raises:
215
+ TypeError: If fn is not callable.
216
+ """
217
+ if not callable(fn):
218
+ raise TypeError(f"{name} must be callable, got {type(fn).__name__}")
219
+
220
+ def _validate_finite_float(self, value: float, name: str) -> float:
221
+ """
222
+ Validate a finite float value (PRIVATE).
223
+
224
+ Args:
225
+ value (float): Value to validate.
226
+ name (str): Input name for error messages.
227
+
228
+ Returns:
229
+ float: Validated float value.
230
+
231
+ Raises:
232
+ TypeError: If value is not numeric.
233
+ ValueError: If value is NaN or infinite.
234
+ """
235
+ if not isinstance(value, (int, float, np.floating)):
236
+ raise TypeError(f"{name} must be a float, got {type(value).__name__}")
237
+
238
+ value_float = float(value)
239
+ if np.isnan(value_float) or np.isinf(value_float):
240
+ raise ValueError(f"{name} must be finite, got {value}")
241
+ return value_float
242
+
243
+ def _validate_generated_texts(
244
+ self, texts: List[List[str]], name: str = "texts"
245
+ ) -> None:
246
+ """
247
+ Validate a list-of-lists of generated texts (PRIVATE).
248
+
249
+ Args:
250
+ texts (List[List[str]]): Nested text list to validate.
251
+ Shape: (n_prompts, n_texts_per_prompt)
252
+ name (str): Argument name used in error messages.
253
+
254
+ Raises:
255
+ ValueError: If the outer list is empty.
256
+ ValueError: If any inner list is empty.
257
+ """
258
+ if len(texts) == 0:
259
+ raise ValueError(f"{name} cannot be empty")
260
+
261
+ for inner in texts:
262
+ if len(inner) == 0:
263
+ raise ValueError(f"{name} cannot be empty")
264
+
265
+ def _validate_completions(
266
+ self, completions: List[List[str]], name: str = "completions"
267
+ ) -> None:
268
+ """
269
+ Validate nested completion lists (PRIVATE).
270
+
271
+ Args:
272
+ completions (List[List[str]]): Generated completions grouped by prompt.
273
+ name (str): Argument name used in error messages.
274
+
275
+ Raises:
276
+ TypeError: If completions is not a nested list of strings.
277
+ ValueError: If completions is empty or contains empty groups/strings.
278
+ """
279
+ self._validate_generated_texts(completions, name)
280
+ expected_length = len(completions[0])
281
+
282
+ for i, group in enumerate(completions):
283
+ if len(group) != expected_length:
284
+ raise ValueError(
285
+ f"{name} must have the same number of completions per group. "
286
+ f"Expected {expected_length}, got {len(group)} at index {i}."
287
+ )
288
+ for j, completion in enumerate(group):
289
+ if not isinstance(completion, str):
290
+ raise TypeError(
291
+ f"{name}[{i}][{j}] must be a string, got {type(completion).__name__}"
292
+ )
293
+ if completion == "":
294
+ raise ValueError(f"{name}[{i}][{j}] cannot be empty")
295
+
296
+ def _validate_threshold(self, threshold: float, name: str = "threshold") -> float:
297
+ """
298
+ Validate a threshold in [0, 1] (PRIVATE).
299
+
300
+ Args:
301
+ threshold (float): Threshold value to validate.
302
+ name (str): Argument name used in error messages.
303
+
304
+ Returns:
305
+ float: Validated threshold as float.
306
+
307
+ Raises:
308
+ TypeError: If threshold is not numeric.
309
+ ValueError: If threshold is outside [0, 1].
310
+ """
311
+ if not isinstance(threshold, (int, float, np.floating)):
312
+ raise TypeError(f"{name} must be numeric, got {type(threshold).__name__}")
313
+
314
+ threshold_value = float(threshold)
315
+ if not 0.0 <= threshold_value <= 1.0:
316
+ raise ValueError(f"{name} must be in [0, 1]. Got {threshold}")
317
+
318
+ return threshold_value
319
+
320
+ def _validate_classifier_scores(
321
+ self, scores: Sequence[float], name: str = "scores"
322
+ ) -> None:
323
+ """
324
+ Validate classifier scores in [0, 1] (PRIVATE).
325
+
326
+ Args:
327
+ scores (Sequence[float]): Scores to validate.
328
+ name (str): Argument name used in error messages.
329
+
330
+ Raises:
331
+ ValueError: If scores is empty or contains invalid values.
332
+ """
333
+ if len(scores) == 0:
334
+ raise ValueError(f"{name} cannot be empty")
335
+
336
+ for i, score in enumerate(scores):
337
+ if not isinstance(score, (int, float, np.floating)):
338
+ raise ValueError(
339
+ f"{name}[{i}] must be numeric, got {type(score).__name__}"
340
+ )
341
+ value = float(score)
342
+ if np.isnan(value) or np.isinf(value):
343
+ raise ValueError(f"{name}[{i}] must be finite. Got {score}")
344
+ if not 0.0 <= value <= 1.0:
345
+ raise ValueError(f"{name}[{i}] must be in [0, 1]. Got {score}")
346
+
347
+ def _validate_paired_completions(
348
+ self,
349
+ group_a_completions: List[List[str]],
350
+ group_b_completions: List[List[str]],
351
+ ) -> None:
352
+ """
353
+ Validate paired completion matrices are shape-aligned (PRIVATE).
354
+
355
+ Args:
356
+ group_a_completions (List[List[str]]): First completion matrix.
357
+ group_b_completions (List[List[str]]): Second completion matrix.
358
+
359
+ Raises:
360
+ ValueError: If the two matrices are not shape-aligned.
361
+ """
362
+ if len(group_a_completions) != len(group_b_completions):
363
+ raise ValueError(
364
+ "group_a_completions and group_b_completions must have the same number of templates. "
365
+ f"Got {len(group_a_completions)} and {len(group_b_completions)}."
366
+ )
367
+ for i, (a_template, b_template) in enumerate(
368
+ zip(group_a_completions, group_b_completions)
369
+ ):
370
+ if len(a_template) != len(b_template):
371
+ raise ValueError(
372
+ "group_a_completions and group_b_completions must be shape-aligned. "
373
+ f"Template index {i} has K={len(a_template)} vs K={len(b_template)}."
374
+ )
375
+
376
+ def _validate_and_cast_scores(
377
+ self,
378
+ completions: List[List[str]],
379
+ scores: List[List[float]] | None = None,
380
+ name: str = "toxicity_scores",
381
+ sentiment_scores: List[List[float]] | None = None,
382
+ ) -> np.ndarray:
383
+ """
384
+ Validate nested score matrices against completion shape (PRIVATE).
385
+
386
+ Supports both generic paired scores in [-1, 1] and toxicity scores
387
+ in [0, 1] while preserving existing metric call signatures.
388
+
389
+ Args:
390
+ completions (List[List[str]]): Completion matrix to align against.
391
+ scores (List[List[float]] | None): Generic score matrix.
392
+ name (str): Score matrix name for error messages.
393
+ sentiment_scores (List[List[float]] | None): Sentiment score matrix.
394
+
395
+ Returns:
396
+ np.ndarray: Validated score matrix as float array.
397
+
398
+ Raises:
399
+ ValueError: If score inputs are missing, misaligned, or invalid.
400
+ """
401
+ if scores is not None and sentiment_scores is not None:
402
+ raise ValueError(
403
+ "Provide either scores or sentiment_scores, not both."
404
+ )
405
+
406
+ score_matrix = sentiment_scores if sentiment_scores is not None else scores
407
+ if score_matrix is None:
408
+ raise ValueError("scores cannot be None")
409
+
410
+ if sentiment_scores is not None:
411
+ min_value = -1.0
412
+ max_value = 1.0
413
+ item_prefix = name
414
+ elif name == "toxicity_scores":
415
+ min_value = 0.0
416
+ max_value = 1.0
417
+ item_prefix = "toxicity score"
418
+ else:
419
+ min_value = -1.0
420
+ max_value = 1.0
421
+ item_prefix = name
422
+
423
+ if len(score_matrix) != len(completions):
424
+ raise ValueError(
425
+ f"{name} must have the same number of templates as its completions. "
426
+ f"Got {len(score_matrix)} and {len(completions)}."
427
+ )
428
+
429
+ rows: List[List[float]] = []
430
+ for i, (template_completions, template_scores) in enumerate(
431
+ zip(completions, score_matrix)
432
+ ):
433
+ if len(template_scores) != len(template_completions):
434
+ raise ValueError(
435
+ f"{name} must match completions shape. "
436
+ f"Template index {i} has K={len(template_completions)} completions "
437
+ f"but {len(template_scores)} "
438
+ f"{'toxicity scores' if name == 'toxicity_scores' else 'scores'}."
439
+ )
440
+
441
+ casted_template: List[float] = []
442
+ for j, score in enumerate(template_scores):
443
+ if not isinstance(score, (int, float, np.floating)):
444
+ raise ValueError(
445
+ f"{item_prefix} at [{i}][{j}] must be numeric, got {type(score)}"
446
+ )
447
+ value = float(score)
448
+ if np.isnan(value) or np.isinf(value):
449
+ raise ValueError(f"{item_prefix} at [{i}][{j}] is invalid: {value}")
450
+ if value < min_value or value > max_value:
451
+ raise ValueError(
452
+ f"{item_prefix} at [{i}][{j}] must be in "
453
+ f"[{min_value:g}, {max_value:g}], got {value}"
454
+ )
455
+ casted_template.append(value)
456
+ rows.append(casted_template)
457
+
458
+ return np.array(rows, dtype=float)
459
+
460
+
461
+ class PromptBasedMetric(BiasMetric):
462
+
463
+ @property
464
+ def category(self) -> str:
465
+ return "prompt_based"
466
+
467
+ def _validate_positive_int(self, value: int, name: str) -> int:
468
+ """
469
+ Validate a positive integer (PRIVATE).
470
+
471
+ Args:
472
+ value (int): Value to validate.
473
+ name (str): Argument name used in error messages.
474
+
475
+ Returns:
476
+ int: Validated integer.
477
+
478
+ Raises:
479
+ ValueError: If value is not a positive integer.
480
+ """
481
+ if not isinstance(value, int) or value <= 0:
482
+ raise ValueError(f"{name} must be a positive integer. Got {value}")
483
+ return value
@@ -0,0 +1,9 @@
1
+ """Embedding-based bias metrics."""
2
+
3
+ from bias_scope.embeddings_based.ceat import CEAT
4
+ from bias_scope.embeddings_based.seat import SEAT
5
+ from bias_scope.embeddings_based.sentence_bias_score import SentenceBiasScore
6
+ from bias_scope.embeddings_based.weat import WEAT
7
+
8
+ # Public API - classes only
9
+ __all__ = ["WEAT", "SEAT", "CEAT", "SentenceBiasScore"]