Spaces:
Running
Running
| """ | |
| model.py β TRALSem: Textual Robust Adversarial Learning for Sentiment Analysis | |
| ================================================================================= | |
| Architecture (Paper Β§3): | |
| Input | |
| β BERT Tokeniser (token_ids, segment_ids, position_ids) | |
| β Data Encoder (token + position + segment embeddings) | |
| β Transformer Encoder (12 layers, multi-head self-attention) | |
| β [CLS] pooling | |
| β Linear Layer (regression-style projection) | |
| β Softmax (classification output) | |
| The Random-Seed mechanism (LCG) ensures reproducibility, and adversarial | |
| perturbations are injected at the embedding level during training (see | |
| adversarial.py and train.py). | |
| """ | |
| import math | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| from config import ( | |
| HIDDEN_SIZE, NUM_ATTENTION_HEADS, NUM_HIDDEN_LAYERS, | |
| INTERMEDIATE_SIZE, HIDDEN_DROPOUT, ATTENTION_DROPOUT, | |
| MAX_SEQ_LEN, LCG_A, LCG_C, LCG_M, LCG_SEED, | |
| ) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Random Seed Generator (LCG) β Paper Β§3.2 | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class LCGRandomSeed: | |
| """ | |
| Linear Congruential Generator for deterministic seed production. | |
| s_{n+1} = (a Β· s_n + c) mod m | |
| Used to set random seeds at each training step for exact reproducibility. | |
| """ | |
| def __init__(self, a: int = LCG_A, c: int = LCG_C, | |
| m: int = LCG_M, seed: int = LCG_SEED): | |
| self.a = a | |
| self.c = c | |
| self.m = m | |
| self.state = seed | |
| def next(self) -> int: | |
| self.state = (self.a * self.state + self.c) % self.m | |
| return self.state | |
| def set_global_seed(self): | |
| """Push deterministic seed into PyTorch + Python.""" | |
| s = self.next() | |
| torch.manual_seed(s) | |
| if torch.cuda.is_available(): | |
| torch.cuda.manual_seed_all(s) | |
| return s | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Multi-Head Self-Attention β Paper Eq: Attention(Q,K,V)=softmax(QK^T/βd_k)V | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class MultiHeadSelfAttention(nn.Module): | |
| """Exact implementation of scaled dot-product multi-head attention.""" | |
| def __init__(self, hidden_size: int = HIDDEN_SIZE, | |
| num_heads: int = NUM_ATTENTION_HEADS, | |
| dropout: float = ATTENTION_DROPOUT): | |
| super().__init__() | |
| assert hidden_size % num_heads == 0 | |
| self.num_heads = num_heads | |
| self.head_dim = hidden_size // num_heads # d_k | |
| self.query = nn.Linear(hidden_size, hidden_size) | |
| self.key = nn.Linear(hidden_size, hidden_size) | |
| self.value = nn.Linear(hidden_size, hidden_size) | |
| self.out = nn.Linear(hidden_size, hidden_size) | |
| self.dropout = nn.Dropout(dropout) | |
| def forward(self, hidden_states: torch.Tensor, | |
| attention_mask: torch.Tensor = None) -> torch.Tensor: | |
| B, L, _ = hidden_states.size() | |
| # Linear projections β (B, num_heads, L, head_dim) | |
| Q = self.query(hidden_states).view(B, L, self.num_heads, self.head_dim).transpose(1, 2) | |
| K = self.key(hidden_states).view(B, L, self.num_heads, self.head_dim).transpose(1, 2) | |
| V = self.value(hidden_states).view(B, L, self.num_heads, self.head_dim).transpose(1, 2) | |
| # Scaled dot-product attention | |
| scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.head_dim) | |
| if attention_mask is not None: | |
| # attention_mask: (B, 1, 1, L) β broadcast across heads & queries | |
| scores = scores + attention_mask | |
| attn_weights = F.softmax(scores, dim=-1) | |
| attn_weights = self.dropout(attn_weights) | |
| context = torch.matmul(attn_weights, V) # (B, H, L, d_k) | |
| context = context.transpose(1, 2).contiguous().view(B, L, -1) | |
| return self.out(context) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Transformer Encoder Block (one layer) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TransformerBlock(nn.Module): | |
| """Pre-LN transformer block: Attention β Add & Norm β FFN β Add & Norm.""" | |
| def __init__(self, hidden_size: int = HIDDEN_SIZE, | |
| num_heads: int = NUM_ATTENTION_HEADS, | |
| intermediate_size: int = INTERMEDIATE_SIZE, | |
| dropout: float = HIDDEN_DROPOUT): | |
| super().__init__() | |
| self.attention = MultiHeadSelfAttention(hidden_size, num_heads) | |
| self.norm1 = nn.LayerNorm(hidden_size) | |
| self.ffn = nn.Sequential( | |
| nn.Linear(hidden_size, intermediate_size), | |
| nn.GELU(), | |
| nn.Linear(intermediate_size, hidden_size), | |
| ) | |
| self.norm2 = nn.LayerNorm(hidden_size) | |
| self.dropout = nn.Dropout(dropout) | |
| def forward(self, x: torch.Tensor, attention_mask=None) -> torch.Tensor: | |
| # Self-attention sub-layer | |
| attn_out = self.attention(self.norm1(x), attention_mask) | |
| x = x + self.dropout(attn_out) | |
| # Feed-forward sub-layer | |
| ffn_out = self.ffn(self.norm2(x)) | |
| x = x + self.dropout(ffn_out) | |
| return x | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Data Encoder (Token + Position + Segment embeddings) β Paper Β§3.1 | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class DataEncoder(nn.Module): | |
| """ | |
| Converts token ids, position ids, and segment ids into a continuous | |
| embedding E = E_token + E_position + E_segment, followed by LayerNorm | |
| and dropout β identical to the BERT embedding layer. | |
| """ | |
| def __init__(self, vocab_size: int = 30522, | |
| hidden_size: int = HIDDEN_SIZE, | |
| max_position: int = MAX_SEQ_LEN, | |
| type_vocab_size: int = 2, | |
| dropout: float = HIDDEN_DROPOUT): | |
| super().__init__() | |
| self.word_embeddings = nn.Embedding(vocab_size, hidden_size) | |
| self.position_embeddings = nn.Embedding(max_position, hidden_size) | |
| self.token_type_embeddings = nn.Embedding(type_vocab_size, hidden_size) | |
| self.norm = nn.LayerNorm(hidden_size) | |
| self.dropout = nn.Dropout(dropout) | |
| def forward(self, input_ids: torch.Tensor, | |
| token_type_ids: torch.Tensor = None) -> torch.Tensor: | |
| B, L = input_ids.size() | |
| position_ids = torch.arange(L, device=input_ids.device).unsqueeze(0).expand(B, -1) | |
| if token_type_ids is None: | |
| token_type_ids = torch.zeros_like(input_ids) | |
| embeddings = ( | |
| self.word_embeddings(input_ids) | |
| + self.position_embeddings(position_ids) | |
| + self.token_type_embeddings(token_type_ids) | |
| ) | |
| return self.dropout(self.norm(embeddings)) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TRALSem β Full Model | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TRALSem(nn.Module): | |
| """ | |
| TRALSem: Textual Robust Adversarial Learning for Sentiment Analysis. | |
| Pipeline: | |
| Input β DataEncoder β [Transformer Γ 12] β [CLS] β LinearLayer β Softmax | |
| The Linear Layer produces a regression-style scalar per class (paper Β§3.4), | |
| and Softmax converts to probabilities. RMSE loss is used during training. | |
| """ | |
| def __init__(self, vocab_size: int = 30522, | |
| num_labels: int = 5, | |
| hidden_size: int = HIDDEN_SIZE, | |
| num_layers: int = NUM_HIDDEN_LAYERS, | |
| num_heads: int = NUM_ATTENTION_HEADS, | |
| intermediate_size: int = INTERMEDIATE_SIZE, | |
| dropout: float = HIDDEN_DROPOUT, | |
| max_seq_len: int = MAX_SEQ_LEN): | |
| super().__init__() | |
| self.num_labels = num_labels | |
| # ββ Data Encoder βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| self.encoder = DataEncoder(vocab_size, hidden_size, max_seq_len, | |
| dropout=dropout) | |
| # ββ Transformer Encoder (12 layers) ββββββββββββββββββββββββββββββ | |
| self.transformer_layers = nn.ModuleList([ | |
| TransformerBlock(hidden_size, num_heads, intermediate_size, dropout) | |
| for _ in range(num_layers) | |
| ]) | |
| self.final_norm = nn.LayerNorm(hidden_size) | |
| # ββ Linear Layer (LL) β regression-style head (Paper Β§3.4) ββββββ | |
| self.classifier = nn.Sequential( | |
| nn.Linear(hidden_size, hidden_size), | |
| nn.Tanh(), | |
| nn.Dropout(dropout), | |
| nn.Linear(hidden_size, num_labels), | |
| ) | |
| self._init_weights() | |
| # ββ weight initialisation βββββββββββββββββββββββββββββββββββββββββββββ | |
| def _init_weights(self): | |
| for module in self.modules(): | |
| if isinstance(module, nn.Linear): | |
| nn.init.xavier_uniform_(module.weight) | |
| if module.bias is not None: | |
| nn.init.zeros_(module.bias) | |
| elif isinstance(module, nn.Embedding): | |
| nn.init.normal_(module.weight, mean=0.0, std=0.02) | |
| elif isinstance(module, nn.LayerNorm): | |
| nn.init.ones_(module.weight) | |
| nn.init.zeros_(module.bias) | |
| # ββ load pre-trained BERT weights βββββββββββββββββββββββββββββββββββββ | |
| def load_pretrained_bert(self, bert_model): | |
| """ | |
| Copy weights from a HuggingFace BertModel into our custom layers. | |
| This bridges the gap between our from-scratch architecture and | |
| pretrained BERT for transfer learning. | |
| """ | |
| # Embeddings | |
| self.encoder.word_embeddings.weight.data.copy_( | |
| bert_model.embeddings.word_embeddings.weight.data) | |
| self.encoder.position_embeddings.weight.data.copy_( | |
| bert_model.embeddings.position_embeddings.weight.data[:MAX_SEQ_LEN]) | |
| self.encoder.token_type_embeddings.weight.data.copy_( | |
| bert_model.embeddings.token_type_embeddings.weight.data) | |
| self.encoder.norm.weight.data.copy_( | |
| bert_model.embeddings.LayerNorm.weight.data) | |
| self.encoder.norm.bias.data.copy_( | |
| bert_model.embeddings.LayerNorm.bias.data) | |
| # Transformer layers | |
| for i, layer in enumerate(self.transformer_layers): | |
| bl = bert_model.encoder.layer[i] | |
| # Attention | |
| layer.attention.query.weight.data.copy_(bl.attention.self.query.weight.data) | |
| layer.attention.query.bias.data.copy_(bl.attention.self.query.bias.data) | |
| layer.attention.key.weight.data.copy_(bl.attention.self.key.weight.data) | |
| layer.attention.key.bias.data.copy_(bl.attention.self.key.bias.data) | |
| layer.attention.value.weight.data.copy_(bl.attention.self.value.weight.data) | |
| layer.attention.value.bias.data.copy_(bl.attention.self.value.bias.data) | |
| layer.attention.out.weight.data.copy_(bl.attention.output.dense.weight.data) | |
| layer.attention.out.bias.data.copy_(bl.attention.output.dense.bias.data) | |
| # LayerNorm 1 | |
| layer.norm1.weight.data.copy_(bl.attention.output.LayerNorm.weight.data) | |
| layer.norm1.bias.data.copy_(bl.attention.output.LayerNorm.bias.data) | |
| # FFN | |
| layer.ffn[0].weight.data.copy_(bl.intermediate.dense.weight.data) | |
| layer.ffn[0].bias.data.copy_(bl.intermediate.dense.bias.data) | |
| layer.ffn[2].weight.data.copy_(bl.output.dense.weight.data) | |
| layer.ffn[2].bias.data.copy_(bl.output.dense.bias.data) | |
| # LayerNorm 2 | |
| layer.norm2.weight.data.copy_(bl.output.LayerNorm.weight.data) | |
| layer.norm2.bias.data.copy_(bl.output.LayerNorm.bias.data) | |
| # ββ forward pass ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def forward(self, input_ids: torch.Tensor, | |
| attention_mask: torch.Tensor = None, | |
| token_type_ids: torch.Tensor = None) -> torch.Tensor: | |
| """ | |
| Returns raw logits (before softmax) of shape (B, num_labels). | |
| Softmax is applied externally in loss / inference. | |
| """ | |
| # 1. Data Encoder | |
| hidden = self.encoder(input_ids, token_type_ids) | |
| # 2. Prepare attention mask β (B, 1, 1, L), -10000 for padded tokens | |
| if attention_mask is not None: | |
| extended_mask = attention_mask[:, None, None, :] | |
| extended_mask = (1.0 - extended_mask) * -1e4 | |
| else: | |
| extended_mask = None | |
| # 3. Transformer Encoder (12 layers) | |
| for layer in self.transformer_layers: | |
| hidden = layer(hidden, extended_mask) | |
| hidden = self.final_norm(hidden) | |
| # 4. [CLS] token pooling | |
| cls_output = hidden[:, 0] | |
| # 5. Linear Layer β logits | |
| logits = self.classifier(cls_output) | |
| return logits | |
| # ββ convenience: softmax probabilities ββββββββββββββββββββββββββββββββ | |
| def predict_proba(self, input_ids, attention_mask=None, | |
| token_type_ids=None) -> torch.Tensor: | |
| logits = self.forward(input_ids, attention_mask, token_type_ids) | |
| return F.softmax(logits, dim=-1) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # RMSE Loss β Paper Β§3.5 | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class RMSELoss(nn.Module): | |
| """ | |
| Root Mean Squared Error loss operating on softmax probabilities. | |
| Converts labels to one-hot and computes RMSE between predicted | |
| probability vector and target vector. | |
| """ | |
| def __init__(self, num_classes: int): | |
| super().__init__() | |
| self.num_classes = num_classes | |
| def forward(self, logits: torch.Tensor, targets: torch.Tensor) -> torch.Tensor: | |
| probs = F.softmax(logits, dim=-1) | |
| one_hot = F.one_hot(targets, num_classes=self.num_classes).float() | |
| mse = torch.mean((probs - one_hot) ** 2) | |
| return torch.sqrt(mse + 1e-8) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Forward-function helper for adversarial modules | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def model_forward(model, input_ids, attention_mask, token_type_ids): | |
| """Plain forward β used as a callable by FreeLB / YOPO.""" | |
| return model(input_ids, attention_mask, token_type_ids) | |