foggughost0's picture
Upload 6 files
449aff1 verified
Raw
History Blame Contribute Delete
4.33 kB
"""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