AnonymousARR42 commited on
Commit
f6d71ea
·
verified ·
1 Parent(s): d939317

Upload longbel.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. longbel.py +981 -0
longbel.py ADDED
@@ -0,0 +1,981 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Core models for LongBEL
3
+ """
4
+ # Copyright (c) Facebook, Inc. and its affiliates.
5
+ # All rights reserved.
6
+ #
7
+ # This source code is licensed under the license found in the
8
+ # LICENSE file in the root directory of this source tree.
9
+
10
+ import json
11
+ import logging
12
+ import os
13
+ import pickle
14
+ import re
15
+ from html import escape
16
+ from typing import Optional
17
+
18
+ import nltk
19
+ import torch
20
+ import torch.nn.functional as F
21
+ from huggingface_hub import hf_hub_download
22
+ from tqdm.auto import tqdm
23
+ from transformers import (
24
+ AutoTokenizer,
25
+ LlamaForCausalLM,
26
+ PretrainedConfig,
27
+ )
28
+
29
+ logger = logging.getLogger(__name__)
30
+ logging.basicConfig(
31
+ level=logging.INFO, # Display INFO and above
32
+ format="%(levelname)s - %(message)s",
33
+ )
34
+
35
+
36
+ # Define a simple config class that inherits from PretrainedConfig
37
+ class LLamaLongBELConfig(PretrainedConfig):
38
+ model_type = "llama_longbel"
39
+
40
+ def __init__(self, **kwargs):
41
+ # Ensure it has llama as base
42
+ kwargs.setdefault("model_type", "llama")
43
+ super().__init__(**kwargs)
44
+
45
+
46
+ def clean_natural(text):
47
+ return (
48
+ text.replace("\xa0", " ")
49
+ .replace("{", "(")
50
+ .replace("}", ")")
51
+ .replace("[", "(")
52
+ .replace("]", ")")
53
+ .replace("\n", " ")
54
+ )
55
+
56
+
57
+ def parse_text(
58
+ data,
59
+ start_entity,
60
+ end_entity,
61
+ start_group,
62
+ end_group,
63
+ nlp,
64
+ ) -> tuple[list[str], list[str], list[dict[str, str]]]:
65
+ """Create simple (source, target) pairs per entity.
66
+
67
+ For each entity in the BigBio page, returns one pair where:
68
+ - source: the sentence text that contains the entity mention
69
+ - target: "<entity> is <annotation>" where <annotation> is the best synonym
70
+ if available (or the normalized id otherwise).
71
+ """
72
+ source_sentences: list[str] = []
73
+ tsv_lines: list[dict[str, str]] = []
74
+ target_texts_dict: dict[tuple[tuple[int, int], ...], str] = {}
75
+ source_texts_dict: dict[tuple[tuple[int, int], ...], str] = {}
76
+ tsv_lines_dict: dict[tuple[tuple[int, int], ...], dict[str, str]] = {}
77
+ all_passages = {}
78
+ for i, passage in enumerate(data.get("passages", [])):
79
+ all_passages[i] = clean_natural(passage["text"][0])
80
+ for passage_id, passage in enumerate(data.get("passages", [])):
81
+ passage_text = passage["text"][0]
82
+ start_offset_passage = passage["offsets"][0][0]
83
+ end_offset_passage = passage["offsets"][0][1]
84
+
85
+ passage_text = clean_natural(passage_text)
86
+
87
+ # Iterate over entities and emit one pair per entity found in this passage
88
+ for entity in data.get("entities", []):
89
+ # min and max of all entity offsets to get the global span of the entity for filtering sentences
90
+ global_start = min(off[0] for off in entity["offsets"])
91
+ global_end = max(off[1] for off in entity["offsets"])
92
+ # Keep only entities whose start falls inside this passage
93
+ if not (start_offset_passage <= global_start < end_offset_passage):
94
+ continue
95
+ entity_text = " ".join(entity["text"])
96
+ entity_text = clean_natural(entity_text)
97
+ # Define entity group
98
+ group_annotation = entity.get("type")
99
+ # Get all offsets, convert to relative, and filter for this sentence
100
+ relative_entity_spans = []
101
+ for off in entity["offsets"]:
102
+ global_start_off, global_end_off = off
103
+ if not (start_offset_passage <= global_start_off < end_offset_passage):
104
+ continue
105
+
106
+ rel_start_off = global_start_off - start_offset_passage
107
+ rel_end_off = global_end_off - start_offset_passage
108
+ relative_entity_spans.append((rel_start_off, rel_end_off))
109
+ relative_entity_spans.sort(key=lambda x: x[0])
110
+
111
+ marked_text = passage_text
112
+ for start_in_sent, end_in_sent in relative_entity_spans:
113
+ marked_text = (
114
+ marked_text[:start_in_sent]
115
+ + start_entity
116
+ + marked_text[start_in_sent:end_in_sent]
117
+ + end_entity
118
+ + marked_text[end_in_sent:]
119
+ )
120
+
121
+ for other_passage_id, other_passage_text in all_passages.items():
122
+ if other_passage_id < passage_id:
123
+ marked_text = other_passage_text + "\n" + marked_text
124
+ elif other_passage_id > passage_id:
125
+ marked_text = marked_text + "\n" + other_passage_text
126
+ # Emit the pair
127
+ doc_id = data.get("id", "")
128
+ tsv_line = {
129
+ "doc_id": doc_id,
130
+ "semantic_group": group_annotation,
131
+ "start_span": global_start,
132
+ "end_span": global_end,
133
+ "mention": entity_text,
134
+ }
135
+ if entity.get("normalized"):
136
+ tsv_line["gold_concept_code"] = entity["normalized"][0]["db_id"]
137
+ tsv_line["gold_concept_name"] = entity["normalized"][0]["db_match"]
138
+
139
+ tsv_lines_dict[(global_start, global_end)] = tsv_line
140
+ source_texts_dict[(global_start, global_end)] = marked_text
141
+ target_entity_text = (
142
+ start_entity
143
+ + entity_text
144
+ + end_entity
145
+ + start_group
146
+ + group_annotation
147
+ + end_group
148
+ )
149
+ target_texts_dict[(global_start, global_end)] = target_entity_text
150
+ # Sort keys to have a deterministic order
151
+ target_texts = []
152
+ sorted_keys = sorted(tsv_lines_dict.keys(), key=lambda x: (x[0], x[1]))
153
+ for entity_id, entity_span in enumerate(sorted_keys):
154
+ tsv_line = tsv_lines_dict[entity_span]
155
+ tsv_line["mention_id"] = f"{data.get('id', '')}.{entity_id + 1}"
156
+ tsv_lines.append(tsv_line)
157
+ source_sentences.append(source_texts_dict[entity_span])
158
+ target_texts.append(target_texts_dict[entity_span])
159
+
160
+ return source_sentences, target_texts, tsv_lines # type: ignore
161
+
162
+
163
+ def get_prefix_allowed_tokens_fn(
164
+ model,
165
+ sources: list[str],
166
+ sem_groups: list[str],
167
+ multiple_answers: bool = False,
168
+ ):
169
+ candidates_trie = model.candidate_trie # type: ignore
170
+ sep_token_id = model.tokenizer.sep_token_id
171
+ eos_token_id = model.tokenizer.eos_token_id
172
+ pad_token_id = model.tokenizer.pad_token_id
173
+ plus_token_id = model.tokenizer.convert_tokens_to_ids("<+>") # type: ignore
174
+ end_group_token_id = model.tokenizer.convert_tokens_to_ids("}") # type: ignore
175
+
176
+ def prefix_allowed_tokens_fn(batch_id, sent):
177
+ sent = sent.tolist()
178
+ if len(sent) > 1 and sent[-1] in [eos_token_id, pad_token_id, sep_token_id]:
179
+ if sep_token_id:
180
+ return [sep_token_id, pad_token_id, eos_token_id]
181
+ else:
182
+ return [pad_token_id, eos_token_id]
183
+
184
+ # Remove the prefix from the sent
185
+ index_sep = len(sent) - 1 - sent[::-1].index(end_group_token_id)
186
+ sent = sent[index_sep:]
187
+
188
+ sem_group = sem_groups[batch_id]
189
+ # Remove everything up to last sep_token_id and add prefix and tgt_lang_id
190
+ if multiple_answers and plus_token_id in sent:
191
+ index_plus = len(sent) - 1 - sent[::-1].index(plus_token_id)
192
+ # Start fresh with decoder start
193
+ if index_plus == len(sent) - 1:
194
+ sent = [end_group_token_id]
195
+ # If there are tokens after the last plus_token_id, keep them
196
+ else:
197
+ sent = [end_group_token_id] + sent[index_plus + 1 :]
198
+ trie_out = candidates_trie[
199
+ sem_group # type: ignore
200
+ ].get(sent)
201
+ if eos_token_id in trie_out:
202
+ if sep_token_id:
203
+ trie_out += [sep_token_id]
204
+ if multiple_answers:
205
+ trie_out += [plus_token_id]
206
+ elif not trie_out:
207
+ if sep_token_id:
208
+ return [sep_token_id, pad_token_id, eos_token_id]
209
+ else:
210
+ return [pad_token_id, eos_token_id]
211
+ return trie_out
212
+
213
+ return prefix_allowed_tokens_fn
214
+
215
+
216
+ def add_headers_to_prompt(source: str, target: str, previous_targets: str):
217
+ if not previous_targets:
218
+ previous_targets = "None"
219
+ input_sentence = f"### Context\n{source.rstrip()}\n\n### Previous Normalizations\n{previous_targets.rstrip()}\n\n### Prediction\n{target.rstrip()}"
220
+ return input_sentence
221
+
222
+
223
+ def parse_prediction(
224
+ outputs: list[str],
225
+ sem_groups: list[str],
226
+ text_to_code: Optional[dict[str, dict[str, str]]] = None,
227
+ multiple_answers: bool = False,
228
+ ) -> tuple[list[str], list[str]]:
229
+ codes = []
230
+ predictions = []
231
+ for output, group in zip(outputs, sem_groups):
232
+ splits = output.split("} ") # type: ignore
233
+ if len(splits) > 1 and splits[-1].strip():
234
+ prediction = splits[-1].strip().replace("<SEP>", "")
235
+ if text_to_code:
236
+ if multiple_answers:
237
+ prediction_list = prediction.split("<+>") # type: ignore
238
+ code_list = set()
239
+ for pred in prediction_list:
240
+ code_list.add(text_to_code[group].get(pred.strip(), "NO_CODE"))
241
+ if len(code_list) > 1 and "NO_CODE" in code_list:
242
+ code_list.remove("NO_CODE")
243
+ code = "+".join(code_list)
244
+ else:
245
+ code = text_to_code[group].get(prediction, "NO_CODE")
246
+ else:
247
+ code = "NO_CODE"
248
+ else:
249
+ print(
250
+ "IndexError: splitting failed or empty prediction, adding empty string as prediction."
251
+ )
252
+ prediction = "NO_PREDICTION"
253
+ code = "NO_CODE"
254
+ codes.append(code)
255
+ predictions.append(prediction)
256
+ return codes, predictions
257
+
258
+
259
+ def compute_score(outputs, tokenizer, prefix_len=0):
260
+ sequences = outputs.sequences # (N, seq_len)
261
+ scores = outputs.scores # list length T = # generated tokens
262
+
263
+ N, total_len = sequences.shape
264
+ T = len(scores)
265
+
266
+ # keep only the generated part (completion)
267
+ sequences = sequences[:, prefix_len : prefix_len + T]
268
+
269
+ # Make sure score is not longer than sequences
270
+ if len(scores) > sequences.size(1):
271
+ scores = scores[: sequences.size(1)]
272
+
273
+ # Compute as usual but now only for completion tokens
274
+ mask = (
275
+ (sequences != tokenizer.pad_token_id)
276
+ & (sequences != tokenizer.eos_token_id)
277
+ & (sequences != tokenizer.bos_token_id)
278
+ )
279
+
280
+ # log-prob for each generated token
281
+ logprob_steps = []
282
+ for t, logits in enumerate(scores):
283
+ log_probs_t = F.log_softmax(logits, dim=-1)
284
+ token_t = sequences[:, t]
285
+ idx = torch.arange(N)
286
+ logprob_steps.append(log_probs_t[idx, token_t])
287
+
288
+ logprobs = torch.stack(logprob_steps, dim=1)
289
+ logprobs.masked_fill_(~mask, 0)
290
+
291
+ lengths = mask.sum(dim=1).clamp(min=1)
292
+ confidence = torch.exp(logprobs.sum(dim=1) / lengths)
293
+
294
+ return confidence.tolist()
295
+
296
+
297
+ def skip_undesired_tokens(outputs, tokenizer):
298
+ sep_token = "<SEP>"
299
+ plus_token = "<+>"
300
+ # Build the list of special tokens to remove
301
+ tokens_to_remove = tokenizer.all_special_tokens[:2]
302
+
303
+ cleaned_outputs = []
304
+ for sequence in outputs:
305
+ # Remove undesired special tokens
306
+ for token in tokens_to_remove:
307
+ sequence = sequence.replace(token, "")
308
+
309
+ # Remove spaces *immediately* after the sep_token adn plus_token (e.g. "<sep> text" → "<sep>text")
310
+ sequence = re.sub(rf"({re.escape(plus_token)})\s+", r"\1", sequence)
311
+ sequence = re.sub(rf"({re.escape(sep_token)})\s+", r"\1", sequence)
312
+
313
+ cleaned_outputs.append(sequence.strip())
314
+
315
+ return cleaned_outputs
316
+
317
+
318
+ def _score_to_rgb(score: float) -> tuple[int, int, int]:
319
+ clipped_score = max(0.0, min(1.0, score))
320
+ red = 255
321
+ channel = int(255 * (1.0 - clipped_score))
322
+ return red, channel, channel
323
+
324
+
325
+ def _build_ansi_saliency_text(
326
+ token_texts: list[str], saliency_scores: list[float]
327
+ ) -> str:
328
+ chunks = []
329
+ for token_text, score in zip(token_texts, saliency_scores):
330
+ red, green, blue = _score_to_rgb(score)
331
+ chunks.append(f"\x1b[48;2;{red};{green};{blue}m{token_text}\x1b[0m")
332
+ return "".join(chunks)
333
+
334
+
335
+ def _build_html_saliency_text(
336
+ token_texts: list[str], saliency_scores: list[float]
337
+ ) -> str:
338
+ chunks = []
339
+ for token_text, score in zip(token_texts, saliency_scores):
340
+ red, green, blue = _score_to_rgb(score)
341
+ chunks.append(
342
+ f'<span style="background-color: rgb({red}, {green}, {blue});">{escape(token_text)}</span>'
343
+ )
344
+ return "".join(chunks)
345
+
346
+
347
+ class LLamaLongBEL(LlamaForCausalLM):
348
+ config_class = LLamaLongBELConfig
349
+
350
+ def __init__(self, config, *args, **kwargs):
351
+ # Initialize the parent LlamaForCausalLM
352
+ super().__init__(config, *args, **kwargs)
353
+
354
+ # Store language from config
355
+ self.lang = getattr(config, "lang", "en")
356
+ self.text_to_code = None
357
+ self.candidate_trie = None
358
+ self.tokenizer = None
359
+
360
+ @classmethod
361
+ def from_pretrained(
362
+ cls,
363
+ pretrained_model_name_or_path,
364
+ *args,
365
+ lang=None,
366
+ text_to_code_path=None,
367
+ candidate_trie_path=None,
368
+ **kwargs,
369
+ ):
370
+ # Remove custom kwargs before passing to parent
371
+ custom_kwargs = {
372
+ "lang": lang,
373
+ "text_to_code_path": text_to_code_path,
374
+ "candidate_trie_path": candidate_trie_path,
375
+ }
376
+
377
+ # Call parent's from_pretrained
378
+ model = super().from_pretrained(
379
+ pretrained_model_name_or_path,
380
+ *args,
381
+ **{k: v for k, v in kwargs.items() if k not in custom_kwargs},
382
+ )
383
+
384
+ # Set up tokenizer
385
+ model.tokenizer = AutoTokenizer.from_pretrained(
386
+ pretrained_model_name_or_path, use_fast=True
387
+ )
388
+ model.tokenizer.padding_side = "left"
389
+
390
+ # Set language: explicit override > config > default
391
+ if lang is not None:
392
+ model.lang = lang
393
+ elif hasattr(model.config, "lang"):
394
+ model.lang = model.config.lang
395
+ else:
396
+ model.lang = "en"
397
+
398
+ logger.info(f"Model language set to: {model.lang}")
399
+
400
+ # Load text_to_code
401
+ text_to_code_file_local = (
402
+ text_to_code_path
403
+ if text_to_code_path is not None
404
+ else os.path.join(pretrained_model_name_or_path, "text_to_code.json")
405
+ )
406
+ try:
407
+ if os.path.exists(text_to_code_file_local):
408
+ with open(text_to_code_file_local, encoding="utf-8") as f:
409
+ model.text_to_code = json.load(f)
410
+ logger.info(
411
+ f"Loaded text_to_code.json from local path: {text_to_code_file_local}"
412
+ )
413
+ else:
414
+ text_to_code_path_hf = hf_hub_download(
415
+ repo_id=pretrained_model_name_or_path,
416
+ filename="text_to_code.json",
417
+ )
418
+ with open(text_to_code_path_hf, encoding="utf-8") as f:
419
+ model.text_to_code = json.load(f)
420
+ logger.info(
421
+ f"Loaded text_to_code.json from HF Hub: {text_to_code_path_hf}"
422
+ )
423
+ except Exception:
424
+ logger.warning("text_to_code.json not found (local or HF hub)")
425
+ model.text_to_code = None
426
+
427
+ # Load candidate_trie
428
+ candidate_trie_file_local = (
429
+ candidate_trie_path
430
+ if candidate_trie_path is not None
431
+ else os.path.join(pretrained_model_name_or_path, "candidate_trie.pkl")
432
+ )
433
+ try:
434
+ if os.path.exists(candidate_trie_file_local):
435
+ with open(candidate_trie_file_local, "rb") as f:
436
+ model.candidate_trie = pickle.load(f)
437
+ logger.info(
438
+ f"Loaded candidate_trie.pkl from local path: {candidate_trie_file_local}"
439
+ )
440
+ else:
441
+ candidate_trie_path_hf = hf_hub_download(
442
+ repo_id=pretrained_model_name_or_path,
443
+ filename="candidate_trie.pkl",
444
+ )
445
+ with open(candidate_trie_path_hf, "rb") as f:
446
+ model.candidate_trie = pickle.load(f)
447
+ logger.info(
448
+ f"Loaded candidate_trie.pkl from HF Hub: {candidate_trie_path_hf}"
449
+ )
450
+ except Exception:
451
+ logger.warning("candidate_trie.pkl not found (local or HF hub)")
452
+ model.candidate_trie = None
453
+
454
+ return model
455
+
456
+ def _compute_gradient_saliency(
457
+ self,
458
+ input_sentences: list[str],
459
+ generated_sequences: torch.Tensor,
460
+ num_beams: int,
461
+ prefix_len: int,
462
+ saliency_method: str = "integrated",
463
+ ig_steps: int = 20,
464
+ ig_baseline: str = "pad",
465
+ ) -> list[dict[str, object]]:
466
+ if not input_sentences:
467
+ return []
468
+
469
+ method = saliency_method.strip().lower()
470
+ if method == "integerated":
471
+ method = "integrated"
472
+ if method not in {"simple", "integrated"}:
473
+ raise ValueError("saliency_method must be one of: 'simple', 'integrated'.")
474
+
475
+ top_sequence_indices = (
476
+ torch.arange(
477
+ len(input_sentences),
478
+ device=generated_sequences.device,
479
+ )
480
+ * num_beams
481
+ )
482
+ top_sequences = generated_sequences.index_select(0, top_sequence_indices)
483
+
484
+ attention_mask = (top_sequences != self.tokenizer.pad_token_id).long() # type: ignore
485
+ input_embeddings = self.get_input_embeddings()(top_sequences).detach() # type: ignore
486
+
487
+ next_tokens = top_sequences[:, 1:]
488
+ output_token_mask = torch.zeros_like(next_tokens, dtype=torch.bool)
489
+ if prefix_len > 0:
490
+ output_token_mask[:, prefix_len - 1 :] = True
491
+
492
+ valid_token_mask = output_token_mask & (
493
+ (next_tokens != self.tokenizer.pad_token_id) # type: ignore
494
+ & (next_tokens != self.tokenizer.eos_token_id) # type: ignore
495
+ & (next_tokens != self.tokenizer.bos_token_id) # type: ignore
496
+ )
497
+
498
+ def _objective_from_embeddings(embeddings: torch.Tensor) -> torch.Tensor:
499
+ forward_outputs = self( # type: ignore
500
+ inputs_embeds=embeddings,
501
+ attention_mask=attention_mask,
502
+ use_cache=False,
503
+ return_dict=True,
504
+ )
505
+ logits = forward_outputs.logits[:, :-1, :]
506
+ log_probs = F.log_softmax(logits, dim=-1)
507
+ token_log_probs = log_probs.gather(
508
+ dim=-1,
509
+ index=next_tokens.unsqueeze(-1),
510
+ ).squeeze(-1)
511
+ return token_log_probs.masked_select(valid_token_mask).sum()
512
+
513
+ if method == "simple":
514
+ simple_embeddings = input_embeddings.detach()
515
+ simple_embeddings.requires_grad_(True)
516
+ self.zero_grad(set_to_none=True) # type: ignore
517
+ with torch.enable_grad():
518
+ objective = _objective_from_embeddings(simple_embeddings)
519
+ gradients = torch.autograd.grad(
520
+ outputs=objective,
521
+ inputs=simple_embeddings,
522
+ retain_graph=False,
523
+ create_graph=False,
524
+ )[0]
525
+ token_importance = gradients.norm(p=2, dim=-1)
526
+ else:
527
+ if ig_baseline == "pad": # type: ignore
528
+ baseline_ids = torch.full_like(
529
+ top_sequences,
530
+ self.tokenizer.pad_token_id, # type: ignore
531
+ )
532
+ baseline_embeddings = self.get_input_embeddings()(baseline_ids).detach() # type: ignore
533
+ elif ig_baseline == "zero":
534
+ baseline_embeddings = torch.zeros_like(input_embeddings)
535
+ elif ig_baseline == "random":
536
+ baseline_embeddings = torch.randn_like(input_embeddings)
537
+ elif ig_baseline == "avg":
538
+ baseline_embeddings = input_embeddings.mean(
539
+ dim=1, keepdim=True
540
+ ).expand_as(input_embeddings)
541
+ else:
542
+ raise ValueError(
543
+ f"Unsupported baseline type '{ig_baseline}'. Choose from 'pad', 'zero', 'random', 'avg'."
544
+ )
545
+
546
+ embedding_delta = input_embeddings - baseline_embeddings
547
+ total_gradients = torch.zeros_like(input_embeddings)
548
+ steps = max(1, ig_steps)
549
+ for step in range(1, steps + 1):
550
+ alpha = float(step) / float(steps)
551
+ interpolated_embeddings = (
552
+ baseline_embeddings + alpha * embedding_delta
553
+ ).detach()
554
+ interpolated_embeddings.requires_grad_(True)
555
+ self.zero_grad(set_to_none=True) # type: ignore
556
+
557
+ with torch.enable_grad():
558
+ objective = _objective_from_embeddings(interpolated_embeddings)
559
+
560
+ gradients = torch.autograd.grad(
561
+ outputs=objective,
562
+ inputs=interpolated_embeddings,
563
+ retain_graph=False,
564
+ create_graph=False,
565
+ )[0]
566
+ total_gradients += gradients.detach()
567
+
568
+ averaged_gradients = total_gradients / float(steps)
569
+ integrated_gradients = embedding_delta * averaged_gradients
570
+ token_importance = integrated_gradients.norm(p=2, dim=-1)
571
+ saliency_maps = []
572
+ sequence_len = top_sequences.size(1)
573
+ prompt_positions = torch.arange(sequence_len, device=top_sequences.device)
574
+ prompt_mask = (prompt_positions.unsqueeze(0) < prefix_len) & (
575
+ top_sequences != self.tokenizer.pad_token_id # type: ignore
576
+ )
577
+
578
+ for sequence_ids, importance_scores, sentence, mask in zip(
579
+ top_sequences,
580
+ token_importance,
581
+ input_sentences,
582
+ prompt_mask,
583
+ ):
584
+ selected_ids = sequence_ids[mask]
585
+ selected_scores = importance_scores[mask]
586
+
587
+ if selected_scores.numel() == 0:
588
+ saliency_maps.append({
589
+ "input_sentence": sentence,
590
+ "token_ids": [],
591
+ "token_strings": [],
592
+ "saliency_scores": [],
593
+ "saliency_method": method,
594
+ "saliency_ansi": "",
595
+ "saliency_html": "",
596
+ })
597
+ continue
598
+
599
+ max_score = selected_scores.max().clamp(min=1e-12)
600
+ normalized_scores = (selected_scores / max_score).tolist()
601
+ selected_ids_list = selected_ids.tolist()
602
+ token_strings = [
603
+ self.tokenizer.decode( # type: ignore
604
+ [token_id],
605
+ skip_special_tokens=False,
606
+ clean_up_tokenization_spaces=False,
607
+ )
608
+ for token_id in selected_ids_list
609
+ ]
610
+
611
+ saliency_maps.append({
612
+ "input_sentence": sentence,
613
+ "token_ids": selected_ids_list,
614
+ "token_strings": token_strings,
615
+ "saliency_scores": normalized_scores,
616
+ "saliency_method": method,
617
+ "saliency_ansi": _build_ansi_saliency_text(
618
+ token_strings,
619
+ normalized_scores,
620
+ ),
621
+ "saliency_html": _build_html_saliency_text(
622
+ token_strings,
623
+ normalized_scores,
624
+ ),
625
+ })
626
+
627
+ return saliency_maps
628
+
629
+ def predict_batch(
630
+ self,
631
+ all_outputs,
632
+ batch_size,
633
+ input_sentences,
634
+ sem_groups,
635
+ mentions,
636
+ mentions_id,
637
+ doc_ids,
638
+ start_spans,
639
+ end_spans,
640
+ gold_concept_codes,
641
+ gold_concept_names,
642
+ constrained,
643
+ multiple_answers,
644
+ num_beams,
645
+ explicability_mode: str = "",
646
+ ig_steps: int = 20,
647
+ ig_baseline: str = "pad",
648
+ **kwargs,
649
+ ):
650
+ input_args = {
651
+ k: v.to(self.device) # type: ignore
652
+ for k, v in self.tokenizer.batch_encode_plus( # type: ignore
653
+ input_sentences, padding="longest", return_tensors="pt"
654
+ ).items()
655
+ }
656
+
657
+ # Constrained decoding
658
+ prefix_allowed_tokens_fn = None
659
+ if constrained:
660
+ if self.candidate_trie is None: # type: ignore
661
+ raise ValueError(
662
+ "candidate_trie is not loaded in the model. Use constrained=False."
663
+ )
664
+ prefix_allowed_tokens_fn = get_prefix_allowed_tokens_fn(
665
+ model=self,
666
+ sources=input_sentences,
667
+ sem_groups=sem_groups,
668
+ multiple_answers=multiple_answers,
669
+ )
670
+ if self.tokenizer.sep_token_id: # type: ignore
671
+ eos_token_id = self.tokenizer.sep_token_id # type: ignore
672
+ else:
673
+ eos_token_id = self.tokenizer.eos_token_id # type: ignore
674
+ outputs = self.generate( # type: ignore
675
+ **input_args,
676
+ max_new_tokens=128,
677
+ num_beams=num_beams,
678
+ num_return_sequences=num_beams,
679
+ output_scores=True,
680
+ return_dict_in_generate=True,
681
+ prefix_allowed_tokens_fn=prefix_allowed_tokens_fn,
682
+ eos_token_id=eos_token_id, # type: ignore
683
+ **kwargs,
684
+ )
685
+ decoded_sequences = self.tokenizer.batch_decode( # type: ignore
686
+ outputs.sequences, # type: ignore
687
+ skip_special_tokens=False,
688
+ clean_up_tokenization_spaces=True,
689
+ )
690
+ cleaned_output_sequences = skip_undesired_tokens(
691
+ decoded_sequences,
692
+ self.tokenizer, # type: ignore
693
+ )
694
+
695
+ prefix_len = input_args["input_ids"].size(1)
696
+
697
+ base_sem_groups = sem_groups.copy()
698
+ base_mentions = mentions.copy()
699
+ base_mentions_id = mentions_id.copy()
700
+ base_doc_ids = doc_ids.copy()
701
+ base_start_spans = start_spans.copy()
702
+ base_end_spans = end_spans.copy()
703
+ base_gold_concept_codes = gold_concept_codes.copy()
704
+ base_gold_concept_names = gold_concept_names.copy()
705
+
706
+ # Duplicate sem_groups and mentions for each beam
707
+ sem_groups = [x for x in sem_groups for _ in range(num_beams)]
708
+ mentions = [x for x in mentions for _ in range(num_beams)]
709
+ mentions_id = [x for x in mentions_id for _ in range(num_beams)]
710
+ gold_concept_codes = [x for x in gold_concept_codes for _ in range(num_beams)] # type: ignore
711
+ gold_concept_names = [x for x in gold_concept_names for _ in range(num_beams)] # type: ignore
712
+ start_spans = [x for x in start_spans for _ in range(num_beams)]
713
+ end_spans = [x for x in end_spans for _ in range(num_beams)]
714
+ doc_ids = [x for x in doc_ids for _ in range(num_beams)]
715
+ # Parse predictions
716
+ pred_concept_codes, pred_concept_names = parse_prediction(
717
+ cleaned_output_sequences,
718
+ sem_groups,
719
+ self.text_to_code, # type: ignore
720
+ multiple_answers=multiple_answers,
721
+ )
722
+ scores = compute_score(
723
+ outputs,
724
+ self.tokenizer, # type: ignore
725
+ prefix_len=prefix_len,
726
+ )
727
+ beam_scores = [
728
+ float(torch.exp(s)) if num_beams > 1 else float("nan")
729
+ for s in (
730
+ outputs.sequences_scores # type: ignore
731
+ if num_beams > 1
732
+ else [torch.tensor(float("nan"))] * len(scores)
733
+ )
734
+ ]
735
+ all_outputs.extend([
736
+ {
737
+ "mention": mention,
738
+ "doc_id": doc_id,
739
+ "mention_id": mention_id,
740
+ "start_span": start_span,
741
+ "end_span": end_span,
742
+ "semantic_group": group,
743
+ "gold_concept_code": gold_concept_code,
744
+ "gold_concept_name": gold_concept_name,
745
+ "pred_concept_name": pred_concept_name,
746
+ "pred_concept_code": pred_concept_code,
747
+ "score": score,
748
+ "beam_score": beam_score,
749
+ "rank": rank + 1,
750
+ }
751
+ for score, beam_score, pred_concept_code, pred_concept_name, mention, doc_id, mention_id, start_span, end_span, group, gold_concept_code, gold_concept_name, rank in zip(
752
+ scores,
753
+ beam_scores,
754
+ pred_concept_codes,
755
+ pred_concept_names,
756
+ mentions,
757
+ doc_ids,
758
+ mentions_id,
759
+ start_spans,
760
+ end_spans,
761
+ sem_groups,
762
+ gold_concept_codes,
763
+ gold_concept_names,
764
+ list(range(num_beams)) * batch_size,
765
+ )
766
+ ])
767
+
768
+ explicability_mode = explicability_mode.strip().lower()
769
+ if explicability_mode not in {"", "simple", "integrated"}:
770
+ raise ValueError(
771
+ "explicability must be one of: '', 'simple', 'integrated'."
772
+ )
773
+
774
+ saliency_maps = []
775
+ if explicability_mode:
776
+ saliency_maps = self._compute_gradient_saliency(
777
+ input_sentences=input_sentences,
778
+ generated_sequences=outputs.sequences, # type: ignore
779
+ num_beams=num_beams,
780
+ prefix_len=prefix_len,
781
+ saliency_method=explicability_mode,
782
+ ig_steps=ig_steps,
783
+ ig_baseline=ig_baseline,
784
+ )
785
+ for idx, saliency_map in enumerate(saliency_maps):
786
+ top_prediction_index = idx * num_beams
787
+ saliency_map.update({
788
+ "mention": base_mentions[idx],
789
+ "doc_id": base_doc_ids[idx],
790
+ "mention_id": base_mentions_id[idx],
791
+ "start_span": base_start_spans[idx],
792
+ "end_span": base_end_spans[idx],
793
+ "semantic_group": base_sem_groups[idx],
794
+ "gold_concept_code": base_gold_concept_codes[idx],
795
+ "gold_concept_name": base_gold_concept_names[idx],
796
+ "pred_concept_name": pred_concept_names[top_prediction_index],
797
+ "pred_concept_code": pred_concept_codes[top_prediction_index],
798
+ "score": scores[top_prediction_index],
799
+ "rank": 1,
800
+ })
801
+
802
+ print(f"Sampling completed. Generated {len(all_outputs)} predictions.")
803
+ return all_outputs, cleaned_output_sequences, saliency_maps
804
+
805
+ def sample(
806
+ self,
807
+ bigbio_pages: list[dict], # type: ignore
808
+ num_beams: int = 5,
809
+ constrained: bool = True,
810
+ explicability_mode: str = "",
811
+ multiple_answers: bool = False,
812
+ batch_size: int = 8,
813
+ start_entity: str = "[",
814
+ end_entity: str = "]",
815
+ start_group: str = "{",
816
+ end_group: str = "}",
817
+ show_progress: bool = True,
818
+ **kwargs,
819
+ ) -> (
820
+ list[dict[str, object]]
821
+ | tuple[list[dict[str, object]], list[dict[str, object]]]
822
+ ):
823
+ explicability_mode = explicability_mode.strip().lower()
824
+ if explicability_mode not in {"", "simple", "integrated"}:
825
+ raise ValueError(
826
+ "explicability must be one of: '', 'simple', 'integrated'."
827
+ )
828
+
829
+ # Prepare input batch
830
+ if self.lang == "fr": # type: ignore
831
+ nlp = nltk.data.load("tokenizers/punkt/french.pickle")
832
+ elif self.lang == "en": # type: ignore
833
+ nlp = nltk.data.load("tokenizers/punkt/english.pickle")
834
+ elif self.lang == "es": # type: ignore
835
+ nlp = nltk.data.load("tokenizers/punkt/spanish.pickle")
836
+ else:
837
+ raise ValueError(f"Unsupported language: {self.lang}") # type: ignore
838
+
839
+ print(
840
+ f"Starting sampling on {len(bigbio_pages)} pages (lang={getattr(self, 'lang', 'unknown')}, constrained={constrained}, beams={num_beams}, batch_size={batch_size})"
841
+ )
842
+
843
+ def _progress(
844
+ iterable, desc: str, total: Optional[int] = None, show: bool = True
845
+ ):
846
+ if show:
847
+ return tqdm(iterable, desc=desc, total=total)
848
+ return iterable
849
+
850
+ all_outputs = []
851
+ all_sources = []
852
+ all_targets = []
853
+ all_entities_info = []
854
+ for data in bigbio_pages:
855
+ sources, targets, entities_info = parse_text(
856
+ data=data,
857
+ start_entity=start_entity,
858
+ end_entity=end_entity,
859
+ start_group=start_group,
860
+ end_group=end_group,
861
+ nlp=nlp, # type: ignore
862
+ )
863
+ all_sources.append(sources)
864
+ all_targets.append(targets)
865
+ all_entities_info.append(entities_info)
866
+
867
+ def _build_sequential_batches():
868
+ # Keep per-page order while still processing multiple pages per batch.
869
+ page_positions = [0] * len(all_sources)
870
+ next_page_idx = 0
871
+ active_pages = []
872
+ batches = []
873
+
874
+ while active_pages or next_page_idx < len(all_sources):
875
+ while len(active_pages) < batch_size and next_page_idx < len(
876
+ all_sources
877
+ ):
878
+ if len(all_sources[next_page_idx]) > 0:
879
+ active_pages.append(next_page_idx)
880
+ next_page_idx += 1
881
+
882
+ if not active_pages:
883
+ break
884
+
885
+ batch = []
886
+ next_active_pages = []
887
+ for page_idx in active_pages:
888
+ item_idx = page_positions[page_idx]
889
+ batch.append((
890
+ all_sources[page_idx][item_idx],
891
+ all_targets[page_idx][item_idx],
892
+ all_entities_info[page_idx][item_idx],
893
+ ))
894
+ page_positions[page_idx] += 1
895
+ if page_positions[page_idx] < len(all_sources[page_idx]):
896
+ next_active_pages.append(page_idx)
897
+
898
+ batches.append(batch)
899
+ active_pages = next_active_pages
900
+
901
+ return batches
902
+
903
+ all_batches = _build_sequential_batches()
904
+
905
+ print(
906
+ f"Input preparation completed. Running generation on {len(all_batches)} batches."
907
+ )
908
+
909
+ all_outputs = []
910
+ all_saliency_maps = []
911
+ batch_previous_targets = {}
912
+ for batch in _progress(
913
+ all_batches,
914
+ desc="Processing batches",
915
+ total=len(all_batches),
916
+ show=show_progress,
917
+ ):
918
+ input_sentences = []
919
+ sem_groups = []
920
+ mentions = []
921
+ doc_ids = []
922
+ mentions_id = []
923
+ gold_concept_codes = []
924
+ gold_concept_names = []
925
+ start_spans = []
926
+ end_spans = []
927
+ for source, target, entity in batch:
928
+ doc_id = entity["doc_id"]
929
+ if doc_id not in batch_previous_targets:
930
+ batch_previous_targets[doc_id] = ""
931
+ previous_targets = batch_previous_targets.get(doc_id)
932
+
933
+ input_sentences.append(
934
+ add_headers_to_prompt(
935
+ source,
936
+ target,
937
+ previous_targets, # type: ignore
938
+ )
939
+ )
940
+ sem_groups.append(entity["semantic_group"])
941
+ mentions.append(entity["mention"])
942
+ doc_ids.append(doc_id)
943
+ mentions_id.append(entity["mention_id"])
944
+ start_spans.append(entity["start_span"])
945
+ end_spans.append(entity["end_span"])
946
+ gold_concept_codes.append(entity.get("gold_concept_code", None)) # type: ignore
947
+ gold_concept_names.append(entity.get("gold_concept_name", None)) # type: ignore
948
+ all_outputs, cleaned_output_sequences, batch_saliency_maps = (
949
+ self.predict_batch(
950
+ all_outputs=all_outputs,
951
+ batch_size=batch_size,
952
+ input_sentences=input_sentences,
953
+ sem_groups=sem_groups,
954
+ mentions=mentions,
955
+ mentions_id=mentions_id,
956
+ doc_ids=doc_ids,
957
+ start_spans=start_spans,
958
+ end_spans=end_spans,
959
+ gold_concept_codes=gold_concept_codes,
960
+ gold_concept_names=gold_concept_names,
961
+ constrained=constrained,
962
+ multiple_answers=multiple_answers,
963
+ num_beams=num_beams,
964
+ explicability_mode=explicability_mode,
965
+ **kwargs,
966
+ )
967
+ )
968
+ if explicability_mode:
969
+ all_saliency_maps.extend(batch_saliency_maps)
970
+ for i, doc_id in enumerate(doc_ids):
971
+ clean_sentence = cleaned_output_sequences[num_beams * i]
972
+ clean_sentence = start_entity + clean_sentence.split(start_entity)[-1]
973
+ clean_sentence = clean_sentence.rstrip() + "\n"
974
+ batch_previous_targets[doc_id] += clean_sentence
975
+
976
+ if explicability_mode:
977
+ return all_outputs, all_saliency_maps # type: ignore
978
+ return all_outputs # type: ignore
979
+
980
+ def encode(self, sentence):
981
+ return self.tokenizer.encode(sentence, return_tensors="pt")[0] # type: ignore