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/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