Tokyosaurus commited on
Commit
a11dea0
Β·
verified Β·
1 Parent(s): edeaff9

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +1016 -0
  2. requirements.txt +5 -0
app.py ADDED
@@ -0,0 +1,1016 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Taglish Gaslighting Detection - Interactive App
3
+ ================================================
4
+ Sequential pipeline: Binary Detection -> Tactic Identification
5
+
6
+ Models trained on Philippine political Reddit discourse (Taglish).
7
+ Dataset: 928 annotated samples (IAA kappa = 0.81 binary / kappa = 0.86 tactic)
8
+ Split: 70 / 15 / 15 (train / val / test)
9
+
10
+ Usage (Hugging Face Spaces):
11
+ Upload this file as app.py in your Space.
12
+ Models are loaded directly from Hugging Face Hub.
13
+ """
14
+
15
+ import os
16
+ import traceback
17
+ import gradio as gr
18
+ import numpy as np
19
+ import pandas as pd
20
+ import torch
21
+ from transformers import AutoModelForSequenceClassification, AutoTokenizer
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # CONFIGURATION - all performance numbers sourced from training_summary.csv
25
+ # (train_model.py run - MCC removed, confusion matrix added)
26
+ # ---------------------------------------------------------------------------
27
+
28
+ # !! UPDATE THIS to your Hugging Face username !!
29
+ HF_USERNAME = "Tokyosaurus"
30
+
31
+ # Model version - points to v2 repos (balanced + sentence-extracted dataset)
32
+ MODEL_VERSION = "v2"
33
+
34
+ class Config:
35
+
36
+ MODELS = {
37
+ "roberta-tagalog": {
38
+ "display": "RoBERTa-Tagalog",
39
+ "description": "",
40
+ "binary_repo": f"{HF_USERNAME}/taglish-roberta-binary-v2",
41
+ "tactic_repo": f"{HF_USERNAME}/taglish-roberta-tactic-v2",
42
+ "performance": {
43
+ "val_binary_macro_f1": 0.8460,
44
+ "val_binary_gas_p": 0.8788,
45
+ "val_binary_gas_r": 0.8056,
46
+ "val_binary_gas_f1": 0.8406,
47
+ "val_binary_roc_auc": 0.8983,
48
+ "test_binary_macro_f1": 0.7758,
49
+ "test_binary_gas_p": 0.8033,
50
+ "test_binary_gas_r": 0.7206,
51
+ "test_binary_gas_f1": 0.7597,
52
+ "test_binary_roc_auc": 0.8832,
53
+ "val_tactic_macro_f1": 0.5984,
54
+ "val_tactic_f1_dd": 0.7000,
55
+ "val_tactic_f1_tm": 0.2424,
56
+ "val_tactic_f1_ci": 0.9000,
57
+ "val_tactic_f1_ki": 0.3636,
58
+ "test_tactic_macro_f1": 0.6111,
59
+ "test_tactic_f1_dd": 0.6250,
60
+ "test_tactic_f1_tm": 0.4615,
61
+ "test_tactic_f1_ci": 0.8889,
62
+ "test_tactic_f1_ki": 0.3077,
63
+ },
64
+ },
65
+ "mbert": {
66
+ "display": "mBERT",
67
+ "description": "",
68
+ "binary_repo": f"{HF_USERNAME}/taglish-mbert-binary-v2",
69
+ "tactic_repo": f"{HF_USERNAME}/taglish-mbert-tactic-v2",
70
+ "performance": {
71
+ "val_binary_macro_f1": 0.8460,
72
+ "val_binary_gas_p": 0.8788,
73
+ "val_binary_gas_r": 0.8056,
74
+ "val_binary_gas_f1": 0.8406,
75
+ "val_binary_roc_auc": 0.9072,
76
+ "test_binary_macro_f1": 0.8171,
77
+ "test_binary_gas_p": 0.9057,
78
+ "test_binary_gas_r": 0.7059,
79
+ "test_binary_gas_f1": 0.7934,
80
+ "test_binary_roc_auc": 0.9252,
81
+ "val_tactic_macro_f1": 0.5670,
82
+ "val_tactic_f1_dd": 0.7179,
83
+ "val_tactic_f1_tm": 0.3077,
84
+ "val_tactic_f1_ci": 0.7826,
85
+ "val_tactic_f1_ki": 0.2400,
86
+ "test_tactic_macro_f1": 0.4948,
87
+ "test_tactic_f1_dd": 0.4848,
88
+ "test_tactic_f1_tm": 0.2857,
89
+ "test_tactic_f1_ci": 0.6829,
90
+ "test_tactic_f1_ki": 0.2308,
91
+ },
92
+ },
93
+ "xlm-roberta": {
94
+ "display": "XLM-RoBERTa",
95
+ "description": "",
96
+ "binary_repo": f"{HF_USERNAME}/taglish-xlm-binary-v2",
97
+ "tactic_repo": f"{HF_USERNAME}/taglish-xlm-tactic-v2",
98
+ "performance": {
99
+ "val_binary_macro_f1": 0.8252,
100
+ "val_binary_gas_p": 0.8310,
101
+ "val_binary_gas_r": 0.8194,
102
+ "val_binary_gas_f1": 0.8252,
103
+ "val_binary_roc_auc": 0.8891,
104
+ "test_binary_macro_f1": 0.7828,
105
+ "test_binary_gas_p": 0.8167,
106
+ "test_binary_gas_r": 0.7206,
107
+ "test_binary_gas_f1": 0.7656,
108
+ "test_binary_roc_auc": 0.8642,
109
+ "val_tactic_macro_f1": 0.5042,
110
+ "val_tactic_f1_dd": 0.6977,
111
+ "val_tactic_f1_tm": 0.1818,
112
+ "val_tactic_f1_ci": 0.6538,
113
+ "val_tactic_f1_ki": 0.1905,
114
+ "test_tactic_macro_f1": 0.4673,
115
+ "test_tactic_f1_dd": 0.5405,
116
+ "test_tactic_f1_tm": 0.1000,
117
+ "test_tactic_f1_ci": 0.6522,
118
+ "test_tactic_f1_ki": 0.2727,
119
+ },
120
+ },
121
+ }
122
+
123
+ BINARY_LABELS = {0: "Non-Gaslighting", 1: "Gaslighting"}
124
+
125
+ TACTIC_LABELS = {
126
+ 0: "Non-Gaslighting",
127
+ 1: "Distortion & Denial",
128
+ 2: "Trivialization & Minimization",
129
+ 3: "Coercion & Intimidation",
130
+ 4: "Knowledge Invalidation",
131
+ }
132
+
133
+ TACTIC_DESCRIPTIONS = {
134
+ 1: "**Distortion & Denial** - Rewrites or denies documented facts, reshapes past events "
135
+ "to alter how they are perceived.",
136
+ 2: "**Trivialization & Minimization** - Downplays or mocks concerns, frames them as "
137
+ "insignificant, exaggerated, or emotionally irrational.",
138
+ 3: "**Coercion & Intimidation** - Pressures, threatens, or silences through fear, "
139
+ "aggression, name-calling, or social dominance.",
140
+ 4: "**Knowledge Invalidation** - Attacks cognitive capacity specifically; implies the "
141
+ "target is incapable of understanding or making valid judgments.",
142
+ }
143
+
144
+ # ── Confusion matrices (test in-domain) ──────────────────────────────────
145
+ # Binary : rows/cols = [Non-Gaslighting, Gaslighting]
146
+ # Tactic : rows/cols = [Non-Gas, D&D, T&M, C&I, KI]
147
+ CONFUSION_MATRICES = {
148
+ "roberta-tagalog": {
149
+ "binary": {
150
+ "labels": ["Non-Gaslighting", "Gaslighting"],
151
+ "matrix": [
152
+ [59, 12],
153
+ [19, 49],
154
+ ],
155
+ },
156
+ "tactic": {
157
+ "labels": ["Non-Gaslighting", "D&D", "T&M", "C&I", "KI"],
158
+ "matrix": [
159
+ [56, 3, 9, 2, 1],
160
+ [ 5,10, 0, 0, 2],
161
+ [ 5, 0, 9, 1, 2],
162
+ [ 0, 1, 0,16, 0],
163
+ [ 8, 1, 4, 0, 4],
164
+ ],
165
+ },
166
+ },
167
+ "mbert": {
168
+ "binary": {
169
+ "labels": ["Non-Gaslighting", "Gaslighting"],
170
+ "matrix": [
171
+ [66, 5],
172
+ [20,48],
173
+ ],
174
+ },
175
+ "tactic": {
176
+ "labels": ["Non-Gaslighting", "D&D", "T&M", "C&I", "KI"],
177
+ "matrix": [
178
+ [62, 5, 0, 3, 1],
179
+ [ 5, 8, 0, 1, 3],
180
+ [ 9, 0, 3, 4, 1],
181
+ [ 1, 1, 0,14, 1],
182
+ [ 9, 2, 1, 2, 3],
183
+ ],
184
+ },
185
+ },
186
+ "xlm-roberta": {
187
+ "binary": {
188
+ "labels": ["Non-Gaslighting", "Gaslighting"],
189
+ "matrix": [
190
+ [60,11],
191
+ [19,49],
192
+ ],
193
+ },
194
+ "tactic": {
195
+ "labels": ["Non-Gaslighting", "D&D", "T&M", "C&I", "KI"],
196
+ "matrix": [
197
+ [59, 7, 1, 3, 1],
198
+ [ 6,10, 0, 1, 0],
199
+ [ 7, 3, 1, 5, 1],
200
+ [ 1, 0, 1,15, 0],
201
+ [ 9, 0, 0, 5, 3],
202
+ ],
203
+ },
204
+ },
205
+ }
206
+
207
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
208
+ MAX_LENGTH = 128
209
+
210
+
211
+ # ---------------------------------------------------------------------------
212
+ # MODEL CACHE - loads from Hugging Face Hub
213
+ # ---------------------------------------------------------------------------
214
+
215
+ class ModelCache:
216
+
217
+ def __init__(self):
218
+ self._cache: dict = {}
219
+
220
+ def load(self, model_key: str) -> dict:
221
+ if model_key in self._cache:
222
+ return self._cache[model_key]
223
+
224
+ info = Config.MODELS[model_key]
225
+ print(f" Loading {info['display']} from Hugging Face Hub ...")
226
+
227
+ def _load(repo_id):
228
+ print(f" Fetching: {repo_id}")
229
+ tok = AutoTokenizer.from_pretrained(repo_id)
230
+ model = AutoModelForSequenceClassification.from_pretrained(repo_id)
231
+ model.to(Config.DEVICE).eval()
232
+ return tok, model
233
+
234
+ try:
235
+ b_tok, b_model = _load(info["binary_repo"])
236
+ t_tok, t_model = _load(info["tactic_repo"])
237
+ except Exception as e:
238
+ raise RuntimeError(
239
+ f"Failed to load {info['display']} from Hub.\n"
240
+ f"Binary repo: {info['binary_repo']}\n"
241
+ f"Tactic repo: {info['tactic_repo']}\n"
242
+ f"Error: {e}"
243
+ )
244
+
245
+ entry = {
246
+ "binary": {"tokenizer": b_tok, "model": b_model},
247
+ "tactic": {"tokenizer": t_tok, "model": t_model},
248
+ "info": info,
249
+ }
250
+ self._cache[model_key] = entry
251
+ print(f" {info['display']} ready")
252
+ return entry
253
+
254
+ _cache = ModelCache()
255
+
256
+
257
+ # ---------------------------------------------------------------------------
258
+ # INFERENCE HELPER
259
+ # ---------------------------------------------------------------------------
260
+
261
+ def _infer(tokenizer, model, text: str):
262
+ """Tokenize, run model, return (probs_np, pred_int, confidence_float)."""
263
+ enc = tokenizer(
264
+ text,
265
+ truncation=True,
266
+ max_length=Config.MAX_LENGTH,
267
+ padding=True,
268
+ return_tensors="pt",
269
+ )
270
+ enc = {k: v.to(Config.DEVICE) for k, v in enc.items()}
271
+ enc.pop("token_type_ids", None) # not used by all architectures
272
+
273
+ with torch.no_grad():
274
+ logits = model(**enc).logits
275
+ probs = torch.softmax(logits, dim=-1)[0].cpu().numpy()
276
+
277
+ pred = int(np.argmax(probs))
278
+ return probs, pred, float(probs[pred])
279
+
280
+
281
+ # ---------------------------------------------------------------------------
282
+ # SINGLE TEXT PREDICTION
283
+ # ---------------------------------------------------------------------------
284
+
285
+ def predict_sequential(text: str, model_key: str):
286
+ if not text or not text.strip():
287
+ return "Please enter some text to analyze.", None, None
288
+
289
+ try:
290
+ models = _cache.load(model_key)
291
+ info = models["info"]
292
+ perf = info["performance"]
293
+
294
+ # ── Step 1: Binary classification ────────────────────────────────────
295
+ b_probs, b_pred, b_conf = _infer(
296
+ models["binary"]["tokenizer"],
297
+ models["binary"]["model"],
298
+ text,
299
+ )
300
+ is_gas = b_pred == 1
301
+ binary_label = Config.BINARY_LABELS[b_pred]
302
+ binary_prob_dict = {
303
+ "Non-Gaslighting": float(b_probs[0]),
304
+ "Gaslighting": float(b_probs[1]),
305
+ }
306
+
307
+ # Zeroed tactic probs prevent Gradio Label widget crashes for Non-GL
308
+ tactic_prob_dict = {
309
+ "Distortion & Denial": 0.0,
310
+ "Trivialization & Minimization": 0.0,
311
+ "Coercion & Intimidation": 0.0,
312
+ "Knowledge Invalidation": 0.0,
313
+ }
314
+ tactic_section = ""
315
+
316
+ # ── Step 2: Tactic classification (only when binary says Gaslighting) ─
317
+ if is_gas:
318
+ t_probs, t_pred, t_conf = _infer(
319
+ models["tactic"]["tokenizer"],
320
+ models["tactic"]["model"],
321
+ text,
322
+ )
323
+
324
+ # ── OVERRIDE LOGIC ──────────────────────────────────────────────
325
+ # The two models disagree: binary says Gaslighting but tactic says
326
+ # Non-Gaslighting (class 0). A text cannot be gaslighting while
327
+ # having no recognisable tactic, so we trust the tactic model and
328
+ # override the binary result to Non-Gaslighting.
329
+ if t_pred == 0:
330
+ is_gas = False
331
+ binary_label = "Non-Gaslighting"
332
+ b_conf = float(t_probs[0])
333
+ # Rebuild binary probs from tactic softmax (safe: no negatives)
334
+ binary_prob_dict = {
335
+ "Non-Gaslighting": float(t_probs[0]),
336
+ "Gaslighting": max(0.0, 1.0 - float(t_probs[0])),
337
+ }
338
+ # Tactic probs remain all-zeros (correct: no tactic detected)
339
+ tactic_section = (
340
+ "_Tactic model overruled binary model: "
341
+ "Text classified as Non-Gaslighting._"
342
+ )
343
+ # ── END OVERRIDE ────────────────────────────────────────────────
344
+ else:
345
+ tactic_label = Config.TACTIC_LABELS[t_pred]
346
+ tactic_desc = Config.TACTIC_DESCRIPTIONS.get(
347
+ t_pred, "_No description available._"
348
+ )
349
+ tactic_prob_dict = {
350
+ Config.TACTIC_LABELS[i]: float(t_probs[i])
351
+ for i in range(1, 5)
352
+ }
353
+ tactic_section = f"""
354
+ ### Tactic: {tactic_label}
355
+ **Confidence:** {t_conf:.1%}
356
+
357
+ {tactic_desc}
358
+ """
359
+ else:
360
+ tactic_section = (
361
+ "_No tactic classification - text is Non-Gaslighting._"
362
+ )
363
+
364
+ # ── Format result card ───────────────────────────────────────────────
365
+ result = f"""
366
+ # Result: {binary_label}
367
+
368
+ **Binary Confidence:** {b_conf:.1%}
369
+
370
+ ---
371
+
372
+ {tactic_section}
373
+
374
+ ---
375
+
376
+ ## Model: {info['display']}
377
+
378
+ | | Validation | Test (In-Domain) |
379
+ |---|---|---|
380
+ | **Binary Macro-F1** | {perf['val_binary_macro_f1']:.4f} | {perf['test_binary_macro_f1']:.4f} |
381
+ | **Gas. Precision / Recall / F1** | {perf['val_binary_gas_p']:.3f} / {perf['val_binary_gas_r']:.3f} / {perf['val_binary_gas_f1']:.3f} | {perf['test_binary_gas_p']:.3f} / {perf['test_binary_gas_r']:.3f} / {perf['test_binary_gas_f1']:.3f} |
382
+ | **Binary ROC-AUC** | {perf['val_binary_roc_auc']:.4f} | {perf['test_binary_roc_auc']:.4f} |
383
+ | **Tactic Macro-F1** | {perf['val_tactic_macro_f1']:.4f} | {perf['test_tactic_macro_f1']:.4f} |
384
+ | **F1: D&D / T&M / C&I / KI** | {perf['val_tactic_f1_dd']:.3f} / {perf['val_tactic_f1_tm']:.3f} / {perf['val_tactic_f1_ci']:.3f} / {perf['val_tactic_f1_ki']:.3f} | {perf['test_tactic_f1_dd']:.3f} / {perf['test_tactic_f1_tm']:.3f} / {perf['test_tactic_f1_ci']:.3f} / {perf['test_tactic_f1_ki']:.3f} |
385
+ """
386
+ return result, binary_prob_dict, tactic_prob_dict
387
+
388
+ except Exception as e:
389
+ return f"Error: {e}\n\n{traceback.format_exc()}", None, None
390
+
391
+
392
+ # ---------------------------------------------------------------------------
393
+ # BATCH PREDICTION
394
+ # ---------------------------------------------------------------------------
395
+
396
+ def batch_predict(file, model_key: str):
397
+ try:
398
+ df = pd.read_csv(file.name)
399
+ if "sentence" not in df.columns:
400
+ return pd.DataFrame({"Error": ["CSV must contain a 'sentence' column"]})
401
+
402
+ models = _cache.load(model_key)
403
+ b_tok, b_mod = models["binary"]["tokenizer"], models["binary"]["model"]
404
+ t_tok, t_mod = models["tactic"]["tokenizer"], models["tactic"]["model"]
405
+
406
+ b_labels, b_confs = [], []
407
+ t_labels, t_confs = [], []
408
+
409
+ for text in df["sentence"].astype(str):
410
+ b_probs, b_pred, b_conf = _infer(b_tok, b_mod, text)
411
+
412
+ if b_pred == 1:
413
+ t_probs, t_pred, t_conf = _infer(t_tok, t_mod, text)
414
+
415
+ # ── OVERRIDE LOGIC ──────────────────────────────────────────
416
+ # Tactic model says Non-Gaslighting β†’ override binary result.
417
+ # Vice versa is impossible: tactic only runs when binary=Gas.
418
+ if t_pred == 0:
419
+ b_labels.append("Non-Gaslighting")
420
+ b_confs.append(f"{float(t_probs[0]):.1%}")
421
+ t_labels.append("N/A")
422
+ t_confs.append("N/A")
423
+ # ── END OVERRIDE ────────────────────────────────────────────
424
+ else:
425
+ b_labels.append("Gaslighting")
426
+ b_confs.append(f"{b_conf:.1%}")
427
+ t_labels.append(Config.TACTIC_LABELS[t_pred])
428
+ t_confs.append(f"{t_conf:.1%}")
429
+ else:
430
+ b_labels.append("Non-Gaslighting")
431
+ b_confs.append(f"{b_conf:.1%}")
432
+ t_labels.append("N/A")
433
+ t_confs.append("N/A")
434
+
435
+ df["binary_prediction"] = b_labels
436
+ df["binary_confidence"] = b_confs
437
+ df["tactic_prediction"] = t_labels
438
+ df["tactic_confidence"] = t_confs
439
+ return df
440
+
441
+ except Exception as e:
442
+ return pd.DataFrame({"Error": [str(e)], "Traceback": [traceback.format_exc()]})
443
+
444
+
445
+ # ---------------------------------------------------------------------------
446
+ # EXAMPLE TEXTS (political Taglish, aligned with training domain)
447
+ # ---------------------------------------------------------------------------
448
+
449
+ EXAMPLES = {
450
+ "Distortion & Denial - History rewrite":
451
+ "Hindi totoo na may human rights abuses nung Martial Law, gawa-gawa lang yan ng mga kalaban nila.",
452
+ "Trivialization & Minimization - Downplay":
453
+ "Ang arte niyo naman sa transport strike. Konting lakad lang, nagrereklamo na kayo agad na para kayong pinahirapan ng todo",
454
+ "Coercion & Intimidation - Scaring":
455
+ "Kung patuloy mong babatikosin ang gobyerno, wag kang magtaka kung may kumatok sa bahay mo isang gabi. Mag-ingat ka sa mga pino-post mo.",
456
+ "Knowledge Invalidation - Attacking intellect":
457
+ "Ang tanga mo naman mag-analisa ng economic data. Hindi ka economist, hindi mo kaya ang intindihin ang mga numero kahit ipaliwanag pa namin.",
458
+ "Non-Gaslighting - Critique":
459
+ "I respectfully disagree with this policy. Based on COA findings, the budget allocation lacks transparency and proper documentation.",
460
+ "Non-Gaslighting - Questioning":
461
+ "Can you provide a reliable source for that claim about the sudden increase in the budget? Gusto ko lang sana mabasa yung full context nung report.",
462
+ }
463
+
464
+
465
+ # ---------------------------------------------------------------------------
466
+ # CONFUSION MATRIX RENDERER
467
+ # ---------------------------------------------------------------------------
468
+
469
+ def _cm_to_html(labels, matrix):
470
+ """Render a confusion matrix as a colour-coded HTML table.
471
+
472
+ Diagonal cells (correct predictions) β†’ shaded green, scaled by count.
473
+ Off-diagonal cells (errors) β†’ shaded red, scaled by count.
474
+ """
475
+ n = len(labels)
476
+ max_off = max(
477
+ matrix[r][c]
478
+ for r in range(n) for c in range(n) if r != c
479
+ ) or 1
480
+
481
+ header_cells = "".join(
482
+ f'<th style="padding:6px 10px;background:#374151;color:#fff;'
483
+ f'text-align:center;font-size:12px;">{lbl}</th>'
484
+ for lbl in labels
485
+ )
486
+
487
+ rows_html = ""
488
+ for r, row_lbl in enumerate(labels):
489
+ row_label_cell = (
490
+ f'<td style="padding:6px 10px;font-weight:bold;white-space:nowrap;'
491
+ f'background:#1f2937;color:#fff;font-size:12px;">True: {row_lbl}</td>'
492
+ )
493
+ cells = ""
494
+ for c, val in enumerate(matrix[r]):
495
+ if r == c: # correct prediction – green
496
+ intensity = min(255, 120 + int(val * 4))
497
+ bg = f"rgb(34,{intensity},34)"
498
+ fg = "#fff"
499
+ else: # error – red
500
+ alpha = val / max_off
501
+ r_ch = int(180 + 75 * alpha)
502
+ bg = f"rgb({r_ch},50,50)"
503
+ fg = "#fff" if alpha > 0.3 else "#ccc"
504
+ cells += (
505
+ f'<td style="padding:6px 10px;text-align:center;'
506
+ f'background:{bg};color:{fg};font-weight:bold;font-size:13px;">'
507
+ f'{val}</td>'
508
+ )
509
+ rows_html += f"<tr>{row_label_cell}{cells}</tr>"
510
+
511
+ return f"""
512
+ <div style="overflow-x:auto;margin:8px 0;">
513
+ <table style="border-collapse:collapse;font-family:monospace;width:100%;">
514
+ <thead>
515
+ <tr>
516
+ <th style="padding:6px 10px;background:#111827;color:#fff;text-align:left;
517
+ font-size:12px;">Actual \\ Predicted</th>
518
+ {header_cells}
519
+ </tr>
520
+ </thead>
521
+ <tbody>{rows_html}</tbody>
522
+ </table>
523
+ </div>
524
+ """
525
+
526
+
527
+ # ---------------------------------------------------------------------------
528
+ # MODEL PROFILES
529
+ # ---------------------------------------------------------------------------
530
+
531
+ def _model_profile(model_key: str):
532
+ strengths = {
533
+ "roberta-tagalog": (
534
+ "Best tactic Macro-F1 (0.6111) and highest C&I detection (0.8889). "
535
+ "Strong ROC-AUC (0.8832) - good probabilistic calibration. "
536
+ "Monolingual Filipino pretraining captures Taglish political nuances."
537
+ ),
538
+ "mbert": (
539
+ "Best binary Gas-F1 (0.7934) and Macro-F1 (0.8171) on test. "
540
+ "Highest ROC-AUC (0.9252) - strongest probabilistic discrimination. "
541
+ "Very high precision (0.9057) - fewest false positives."
542
+ ),
543
+ "xlm-roberta": (
544
+ "Most balanced binary precision/recall (0.8167/0.7206). "
545
+ "Best D&D detection among all models (0.5405). "
546
+ "SentencePiece tokenizer handles Tagalog morphology well."
547
+ ),
548
+ }
549
+ limitations = {
550
+ "roberta-tagalog": (
551
+ "T&M remains hard (0.4615 test). "
552
+ "Lower binary test Macro-F1 (0.7758) than mBERT. "
553
+ "Most domain-specific - may struggle outside political discourse."
554
+ ),
555
+ "mbert": (
556
+ "Precision-biased (P=0.9057 > R=0.7059) - misses more gaslighting. "
557
+ "Weakest tactic Macro-F1 (0.4948). "
558
+ "WordPiece oversegments Tagalog tokens."
559
+ ),
560
+ "xlm-roberta": (
561
+ "Lowest tactic Macro-F1 (0.4673). T&M F1 = 0.1000 on test - near-random. "
562
+ "Lowest binary ROC-AUC (0.8642). "
563
+ "Over-sensitive to domain shift."
564
+ ),
565
+ }
566
+ return strengths.get(model_key, ""), limitations.get(model_key, "")
567
+
568
+
569
+ # ---------------------------------------------------------------------------
570
+ # GRADIO INTERFACE
571
+ # ---------------------------------------------------------------------------
572
+
573
+ def _model_choices():
574
+ return [
575
+ (f"{Config.MODELS[k]['display']} - {Config.MODELS[k]['description']}", k)
576
+ for k in Config.MODELS
577
+ ]
578
+
579
+
580
+ def create_interface():
581
+
582
+ css = """
583
+ .prediction-output {
584
+ font-size: 15px !important;
585
+ line-height: 1.8 !important;
586
+ padding: 24px !important;
587
+ margin-top: 8px !important;
588
+ border: 1px solid var(--block-border-color) !important;
589
+ border-radius: 10px !important;
590
+ background-color: var(--block-background-fill) !important;
591
+ color: var(--body-text-color) !important;
592
+ }
593
+ .gradio-container { max-width: 1400px; margin: auto; }
594
+ .model-card {
595
+ border: 1px solid var(--block-border-color);
596
+ border-radius: 8px;
597
+ padding: 15px;
598
+ margin: 10px 0;
599
+ background-color: var(--block-background-fill);
600
+ }
601
+ """
602
+
603
+ with gr.Blocks(title="Taglish Gaslighting Detector", theme=gr.themes.Soft(), css=css) as app:
604
+
605
+ gr.Markdown("""
606
+ # Taglish Political Gaslighting Detection
607
+ **Sequential Pipeline: Binary Detection -> Tactic Identification**
608
+
609
+ Trained on Philippine political Reddit discourse (r/Philippines, r/PhilippinesPolitics, r/31MillionRegrets).
610
+ Dataset: **944 gold-standard samples** - Purely human-annotated - Balanced tactic classes (118 per tactic) - Key-sentence extracted
611
+ """)
612
+
613
+ with gr.Tabs():
614
+
615
+ # ── Analyze Text ─────────────────────────────────────────────────
616
+ with gr.Tab("Analyze Text"):
617
+
618
+ gr.Markdown("### Analyze a single Taglish post")
619
+
620
+ with gr.Row():
621
+ with gr.Column(scale=1):
622
+ text_input = gr.Textbox(
623
+ label="Input text",
624
+ placeholder="Paste Tagalog / Taglish text here ...",
625
+ lines=6,
626
+ )
627
+ model_dd = gr.Dropdown(
628
+ choices=_model_choices(),
629
+ value="roberta-tagalog",
630
+ label="Model",
631
+ info="RoBERTa-Tagalog is recommended for political Taglish.",
632
+ )
633
+ analyze_btn = gr.Button("Analyze", variant="primary", size="lg")
634
+
635
+ gr.Markdown("""
636
+ **Model guide (v2 - balanced dataset)**
637
+ - **mBERT** - best binary Gas-F1 (0.7934) and Macro-F1 (0.8171); ROC-AUC 0.9252
638
+ - **RoBERTa-Tagalog** - best tactic Macro-F1 (0.6111); Gas-F1 0.7597; ROC-AUC 0.8832
639
+ - **XLM-RoBERTa** - balanced binary precision/recall (0.8167/0.7206); ROC-AUC 0.8642
640
+ """)
641
+
642
+ with gr.Column(scale=2):
643
+ pred_output = gr.Markdown(
644
+ label="Result",
645
+ elem_classes=["prediction-output"],
646
+ )
647
+ with gr.Row():
648
+ binary_plot = gr.Label(
649
+ label="Binary probabilities",
650
+ num_top_classes=2,
651
+ )
652
+ tactic_plot = gr.Label(
653
+ label="Tactic probabilities (if gaslighting)",
654
+ num_top_classes=4,
655
+ )
656
+
657
+ gr.Markdown("### Quick examples")
658
+ for row_keys in [list(EXAMPLES.keys())[:3], list(EXAMPLES.keys())[3:]]:
659
+ with gr.Row():
660
+ for name in row_keys:
661
+ gr.Button(name, size="sm").click(
662
+ fn=lambda n=name: EXAMPLES[n],
663
+ inputs=None,
664
+ outputs=text_input,
665
+ )
666
+
667
+ analyze_btn.click(
668
+ fn=predict_sequential,
669
+ inputs=[text_input, model_dd],
670
+ outputs=[pred_output, binary_plot, tactic_plot],
671
+ )
672
+
673
+ # ── Batch Processing ──────────────────────────────────────────────
674
+ with gr.Tab("Batch Processing"):
675
+
676
+ gr.Markdown("""
677
+ ### Process multiple texts from a CSV file
678
+ Upload a CSV with a `sentence` column.
679
+ The pipeline runs binary classification on every row,
680
+ then tactic classification on rows flagged as gaslighting.
681
+ """)
682
+
683
+ with gr.Row():
684
+ with gr.Column():
685
+ file_input = gr.File(label="Upload CSV", file_types=[".csv"])
686
+ batch_model = gr.Dropdown(
687
+ choices=_model_choices(),
688
+ value="roberta-tagalog",
689
+ label="Model",
690
+ )
691
+ process_btn = gr.Button("Process", variant="primary", size="lg")
692
+ gr.Markdown("""
693
+ **Required CSV format**
694
+ ```
695
+ sentence
696
+ "First text here"
697
+ "Second text here"
698
+ ```
699
+ """)
700
+
701
+ with gr.Column():
702
+ batch_output = gr.Dataframe(
703
+ label="Results preview",
704
+ wrap=True,
705
+ interactive=False,
706
+ )
707
+ download_btn = gr.File(label="Download results")
708
+
709
+ def process_and_save(file, model):
710
+ if file is None:
711
+ return pd.DataFrame({"Error": ["Please upload a CSV file"]}), None
712
+ results = batch_predict(file, model)
713
+ out_path = "batch_predictions.csv"
714
+ results.to_csv(out_path, index=False, encoding="utf-8-sig")
715
+ return results, out_path
716
+
717
+ process_btn.click(
718
+ fn=process_and_save,
719
+ inputs=[file_input, batch_model],
720
+ outputs=[batch_output, download_btn],
721
+ )
722
+
723
+ # ── Model Performance ─────────────────────────────────────────────
724
+ with gr.Tab("Model Performance"):
725
+
726
+ gr.Markdown("## Evaluation Results *(in-domain test set - train_model.py)*")
727
+
728
+ gr.Markdown("### Binary Classification")
729
+ binary_rows = []
730
+ for k, info in Config.MODELS.items():
731
+ p = info["performance"]
732
+ binary_rows.append({
733
+ "Model": info["display"],
734
+ "Val Macro-F1": f"{p['val_binary_macro_f1']:.4f}",
735
+ "Test Gas-P": f"{p['test_binary_gas_p']:.4f}",
736
+ "Test Gas-R": f"{p['test_binary_gas_r']:.4f}",
737
+ "Test Gas-F1": f"{p['test_binary_gas_f1']:.4f}",
738
+ "Test Macro-F1": f"{p['test_binary_macro_f1']:.4f}",
739
+ "Test ROC-AUC": f"{p['test_binary_roc_auc']:.4f}",
740
+ })
741
+ gr.Dataframe(pd.DataFrame(binary_rows), wrap=True)
742
+
743
+ gr.Markdown("""
744
+ > Confusion matrices (2Γ—2 per model) are shown in the per-model accordions below.
745
+
746
+ **Key findings - Binary:**
747
+ - **mBERT** achieves the best binary Gas-F1 (0.7934) and Macro-F1 (0.8171) on the test set
748
+ - **mBERT** also leads on ROC-AUC (0.9252) - strongest probabilistic discrimination
749
+ - **mBERT** is heavily precision-biased (P=0.9057 > R=0.7059) - fewest false positives but misses more true positives
750
+ - **RoBERTa-Tagalog** and **XLM-RoBERTa** share the same recall (0.7206) with different precision profiles
751
+ - **XLM-RoBERTa** has the most balanced binary precision/recall (0.8167 / 0.7206)
752
+ - All 3 models exceed ROC-AUC 0.86 - all are strong probabilistic classifiers
753
+ """)
754
+
755
+ gr.Markdown("---")
756
+
757
+ gr.Markdown("### Tactic Classification (5-class: Non-Gas + 4 tactics)")
758
+ tactic_rows = []
759
+ for k, info in Config.MODELS.items():
760
+ p = info["performance"]
761
+ tactic_rows.append({
762
+ "Model": info["display"],
763
+ "Val Macro-F1": f"{p['val_tactic_macro_f1']:.4f}",
764
+ "Test Macro-F1": f"{p['test_tactic_macro_f1']:.4f}",
765
+ "Test F1 D&D": f"{p['test_tactic_f1_dd']:.4f}",
766
+ "Test F1 T&M": f"{p['test_tactic_f1_tm']:.4f}",
767
+ "Test F1 C&I": f"{p['test_tactic_f1_ci']:.4f}",
768
+ "Test F1 KI": f"{p['test_tactic_f1_ki']:.4f}",
769
+ })
770
+ gr.Dataframe(pd.DataFrame(tactic_rows), wrap=True)
771
+
772
+ gr.Markdown("""
773
+ > Confusion matrices (5Γ—5 per model) are shown in the per-model accordions below.
774
+
775
+ **Key findings - Tactic:**
776
+ - **RoBERTa-Tagalog** leads tactic Macro-F1 (0.6111) - best overall tactic classifier
777
+ - **T&M (Trivialization & Minimization) is the hardest tactic** across all models:
778
+ RoBERTa 0.4615, mBERT 0.2857, XLM-RoBERTa 0.1000 - sarcasm/dismissal overlaps with normal discourse
779
+ - **C&I (Coercion & Intimidation) is the easiest** across all models:
780
+ RoBERTa 0.8889, mBERT 0.6829, XLM-RoBERTa 0.6522 - aggressive language is lexically distinctive
781
+ - **RoBERTa-Tagalog C&I = 0.8889** - near-perfect detection of intimidation language
782
+ - **XLM-RoBERTa T&M = 0.1000** - near-random; sarcasm and dismissal are opaque to cross-lingual models
783
+ - **mBERT** has the weakest tactic Macro-F1 (0.4948) despite leading on binary
784
+ - Approximately 83 training samples per tactic class remains the primary performance ceiling
785
+
786
+ **Metric guide (Section 4.5):**
787
+ - **Gas-F1** - F1 for the Gaslighting (positive) class; primary binary metric
788
+ - **Macro-F1** - equal weight to all classes; primary tactic metric
789
+ - **ROC-AUC** - probability calibration; binary only
790
+ - **Confusion matrix** - saved per model per test split (binary: 2x2, tactic: 5x5)
791
+ """)
792
+
793
+ gr.Markdown("---")
794
+
795
+ gr.Markdown("### Per-model full breakdown")
796
+ for k, info in Config.MODELS.items():
797
+ p = info["performance"]
798
+ strengths, limitations = _model_profile(k)
799
+ cm_data = Config.CONFUSION_MATRICES[k]
800
+ bin_cm_html = _cm_to_html(
801
+ cm_data["binary"]["labels"],
802
+ cm_data["binary"]["matrix"],
803
+ )
804
+ tac_cm_html = _cm_to_html(
805
+ cm_data["tactic"]["labels"],
806
+ cm_data["tactic"]["matrix"],
807
+ )
808
+ with gr.Accordion(f"{info['display']}", open=False):
809
+ gr.Markdown(f"""
810
+ **{info['display']}**
811
+
812
+ **Binary Classification**
813
+
814
+ | Metric | Validation | Test (In-Domain) |
815
+ |--------|-----------|-----------------|
816
+ | Macro-F1 | {p['val_binary_macro_f1']:.4f} | {p['test_binary_macro_f1']:.4f} |
817
+ | Gas. Precision | {p['val_binary_gas_p']:.4f} | {p['test_binary_gas_p']:.4f} |
818
+ | Gas. Recall | {p['val_binary_gas_r']:.4f} | {p['test_binary_gas_r']:.4f} |
819
+ | Gas. F1 | {p['val_binary_gas_f1']:.4f} | {p['test_binary_gas_f1']:.4f} |
820
+ | ROC-AUC | {p['val_binary_roc_auc']:.4f} | {p['test_binary_roc_auc']:.4f} |
821
+
822
+ **Binary Confusion Matrix (Test In-Domain)**
823
+ """)
824
+ gr.HTML(bin_cm_html)
825
+ gr.Markdown(f"""
826
+ ---
827
+
828
+ **Tactic Classification**
829
+
830
+ | Metric | Validation | Test (In-Domain) |
831
+ |--------|-----------|-----------------|
832
+ | Macro-F1 | {p['val_tactic_macro_f1']:.4f} | {p['test_tactic_macro_f1']:.4f} |
833
+ | F1 Distortion & Denial | {p['val_tactic_f1_dd']:.4f} | {p['test_tactic_f1_dd']:.4f} |
834
+ | F1 Trivialization & Min. | {p['val_tactic_f1_tm']:.4f} | {p['test_tactic_f1_tm']:.4f} |
835
+ | F1 Coercion & Intimidation | {p['val_tactic_f1_ci']:.4f} | {p['test_tactic_f1_ci']:.4f} |
836
+ | F1 Knowledge Invalidation | {p['val_tactic_f1_ki']:.4f} | {p['test_tactic_f1_ki']:.4f} |
837
+
838
+ **Tactic Confusion Matrix (Test In-Domain)**
839
+ """)
840
+ gr.HTML(tac_cm_html)
841
+ gr.Markdown(f"""
842
+ ---
843
+
844
+ **Strengths:** {strengths}
845
+
846
+ **Limitations:** {limitations}
847
+ """)
848
+
849
+ # ── Tactics Guide ─────────────────────────────────────────────────
850
+ with gr.Tab("Tactics Guide"):
851
+
852
+ gr.Markdown("## Understanding the 4 Gaslighting Tactics")
853
+
854
+ with gr.Accordion("Distortion & Denial", open=True):
855
+ gr.Markdown("""
856
+ **Definition:** Statements that reinterpret, rewrite, or deny reality,
857
+ particularly by reshaping past or present events to alter how they are perceived.
858
+
859
+ **Key linguistic cues:** temporal markers ("dati", "noon", "kanina"),
860
+ claims about how events "really" happened, false certainty about another's experience.
861
+
862
+ **Examples:**
863
+ - *"Hindi naman ganyan nangyari dati."*
864
+ - *"Walang nangyaring martial law abuses. Propaganda lang yan."*
865
+ - *"Binabago mo lang ang mga salita ko para lumabas akong masama."*
866
+ """)
867
+
868
+ with gr.Accordion("Trivialization & Minimization", open=False):
869
+ gr.Markdown("""
870
+ **Definition:** Statements that downplay or mock concerns, framing them as
871
+ insignificant, exaggerated, or emotionally irrational.
872
+
873
+ **Key linguistic cues:** "OA/arte/joke lang", "di big deal", "move on ka na",
874
+ "kalma ka lang".
875
+
876
+ **Examples:**
877
+ - *"Ang liit na bagay, pinapalaki mo."*
878
+ - *"Drama mo naman, OA ka talaga."*
879
+ - *"Wala namang mangyayari kahit magreklamo ka."*
880
+ """)
881
+
882
+ with gr.Accordion("Coercion & Intimidation", open=False):
883
+ gr.Markdown("""
884
+ **Definition:** Statements that pressure, threaten, or silence through
885
+ fear, aggression, name-calling, or social dominance.
886
+
887
+ **Key linguistic cues:** demeaning commands ("tumahimik ka na"),
888
+ shaming language, bullying phrases intended to suppress speech.
889
+
890
+ **Note:** Plain blackmail or threats without a gaslighting mechanism
891
+ are classified as Non-Gaslighting per the codebook.
892
+
893
+ **Examples:**
894
+ - *"Tumahimik ka na lang, wala kang alam."*
895
+ - *"Arte mo, nakakainis ka."*
896
+ - *"Mga katulad mo ang dahilan kung bakit hindi makatayo ang Pilipinas."*
897
+ """)
898
+
899
+ with gr.Accordion("Knowledge Invalidation", open=False):
900
+ gr.Markdown("""
901
+ **Definition:** Statements that attack cognitive capacity specifically -
902
+ implying the target is incapable of understanding, reasoning, or making
903
+ valid judgments.
904
+
905
+ **Key linguistic cues:** intelligence insults ("bobo", "tanga",
906
+ "mahina umintindi"), claims the person cannot grasp "simple" ideas.
907
+
908
+ **Examples:**
909
+ - *"Hindi mo kasi naiintindihan, ang bobo mo."*
910
+ - *"Simple lang yan, di mo pa gets."*
911
+ - *"Ang tanga mo naman mag-analisa ng economic data."*
912
+ """)
913
+
914
+ gr.Markdown("""
915
+ ---
916
+ **Decision order (from the annotation codebook):**
917
+ 1. If threats are central - **Coercion & Intimidation**
918
+ 2. Else if denial/rewrite drives the move - **Distortion & Denial**
919
+ 3. Else if downplaying drives the move - **Trivialization & Minimization**
920
+ 4. Else if cognitive attack is specific - **Knowledge Invalidation**
921
+ """)
922
+
923
+ # ── About ─────────────────────────────────────────────────────────
924
+ with gr.Tab("About"):
925
+
926
+ gr.Markdown(f"""
927
+ ## About This System
928
+
929
+ ### Research Context
930
+ Developed as part of a thesis on detecting manipulation in Taglish political discourse.
931
+ The system uses a **sequential classification pipeline**:
932
+ 1. **Binary Classifier** - Gaslighting vs. Non-Gaslighting
933
+ 2. **Tactic Classifier** - identifies the specific tactic (4 classes + Non-Gaslighting)
934
+
935
+ ### Dataset
936
+ | Item | Value |
937
+ |------|-------|
938
+ | Total annotated rows | 2,134 |
939
+ | Inter-annotator kappa (binary) | 0.8133 |
940
+ | Inter-annotator kappa (tactic) | 0.8646 |
941
+ | Gold-standard rows used | 944 |
942
+ | Training / Val / Test split | 662 / 143 / 139 |
943
+ | Tactic balance (train) | 83 per tactic class (perfectly balanced) |
944
+ | Sentence extraction | Key-sentence heuristic (cue words + position) |
945
+ | Sources | r/Philippines, r/PhilippinesPolitics, r/31MillionRegrets |
946
+ | Language | Taglish (Tagalog-English code-switching) |
947
+
948
+ ### Best Model Summary (in-domain test set - v2, balanced dataset)
949
+ | Task | Best Model | Primary Metric | ROC-AUC | Hardest Tactic |
950
+ |------|-----------|---------------|---------|---------------|
951
+ | Binary (Gas-F1) | mBERT | Gas-F1 = 0.7934 | 0.9252 | - |
952
+ | Binary (ROC-AUC) | mBERT | ROC-AUC = 0.9252 | 0.9252 | - |
953
+ | Tactic (Macro-F1) | RoBERTa-Tagalog | Macro-F1 = 0.6111 | - | T&M (F1 = 0.4615) |
954
+
955
+ ### Evaluation Metrics (Section 4.5)
956
+ | Task | Metrics |
957
+ |------|---------|
958
+ | Binary | Gas. Precision, Gas. Recall, Gas. F1, Macro-F1, ROC-AUC, Confusion matrix (2x2) |
959
+ | Tactic | Per-class P / R / F1 (D&D, T&M, C&I, KI), Macro-F1, Confusion matrix (5x5) |
960
+
961
+ Confusion matrices are displayed per model in the Model Performance tab (per-model accordions).
962
+
963
+ ### Technical Details
964
+ - Framework: PyTorch + Hugging Face Transformers
965
+ - Max sequence length: 128 tokens
966
+ - Epochs: up to 8 with early stopping (patience = 3)
967
+ - Optimizer: AdamW, lr = 1e-5, warmup ratio = 0.1
968
+ - Label smoothing: 0.1
969
+ - Checkpoint selection: Gas-F1 (binary), Macro-F1 (tactic)
970
+ - Class-weighted cross-entropy loss
971
+ - Hardware: {Config.DEVICE.upper()}
972
+
973
+ ### Appropriate Uses
974
+ Research on online manipulation, content moderation assistance,
975
+ educational tool, supporting human moderators.
976
+
977
+ ### Limitations
978
+ Trained on political discourse - performance may degrade on other domains.
979
+ Approximately 83 samples per tactic class limits fine-grained detection.
980
+ Not a substitute for human judgment. Cultural nuances may be missed.
981
+
982
+ ### Citation
983
+ ```
984
+ Gomez, Tugado (2026). Transformers for Taglish Political Gaslighting:
985
+ Binary Detection, Tactic Classification, and Zero-Shot Transfer.
986
+ Ateneo De Naga University.
987
+ ```
988
+
989
+ ---
990
+ **Version**: 2.0 - **Updated**: {pd.Timestamp.now().strftime('%B %Y')}
991
+ """)
992
+
993
+ gr.Markdown("""
994
+ ---
995
+ **Disclaimer:** Research prototype. Results should be interpreted carefully and in context.
996
+ Always apply human judgment, especially for content moderation decisions.
997
+ """)
998
+
999
+ return app
1000
+
1001
+
1002
+ # ---------------------------------------------------------------------------
1003
+ # MAIN
1004
+ # ---------------------------------------------------------------------------
1005
+
1006
+ if __name__ == "__main__":
1007
+
1008
+ print("\n" + "=" * 70)
1009
+ print("TAGLISH POLITICAL GASLIGHTING DETECTION - APP")
1010
+ print("=" * 70)
1011
+ print(f"Device : {Config.DEVICE}")
1012
+ print("Models will be loaded from Hugging Face Hub on first use.")
1013
+ print("\nLaunching Gradio ...\n" + "=" * 70)
1014
+
1015
+ app = create_interface()
1016
+ app.launch()
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ gradio>=4.44.0
2
+ torch>=2.3.0
3
+ transformers>=4.44.0
4
+ numpy>=1.26.0
5
+ pandas>=2.2.0