Download emotion_classifier_model.py from RustyLinux/MiaPredict: direct link, hf CLI and curl.
- Browser
- Download file 8 kB
-
https://huggingface.co/RustyLinux/MiaPredict/resolve/fed84d1683858ee761beaa7e133492e0dca20e8d/emotion_classifier_model.py
- Command line
-
hf download hf://RustyLinux/MiaPredict@fed84d1683858ee761beaa7e133492e0dca20e8d/emotion_classifier_model.py
-
curl -L -o emotion_classifier_model.py https://huggingface.co/RustyLinux/MiaPredict/resolve/fed84d1683858ee761beaa7e133492e0dca20e8d/emotion_classifier_model.py
8 kB
| """ | |
| === MIA · Clasificador de Emociones (Pretrained Encoder + MLP) === | |
| - Mantiene compatibilidad con tu API pública. | |
| - Permite usar tu TextEmbedder aleatorio (emb_dim) o un encoder preentrenado (BETO) con 768D. | |
| - Expone freeze/unfreeze para controlar el fine-tuning desde el trainer. | |
| """ | |
| import torch | |
| import torch.nn as nn | |
| from typing import List, Optional | |
| from transformers import AutoTokenizer, AutoModel | |
| # ==================== MÓDULO 1A: TextEmbedder (embedding aleatorio) ==================== | |
| class TextEmbedder(nn.Module): | |
| """ | |
| Módulo de Embedding simple: | |
| - Usa el tokenizador de BETO para sub-palabras (por conveniencia, vocab, pad_id, etc.) | |
| - La representación es un embedding aleatorio + mean pooling (no contextual). | |
| """ | |
| def __init__( | |
| self, | |
| model_name: str = "dccuchile/bert-base-spanish-wwm-cased", | |
| emb_dim: int = 300, | |
| max_length: int = 128, | |
| device: Optional[torch.device] = None | |
| ): | |
| super().__init__() | |
| self.tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=True) | |
| self.vocab_size = self.tokenizer.vocab_size | |
| self.pad_id = self.tokenizer.pad_token_id | |
| self.cls_id = self.tokenizer.cls_token_id | |
| self.sep_id = self.tokenizer.sep_token_id | |
| self.max_length = max_length | |
| # Capa de embedding | |
| self.embedding = nn.Embedding(self.vocab_size, emb_dim, padding_idx=self.pad_id) | |
| nn.init.xavier_uniform_(self.embedding.weight) | |
| with torch.no_grad(): | |
| if self.pad_id is not None: | |
| self.embedding.weight[self.pad_id].zero_() | |
| # Regularización opcional (ayuda contra sobreajuste) | |
| self.emb_dropout = nn.Dropout(p=0.1) | |
| self.device = device or torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| self.to(self.device) | |
| def embed_batch(self, texts: List[str]) -> torch.Tensor: | |
| batch = self.tokenizer( | |
| texts, padding=True, truncation=True, max_length=self.max_length, return_tensors="pt" | |
| ) | |
| input_ids = batch["input_ids"].to(self.device) # [B, T] | |
| attention_mask = batch["attention_mask"].to(self.device) # [B, T] | |
| embeds = self.embedding(input_ids) # [B, T, E] | |
| if self.training: | |
| embeds = self.emb_dropout(embeds) | |
| mask = attention_mask.bool() # [B, T] | |
| if self.cls_id is not None: | |
| mask = mask & (input_ids != self.cls_id) | |
| if self.sep_id is not None: | |
| mask = mask & (input_ids != self.sep_id) | |
| mask_f = mask.unsqueeze(-1).float() # [B, T, 1] | |
| summed = (embeds * mask_f).sum(dim=1) # [B, E] | |
| counts = mask_f.sum(dim=1).clamp(min=1.0) # [B, 1] | |
| sentence_vecs = summed / counts # [B, E] | |
| return sentence_vecs | |
| def embed_sentence(self, text: str) -> torch.Tensor: | |
| return self.embed_batch([text])[0] | |
| # ==================== MÓDULO 1B: BETOEmbedder (encoder preentrenado) ==================== | |
| class BETOEmbedder(nn.Module): | |
| """ | |
| Usa el encoder de BETO (BERT en español) para obtener embeddings contextuales. | |
| Mean pooling sobre last_hidden_state. | |
| Salida: [B, 768] | |
| """ | |
| def __init__( | |
| self, | |
| model_name: str = "dccuchile/bert-base-spanish-wwm-cased", | |
| max_length: int = 128, | |
| device: Optional[torch.device] = None | |
| ): | |
| super().__init__() | |
| self.device = device or torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| self.tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=True) | |
| self.encoder = AutoModel.from_pretrained(model_name) | |
| self.max_length = max_length | |
| self.encoder.to(self.device) | |
| def embed_batch(self, texts: List[str]) -> torch.Tensor: | |
| inputs = self.tokenizer( | |
| texts, padding=True, truncation=True, max_length=self.max_length, return_tensors="pt" | |
| ).to(self.device) | |
| outputs = self.encoder(**inputs) # last_hidden_state [B, T, 768] | |
| last_hidden = outputs.last_hidden_state | |
| mask = inputs["attention_mask"].unsqueeze(-1).float() # [B, T, 1] | |
| pooled = (last_hidden * mask).sum(dim=1) / mask.sum(dim=1).clamp(min=1e-9) # [B, 768] | |
| return pooled | |
| # ==================== MÓDULO 2: MLP Classifier ==================== | |
| class MLPClassifier(nn.Module): | |
| """ | |
| Feedforward para clasificación de emociones: | |
| Input → 128 → 64 → 6 (logits) | |
| """ | |
| def __init__( | |
| self, | |
| input_dim: int = 300, | |
| hidden1: int = 128, | |
| hidden2: int = 64, | |
| num_classes: int = 6, | |
| dropout: float = 0.3 | |
| ): | |
| super().__init__() | |
| self.fc1 = nn.Linear(input_dim, hidden1) | |
| self.relu1 = nn.ReLU() | |
| self.dropout1 = nn.Dropout(dropout) | |
| self.fc2 = nn.Linear(hidden1, hidden2) | |
| self.relu2 = nn.ReLU() | |
| self.dropout2 = nn.Dropout(dropout) | |
| self.fc3 = nn.Linear(hidden2, num_classes) | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| x = self.fc1(x); x = self.relu1(x); x = self.dropout1(x) | |
| x = self.fc2(x); x = self.relu2(x); x = self.dropout2(x) | |
| x = self.fc3(x) | |
| return x | |
| # ==================== MÓDULO 3: Modelo Completo ==================== | |
| class EmotionClassifier(nn.Module): | |
| """ | |
| Integra embedder (aleatorio o BETO) + MLP. | |
| - `pretrained_encoder=None` → usa TextEmbedder (emb_dim configurable) | |
| - `pretrained_encoder="beto"` → usa BETOEmbedder (salida 768D) | |
| """ | |
| def __init__( | |
| self, | |
| model_name: str = "dccuchile/bert-base-spanish-wwm-cased", | |
| emb_dim: int = 300, | |
| max_length: int = 128, | |
| hidden1: int = 128, | |
| hidden2: int = 64, | |
| num_classes: int = 6, | |
| dropout: float = 0.3, | |
| device: Optional[torch.device] = None, | |
| pretrained_encoder: Optional[str] = None | |
| ): | |
| super().__init__() | |
| self.device = device or torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| if pretrained_encoder == "beto": | |
| self.embedder = BETOEmbedder(model_name=model_name, max_length=max_length, device=self.device) | |
| embed_dim = 768 | |
| else: | |
| self.embedder = TextEmbedder(model_name=model_name, emb_dim=emb_dim, max_length=max_length, device=self.device) | |
| embed_dim = emb_dim | |
| self.classifier = MLPClassifier( | |
| input_dim=embed_dim, hidden1=hidden1, hidden2=hidden2, num_classes=num_classes, dropout=dropout | |
| ) | |
| self.label_map = {0: "tristeza", 1: "alegría", 2: "amor", 3: "ira", 4: "miedo", 5: "sorpresa"} | |
| self.to(self.device) | |
| # ---------- Forward & Utils ---------- | |
| def forward(self, texts: List[str]) -> torch.Tensor: | |
| embeddings = self.embedder.embed_batch(texts) # [B, D] | |
| logits = self.classifier(embeddings) # [B, C] | |
| return logits | |
| def predict(self, texts: List[str], return_probs: bool = False): | |
| self.eval() | |
| with torch.no_grad(): | |
| logits = self.forward(texts) | |
| probs = torch.softmax(logits, dim=-1) | |
| predictions = torch.argmax(probs, dim=-1) | |
| emotions = [self.label_map[p.item()] for p in predictions] | |
| if return_probs: | |
| return emotions, probs.cpu().numpy() | |
| return emotions | |
| def predict_single(self, text: str, return_probs: bool = False): | |
| out = self.predict([text], return_probs=return_probs) | |
| if return_probs: | |
| emotions, probs = out | |
| return emotions[0], probs[0] | |
| return out[0] | |
| # ---------- Fine-tuning helpers ---------- | |
| def freeze_encoder(self): | |
| for p in self.embedder.parameters(): | |
| p.requires_grad = False | |
| def unfreeze_encoder(self): | |
| for p in self.embedder.parameters(): | |
| p.requires_grad = True |