import gradio as gr import torch import torch.nn.functional as F import numpy as np from transformers import BertForSequenceClassification, BertTokenizer import transformers from lime.lime_text import LimeTextExplainer from textblob import TextBlob import shap import matplotlib.pyplot as plt import matplotlib matplotlib.use("Agg") import io from PIL import Image MODEL_NAME = "UdaniSJ/hate-speech-severity-bert" print("Loading model...") tokenizer = BertTokenizer.from_pretrained(MODEL_NAME) model = BertForSequenceClassification.from_pretrained(MODEL_NAME) device = torch.device("cpu") model.to(device) model.eval() print("Model loaded!") class_names = ["Level 0 (Non-hate)", "Level 1 (Mild)", "Level 2 (Severe)"] explainer_lime = LimeTextExplainer(class_names=class_names) def bert_predict_proba(texts): inputs = tokenizer(list(texts), truncation=True, padding=True, max_length=128, return_tensors="pt").to(device) with torch.no_grad(): outputs = model(**inputs) probs = F.softmax(outputs.logits, dim=1) return probs.cpu().numpy() def check_friendly_context(text, severity_score): blob = TextBlob(text) sentiment = blob.sentiment.polarity flags = [] if severity_score > 0.5 and sentiment > 0.2: flags.append("Positive sentiment detected") affection_words = ["love","friend","bro","sis","mate","buddy","homie","fam","bestie","lol","haha","miss","care","heart","dawg","bruh","ily"] if any(w in text.lower() for w in affection_words): flags.append("Affection language detected") if "?" in text and severity_score > 0.5: flags.append("Questioning tone detected") return flags, sentiment def get_base_prediction(text, friendly_context): probs = bert_predict_proba([text])[0] weights = np.array([0.0, 0.5, 1.0]) severity_score = float(probs @ weights) original_score = severity_score auto_flags, sentiment = check_friendly_context(text, severity_score) context_note = "" if friendly_context: severity_score = severity_score * 0.6 context_note = "Friendly context applied. Original: " + str(round(original_score,3)) + " Adjusted: " + str(round(severity_score,3)) elif auto_flags: reduction = 0.15 * len(auto_flags) severity_score = max(0, severity_score - reduction) context_note = "Friendly signals detected. Adjusted: " + str(round(original_score,3)) + " to " + str(round(severity_score,3)) if severity_score < 0.35: level_str = "Level 0 - Non-hate Speech" elif severity_score < 0.65: level_str = "Level 1 - Mild/Offensive" else: level_str = "Level 2 - Severe Hate Speech" if severity_score >= 0.8: decision = "AUTO-FLAG: High severity" elif severity_score >= 0.5: decision = "HUMAN REVIEW: Ambiguous" else: decision = "ALLOW: Low severity" result = level_str + "\n\n" result += "Severity Score: " + str(round(severity_score,3)) + " / 1.000\n" result += "Sentiment: " + str(round(sentiment,2)) + "\n" result += context_note + "\n\n" result += "Decision: " + decision + "\n\n" result += "Probabilities:\n" result += " Non-hate: " + str(round(probs[0]*100,1)) + "%\n" result += " Mild: " + str(round(probs[1]*100,1)) + "%\n" result += " Severe: " + str(round(probs[2]*100,1)) + "%" return result, severity_score, probs def predict_with_lime(text, friendly_context): if not text.strip(): return "Please enter some text.", 0.0, None result, severity_score, probs = get_base_prediction(text, friendly_context) try: pred_level = int(np.argmax(probs)) exp = explainer_lime.explain_instance(text, bert_predict_proba, num_features=8, num_samples=300, labels=[pred_level]) word_weights = exp.as_list(label=pred_level) words = [w[0] for w in word_weights] scores = [w[1] for w in word_weights] colors = ["#e74c3c" if s > 0 else "#2ecc71" for s in scores] fig, ax = plt.subplots(figsize=(8, 4)) ax.barh(words, scores, color=colors) ax.axvline(x=0, color="black", linewidth=0.8) ax.set_xlabel("Word Importance (red=increases severity, green=decreases)") ax.set_title("LIME Explanation - " + class_names[pred_level]) plt.tight_layout() buf = io.BytesIO() plt.savefig(buf, format="png", dpi=100, bbox_inches="tight") buf.seek(0) img = Image.open(buf) plt.close() except Exception as e: print("LIME error: " + str(e)) img = None return result, severity_score, img def predict_with_shap(text, friendly_context): if not text.strip(): return "Please enter some text.", 0.0, None result, severity_score, probs = get_base_prediction(text, friendly_context) try: pred_level = int(np.argmax(probs)) bert_pipeline = transformers.pipeline( "text-classification", model=model, tokenizer=tokenizer, device=-1, return_all_scores=True, truncation=True, max_length=128 ) explainer_shap = shap.Explainer(bert_pipeline) shap_values = explainer_shap([text]) tokens = shap_values.data[0] values = shap_values.values[0, :, pred_level] valid = [(t, v) for t, v in zip(tokens, values) if t not in ["", "[PAD]", "[CLS]", "[SEP]"]] if not valid: return result, severity_score, None pairs = sorted(valid, key=lambda x: abs(x[1]), reverse=True)[:10] top_tokens = [p[0] for p in pairs] top_values = [p[1] for p in pairs] colors = ["#e74c3c" if v > 0 else "#2ecc71" for v in top_values] fig, ax = plt.subplots(figsize=(8, 4)) ax.barh(top_tokens, top_values, color=colors) ax.axvline(x=0, color="black", linewidth=0.8) ax.set_xlabel("SHAP Value (red=increases severity, green=decreases)") ax.set_title("SHAP Explanation - " + class_names[pred_level]) plt.tight_layout() buf = io.BytesIO() plt.savefig(buf, format="png", dpi=100, bbox_inches="tight") buf.seek(0) img = Image.open(buf) plt.close() except Exception as e: print("SHAP error: " + str(e)) img = None return result, severity_score, img with gr.Blocks(title="Hate Speech Severity Predictor") as demo: gr.Markdown("# Hate Speech Severity Predictor") gr.Markdown("### Explainable AI - BERT + LIME + SHAP") gr.Markdown("**MSc Research Project | University of Moratuwa**") gr.Markdown("---") with gr.Row(): with gr.Column(scale=2): text_input = gr.Textbox(label="Enter Text", placeholder="Type here...", lines=4) friendly_checkbox = gr.Checkbox(label="Friendly/Known context", value=False) with gr.Row(): lime_btn = gr.Button("Analyse with LIME", variant="primary") shap_btn = gr.Button("Analyse with SHAP", variant="secondary") gr.Examples( examples=[ ["I love all people regardless of background", False], ["you idiot i cant believe you lol", False], ["you are my friend for life bro", True], ["those people control all the world banks", False], ["the duffers are too homophobic", False], ], inputs=[text_input, friendly_checkbox], label="Try these examples") with gr.Column(scale=3): result_output = gr.Textbox(label="Analysis Results", lines=14) severity_slider = gr.Slider(minimum=0, maximum=1, label="Severity Score", interactive=False) with gr.Tabs(): with gr.Tab("LIME Explanation"): gr.Markdown("**LIME** - Fast word-level importance. Takes ~30 seconds.") lime_plot = gr.Image(label="LIME Word Importance") with gr.Tab("SHAP Explanation"): gr.Markdown("**SHAP** - Theoretically grounded token importance. Takes ~2-3 minutes.") shap_plot = gr.Image(label="SHAP Token Importance") gr.Markdown("---") gr.Markdown("Level 0 = Non-hate (Score < 0.35) | Level 1 = Mild (0.35-0.65) | Level 2 = Severe (> 0.65)") gr.Markdown("Red bars = increases severity | Green bars = decreases severity") lime_btn.click(fn=predict_with_lime, inputs=[text_input, friendly_checkbox], outputs=[result_output, severity_slider, lime_plot]) shap_btn.click(fn=predict_with_shap, inputs=[text_input, friendly_checkbox], outputs=[result_output, severity_slider, shap_plot]) demo.launch()