Text Generation
Transformers
Safetensors
Spanish
llama_longbel
biomedical-entity-linking
entity-linking
entity-disambiguation
named-entity-linking
biomedical
healthcare
snomed
spaccc
medprocner
symptemist
distemist
constrained-decoding
causal-lm
llm
conversational
custom_code
Eval Results (legacy)
Instructions to use Aremaki/LongBEL_1B_SPACCC with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use Aremaki/LongBEL_1B_SPACCC with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="Aremaki/LongBEL_1B_SPACCC", trust_remote_code=True) messages = [ {"role": "user", "content": "Who are you?"}, ] pipe(messages)# pip install -U transformers accelerate # Load model directly from transformers import AutoModelForCausalLM model = AutoModelForCausalLM.from_pretrained("Aremaki/LongBEL_1B_SPACCC", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use Aremaki/LongBEL_1B_SPACCC with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "Aremaki/LongBEL_1B_SPACCC" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "Aremaki/LongBEL_1B_SPACCC", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker
docker model run hf.co/Aremaki/LongBEL_1B_SPACCC
- SGLang
How to use Aremaki/LongBEL_1B_SPACCC with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "Aremaki/LongBEL_1B_SPACCC" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "Aremaki/LongBEL_1B_SPACCC", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "Aremaki/LongBEL_1B_SPACCC" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "Aremaki/LongBEL_1B_SPACCC", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }' - Docker Model Runner
How to use Aremaki/LongBEL_1B_SPACCC with Docker Model Runner:
docker model run hf.co/Aremaki/LongBEL_1B_SPACCC
Download longbel.py from Aremaki/LongBEL_1B_SPACCC: direct link, hf CLI and curl.
- Browser
- Download file 34.4 kB
-
https://huggingface.co/Aremaki/LongBEL_1B_SPACCC/resolve/main/longbel.py
- Command line
-
hf download hf://Aremaki/LongBEL_1B_SPACCC/longbel.py
-
curl -L -o longbel.py https://huggingface.co/Aremaki/LongBEL_1B_SPACCC/resolve/main/longbel.py
34.4 kB
| """ | |
| Core models for LongBEL | |
| """ | |
| # Copyright (c) Facebook, Inc. and its affiliates. | |
| # All rights reserved. | |
| # | |
| # This source code is licensed under the license found in the | |
| # LICENSE file in the root directory of this source tree. | |
| import json | |
| import logging | |
| import os | |
| import pickle | |
| import re | |
| from html import escape | |
| import torch | |
| import torch.nn.functional as F | |
| from huggingface_hub import hf_hub_download | |
| from IPython.display import HTML, display | |
| from tqdm.auto import tqdm | |
| from transformers import ( | |
| AutoTokenizer, | |
| LlamaForCausalLM, | |
| PretrainedConfig, | |
| ) | |
| logger = logging.getLogger(__name__) | |
| logging.basicConfig( | |
| level=logging.INFO, # Display INFO and above | |
| format="%(levelname)s - %(message)s", | |
| ) | |
| # Define a simple config class that inherits from PretrainedConfig | |
| class LLamaLongBELConfig(PretrainedConfig): | |
| model_type = "llama_longbel" | |
| def __init__(self, **kwargs): | |
| # Ensure it has llama as base | |
| kwargs.setdefault("model_type", "llama") | |
| super().__init__(**kwargs) | |
| def clean_natural(text): | |
| return ( | |
| text.replace("\xa0", " ") | |
| .replace("{", "(") | |
| .replace("}", ")") | |
| .replace("[", "(") | |
| .replace("]", ")") | |
| .replace("\n", " ") | |
| ) | |
| def parse_text( | |
| data, | |
| start_entity, | |
| end_entity, | |
| start_group, | |
| end_group, | |
| ) -> tuple[list[str], list[str], list[dict[str, str]]]: | |
| """Create simple (source, target) pairs per entity. | |
| For each entity in the BigBio page, returns one pair where: | |
| - source: the sentence text that contains the entity mention | |
| - target: "<entity> is <annotation>" where <annotation> is the best synonym | |
| if available (or the normalized id otherwise). | |
| """ | |
| source_sentences: list[str] = [] | |
| tsv_lines: list[dict[str, str]] = [] | |
| target_texts_dict: dict[tuple[tuple[int, int], ...], str] = {} | |
| source_texts_dict: dict[tuple[tuple[int, int], ...], str] = {} | |
| tsv_lines_dict: dict[tuple[tuple[int, int], ...], dict[str, str]] = {} | |
| all_passages = {} | |
| for i, passage in enumerate(data.get("passages", [])): | |
| all_passages[i] = clean_natural(passage["text"][0]) | |
| for passage_id, passage in enumerate(data.get("passages", [])): | |
| passage_text = passage["text"][0] | |
| start_offset_passage = passage["offsets"][0][0] | |
| end_offset_passage = passage["offsets"][0][1] | |
| passage_text = clean_natural(passage_text) | |
| # Iterate over entities and emit one pair per entity found in this passage | |
| for entity in data.get("entities", []): | |
| # min and max of all entity offsets to get the global span of the entity for filtering sentences | |
| global_start = min(off[0] for off in entity["offsets"]) | |
| global_end = max(off[1] for off in entity["offsets"]) | |
| # Keep only entities whose start falls inside this passage | |
| if not (start_offset_passage <= global_start < end_offset_passage): | |
| continue | |
| entity_text = " ".join(entity["text"]) | |
| entity_text = clean_natural(entity_text) | |
| # Define entity group | |
| group_annotation = entity.get("type") | |
| # Get all offsets, convert to relative, and filter for this sentence | |
| relative_entity_spans = [] | |
| for off in entity["offsets"]: | |
| global_start_off, global_end_off = off | |
| if not (start_offset_passage <= global_start_off < end_offset_passage): | |
| continue | |
| rel_start_off = global_start_off - start_offset_passage | |
| rel_end_off = global_end_off - start_offset_passage | |
| relative_entity_spans.append((rel_start_off, rel_end_off)) | |
| relative_entity_spans.sort(key=lambda x: x[0]) | |
| marked_text = passage_text | |
| for start_in_sent, end_in_sent in relative_entity_spans: | |
| marked_text = ( | |
| marked_text[:start_in_sent] | |
| + start_entity | |
| + marked_text[start_in_sent:end_in_sent] | |
| + end_entity | |
| + marked_text[end_in_sent:] | |
| ) | |
| for other_passage_id, other_passage_text in all_passages.items(): | |
| if other_passage_id < passage_id: | |
| marked_text = other_passage_text + "\n" + marked_text | |
| elif other_passage_id > passage_id: | |
| marked_text = marked_text + "\n" + other_passage_text | |
| # Emit the pair | |
| doc_id = data.get("id", "") | |
| tsv_line = { | |
| "doc_id": doc_id, | |
| "semantic_group": group_annotation, | |
| "start_span": global_start, | |
| "end_span": global_end, | |
| "mention": entity_text, | |
| } | |
| # Keep original entity id if present | |
| if entity.get("id") is not None: | |
| tsv_line["mention_id"] = str(entity["id"]) | |
| if entity.get("normalized"): | |
| tsv_line["gold_concept_code"] = entity["normalized"][0]["db_id"] | |
| tsv_line["gold_concept_name"] = entity["normalized"][0]["db_match"] | |
| key = (global_start, global_end, group_annotation) | |
| tsv_lines_dict[key] = tsv_line | |
| source_texts_dict[key] = marked_text | |
| target_entity_text = ( | |
| start_entity | |
| + entity_text | |
| + end_entity | |
| + start_group | |
| + group_annotation | |
| + end_group | |
| ) | |
| target_texts_dict[key] = target_entity_text | |
| # Sort keys to have a deterministic order | |
| target_texts = [] | |
| sorted_keys = sorted( | |
| tsv_lines_dict.keys(), | |
| key=lambda x: (x[0], x[1], str(x[2]) if len(x) > 2 else ""), | |
| ) | |
| for entity_idx, entity_span in enumerate(sorted_keys): | |
| tsv_line = tsv_lines_dict[entity_span] | |
| if "mention_id" not in tsv_line: | |
| tsv_line["mention_id"] = f"{data.get('id', '')}.{entity_idx + 1}" | |
| tsv_lines.append(tsv_line) | |
| source_sentences.append(source_texts_dict[entity_span]) | |
| target_texts.append(target_texts_dict[entity_span]) | |
| return source_sentences, target_texts, tsv_lines # type: ignore | |
| def get_prefix_allowed_tokens_fn( | |
| model, | |
| sources: list[str], | |
| sem_groups: list[str], | |
| multiple_answers: bool = False, | |
| ): | |
| candidates_trie = model.candidate_trie # type: ignore | |
| sep_token_id = model.tokenizer.sep_token_id | |
| eos_token_id = model.tokenizer.eos_token_id | |
| pad_token_id = model.tokenizer.pad_token_id | |
| plus_token_id = model.tokenizer.convert_tokens_to_ids("<+>") # type: ignore | |
| end_group_token_id = model.tokenizer.convert_tokens_to_ids("}") # type: ignore | |
| def prefix_allowed_tokens_fn(batch_id, sent): | |
| sent = sent.tolist() | |
| if len(sent) > 1 and sent[-1] in [eos_token_id, pad_token_id, sep_token_id]: | |
| if sep_token_id: | |
| return [sep_token_id, pad_token_id, eos_token_id] | |
| else: | |
| return [pad_token_id, eos_token_id] | |
| # Remove the prefix from the sent | |
| index_sep = len(sent) - 1 - sent[::-1].index(end_group_token_id) | |
| sent = sent[index_sep:] | |
| sem_group = sem_groups[batch_id] | |
| # Remove everything up to last sep_token_id and add prefix | |
| if multiple_answers and plus_token_id in sent: | |
| index_plus = len(sent) - 1 - sent[::-1].index(plus_token_id) | |
| # Start fresh with decoder start | |
| if index_plus == len(sent) - 1: | |
| sent = [end_group_token_id] | |
| # If there are tokens after the last plus_token_id, keep them | |
| else: | |
| sent = [end_group_token_id] + sent[index_plus + 1 :] | |
| trie_out = candidates_trie[ | |
| sem_group # type: ignore | |
| ].get(sent) | |
| if eos_token_id in trie_out: | |
| if sep_token_id: | |
| trie_out += [sep_token_id] | |
| if multiple_answers: | |
| trie_out += [plus_token_id] | |
| elif not trie_out: | |
| if sep_token_id: | |
| return [sep_token_id, pad_token_id, eos_token_id] | |
| else: | |
| return [pad_token_id, eos_token_id] | |
| return trie_out | |
| return prefix_allowed_tokens_fn | |
| def add_headers_to_prompt(source: str, target: str, previous_targets: str): | |
| if not previous_targets: | |
| previous_targets = "None" | |
| input_sentence = f"### Context\n{source.rstrip()}\n\n### Previous Normalizations\n{previous_targets.rstrip()}\n\n### Prediction\n{target.rstrip()}" | |
| return input_sentence | |
| def parse_prediction( | |
| outputs: list[str], | |
| sem_groups: list[str], | |
| text_to_code: dict[str, dict[str, str]] | None = None, | |
| multiple_answers: bool = False, | |
| ) -> tuple[list[str], list[str]]: | |
| codes = [] | |
| predictions = [] | |
| for output, group in zip(outputs, sem_groups, strict=False): | |
| splits = output.split("} ") # type: ignore | |
| if len(splits) > 1 and splits[-1].strip(): | |
| prediction = splits[-1].strip().replace("<SEP>", "") | |
| if text_to_code: | |
| if multiple_answers: | |
| prediction_list = prediction.split("<+>") # type: ignore | |
| code_list = set() | |
| for pred in prediction_list: | |
| code_list.add(text_to_code[group].get(pred.strip(), "NO_CODE")) | |
| if len(code_list) > 1 and "NO_CODE" in code_list: | |
| code_list.remove("NO_CODE") | |
| code = "+".join(code_list) | |
| else: | |
| code = text_to_code[group].get(prediction, "NO_CODE") | |
| else: | |
| code = "NO_CODE" | |
| else: | |
| print( | |
| "IndexError: splitting failed or empty prediction, adding empty string as prediction." | |
| ) | |
| prediction = "NO_PREDICTION" | |
| code = "NO_CODE" | |
| codes.append(code) | |
| predictions.append(prediction) | |
| return codes, predictions | |
| def compute_score(outputs, tokenizer, prefix_len=0): | |
| sequences = outputs.sequences # (N, seq_len) | |
| scores = outputs.scores # list length T = # generated tokens | |
| N, total_len = sequences.shape | |
| T = len(scores) | |
| # keep only the generated part (completion) | |
| sequences = sequences[:, prefix_len : prefix_len + T] | |
| # Make sure score is not longer than sequences | |
| if len(scores) > sequences.size(1): | |
| scores = scores[: sequences.size(1)] | |
| # Compute as usual but now only for completion tokens | |
| mask = ( | |
| (sequences != tokenizer.pad_token_id) | |
| & (sequences != tokenizer.eos_token_id) | |
| & (sequences != tokenizer.bos_token_id) | |
| ) | |
| # log-prob for each generated token | |
| logprob_steps = [] | |
| for t, logits in enumerate(scores): | |
| log_probs_t = F.log_softmax(logits, dim=-1) | |
| token_t = sequences[:, t] | |
| idx = torch.arange(N) | |
| logprob_steps.append(log_probs_t[idx, token_t]) | |
| logprobs = torch.stack(logprob_steps, dim=1) | |
| logprobs.masked_fill_(~mask, 0) | |
| lengths = mask.sum(dim=1).clamp(min=1) | |
| confidence = torch.exp(logprobs.sum(dim=1) / lengths) | |
| return confidence.tolist() | |
| def skip_undesired_tokens(outputs, tokenizer): | |
| sep_token = "<SEP>" | |
| plus_token = "<+>" | |
| # Build the list of special tokens to remove | |
| tokens_to_remove = tokenizer.all_special_tokens[:2] | |
| cleaned_outputs = [] | |
| for sequence in outputs: | |
| # Remove undesired special tokens | |
| for token in tokens_to_remove: | |
| sequence = sequence.replace(token, "") | |
| # Remove spaces *immediately* after the sep_token adn plus_token (e.g. "<sep> text" → "<sep>text") | |
| sequence = re.sub(rf"({re.escape(plus_token)})\s+", r"\1", sequence) | |
| sequence = re.sub(rf"({re.escape(sep_token)})\s+", r"\1", sequence) | |
| cleaned_outputs.append(sequence.strip()) | |
| return cleaned_outputs | |
| def _score_to_rgb(score: float) -> tuple[int, int, int]: | |
| clipped_score = max(0.0, min(1.0, score)) | |
| red = 255 | |
| channel = int(255 * (1.0 - clipped_score)) | |
| return red, channel, channel | |
| def _build_ansi_saliency_text( | |
| token_texts: list[str], saliency_scores: list[float] | |
| ) -> str: | |
| chunks = [] | |
| for token_text, score in zip(token_texts, saliency_scores, strict=False): | |
| red, green, blue = _score_to_rgb(score) | |
| chunks.append(f"\x1b[48;2;{red};{green};{blue}m{token_text}\x1b[0m") | |
| return "".join(chunks) | |
| def _build_html_saliency_text( | |
| token_texts: list[str], saliency_scores: list[float] | |
| ) -> str: | |
| chunks = [] | |
| for token_text, score in zip(token_texts, saliency_scores, strict=False): | |
| red, green, blue = _score_to_rgb(score) | |
| chunks.append( | |
| f'<span style="background-color: rgb({red}, {green}, {blue});">{escape(token_text)}</span>' | |
| ) | |
| return "".join(chunks) | |
| class LLamaLongBEL(LlamaForCausalLM): | |
| config_class = LLamaLongBELConfig | |
| def __init__(self, config, *args, **kwargs): | |
| # Initialize the parent LlamaForCausalLM | |
| super().__init__(config, *args, **kwargs) | |
| self.text_to_code = None | |
| self.candidate_trie = None | |
| self.tokenizer = None | |
| def from_pretrained( | |
| cls, | |
| pretrained_model_name_or_path, | |
| *args, | |
| text_to_code_path=None, | |
| candidate_trie_path=None, | |
| **kwargs, | |
| ): | |
| # Remove custom kwargs before passing to parent | |
| custom_kwargs = { | |
| "text_to_code_path": text_to_code_path, | |
| "candidate_trie_path": candidate_trie_path, | |
| } | |
| # Call parent's from_pretrained | |
| model = super().from_pretrained( | |
| pretrained_model_name_or_path, | |
| *args, | |
| **{k: v for k, v in kwargs.items() if k not in custom_kwargs}, | |
| ) | |
| # Set up tokenizer | |
| model.tokenizer = AutoTokenizer.from_pretrained( | |
| pretrained_model_name_or_path, use_fast=True | |
| ) | |
| model.tokenizer.padding_side = "left" | |
| # Load text_to_code | |
| text_to_code_file_local = ( | |
| text_to_code_path | |
| if text_to_code_path is not None | |
| else os.path.join(pretrained_model_name_or_path, "text_to_code.json") | |
| ) | |
| try: | |
| if os.path.exists(text_to_code_file_local): | |
| with open(text_to_code_file_local, encoding="utf-8") as f: | |
| model.text_to_code = json.load(f) | |
| logger.info( | |
| f"Loaded text_to_code.json from local path: {text_to_code_file_local}" | |
| ) | |
| else: | |
| text_to_code_path_hf = hf_hub_download( | |
| repo_id=pretrained_model_name_or_path, | |
| filename="text_to_code.json", | |
| ) | |
| with open(text_to_code_path_hf, encoding="utf-8") as f: | |
| model.text_to_code = json.load(f) | |
| logger.info( | |
| f"Loaded text_to_code.json from HF Hub: {text_to_code_path_hf}" | |
| ) | |
| except Exception: | |
| logger.warning("text_to_code.json not found (local or HF hub)") | |
| model.text_to_code = None | |
| # Load candidate_trie | |
| candidate_trie_file_local = ( | |
| candidate_trie_path | |
| if candidate_trie_path is not None | |
| else os.path.join(pretrained_model_name_or_path, "candidate_trie.pkl") | |
| ) | |
| try: | |
| if os.path.exists(candidate_trie_file_local): | |
| with open(candidate_trie_file_local, "rb") as f: | |
| model.candidate_trie = pickle.load(f) | |
| logger.info( | |
| f"Loaded candidate_trie.pkl from local path: {candidate_trie_file_local}" | |
| ) | |
| else: | |
| candidate_trie_path_hf = hf_hub_download( | |
| repo_id=pretrained_model_name_or_path, | |
| filename="candidate_trie.pkl", | |
| ) | |
| with open(candidate_trie_path_hf, "rb") as f: | |
| model.candidate_trie = pickle.load(f) | |
| logger.info( | |
| f"Loaded candidate_trie.pkl from HF Hub: {candidate_trie_path_hf}" | |
| ) | |
| except Exception: | |
| logger.warning("candidate_trie.pkl not found (local or HF hub)") | |
| model.candidate_trie = None | |
| return model | |
| def _compute_gradient_saliency( | |
| self, | |
| input_sentences: list[str], | |
| generated_sequences: torch.Tensor, | |
| num_beams: int, | |
| prefix_len: int, | |
| ) -> list[dict[str, object]]: | |
| if not input_sentences: | |
| return [] | |
| top_sequence_indices = ( | |
| torch.arange( | |
| len(input_sentences), | |
| device=generated_sequences.device, | |
| ) | |
| * num_beams | |
| ) | |
| top_sequences = generated_sequences.index_select(0, top_sequence_indices) | |
| attention_mask = (top_sequences != self.tokenizer.pad_token_id).long() # type: ignore | |
| input_embeddings = self.get_input_embeddings()(top_sequences).detach() # type: ignore | |
| next_tokens = top_sequences[:, 1:] | |
| output_token_mask = torch.zeros_like(next_tokens, dtype=torch.bool) | |
| if prefix_len > 0: | |
| output_token_mask[:, prefix_len - 1 :] = True | |
| valid_token_mask = output_token_mask & ( | |
| (next_tokens != self.tokenizer.pad_token_id) # type: ignore | |
| & (next_tokens != self.tokenizer.eos_token_id) # type: ignore | |
| & (next_tokens != self.tokenizer.bos_token_id) # type: ignore | |
| ) | |
| def _objective_from_embeddings(embeddings: torch.Tensor) -> torch.Tensor: | |
| forward_outputs = self( # type: ignore | |
| inputs_embeds=embeddings, | |
| attention_mask=attention_mask, | |
| use_cache=False, | |
| return_dict=True, | |
| ) | |
| logits = forward_outputs.logits[:, :-1, :] | |
| log_probs = F.log_softmax(logits, dim=-1) | |
| token_log_probs = log_probs.gather( | |
| dim=-1, | |
| index=next_tokens.unsqueeze(-1), | |
| ).squeeze(-1) | |
| return token_log_probs.masked_select(valid_token_mask).sum() | |
| simple_embeddings = input_embeddings.detach() | |
| simple_embeddings.requires_grad_(True) | |
| self.zero_grad(set_to_none=True) # type: ignore | |
| with torch.enable_grad(): | |
| objective = _objective_from_embeddings(simple_embeddings) | |
| gradients = torch.autograd.grad( | |
| outputs=objective, | |
| inputs=simple_embeddings, | |
| retain_graph=False, | |
| create_graph=False, | |
| )[0] | |
| token_importance = gradients.norm(p=2, dim=-1) | |
| saliency_maps = [] | |
| sequence_len = top_sequences.size(1) | |
| prompt_positions = torch.arange(sequence_len, device=top_sequences.device) | |
| prompt_mask = (prompt_positions.unsqueeze(0) < prefix_len) & ( | |
| top_sequences != self.tokenizer.pad_token_id # type: ignore | |
| ) | |
| for sequence_ids, importance_scores, sentence, mask in zip( | |
| top_sequences, token_importance, input_sentences, prompt_mask, strict=False | |
| ): | |
| selected_ids = sequence_ids[mask] | |
| selected_scores = importance_scores[mask] | |
| if selected_scores.numel() == 0: | |
| saliency_maps.append({ | |
| "input_sentence": sentence, | |
| "token_ids": [], | |
| "token_strings": [], | |
| "saliency_scores": [], | |
| "saliency_ansi": "", | |
| "saliency_html": "", | |
| }) | |
| continue | |
| max_score = selected_scores.max().clamp(min=1e-12) | |
| normalized_scores = (selected_scores / max_score).tolist() | |
| selected_ids_list = selected_ids.tolist() | |
| token_strings = [ | |
| self.tokenizer.decode( # type: ignore | |
| [token_id], | |
| skip_special_tokens=False, | |
| clean_up_tokenization_spaces=False, | |
| ) | |
| for token_id in selected_ids_list | |
| ] | |
| saliency_maps.append({ | |
| "input_sentence": sentence, | |
| "token_ids": selected_ids_list, | |
| "token_strings": token_strings, | |
| "saliency_scores": normalized_scores, | |
| "saliency_ansi": _build_ansi_saliency_text( | |
| token_strings, | |
| normalized_scores, | |
| ), | |
| "saliency_html": _build_html_saliency_text( | |
| token_strings, | |
| normalized_scores, | |
| ), | |
| }) | |
| return saliency_maps | |
| def display_saliency_map(self, saliency_map): | |
| saliency_html = re.sub( | |
| r"<span[^>]*>\s*<\|begin_of_text\|>\s*</span>", | |
| "", | |
| saliency_map["saliency_html"], | |
| count=1, | |
| ) | |
| pred_name = escape(str(saliency_map.get("pred_concept_name", ""))) | |
| pred_code = escape(str(saliency_map.get("pred_concept_code", ""))) | |
| full_html = f""" | |
| <div style=" | |
| font-family: Times New Roman, Times, serif, monospace; | |
| font-size: 18px; | |
| line-height: 1.6; | |
| white-space: pre-wrap; | |
| border: 1px solid #ddd; | |
| border-radius: 8px; | |
| padding: 12px; | |
| background: #fafafa; | |
| ">{saliency_html} → {pred_name} ({pred_code})</div> | |
| """ | |
| display(HTML(full_html)) | |
| def predict_batch( | |
| self, | |
| all_outputs, | |
| batch_size, | |
| input_sentences, | |
| sem_groups, | |
| mentions, | |
| mentions_id, | |
| doc_ids, | |
| start_spans, | |
| end_spans, | |
| gold_concept_codes, | |
| gold_concept_names, | |
| constrained, | |
| multiple_answers, | |
| num_beams, | |
| with_saliency_maps: bool = False, | |
| **kwargs, | |
| ): | |
| input_args = { | |
| k: v.to(self.device) # type: ignore | |
| for k, v in self.tokenizer.batch_encode_plus( # type: ignore | |
| input_sentences, padding="longest", return_tensors="pt" | |
| ).items() | |
| } | |
| # Constrained decoding | |
| prefix_allowed_tokens_fn = None | |
| if constrained: | |
| if self.candidate_trie is None: # type: ignore | |
| raise ValueError( | |
| "candidate_trie is not loaded in the model. Use constrained=False." | |
| ) | |
| prefix_allowed_tokens_fn = get_prefix_allowed_tokens_fn( | |
| model=self, | |
| sources=input_sentences, | |
| sem_groups=sem_groups, | |
| multiple_answers=multiple_answers, | |
| ) | |
| if self.tokenizer.sep_token_id: # type: ignore | |
| eos_token_id = self.tokenizer.sep_token_id # type: ignore | |
| else: | |
| eos_token_id = self.tokenizer.eos_token_id # type: ignore | |
| outputs = self.generate( # type: ignore | |
| **input_args, | |
| max_new_tokens=128, | |
| num_beams=num_beams, | |
| num_return_sequences=num_beams, | |
| output_scores=True, | |
| return_dict_in_generate=True, | |
| prefix_allowed_tokens_fn=prefix_allowed_tokens_fn, | |
| eos_token_id=eos_token_id, # type: ignore | |
| **kwargs, | |
| ) | |
| decoded_sequences = self.tokenizer.batch_decode( # type: ignore | |
| outputs.sequences, # type: ignore | |
| skip_special_tokens=False, | |
| clean_up_tokenization_spaces=True, | |
| ) | |
| cleaned_output_sequences = skip_undesired_tokens( | |
| decoded_sequences, | |
| self.tokenizer, # type: ignore | |
| ) | |
| prefix_len = input_args["input_ids"].size(1) | |
| base_sem_groups = sem_groups.copy() | |
| base_mentions = mentions.copy() | |
| base_mentions_id = mentions_id.copy() | |
| base_doc_ids = doc_ids.copy() | |
| base_start_spans = start_spans.copy() | |
| base_end_spans = end_spans.copy() | |
| base_gold_concept_codes = gold_concept_codes.copy() | |
| base_gold_concept_names = gold_concept_names.copy() | |
| # Duplicate sem_groups and mentions for each beam | |
| sem_groups = [x for x in sem_groups for _ in range(num_beams)] | |
| mentions = [x for x in mentions for _ in range(num_beams)] | |
| mentions_id = [x for x in mentions_id for _ in range(num_beams)] | |
| gold_concept_codes = [x for x in gold_concept_codes for _ in range(num_beams)] # type: ignore | |
| gold_concept_names = [x for x in gold_concept_names for _ in range(num_beams)] # type: ignore | |
| start_spans = [x for x in start_spans for _ in range(num_beams)] | |
| end_spans = [x for x in end_spans for _ in range(num_beams)] | |
| doc_ids = [x for x in doc_ids for _ in range(num_beams)] | |
| # Parse predictions | |
| pred_concept_codes, pred_concept_names = parse_prediction( | |
| cleaned_output_sequences, | |
| sem_groups, | |
| self.text_to_code, # type: ignore | |
| multiple_answers=multiple_answers, | |
| ) | |
| scores = compute_score( | |
| outputs, | |
| self.tokenizer, # type: ignore | |
| prefix_len=prefix_len, | |
| ) | |
| beam_scores = [ | |
| float(torch.exp(s)) if num_beams > 1 else float("nan") | |
| for s in ( | |
| outputs.sequences_scores # type: ignore | |
| if num_beams > 1 | |
| else [torch.tensor(float("nan"))] * len(scores) | |
| ) | |
| ] | |
| all_outputs.extend([ | |
| { | |
| "mention": mention, | |
| "doc_id": doc_id, | |
| "mention_id": mention_id, | |
| "start_span": start_span, | |
| "end_span": end_span, | |
| "semantic_group": group, | |
| "gold_concept_code": gold_concept_code, | |
| "gold_concept_name": gold_concept_name, | |
| "pred_concept_name": pred_concept_name, | |
| "pred_concept_code": pred_concept_code, | |
| "score": score, | |
| "beam_score": beam_score, | |
| "rank": rank + 1, | |
| } | |
| 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( | |
| scores, | |
| beam_scores, | |
| pred_concept_codes, | |
| pred_concept_names, | |
| mentions, | |
| doc_ids, | |
| mentions_id, | |
| start_spans, | |
| end_spans, | |
| sem_groups, | |
| gold_concept_codes, | |
| gold_concept_names, | |
| list(range(num_beams)) * batch_size, | |
| strict=False, | |
| ) | |
| ]) | |
| saliency_maps = [] | |
| if with_saliency_maps: | |
| saliency_maps = self._compute_gradient_saliency( | |
| input_sentences=input_sentences, | |
| generated_sequences=outputs.sequences, # type: ignore | |
| num_beams=num_beams, | |
| prefix_len=prefix_len, | |
| ) | |
| for idx, saliency_map in enumerate(saliency_maps): | |
| top_prediction_index = idx * num_beams | |
| saliency_map.update({ | |
| "mention": base_mentions[idx], | |
| "doc_id": base_doc_ids[idx], | |
| "mention_id": base_mentions_id[idx], | |
| "start_span": base_start_spans[idx], | |
| "end_span": base_end_spans[idx], | |
| "semantic_group": base_sem_groups[idx], | |
| "gold_concept_code": base_gold_concept_codes[idx], | |
| "gold_concept_name": base_gold_concept_names[idx], | |
| "pred_concept_name": pred_concept_names[top_prediction_index], | |
| "pred_concept_code": pred_concept_codes[top_prediction_index], | |
| "score": scores[top_prediction_index], | |
| "rank": 1, | |
| }) | |
| print(f"Sampling completed. Generated {len(all_outputs)} predictions.") | |
| return all_outputs, cleaned_output_sequences, saliency_maps | |
| def sample( | |
| self, | |
| bigbio_pages: list[dict], # type: ignore | |
| num_beams: int = 5, | |
| constrained: bool = True, | |
| with_saliency_maps: bool = False, | |
| multiple_answers: bool = False, | |
| batch_size: int = 8, | |
| start_entity: str = "[", | |
| end_entity: str = "]", | |
| start_group: str = "{", | |
| end_group: str = "}", | |
| show_progress: bool = True, | |
| **kwargs, | |
| ) -> ( | |
| list[dict[str, object]] | |
| | tuple[list[dict[str, object]], list[dict[str, object]]] | |
| ): | |
| print( | |
| f"Starting sampling on {len(bigbio_pages)} pages, constrained={constrained}, beams={num_beams}, batch_size={batch_size})" | |
| ) | |
| def _progress(iterable, desc: str, total: int | None = None, show: bool = True): | |
| if show: | |
| return tqdm(iterable, desc=desc, total=total) | |
| return iterable | |
| all_outputs = [] | |
| all_sources = [] | |
| all_targets = [] | |
| all_entities_info = [] | |
| for data in bigbio_pages: | |
| sources, targets, entities_info = parse_text( | |
| data=data, | |
| start_entity=start_entity, | |
| end_entity=end_entity, | |
| start_group=start_group, | |
| end_group=end_group, | |
| ) | |
| all_sources.append(sources) | |
| all_targets.append(targets) | |
| all_entities_info.append(entities_info) | |
| def _build_sequential_batches(): | |
| # Keep per-page order while still processing multiple pages per batch. | |
| page_positions = [0] * len(all_sources) | |
| next_page_idx = 0 | |
| active_pages = [] | |
| batches = [] | |
| while active_pages or next_page_idx < len(all_sources): | |
| while len(active_pages) < batch_size and next_page_idx < len( | |
| all_sources | |
| ): | |
| if len(all_sources[next_page_idx]) > 0: | |
| active_pages.append(next_page_idx) | |
| next_page_idx += 1 | |
| if not active_pages: | |
| break | |
| batch = [] | |
| next_active_pages = [] | |
| for page_idx in active_pages: | |
| item_idx = page_positions[page_idx] | |
| batch.append(( | |
| all_sources[page_idx][item_idx], | |
| all_targets[page_idx][item_idx], | |
| all_entities_info[page_idx][item_idx], | |
| )) | |
| page_positions[page_idx] += 1 | |
| if page_positions[page_idx] < len(all_sources[page_idx]): | |
| next_active_pages.append(page_idx) | |
| batches.append(batch) | |
| active_pages = next_active_pages | |
| return batches | |
| all_batches = _build_sequential_batches() | |
| print( | |
| f"Input preparation completed. Running generation on {len(all_batches)} batches." | |
| ) | |
| all_outputs = [] | |
| all_saliency_maps = [] | |
| batch_previous_targets = {} | |
| for batch in _progress( | |
| all_batches, | |
| desc="Processing batches", | |
| total=len(all_batches), | |
| show=show_progress, | |
| ): | |
| input_sentences = [] | |
| sem_groups = [] | |
| mentions = [] | |
| doc_ids = [] | |
| mentions_id = [] | |
| gold_concept_codes = [] | |
| gold_concept_names = [] | |
| start_spans = [] | |
| end_spans = [] | |
| for source, target, entity in batch: | |
| doc_id = entity["doc_id"] | |
| if doc_id not in batch_previous_targets: | |
| batch_previous_targets[doc_id] = "" | |
| previous_targets = batch_previous_targets.get(doc_id) | |
| input_sentences.append( | |
| add_headers_to_prompt( | |
| source, | |
| target, | |
| previous_targets, # type: ignore | |
| ) | |
| ) | |
| sem_groups.append(entity["semantic_group"]) | |
| mentions.append(entity["mention"]) | |
| doc_ids.append(doc_id) | |
| mentions_id.append(entity["mention_id"]) | |
| start_spans.append(entity["start_span"]) | |
| end_spans.append(entity["end_span"]) | |
| gold_concept_codes.append(entity.get("gold_concept_code", None)) # type: ignore | |
| gold_concept_names.append(entity.get("gold_concept_name", None)) # type: ignore | |
| all_outputs, cleaned_output_sequences, batch_saliency_maps = ( | |
| self.predict_batch( | |
| all_outputs=all_outputs, | |
| batch_size=batch_size, | |
| input_sentences=input_sentences, | |
| sem_groups=sem_groups, | |
| mentions=mentions, | |
| mentions_id=mentions_id, | |
| doc_ids=doc_ids, | |
| start_spans=start_spans, | |
| end_spans=end_spans, | |
| gold_concept_codes=gold_concept_codes, | |
| gold_concept_names=gold_concept_names, | |
| constrained=constrained, | |
| multiple_answers=multiple_answers, | |
| num_beams=num_beams, | |
| with_saliency_maps=with_saliency_maps, | |
| **kwargs, | |
| ) | |
| ) | |
| if with_saliency_maps: | |
| all_saliency_maps.extend(batch_saliency_maps) | |
| for i, doc_id in enumerate(doc_ids): | |
| clean_sentence = cleaned_output_sequences[num_beams * i] | |
| clean_sentence = start_entity + clean_sentence.split(start_entity)[-1] | |
| clean_sentence = clean_sentence.rstrip() + "\n" | |
| batch_previous_targets[doc_id] += clean_sentence | |
| if with_saliency_maps: | |
| return all_outputs, all_saliency_maps # type: ignore | |
| return all_outputs # type: ignore | |
| def encode(self, sentence): | |
| return self.tokenizer.encode(sentence, return_tensors="pt")[0] # type: ignore | |