""" ============================================================ Multi-Model Classifier - Ensemble Approach ============================================================ استخدام 4 نماذج (Primary, Secondary, Tertiary الجاهز، و Emotion). """ import os import re import torch import torch.nn as nn import torch.nn.functional as F from torch.utils.data import Dataset from transformers import AutoTokenizer, AutoModel, pipeline import numpy as np from typing import List, Dict from huggingface_hub import hf_hub_download class MentalHealthDataset(Dataset): def __init__(self, texts, labels, tokenizer, max_len=128): self.texts = texts self.labels = labels self.tokenizer = tokenizer self.max_len = max_len def __len__(self): return len(self.texts) def __getitem__(self, idx): encoding = self.tokenizer( str(self.texts[idx]), max_length=self.max_len, padding='max_length', truncation=True, return_tensors='pt' ) return { 'input_ids': encoding['input_ids'].squeeze(), 'attention_mask': encoding['attention_mask'].squeeze(), 'labels': torch.tensor(self.labels[idx], dtype=torch.long) } class MentalHealthClassifier(nn.Module): def __init__(self, model_name, n_classes, dropout=0.3): super().__init__() self.model_name = model_name try: self.bert = AutoModel.from_pretrained(model_name) except Exception as e: print(f"⚠ Failed loading {model_name}: {e}. Falling back to distilbert-base-uncased") self.bert = AutoModel.from_pretrained("distilbert-base-uncased") self.model_name = "distilbert-base-uncased" self.drop = nn.Dropout(dropout) self.fc = nn.Linear(self.bert.config.hidden_size, n_classes) def forward(self, input_ids, attention_mask): outputs = self.bert(input_ids=input_ids, attention_mask=attention_mask) pooled = outputs.last_hidden_state[:, 0, :] dropped = self.drop(pooled) return self.fc(dropped) class FocalLoss(nn.Module): def __init__(self, alpha=0.25, gamma=2.0, class_weights=None): super().__init__() self.alpha = alpha self.gamma = gamma self.class_weights = class_weights def forward(self, inputs, targets): ce_loss = F.cross_entropy(inputs, targets, weight=self.class_weights, reduction='none') pt = torch.exp(-ce_loss) focal_loss = self.alpha * (1 - pt) ** self.gamma * ce_loss return focal_loss.mean() class EnsembleDiagnoser: EMOTION_TO_LABEL_BIAS = { 'sadness': {'Depression': 0.4, 'Suicide': 0.2}, 'fear': {'Anxiety/Stress': 0.4, 'PTSD': 0.3}, 'anger': {'Bipolar': 0.2, 'PTSD': 0.2}, 'disgust': {'Depression': 0.2}, 'joy': {'Normal': 0.2, 'Bipolar': 0.4}, 'surprise': {'Normal': 0.1, 'Bipolar': 0.2}, 'neutral': {'Normal': 0.3}, } def __init__(self, models_config: dict, labels: List[str], device=None): self.device = device or torch.device('cuda' if torch.cuda.is_available() else 'cpu') self.labels = labels self.label2idx = {lbl: i for i, lbl in enumerate(labels)} self.idx2label = {i: lbl for i, lbl in enumerate(labels)} self.models_config = models_config self.primary_model = None self.primary_tokenizer = None self.secondary_model = None self.secondary_tokenizer = None self.emotion_pipe = None self.tertiary_pipe = None def load_pretrained(self, primary_path: str = None, secondary_path: str = None): REPO_ID = "Ziad9022/Mentallico-Weights" # ====== Primary Model ====== primary_cfg = self.models_config['primary'] if primary_path: if not os.path.exists(primary_path): filename = os.path.basename(primary_path) print(f"📥 Downloading primary weights '{filename}' from Hub...") primary_path = hf_hub_download(repo_id=REPO_ID, filename=filename) ckpt = torch.load(primary_path, map_location=self.device, weights_only=False) model_name = ckpt.get('model_name', primary_cfg['name']) self.primary_model = MentalHealthClassifier(model_name, len(self.labels)) self.primary_model.load_state_dict(ckpt['model_state_dict']) self.primary_model.to(self.device).eval() self.primary_tokenizer = AutoTokenizer.from_pretrained(model_name) print(f"✓ Loaded primary model from Hub cache: {primary_path}") # ====== Secondary Model ====== secondary_cfg = self.models_config['secondary'] if secondary_path: if not os.path.exists(secondary_path): filename = os.path.basename(secondary_path) print(f"📥 Downloading secondary weights '{filename}' from Hub...") secondary_path = hf_hub_download(repo_id=REPO_ID, filename=filename) ckpt = torch.load(secondary_path, map_location=self.device, weights_only=False) model_name = ckpt.get('model_name', secondary_cfg['name']) self.secondary_model = MentalHealthClassifier(model_name, len(self.labels)) self.secondary_model.load_state_dict(ckpt['model_state_dict']) self.secondary_model.to(self.device).eval() self.secondary_tokenizer = AutoTokenizer.from_pretrained(model_name) print(f"✓ Loaded secondary model from Hub cache: {secondary_path}") device_id = 0 if self.device.type == 'cuda' else -1 # Emotion Pipeline try: emotion_name = self.models_config['emotion']['name'] self.emotion_pipe = pipeline("text-classification", model=emotion_name, top_k=None, device=device_id) print(f"✓ Loaded emotion pipeline: {emotion_name}") except Exception as e: print(f"⚠ Could not load emotion pipeline: {e}") self.emotion_pipe = None # Tertiary Pipeline try: tertiary_name = self.models_config['tertiary']['name'] self.tertiary_pipe = pipeline("text-classification", model=tertiary_name, top_k=None, device=device_id) print(f"✓ Loaded tertiary pipeline: {tertiary_name}") except Exception as e: print(f"⚠ Could not load tertiary pipeline: {e}") self.tertiary_pipe = None @torch.no_grad() def _predict_single(self, model, tokenizer, text: str, max_len=128): if model is None: return None encoding = tokenizer(str(text), max_length=max_len, padding='max_length', truncation=True, return_tensors='pt') input_ids = encoding['input_ids'].to(self.device) attention_mask = encoding['attention_mask'].to(self.device) logits = model(input_ids, attention_mask) probs = F.softmax(logits, dim=1).cpu().numpy()[0] return probs def _emotion_signal(self, text: str) -> np.ndarray: if self.emotion_pipe is None: return np.zeros(len(self.labels)) try: results = self.emotion_pipe(text[:512]) if isinstance(results, list) and len(results) > 0: if isinstance(results[0], list): results = results[0] bias = np.zeros(len(self.labels)) for r in results: emotion, score = r['label'].lower(), r['score'] if emotion in self.EMOTION_TO_LABEL_BIAS: for lbl, weight in self.EMOTION_TO_LABEL_BIAS[emotion].items(): if lbl in self.label2idx: bias[self.label2idx[lbl]] += score * weight if bias.sum() > 0: bias = bias / bias.sum() return bias except: return np.zeros(len(self.labels)) def _tertiary_signal(self, text: str) -> np.ndarray: if self.tertiary_pipe is None: return np.zeros(len(self.labels)) try: results = self.tertiary_pipe(text[:512]) if isinstance(results, list) and len(results) > 0: if isinstance(results[0], list): results = results[0] bias = np.zeros(len(self.labels)) mapping = { 'depression': 'Depression', 'anxiety': 'Anxiety/Stress', 'stress': 'Anxiety/Stress', 'bipolar': 'Bipolar', 'ptsd': 'PTSD', 'suicide': 'Suicide', 'normal': 'Normal' } for r in results: lbl, score = r['label'].lower(), r['score'] for key, mapped_lbl in mapping.items(): if key in lbl and mapped_lbl in self.label2idx: bias[self.label2idx[mapped_lbl]] += score if bias.sum() > 0: bias = bias / bias.sum() return bias except: return np.zeros(len(self.labels)) def predict(self, text: str, return_details=False) -> dict: individual_probs = {} individual_probs['primary'] = self._predict_single(self.primary_model, self.primary_tokenizer, text) if self.primary_model else None individual_probs['secondary'] = self._predict_single(self.secondary_model, self.secondary_tokenizer, text) if self.secondary_model else None individual_probs['tertiary'] = self._tertiary_signal(text) individual_probs['emotion_bias'] = self._emotion_signal(text) final_probs = np.zeros(len(self.labels)) total_weight = 0.0 if individual_probs['primary'] is not None: w = self.models_config['primary']['weight'] final_probs += w * individual_probs['primary'] total_weight += w if individual_probs['secondary'] is not None: w = self.models_config['secondary']['weight'] final_probs += w * individual_probs['secondary'] total_weight += w if individual_probs['tertiary'].sum() > 0: w = self.models_config['tertiary']['weight'] final_probs += w * individual_probs['tertiary'] total_weight += w if individual_probs['emotion_bias'].sum() > 0: w = self.models_config['emotion']['weight'] final_probs += w * individual_probs['emotion_bias'] total_weight += w if total_weight > 0: final_probs = final_probs / total_weight if final_probs.sum() == 0: return {'label': 'Normal', 'confidence': 0.0, 'probabilities': dict(zip(self.labels, [1.0/len(self.labels)]*len(self.labels))), 'error': 'No models loaded'} # ======================================================== # 💉 CLINICAL HEURISTIC BOOSTING (To fix Class Imbalance) # ======================================================== text_lower = text.lower() # 1. PTSD Booster ptsd_anchors = ['flashback', 'nightmare', 'accident', 'trauma', 'panic attack'] if any(word in text_lower for word in ptsd_anchors): if 'PTSD' in self.label2idx: final_probs[self.label2idx['PTSD']] *= 5 # 2. Bipolar Booster bipolar_anchors = ['superpower', 'endless energy', 'racing', 'billionaire', "haven't slept"] if any(word in text_lower for word in bipolar_anchors): if 'Bipolar' in self.label2idx: final_probs[self.label2idx['Bipolar']] *= 3.5 # إعادة الحساب بعد التضخيم if final_probs.sum() > 0: final_probs = final_probs / final_probs.sum() pred_idx = int(np.argmax(final_probs)) pred_label = self.idx2label[pred_idx] confidence = float(final_probs[pred_idx]) probabilities_dict = dict(zip(self.labels, final_probs.tolist())) # ======================================================== # ======================================================== # 🚨 START OF EMERGENCY SAFETY OVERRIDE (SUICIDE PROTOCOL) # ======================================================== suicide_keywords = [ r'\bsuicide\b', r'\bkill myself\b', r'\bend my life\b', r'\bdie\b', r'\bno hope\b', 'انتحر', 'اموت نفسي', 'انهي حياتي', 'اقتل نفسي' ] has_suicide_keywords = any(re.search(word, text_lower) for word in suicide_keywords) suicide_prob = probabilities_dict.get('Suicide', 0.0) is_suicide_probable = suicide_prob > 0.35 if has_suicide_keywords or is_suicide_probable: pred_label = 'Suicide' confidence = max(suicide_prob, 0.99) probabilities_dict['Suicide'] = confidence # ======================================================== # 🚨 END OF EMERGENCY SAFETY OVERRIDE # ======================================================== result = { 'label': pred_label, 'confidence': confidence, 'probabilities': probabilities_dict, } if return_details: result['details'] = {k: (v.tolist() if isinstance(v, np.ndarray) else v) for k, v in individual_probs.items()} return result