granuscore 1.0.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.
- granuscore/__init__.py +13 -0
- granuscore/artifcats.py +116 -0
- granuscore/bucket_output.py +53 -0
- granuscore/cache.py +189 -0
- granuscore/claim_splitter.py +300 -0
- granuscore/default_assets.py +38 -0
- granuscore/granularity_predictor.py +796 -0
- granuscore/loader.py +118 -0
- granuscore/pipeline.py +562 -0
- granuscore/text_cleaner.py +61 -0
- granuscore/utils.py +48 -0
- granuscore-1.0.0.dist-info/METADATA +242 -0
- granuscore-1.0.0.dist-info/RECORD +16 -0
- granuscore-1.0.0.dist-info/WHEEL +5 -0
- granuscore-1.0.0.dist-info/licenses/LICENSE +21 -0
- granuscore-1.0.0.dist-info/top_level.txt +1 -0
granuscore/loader.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Module for reading files."""
|
|
2
|
+
import csv
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Reader:
|
|
9
|
+
"""General file reader."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, encoding="utf-8"):
|
|
12
|
+
self.enc = encoding
|
|
13
|
+
|
|
14
|
+
def read(self, file):
|
|
15
|
+
"""Read a file."""
|
|
16
|
+
|
|
17
|
+
# if already an open file object:
|
|
18
|
+
if hasattr(file, "read"):
|
|
19
|
+
return self.process(file)
|
|
20
|
+
|
|
21
|
+
# otherwise treat as file path:
|
|
22
|
+
path = Path(file)
|
|
23
|
+
if not path.is_file():
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
with open(file, "r", encoding=self.enc) as f:
|
|
27
|
+
return self.process(f)
|
|
28
|
+
|
|
29
|
+
def write(self, file, lines, mode='a'):
|
|
30
|
+
"""Write lines to file."""
|
|
31
|
+
if os.path.dirname(file):
|
|
32
|
+
os.makedirs(os.path.dirname(file), exist_ok=True)
|
|
33
|
+
with open(file, mode, encoding=self.enc) as f:
|
|
34
|
+
self._write(f, lines)
|
|
35
|
+
|
|
36
|
+
def _write(self, file, lines):
|
|
37
|
+
"""Write lines to an opened file."""
|
|
38
|
+
|
|
39
|
+
def process(self, file):
|
|
40
|
+
"""Process an opened file."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class JSONLineReader(Reader):
|
|
44
|
+
"""Reader for .jsonl files."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, pretty_print: bool = False):
|
|
47
|
+
super().__init__()
|
|
48
|
+
self.pretty_print = pretty_print
|
|
49
|
+
|
|
50
|
+
def process(self, file):
|
|
51
|
+
"""Read each line as json object."""
|
|
52
|
+
data = []
|
|
53
|
+
for line in file.readlines():
|
|
54
|
+
try:
|
|
55
|
+
data.append(json.loads(line.strip()))
|
|
56
|
+
except json.decoder.JSONDecodeError:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
return data
|
|
60
|
+
|
|
61
|
+
def _write(self, file, lines):
|
|
62
|
+
for line in lines:
|
|
63
|
+
if self.pretty_print:
|
|
64
|
+
json.dump(line, file, indent=2, ensure_ascii=False)
|
|
65
|
+
else:
|
|
66
|
+
json.dump(line, file, ensure_ascii=False)
|
|
67
|
+
file.write('\n')
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class JSONReader(Reader):
|
|
71
|
+
"""Reader for .json files."""
|
|
72
|
+
|
|
73
|
+
def __init__(self, pretty_print: bool = True):
|
|
74
|
+
super().__init__()
|
|
75
|
+
self.pretty_print = pretty_print
|
|
76
|
+
|
|
77
|
+
def process(self, file):
|
|
78
|
+
"""Read file as json object."""
|
|
79
|
+
return json.load(file)
|
|
80
|
+
|
|
81
|
+
def _write(self, file, dictionary):
|
|
82
|
+
if self.pretty_print:
|
|
83
|
+
json.dump(dictionary, file, indent=2)
|
|
84
|
+
else:
|
|
85
|
+
json.dump(dictionary, file)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class LineReader(Reader):
|
|
89
|
+
"""Line reader for files."""
|
|
90
|
+
|
|
91
|
+
def process(self, file):
|
|
92
|
+
"""Read each line as an entry in a list."""
|
|
93
|
+
data = []
|
|
94
|
+
for line in file.readlines():
|
|
95
|
+
data.append(line)
|
|
96
|
+
|
|
97
|
+
return data
|
|
98
|
+
|
|
99
|
+
def _write(self, file, lines):
|
|
100
|
+
for line in lines:
|
|
101
|
+
file.write(line)
|
|
102
|
+
file.write('\n')
|
|
103
|
+
|
|
104
|
+
class TSVReader(Reader):
|
|
105
|
+
"""Reader for .tsv files."""
|
|
106
|
+
|
|
107
|
+
def __init__(self, delimiter='\t'):
|
|
108
|
+
super().__init__()
|
|
109
|
+
self.delimiter = delimiter
|
|
110
|
+
|
|
111
|
+
def process(self, file):
|
|
112
|
+
"""Read each row of the TSV as a list of values."""
|
|
113
|
+
reader = csv.reader(file, delimiter=self.delimiter)
|
|
114
|
+
return [row for row in reader]
|
|
115
|
+
|
|
116
|
+
def _write(self, file, rows):
|
|
117
|
+
writer = csv.writer(file, delimiter=self.delimiter)
|
|
118
|
+
writer.writerows(rows)
|
granuscore/pipeline.py
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Iterable, Literal, Any, Callable
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import torch
|
|
6
|
+
from tqdm import tqdm
|
|
7
|
+
|
|
8
|
+
from granuscore.claim_splitter import UnitSplitter, SpacyNounPhraseSplitter
|
|
9
|
+
from granuscore.granularity_predictor import (
|
|
10
|
+
HitGranularityPredictor,
|
|
11
|
+
SentenceTransformerGranularityPredictor,
|
|
12
|
+
LLMGranularityPredictor,
|
|
13
|
+
WordNetGranularityPredictor,
|
|
14
|
+
LengthGranularityPredictor,
|
|
15
|
+
PredictorType
|
|
16
|
+
)
|
|
17
|
+
from granuscore.utils import iter_batches
|
|
18
|
+
|
|
19
|
+
Pooling = Literal["sum", "mean", "lower_quantile_mean", "min", "max", "weighted_mean"]
|
|
20
|
+
PoolingScope = Literal["document", "sentence"]
|
|
21
|
+
PREDICTOR_REGISTRY = {
|
|
22
|
+
"hit": HitGranularityPredictor,
|
|
23
|
+
"sentence_transformer": SentenceTransformerGranularityPredictor,
|
|
24
|
+
"llm": LLMGranularityPredictor,
|
|
25
|
+
"wordnet": WordNetGranularityPredictor,
|
|
26
|
+
"length": LengthGranularityPredictor,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class PreSplitText:
|
|
32
|
+
"""
|
|
33
|
+
Container for pre-split referential units.
|
|
34
|
+
|
|
35
|
+
This can be used to bypass Granuscore's automatic claim splitting
|
|
36
|
+
and directly score a provided decomposition.
|
|
37
|
+
|
|
38
|
+
Attributes
|
|
39
|
+
----------
|
|
40
|
+
units:
|
|
41
|
+
A list of pre-extracted referential units.
|
|
42
|
+
text:
|
|
43
|
+
Original text.
|
|
44
|
+
"""
|
|
45
|
+
referential_units: list[str]
|
|
46
|
+
text: str
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class GranuScore:
|
|
50
|
+
"""
|
|
51
|
+
End-to-end granularity scoring pipeline.
|
|
52
|
+
|
|
53
|
+
GranuScore splits text into referential units, assigns a
|
|
54
|
+
continuous granularity score to each unit, and aggregates these
|
|
55
|
+
scores into a single text-level score.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
predictor_type: PredictorType = "hit",
|
|
61
|
+
claim_splitter: UnitSplitter | None = None,
|
|
62
|
+
**predictor_kwargs,
|
|
63
|
+
):
|
|
64
|
+
"""
|
|
65
|
+
Initialize the Granuscore pipeline.
|
|
66
|
+
|
|
67
|
+
All parameters are optional and only need to be provided if you want
|
|
68
|
+
to customize parts of the pipeline. By default, Granuscore uses the settings from the paper.
|
|
69
|
+
|
|
70
|
+
Default configuration
|
|
71
|
+
---------------------
|
|
72
|
+
- predictor_type: ``"hit"``
|
|
73
|
+
- model_name: ``"Hierarchy-Transformers/HiT-MiniLM-L12-WordNetNoun"``
|
|
74
|
+
- search_method: ``"random_anchors"``
|
|
75
|
+
- random_anchors_k: ``999``
|
|
76
|
+
- use_cache: ``True``
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
predictor_type:
|
|
81
|
+
Optional predictor_type.
|
|
82
|
+
model_name:
|
|
83
|
+
Optional model_name.
|
|
84
|
+
claim_splitter:
|
|
85
|
+
Optional component used to split text into referential units.
|
|
86
|
+
Defaults to a spaCy-based noun phrase splitter.
|
|
87
|
+
**predictor_kwargs:
|
|
88
|
+
Additional optional parameters passed to predictor.
|
|
89
|
+
"""
|
|
90
|
+
self.claim_splitter = claim_splitter or SpacyNounPhraseSplitter()
|
|
91
|
+
|
|
92
|
+
if predictor_type not in PREDICTOR_REGISTRY:
|
|
93
|
+
raise ValueError(f"Unknown predictor_type '{predictor_type}'.")
|
|
94
|
+
else:
|
|
95
|
+
predictor_cls = PREDICTOR_REGISTRY[predictor_type]
|
|
96
|
+
self.granu_predictor = predictor_cls(**predictor_kwargs)
|
|
97
|
+
self.percentile_output = self.granu_predictor.percentile_output
|
|
98
|
+
|
|
99
|
+
def __call__(
|
|
100
|
+
self,
|
|
101
|
+
answers: str | list[str] | np.ndarray,
|
|
102
|
+
split: bool = True,
|
|
103
|
+
pooling: Pooling = "mean",
|
|
104
|
+
pooling_scope: PoolingScope = "sentence",
|
|
105
|
+
scope_pooling_method: Pooling = "lower_quantile_mean",
|
|
106
|
+
return_details: bool = False,
|
|
107
|
+
return_buckets: bool = False,
|
|
108
|
+
return_percentiles: bool = True,
|
|
109
|
+
percentile_before_pooling: bool = True,
|
|
110
|
+
batched: bool = True,
|
|
111
|
+
batch_size: int = 128,
|
|
112
|
+
tail_q: float = 0.5,
|
|
113
|
+
scope_tail_q: float = 0.8,
|
|
114
|
+
show_progress_bar: bool = False,
|
|
115
|
+
convert_to_numpy: bool = True,
|
|
116
|
+
convert_to_tensor: bool = False,
|
|
117
|
+
encoding_batch_size: int | None = 128,
|
|
118
|
+
) -> str | float | list[float] | list[str] | np.ndarray | torch.Tensor | dict | list[dict]:
|
|
119
|
+
"""
|
|
120
|
+
Shorthand for :meth:`predict`.
|
|
121
|
+
|
|
122
|
+
Allows calling the instance directly as a function.
|
|
123
|
+
|
|
124
|
+
Parameters
|
|
125
|
+
----------
|
|
126
|
+
answers:
|
|
127
|
+
One or more input texts.
|
|
128
|
+
split:
|
|
129
|
+
Whether to split texts into referential units before scoring.
|
|
130
|
+
pooling:
|
|
131
|
+
Aggregation strategy for referential scores ("mean", "min", "lower_quantile_mean", "sum").
|
|
132
|
+
"lower_quantile_mean" selects the lowest ``tail_q`` fraction of referential scores
|
|
133
|
+
(the lower tail of the distribution) and returns the mean score within this
|
|
134
|
+
subset.
|
|
135
|
+
pooling_scope:
|
|
136
|
+
Defines the structural scope over which referential units are grouped
|
|
137
|
+
prior to aggregation.
|
|
138
|
+
"document" pools over all referential units in the text.
|
|
139
|
+
"sentence" applies the specified ``pooling`` strategy independently
|
|
140
|
+
within each sentence, followed by aggregation over sentence-level
|
|
141
|
+
scores.
|
|
142
|
+
scope_pooling_method:
|
|
143
|
+
Aggregation strategy applied over structural scopes when
|
|
144
|
+
``pooling_scope`` is not ``"document"``. For example, when
|
|
145
|
+
``pooling_scope="sentence"``, this parameter controls how
|
|
146
|
+
sentence-level scores are aggregated into a single text-level score.
|
|
147
|
+
Defaults to ``"lower_quantile_mean"``.
|
|
148
|
+
return_details:
|
|
149
|
+
Whether to return detailed per-claim scores.
|
|
150
|
+
return_buckets:
|
|
151
|
+
Whether to output the score as text interpretation. Default False.
|
|
152
|
+
return_percentiles:
|
|
153
|
+
Whether to output the score as in percentiles. Default True.
|
|
154
|
+
percentile_before_pooling:
|
|
155
|
+
Whether you want to work with percentiles before pooling operations. Default True.
|
|
156
|
+
batched:
|
|
157
|
+
If True, process in chunks of batch_size.
|
|
158
|
+
batch_size:
|
|
159
|
+
Chunk size used when batched=True.
|
|
160
|
+
tail_q:
|
|
161
|
+
Lower-tail fraction used by ``pooling="lower_quantile_mean"``. Must be in (0, 1]. Defaults to 0.1.
|
|
162
|
+
scope_tail_q:
|
|
163
|
+
Lower-tail fraction used by ``scope_pooling_method="lower_quantile_mean"``. Must be in (0, 1]. Defaults to 0.8.
|
|
164
|
+
show_progress_bar:
|
|
165
|
+
If True, display a progress bar over input batches.
|
|
166
|
+
convert_to_numpy:
|
|
167
|
+
If True, convert returned scores to NumPy types. For non-detailed output,
|
|
168
|
+
this yields a ``numpy.ndarray`` (or ``numpy.float32`` for single input).
|
|
169
|
+
For detailed output, score values are converted to NumPy scalars.
|
|
170
|
+
convert_to_tensor:
|
|
171
|
+
If True, convert returned scores to PyTorch tensors. For non-detailed
|
|
172
|
+
output, this yields a 1D ``torch.Tensor`` (or 0D tensor for single input).
|
|
173
|
+
For detailed output, score values are converted to scalar tensors.
|
|
174
|
+
encoding_batch_size:
|
|
175
|
+
Maximum number of texts to encode simultaneously in the underlying model.
|
|
176
|
+
If None, no batching limit is applied. Use lower values if experiencing OOM errors.
|
|
177
|
+
|
|
178
|
+
Returns
|
|
179
|
+
-------
|
|
180
|
+
str | float | list[float] | list[str] | np.ndarray | torch.tensor | dict | list[dict]
|
|
181
|
+
Granularity score(s), optionally with detailed information.
|
|
182
|
+
"""
|
|
183
|
+
return self.predict(
|
|
184
|
+
answers=answers,
|
|
185
|
+
split=split,
|
|
186
|
+
pooling=pooling,
|
|
187
|
+
pooling_scope=pooling_scope,
|
|
188
|
+
scope_pooling_method=scope_pooling_method,
|
|
189
|
+
return_details=return_details,
|
|
190
|
+
return_buckets=return_buckets,
|
|
191
|
+
return_percentiles=return_percentiles,
|
|
192
|
+
percentile_before_pooling=percentile_before_pooling,
|
|
193
|
+
batched=batched,
|
|
194
|
+
batch_size=batch_size,
|
|
195
|
+
tail_q=tail_q,
|
|
196
|
+
scope_tail_q=scope_tail_q,
|
|
197
|
+
show_progress_bar=show_progress_bar,
|
|
198
|
+
convert_to_numpy=convert_to_numpy,
|
|
199
|
+
convert_to_tensor=convert_to_tensor,
|
|
200
|
+
encoding_batch_size=encoding_batch_size
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
@staticmethod
|
|
204
|
+
def _validate_pooling(pooling: Pooling) -> None:
|
|
205
|
+
if pooling not in {"sum", "mean", "lower_quantile_mean", "min", "max", "weighted_mean"}:
|
|
206
|
+
raise ValueError('pooling must be either "sum", "mean", "weighted_mean", "lower_quantile_mean", "min", or "max"')
|
|
207
|
+
|
|
208
|
+
@staticmethod
|
|
209
|
+
def _validate_batching(batched: bool, batch_size: int) -> None:
|
|
210
|
+
if not isinstance(batched, bool):
|
|
211
|
+
raise TypeError("batched must be a bool")
|
|
212
|
+
if batch_size <= 0:
|
|
213
|
+
raise ValueError("batch_size must be > 0")
|
|
214
|
+
|
|
215
|
+
@staticmethod
|
|
216
|
+
def _validate_encoding_batch_size(encoding_batch_size: int) -> None:
|
|
217
|
+
if isinstance(encoding_batch_size, int) and encoding_batch_size <= 0:
|
|
218
|
+
raise ValueError("batch_size must be > 0")
|
|
219
|
+
|
|
220
|
+
def _pool(self, scores: Iterable[float], pooling: Pooling, tail_q: float, weights: np.ndarray | None = None) -> float:
|
|
221
|
+
scores_list = list(scores)
|
|
222
|
+
if pooling == "sum":
|
|
223
|
+
return float(sum(scores_list))
|
|
224
|
+
if pooling == "min":
|
|
225
|
+
return float(min(scores_list))
|
|
226
|
+
if pooling == "max":
|
|
227
|
+
return float(max(scores_list))
|
|
228
|
+
if pooling == "lower_quantile_mean":
|
|
229
|
+
if not (0.0 < tail_q <= 1.0):
|
|
230
|
+
raise ValueError("tail_q must be in (0, 1].")
|
|
231
|
+
scores_sorted = sorted(scores_list)
|
|
232
|
+
k = max(1, int(np.ceil(len(scores_sorted) * tail_q)))
|
|
233
|
+
lower_tail = scores_sorted[:k]
|
|
234
|
+
return float(np.mean(lower_tail))
|
|
235
|
+
if pooling == "weighted_mean":
|
|
236
|
+
assert weights is not None
|
|
237
|
+
return float(np.average(scores_list, weights=weights)) if scores_list else float(self.granu_predictor.NO_FACT_SCORE)
|
|
238
|
+
|
|
239
|
+
return float(np.mean(scores_list)) if scores_list else float(self.granu_predictor.NO_FACT_SCORE)
|
|
240
|
+
|
|
241
|
+
@staticmethod
|
|
242
|
+
def _conversion_functions(convert_to_tensor: bool, convert_to_numpy: bool) -> tuple[Callable, Any]:
|
|
243
|
+
if convert_to_tensor:
|
|
244
|
+
def to_scalar(x: float):
|
|
245
|
+
# 0-d tensor scalar
|
|
246
|
+
return torch.tensor(float(x), dtype=torch.float32)
|
|
247
|
+
|
|
248
|
+
def finalize_vector(xs: list[float]):
|
|
249
|
+
return torch.tensor([float(v) for v in xs], dtype=torch.float32)
|
|
250
|
+
|
|
251
|
+
elif convert_to_numpy:
|
|
252
|
+
def to_scalar(x: float):
|
|
253
|
+
return np.float32(x)
|
|
254
|
+
|
|
255
|
+
def finalize_vector(xs: list[float]):
|
|
256
|
+
return np.asarray(xs, dtype=np.float32)
|
|
257
|
+
|
|
258
|
+
else:
|
|
259
|
+
def to_scalar(x: float):
|
|
260
|
+
return float(x)
|
|
261
|
+
|
|
262
|
+
def finalize_vector(xs: list[float]):
|
|
263
|
+
return xs
|
|
264
|
+
return to_scalar, finalize_vector
|
|
265
|
+
|
|
266
|
+
def _process_batch_of_facts(
|
|
267
|
+
self,
|
|
268
|
+
texts: list[str],
|
|
269
|
+
facts_per_text: list[list[dict[str, Any]]],
|
|
270
|
+
pooling: Pooling,
|
|
271
|
+
scope_pooling_method: Pooling,
|
|
272
|
+
tail_q: float,
|
|
273
|
+
scope_tail_q: float,
|
|
274
|
+
return_details: bool,
|
|
275
|
+
to_scalar: Callable,
|
|
276
|
+
percentile_before_pooling: bool = True,
|
|
277
|
+
encoding_batch_size: int | None = None,
|
|
278
|
+
) -> tuple[list[float], list[dict]]:
|
|
279
|
+
"""
|
|
280
|
+
Process a batch of texts with their referential units in a truly batched manner.
|
|
281
|
+
|
|
282
|
+
Parameters
|
|
283
|
+
----------
|
|
284
|
+
texts:
|
|
285
|
+
Original texts.
|
|
286
|
+
facts_per_text:
|
|
287
|
+
List of lists of fact lists, for each scope inside the texts.
|
|
288
|
+
pooling:
|
|
289
|
+
Aggregation strategy.
|
|
290
|
+
scope_pooling_method:
|
|
291
|
+
Aggregation strategy applied over structural scopes when
|
|
292
|
+
``pooling_scope`` is not ``"document"``. For example, when
|
|
293
|
+
``pooling_scope="sentence"``, this parameter controls how
|
|
294
|
+
sentence-level scores are aggregated into a single text-level score.
|
|
295
|
+
tail_q:
|
|
296
|
+
Lower-tail fraction used by ``pooling="lower_quantile_mean"``. Must be in (0, 1]. Defaults to 0.1.
|
|
297
|
+
scope_tail_q:
|
|
298
|
+
Lower-tail fraction used by ``scope_pooling_method="lower_quantile_mean"``. Must be in (0, 1]. Defaults to 0.1.
|
|
299
|
+
return_details:
|
|
300
|
+
Whether to build detailed output dicts.
|
|
301
|
+
to_scalar:
|
|
302
|
+
Conversion function for scalar scores.
|
|
303
|
+
encoding_batch_size:
|
|
304
|
+
Maximum number of texts to encode simultaneously in the underlying model.
|
|
305
|
+
If None, no batching limit is applied. Use lower values if experiencing OOM errors.
|
|
306
|
+
percentile_before_pooling:
|
|
307
|
+
Whether you want to work with percentiles before pooling operations. Default True.
|
|
308
|
+
|
|
309
|
+
Returns
|
|
310
|
+
-------
|
|
311
|
+
tuple[list[float], list[dict]]
|
|
312
|
+
Pooled scores and optional detailed dicts.
|
|
313
|
+
"""
|
|
314
|
+
# facts_per_text: List[List[List[str]]] == [text][scope][fact]
|
|
315
|
+
all_facts: list[str] = []
|
|
316
|
+
fact_map: list[tuple[int, int, int]] = [] # (text_idx, scope_idx, fact_pos)
|
|
317
|
+
|
|
318
|
+
for text_idx, scopes in enumerate(facts_per_text):
|
|
319
|
+
for scope_idx, scope in enumerate(scopes):
|
|
320
|
+
for fact_pos, fact in enumerate(scope.get('referential_units', [])):
|
|
321
|
+
all_facts.append(fact)
|
|
322
|
+
fact_map.append((text_idx, scope_idx, fact_pos))
|
|
323
|
+
|
|
324
|
+
# Score all facts in one call (internally batched via encoding_batch_size)
|
|
325
|
+
if all_facts:
|
|
326
|
+
all_scores = list(self.granu_predictor(all_facts, encoding_batch_size=encoding_batch_size))
|
|
327
|
+
if percentile_before_pooling:
|
|
328
|
+
all_scores = self.percentile_output.score_to_percentile(all_scores)
|
|
329
|
+
else:
|
|
330
|
+
all_scores = []
|
|
331
|
+
|
|
332
|
+
scores_per_text: list[list[list[float]]] = [
|
|
333
|
+
[[] for _ in scopes] for scopes in facts_per_text
|
|
334
|
+
]
|
|
335
|
+
|
|
336
|
+
for (text_idx, scope_idx, fact_pos), score in zip(fact_map, all_scores):
|
|
337
|
+
scores_per_text[text_idx][scope_idx].append(float(score))
|
|
338
|
+
|
|
339
|
+
# Handle scopes with no facts: keep a sentinel in that scope
|
|
340
|
+
for text_idx, scopes in enumerate(scores_per_text):
|
|
341
|
+
for scope_idx, scores in enumerate(scopes):
|
|
342
|
+
if not scores:
|
|
343
|
+
no_fact_score = float(self.granu_predictor.NO_FACT_SCORE)
|
|
344
|
+
if percentile_before_pooling:
|
|
345
|
+
no_fact_score = self.percentile_output.score_to_percentile(no_fact_score)
|
|
346
|
+
scores_per_text[text_idx][scope_idx] = [no_fact_score]
|
|
347
|
+
|
|
348
|
+
# Pool and create outputs
|
|
349
|
+
pooled_scores = []
|
|
350
|
+
detailed = []
|
|
351
|
+
|
|
352
|
+
for text, facts_scopes, score_scopes in zip(texts, facts_per_text, scores_per_text):
|
|
353
|
+
weights = np.array([len(scope) for scope in score_scopes])
|
|
354
|
+
scope_pooled_scores = [
|
|
355
|
+
self._pool(scope_scores, pooling, tail_q) for scope_scores in score_scopes
|
|
356
|
+
]
|
|
357
|
+
pooled = to_scalar(self._pool(scope_pooled_scores, scope_pooling_method, scope_tail_q, weights=weights))
|
|
358
|
+
pooled_scores.append(pooled)
|
|
359
|
+
|
|
360
|
+
if return_details:
|
|
361
|
+
scopes_detail = []
|
|
362
|
+
for scope_idx, (scope, scope_scores, scope_pooled) in enumerate(
|
|
363
|
+
zip(facts_scopes, score_scopes, scope_pooled_scores)
|
|
364
|
+
):
|
|
365
|
+
pooled_percentile = scope_pooled if percentile_before_pooling else self.percentile_output.score_to_percentile(scope_pooled)
|
|
366
|
+
scopes_detail.append(
|
|
367
|
+
{
|
|
368
|
+
"scope_index": scope_idx,
|
|
369
|
+
"scope_text": scope['text'],
|
|
370
|
+
"pooled_score": to_scalar(scope_pooled),
|
|
371
|
+
"pooled_percentile": pooled_percentile,
|
|
372
|
+
"bucket_output": self.percentile_output.bucket(pooled_percentile),
|
|
373
|
+
"unit_scores": {
|
|
374
|
+
fact: to_scalar(score)
|
|
375
|
+
for fact, score in zip(scope['referential_units'], scope_scores)
|
|
376
|
+
},
|
|
377
|
+
}
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
detailed.append(
|
|
381
|
+
{
|
|
382
|
+
"text": text,
|
|
383
|
+
"pooled_score": to_scalar(pooled),
|
|
384
|
+
"pooled_percentile": pooled if percentile_before_pooling else self.percentile_output.score_to_percentile(pooled),
|
|
385
|
+
"bucket_output": self.percentile_output.bucket(pooled),
|
|
386
|
+
"scopes": scopes_detail,
|
|
387
|
+
}
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
return pooled_scores, detailed
|
|
391
|
+
|
|
392
|
+
def score_to_percentile(self, scores: float | list[float] | np.ndarray) -> float | list[float] | np.ndarray:
|
|
393
|
+
if self.percentile_output:
|
|
394
|
+
return self.percentile_output.score_to_percentile(scores)
|
|
395
|
+
else:
|
|
396
|
+
raise AttributeError("Percentile Output not supported with current Predictor Type and setting.")
|
|
397
|
+
|
|
398
|
+
def predict(
|
|
399
|
+
self,
|
|
400
|
+
answers: str | list[str] | np.ndarray,
|
|
401
|
+
split: bool = True,
|
|
402
|
+
pooling: Pooling = "mean",
|
|
403
|
+
pooling_scope: PoolingScope = "sentence",
|
|
404
|
+
scope_pooling_method: Pooling = "lower_quantile_mean",
|
|
405
|
+
return_details: bool = False,
|
|
406
|
+
return_buckets: bool = False,
|
|
407
|
+
return_percentiles: bool = True,
|
|
408
|
+
percentile_before_pooling: bool = True,
|
|
409
|
+
batched: bool = True,
|
|
410
|
+
batch_size: int = 128,
|
|
411
|
+
tail_q: float = 0.5,
|
|
412
|
+
scope_tail_q: float = 0.8,
|
|
413
|
+
show_progress_bar: bool = False,
|
|
414
|
+
convert_to_numpy: bool = True,
|
|
415
|
+
convert_to_tensor: bool = False,
|
|
416
|
+
encoding_batch_size: int | None = 128,
|
|
417
|
+
) -> str | float | list[float] | list[str] | np.ndarray | torch.Tensor | dict | list[dict]:
|
|
418
|
+
"""
|
|
419
|
+
Predict granularity scores for one or more texts.
|
|
420
|
+
|
|
421
|
+
Parameters
|
|
422
|
+
----------
|
|
423
|
+
answers:
|
|
424
|
+
One or more input texts.
|
|
425
|
+
split:
|
|
426
|
+
Whether to split texts into referential units before scoring.
|
|
427
|
+
pooling:
|
|
428
|
+
Aggregation strategy for referential scores ("mean", "min", "lower_quantile_mean", "sum").
|
|
429
|
+
"lower_quantile_mean" selects the lowest ``tail_q`` fraction of referential scores
|
|
430
|
+
(the lower tail of the distribution) and returns the mean score within this
|
|
431
|
+
subset.
|
|
432
|
+
pooling_scope:
|
|
433
|
+
Defines the structural scope over which referential units are grouped
|
|
434
|
+
prior to aggregation.
|
|
435
|
+
"document" pools over all referential units in the text.
|
|
436
|
+
"sentence" applies the specified ``pooling`` strategy independently
|
|
437
|
+
within each sentence, followed by aggregation over sentence-level
|
|
438
|
+
scores.
|
|
439
|
+
scope_pooling_method:
|
|
440
|
+
Aggregation strategy applied over structural scopes when
|
|
441
|
+
``pooling_scope`` is not ``"document"``. For example, when
|
|
442
|
+
``pooling_scope="sentence"``, this parameter controls how
|
|
443
|
+
sentence-level scores are aggregated into a single text-level score.
|
|
444
|
+
Defaults to ``"lower_quantile_mean"``.
|
|
445
|
+
return_details:
|
|
446
|
+
Whether to return detailed per-claim scores.
|
|
447
|
+
return_buckets:
|
|
448
|
+
Whether to output the score as text interpretation. Default False.
|
|
449
|
+
return_percentiles:
|
|
450
|
+
Whether to output the score as in percentiles. Default True.
|
|
451
|
+
percentile_before_pooling:
|
|
452
|
+
Whether you want to work with percentiles before pooling operations. Default True.
|
|
453
|
+
batched:
|
|
454
|
+
If True, process in chunks of batch_size.
|
|
455
|
+
batch_size:
|
|
456
|
+
Chunk size used when batched=True.
|
|
457
|
+
tail_q:
|
|
458
|
+
Lower-tail fraction used by ``pooling="lower_quantile_mean"``. Must be in (0, 1]. Defaults to 0.1.
|
|
459
|
+
scope_tail_q:
|
|
460
|
+
Lower-tail fraction used by ``scope_pooling_method="lower_quantile_mean"``. Must be in (0, 1]. Defaults to 0.8.
|
|
461
|
+
show_progress_bar:
|
|
462
|
+
If True, display a progress bar over input batches.
|
|
463
|
+
convert_to_numpy:
|
|
464
|
+
If True, convert returned scores to NumPy types. For non-detailed output,
|
|
465
|
+
this yields a ``numpy.ndarray`` (or ``numpy.float32`` for single input).
|
|
466
|
+
For detailed output, score values are converted to NumPy scalars.
|
|
467
|
+
convert_to_tensor:
|
|
468
|
+
If True, convert returned scores to PyTorch tensors. For non-detailed
|
|
469
|
+
output, this yields a 1D ``torch.Tensor`` (or 0D tensor for single input).
|
|
470
|
+
For detailed output, score values are converted to scalar tensors.
|
|
471
|
+
encoding_batch_size:
|
|
472
|
+
Maximum number of texts to encode simultaneously in the underlying model.
|
|
473
|
+
If None, no batching limit is applied. Use lower values if experiencing OOM errors.
|
|
474
|
+
|
|
475
|
+
Returns
|
|
476
|
+
-------
|
|
477
|
+
str | float | list[float] | list[str] | np.ndarray | torch.tensor | dict | list[dict]
|
|
478
|
+
Granularity score(s), optionally with detailed information.
|
|
479
|
+
"""
|
|
480
|
+
self._validate_pooling(pooling)
|
|
481
|
+
self._validate_batching(batched, batch_size)
|
|
482
|
+
self._validate_encoding_batch_size(encoding_batch_size)
|
|
483
|
+
if convert_to_tensor:
|
|
484
|
+
convert_to_numpy = False
|
|
485
|
+
if return_buckets:
|
|
486
|
+
return_percentiles = False
|
|
487
|
+
to_scalar, finalize_vector = self._conversion_functions(convert_to_tensor, convert_to_numpy)
|
|
488
|
+
percentile_before_pooling = percentile_before_pooling and return_percentiles
|
|
489
|
+
|
|
490
|
+
if self.percentile_output is None:
|
|
491
|
+
if (
|
|
492
|
+
return_buckets
|
|
493
|
+
or return_percentiles
|
|
494
|
+
or percentile_before_pooling
|
|
495
|
+
):
|
|
496
|
+
raise ValueError(
|
|
497
|
+
"The selected predictor does not support percentile-based outputs. "
|
|
498
|
+
"Disable return_buckets, return_percentiles, "
|
|
499
|
+
"and percentile_before_pooling."
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
is_single_input = isinstance(answers, str)
|
|
503
|
+
if is_single_input:
|
|
504
|
+
answers = [answers]
|
|
505
|
+
|
|
506
|
+
if not batched:
|
|
507
|
+
batch_size = len(answers)
|
|
508
|
+
num_batches = (len(answers) + batch_size - 1) // batch_size
|
|
509
|
+
batch_iter = iter_batches(answers, batch_size)
|
|
510
|
+
if show_progress_bar:
|
|
511
|
+
batch_iter = tqdm(
|
|
512
|
+
batch_iter,
|
|
513
|
+
total=num_batches,
|
|
514
|
+
desc="Granuscore",
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
total_scores: list[float] = []
|
|
518
|
+
detailed: list[dict] = []
|
|
519
|
+
|
|
520
|
+
for batch in batch_iter:
|
|
521
|
+
if not split:
|
|
522
|
+
scores = [float(s) for s in self.granu_predictor(batch, encoding_batch_size=encoding_batch_size)]
|
|
523
|
+
if return_details:
|
|
524
|
+
for answer, score in zip(batch, scores):
|
|
525
|
+
detailed.append({
|
|
526
|
+
"answer": answer,
|
|
527
|
+
"pooled_score": to_scalar(score),
|
|
528
|
+
"atomic_scores": {},
|
|
529
|
+
})
|
|
530
|
+
else:
|
|
531
|
+
total_scores.extend(scores)
|
|
532
|
+
else:
|
|
533
|
+
facts_per_answer = self.claim_splitter(batch, split_scope=pooling_scope)
|
|
534
|
+
batch_scores, batch_detailed = self._process_batch_of_facts(
|
|
535
|
+
texts=list(batch),
|
|
536
|
+
facts_per_text=facts_per_answer,
|
|
537
|
+
pooling=pooling,
|
|
538
|
+
percentile_before_pooling=percentile_before_pooling,
|
|
539
|
+
scope_pooling_method=scope_pooling_method,
|
|
540
|
+
tail_q=tail_q,
|
|
541
|
+
scope_tail_q=scope_tail_q,
|
|
542
|
+
return_details=return_details,
|
|
543
|
+
to_scalar=to_scalar,
|
|
544
|
+
encoding_batch_size=encoding_batch_size
|
|
545
|
+
)
|
|
546
|
+
total_scores.extend(batch_scores)
|
|
547
|
+
if return_details:
|
|
548
|
+
detailed.extend(batch_detailed)
|
|
549
|
+
|
|
550
|
+
if return_details:
|
|
551
|
+
output = detailed
|
|
552
|
+
elif return_buckets:
|
|
553
|
+
output = self.percentile_output.bucket(total_scores)
|
|
554
|
+
elif return_percentiles:
|
|
555
|
+
if split:
|
|
556
|
+
output = finalize_vector(total_scores if percentile_before_pooling else self.percentile_output.score_to_percentile(total_scores))
|
|
557
|
+
else:
|
|
558
|
+
output = finalize_vector(self.percentile_output.score_to_percentile(total_scores))
|
|
559
|
+
else:
|
|
560
|
+
output = finalize_vector(total_scores)
|
|
561
|
+
|
|
562
|
+
return output[0] if is_single_input else output
|