@@ -33,6 +33,7 @@ def __init__(
3333 lang : Lang = "en" ,
3434 taxonomy_head : str | None = None ,
3535 include_taxonomy : bool | list | dict = True ,
36+ batch_size : int = 16 ,
3637 ** tok_kwargs : object ,
3738 ) -> None :
3839 """Initialize the transformer detector.
@@ -48,11 +49,16 @@ def __init__(
4849 enter only as text); ``{"categories": {...}, "subcategories": {...}}`` controls both
4950 sets; a list of names selects a subset of the trained categories. Only meaningful
5051 together with ``taxonomy_head``.
52+ :param batch_size: Default number of (prompt, answer) pairs scored together in one pass
53+ by :meth:`predict_prompt_batch`. Must be >= 1.
5154 :param tok_kwargs: Additional keyword arguments for the tokenizer.
5255 """
5356 if lang not in LANG_TO_PASSAGE :
5457 raise ValueError (f"Invalid language. Choose from { ', ' .join (LANG_TO_PASSAGE )} " )
58+ if batch_size < 1 :
59+ raise ValueError ("batch_size must be >= 1" )
5560 self .lang , self .max_length = lang , max_length
61+ self .batch_size = batch_size
5662 self .tokenizer = AutoTokenizer .from_pretrained (model_path , ** tok_kwargs )
5763 self .model = AutoModelForTokenClassification .from_pretrained (model_path , ** tok_kwargs )
5864 self .device = device or (
@@ -151,49 +157,43 @@ def _group_passages_into_chunks(
151157 return groups if groups else [context ]
152158
153159 # ------------------------------------------------------------------
154- # Single-chunk prediction (the original _predict logic)
160+ # Shared decoding helper for single-example and batched prediction
155161 # ------------------------------------------------------------------
156162
157- def _predict_single (self , prompt : str , answer : str , output_format : str ) -> list :
158- """Run prediction on a single (prompt, answer) pair that fits in ``max_length``.
163+ def _decode_sample (
164+ self ,
165+ input_ids : torch .Tensor ,
166+ probabilities : torch .Tensor ,
167+ offsets : torch .Tensor ,
168+ answer_start_token : int ,
169+ answer : str ,
170+ output_format : str ,
171+ ) -> list :
172+ """Decode one sample's model outputs into tokens or spans.
159173
160- :param prompt: The prompt string.
161- :param answer: The answer string.
174+ Shared by the single-example and batched prediction paths so both produce
175+ identical output for the same inputs. All tensors must already be trimmed
176+ to this sample's true (unpadded) sequence length.
177+
178+ :param input_ids: Token ids for the sample, shape ``(seq_len,)``.
179+ :param probabilities: Softmax probabilities, shape ``(seq_len, num_labels)``.
180+ :param offsets: Character offset mapping, shape ``(seq_len, 2)``.
181+ :param answer_start_token: Index of the first answer-side token.
182+ :param answer: The original answer string.
162183 :param output_format: ``"tokens"`` or ``"spans"``.
163184 """
164- encoding , _ , offsets , answer_start_token = HallucinationDataset .prepare_tokenized_input (
165- self .tokenizer , prompt , answer , self .max_length
166- )
167-
168- labels = torch .full_like (encoding .input_ids [0 ], - 100 , device = self .device )
169- labels [answer_start_token :] = 0
170-
171- encoding = {
172- key : value .to (self .device )
173- for key , value in encoding .items ()
174- if key in ["input_ids" , "attention_mask" , "labels" ]
175- }
176-
177- with torch .no_grad ():
178- outputs = self .model (** encoding )
179- logits = outputs .logits
180- token_preds = torch .argmax (logits , dim = - 1 )[0 ]
181- probabilities = torch .softmax (logits , dim = - 1 )[0 ]
182-
183- token_preds = torch .where (labels == - 100 , labels , token_preds )
185+ token_preds = torch .argmax (probabilities , dim = - 1 )
184186
185187 if output_format == "tokens" :
186188 token_probs : list [dict ] = []
187- input_ids = encoding ["input_ids" ][0 ]
188- for i , (token , pred , prob ) in enumerate (zip (input_ids , token_preds , probabilities )):
189- if labels [i ].item () != - 100 :
190- token_probs .append (
191- {
192- "token" : self .tokenizer .decode ([token ]),
193- "pred" : pred .item (),
194- "prob" : prob [1 ].item (),
195- }
196- )
189+ for i in range (answer_start_token , input_ids .size (0 )):
190+ token_probs .append (
191+ {
192+ "token" : self .tokenizer .decode ([input_ids [i ]]),
193+ "pred" : token_preds [i ].item (),
194+ "prob" : probabilities [i , 1 ].item (),
195+ }
196+ )
197197 return token_probs
198198
199199 # output_format == "spans"
@@ -206,9 +206,6 @@ def _predict_single(self, prompt: str, answer: str, output_format: str) -> list:
206206 current_span : dict | None = None
207207
208208 for i in range (answer_start_token , token_preds .size (0 )):
209- if labels [i ].item () == - 100 :
210- continue
211-
212209 token_start , token_end = offsets [i ].tolist ()
213210 if token_start == token_end :
214211 continue
@@ -221,11 +218,7 @@ def _predict_single(self, prompt: str, answer: str, output_format: str) -> list:
221218
222219 if is_hallucination :
223220 if current_span is None :
224- current_span = {
225- "start" : rel_start ,
226- "end" : rel_end ,
227- "confidence" : confidence ,
228- }
221+ current_span = {"start" : rel_start , "end" : rel_end , "confidence" : confidence }
229222 else :
230223 current_span ["end" ] = rel_end
231224 current_span ["confidence" ] = max (current_span ["confidence" ], confidence )
@@ -241,6 +234,106 @@ def _predict_single(self, prompt: str, answer: str, output_format: str) -> list:
241234
242235 return spans
243236
237+ # ------------------------------------------------------------------
238+ # Single-chunk prediction (the original _predict logic)
239+ # ------------------------------------------------------------------
240+
241+ def _predict_single (self , prompt : str , answer : str , output_format : str ) -> list :
242+ """Run prediction on a single (prompt, answer) pair that fits in ``max_length``.
243+
244+ :param prompt: The prompt string.
245+ :param answer: The answer string.
246+ :param output_format: ``"tokens"`` or ``"spans"``.
247+ """
248+ encoding , _ , offsets , answer_start_token = HallucinationDataset .prepare_tokenized_input (
249+ self .tokenizer , prompt , answer , self .max_length
250+ )
251+
252+ encoding = {
253+ key : value .to (self .device )
254+ for key , value in encoding .items ()
255+ if key in ["input_ids" , "attention_mask" , "labels" ]
256+ }
257+
258+ with torch .no_grad ():
259+ outputs = self .model (** encoding )
260+ probabilities = torch .softmax (outputs .logits , dim = - 1 )[0 ]
261+ input_ids = encoding ["input_ids" ][0 ]
262+
263+ return self ._decode_sample (
264+ input_ids , probabilities , offsets , answer_start_token , answer , output_format
265+ )
266+
267+ # ------------------------------------------------------------------
268+ # Batched prediction with one forward pass for many pairs
269+ # ------------------------------------------------------------------
270+
271+ def _predict_batch (
272+ self , prompts : list [str ], answers : list [str ], output_format : str
273+ ) -> list [list ]:
274+ """Tokenize ``prompts``/``answers`` as one padded batch and score them in one pass.
275+
276+ :param prompts: Prompt strings for this batch (already length-validated
277+ against ``answers`` by the caller).
278+ :param answers: Answer strings for this batch.
279+ :param output_format: ``"tokens"`` or ``"spans"``.
280+ :returns: One prediction list per (prompt, answer) pair, in input order.
281+ """
282+ if self .tokenizer .padding_side != "right" :
283+ raise ValueError (
284+ "TransformerDetector batched inference requires a right-padding tokenizer."
285+ )
286+
287+ batch = self .tokenizer (
288+ prompts ,
289+ answers ,
290+ truncation = "only_first" ,
291+ max_length = self .max_length ,
292+ padding = True ,
293+ return_offsets_mapping = True ,
294+ return_tensors = "pt" ,
295+ add_special_tokens = True ,
296+ )
297+ offsets = batch .pop ("offset_mapping" )
298+ seq_lens = batch ["attention_mask" ].sum (dim = 1 )
299+
300+ model_inputs = {
301+ key : value .to (self .device )
302+ for key , value in batch .items ()
303+ if key in ["input_ids" , "attention_mask" ]
304+ }
305+
306+ with torch .no_grad ():
307+ outputs = self .model (** model_inputs )
308+ probabilities = torch .softmax (outputs .logits , dim = - 1 )
309+
310+ results : list [list ] = []
311+ for i , answer in enumerate (answers ):
312+ seq_len = int (seq_lens [i ].item ())
313+ sequence_ids = batch .sequence_ids (i )
314+ answer_start_token = next (
315+ (idx for idx , seq_id in enumerate (sequence_ids ) if seq_id == 1 ),
316+ seq_len - 1 ,
317+ )
318+
319+ if seq_len >= self .max_length :
320+ logger .warning (
321+ f"predict_prompt_batch: item { i } ({ seq_len } tokens) reached "
322+ f"max length ({ self .max_length } ) and may have been truncated."
323+ )
324+
325+ results .append (
326+ self ._decode_sample (
327+ batch ["input_ids" ][i , :seq_len ],
328+ probabilities [i , :seq_len ],
329+ offsets [i , :seq_len ],
330+ answer_start_token ,
331+ answer ,
332+ output_format ,
333+ )
334+ )
335+ return results
336+
244337 # ------------------------------------------------------------------
245338 # Multi-chunk prediction with max() aggregation
246339 # ------------------------------------------------------------------
@@ -438,16 +531,48 @@ def predict_prompt_batch(
438531 answers : list [str ],
439532 output_format : str = "tokens" ,
440533 min_confidence : float = 0.0 ,
534+ batch_size : int | None = None ,
441535 ) -> list :
442536 """Predict hallucination tokens or spans from the provided prompts and answers.
443537
444538 :param prompts: List of prompt strings.
445539 :param answers: List of answer strings.
446540 :param output_format: ``"tokens"`` or ``"spans"``.
447541 :param min_confidence: Drop ``"spans"`` below this confidence threshold (``[0, 1]``).
542+ :param batch_size: Max number of pairs tokenized and scored together in one forward
543+ pass. Defaults to ``self.batch_size`` or the whole input list if that is also
544+ unset. Every sample in a batch is padded to the longest sequence in that batch,
545+ so memory scales with ``batch_size x longest_sequence_in_batch``.
448546 :returns: List of prediction lists, one per input pair.
547+ :raises ValueError: If ``len(prompts) != len(answers)``.
449548 """
549+ if len (prompts ) != len (answers ):
550+ raise ValueError ("Number of prompts must match number of answers" )
551+ if not prompts :
552+ return []
553+ if output_format not in ("tokens" , "spans" ):
554+ raise ValueError (
555+ f"TransformerDetector doesn't support '{ output_format } ' format."
556+ " Use 'tokens' or 'spans'"
557+ )
558+ if batch_size is not None and batch_size < 1 :
559+ raise ValueError ("batch_size must be >= 1" )
560+ self ._validate_min_confidence (min_confidence )
561+
562+ effective_batch_size = self .batch_size if batch_size is None else batch_size
563+
564+ results : list [list ] = []
565+ for start in range (0 , len (prompts ), effective_batch_size ):
566+ end = start + effective_batch_size
567+ results .extend (
568+ self ._predict_batch (prompts [start :end ], answers [start :end ], output_format )
569+ )
570+
571+ if output_format == "spans" and self .typer is not None :
572+ for i in range (len (results )):
573+ results [i ] = self .typer .type_spans (answers [i ], prompts [i ], results [i ])
574+
450575 return [
451- self .predict_prompt ( p , a , output_format , min_confidence )
452- for p , a in zip ( prompts , answers )
576+ self ._filter_spans_by_confidence ( result , output_format , min_confidence )
577+ for result in results
453578 ]
0 commit comments