PII_RobBERT / modeling.py
UMCU's picture
Upload 9 files
b2df892 verified
Raw
History Blame Contribute Delete
54.3 kB
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,
)
@property
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)
@classmethod
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)
@classmethod
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)
@classmethod
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