CoolFace
Apppublic

milanchndr/Toxic-Comment-Classifier-Explainer

sourceHugging Faceupdated 10mo agoView on Hugging Face
0likes
app.py203 linesDownload Raw Back to root
1from fastapi import FastAPI2from pydantic import BaseModel3from transformers import AutoTokenizer, AutoModelForSequenceClassification4import torch5import numpy as np6import re7 8# ----------------------------------------------------------------------9# CONFIG10# ----------------------------------------------------------------------11MODEL_ID = "milanchndr/toxicity-classifier-mdeberta" 12LABELS = ["toxic", "severe_toxic", "obscene", "threat", "insult", "identity_hate"]13MAX_LEN = 25614 15device = torch.device("cuda" if torch.cuda.is_available() else "cpu")16 17# ----------------------------------------------------------------------18# LOAD MODEL19# ----------------------------------------------------------------------20tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)21model = AutoModelForSequenceClassification.from_pretrained(22    MODEL_ID, output_attentions=True23).to(device)24model.eval()25 26# ----------------------------------------------------------------------27# INPUT BODY28# ----------------------------------------------------------------------29class TextInput(BaseModel):30    text: str31 32# ----------------------------------------------------------------------33# FASTAPI APP34# ----------------------------------------------------------------------35app = FastAPI(title="Toxicity Classification API")36 37# ----------------------------------------------------------------------38# UTILS39# ----------------------------------------------------------------------40def clean_text(text):41    """Basic text normalization."""42    text = text.lower()43    text = re.sub(r"http\S+|www\.\S+", " ", text)44    text = re.sub(r"\s+", " ", text).strip()45    return text46 47def clean_token_text(token: str) -> str:48    """49    Removes tokenizer artifacts (like Ġ or leading underscores)50    to produce clean, readable text for the UI.51    """52    # Remove RoBERTa/DeBERTa specific 'Ġ' char53    t = token.replace("Ġ", "")54    # Remove standard whitespace or underscores55    t = t.strip().lstrip("_")56    return t57 58# ----------------------------------------------------------------------59# WORD IMPORTANCE (Leave-one-out)60# ----------------------------------------------------------------------61def compute_word_importance(words, base_pred, label_idx):62    """63    Computes importance by masking one word at a time.64    """65    scores = []66    67    # Limit computation to prevent timeouts on very long texts68    # If text is huge, you might want to limit 'words' to first 10069    70    for i in range(len(words)):71        # Construct text without the i-th word72        perturbed = words[:i] + words[i+1:]73        perturbed_text = " ".join(perturbed)74 75        inputs = tokenizer(76            perturbed_text,77            truncation=True,78            padding="max_length",79            max_length=MAX_LEN,80            return_tensors="pt"81        ).to(device)82 83        with torch.no_grad():84            out = torch.sigmoid(model(**inputs).logits)85            perturbed_pred = out[0, label_idx].item()86 87        # Importance = Drop in confidence when word is removed88        # (Positive score = word contributed to the label)89        scores.append(base_pred - perturbed_pred)90 91    return scores92 93# ----------------------------------------------------------------------94# MAIN ENDPOINT95# ----------------------------------------------------------------------96@app.post("/predict")97async def predict(input_data: TextInput):98    # 1. Prepare Input99    raw_text = input_data.text100    text = clean_text(raw_text)101 102    inputs = tokenizer(103        text,104        truncation=True,105        padding="max_length",106        max_length=MAX_LEN,107        return_tensors="pt",108        return_attention_mask=True109    ).to(device)110 111    # 2. Model Inference112    with torch.no_grad():113        outputs = model(**inputs)114        logits = outputs.logits115        probs = torch.sigmoid(logits)[0].cpu().numpy()116        117        # Extract Attention (Last Layer, Average Heads)118        attentions = outputs.attentions[-1] 119        attn = attentions[0].mean(dim=0).cpu().numpy()120 121    # 3. Process Logic & Labels122    label_idx = int(np.argmax(probs))123    label_name = LABELS[label_idx]124    base_pred = float(probs[label_idx])125    126    # Determine Final Label (highest prob)127    final_label = label_name if base_pred > 0.5 else "neutral"128 129    # 4. Process Tokens & Attention Matrix130    # Get valid length (ignore padding)131    valid_len = int(inputs["attention_mask"][0].sum().item())132    133    # Raw tokens (e.g. ['[CLS]', 'Ġyou', 'Ġare', ...])134    raw_tokens = tokenizer.convert_ids_to_tokens(inputs["input_ids"][0])[:valid_len]135    136    # Clean tokens for UI (e.g. ['[CLS]', 'you', 'are', ...])137    clean_tokens = [clean_token_text(t) for t in raw_tokens]138    139    # Slice matrix to NxN valid size140    attn_matrix = attn[:valid_len, :valid_len]141 142    # 5. Calculate Top-K Attention Links (Optimization B)143    # Instead of sending full matrix for rendering, send sparse top connections144    attention_links = {}145    for i, token in enumerate(clean_tokens):146        # Skip special tokens for clarity if desired, or keep them147        if token in ["[CLS]", "[SEP]"]: 148            continue149            150        row = attn_matrix[i]151        # Get top 3 indices (excluding self-attention if desired, but keeping here)152        top_indices = row.argsort()[-4:][::-1] 153        154        links = []155        for idx in top_indices:156            # Filter low attention noise157            score = float(row[idx])158            if score > 0.05: 159                links.append([clean_tokens[idx], score])160        161        attention_links[token] = links162 163    # 6. Calculate Word Importance164    # Note: We use space-split words for Leave-One-Out as per original logic165    words = text.split()166    importance_scores = compute_word_importance(words, base_pred, label_idx)167    168    # 7. Calculate Top Contributors (Optimization A)169    # Combine words and scores, sort by score descending170    contributors = [171        {"token": w, "score": s} 172        for w, s in zip(words, importance_scores)173    ]174    # Sort by absolute impact or positive impact? usually positive for "toxicity"175    top_contributors = sorted(contributors, key=lambda x: x["score"], reverse=True)[:5]176 177    # 8. Construct Response178    return {179        "input_text": raw_text,180        "final_label": final_label,181        182        "probabilities": {label: float(p) for label, p in zip(LABELS, probs)},183        184        # Clean tokens for the main display185        "tokens": clean_tokens, 186 187        "attention": {188            "tokens": clean_tokens,189            # Full matrix (good for heatmaps)190            "matrix": [[float(v) for v in row] for row in attn_matrix],191            # Sparse links (good for arc diagrams / fast UI)192            "links": attention_links193        },194 195        "word_importance": {196            "label": str(label_name),197            "tokens": words, # The exact words used for the importance calc198            "importance_scores": [float(v) for v in importance_scores],199            "base_value": float(base_pred - sum(importance_scores)),200            "prediction": float(base_pred),201            "top_contributors": top_contributors202        }203    }