UMCU commited on
Commit
1361cf4
·
verified ·
1 Parent(s): c1ca42a

Upload modeling.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. modeling.py +1495 -0
modeling.py ADDED
@@ -0,0 +1,1495 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from itertools import islice
2
+ from typing import Dict, List, Optional, Tuple, Union
3
+
4
+ import torch
5
+ import torch.nn as nn
6
+ from torchcrf import CRF
7
+ from transformers import PretrainedConfig, PreTrainedModel
8
+ from transformers.modeling_outputs import TokenClassifierOutput
9
+
10
+ try:
11
+ from transformers.models.eurobert.modeling_eurobert import EuroBertModel
12
+ except Exception:
13
+ try:
14
+ from transformers import EuroBertModel
15
+ except Exception:
16
+ EuroBertModel = None
17
+ print("COULD NOT IMPORT EUROBERT MODEL")
18
+
19
+ # Large negative number for masking impossible transitions
20
+ LARGE_NEGATIVE_NUMBER = -1e9
21
+ NUM_PER_LAYER = 16
22
+
23
+
24
+ def _build_backbone_from_config(config):
25
+ """
26
+ Build a backbone model structure from config only.
27
+ Never call from_pretrained() here; outer model loading will restore weights.
28
+ """
29
+ from transformers import AutoConfig, AutoModel
30
+
31
+ backbone_name = getattr(config, "backbone_model_name", None)
32
+ if backbone_name is None:
33
+ backbone_name = getattr(config, "_name_or_path", None)
34
+
35
+ if backbone_name is None:
36
+ raise ValueError(
37
+ "config.backbone_model_name (or config._name_or_path) is required to load backbone"
38
+ )
39
+
40
+ backbone_config = AutoConfig.from_pretrained(
41
+ backbone_name,
42
+ trust_remote_code=True,
43
+ )
44
+
45
+ if hasattr(config, "hidden_dropout_prob"):
46
+ backbone_config.hidden_dropout_prob = getattr(
47
+ config, "hidden_dropout_prob", 0.1
48
+ )
49
+
50
+ if hasattr(config, "num_labels"):
51
+ backbone_config.num_labels = getattr(config, "num_labels")
52
+
53
+ if "eurobert" in backbone_name.lower() and EuroBertModel is not None:
54
+ backbone = EuroBertModel(backbone_config)
55
+ else:
56
+ backbone = AutoModel.from_config(
57
+ backbone_config,
58
+ trust_remote_code=True,
59
+ )
60
+
61
+ if getattr(config, "backbone_model_name", None) is None:
62
+ config.backbone_model_name = backbone_name
63
+
64
+ return backbone, backbone_name
65
+
66
+
67
+ class MultiHeadCRFConfig(PretrainedConfig):
68
+ """
69
+ Configuration class for Multi-Head CRF models.
70
+ """
71
+
72
+ model_type = "multihead-crf-tagger"
73
+
74
+ def __init__(
75
+ self,
76
+ entity_types: Optional[List[str]] = None,
77
+ number_of_layers_per_head: int = 1,
78
+ crf_reduction: str = "mean",
79
+ freeze_backbone: bool = False,
80
+ num_frozen_encoders: int = 0,
81
+ classifier_dropout: float = 0.1,
82
+ classifier_hidden_layers: Optional[Tuple] = None,
83
+ class_weights: Optional[List[float]] = None,
84
+ backbone_model_name: Optional[str] = None,
85
+ **kwargs,
86
+ ):
87
+ self.entity_types = entity_types or []
88
+ self.number_of_layers_per_head = number_of_layers_per_head
89
+ self.crf_reduction = crf_reduction
90
+ self.freeze_backbone = freeze_backbone
91
+ self.num_frozen_encoders = num_frozen_encoders
92
+ self.classifier_dropout = classifier_dropout
93
+ self.classifier_hidden_layers = classifier_hidden_layers
94
+ self.class_weights = class_weights
95
+ self.backbone_model_name = backbone_model_name
96
+ super().__init__(**kwargs)
97
+
98
+
99
+ class MultiHeadCRF(nn.Module):
100
+ """
101
+ Custom CRF implementation with BIO transition masking.
102
+ """
103
+
104
+ def __init__(self, num_tags: int, batch_first: bool = True) -> None:
105
+ if num_tags <= 0:
106
+ raise ValueError(f"invalid number of tags: {num_tags}")
107
+ super().__init__()
108
+ self.num_tags = num_tags
109
+ self.batch_first = batch_first
110
+ self.start_transitions = nn.Parameter(torch.empty(num_tags))
111
+ self.end_transitions = nn.Parameter(torch.empty(num_tags))
112
+ self.transitions = nn.Parameter(torch.empty(num_tags, num_tags))
113
+
114
+ self.reset_parameters()
115
+ self.mask_impossible_transitions()
116
+
117
+ def reset_parameters(self) -> None:
118
+ nn.init.uniform_(self.start_transitions, -0.1, 0.1)
119
+ nn.init.uniform_(self.end_transitions, -0.1, 0.1)
120
+ nn.init.uniform_(self.transitions, -0.1, 0.1)
121
+
122
+ def mask_impossible_transitions(self) -> None:
123
+ with torch.no_grad():
124
+ if self.num_tags > 2:
125
+ self.start_transitions[2] = LARGE_NEGATIVE_NUMBER
126
+ self.transitions[0][2] = LARGE_NEGATIVE_NUMBER
127
+
128
+ if self.num_tags > 3:
129
+ self.start_transitions[3] = LARGE_NEGATIVE_NUMBER
130
+ for i in range(3):
131
+ self.transitions[i][3] = LARGE_NEGATIVE_NUMBER
132
+ for i in range(3):
133
+ self.transitions[3][i] = LARGE_NEGATIVE_NUMBER
134
+
135
+ def __repr__(self) -> str:
136
+ return f"{self.__class__.__name__}(num_tags={self.num_tags})"
137
+
138
+ def forward(
139
+ self,
140
+ emissions: torch.Tensor,
141
+ tags: torch.Tensor,
142
+ mask: Optional[torch.Tensor] = None,
143
+ reduction: str = "mean",
144
+ ) -> torch.Tensor:
145
+ self._validate(emissions, tags=tags, mask=mask)
146
+ if reduction not in ("none", "sum", "mean", "token_mean"):
147
+ raise ValueError(f"invalid reduction: {reduction}")
148
+ if mask is None:
149
+ mask = torch.ones_like(tags, dtype=torch.uint8)
150
+
151
+ device = emissions.device
152
+ tags = tags.to(device)
153
+ mask = mask.to(device)
154
+
155
+ if self.batch_first:
156
+ emissions = emissions.transpose(0, 1)
157
+ tags = tags.transpose(0, 1)
158
+ mask = mask.transpose(0, 1)
159
+
160
+ numerator = self._compute_score(emissions, tags, mask)
161
+ denominator = self._compute_normalizer(emissions, mask)
162
+ llh = numerator - denominator
163
+ nllh = -llh
164
+
165
+ if reduction == "none":
166
+ return nllh
167
+ if reduction == "sum":
168
+ return nllh.sum()
169
+ if reduction == "mean":
170
+ return nllh.mean()
171
+ return nllh.sum() / mask.type_as(emissions).sum()
172
+
173
+ def decode(
174
+ self, emissions: torch.Tensor, mask: Optional[torch.Tensor] = None
175
+ ) -> List[List[int]]:
176
+ self._validate(emissions, mask=mask)
177
+ if mask is None:
178
+ mask = emissions.new_ones(emissions.shape[:2], dtype=torch.uint8)
179
+
180
+ if self.batch_first:
181
+ emissions = emissions.transpose(0, 1)
182
+ mask = mask.transpose(0, 1)
183
+
184
+ return self._viterbi_decode(emissions, mask)
185
+
186
+ def _validate(
187
+ self,
188
+ emissions: torch.Tensor,
189
+ tags: Optional[torch.Tensor] = None,
190
+ mask: Optional[torch.Tensor] = None,
191
+ ) -> None:
192
+ if emissions.dim() != 3:
193
+ raise ValueError(
194
+ f"emissions must have dimension of 3, got {emissions.dim()}"
195
+ )
196
+ if emissions.size(2) != self.num_tags:
197
+ raise ValueError(
198
+ f"expected last dimension of emissions is {self.num_tags}, "
199
+ f"got {emissions.size(2)}"
200
+ )
201
+
202
+ if tags is not None and emissions.shape[:2] != tags.shape:
203
+ raise ValueError(
204
+ "the first two dimensions of emissions and tags must match, "
205
+ f"got {tuple(emissions.shape[:2])} and {tuple(tags.shape)}"
206
+ )
207
+
208
+ if mask is not None:
209
+ if emissions.shape[:2] != mask.shape:
210
+ raise ValueError(
211
+ "the first two dimensions of emissions and mask must match, "
212
+ f"got {tuple(emissions.shape[:2])} and {tuple(mask.shape)}"
213
+ )
214
+ no_empty_seq = not self.batch_first and mask[0].all()
215
+ no_empty_seq_bf = self.batch_first and mask[:, 0].all()
216
+ if not no_empty_seq and not no_empty_seq_bf:
217
+ raise ValueError("mask of the first timestep must all be on")
218
+
219
+ def _compute_score(
220
+ self, emissions: torch.Tensor, tags: torch.Tensor, mask: torch.Tensor
221
+ ) -> torch.Tensor:
222
+ assert emissions.dim() == 3 and tags.dim() == 2
223
+ assert emissions.shape[:2] == tags.shape
224
+ assert emissions.size(2) == self.num_tags
225
+ assert mask.shape == tags.shape
226
+ assert mask[0].all()
227
+
228
+ device = emissions.device
229
+ tags = tags.to(device)
230
+ mask = mask.to(device)
231
+
232
+ seq_length, batch_size = tags.shape
233
+ mask = mask.type_as(emissions)
234
+
235
+ batch_indices = torch.arange(batch_size, device=device)
236
+ score = self.start_transitions[tags[0]]
237
+ score += emissions[0, batch_indices, tags[0]]
238
+
239
+ for i in range(1, seq_length):
240
+ score += self.transitions[tags[i - 1], tags[i]] * mask[i]
241
+ score += emissions[i, batch_indices, tags[i]] * mask[i]
242
+
243
+ seq_ends = mask.long().sum(dim=0) - 1
244
+ last_tags = tags[seq_ends, batch_indices]
245
+ score += self.end_transitions[last_tags]
246
+
247
+ return score
248
+
249
+ def _compute_normalizer(
250
+ self, emissions: torch.Tensor, mask: torch.Tensor
251
+ ) -> torch.Tensor:
252
+ assert emissions.dim() == 3 and mask.dim() == 2
253
+ assert emissions.shape[:2] == mask.shape
254
+ assert emissions.size(2) == self.num_tags
255
+ assert mask[0].all()
256
+
257
+ seq_length = emissions.size(0)
258
+ score = self.start_transitions + emissions[0]
259
+
260
+ for i in range(1, seq_length):
261
+ broadcast_score = score.unsqueeze(2)
262
+ broadcast_emissions = emissions[i].unsqueeze(1)
263
+ next_score = broadcast_score + self.transitions + broadcast_emissions
264
+ next_score = torch.logsumexp(next_score, dim=1)
265
+ score = torch.where(mask[i].unsqueeze(1).bool(), next_score, score)
266
+
267
+ score += self.end_transitions
268
+ return torch.logsumexp(score, dim=1)
269
+
270
+ def _viterbi_decode(
271
+ self, emissions: torch.Tensor, mask: torch.Tensor
272
+ ) -> List[List[int]]:
273
+ assert emissions.dim() == 3 and mask.dim() == 2
274
+ assert emissions.shape[:2] == mask.shape
275
+ assert emissions.size(2) == self.num_tags
276
+ assert mask[0].all()
277
+
278
+ seq_length, batch_size = mask.shape
279
+ score = self.start_transitions + emissions[0]
280
+ history = []
281
+
282
+ for i in range(1, seq_length):
283
+ broadcast_score = score.unsqueeze(2)
284
+ broadcast_emission = emissions[i].unsqueeze(1)
285
+ next_score = broadcast_score + self.transitions + broadcast_emission
286
+ next_score, indices = next_score.max(dim=1)
287
+ score = torch.where(mask[i].unsqueeze(1).bool(), next_score, score)
288
+ history.append(indices)
289
+
290
+ score += self.end_transitions
291
+
292
+ seq_ends = mask.long().sum(dim=0) - 1
293
+ best_tags_list = []
294
+
295
+ for idx in range(batch_size):
296
+ _, best_last_tag = score[idx].max(dim=0)
297
+ best_tags = [best_last_tag.item()]
298
+
299
+ for hist in reversed(history[: seq_ends[idx]]):
300
+ best_last_tag = hist[idx][best_tags[-1]]
301
+ best_tags.append(best_last_tag.item())
302
+
303
+ best_tags.reverse()
304
+ best_tags_list.append(best_tags)
305
+
306
+ return best_tags_list
307
+
308
+
309
+ class TokenClassificationModelCRF(PreTrainedModel):
310
+ """
311
+ Custom token classification model with CRF layer and configurable classifier head.
312
+ """
313
+
314
+ def __init__(
315
+ self,
316
+ config,
317
+ base_model=None,
318
+ freeze_backbone=False,
319
+ classifier_hidden_layers=None,
320
+ classifier_dropout=0.1,
321
+ ):
322
+ super().__init__(config)
323
+ self.config = config
324
+ self.num_labels = config.num_labels
325
+
326
+ if base_model is None:
327
+ self.roberta, backbone_name = _build_backbone_from_config(config)
328
+ else:
329
+ if hasattr(base_model, "roberta"):
330
+ self.roberta = base_model.roberta
331
+ else:
332
+ self.roberta = base_model
333
+ backbone_name = (
334
+ getattr(getattr(self.roberta, "config", None), "_name_or_path", None)
335
+ or getattr(config, "backbone_model_name", None)
336
+ or getattr(config, "_name_or_path", None)
337
+ )
338
+ if getattr(config, "backbone_model_name", None) is None:
339
+ config.backbone_model_name = backbone_name
340
+
341
+ self.lm_output_size = self.roberta.config.hidden_size
342
+
343
+ self.config.freeze_backbone = freeze_backbone
344
+ self.config.classifier_hidden_layers = classifier_hidden_layers
345
+ self.config.classifier_dropout = classifier_dropout
346
+
347
+ if freeze_backbone:
348
+ print("+" * 30, "\n\n", "Freezing backbone...", "+" * 30, "\n\n")
349
+ for param in self.roberta.parameters():
350
+ param.requires_grad = False
351
+ self.roberta.eval()
352
+ else:
353
+ print("+" * 30, "\n\n", "NOT Freezing backbone...", "+" * 30, "\n\n")
354
+ self.roberta.train(True)
355
+
356
+ self.dropout = nn.Dropout(getattr(config, "hidden_dropout_prob", 0.1))
357
+ self.crf = CRF(self.num_labels, batch_first=True)
358
+
359
+ self._build_classifier_head(classifier_hidden_layers, classifier_dropout)
360
+ self.post_init()
361
+
362
+ def _build_classifier_head(self, hidden_layers, dropout_rate):
363
+ layers = []
364
+ input_size = self.lm_output_size
365
+
366
+ if not hidden_layers:
367
+ self.classifier = nn.Sequential(
368
+ nn.Dropout(dropout_rate), nn.Linear(input_size, self.num_labels)
369
+ )
370
+ return
371
+
372
+ for hidden_size in hidden_layers:
373
+ layers.append(nn.Linear(input_size, hidden_size))
374
+ layers.append(nn.ReLU())
375
+ layers.append(nn.Dropout(dropout_rate))
376
+ input_size = hidden_size
377
+
378
+ layers.append(nn.Linear(input_size, self.num_labels))
379
+ self.classifier = nn.Sequential(*layers)
380
+
381
+ def forward(
382
+ self,
383
+ input_ids: Optional[torch.LongTensor] = None,
384
+ attention_mask: Optional[torch.FloatTensor] = None,
385
+ token_type_ids: Optional[torch.LongTensor] = None,
386
+ position_ids: Optional[torch.LongTensor] = None,
387
+ head_mask: Optional[torch.FloatTensor] = None,
388
+ inputs_embeds: Optional[torch.FloatTensor] = None,
389
+ labels: Optional[torch.LongTensor] = None,
390
+ output_attentions: Optional[bool] = None,
391
+ output_hidden_states: Optional[bool] = None,
392
+ return_dict: Optional[bool] = None,
393
+ **kwargs,
394
+ ) -> Union[Tuple[torch.Tensor], TokenClassifierOutput]:
395
+ return_dict = (
396
+ return_dict if return_dict is not None else self.config.use_return_dict
397
+ )
398
+
399
+ try:
400
+ outputs = self.roberta(
401
+ input_ids,
402
+ attention_mask=attention_mask,
403
+ token_type_ids=token_type_ids,
404
+ position_ids=position_ids,
405
+ head_mask=head_mask,
406
+ inputs_embeds=inputs_embeds,
407
+ output_attentions=output_attentions,
408
+ output_hidden_states=output_hidden_states,
409
+ return_dict=return_dict,
410
+ )
411
+ except TypeError:
412
+ outputs = self.roberta(
413
+ input_ids,
414
+ attention_mask=attention_mask,
415
+ position_ids=position_ids,
416
+ inputs_embeds=inputs_embeds,
417
+ output_attentions=output_attentions,
418
+ output_hidden_states=output_hidden_states,
419
+ return_dict=return_dict,
420
+ )
421
+
422
+ sequence_output = self.dropout(outputs.last_hidden_state)
423
+ logits = self.classifier(sequence_output)
424
+
425
+ loss = None
426
+ if labels is not None:
427
+ labels_long = labels.long()
428
+ if attention_mask is not None:
429
+ mask = attention_mask.bool()
430
+ loss = -self.crf(logits, labels_long, mask=mask, reduction="mean")
431
+ else:
432
+ if not getattr(self, "_warned_no_attention_mask", False):
433
+ print(
434
+ "WARNING: attention_mask is None; CRF loss will include padding tokens."
435
+ )
436
+ self._warned_no_attention_mask = True
437
+ loss = -self.crf(logits, labels_long, reduction="mean")
438
+
439
+ if not return_dict:
440
+ output = (logits,) + outputs[2:]
441
+ return ((loss,) + output) if loss is not None else output
442
+
443
+ return TokenClassifierOutput(
444
+ loss=loss,
445
+ logits=logits,
446
+ hidden_states=outputs.hidden_states,
447
+ attentions=outputs.attentions,
448
+ )
449
+
450
+ @property
451
+ def device_info(self):
452
+ return next(self.parameters()).device
453
+
454
+ def get_input_embeddings(self):
455
+ return self.roberta.get_input_embeddings()
456
+
457
+ def set_input_embeddings(self, value):
458
+ self.roberta.set_input_embeddings(value)
459
+
460
+ @classmethod
461
+ def from_pretrained(cls, pretrained_model_name_or_path, *model_args, **kwargs):
462
+ config = kwargs.pop("config", None)
463
+ if config is None:
464
+ from transformers import AutoConfig
465
+
466
+ config = AutoConfig.from_pretrained(pretrained_model_name_or_path, **kwargs)
467
+
468
+ freeze_backbone = getattr(config, "freeze_backbone", False)
469
+ classifier_hidden_layers = getattr(config, "classifier_hidden_layers", None)
470
+ classifier_dropout = getattr(config, "classifier_dropout", 0.1)
471
+
472
+ model = cls(
473
+ config=config,
474
+ freeze_backbone=freeze_backbone,
475
+ classifier_hidden_layers=classifier_hidden_layers,
476
+ classifier_dropout=classifier_dropout,
477
+ )
478
+
479
+ try:
480
+ state_dict = torch.load(
481
+ f"{pretrained_model_name_or_path}/pytorch_model.bin", map_location="cpu"
482
+ )
483
+ model.load_state_dict(state_dict)
484
+ except Exception:
485
+ print(
486
+ "Warning: Could not load pre-trained weights. Using randomly initialized model."
487
+ )
488
+
489
+ return model
490
+
491
+
492
+ class TokenClassificationModelMultiHeadCRF(PreTrainedModel):
493
+ """
494
+ Multi-Head CRF model for token classification with multiple entity types.
495
+ """
496
+
497
+ config_class = MultiHeadCRFConfig
498
+ base_model_prefix = "roberta"
499
+ _keys_to_ignore_on_load_unexpected = [r"pooler"]
500
+
501
+ def __init__(self, config, base_model=None, freeze_backbone=None):
502
+ super().__init__(config)
503
+ self.config = config
504
+
505
+ self.entity_types = getattr(config, "entity_types", [])
506
+ if not self.entity_types:
507
+ raise ValueError("entity_types must be provided in config")
508
+
509
+ self.num_labels = config.num_labels
510
+ self.number_of_layers_per_head = getattr(config, "number_of_layers_per_head", 1)
511
+ self.crf_reduction = getattr(config, "crf_reduction", "mean")
512
+ freeze_backbone = (
513
+ freeze_backbone
514
+ if freeze_backbone is not None
515
+ else getattr(config, "freeze_backbone", False)
516
+ )
517
+ self.num_frozen_encoders = getattr(config, "num_frozen_encoders", 0)
518
+ classifier_dropout = getattr(config, "classifier_dropout", 0.1)
519
+
520
+ if base_model is None:
521
+ self.roberta, backbone_name = _build_backbone_from_config(config)
522
+ else:
523
+ if hasattr(base_model, "roberta"):
524
+ self.roberta = base_model.roberta
525
+ else:
526
+ self.roberta = base_model
527
+ backbone_name = (
528
+ getattr(getattr(self.roberta, "config", None), "_name_or_path", None)
529
+ or getattr(config, "backbone_model_name", None)
530
+ or getattr(config, "_name_or_path", None)
531
+ )
532
+ if getattr(config, "backbone_model_name", None) is None:
533
+ config.backbone_model_name = backbone_name
534
+
535
+ self.hidden_size = self.roberta.config.hidden_size
536
+ self.dropout = nn.Dropout(getattr(config, "hidden_dropout_prob", 0.1))
537
+
538
+ print(f"Creating Multi-Head CRF with entity types: {sorted(self.entity_types)}")
539
+
540
+ for entity_type in self.entity_types:
541
+ for i in range(self.number_of_layers_per_head):
542
+ setattr(
543
+ self,
544
+ f"{entity_type}_dense_{i}",
545
+ nn.Linear(self.hidden_size, self.hidden_size),
546
+ )
547
+ setattr(
548
+ self,
549
+ f"{entity_type}_dense_activation_{i}",
550
+ nn.GELU(approximate="none"),
551
+ )
552
+ setattr(
553
+ self, f"{entity_type}_dropout_{i}", nn.Dropout(classifier_dropout)
554
+ )
555
+
556
+ setattr(
557
+ self,
558
+ f"{entity_type}_classifier",
559
+ nn.Linear(self.hidden_size, self.num_labels),
560
+ )
561
+ setattr(
562
+ self,
563
+ f"{entity_type}_crf",
564
+ MultiHeadCRF(num_tags=self.num_labels, batch_first=True),
565
+ )
566
+
567
+ if freeze_backbone:
568
+ self._freeze_backbone()
569
+
570
+ self.post_init()
571
+
572
+ def _freeze_backbone(self):
573
+ print("+" * 30, "\n\n", "Freezing backbone...", "+" * 30, "\n\n")
574
+
575
+ for param in self.roberta.embeddings.parameters():
576
+ param.requires_grad = False
577
+
578
+ if self.num_frozen_encoders > 0:
579
+ for _, param in islice(
580
+ self.roberta.encoder.named_parameters(),
581
+ self.num_frozen_encoders * NUM_PER_LAYER,
582
+ ):
583
+ param.requires_grad = False
584
+
585
+ def reset_head_parameters(self):
586
+ for entity_type in self.entity_types:
587
+ for i in range(self.number_of_layers_per_head):
588
+ getattr(self, f"{entity_type}_dense_{i}").reset_parameters()
589
+ getattr(self, f"{entity_type}_classifier").reset_parameters()
590
+ getattr(self, f"{entity_type}_crf").reset_parameters()
591
+ getattr(self, f"{entity_type}_crf").mask_impossible_transitions()
592
+
593
+ def forward(
594
+ self,
595
+ input_ids: Optional[torch.LongTensor] = None,
596
+ attention_mask: Optional[torch.FloatTensor] = None,
597
+ token_type_ids: Optional[torch.LongTensor] = None,
598
+ position_ids: Optional[torch.LongTensor] = None,
599
+ head_mask: Optional[torch.FloatTensor] = None,
600
+ inputs_embeds: Optional[torch.FloatTensor] = None,
601
+ labels: Optional[Dict[str, torch.LongTensor]] = None,
602
+ output_attentions: Optional[bool] = None,
603
+ output_hidden_states: Optional[bool] = None,
604
+ return_dict: Optional[bool] = None,
605
+ **kwargs,
606
+ ):
607
+ return_dict = (
608
+ return_dict if return_dict is not None else self.config.use_return_dict
609
+ )
610
+
611
+ try:
612
+ outputs = self.roberta(
613
+ input_ids,
614
+ attention_mask=attention_mask,
615
+ token_type_ids=token_type_ids,
616
+ position_ids=position_ids,
617
+ head_mask=head_mask,
618
+ inputs_embeds=inputs_embeds,
619
+ output_attentions=output_attentions,
620
+ output_hidden_states=output_hidden_states,
621
+ return_dict=return_dict,
622
+ )
623
+ except TypeError:
624
+ outputs = self.roberta(
625
+ input_ids,
626
+ attention_mask=attention_mask,
627
+ position_ids=position_ids,
628
+ inputs_embeds=inputs_embeds,
629
+ output_attentions=output_attentions,
630
+ output_hidden_states=output_hidden_states,
631
+ return_dict=return_dict,
632
+ )
633
+
634
+ sequence_output = outputs[0]
635
+ sequence_output = self.dropout(sequence_output)
636
+
637
+ logits = {}
638
+ for entity_type in self.entity_types:
639
+ head_output = sequence_output
640
+ for i in range(self.number_of_layers_per_head):
641
+ head_output = getattr(self, f"{entity_type}_dense_{i}")(head_output)
642
+ head_output = getattr(self, f"{entity_type}_dense_activation_{i}")(
643
+ head_output
644
+ )
645
+ head_output = getattr(self, f"{entity_type}_dropout_{i}")(head_output)
646
+ logits[entity_type] = getattr(self, f"{entity_type}_classifier")(
647
+ head_output
648
+ )
649
+
650
+ if labels is not None:
651
+ losses = {}
652
+ mask = attention_mask.bool() if attention_mask is not None else None
653
+
654
+ for entity_type in self.entity_types:
655
+ if entity_type in labels:
656
+ entity_labels = (
657
+ labels[entity_type].long().to(logits[entity_type].device)
658
+ )
659
+ crf = getattr(self, f"{entity_type}_crf")
660
+ if mask is not None:
661
+ losses[entity_type] = crf(
662
+ logits[entity_type],
663
+ entity_labels,
664
+ mask=mask,
665
+ reduction=self.crf_reduction,
666
+ )
667
+ else:
668
+ if not getattr(self, "_warned_no_attention_mask", False):
669
+ print(
670
+ "WARNING: attention_mask is None; CRF loss will include padding tokens."
671
+ )
672
+ self._warned_no_attention_mask = True
673
+ losses[entity_type] = crf(
674
+ logits[entity_type],
675
+ entity_labels,
676
+ reduction=self.crf_reduction,
677
+ )
678
+
679
+ total_loss = sum(losses.values())
680
+ return total_loss, logits
681
+
682
+ predictions = {}
683
+ mask = attention_mask.bool() if attention_mask is not None else None
684
+
685
+ for entity_type in self.entity_types:
686
+ crf = getattr(self, f"{entity_type}_crf")
687
+ if mask is not None:
688
+ decoded = crf.decode(logits[entity_type], mask=mask)
689
+ else:
690
+ decoded = crf.decode(logits[entity_type])
691
+ predictions[entity_type] = torch.tensor(decoded)
692
+
693
+ return [predictions[ent] for ent in sorted(self.entity_types)]
694
+
695
+ def get_input_embeddings(self):
696
+ return self.roberta.get_input_embeddings()
697
+
698
+ def set_input_embeddings(self, value):
699
+ self.roberta.set_input_embeddings(value)
700
+
701
+ @classmethod
702
+ def from_pretrained(cls, pretrained_model_name_or_path, *model_args, **kwargs):
703
+ import json
704
+ import os
705
+
706
+ config = kwargs.pop("config", None)
707
+
708
+ if config is None:
709
+ config_file = os.path.join(pretrained_model_name_or_path, "config.json")
710
+ if os.path.exists(config_file):
711
+ with open(config_file, "r") as f:
712
+ config_dict = json.load(f)
713
+ config = MultiHeadCRFConfig(**config_dict)
714
+ else:
715
+ from transformers import AutoConfig
716
+
717
+ config = AutoConfig.from_pretrained(
718
+ pretrained_model_name_or_path,
719
+ trust_remote_code=kwargs.get("trust_remote_code", True),
720
+ )
721
+
722
+ roberta_defaults = {
723
+ "layer_norm_eps": 1e-5,
724
+ "hidden_size": 768,
725
+ "num_hidden_layers": 12,
726
+ "num_attention_heads": 12,
727
+ "intermediate_size": 3072,
728
+ "hidden_act": "gelu",
729
+ "hidden_dropout_prob": 0.1,
730
+ "attention_probs_dropout_prob": 0.1,
731
+ "max_position_embeddings": 514,
732
+ "type_vocab_size": 1,
733
+ "initializer_range": 0.02,
734
+ "vocab_size": 52000,
735
+ "pad_token_id": 1,
736
+ "bos_token_id": 0,
737
+ "eos_token_id": 2,
738
+ "position_embedding_type": "absolute",
739
+ "use_cache": True,
740
+ "is_decoder": False,
741
+ "add_cross_attention": False,
742
+ "chunk_size_feed_forward": 0,
743
+ "output_hidden_states": False,
744
+ "output_attentions": False,
745
+ "torchscript": False,
746
+ "tie_word_embeddings": True,
747
+ "return_dict": True,
748
+ "gradient_checkpointing": False,
749
+ "pruned_heads": {},
750
+ "problem_type": None,
751
+ "embedding_size": None,
752
+ }
753
+
754
+ for key, default_value in roberta_defaults.items():
755
+ if not hasattr(config, key) or getattr(config, key) is None:
756
+ setattr(config, key, default_value)
757
+
758
+ freeze_backbone = getattr(config, "freeze_backbone", False)
759
+ model = cls(config=config, freeze_backbone=freeze_backbone)
760
+
761
+ weight_file = os.path.join(pretrained_model_name_or_path, "pytorch_model.bin")
762
+ safetensors_file = os.path.join(
763
+ pretrained_model_name_or_path, "model.safetensors"
764
+ )
765
+
766
+ try:
767
+ if os.path.exists(safetensors_file):
768
+ from safetensors.torch import load_file
769
+
770
+ state_dict = load_file(safetensors_file)
771
+ model.load_state_dict(state_dict)
772
+ elif os.path.exists(weight_file):
773
+ state_dict = torch.load(weight_file, map_location="cpu")
774
+ model.load_state_dict(state_dict)
775
+ else:
776
+ print(
777
+ "Warning: No pre-trained weights found. Using randomly initialized model."
778
+ )
779
+ except Exception as e:
780
+ print(f"Warning: Could not load pre-trained weights: {e}")
781
+
782
+ return model
783
+
784
+
785
+ class MultiHeadConfig(PretrainedConfig):
786
+ """
787
+ Configuration class for Multi-Head models (without CRF).
788
+ """
789
+
790
+ model_type = "multihead-tagger"
791
+
792
+ def __init__(
793
+ self,
794
+ entity_types: Optional[List[str]] = None,
795
+ number_of_layers_per_head: int = 1,
796
+ freeze_backbone: bool = False,
797
+ num_frozen_encoders: int = 0,
798
+ classifier_dropout: float = 0.1,
799
+ use_class_weights: bool = False,
800
+ class_weights: Optional[Dict[str, List[float]]] = None,
801
+ backbone_model_name: Optional[str] = None,
802
+ **kwargs,
803
+ ):
804
+ self.entity_types = entity_types or []
805
+ self.number_of_layers_per_head = number_of_layers_per_head
806
+ self.freeze_backbone = freeze_backbone
807
+ self.num_frozen_encoders = num_frozen_encoders
808
+ self.classifier_dropout = classifier_dropout
809
+ self.use_class_weights = use_class_weights
810
+ self.class_weights = class_weights
811
+ self.backbone_model_name = backbone_model_name
812
+ super().__init__(**kwargs)
813
+
814
+
815
+ class TokenClassificationModelMultiHead(PreTrainedModel):
816
+ """
817
+ Multi-Head model for token classification with multiple entity types (no CRF).
818
+ """
819
+
820
+ config_class = MultiHeadConfig
821
+ base_model_prefix = "roberta"
822
+ _keys_to_ignore_on_load_unexpected = [r"pooler"]
823
+
824
+ def __init__(self, config, base_model=None, freeze_backbone=None):
825
+ super().__init__(config)
826
+ self.config = config
827
+
828
+ self.entity_types = getattr(config, "entity_types", [])
829
+ if not self.entity_types:
830
+ raise ValueError("entity_types must be provided in config")
831
+
832
+ self.num_labels = config.num_labels
833
+ self.number_of_layers_per_head = getattr(config, "number_of_layers_per_head", 1)
834
+ freeze_backbone = (
835
+ freeze_backbone
836
+ if freeze_backbone is not None
837
+ else getattr(config, "freeze_backbone", False)
838
+ )
839
+ self.num_frozen_encoders = getattr(config, "num_frozen_encoders", 0)
840
+ classifier_dropout = getattr(config, "classifier_dropout", 0.1)
841
+
842
+ self.use_class_weights = getattr(config, "use_class_weights", False)
843
+ self.class_weights = getattr(config, "class_weights", None)
844
+
845
+ if base_model is None:
846
+ self.roberta, backbone_name = _build_backbone_from_config(config)
847
+ else:
848
+ if hasattr(base_model, "roberta"):
849
+ self.roberta = base_model.roberta
850
+ else:
851
+ self.roberta = base_model
852
+ backbone_name = (
853
+ getattr(getattr(self.roberta, "config", None), "_name_or_path", None)
854
+ or getattr(config, "backbone_model_name", None)
855
+ or getattr(config, "_name_or_path", None)
856
+ )
857
+ if getattr(config, "backbone_model_name", None) is None:
858
+ config.backbone_model_name = backbone_name
859
+
860
+ self.hidden_size = self.roberta.config.hidden_size
861
+ self.dropout = nn.Dropout(getattr(config, "hidden_dropout_prob", 0.1))
862
+
863
+ print(
864
+ f"Creating Multi-Head model with entity types: {sorted(self.entity_types)}"
865
+ )
866
+
867
+ for entity_type in self.entity_types:
868
+ for i in range(self.number_of_layers_per_head):
869
+ setattr(
870
+ self,
871
+ f"{entity_type}_dense_{i}",
872
+ nn.Linear(self.hidden_size, self.hidden_size),
873
+ )
874
+ setattr(
875
+ self,
876
+ f"{entity_type}_dense_activation_{i}",
877
+ nn.GELU(approximate="none"),
878
+ )
879
+ setattr(
880
+ self, f"{entity_type}_dropout_{i}", nn.Dropout(classifier_dropout)
881
+ )
882
+
883
+ setattr(
884
+ self,
885
+ f"{entity_type}_classifier",
886
+ nn.Linear(self.hidden_size, self.num_labels),
887
+ )
888
+
889
+ self.loss_fns = nn.ModuleDict()
890
+ for entity_type in self.entity_types:
891
+ if (
892
+ self.use_class_weights
893
+ and self.class_weights
894
+ and entity_type in self.class_weights
895
+ ):
896
+ weight = torch.tensor(
897
+ self.class_weights[entity_type], dtype=torch.float
898
+ )
899
+ self.loss_fns[entity_type] = nn.CrossEntropyLoss(
900
+ weight=weight, ignore_index=-100
901
+ )
902
+ else:
903
+ self.loss_fns[entity_type] = nn.CrossEntropyLoss(ignore_index=-100)
904
+
905
+ if freeze_backbone:
906
+ self._freeze_backbone()
907
+
908
+ self.post_init()
909
+
910
+ def _freeze_backbone(self):
911
+ print("+" * 30, "\n\n", "Freezing backbone...", "+" * 30, "\n\n")
912
+
913
+ for param in self.roberta.embeddings.parameters():
914
+ param.requires_grad = False
915
+
916
+ if self.num_frozen_encoders > 0:
917
+ for _, param in islice(
918
+ self.roberta.encoder.named_parameters(),
919
+ self.num_frozen_encoders * NUM_PER_LAYER,
920
+ ):
921
+ param.requires_grad = False
922
+
923
+ def reset_head_parameters(self):
924
+ for entity_type in self.entity_types:
925
+ for i in range(self.number_of_layers_per_head):
926
+ getattr(self, f"{entity_type}_dense_{i}").reset_parameters()
927
+ getattr(self, f"{entity_type}_classifier").reset_parameters()
928
+
929
+ def forward(
930
+ self,
931
+ input_ids: Optional[torch.LongTensor] = None,
932
+ attention_mask: Optional[torch.FloatTensor] = None,
933
+ token_type_ids: Optional[torch.LongTensor] = None,
934
+ position_ids: Optional[torch.LongTensor] = None,
935
+ head_mask: Optional[torch.FloatTensor] = None,
936
+ inputs_embeds: Optional[torch.FloatTensor] = None,
937
+ labels: Optional[Dict[str, torch.LongTensor]] = None,
938
+ output_attentions: Optional[bool] = None,
939
+ output_hidden_states: Optional[bool] = None,
940
+ return_dict: Optional[bool] = None,
941
+ **kwargs,
942
+ ):
943
+ return_dict = (
944
+ return_dict if return_dict is not None else self.config.use_return_dict
945
+ )
946
+
947
+ try:
948
+ outputs = self.roberta(
949
+ input_ids,
950
+ attention_mask=attention_mask,
951
+ token_type_ids=token_type_ids,
952
+ position_ids=position_ids,
953
+ head_mask=head_mask,
954
+ inputs_embeds=inputs_embeds,
955
+ output_attentions=output_attentions,
956
+ output_hidden_states=output_hidden_states,
957
+ return_dict=return_dict,
958
+ )
959
+ except TypeError:
960
+ outputs = self.roberta(
961
+ input_ids,
962
+ attention_mask=attention_mask,
963
+ position_ids=position_ids,
964
+ inputs_embeds=inputs_embeds,
965
+ output_attentions=output_attentions,
966
+ output_hidden_states=output_hidden_states,
967
+ return_dict=return_dict,
968
+ )
969
+
970
+ sequence_output = outputs[0]
971
+ sequence_output = self.dropout(sequence_output)
972
+
973
+ logits = {}
974
+ for entity_type in self.entity_types:
975
+ head_output = sequence_output
976
+ for i in range(self.number_of_layers_per_head):
977
+ head_output = getattr(self, f"{entity_type}_dense_{i}")(head_output)
978
+ head_output = getattr(self, f"{entity_type}_dense_activation_{i}")(
979
+ head_output
980
+ )
981
+ head_output = getattr(self, f"{entity_type}_dropout_{i}")(head_output)
982
+ logits[entity_type] = getattr(self, f"{entity_type}_classifier")(
983
+ head_output
984
+ )
985
+
986
+ if labels is not None:
987
+ losses = {}
988
+
989
+ for entity_type in self.entity_types:
990
+ if entity_type in labels:
991
+ entity_labels = (
992
+ labels[entity_type].long().to(logits[entity_type].device)
993
+ )
994
+ entity_logits = logits[entity_type]
995
+ loss_fct = self.loss_fns[entity_type]
996
+
997
+ if hasattr(loss_fct, "weight") and loss_fct.weight is not None:
998
+ loss_fct.weight = loss_fct.weight.to(entity_logits.device)
999
+
1000
+ losses[entity_type] = loss_fct(
1001
+ entity_logits.view(-1, self.num_labels),
1002
+ entity_labels.view(-1),
1003
+ )
1004
+
1005
+ total_loss = sum(losses.values())
1006
+ return total_loss, logits
1007
+
1008
+ predictions = {}
1009
+ for entity_type in self.entity_types:
1010
+ preds = torch.argmax(logits[entity_type], dim=-1)
1011
+ predictions[entity_type] = preds
1012
+
1013
+ return [predictions[ent] for ent in sorted(self.entity_types)]
1014
+
1015
+ def get_input_embeddings(self):
1016
+ return self.roberta.get_input_embeddings()
1017
+
1018
+ def set_input_embeddings(self, value):
1019
+ self.roberta.set_input_embeddings(value)
1020
+
1021
+ @classmethod
1022
+ def from_pretrained(cls, pretrained_model_name_or_path, *model_args, **kwargs):
1023
+ import json
1024
+ import os
1025
+
1026
+ config = kwargs.pop("config", None)
1027
+
1028
+ if config is None:
1029
+ config_file = os.path.join(pretrained_model_name_or_path, "config.json")
1030
+ if os.path.exists(config_file):
1031
+ with open(config_file, "r") as f:
1032
+ config_dict = json.load(f)
1033
+ config = MultiHeadConfig(**config_dict)
1034
+ else:
1035
+ from transformers import AutoConfig
1036
+
1037
+ config = AutoConfig.from_pretrained(
1038
+ pretrained_model_name_or_path,
1039
+ trust_remote_code=kwargs.get("trust_remote_code", True),
1040
+ )
1041
+
1042
+ roberta_defaults = {
1043
+ "layer_norm_eps": 1e-5,
1044
+ "hidden_size": 768,
1045
+ "num_hidden_layers": 12,
1046
+ "num_attention_heads": 12,
1047
+ "intermediate_size": 3072,
1048
+ "hidden_act": "gelu",
1049
+ "hidden_dropout_prob": 0.1,
1050
+ "attention_probs_dropout_prob": 0.1,
1051
+ "max_position_embeddings": 514,
1052
+ "type_vocab_size": 1,
1053
+ "initializer_range": 0.02,
1054
+ "vocab_size": 52000,
1055
+ "pad_token_id": 1,
1056
+ "bos_token_id": 0,
1057
+ "eos_token_id": 2,
1058
+ "position_embedding_type": "absolute",
1059
+ "use_cache": True,
1060
+ "is_decoder": False,
1061
+ "add_cross_attention": False,
1062
+ "chunk_size_feed_forward": 0,
1063
+ "output_hidden_states": False,
1064
+ "output_attentions": False,
1065
+ "torchscript": False,
1066
+ "tie_word_embeddings": True,
1067
+ "return_dict": True,
1068
+ "gradient_checkpointing": False,
1069
+ "pruned_heads": {},
1070
+ "problem_type": None,
1071
+ "embedding_size": None,
1072
+ }
1073
+
1074
+ for key, default_value in roberta_defaults.items():
1075
+ if not hasattr(config, key) or getattr(config, key) is None:
1076
+ setattr(config, key, default_value)
1077
+
1078
+ freeze_backbone = getattr(config, "freeze_backbone", False)
1079
+ model = cls(config=config, freeze_backbone=freeze_backbone)
1080
+
1081
+ weight_file = os.path.join(pretrained_model_name_or_path, "pytorch_model.bin")
1082
+ safetensors_file = os.path.join(
1083
+ pretrained_model_name_or_path, "model.safetensors"
1084
+ )
1085
+
1086
+ try:
1087
+ if os.path.exists(safetensors_file):
1088
+ from safetensors.torch import load_file
1089
+
1090
+ state_dict = load_file(safetensors_file)
1091
+ model.load_state_dict(state_dict)
1092
+ elif os.path.exists(weight_file):
1093
+ state_dict = torch.load(weight_file, map_location="cpu")
1094
+ model.load_state_dict(state_dict)
1095
+ else:
1096
+ print(
1097
+ "Warning: No pre-trained weights found. Using randomly initialized model."
1098
+ )
1099
+ except Exception as e:
1100
+ print(f"Warning: Could not load pre-trained weights: {e}")
1101
+
1102
+ return model
1103
+
1104
+
1105
+ class TokenClassificationModel(PreTrainedModel):
1106
+ """
1107
+ Custom token classification model with configurable classifier head (no CRF).
1108
+ """
1109
+
1110
+ def __init__(self, config, base_model=None):
1111
+ super().__init__(config)
1112
+ self.config = config
1113
+ self.num_labels = config.num_labels
1114
+
1115
+ if base_model is None:
1116
+ self.roberta, backbone_name = _build_backbone_from_config(config)
1117
+ else:
1118
+ if hasattr(base_model, "roberta"):
1119
+ self.roberta = base_model.roberta
1120
+ else:
1121
+ self.roberta = base_model
1122
+ backbone_name = (
1123
+ getattr(getattr(self.roberta, "config", None), "_name_or_path", None)
1124
+ or getattr(config, "backbone_model_name", None)
1125
+ or getattr(config, "_name_or_path", None)
1126
+ )
1127
+
1128
+ if getattr(config, "backbone_model_name", None) is None:
1129
+ config.backbone_model_name = backbone_name
1130
+
1131
+ self.dropout = nn.Dropout(getattr(config, "hidden_dropout_prob", 0.1))
1132
+
1133
+ classifier_hidden_layers = getattr(config, "classifier_hidden_layers", None)
1134
+ classifier_dropout = getattr(config, "classifier_dropout", 0.1)
1135
+
1136
+ if classifier_hidden_layers is not None:
1137
+ in_size = self.roberta.config.hidden_size
1138
+ layers = []
1139
+ if classifier_hidden_layers:
1140
+ for h in classifier_hidden_layers:
1141
+ layers += [
1142
+ nn.Linear(in_size, h),
1143
+ nn.ReLU(),
1144
+ nn.Dropout(classifier_dropout),
1145
+ ]
1146
+ in_size = h
1147
+ layers.append(nn.Linear(in_size, config.num_labels))
1148
+ self.classifier = nn.Sequential(*layers)
1149
+ else:
1150
+ self.classifier = nn.Linear(
1151
+ self.roberta.config.hidden_size, config.num_labels
1152
+ )
1153
+
1154
+ if isinstance(self.classifier, nn.Sequential):
1155
+ for module in self.classifier:
1156
+ if isinstance(module, nn.Linear):
1157
+ self._init_weights(module)
1158
+ elif isinstance(self.classifier, nn.Linear):
1159
+ self._init_weights(self.classifier)
1160
+
1161
+ self.post_init()
1162
+
1163
+ def forward(
1164
+ self,
1165
+ input_ids: Optional[torch.LongTensor] = None,
1166
+ attention_mask: Optional[torch.FloatTensor] = None,
1167
+ token_type_ids: Optional[torch.LongTensor] = None,
1168
+ position_ids: Optional[torch.LongTensor] = None,
1169
+ head_mask: Optional[torch.FloatTensor] = None,
1170
+ inputs_embeds: Optional[torch.FloatTensor] = None,
1171
+ labels: Optional[torch.LongTensor] = None,
1172
+ output_attentions: Optional[bool] = None,
1173
+ output_hidden_states: Optional[bool] = None,
1174
+ return_dict: Optional[bool] = None,
1175
+ **kwargs,
1176
+ ) -> Union[Tuple[torch.Tensor], TokenClassifierOutput]:
1177
+ return_dict = (
1178
+ return_dict if return_dict is not None else self.config.use_return_dict
1179
+ )
1180
+
1181
+ try:
1182
+ outputs = self.roberta(
1183
+ input_ids,
1184
+ attention_mask=attention_mask,
1185
+ token_type_ids=token_type_ids,
1186
+ position_ids=position_ids,
1187
+ head_mask=head_mask,
1188
+ inputs_embeds=inputs_embeds,
1189
+ output_attentions=output_attentions,
1190
+ output_hidden_states=output_hidden_states,
1191
+ return_dict=return_dict,
1192
+ )
1193
+ except TypeError:
1194
+ outputs = self.roberta(
1195
+ input_ids,
1196
+ attention_mask=attention_mask,
1197
+ position_ids=position_ids,
1198
+ inputs_embeds=inputs_embeds,
1199
+ output_attentions=output_attentions,
1200
+ output_hidden_states=output_hidden_states,
1201
+ return_dict=return_dict,
1202
+ )
1203
+
1204
+ sequence_output = outputs.last_hidden_state
1205
+ sequence_output = self.dropout(sequence_output)
1206
+ logits = self.classifier(sequence_output)
1207
+
1208
+ loss = None
1209
+ if labels is not None:
1210
+ loss_fct = nn.CrossEntropyLoss()
1211
+ if attention_mask is not None:
1212
+ active_loss = attention_mask.view(-1) == 1
1213
+ active_logits = logits.view(-1, self.num_labels)[active_loss]
1214
+ active_labels = labels.view(-1)[active_loss]
1215
+ loss = loss_fct(active_logits, active_labels)
1216
+ else:
1217
+ loss = loss_fct(logits.view(-1, self.num_labels), labels.view(-1))
1218
+
1219
+ if not return_dict:
1220
+ output = (logits,) + outputs[2:]
1221
+ return ((loss,) + output) if loss is not None else output
1222
+
1223
+ return TokenClassifierOutput(
1224
+ loss=loss,
1225
+ logits=logits,
1226
+ hidden_states=outputs.hidden_states,
1227
+ attentions=outputs.attentions,
1228
+ )
1229
+
1230
+ def get_input_embeddings(self):
1231
+ return self.roberta.get_input_embeddings()
1232
+
1233
+ def set_input_embeddings(self, value):
1234
+ self.roberta.set_input_embeddings(value)
1235
+
1236
+
1237
+ def load_custom_cardioner_multiclass_model(model_path: str, device: str = "auto"):
1238
+ import os
1239
+
1240
+ import torch
1241
+ from transformers import AutoModelForTokenClassification, AutoTokenizer
1242
+
1243
+ required_files = ["config.json", "modeling.py", "pytorch_model.bin"]
1244
+ missing_files = [
1245
+ f for f in required_files if not os.path.exists(os.path.join(model_path, f))
1246
+ ]
1247
+
1248
+ if missing_files:
1249
+ raise FileNotFoundError(
1250
+ f"Missing required files in {model_path}: {missing_files}"
1251
+ )
1252
+
1253
+ print(f"Loading custom CardioNER multiclass model from: {model_path}")
1254
+
1255
+ tokenizer = AutoTokenizer.from_pretrained(model_path)
1256
+
1257
+ model = AutoModelForTokenClassification.from_pretrained(
1258
+ model_path,
1259
+ trust_remote_code=True,
1260
+ use_safetensors=True,
1261
+ )
1262
+
1263
+ if device == "auto":
1264
+ device = "cuda" if torch.cuda.is_available() else "cpu"
1265
+
1266
+ model = model.to(device)
1267
+
1268
+ print(f"Model loaded successfully on {device}")
1269
+ print(f"Model type: {type(model).__name__}")
1270
+ print(f"Number of labels: {model.num_labels}")
1271
+
1272
+ return model, tokenizer, model.config
1273
+
1274
+
1275
+ def load_custom_multihead_crf_model(model_path: str, device: str = "auto"):
1276
+ import json
1277
+ import os
1278
+
1279
+ from transformers import AutoTokenizer
1280
+
1281
+ required_files = ["config.json", "modeling.py"]
1282
+ missing_files = [
1283
+ f for f in required_files if not os.path.exists(os.path.join(model_path, f))
1284
+ ]
1285
+
1286
+ if missing_files:
1287
+ raise FileNotFoundError(
1288
+ f"Missing required files in {model_path}: {missing_files}"
1289
+ )
1290
+
1291
+ print(f"Loading Multi-Head CRF model from: {model_path}")
1292
+
1293
+ tokenizer = AutoTokenizer.from_pretrained(model_path)
1294
+
1295
+ with open(os.path.join(model_path, "config.json"), "r") as f:
1296
+ config_dict = json.load(f)
1297
+
1298
+ config = MultiHeadCRFConfig(**config_dict)
1299
+
1300
+ model = TokenClassificationModelMultiHeadCRF.from_pretrained(
1301
+ model_path, config=config
1302
+ )
1303
+
1304
+ if device == "auto":
1305
+ device = "cuda" if torch.cuda.is_available() else "cpu"
1306
+
1307
+ model = model.to(device)
1308
+
1309
+ print(f"Model loaded successfully on {device}")
1310
+ print(f"Model type: {type(model).__name__}")
1311
+ print(f"Entity types: {model.entity_types}")
1312
+ print(f"Number of labels per head: {model.num_labels}")
1313
+
1314
+ return model, tokenizer, model.config
1315
+
1316
+
1317
+ def validate_custom_multiclass_model_directory(model_path: str) -> dict:
1318
+ import json
1319
+ import os
1320
+
1321
+ validation_results = {
1322
+ "valid": True,
1323
+ "errors": [],
1324
+ "warnings": [],
1325
+ "files_found": [],
1326
+ "model_info": {},
1327
+ }
1328
+
1329
+ required_files = {
1330
+ "config.json": "Model configuration",
1331
+ "modeling.py": "Custom model class definition",
1332
+ "pytorch_model.bin": "Model weights",
1333
+ }
1334
+
1335
+ optional_files = {
1336
+ "tokenizer.json": "Tokenizer vocabulary",
1337
+ "tokenizer_config.json": "Tokenizer configuration",
1338
+ "training_args.json": "Training arguments",
1339
+ }
1340
+
1341
+ for filename, description in required_files.items():
1342
+ filepath = os.path.join(model_path, filename)
1343
+ if os.path.exists(filepath):
1344
+ validation_results["files_found"].append(f"{filename} ({description})")
1345
+ else:
1346
+ validation_results["valid"] = False
1347
+ validation_results["errors"].append(
1348
+ f"Missing required file: {filename} - {description}"
1349
+ )
1350
+
1351
+ for filename, description in optional_files.items():
1352
+ filepath = os.path.join(model_path, filename)
1353
+ if os.path.exists(filepath):
1354
+ validation_results["files_found"].append(f"{filename} ({description})")
1355
+ else:
1356
+ validation_results["warnings"].append(
1357
+ f"Missing optional file: {filename} - {description}"
1358
+ )
1359
+
1360
+ config_path = os.path.join(model_path, "config.json")
1361
+ if os.path.exists(config_path):
1362
+ try:
1363
+ with open(config_path, "r") as f:
1364
+ config = json.load(f)
1365
+
1366
+ validation_results["model_info"]["num_labels"] = config.get(
1367
+ "num_labels", "Unknown"
1368
+ )
1369
+ validation_results["model_info"]["model_type"] = config.get(
1370
+ "model_type", "Unknown"
1371
+ )
1372
+ validation_results["model_info"]["has_auto_map"] = "auto_map" in config
1373
+ validation_results["model_info"]["classifier_hidden_layers"] = config.get(
1374
+ "classifier_hidden_layers", None
1375
+ )
1376
+ validation_results["model_info"]["freeze_backbone"] = config.get(
1377
+ "freeze_backbone", None
1378
+ )
1379
+ validation_results["model_info"]["use_crf"] = (
1380
+ "TokenClassificationModelCRF" in str(config.get("architectures", []))
1381
+ )
1382
+
1383
+ if not config.get("auto_map"):
1384
+ validation_results["warnings"].append(
1385
+ "No auto_map found in config - may not load correctly with trust_remote_code=True"
1386
+ )
1387
+
1388
+ except json.JSONDecodeError as e:
1389
+ validation_results["valid"] = False
1390
+ validation_results["errors"].append(f"Invalid config.json: {str(e)}")
1391
+
1392
+ modeling_path = os.path.join(model_path, "modeling.py")
1393
+ if os.path.exists(modeling_path):
1394
+ try:
1395
+ with open(modeling_path, "r") as f:
1396
+ content = f.read()
1397
+
1398
+ required_classes = [
1399
+ "TokenClassificationModel",
1400
+ "TokenClassificationModelCRF",
1401
+ ]
1402
+ missing_classes = [cls for cls in required_classes if cls not in content]
1403
+
1404
+ if missing_classes:
1405
+ validation_results["valid"] = False
1406
+ validation_results["errors"].append(
1407
+ f"modeling.py missing required classes: {missing_classes}"
1408
+ )
1409
+
1410
+ except Exception as e:
1411
+ validation_results["warnings"].append(
1412
+ f"Could not read modeling.py: {str(e)}"
1413
+ )
1414
+
1415
+ return validation_results
1416
+
1417
+
1418
+ try:
1419
+ from transformers import AutoConfig
1420
+
1421
+ AutoConfig.register("multihead-crf-tagger", MultiHeadCRFConfig)
1422
+ except Exception:
1423
+ pass
1424
+
1425
+
1426
+ def patch_legacy_model(
1427
+ model_path: str, backbone_model_name: str, dry_run: bool = True
1428
+ ) -> bool:
1429
+ import json
1430
+ import os
1431
+ import shutil
1432
+
1433
+ config_path = os.path.join(model_path, "config.json")
1434
+
1435
+ if not os.path.exists(config_path):
1436
+ print(f"ERROR: config.json not found at {config_path}")
1437
+ return False
1438
+
1439
+ with open(config_path, "r") as f:
1440
+ config = json.load(f)
1441
+
1442
+ if "backbone_model_name" in config:
1443
+ print(f"Model already has backbone_model_name: {config['backbone_model_name']}")
1444
+ if config["backbone_model_name"] == backbone_model_name:
1445
+ print("No changes needed.")
1446
+ return True
1447
+ else:
1448
+ print(f"WARNING: Existing backbone_model_name differs from provided value!")
1449
+ print(f" Existing: {config['backbone_model_name']}")
1450
+ print(f" Provided: {backbone_model_name}")
1451
+ if dry_run:
1452
+ print("Would update to new value (dry_run=True)")
1453
+ else:
1454
+ print("Updating to new value...")
1455
+
1456
+ config["backbone_model_name"] = backbone_model_name
1457
+
1458
+ if dry_run:
1459
+ print(f"\n[DRY RUN] Would patch {config_path}:")
1460
+ print(f' Adding: backbone_model_name = "{backbone_model_name}"')
1461
+ print("\nTo apply this patch, run with dry_run=False")
1462
+ return True
1463
+
1464
+ backup_path = config_path + ".backup"
1465
+ shutil.copy2(config_path, backup_path)
1466
+ print(f"Created backup at {backup_path}")
1467
+
1468
+ with open(config_path, "w") as f:
1469
+ json.dump(config, f, indent=2)
1470
+
1471
+ print(f"Successfully patched {config_path}")
1472
+ print(f' Added: backbone_model_name = "{backbone_model_name}"')
1473
+
1474
+ return True
1475
+
1476
+
1477
+ def patch_multiple_models(
1478
+ model_paths: list, backbone_model_name: str, dry_run: bool = True
1479
+ ) -> dict:
1480
+ results = {}
1481
+ for path in model_paths:
1482
+ print(f"\n{'=' * 60}")
1483
+ print(f"Processing: {path}")
1484
+ print("=" * 60)
1485
+ results[path] = patch_legacy_model(path, backbone_model_name, dry_run)
1486
+
1487
+ print(f"\n{'=' * 60}")
1488
+ print("SUMMARY")
1489
+ print("=" * 60)
1490
+ success = sum(1 for v in results.values() if v)
1491
+ print(
1492
+ f"Successfully {'would patch' if dry_run else 'patched'}: {success}/{len(model_paths)}"
1493
+ )
1494
+
1495
+ return results