Spaces:
Running
Running
Download backend/src/pooling.py from VivDubs/eumora-api: direct link, hf CLI and curl.
- Browser
- Download file 5.93 kB
-
https://huggingface.co/spaces/VivDubs/eumora-api/resolve/main/backend/src/pooling.py
- Command line
-
hf download hf://spaces/VivDubs/eumora-api/backend/src/pooling.py
-
curl -L -o pooling.py https://huggingface.co/spaces/VivDubs/eumora-api/resolve/main/backend/src/pooling.py
5.93 kB
| """Optional pooling heads for the research/ablation track. | |
| DEPLOYMENT NOTE | |
| --------------- | |
| The production model MUST remain a stock `DebertaV2ForSequenceClassification` | |
| so the HF Inference API can serve it through the standard `text-classification` | |
| pipeline (no `trust_remote_code` on the Inference API). Everything in this file | |
| is for the ablation table in the paper, not for the deployed checkpoint. | |
| Motivation: DeBERTa's default `ContextPooler` reads only the [CLS] token. That is | |
| reasonable for a tweet, where the emotional payload is one clause. A verse of | |
| lyrics spreads the signal across the whole passage — "and at once I knew / I was | |
| not magnificent" carries its weight in the second line, not the first token. Mean | |
| and attention pooling recover that. | |
| Use `--pooling attention` in train_v2.py to run the ablation. | |
| """ | |
| from __future__ import annotations | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| from transformers import AutoConfig, AutoModel | |
| from transformers.modeling_outputs import SequenceClassifierOutput | |
| class AttentionPooling(nn.Module): | |
| """Learned additive attention over token hidden states. | |
| Computes a scalar score per token, softmaxes over the (masked) sequence, and | |
| returns the weighted sum. Unlike [CLS] pooling this lets the model learn that | |
| e.g. "hollow", "still", "again" carry the emotional load in a lyric line. | |
| """ | |
| def __init__(self, hidden_size: int, dropout: float = 0.1): | |
| super().__init__() | |
| self.attn = nn.Sequential( | |
| nn.Linear(hidden_size, hidden_size), | |
| nn.Tanh(), | |
| nn.Dropout(dropout), | |
| nn.Linear(hidden_size, 1), | |
| ) | |
| def forward(self, hidden: torch.Tensor, mask: torch.Tensor) -> torch.Tensor: | |
| # hidden: (B, T, H), mask: (B, T) | |
| scores = self.attn(hidden).squeeze(-1) # (B, T) | |
| scores = scores.masked_fill(mask == 0, torch.finfo(scores.dtype).min) | |
| weights = F.softmax(scores, dim=-1).unsqueeze(-1) # (B, T, 1) | |
| return (hidden * weights).sum(dim=1) # (B, H) | |
| class MeanMaxAttentionPool(nn.Module): | |
| """Concatenation of masked-mean, masked-max, and attention pooling. | |
| The three views are complementary: mean captures overall tone, max captures | |
| the single most charged token, attention learns a task-specific weighting. | |
| Output dimension is 3 * hidden_size. | |
| """ | |
| def __init__(self, hidden_size: int, dropout: float = 0.1): | |
| super().__init__() | |
| self.attention = AttentionPooling(hidden_size, dropout) | |
| self.output_size = hidden_size * 3 | |
| def forward(self, hidden: torch.Tensor, mask: torch.Tensor) -> torch.Tensor: | |
| mask_f = mask.unsqueeze(-1).float() | |
| summed = (hidden * mask_f).sum(dim=1) | |
| counts = mask_f.sum(dim=1).clamp(min=1e-9) | |
| mean_pooled = summed / counts | |
| max_pooled = hidden.masked_fill(mask.unsqueeze(-1) == 0, torch.finfo(hidden.dtype).min) | |
| max_pooled = max_pooled.max(dim=1).values | |
| attn_pooled = self.attention(hidden, mask) | |
| return torch.cat([mean_pooled, max_pooled, attn_pooled], dim=-1) | |
| class MultiSampleDropoutHead(nn.Module): | |
| """Classifier that averages logits over several independent dropout masks. | |
| Cheap variance reduction: one forward pass through the backbone, N cheap | |
| passes through the linear head. Converges faster and generalizes better than | |
| a single-mask head, which matters when the tail classes have few examples. | |
| Reference: Inoue 2019, "Multi-Sample Dropout for Accelerated Training and | |
| Better Generalization" (arXiv:1905.09788) | |
| """ | |
| def __init__(self, input_size: int, num_labels: int, num_samples: int = 5, p: float = 0.3): | |
| super().__init__() | |
| self.dropouts = nn.ModuleList([nn.Dropout(p) for _ in range(num_samples)]) | |
| self.classifier = nn.Linear(input_size, num_labels) | |
| def forward(self, features: torch.Tensor) -> torch.Tensor: | |
| return torch.stack( | |
| [self.classifier(d(features)) for d in self.dropouts], dim=0 | |
| ).mean(dim=0) | |
| class EumoraClassifier(nn.Module): | |
| """DeBERTa backbone + rich pooling + multi-sample dropout head. | |
| Research-track model. Same backbone weights as the deployed checkpoint, so a | |
| TAPT-adapted encoder can be dropped into either path for a clean ablation. | |
| """ | |
| def __init__( | |
| self, | |
| model_name: str, | |
| num_labels: int, | |
| pooling: str = "attention", | |
| dropout: float = 0.3, | |
| num_dropout_samples: int = 5, | |
| ): | |
| super().__init__() | |
| self.config = AutoConfig.from_pretrained(model_name, num_labels=num_labels) | |
| self.backbone = AutoModel.from_pretrained(model_name, config=self.config) | |
| hidden = self.config.hidden_size | |
| self.pooling_type = pooling | |
| if pooling == "attention": | |
| self.pool = MeanMaxAttentionPool(hidden, dropout=0.1) | |
| head_input = self.pool.output_size | |
| elif pooling == "mean": | |
| self.pool = None | |
| head_input = hidden | |
| else: # "cls" | |
| self.pool = None | |
| head_input = hidden | |
| self.head = MultiSampleDropoutHead( | |
| head_input, num_labels, num_samples=num_dropout_samples, p=dropout | |
| ) | |
| def forward(self, input_ids=None, attention_mask=None, labels=None, **kwargs): | |
| hidden = self.backbone( | |
| input_ids=input_ids, attention_mask=attention_mask | |
| ).last_hidden_state | |
| if self.pooling_type == "attention": | |
| features = self.pool(hidden, attention_mask) | |
| elif self.pooling_type == "mean": | |
| mask_f = attention_mask.unsqueeze(-1).float() | |
| features = (hidden * mask_f).sum(dim=1) / mask_f.sum(dim=1).clamp(min=1e-9) | |
| else: | |
| features = hidden[:, 0] | |
| logits = self.head(features) | |
| return SequenceClassifierOutput(logits=logits) | |