Instructions to use UMCU/PII_RobBERT with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use UMCU/PII_RobBERT with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("token-classification", model="UMCU/PII_RobBERT", trust_remote_code=True)# Load model directly from transformers import AutoTokenizer, AutoModelForTokenClassification tokenizer = AutoTokenizer.from_pretrained("UMCU/PII_RobBERT", trust_remote_code=True) model = AutoModelForTokenClassification.from_pretrained("UMCU/PII_RobBERT", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
| from itertools import islice | |
| from typing import Dict, List, Optional, Tuple, Union | |
| import torch | |
| import torch.nn as nn | |
| from torchcrf import CRF | |
| from transformers import PretrainedConfig, PreTrainedModel | |
| from transformers.modeling_outputs import TokenClassifierOutput | |
| try: | |
| from transformers.models.eurobert.modeling_eurobert import EuroBertModel | |
| except Exception: | |
| try: | |
| from transformers import EuroBertModel | |
| except Exception: | |
| EuroBertModel = None | |
| print("COULD NOT IMPORT EUROBERT MODEL") | |
| # Large negative number for masking impossible transitions | |
| LARGE_NEGATIVE_NUMBER = -1e9 | |
| NUM_PER_LAYER = 16 | |
| def _build_backbone_from_config(config): | |
| """ | |
| Build a backbone model structure from config only. | |
| Never call from_pretrained() here; outer model loading will restore weights. | |
| """ | |
| from transformers import AutoConfig, AutoModel | |
| backbone_name = getattr(config, "backbone_model_name", None) | |
| if backbone_name is None: | |
| backbone_name = getattr(config, "_name_or_path", None) | |
| if backbone_name is None: | |
| raise ValueError( | |
| "config.backbone_model_name (or config._name_or_path) is required to load backbone" | |
| ) | |
| backbone_config = AutoConfig.from_pretrained( | |
| backbone_name, | |
| trust_remote_code=True, | |
| ) | |
| if hasattr(config, "hidden_dropout_prob"): | |
| backbone_config.hidden_dropout_prob = getattr( | |
| config, "hidden_dropout_prob", 0.1 | |
| ) | |
| if hasattr(config, "num_labels"): | |
| backbone_config.num_labels = getattr(config, "num_labels") | |
| if "eurobert" in backbone_name.lower() and EuroBertModel is not None: | |
| backbone = EuroBertModel(backbone_config) | |
| else: | |
| backbone = AutoModel.from_config( | |
| backbone_config, | |
| trust_remote_code=True, | |
| ) | |
| if getattr(config, "backbone_model_name", None) is None: | |
| config.backbone_model_name = backbone_name | |
| return backbone, backbone_name | |
| class MultiHeadCRFConfig(PretrainedConfig): | |
| """ | |
| Configuration class for Multi-Head CRF models. | |
| """ | |
| model_type = "multihead-crf-tagger" | |
| def __init__( | |
| self, | |
| entity_types: Optional[List[str]] = None, | |
| number_of_layers_per_head: int = 1, | |
| crf_reduction: str = "mean", | |
| freeze_backbone: bool = False, | |
| num_frozen_encoders: int = 0, | |
| classifier_dropout: float = 0.1, | |
| classifier_hidden_layers: Optional[Tuple] = None, | |
| class_weights: Optional[List[float]] = None, | |
| backbone_model_name: Optional[str] = None, | |
| **kwargs, | |
| ): | |
| self.entity_types = entity_types or [] | |
| self.number_of_layers_per_head = number_of_layers_per_head | |
| self.crf_reduction = crf_reduction | |
| self.freeze_backbone = freeze_backbone | |
| self.num_frozen_encoders = num_frozen_encoders | |
| self.classifier_dropout = classifier_dropout | |
| self.classifier_hidden_layers = classifier_hidden_layers | |
| self.class_weights = class_weights | |
| self.backbone_model_name = backbone_model_name | |
| super().__init__(**kwargs) | |
| class MultiHeadCRF(nn.Module): | |
| """ | |
| Custom CRF implementation with BIO transition masking. | |
| """ | |
| def __init__(self, num_tags: int, batch_first: bool = True) -> None: | |
| if num_tags <= 0: | |
| raise ValueError(f"invalid number of tags: {num_tags}") | |
| super().__init__() | |
| self.num_tags = num_tags | |
| self.batch_first = batch_first | |
| self.start_transitions = nn.Parameter(torch.empty(num_tags)) | |
| self.end_transitions = nn.Parameter(torch.empty(num_tags)) | |
| self.transitions = nn.Parameter(torch.empty(num_tags, num_tags)) | |
| self.reset_parameters() | |
| self.mask_impossible_transitions() | |
| def reset_parameters(self) -> None: | |
| nn.init.uniform_(self.start_transitions, -0.1, 0.1) | |
| nn.init.uniform_(self.end_transitions, -0.1, 0.1) | |
| nn.init.uniform_(self.transitions, -0.1, 0.1) | |
| def mask_impossible_transitions(self) -> None: | |
| with torch.no_grad(): | |
| if self.num_tags > 2: | |
| self.start_transitions[2] = LARGE_NEGATIVE_NUMBER | |
| self.transitions[0][2] = LARGE_NEGATIVE_NUMBER | |
| if self.num_tags > 3: | |
| self.start_transitions[3] = LARGE_NEGATIVE_NUMBER | |
| for i in range(3): | |
| self.transitions[i][3] = LARGE_NEGATIVE_NUMBER | |
| for i in range(3): | |
| self.transitions[3][i] = LARGE_NEGATIVE_NUMBER | |
| def __repr__(self) -> str: | |
| return f"{self.__class__.__name__}(num_tags={self.num_tags})" | |
| def forward( | |
| self, | |
| emissions: torch.Tensor, | |
| tags: torch.Tensor, | |
| mask: Optional[torch.Tensor] = None, | |
| reduction: str = "mean", | |
| ) -> torch.Tensor: | |
| self._validate(emissions, tags=tags, mask=mask) | |
| if reduction not in ("none", "sum", "mean", "token_mean"): | |
| raise ValueError(f"invalid reduction: {reduction}") | |
| if mask is None: | |
| mask = torch.ones_like(tags, dtype=torch.uint8) | |
| device = emissions.device | |
| tags = tags.to(device) | |
| mask = mask.to(device) | |
| if self.batch_first: | |
| emissions = emissions.transpose(0, 1) | |
| tags = tags.transpose(0, 1) | |
| mask = mask.transpose(0, 1) | |
| numerator = self._compute_score(emissions, tags, mask) | |
| denominator = self._compute_normalizer(emissions, mask) | |
| llh = numerator - denominator | |
| nllh = -llh | |
| if reduction == "none": | |
| return nllh | |
| if reduction == "sum": | |
| return nllh.sum() | |
| if reduction == "mean": | |
| return nllh.mean() | |
| return nllh.sum() / mask.type_as(emissions).sum() | |
| def decode( | |
| self, emissions: torch.Tensor, mask: Optional[torch.Tensor] = None | |
| ) -> List[List[int]]: | |
| self._validate(emissions, mask=mask) | |
| if mask is None: | |
| mask = emissions.new_ones(emissions.shape[:2], dtype=torch.uint8) | |
| if self.batch_first: | |
| emissions = emissions.transpose(0, 1) | |
| mask = mask.transpose(0, 1) | |
| return self._viterbi_decode(emissions, mask) | |
| def _validate( | |
| self, | |
| emissions: torch.Tensor, | |
| tags: Optional[torch.Tensor] = None, | |
| mask: Optional[torch.Tensor] = None, | |
| ) -> None: | |
| if emissions.dim() != 3: | |
| raise ValueError( | |
| f"emissions must have dimension of 3, got {emissions.dim()}" | |
| ) | |
| if emissions.size(2) != self.num_tags: | |
| raise ValueError( | |
| f"expected last dimension of emissions is {self.num_tags}, " | |
| f"got {emissions.size(2)}" | |
| ) | |
| if tags is not None and emissions.shape[:2] != tags.shape: | |
| raise ValueError( | |
| "the first two dimensions of emissions and tags must match, " | |
| f"got {tuple(emissions.shape[:2])} and {tuple(tags.shape)}" | |
| ) | |
| if mask is not None: | |
| if emissions.shape[:2] != mask.shape: | |
| raise ValueError( | |
| "the first two dimensions of emissions and mask must match, " | |
| f"got {tuple(emissions.shape[:2])} and {tuple(mask.shape)}" | |
| ) | |
| no_empty_seq = not self.batch_first and mask[0].all() | |
| no_empty_seq_bf = self.batch_first and mask[:, 0].all() | |
| if not no_empty_seq and not no_empty_seq_bf: | |
| raise ValueError("mask of the first timestep must all be on") | |
| def _compute_score( | |
| self, emissions: torch.Tensor, tags: torch.Tensor, mask: torch.Tensor | |
| ) -> torch.Tensor: | |
| assert emissions.dim() == 3 and tags.dim() == 2 | |
| assert emissions.shape[:2] == tags.shape | |
| assert emissions.size(2) == self.num_tags | |
| assert mask.shape == tags.shape | |
| assert mask[0].all() | |
| device = emissions.device | |
| tags = tags.to(device) | |
| mask = mask.to(device) | |
| seq_length, batch_size = tags.shape | |
| mask = mask.type_as(emissions) | |
| batch_indices = torch.arange(batch_size, device=device) | |
| score = self.start_transitions[tags[0]] | |
| score += emissions[0, batch_indices, tags[0]] | |
| for i in range(1, seq_length): | |
| score += self.transitions[tags[i - 1], tags[i]] * mask[i] | |
| score += emissions[i, batch_indices, tags[i]] * mask[i] | |
| seq_ends = mask.long().sum(dim=0) - 1 | |
| last_tags = tags[seq_ends, batch_indices] | |
| score += self.end_transitions[last_tags] | |
| return score | |
| def _compute_normalizer( | |
| self, emissions: torch.Tensor, mask: torch.Tensor | |
| ) -> torch.Tensor: | |
| assert emissions.dim() == 3 and mask.dim() == 2 | |
| assert emissions.shape[:2] == mask.shape | |
| assert emissions.size(2) == self.num_tags | |
| assert mask[0].all() | |
| seq_length = emissions.size(0) | |
| score = self.start_transitions + emissions[0] | |
| for i in range(1, seq_length): | |
| broadcast_score = score.unsqueeze(2) | |
| broadcast_emissions = emissions[i].unsqueeze(1) | |
| next_score = broadcast_score + self.transitions + broadcast_emissions | |
| next_score = torch.logsumexp(next_score, dim=1) | |
| score = torch.where(mask[i].unsqueeze(1).bool(), next_score, score) | |
| score += self.end_transitions | |
| return torch.logsumexp(score, dim=1) | |
| def _viterbi_decode( | |
| self, emissions: torch.Tensor, mask: torch.Tensor | |
| ) -> List[List[int]]: | |
| assert emissions.dim() == 3 and mask.dim() == 2 | |
| assert emissions.shape[:2] == mask.shape | |
| assert emissions.size(2) == self.num_tags | |
| assert mask[0].all() | |
| seq_length, batch_size = mask.shape | |
| score = self.start_transitions + emissions[0] | |
| history = [] | |
| for i in range(1, seq_length): | |
| broadcast_score = score.unsqueeze(2) | |
| broadcast_emission = emissions[i].unsqueeze(1) | |
| next_score = broadcast_score + self.transitions + broadcast_emission | |
| next_score, indices = next_score.max(dim=1) | |
| score = torch.where(mask[i].unsqueeze(1).bool(), next_score, score) | |
| history.append(indices) | |
| score += self.end_transitions | |
| seq_ends = mask.long().sum(dim=0) - 1 | |
| best_tags_list = [] | |
| for idx in range(batch_size): | |
| _, best_last_tag = score[idx].max(dim=0) | |
| best_tags = [best_last_tag.item()] | |
| for hist in reversed(history[: seq_ends[idx]]): | |
| best_last_tag = hist[idx][best_tags[-1]] | |
| best_tags.append(best_last_tag.item()) | |
| best_tags.reverse() | |
| best_tags_list.append(best_tags) | |
| return best_tags_list | |
| class TokenClassificationModelCRF(PreTrainedModel): | |
| """ | |
| Custom token classification model with CRF layer and configurable classifier head. | |
| """ | |
| def __init__( | |
| self, | |
| config, | |
| base_model=None, | |
| freeze_backbone=False, | |
| classifier_hidden_layers=None, | |
| classifier_dropout=0.1, | |
| ): | |
| super().__init__(config) | |
| self.config = config | |
| self.num_labels = config.num_labels | |
| if base_model is None: | |
| self.roberta, backbone_name = _build_backbone_from_config(config) | |
| else: | |
| if hasattr(base_model, "roberta"): | |
| self.roberta = base_model.roberta | |
| else: | |
| self.roberta = base_model | |
| backbone_name = ( | |
| getattr(getattr(self.roberta, "config", None), "_name_or_path", None) | |
| or getattr(config, "backbone_model_name", None) | |
| or getattr(config, "_name_or_path", None) | |
| ) | |
| if getattr(config, "backbone_model_name", None) is None: | |
| config.backbone_model_name = backbone_name | |
| self.lm_output_size = self.roberta.config.hidden_size | |
| self.config.freeze_backbone = freeze_backbone | |
| self.config.classifier_hidden_layers = classifier_hidden_layers | |
| self.config.classifier_dropout = classifier_dropout | |
| if freeze_backbone: | |
| print("+" * 30, "\n\n", "Freezing backbone...", "+" * 30, "\n\n") | |
| for param in self.roberta.parameters(): | |
| param.requires_grad = False | |
| self.roberta.eval() | |
| else: | |
| print("+" * 30, "\n\n", "NOT Freezing backbone...", "+" * 30, "\n\n") | |
| self.roberta.train(True) | |
| self.dropout = nn.Dropout(getattr(config, "hidden_dropout_prob", 0.1)) | |
| self.crf = CRF(self.num_labels, batch_first=True) | |
| self._build_classifier_head(classifier_hidden_layers, classifier_dropout) | |
| self.post_init() | |
| def _build_classifier_head(self, hidden_layers, dropout_rate): | |
| layers = [] | |
| input_size = self.lm_output_size | |
| if not hidden_layers: | |
| self.classifier = nn.Sequential( | |
| nn.Dropout(dropout_rate), nn.Linear(input_size, self.num_labels) | |
| ) | |
| return | |
| for hidden_size in hidden_layers: | |
| layers.append(nn.Linear(input_size, hidden_size)) | |
| layers.append(nn.ReLU()) | |
| layers.append(nn.Dropout(dropout_rate)) | |
| input_size = hidden_size | |
| layers.append(nn.Linear(input_size, self.num_labels)) | |
| self.classifier = nn.Sequential(*layers) | |
| def forward( | |
| self, | |
| input_ids: Optional[torch.LongTensor] = None, | |
| attention_mask: Optional[torch.FloatTensor] = None, | |
| token_type_ids: Optional[torch.LongTensor] = None, | |
| position_ids: Optional[torch.LongTensor] = None, | |
| head_mask: Optional[torch.FloatTensor] = None, | |
| inputs_embeds: Optional[torch.FloatTensor] = None, | |
| labels: Optional[torch.LongTensor] = None, | |
| output_attentions: Optional[bool] = None, | |
| output_hidden_states: Optional[bool] = None, | |
| return_dict: Optional[bool] = None, | |
| **kwargs, | |
| ) -> Union[Tuple[torch.Tensor], TokenClassifierOutput]: | |
| return_dict = ( | |
| return_dict if return_dict is not None else self.config.use_return_dict | |
| ) | |
| try: | |
| outputs = self.roberta( | |
| input_ids, | |
| attention_mask=attention_mask, | |
| token_type_ids=token_type_ids, | |
| position_ids=position_ids, | |
| head_mask=head_mask, | |
| inputs_embeds=inputs_embeds, | |
| output_attentions=output_attentions, | |
| output_hidden_states=output_hidden_states, | |
| return_dict=return_dict, | |
| ) | |
| except TypeError: | |
| outputs = self.roberta( | |
| input_ids, | |
| attention_mask=attention_mask, | |
| position_ids=position_ids, | |
| inputs_embeds=inputs_embeds, | |
| output_attentions=output_attentions, | |
| output_hidden_states=output_hidden_states, | |
| return_dict=return_dict, | |
| ) | |
| sequence_output = self.dropout(outputs.last_hidden_state) | |
| logits = self.classifier(sequence_output) | |
| loss = None | |
| if labels is not None: | |
| labels_long = labels.long() | |
| if attention_mask is not None: | |
| mask = attention_mask.bool() | |
| loss = -self.crf(logits, labels_long, mask=mask, reduction="mean") | |
| else: | |
| if not getattr(self, "_warned_no_attention_mask", False): | |
| print( | |
| "WARNING: attention_mask is None; CRF loss will include padding tokens." | |
| ) | |
| self._warned_no_attention_mask = True | |
| loss = -self.crf(logits, labels_long, reduction="mean") | |
| if not return_dict: | |
| output = (logits,) + outputs[2:] | |
| return ((loss,) + output) if loss is not None else output | |
| return TokenClassifierOutput( | |
| loss=loss, | |
| logits=logits, | |
| hidden_states=outputs.hidden_states, | |
| attentions=outputs.attentions, | |
| ) | |
| def device_info(self): | |
| return next(self.parameters()).device | |
| def get_input_embeddings(self): | |
| return self.roberta.get_input_embeddings() | |
| def set_input_embeddings(self, value): | |
| self.roberta.set_input_embeddings(value) | |
| def from_pretrained(cls, pretrained_model_name_or_path, *model_args, **kwargs): | |
| config = kwargs.pop("config", None) | |
| if config is None: | |
| from transformers import AutoConfig | |
| config = AutoConfig.from_pretrained(pretrained_model_name_or_path, **kwargs) | |
| freeze_backbone = getattr(config, "freeze_backbone", False) | |
| classifier_hidden_layers = getattr(config, "classifier_hidden_layers", None) | |
| classifier_dropout = getattr(config, "classifier_dropout", 0.1) | |
| model = cls( | |
| config=config, | |
| freeze_backbone=freeze_backbone, | |
| classifier_hidden_layers=classifier_hidden_layers, | |
| classifier_dropout=classifier_dropout, | |
| ) | |
| try: | |
| state_dict = torch.load( | |
| f"{pretrained_model_name_or_path}/pytorch_model.bin", map_location="cpu" | |
| ) | |
| model.load_state_dict(state_dict) | |
| except Exception: | |
| print( | |
| "Warning: Could not load pre-trained weights. Using randomly initialized model." | |
| ) | |
| return model | |
| class TokenClassificationModelMultiHeadCRF(PreTrainedModel): | |
| """ | |
| Multi-Head CRF model for token classification with multiple entity types. | |
| """ | |
| config_class = MultiHeadCRFConfig | |
| base_model_prefix = "roberta" | |
| _keys_to_ignore_on_load_unexpected = [r"pooler"] | |
| def __init__(self, config, base_model=None, freeze_backbone=None): | |
| super().__init__(config) | |
| self.config = config | |
| self.entity_types = getattr(config, "entity_types", []) | |
| if not self.entity_types: | |
| raise ValueError("entity_types must be provided in config") | |
| self.num_labels = config.num_labels | |
| self.number_of_layers_per_head = getattr(config, "number_of_layers_per_head", 1) | |
| self.crf_reduction = getattr(config, "crf_reduction", "mean") | |
| freeze_backbone = ( | |
| freeze_backbone | |
| if freeze_backbone is not None | |
| else getattr(config, "freeze_backbone", False) | |
| ) | |
| self.num_frozen_encoders = getattr(config, "num_frozen_encoders", 0) | |
| classifier_dropout = getattr(config, "classifier_dropout", 0.1) | |
| if base_model is None: | |
| self.roberta, backbone_name = _build_backbone_from_config(config) | |
| else: | |
| if hasattr(base_model, "roberta"): | |
| self.roberta = base_model.roberta | |
| else: | |
| self.roberta = base_model | |
| backbone_name = ( | |
| getattr(getattr(self.roberta, "config", None), "_name_or_path", None) | |
| or getattr(config, "backbone_model_name", None) | |
| or getattr(config, "_name_or_path", None) | |
| ) | |
| if getattr(config, "backbone_model_name", None) is None: | |
| config.backbone_model_name = backbone_name | |
| self.hidden_size = self.roberta.config.hidden_size | |
| self.dropout = nn.Dropout(getattr(config, "hidden_dropout_prob", 0.1)) | |
| print(f"Creating Multi-Head CRF with entity types: {sorted(self.entity_types)}") | |
| for entity_type in self.entity_types: | |
| for i in range(self.number_of_layers_per_head): | |
| setattr( | |
| self, | |
| f"{entity_type}_dense_{i}", | |
| nn.Linear(self.hidden_size, self.hidden_size), | |
| ) | |
| setattr( | |
| self, | |
| f"{entity_type}_dense_activation_{i}", | |
| nn.GELU(approximate="none"), | |
| ) | |
| setattr( | |
| self, f"{entity_type}_dropout_{i}", nn.Dropout(classifier_dropout) | |
| ) | |
| setattr( | |
| self, | |
| f"{entity_type}_classifier", | |
| nn.Linear(self.hidden_size, self.num_labels), | |
| ) | |
| setattr( | |
| self, | |
| f"{entity_type}_crf", | |
| MultiHeadCRF(num_tags=self.num_labels, batch_first=True), | |
| ) | |
| if freeze_backbone: | |
| self._freeze_backbone() | |
| self.post_init() | |
| def _freeze_backbone(self): | |
| print("+" * 30, "\n\n", "Freezing backbone...", "+" * 30, "\n\n") | |
| for param in self.roberta.embeddings.parameters(): | |
| param.requires_grad = False | |
| if self.num_frozen_encoders > 0: | |
| for _, param in islice( | |
| self.roberta.encoder.named_parameters(), | |
| self.num_frozen_encoders * NUM_PER_LAYER, | |
| ): | |
| param.requires_grad = False | |
| def reset_head_parameters(self): | |
| for entity_type in self.entity_types: | |
| for i in range(self.number_of_layers_per_head): | |
| getattr(self, f"{entity_type}_dense_{i}").reset_parameters() | |
| getattr(self, f"{entity_type}_classifier").reset_parameters() | |
| getattr(self, f"{entity_type}_crf").reset_parameters() | |
| getattr(self, f"{entity_type}_crf").mask_impossible_transitions() | |
| def forward( | |
| self, | |
| input_ids: Optional[torch.LongTensor] = None, | |
| attention_mask: Optional[torch.FloatTensor] = None, | |
| token_type_ids: Optional[torch.LongTensor] = None, | |
| position_ids: Optional[torch.LongTensor] = None, | |
| head_mask: Optional[torch.FloatTensor] = None, | |
| inputs_embeds: Optional[torch.FloatTensor] = None, | |
| labels: Optional[Dict[str, torch.LongTensor]] = None, | |
| output_attentions: Optional[bool] = None, | |
| output_hidden_states: Optional[bool] = None, | |
| return_dict: Optional[bool] = None, | |
| **kwargs, | |
| ): | |
| return_dict = ( | |
| return_dict if return_dict is not None else self.config.use_return_dict | |
| ) | |
| try: | |
| outputs = self.roberta( | |
| input_ids, | |
| attention_mask=attention_mask, | |
| token_type_ids=token_type_ids, | |
| position_ids=position_ids, | |
| head_mask=head_mask, | |
| inputs_embeds=inputs_embeds, | |
| output_attentions=output_attentions, | |
| output_hidden_states=output_hidden_states, | |
| return_dict=return_dict, | |
| ) | |
| except TypeError: | |
| outputs = self.roberta( | |
| input_ids, | |
| attention_mask=attention_mask, | |
| position_ids=position_ids, | |
| inputs_embeds=inputs_embeds, | |
| output_attentions=output_attentions, | |
| output_hidden_states=output_hidden_states, | |
| return_dict=return_dict, | |
| ) | |
| sequence_output = outputs[0] | |
| sequence_output = self.dropout(sequence_output) | |
| logits = {} | |
| for entity_type in self.entity_types: | |
| head_output = sequence_output | |
| for i in range(self.number_of_layers_per_head): | |
| head_output = getattr(self, f"{entity_type}_dense_{i}")(head_output) | |
| head_output = getattr(self, f"{entity_type}_dense_activation_{i}")( | |
| head_output | |
| ) | |
| head_output = getattr(self, f"{entity_type}_dropout_{i}")(head_output) | |
| logits[entity_type] = getattr(self, f"{entity_type}_classifier")( | |
| head_output | |
| ) | |
| if labels is not None: | |
| losses = {} | |
| mask = attention_mask.bool() if attention_mask is not None else None | |
| for entity_type in self.entity_types: | |
| if entity_type in labels: | |
| entity_labels = ( | |
| labels[entity_type].long().to(logits[entity_type].device) | |
| ) | |
| crf = getattr(self, f"{entity_type}_crf") | |
| if mask is not None: | |
| losses[entity_type] = crf( | |
| logits[entity_type], | |
| entity_labels, | |
| mask=mask, | |
| reduction=self.crf_reduction, | |
| ) | |
| else: | |
| if not getattr(self, "_warned_no_attention_mask", False): | |
| print( | |
| "WARNING: attention_mask is None; CRF loss will include padding tokens." | |
| ) | |
| self._warned_no_attention_mask = True | |
| losses[entity_type] = crf( | |
| logits[entity_type], | |
| entity_labels, | |
| reduction=self.crf_reduction, | |
| ) | |
| total_loss = sum(losses.values()) | |
| return total_loss, logits | |
| predictions = {} | |
| mask = attention_mask.bool() if attention_mask is not None else None | |
| for entity_type in self.entity_types: | |
| crf = getattr(self, f"{entity_type}_crf") | |
| if mask is not None: | |
| decoded = crf.decode(logits[entity_type], mask=mask) | |
| else: | |
| decoded = crf.decode(logits[entity_type]) | |
| predictions[entity_type] = torch.tensor(decoded) | |
| return [predictions[ent] for ent in sorted(self.entity_types)] | |
| def get_input_embeddings(self): | |
| return self.roberta.get_input_embeddings() | |
| def set_input_embeddings(self, value): | |
| self.roberta.set_input_embeddings(value) | |
| def from_pretrained(cls, pretrained_model_name_or_path, *model_args, **kwargs): | |
| import json | |
| import os | |
| config = kwargs.pop("config", None) | |
| if config is None: | |
| config_file = os.path.join(pretrained_model_name_or_path, "config.json") | |
| if os.path.exists(config_file): | |
| with open(config_file, "r") as f: | |
| config_dict = json.load(f) | |
| config = MultiHeadCRFConfig(**config_dict) | |
| else: | |
| from transformers import AutoConfig | |
| config = AutoConfig.from_pretrained( | |
| pretrained_model_name_or_path, | |
| trust_remote_code=kwargs.get("trust_remote_code", True), | |
| ) | |
| roberta_defaults = { | |
| "layer_norm_eps": 1e-5, | |
| "hidden_size": 768, | |
| "num_hidden_layers": 12, | |
| "num_attention_heads": 12, | |
| "intermediate_size": 3072, | |
| "hidden_act": "gelu", | |
| "hidden_dropout_prob": 0.1, | |
| "attention_probs_dropout_prob": 0.1, | |
| "max_position_embeddings": 514, | |
| "type_vocab_size": 1, | |
| "initializer_range": 0.02, | |
| "vocab_size": 52000, | |
| "pad_token_id": 1, | |
| "bos_token_id": 0, | |
| "eos_token_id": 2, | |
| "position_embedding_type": "absolute", | |
| "use_cache": True, | |
| "is_decoder": False, | |
| "add_cross_attention": False, | |
| "chunk_size_feed_forward": 0, | |
| "output_hidden_states": False, | |
| "output_attentions": False, | |
| "torchscript": False, | |
| "tie_word_embeddings": True, | |
| "return_dict": True, | |
| "gradient_checkpointing": False, | |
| "pruned_heads": {}, | |
| "problem_type": None, | |
| "embedding_size": None, | |
| } | |
| for key, default_value in roberta_defaults.items(): | |
| if not hasattr(config, key) or getattr(config, key) is None: | |
| setattr(config, key, default_value) | |
| freeze_backbone = getattr(config, "freeze_backbone", False) | |
| model = cls(config=config, freeze_backbone=freeze_backbone) | |
| weight_file = os.path.join(pretrained_model_name_or_path, "pytorch_model.bin") | |
| safetensors_file = os.path.join( | |
| pretrained_model_name_or_path, "model.safetensors" | |
| ) | |
| try: | |
| if os.path.exists(safetensors_file): | |
| from safetensors.torch import load_file | |
| state_dict = load_file(safetensors_file) | |
| model.load_state_dict(state_dict) | |
| elif os.path.exists(weight_file): | |
| state_dict = torch.load(weight_file, map_location="cpu") | |
| model.load_state_dict(state_dict) | |
| else: | |
| print( | |
| "Warning: No pre-trained weights found. Using randomly initialized model." | |
| ) | |
| except Exception as e: | |
| print(f"Warning: Could not load pre-trained weights: {e}") | |
| return model | |
| class MultiHeadConfig(PretrainedConfig): | |
| """ | |
| Configuration class for Multi-Head models (without CRF). | |
| """ | |
| model_type = "multihead-tagger" | |
| def __init__( | |
| self, | |
| entity_types: Optional[List[str]] = None, | |
| number_of_layers_per_head: int = 1, | |
| freeze_backbone: bool = False, | |
| num_frozen_encoders: int = 0, | |
| classifier_dropout: float = 0.1, | |
| use_class_weights: bool = False, | |
| class_weights: Optional[Dict[str, List[float]]] = None, | |
| backbone_model_name: Optional[str] = None, | |
| **kwargs, | |
| ): | |
| self.entity_types = entity_types or [] | |
| self.number_of_layers_per_head = number_of_layers_per_head | |
| self.freeze_backbone = freeze_backbone | |
| self.num_frozen_encoders = num_frozen_encoders | |
| self.classifier_dropout = classifier_dropout | |
| self.use_class_weights = use_class_weights | |
| self.class_weights = class_weights | |
| self.backbone_model_name = backbone_model_name | |
| super().__init__(**kwargs) | |
| class TokenClassificationModelMultiHead(PreTrainedModel): | |
| """ | |
| Multi-Head model for token classification with multiple entity types (no CRF). | |
| """ | |
| config_class = MultiHeadConfig | |
| base_model_prefix = "roberta" | |
| _keys_to_ignore_on_load_unexpected = [r"pooler"] | |
| def __init__(self, config, base_model=None, freeze_backbone=None): | |
| super().__init__(config) | |
| self.config = config | |
| self.entity_types = getattr(config, "entity_types", []) | |
| if not self.entity_types: | |
| raise ValueError("entity_types must be provided in config") | |
| self.num_labels = config.num_labels | |
| self.number_of_layers_per_head = getattr(config, "number_of_layers_per_head", 1) | |
| freeze_backbone = ( | |
| freeze_backbone | |
| if freeze_backbone is not None | |
| else getattr(config, "freeze_backbone", False) | |
| ) | |
| self.num_frozen_encoders = getattr(config, "num_frozen_encoders", 0) | |
| classifier_dropout = getattr(config, "classifier_dropout", 0.1) | |
| self.use_class_weights = getattr(config, "use_class_weights", False) | |
| self.class_weights = getattr(config, "class_weights", None) | |
| if base_model is None: | |
| self.roberta, backbone_name = _build_backbone_from_config(config) | |
| else: | |
| if hasattr(base_model, "roberta"): | |
| self.roberta = base_model.roberta | |
| else: | |
| self.roberta = base_model | |
| backbone_name = ( | |
| getattr(getattr(self.roberta, "config", None), "_name_or_path", None) | |
| or getattr(config, "backbone_model_name", None) | |
| or getattr(config, "_name_or_path", None) | |
| ) | |
| if getattr(config, "backbone_model_name", None) is None: | |
| config.backbone_model_name = backbone_name | |
| self.hidden_size = self.roberta.config.hidden_size | |
| self.dropout = nn.Dropout(getattr(config, "hidden_dropout_prob", 0.1)) | |
| print( | |
| f"Creating Multi-Head model with entity types: {sorted(self.entity_types)}" | |
| ) | |
| for entity_type in self.entity_types: | |
| for i in range(self.number_of_layers_per_head): | |
| setattr( | |
| self, | |
| f"{entity_type}_dense_{i}", | |
| nn.Linear(self.hidden_size, self.hidden_size), | |
| ) | |
| setattr( | |
| self, | |
| f"{entity_type}_dense_activation_{i}", | |
| nn.GELU(approximate="none"), | |
| ) | |
| setattr( | |
| self, f"{entity_type}_dropout_{i}", nn.Dropout(classifier_dropout) | |
| ) | |
| setattr( | |
| self, | |
| f"{entity_type}_classifier", | |
| nn.Linear(self.hidden_size, self.num_labels), | |
| ) | |
| self.loss_fns = nn.ModuleDict() | |
| for entity_type in self.entity_types: | |
| if ( | |
| self.use_class_weights | |
| and self.class_weights | |
| and entity_type in self.class_weights | |
| ): | |
| weight = torch.tensor( | |
| self.class_weights[entity_type], dtype=torch.float | |
| ) | |
| self.loss_fns[entity_type] = nn.CrossEntropyLoss( | |
| weight=weight, ignore_index=-100 | |
| ) | |
| else: | |
| self.loss_fns[entity_type] = nn.CrossEntropyLoss(ignore_index=-100) | |
| if freeze_backbone: | |
| self._freeze_backbone() | |
| self.post_init() | |
| def _freeze_backbone(self): | |
| print("+" * 30, "\n\n", "Freezing backbone...", "+" * 30, "\n\n") | |
| for param in self.roberta.embeddings.parameters(): | |
| param.requires_grad = False | |
| if self.num_frozen_encoders > 0: | |
| for _, param in islice( | |
| self.roberta.encoder.named_parameters(), | |
| self.num_frozen_encoders * NUM_PER_LAYER, | |
| ): | |
| param.requires_grad = False | |
| def reset_head_parameters(self): | |
| for entity_type in self.entity_types: | |
| for i in range(self.number_of_layers_per_head): | |
| getattr(self, f"{entity_type}_dense_{i}").reset_parameters() | |
| getattr(self, f"{entity_type}_classifier").reset_parameters() | |
| def forward( | |
| self, | |
| input_ids: Optional[torch.LongTensor] = None, | |
| attention_mask: Optional[torch.FloatTensor] = None, | |
| token_type_ids: Optional[torch.LongTensor] = None, | |
| position_ids: Optional[torch.LongTensor] = None, | |
| head_mask: Optional[torch.FloatTensor] = None, | |
| inputs_embeds: Optional[torch.FloatTensor] = None, | |
| labels: Optional[Dict[str, torch.LongTensor]] = None, | |
| output_attentions: Optional[bool] = None, | |
| output_hidden_states: Optional[bool] = None, | |
| return_dict: Optional[bool] = None, | |
| **kwargs, | |
| ): | |
| return_dict = ( | |
| return_dict if return_dict is not None else self.config.use_return_dict | |
| ) | |
| try: | |
| outputs = self.roberta( | |
| input_ids, | |
| attention_mask=attention_mask, | |
| token_type_ids=token_type_ids, | |
| position_ids=position_ids, | |
| head_mask=head_mask, | |
| inputs_embeds=inputs_embeds, | |
| output_attentions=output_attentions, | |
| output_hidden_states=output_hidden_states, | |
| return_dict=return_dict, | |
| ) | |
| except TypeError: | |
| outputs = self.roberta( | |
| input_ids, | |
| attention_mask=attention_mask, | |
| position_ids=position_ids, | |
| inputs_embeds=inputs_embeds, | |
| output_attentions=output_attentions, | |
| output_hidden_states=output_hidden_states, | |
| return_dict=return_dict, | |
| ) | |
| sequence_output = outputs[0] | |
| sequence_output = self.dropout(sequence_output) | |
| logits = {} | |
| for entity_type in self.entity_types: | |
| head_output = sequence_output | |
| for i in range(self.number_of_layers_per_head): | |
| head_output = getattr(self, f"{entity_type}_dense_{i}")(head_output) | |
| head_output = getattr(self, f"{entity_type}_dense_activation_{i}")( | |
| head_output | |
| ) | |
| head_output = getattr(self, f"{entity_type}_dropout_{i}")(head_output) | |
| logits[entity_type] = getattr(self, f"{entity_type}_classifier")( | |
| head_output | |
| ) | |
| if labels is not None: | |
| losses = {} | |
| for entity_type in self.entity_types: | |
| if entity_type in labels: | |
| entity_labels = ( | |
| labels[entity_type].long().to(logits[entity_type].device) | |
| ) | |
| entity_logits = logits[entity_type] | |
| loss_fct = self.loss_fns[entity_type] | |
| if hasattr(loss_fct, "weight") and loss_fct.weight is not None: | |
| loss_fct.weight = loss_fct.weight.to(entity_logits.device) | |
| losses[entity_type] = loss_fct( | |
| entity_logits.view(-1, self.num_labels), | |
| entity_labels.view(-1), | |
| ) | |
| total_loss = sum(losses.values()) | |
| return total_loss, logits | |
| predictions = {} | |
| for entity_type in self.entity_types: | |
| preds = torch.argmax(logits[entity_type], dim=-1) | |
| predictions[entity_type] = preds | |
| return [predictions[ent] for ent in sorted(self.entity_types)] | |
| def get_input_embeddings(self): | |
| return self.roberta.get_input_embeddings() | |
| def set_input_embeddings(self, value): | |
| self.roberta.set_input_embeddings(value) | |
| def from_pretrained(cls, pretrained_model_name_or_path, *model_args, **kwargs): | |
| import json | |
| import os | |
| config = kwargs.pop("config", None) | |
| if config is None: | |
| config_file = os.path.join(pretrained_model_name_or_path, "config.json") | |
| if os.path.exists(config_file): | |
| with open(config_file, "r") as f: | |
| config_dict = json.load(f) | |
| config = MultiHeadConfig(**config_dict) | |
| else: | |
| from transformers import AutoConfig | |
| config = AutoConfig.from_pretrained( | |
| pretrained_model_name_or_path, | |
| trust_remote_code=kwargs.get("trust_remote_code", True), | |
| ) | |
| roberta_defaults = { | |
| "layer_norm_eps": 1e-5, | |
| "hidden_size": 768, | |
| "num_hidden_layers": 12, | |
| "num_attention_heads": 12, | |
| "intermediate_size": 3072, | |
| "hidden_act": "gelu", | |
| "hidden_dropout_prob": 0.1, | |
| "attention_probs_dropout_prob": 0.1, | |
| "max_position_embeddings": 514, | |
| "type_vocab_size": 1, | |
| "initializer_range": 0.02, | |
| "vocab_size": 52000, | |
| "pad_token_id": 1, | |
| "bos_token_id": 0, | |
| "eos_token_id": 2, | |
| "position_embedding_type": "absolute", | |
| "use_cache": True, | |
| "is_decoder": False, | |
| "add_cross_attention": False, | |
| "chunk_size_feed_forward": 0, | |
| "output_hidden_states": False, | |
| "output_attentions": False, | |
| "torchscript": False, | |
| "tie_word_embeddings": True, | |
| "return_dict": True, | |
| "gradient_checkpointing": False, | |
| "pruned_heads": {}, | |
| "problem_type": None, | |
| "embedding_size": None, | |
| } | |
| for key, default_value in roberta_defaults.items(): | |
| if not hasattr(config, key) or getattr(config, key) is None: | |
| setattr(config, key, default_value) | |
| freeze_backbone = getattr(config, "freeze_backbone", False) | |
| model = cls(config=config, freeze_backbone=freeze_backbone) | |
| weight_file = os.path.join(pretrained_model_name_or_path, "pytorch_model.bin") | |
| safetensors_file = os.path.join( | |
| pretrained_model_name_or_path, "model.safetensors" | |
| ) | |
| try: | |
| if os.path.exists(safetensors_file): | |
| from safetensors.torch import load_file | |
| state_dict = load_file(safetensors_file) | |
| model.load_state_dict(state_dict) | |
| elif os.path.exists(weight_file): | |
| state_dict = torch.load(weight_file, map_location="cpu") | |
| model.load_state_dict(state_dict) | |
| else: | |
| print( | |
| "Warning: No pre-trained weights found. Using randomly initialized model." | |
| ) | |
| except Exception as e: | |
| print(f"Warning: Could not load pre-trained weights: {e}") | |
| return model | |
| class TokenClassificationModel(PreTrainedModel): | |
| """ | |
| Custom token classification model with configurable classifier head (no CRF). | |
| """ | |
| def __init__(self, config, base_model=None): | |
| super().__init__(config) | |
| self.config = config | |
| self.num_labels = config.num_labels | |
| if base_model is None: | |
| self.roberta, backbone_name = _build_backbone_from_config(config) | |
| else: | |
| if hasattr(base_model, "roberta"): | |
| self.roberta = base_model.roberta | |
| else: | |
| self.roberta = base_model | |
| backbone_name = ( | |
| getattr(getattr(self.roberta, "config", None), "_name_or_path", None) | |
| or getattr(config, "backbone_model_name", None) | |
| or getattr(config, "_name_or_path", None) | |
| ) | |
| if getattr(config, "backbone_model_name", None) is None: | |
| config.backbone_model_name = backbone_name | |
| self.dropout = nn.Dropout(getattr(config, "hidden_dropout_prob", 0.1)) | |
| classifier_hidden_layers = getattr(config, "classifier_hidden_layers", None) | |
| classifier_dropout = getattr(config, "classifier_dropout", 0.1) | |
| if classifier_hidden_layers is not None: | |
| in_size = self.roberta.config.hidden_size | |
| layers = [] | |
| if classifier_hidden_layers: | |
| for h in classifier_hidden_layers: | |
| layers += [ | |
| nn.Linear(in_size, h), | |
| nn.ReLU(), | |
| nn.Dropout(classifier_dropout), | |
| ] | |
| in_size = h | |
| layers.append(nn.Linear(in_size, config.num_labels)) | |
| self.classifier = nn.Sequential(*layers) | |
| else: | |
| self.classifier = nn.Linear( | |
| self.roberta.config.hidden_size, config.num_labels | |
| ) | |
| if isinstance(self.classifier, nn.Sequential): | |
| for module in self.classifier: | |
| if isinstance(module, nn.Linear): | |
| self._init_weights(module) | |
| elif isinstance(self.classifier, nn.Linear): | |
| self._init_weights(self.classifier) | |
| self.post_init() | |
| def forward( | |
| self, | |
| input_ids: Optional[torch.LongTensor] = None, | |
| attention_mask: Optional[torch.FloatTensor] = None, | |
| token_type_ids: Optional[torch.LongTensor] = None, | |
| position_ids: Optional[torch.LongTensor] = None, | |
| head_mask: Optional[torch.FloatTensor] = None, | |
| inputs_embeds: Optional[torch.FloatTensor] = None, | |
| labels: Optional[torch.LongTensor] = None, | |
| output_attentions: Optional[bool] = None, | |
| output_hidden_states: Optional[bool] = None, | |
| return_dict: Optional[bool] = None, | |
| **kwargs, | |
| ) -> Union[Tuple[torch.Tensor], TokenClassifierOutput]: | |
| return_dict = ( | |
| return_dict if return_dict is not None else self.config.use_return_dict | |
| ) | |
| try: | |
| outputs = self.roberta( | |
| input_ids, | |
| attention_mask=attention_mask, | |
| token_type_ids=token_type_ids, | |
| position_ids=position_ids, | |
| head_mask=head_mask, | |
| inputs_embeds=inputs_embeds, | |
| output_attentions=output_attentions, | |
| output_hidden_states=output_hidden_states, | |
| return_dict=return_dict, | |
| ) | |
| except TypeError: | |
| outputs = self.roberta( | |
| input_ids, | |
| attention_mask=attention_mask, | |
| position_ids=position_ids, | |
| inputs_embeds=inputs_embeds, | |
| output_attentions=output_attentions, | |
| output_hidden_states=output_hidden_states, | |
| return_dict=return_dict, | |
| ) | |
| sequence_output = outputs.last_hidden_state | |
| sequence_output = self.dropout(sequence_output) | |
| logits = self.classifier(sequence_output) | |
| loss = None | |
| if labels is not None: | |
| loss_fct = nn.CrossEntropyLoss() | |
| if attention_mask is not None: | |
| active_loss = attention_mask.view(-1) == 1 | |
| active_logits = logits.view(-1, self.num_labels)[active_loss] | |
| active_labels = labels.view(-1)[active_loss] | |
| loss = loss_fct(active_logits, active_labels) | |
| else: | |
| loss = loss_fct(logits.view(-1, self.num_labels), labels.view(-1)) | |
| if not return_dict: | |
| output = (logits,) + outputs[2:] | |
| return ((loss,) + output) if loss is not None else output | |
| return TokenClassifierOutput( | |
| loss=loss, | |
| logits=logits, | |
| hidden_states=outputs.hidden_states, | |
| attentions=outputs.attentions, | |
| ) | |
| def get_input_embeddings(self): | |
| return self.roberta.get_input_embeddings() | |
| def set_input_embeddings(self, value): | |
| self.roberta.set_input_embeddings(value) | |
| def load_custom_cardioner_multiclass_model(model_path: str, device: str = "auto"): | |
| import os | |
| import torch | |
| from transformers import AutoModelForTokenClassification, AutoTokenizer | |
| required_files = ["config.json", "modeling.py", "pytorch_model.bin"] | |
| missing_files = [ | |
| f for f in required_files if not os.path.exists(os.path.join(model_path, f)) | |
| ] | |
| if missing_files: | |
| raise FileNotFoundError( | |
| f"Missing required files in {model_path}: {missing_files}" | |
| ) | |
| print(f"Loading custom CardioNER multiclass model from: {model_path}") | |
| tokenizer = AutoTokenizer.from_pretrained(model_path) | |
| model = AutoModelForTokenClassification.from_pretrained( | |
| model_path, | |
| trust_remote_code=True, | |
| use_safetensors=True, | |
| ) | |
| if device == "auto": | |
| device = "cuda" if torch.cuda.is_available() else "cpu" | |
| model = model.to(device) | |
| print(f"Model loaded successfully on {device}") | |
| print(f"Model type: {type(model).__name__}") | |
| print(f"Number of labels: {model.num_labels}") | |
| return model, tokenizer, model.config | |
| def load_custom_multihead_crf_model(model_path: str, device: str = "auto"): | |
| import json | |
| import os | |
| from transformers import AutoTokenizer | |
| required_files = ["config.json", "modeling.py"] | |
| missing_files = [ | |
| f for f in required_files if not os.path.exists(os.path.join(model_path, f)) | |
| ] | |
| if missing_files: | |
| raise FileNotFoundError( | |
| f"Missing required files in {model_path}: {missing_files}" | |
| ) | |
| print(f"Loading Multi-Head CRF model from: {model_path}") | |
| tokenizer = AutoTokenizer.from_pretrained(model_path) | |
| with open(os.path.join(model_path, "config.json"), "r") as f: | |
| config_dict = json.load(f) | |
| config = MultiHeadCRFConfig(**config_dict) | |
| model = TokenClassificationModelMultiHeadCRF.from_pretrained( | |
| model_path, config=config | |
| ) | |
| if device == "auto": | |
| device = "cuda" if torch.cuda.is_available() else "cpu" | |
| model = model.to(device) | |
| print(f"Model loaded successfully on {device}") | |
| print(f"Model type: {type(model).__name__}") | |
| print(f"Entity types: {model.entity_types}") | |
| print(f"Number of labels per head: {model.num_labels}") | |
| return model, tokenizer, model.config | |
| def validate_custom_multiclass_model_directory(model_path: str) -> dict: | |
| import json | |
| import os | |
| validation_results = { | |
| "valid": True, | |
| "errors": [], | |
| "warnings": [], | |
| "files_found": [], | |
| "model_info": {}, | |
| } | |
| required_files = { | |
| "config.json": "Model configuration", | |
| "modeling.py": "Custom model class definition", | |
| "pytorch_model.bin": "Model weights", | |
| } | |
| optional_files = { | |
| "tokenizer.json": "Tokenizer vocabulary", | |
| "tokenizer_config.json": "Tokenizer configuration", | |
| "training_args.json": "Training arguments", | |
| } | |
| for filename, description in required_files.items(): | |
| filepath = os.path.join(model_path, filename) | |
| if os.path.exists(filepath): | |
| validation_results["files_found"].append(f"{filename} ({description})") | |
| else: | |
| validation_results["valid"] = False | |
| validation_results["errors"].append( | |
| f"Missing required file: {filename} - {description}" | |
| ) | |
| for filename, description in optional_files.items(): | |
| filepath = os.path.join(model_path, filename) | |
| if os.path.exists(filepath): | |
| validation_results["files_found"].append(f"{filename} ({description})") | |
| else: | |
| validation_results["warnings"].append( | |
| f"Missing optional file: {filename} - {description}" | |
| ) | |
| config_path = os.path.join(model_path, "config.json") | |
| if os.path.exists(config_path): | |
| try: | |
| with open(config_path, "r") as f: | |
| config = json.load(f) | |
| validation_results["model_info"]["num_labels"] = config.get( | |
| "num_labels", "Unknown" | |
| ) | |
| validation_results["model_info"]["model_type"] = config.get( | |
| "model_type", "Unknown" | |
| ) | |
| validation_results["model_info"]["has_auto_map"] = "auto_map" in config | |
| validation_results["model_info"]["classifier_hidden_layers"] = config.get( | |
| "classifier_hidden_layers", None | |
| ) | |
| validation_results["model_info"]["freeze_backbone"] = config.get( | |
| "freeze_backbone", None | |
| ) | |
| validation_results["model_info"]["use_crf"] = ( | |
| "TokenClassificationModelCRF" in str(config.get("architectures", [])) | |
| ) | |
| if not config.get("auto_map"): | |
| validation_results["warnings"].append( | |
| "No auto_map found in config - may not load correctly with trust_remote_code=True" | |
| ) | |
| except json.JSONDecodeError as e: | |
| validation_results["valid"] = False | |
| validation_results["errors"].append(f"Invalid config.json: {str(e)}") | |
| modeling_path = os.path.join(model_path, "modeling.py") | |
| if os.path.exists(modeling_path): | |
| try: | |
| with open(modeling_path, "r") as f: | |
| content = f.read() | |
| required_classes = [ | |
| "TokenClassificationModel", | |
| "TokenClassificationModelCRF", | |
| ] | |
| missing_classes = [cls for cls in required_classes if cls not in content] | |
| if missing_classes: | |
| validation_results["valid"] = False | |
| validation_results["errors"].append( | |
| f"modeling.py missing required classes: {missing_classes}" | |
| ) | |
| except Exception as e: | |
| validation_results["warnings"].append( | |
| f"Could not read modeling.py: {str(e)}" | |
| ) | |
| return validation_results | |
| try: | |
| from transformers import AutoConfig | |
| AutoConfig.register("multihead-crf-tagger", MultiHeadCRFConfig) | |
| except Exception: | |
| pass | |
| def patch_legacy_model( | |
| model_path: str, backbone_model_name: str, dry_run: bool = True | |
| ) -> bool: | |
| import json | |
| import os | |
| import shutil | |
| config_path = os.path.join(model_path, "config.json") | |
| if not os.path.exists(config_path): | |
| print(f"ERROR: config.json not found at {config_path}") | |
| return False | |
| with open(config_path, "r") as f: | |
| config = json.load(f) | |
| if "backbone_model_name" in config: | |
| print(f"Model already has backbone_model_name: {config['backbone_model_name']}") | |
| if config["backbone_model_name"] == backbone_model_name: | |
| print("No changes needed.") | |
| return True | |
| else: | |
| print(f"WARNING: Existing backbone_model_name differs from provided value!") | |
| print(f" Existing: {config['backbone_model_name']}") | |
| print(f" Provided: {backbone_model_name}") | |
| if dry_run: | |
| print("Would update to new value (dry_run=True)") | |
| else: | |
| print("Updating to new value...") | |
| config["backbone_model_name"] = backbone_model_name | |
| if dry_run: | |
| print(f"\n[DRY RUN] Would patch {config_path}:") | |
| print(f' Adding: backbone_model_name = "{backbone_model_name}"') | |
| print("\nTo apply this patch, run with dry_run=False") | |
| return True | |
| backup_path = config_path + ".backup" | |
| shutil.copy2(config_path, backup_path) | |
| print(f"Created backup at {backup_path}") | |
| with open(config_path, "w") as f: | |
| json.dump(config, f, indent=2) | |
| print(f"Successfully patched {config_path}") | |
| print(f' Added: backbone_model_name = "{backbone_model_name}"') | |
| return True | |
| def patch_multiple_models( | |
| model_paths: list, backbone_model_name: str, dry_run: bool = True | |
| ) -> dict: | |
| results = {} | |
| for path in model_paths: | |
| print(f"\n{'=' * 60}") | |
| print(f"Processing: {path}") | |
| print("=" * 60) | |
| results[path] = patch_legacy_model(path, backbone_model_name, dry_run) | |
| print(f"\n{'=' * 60}") | |
| print("SUMMARY") | |
| print("=" * 60) | |
| success = sum(1 for v in results.values() if v) | |
| print( | |
| f"Successfully {'would patch' if dry_run else 'patched'}: {success}/{len(model_paths)}" | |
| ) | |
| return results | |