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.
- bias_scope/__init__.py +178 -0
- bias_scope/base.py +483 -0
- bias_scope/embeddings_based/__init__.py +9 -0
- bias_scope/embeddings_based/_helpers.py +135 -0
- bias_scope/embeddings_based/ceat.py +273 -0
- bias_scope/embeddings_based/seat.py +107 -0
- bias_scope/embeddings_based/sentence_bias_score.py +320 -0
- bias_scope/embeddings_based/weat.py +187 -0
- bias_scope/generated_text_based/__init__.py +50 -0
- bias_scope/generated_text_based/_helpers.py +301 -0
- bias_scope/generated_text_based/cooccurrence_bias_score.py +323 -0
- bias_scope/generated_text_based/counterfactual_sentiment_bias.py +78 -0
- bias_scope/generated_text_based/demographic_representation.py +270 -0
- bias_scope/generated_text_based/emt.py +64 -0
- bias_scope/generated_text_based/fgb.py +76 -0
- bias_scope/generated_text_based/gender_polarity.py +163 -0
- bias_scope/generated_text_based/honest.py +138 -0
- bias_scope/generated_text_based/marked_persons.py +262 -0
- bias_scope/generated_text_based/perspective_api.py +219 -0
- bias_scope/generated_text_based/pgb.py +75 -0
- bias_scope/generated_text_based/psycholinguistic_norms.py +223 -0
- bias_scope/generated_text_based/regard_score.py +250 -0
- bias_scope/generated_text_based/score_parity.py +208 -0
- bias_scope/generated_text_based/social_group_substitution.py +396 -0
- bias_scope/generated_text_based/stereotypical_associations.py +296 -0
- bias_scope/generated_text_based/toxicity_fraction.py +191 -0
- bias_scope/generated_text_based/toxicity_probability.py +137 -0
- bias_scope/probability_based/__init__.py +33 -0
- bias_scope/probability_based/_helpers.py +117 -0
- bias_scope/probability_based/_scorers.py +100 -0
- bias_scope/probability_based/aul.py +171 -0
- bias_scope/probability_based/aula.py +282 -0
- bias_scope/probability_based/cat.py +187 -0
- bias_scope/probability_based/cbs.py +297 -0
- bias_scope/probability_based/crows_pairs.py +196 -0
- bias_scope/probability_based/disco.py +151 -0
- bias_scope/probability_based/icat.py +131 -0
- bias_scope/probability_based/lmb.py +489 -0
- bias_scope/probability_based/lpbs.py +203 -0
- bias_scope/prompts_based/__init__.py +74 -0
- bias_scope/prompts_based/analogical_reasoning_bias.py +396 -0
- bias_scope/prompts_based/bbq.py +286 -0
- bias_scope/prompts_based/bold.py +267 -0
- bias_scope/prompts_based/counterfactual_fairness.py +279 -0
- bias_scope/prompts_based/demographic_representation_bias.py +269 -0
- bias_scope/prompts_based/opinion_consistency_across_personas.py +280 -0
- bias_scope/prompts_based/realtoxicityprompts.py +246 -0
- bias_scope/prompts_based/stereoset.py +431 -0
- bias_scope/prompts_based/tof_nof.py +338 -0
- bias_scope/prompts_based/truthfulqa.py +336 -0
- bias_scope/prompts_based/unqover.py +683 -0
- bias_scope/utils.py +92 -0
- bias_scope-0.1.0.dist-info/METADATA +32 -0
- bias_scope-0.1.0.dist-info/RECORD +55 -0
- 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"]
|