| """Self-contained definition of the hybrid RoBERTa + linguistic-features model. |
| |
| The released checkpoint ``hybrid_model_best.pt`` is a plain PyTorch |
| ``state_dict`` for the ``HybridClassifier`` defined here. Import this module to |
| rebuild the architecture and load the weights. |
| """ |
| import torch |
| import torch.nn as nn |
| from transformers import RobertaModel |
|
|
| CONFIG = { |
| "roberta_model": "roberta-base", |
| "roberta_dim": 768, |
| "ling_dim": 25, |
| "ling_hidden": 256, |
| "hidden_dim": 768, |
| "dropout": 0.15, |
| "freeze_bottom_layers": 6, |
| } |
|
|
| LING_FEATURE_NAMES = [ |
| "msttr", "avg_word_len", "hapax_ratio", "function_ratio", "punct_density", |
| "char_entropy", "burstiness", "repetition_ratio", "avg_sent_len", |
| "sent_len_std", "noun_ratio", "verb_ratio", "adj_ratio", "adv_ratio", |
| "pron_ratio", "pos_diversity", "avg_tree_depth", "max_tree_depth", |
| "sub_clause_ratio", "dm_density", "sent_len_cv", "fp_ratio", |
| "num_sentences", "words_per_sent", "perplexity", |
| ] |
|
|
|
|
| class FeatureAttention(nn.Module): |
| def __init__(self, num_features): |
| super().__init__() |
| self.attention = nn.Sequential( |
| nn.Linear(num_features, num_features), |
| nn.Tanh(), |
| nn.Linear(num_features, num_features), |
| nn.Softmax(dim=-1), |
| ) |
|
|
| def forward(self, features): |
| attn_weights = self.attention(features) |
| weighted_features = features * attn_weights |
| return weighted_features, attn_weights |
|
|
|
|
| class GatedFusion(nn.Module): |
| def __init__(self, dim): |
| super().__init__() |
| self.gate = nn.Sequential( |
| nn.Linear(dim * 2, dim), |
| nn.Sigmoid(), |
| ) |
|
|
| def forward(self, cls_emb, ling_emb): |
| combined = torch.cat([cls_emb, ling_emb], dim=-1) |
| g = self.gate(combined) |
| fused = g * cls_emb + (1 - g) * ling_emb |
| return fused, g |
|
|
|
|
| class HybridClassifier(nn.Module): |
| """RoBERTa CLS embedding gated-fused with attended linguistic features.""" |
|
|
| def __init__(self, config=CONFIG): |
| super().__init__() |
| self.roberta = RobertaModel.from_pretrained(config["roberta_model"]) |
| if config["freeze_bottom_layers"] > 0: |
| for param in self.roberta.embeddings.parameters(): |
| param.requires_grad = False |
| for i in range(config["freeze_bottom_layers"]): |
| for param in self.roberta.encoder.layer[i].parameters(): |
| param.requires_grad = False |
| self.feature_attention = FeatureAttention(config["ling_dim"]) |
| self.ling_projection = nn.Sequential( |
| nn.Linear(config["ling_dim"], config["ling_hidden"]), |
| nn.LayerNorm(config["ling_hidden"]), |
| nn.GELU(), |
| nn.Dropout(config["dropout"]), |
| nn.Linear(config["ling_hidden"], config["roberta_dim"]), |
| nn.LayerNorm(config["roberta_dim"]), |
| nn.GELU(), |
| nn.Dropout(config["dropout"]), |
| ) |
| self.gated_fusion = GatedFusion(config["roberta_dim"]) |
| self.classifier = nn.Sequential( |
| nn.Linear(config["roberta_dim"], config["hidden_dim"]), |
| nn.GELU(), |
| nn.Dropout(config["dropout"]), |
| nn.Linear(config["hidden_dim"], 2), |
| ) |
| self.dropout = nn.Dropout(config["dropout"]) |
| self.config = config |
|
|
| def forward(self, input_ids, attention_mask, ling_features, return_gate=False): |
| outputs = self.roberta(input_ids=input_ids, attention_mask=attention_mask) |
| cls_embedding = outputs.last_hidden_state[:, 0, :] |
| cls_embedding = self.dropout(cls_embedding) |
| attended_features, attn_weights = self.feature_attention(ling_features) |
| ling_proj = self.ling_projection(attended_features) |
| fused, gate_values = self.gated_fusion(cls_embedding, ling_proj) |
| logits = self.classifier(fused) |
| if return_gate: |
| return logits, gate_values, attn_weights |
| return logits |
|
|
|
|
| def load_model(checkpoint_path="hybrid_model_best.pt", device="cpu"): |
| """Build the model and load the released weights. Returns an eval-mode model.""" |
| model = HybridClassifier(CONFIG) |
| state_dict = torch.load(checkpoint_path, map_location=device) |
| model.load_state_dict(state_dict) |
| model.to(device) |
| model.eval() |
| return model |
|
|