Spaces:
Build error
Build error
File size: 8,763 Bytes
801a2fb 0b38cf3 801a2fb 0b38cf3 801a2fb da5fc4e 801a2fb da5fc4e 801a2fb 0b38cf3 801a2fb 0b38cf3 801a2fb 0b38cf3 801a2fb 0b38cf3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | """
Defense demo: Multi-Layered Sarcasm-Aware Harmful Content Detection.
One codebase, two deployments:
- HF Spaces (16GB): live XLM-R sarcasm + live baseline + live Mechanism B
- Local laptop (4GB): live XLM-R sarcasm + curated showcase (Qwen models skipped)
The app auto-detects what it can load. Set env vars to control:
LOAD_QWEN=1 attempt to load the two Qwen classifiers (default on Spaces)
SARCASM_MODEL=... HF repo or local dir for the XLM-R sarcasm detector
BASELINE_ADAPTER=... / MECHB_ADAPTER=... HF repos or local dirs for adapters
Run: python app.py
"""
import os
import json
import torch
import spaces
import gradio as gr
import numpy as np
# ----------------------------- configuration ------------------------------ #
SARCASM_MODEL = os.environ.get("SARCASM_MODEL", "mlaskri/sarcasm-xlmr-hyperparams-only")
BASELINE_ADAPTER = os.environ.get("BASELINE_ADAPTER", "mlaskri/baseline-combined-seed42")
MECHB_ADAPTER = os.environ.get("MECHB_ADAPTER", "mlaskri/harmful-content-mechanism-b-v2")
QWEN_BASE = "Qwen/Qwen2.5-3B-Instruct"
LOAD_QWEN = os.environ.get("LOAD_QWEN", "1") == "1"
SARCASM_TEMPERATURE = 1.953 # validation-fit temperature (thesis Table 4.5)
SARCASM_THRESHOLD = 0.48 # tuned decision threshold
BASELINE_THRESHOLD = 0.35 # validation-selected (thesis Table 5.1)
MECHB_THRESHOLD = 0.60 # validation-selected (thesis Table 5.4)
SHOWCASE_FILE = "curated_examples.json"
# ----------------------------- model loading ------------------------------ #
print("Loading sarcasm detector (XLM-R)...")
from transformers import AutoTokenizer, AutoModelForSequenceClassification
sar_tok = AutoTokenizer.from_pretrained(SARCASM_MODEL)
sar_model = AutoModelForSequenceClassification.from_pretrained(SARCASM_MODEL)
sar_model.eval() # small; runs on CPU inside the GPU-decorated fn or CPU fallback
print("Sarcasm detector ready.")
qwen_models = {}
if LOAD_QWEN:
try:
from peft import PeftModel
print("Loading Qwen classifiers (baseline + Mechanism B)... this takes a few minutes on CPU.")
qtok = AutoTokenizer.from_pretrained(QWEN_BASE)
if qtok.pad_token is None:
qtok.pad_token = qtok.eos_token
for name, adapter in [("baseline", BASELINE_ADAPTER), ("mechanism_b", MECHB_ADAPTER)]:
base = AutoModelForSequenceClassification.from_pretrained(
QWEN_BASE,
num_labels=2,
torch_dtype=torch.float16,
device_map="cpu", # <-- force CPU at load; GPU only inside @spaces.GPU
low_cpu_mem_usage=True,
)
base.config.pad_token_id = qtok.pad_token_id
m = PeftModel.from_pretrained(base, adapter, device_map="cpu")
m.eval()
qwen_models[name] = m
print(f" {name} ready.")
except Exception as e:
print(f"Qwen loading failed ({e}) -- running in sarcasm-only mode.")
qwen_models = {}
else:
print("LOAD_QWEN=0 -- sarcasm-only mode (local laptop).")
LIVE_HARMFUL = bool(qwen_models)
# ----------------------------- inference fns ------------------------------ #
@torch.no_grad()
def sarcasm_score(text):
dev = "cuda" if torch.cuda.is_available() else "cpu"
model = sar_model.to(dev)
enc = sar_tok(text, truncation=True, max_length=128, return_tensors="pt").to(dev)
logits = model(**enc).logits
# temperature scaling on logits, then softmax
p = torch.softmax(logits / SARCASM_TEMPERATURE, dim=-1)[0, 1].item()
return p
@torch.no_grad()
def harmful_pred(name, text):
m = qwen_models[name]
dev = "cuda" if torch.cuda.is_available() else "cpu"
m = m.to(dev)
enc = qtok(text, truncation=True, max_length=128, return_tensors="pt").to(dev)
logits = m(**enc).logits
return torch.softmax(logits.float(), dim=-1)[0, 1].item()
def fmt_pct(p):
return f"{p:.3f}"
@spaces.GPU(duration=90)
def analyze(text):
text = (text or "").strip()
if not text:
return "Please enter some text.", "", ""
s = sarcasm_score(text)
s_label = "SARCASTIC" if s >= SARCASM_THRESHOLD else "not sarcastic"
sar_out = (f"**Calibrated sarcasm probability: {fmt_pct(s)}** \n"
f"Decision (threshold {SARCASM_THRESHOLD}): **{s_label}** \n"
f"*Twitter-XLM-RoBERTa, hyperparameters-only configuration; "
f"temperature-scaled (T={SARCASM_TEMPERATURE}).*")
if not LIVE_HARMFUL:
harm_out = ("*Harmful-content classifiers (3B parameters each) exceed this machine's memory. "
"See the Showcase tab for committed evaluation results comparing all mechanisms β "
"every number traces to results files in the project repository.*")
return sar_out, harm_out, ""
pb = harmful_pred("baseline", text)
pm = harmful_pred("mechanism_b", text)
lb = "HARMFUL" if pb >= BASELINE_THRESHOLD else "non-harmful"
lm = "HARMFUL" if pm >= MECHB_THRESHOLD else "non-harmful"
harm_out = (f"| Model | p(harmful) | Decision |\n|---|---|---|\n"
f"| Baseline (no sarcasm signal) | {fmt_pct(pb)} | **{lb}** |\n"
f"| Mechanism B (sarcasm-aware) | {fmt_pct(pm)} | **{lm}** |")
note = ""
if lb != lm:
note = ("**The two models disagree on this example** β the sarcasm-aware model's decision "
"differs from the baseline's, illustrating the effect studied in this thesis.")
return sar_out, harm_out, note
# ----------------------------- showcase tab ------------------------------- #
def load_showcase():
if os.path.exists(SHOWCASE_FILE):
with open(SHOWCASE_FILE, encoding="utf-8") as f:
return json.load(f)
return []
SHOWCASE = load_showcase()
def showcase_view(idx):
if not SHOWCASE:
return "No curated examples loaded (curated_examples.json missing)."
ex = SHOWCASE[int(idx)]
rows = "\n".join(
f"| {m['name']} | {m['prob']} | **{m['decision']}** |"
for m in ex["models"])
return (f"### {ex['title']}\n\n"
f"**Text ({ex['language']}):** {ex['text']}\n\n"
f"**True label:** {ex['true_label']} | **Sarcasm score:** {ex['sarcasm_score']}\n\n"
f"| Model | p(harmful) | Decision |\n|---|---|---|\n{rows}\n\n"
f"**Why this example matters:** {ex['story']}\n\n"
f"*Source: {ex['source']} (held-out test set; predictions from committed evaluation artifacts).*")
# ----------------------------- UI ----------------------------------------- #
with gr.Blocks(title="Sarcasm-Aware Harmful Content Detection β PFE Demo") as demo:
gr.Markdown(
"# Multi-Layered Multilingual Sarcasm-Aware Harmful Content Detection\n"
"**PFE defense demo β ESTIN 2025/2026** Β· Arabic + English Β· "
+ ("**Live mode: full pipeline**" if LIVE_HARMFUL else "**Local mode: live sarcasm layer + committed results**"))
with gr.Tab("Live analysis"):
inp = gr.Textbox(label="Enter any text (Arabic or English)", lines=3,
placeholder="Type here β e.g. a tweet, a comment...")
btn = gr.Button("Analyze", variant="primary")
out_sar = gr.Markdown(label="Layer 1 β Sarcasm detection")
out_harm = gr.Markdown(label="Layer 2 β Harmful-content detection")
out_note = gr.Markdown()
btn.click(analyze, inputs=inp, outputs=[out_sar, out_harm, out_note])
with gr.Tab("Showcase: findings on the evaluation set"):
gr.Markdown("Hand-picked held-out test examples illustrating the thesis's main findings. "
"All predictions are committed evaluation artifacts, reproducible from the repository.")
if SHOWCASE:
slider = gr.Slider(0, len(SHOWCASE) - 1, step=1, value=0, label="Example")
view = gr.Markdown(showcase_view(0))
slider.change(showcase_view, inputs=slider, outputs=view)
else:
gr.Markdown("*curated_examples.json not found β add it next to app.py.*")
with gr.Tab("About"):
gr.Markdown(
"**Architecture:** Layer 1 (Twitter-XLM-RoBERTa sarcasm detector, temperature-calibrated) β "
"integration mechanisms β Layer 2 (Qwen2.5-3B + LoRA harmful-content classifier).\n\n"
"**Mechanism B** transfers the sarcasm layer's adapter weights into the harmful-content "
"classifier's initialization β the best-performing mechanism (macro-F1 0.8366 vs 0.8301 baseline; "
"harmful-class McNemar p < 10β»Β³β΄).\n\n"
"Thresholds shown are the validation-selected values frozen before test evaluation, "
"as documented in the thesis.")
demo.launch() |