Views
No views yet
en1import enum
2import transformers
3import torch
4from transformers.pipelines import PIPELINE_REGISTRY
5from transformers import (
6 pipeline,
7 Pipeline,
8 TextGenerationPipeline,
9 PreTrainedTokenizer,
10 AutoModelForCausalLM,
11 PreTrainedTokenizer
12)
13from transformers.pipelines.text_generation import Chat, ReturnType
14from typing import (
15 Dict,
16 Callable,
17 Tuple,
18 List,
19)
20
21
22class LevelToScorePipeline(TextGenerationPipeline):
23
24 def __init__(
25 self,
26 level_to_score_func: Callable[[Tuple[torch.FloatTensor], PreTrainedTokenizer], Tuple[List[float], List[List[float]]]],
27 *args,
28 **kwargs
29 ):
30 super().__init__(*args, **kwargs)
31 self._level_to_score_func = level_to_score_func
32
33 def preprocess(
34 self,
35 prompt_text,
36 prefix="",
37 handle_long_generation=None,
38 add_special_tokens=None,
39 truncation=None,
40 padding=None,
41 max_length=None,
42 continue_final_message=None,
43 **generate_kwargs,
44 ):
45 # Only set non-None tokenizer kwargs, so as to rely on the tokenizer's defaults
46 tokenizer_kwargs = {
47 "add_special_tokens": add_special_tokens,
48 "truncation": truncation,
49 "padding": padding,
50 "max_length": max_length,
51 }
52 tokenizer_kwargs = {key: value for key, value in tokenizer_kwargs.items() if value is not None}
53
54 if isinstance(prompt_text, Chat):
55 tokenizer_kwargs.pop("add_special_tokens", None) # ignore add_special_tokens on chats
56 # If the user passes a chat that ends in an assistant message, we treat it as a prefill by default
57 # because very few models support multiple separate, consecutive assistant messages
58 if continue_final_message is None:
59 continue_final_message = prompt_text.messages[-1]["role"] == "assistant"
60 inputs = self.tokenizer.apply_chat_template(
61 prompt_text.messages,
62 add_generation_prompt=not continue_final_message,
63 continue_final_message=continue_final_message,
64 return_dict=True,
65 return_tensors=self.framework,
66 **tokenizer_kwargs,
67 )
68 else:
69 inputs = self.tokenizer(prefix + prompt_text, return_tensors=self.framework, **tokenizer_kwargs)
70
71 inputs["prompt_text"] = prompt_text
72
73 if handle_long_generation == "hole":
74 cur_len = inputs["input_ids"].shape[-1]
75 if "max_new_tokens" in generate_kwargs:
76 new_tokens = generate_kwargs["max_new_tokens"]
77 else:
78 new_tokens = generate_kwargs.get("max_length", self.generation_config.max_length) - cur_len
79 if new_tokens < 0:
80 raise ValueError("We cannot infer how many new tokens are expected")
81 if cur_len + new_tokens > self.tokenizer.model_max_length:
82 keep_length = self.tokenizer.model_max_length - new_tokens
83 if keep_length <= 0:
84 raise ValueError(
85 "We cannot use `hole` to handle this generation the number of desired tokens exceeds the"
86 " models max length"
87 )
88
89 inputs["input_ids"] = inputs["input_ids"][:, -keep_length:]
90 if "attention_mask" in inputs:
91 inputs["attention_mask"] = inputs["attention_mask"][:, -keep_length:]
92
93 return inputs
94
95 def _forward(self, model_inputs, **generate_kwargs):
96 input_ids = model_inputs["input_ids"]
97 attention_mask = model_inputs.get("attention_mask", None)
98 # Allow empty prompts
99 if input_ids.shape[1] == 0:
100 input_ids = None
101 attention_mask = None
102 in_b = 1
103 else:
104 in_b = input_ids.shape[0]
105 prompt_text = model_inputs.pop("prompt_text")
106
107 # If there is a prefix, we may need to adjust the generation length. Do so without permanently modifying
108 # generate_kwargs, as some of the parameterization may come from the initialization of the pipeline.
109 prefix_length = generate_kwargs.pop("prefix_length", 0)
110 if prefix_length > 0:
111 has_max_new_tokens = "max_new_tokens" in generate_kwargs or (
112 "generation_config" in generate_kwargs
113 and generate_kwargs["generation_config"].max_new_tokens is not None
114 )
115 if not has_max_new_tokens:
116 generate_kwargs["max_length"] = generate_kwargs.get("max_length") or self.generation_config.max_length
117 generate_kwargs["max_length"] += prefix_length
118 has_min_new_tokens = "min_new_tokens" in generate_kwargs or (
119 "generation_config" in generate_kwargs
120 and generate_kwargs["generation_config"].min_new_tokens is not None
121 )
122 if not has_min_new_tokens and "min_length" in generate_kwargs:
123 generate_kwargs["min_length"] += prefix_length
124
125 # User-defined `generation_config` passed to the pipeline call take precedence
126 if "generation_config" not in generate_kwargs:
127 generate_kwargs["generation_config"] = self.generation_config
128
129 generate_kwargs["output_scores"] = not generate_kwargs.get("do_sample", False)
130 generate_kwargs["return_dict_in_generate"] = True
131
132 generated_sequence = self.model.generate(input_ids=input_ids, attention_mask=attention_mask, **generate_kwargs)
133
134 logits = None
135
136 # TODO: check good default
137 if generate_kwargs.get("return_scores", True):
138 assert not generate_kwargs.get("do_sample", False), "return_logits=True is only supported for do_sample=False"
139
140 # Proceed to process logits and convert to score average.
141 # next_token_logits is [batch_size, vocab_size]
142 # raw_logits is a tuple of ([next_token_logits, past_key_values])
143
144 logits = generated_sequence.scores
145
146 out_b = generated_sequence.sequences.shape[0]
147 if self.framework == "pt":
148 generated_sequence = generated_sequence.sequences.reshape(in_b, out_b // in_b, *generated_sequence.sequences.shape[1:])
149 # elif self.framework == "tf":
150 # generated_sequence = tf.reshape(generated_sequence, (in_b, out_b // in_b, *generated_sequence.shape[1:]))
151 return {"generated_sequence": generated_sequence, "input_ids": input_ids, "prompt_text": prompt_text, "logits": logits}
152
153 def postprocess(
154 self,
155 model_outputs,
156 return_type=ReturnType.FULL_TEXT,
157 clean_up_tokenization_spaces=True,
158 continue_final_message=None,
159 ):
160 generated_sequence = model_outputs["generated_sequence"][0]
161 input_ids = model_outputs["input_ids"]
162 prompt_text = model_outputs["prompt_text"]
163 logits = model_outputs["logits"]
164
165 #TODO: This is now making many assumptions about how the logits are ordered,
166 # Should think about how to make this explicit
167 scores, selective_logits = self._level_to_score_func(logits, self.tokenizer)
168
169 generated_sequence = generated_sequence.numpy().tolist()
170 records = []
171 for sequence in generated_sequence:
172 if return_type == ReturnType.TENSORS:
173 record = {"generated_token_ids": sequence}
174 elif return_type in {ReturnType.NEW_TEXT, ReturnType.FULL_TEXT}:
175 # Decode text
176 text = self.tokenizer.decode(
177 sequence,
178 skip_special_tokens=True,
179 clean_up_tokenization_spaces=clean_up_tokenization_spaces,
180 )
181
182 # Remove PADDING prompt of the sequence if XLNet or Transfo-XL model is used
183 if input_ids is None:
184 prompt_length = 0
185 else:
186 prompt_length = len(
187 self.tokenizer.decode(
188 input_ids[0],
189 skip_special_tokens=True,
190 clean_up_tokenization_spaces=clean_up_tokenization_spaces,
191 )
192 )
193
194 all_text = text[prompt_length:]
195 if return_type == ReturnType.FULL_TEXT:
196 if isinstance(prompt_text, str):
197 all_text = prompt_text + all_text
198 elif isinstance(prompt_text, Chat):
199 if continue_final_message is None:
200 # If the user passes a chat ending in an assistant message, we treat it as a prefill by
201 # default because very few models support multiple separate, consecutive assistant messages
202 continue_final_message = prompt_text.messages[-1]["role"] == "assistant"
203 if continue_final_message:
204 # With assistant prefill, concat onto the end of the last message
205 all_text = list(prompt_text.messages)[:-1] + [
206 {
207 "role": prompt_text.messages[-1]["role"],
208 "content": prompt_text.messages[-1]["content"] + all_text,
209 }
210 ]
211 else:
212 # When we're not starting from a prefill, the output is a new assistant message
213 all_text = list(prompt_text.messages) + [{"role": "assistant", "content": all_text}]
214 record = {
215 "generated_text": all_text,
216 "score": scores[0],
217 "selective_logits": selective_logits[0]
218 }
219 records.append(record)
220
221 return records
222
223
224class SingleLabelRankDict:
225 def __init__(
226 self,
227 rank_dict: Dict[Text, Any]
228 ):
229 self._rank_dict = rank_dict
230
231 def __len__(self) -> int:
232 return len(self._rank_dict)
233
234 def get_rank_dict(self, tokenizer: PreTrainedTokenizer) -> Dict[int, Any]:
235 return {tokenizer.convert_tokens_to_ids([token])[0]: value for token, value in self._rank_dict.items()}
236
237 def to_tokenizer(self, tokenizer: PreTrainedTokenizer) -> PreTrainedTokenizer:
238 """Augment tokenizer vocab with `rank_dict` IN-PLACE.
239 """
240 vocabs: List[Text] = self._rank_dict.keys()
241 new_vocab = [vocab for vocab in vocabs if vocab not in tokenizer.get_vocab()]
242 tokenizer.add_tokens(new_vocab)
243 return tokenizer
244 def from_tokenizer(cls, tokenizer: PreTrainedTokenizer) -> "SingleLabelRankDict":
245 vocab = tokenizer.get_vocab()
246 rank_dict = {}
247 pattern = re.compile(r" <\|label_level_(\d+)\|>")
248
249 for token in vocab.keys():
250 match = pattern.match(token)
251 if match:
252 value = int(match.group(1))
253 # normalized_value = value / (len(vocab) - 1)
254 rank_dict[token] = value
255
256 # normalize rank_values
257 num_levels = max(rank_dict.values()) + 1
258 for token in rank_dict.keys():
259 rank_dict[token] = 1. / num_levels * (rank_dict[token] + 0.5)
260
261 return cls(rank_dict=rank_dict)
262
263
264model = transformers.AutoModelForCausalLM.from_pretrained(
265 "Zhengping/conditional-probability-regression",
266 torch_dtype="auto",
267 attn_implementation="flash_attention_2",
268)
269tokenizer = transformers.AutoTokenizer.from_pretrained(
270 "Zhengping/conditional-probability-regression",
271)
272
273rank_dict = SingleLabelRankDict.from_tokenizer(tokenizer)
274
275PIPELINE_REGISTRY.register_pipeline(
276 "level-to-score",
277 pipeline_class=LevelToScorePipeline,
278 pt_model=AutoModelForCausalLM
279)
280
281# This allows fine-grained labeling, the greedy decoding gives a coarse score,
282# one can also attach their own level-to-score function to the pipeline, e.g. using UNLI
283# label transformation to get it more binarized
284def _level_to_score_func(
285 logits: Tuple[torch.FloatTensor],
286 tokenizer: PreTrainedTokenizer
287) -> Tuple[List[float], List[float]]:
288 """ """
289 logits = logits[0]
290 num_labels = len(rank_dict)
291 considering_ids = tokenizer.convert_tokens_to_ids([f" <|label_level_{i}|>" for i in range(num_labels)])
292 selective_logits = torch.index_select(logits, 1, torch.tensor(considering_ids, device=logits.device))
293 step_size = 1 / num_labels
294 expectation = torch.tensor([[i * step_size + 1 / 2 * step_size for i in range(num_labels)]], device=selective_logits.device)
295 scores = torch.softmax(selective_logits, dim=-1) @ expectation.T
296 scores = scores.squeeze(-1).tolist()
297 return scores, selective_logits.tolist()
298
299pipe = pipeline(
300 "level-to-score",
301 model=model,
302 max_new_tokens=2,
303 tokenizer=tokenizer,
304 device=0,
305 level_to_score_func=_level_to_score_func,
306 torch_dtype=torch.bfloat16,
307)
308
309template = UNLITemplate()
310
311premise = "Sam is sleeping."
312hypothesis = "Sam is awake."
313
314inputs = [
315 {
316 "role": "user",
317 "content": "### Question: Given the premise \"{premise}\", how likely is it that the hypothesis \"{hypothesis}\" is true?\n\n".format(
318 premise=premise,
319 hypothesis=hypothesis
320 )
321 },
322 {
323 "role": "assitant",
324 "content": "### Answer:"
325 }
326]
327
328result = pipe(inputs)
329print(result)TODO1@article{wang2025always,
2 title={Always Tell Me The Odds: Fine-grained Conditional Probability Estimation},
3 author={Wang, Liaoyaqi and Jiang, Zhengping and Liu, Anqi and Van Durme, Benjamin},
4 journal={arXiv preprint arXiv:2505.01595},
5 year={2025}
6}