ggapar commited on
Commit
20b4278
Β·
verified Β·
1 Parent(s): ffbda29

Upload 3 files

Browse files
Files changed (3) hide show
  1. README.md +37 -0
  2. app.py +363 -0
  3. requirements.txt +7 -0
README.md ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: DFK Content Classifier
3
+ emoji: πŸ›‘οΈ
4
+ colorFrom: red
5
+ colorTo: gray
6
+ sdk: gradio
7
+ sdk_version: 4.0.0
8
+ app_file: app.py
9
+ pinned: false
10
+ license: apache-2.0
11
+ models:
12
+ - ggapar/Ministral-3-8B-Base-2512-DFK
13
+ tags:
14
+ - text-classification
15
+ - indonesian
16
+ - dfk
17
+ - disinformasi
18
+ - fitnah
19
+ - ujaran-kebencian
20
+ ---
21
+
22
+ # DFK Content Classifier
23
+
24
+ Klasifikasi konten berbahaya Bahasa Indonesia menggunakan **Ministral-3-8B** + LoRA Fine-tuning.
25
+
26
+ ## Kelas
27
+ - 🟒 **Fakta** β€” konten yang sesuai fakta
28
+ - πŸ”΄ **Disinformasi** β€” informasi menyesatkan
29
+ - 🟠 **Fitnah** β€” tuduhan tanpa bukti
30
+ - ⚫ **Ujaran Kebencian** β€” konten menyerang kelompok tertentu
31
+
32
+ ## Fitur
33
+ - βœ… Label klasifikasi
34
+ - βœ… Reasoning / penjelasan
35
+ - βœ… Trust Score via MTLA (Multi-Token Logit Averaging)
36
+ - βœ… Voting consistency dari multiple trials
37
+ - βœ… API endpoint untuk integrasi sistem
app.py ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ DFK Content Classification β€” HuggingFace Spaces (CPU Basic β€” Gratis)
3
+ =====================================================================
4
+ Model : ggapar/Ministral-3-8B-Base-2512-DFK (LoRA adapter)
5
+ Base : mistralai/Ministral-3-8B-Base-2512 (float32, CPU)
6
+ GPU : CPU Basic (gratis, tanpa GPU)
7
+ Catatan: Inference lebih lambat (~2-5 menit/request) karena CPU only
8
+ """
9
+
10
+ import os
11
+ import re
12
+ import gc
13
+ import torch
14
+ import numpy as np
15
+ import gradio as gr
16
+
17
+ from collections import Counter
18
+ from transformers import AutoModelForCausalLM, AutoTokenizer
19
+ from peft import PeftModel
20
+
21
+ # ================================================================
22
+ # KONFIGURASI
23
+ # ================================================================
24
+ BASE_MODEL = "mistralai/Ministral-3-8B-Base-2512"
25
+ ADAPTER_REPO = "ggapar/Ministral-3-8B-Base-2512-DFK"
26
+ HF_TOKEN = os.environ.get("HF_TOKEN", "")
27
+
28
+ SYSTEM_PROMPT = (
29
+ "Anda adalah sistem deteksi konten DFK (Disinformasi, Fitnah, Kebencian). "
30
+ "Klasifikasikan teks ke dalam: Fakta, Disinformasi, Fitnah, atau Ujaran Kebencian. "
31
+ "Berikan label dan penjelasan yang jelas."
32
+ )
33
+
34
+ LABEL_INFO = {
35
+ "fakta" : ("🟒", "#dcfce7", "#166534", "Konten yang sesuai dengan fakta"),
36
+ "disinformasi" : ("πŸ”΄", "#fee2e2", "#991b1b", "Informasi yang menyesatkan"),
37
+ "fitnah" : ("🟠", "#ffedd5", "#9a3412", "Tuduhan tanpa bukti"),
38
+ "ujaran_kebencian": ("⚫", "#f1f5f9", "#1e293b", "Konten menyerang kelompok tertentu"),
39
+ "unknown" : ("βšͺ", "#f8fafc", "#64748b", "Label tidak terdeteksi"),
40
+ }
41
+
42
+ # ================================================================
43
+ # LOAD MODEL β€” di CPU dulu, GPU dialokasikan saat inference
44
+ # Dengan ZeroGPU, model di-load ke CPU saat startup
45
+ # GPU baru dialokasikan saat fungsi @spaces.GPU dipanggil
46
+ # ================================================================
47
+ print("Loading tokenizer...")
48
+ tokenizer = AutoTokenizer.from_pretrained(
49
+ ADAPTER_REPO,
50
+ trust_remote_code = True,
51
+ token = HF_TOKEN or None,
52
+ )
53
+ if tokenizer.pad_token is None:
54
+ tokenizer.pad_token = tokenizer.eos_token
55
+
56
+ print("Loading base model (CPU, float32)...")
57
+ # CPU Basic tidak support bfloat16/4-bit β€” pakai float32
58
+ # Model akan lebih lambat (~2-5 menit/request) tapi tetap fungsional
59
+ base_model = AutoModelForCausalLM.from_pretrained(
60
+ BASE_MODEL,
61
+ torch_dtype = torch.float32, # ← CPU butuh float32
62
+ device_map = "cpu",
63
+ trust_remote_code = True,
64
+ token = HF_TOKEN or None,
65
+ low_cpu_mem_usage = True, # ← hemat RAM saat loading
66
+ )
67
+
68
+ print("Loading LoRA adapter...")
69
+ model = PeftModel.from_pretrained(
70
+ base_model,
71
+ ADAPTER_REPO,
72
+ token = HF_TOKEN or None,
73
+ )
74
+ model.eval()
75
+ print("βœ… Model loaded ke CPU β€” siap inference (estimasi 2-5 menit/request)")
76
+
77
+ # ================================================================
78
+ # HELPER FUNCTIONS
79
+ # ================================================================
80
+ def extract_label(text: str) -> str:
81
+ t = text.lower().strip()
82
+ if "ujaran kebencian" in t[:80] or "ujaran_kebencian" in t[:80]:
83
+ return "ujaran_kebencian"
84
+ m = re.search(r'label\s*:\s*\*{0,2}([\w\s]+?)\*{0,2}[.,]', t)
85
+ if m:
86
+ lbl = m.group(1).strip()
87
+ for kw in ["ujaran kebencian", "disinformasi", "fitnah", "fakta"]:
88
+ if kw in lbl:
89
+ return kw.replace(" ", "_")
90
+ for kw in ["ujaran kebencian", "disinformasi", "fitnah", "fakta"]:
91
+ if kw in t[:80]:
92
+ return kw.replace(" ", "_")
93
+ for kw in ["ujaran kebencian", "disinformasi", "fitnah", "fakta"]:
94
+ if kw in t:
95
+ return kw.replace(" ", "_")
96
+ return "unknown"
97
+
98
+ def extract_reasoning(text: str) -> str:
99
+ m = re.search(r'penjelasan\s*:\s*(.*)', text, re.DOTALL | re.IGNORECASE)
100
+ if m:
101
+ return m.group(1).strip()
102
+ lines = text.strip().split('\n')
103
+ return ' '.join(lines[1:]).strip() if len(lines) > 1 else text.strip()
104
+
105
+ def compute_mtla_confidence(scores_list, gen_ids, K: int = 10) -> float:
106
+ K_act = min(K, len(scores_list), len(gen_ids))
107
+ log_probs = []
108
+ for t in range(K_act):
109
+ probs = torch.softmax(scores_list[t], dim=-1)
110
+ tok_prob = probs[0, gen_ids[t].item()].item()
111
+ log_probs.append(np.log(max(tok_prob, 1e-10)))
112
+ avg_lp = float(np.mean(log_probs))
113
+ return round(float(1.0 / (1.0 + np.exp(-(avg_lp + 2.5) * 1.5))), 4)
114
+
115
+ # ================================================================
116
+ # FUNGSI INFERENCE β€” decorator @spaces.GPU wajib untuk ZeroGPU
117
+ # GPU dialokasikan hanya saat fungsi ini dipanggil
118
+ # ================================================================
119
+ def classify_dfk(text: str, num_trials: int, temperature: float):
120
+ if not text or not text.strip():
121
+ return ("β€”", "0%", "β€”", "β€”", "β€”",
122
+ "Masukkan teks yang ingin diklasifikasi.", [], "")
123
+
124
+ device = "cpu"
125
+
126
+ messages = [
127
+ {"role": "system", "content": SYSTEM_PROMPT},
128
+ {"role": "user", "content": f"Klasifikasikan konten berikut:\n{text}"},
129
+ ]
130
+ prompt = tokenizer.apply_chat_template(
131
+ messages, tokenize=False, add_generation_prompt=True
132
+ )
133
+
134
+ inputs = tokenizer(
135
+ [prompt] * int(num_trials),
136
+ return_tensors = "pt",
137
+ padding = True,
138
+ truncation = True,
139
+ max_length = 1900,
140
+ ).to(device)
141
+
142
+ with torch.inference_mode():
143
+ out = model.generate(
144
+ **inputs,
145
+ max_new_tokens = 256,
146
+ temperature = float(temperature),
147
+ do_sample = True,
148
+ return_dict_in_generate = True,
149
+ output_scores = True,
150
+ use_cache = True,
151
+ )
152
+
153
+ # Kumpulkan hasil per trial
154
+ trials = []
155
+ for i in range(int(num_trials)):
156
+ gen_ids = out.sequences[i][inputs.input_ids.shape[1]:]
157
+ gen_text = tokenizer.decode(gen_ids, skip_special_tokens=True)
158
+ scores_i = [s[i:i+1] for s in out.scores]
159
+ conf = compute_mtla_confidence(scores_i, gen_ids, K=10)
160
+ trials.append({
161
+ "label" : extract_label(gen_text),
162
+ "reasoning": extract_reasoning(gen_text),
163
+ "confidence": conf,
164
+ })
165
+
166
+ # Voting
167
+ vote = Counter(t["label"] for t in trials)
168
+ best_label, count = vote.most_common(1)[0]
169
+ winners = [t for t in trials if t["label"] == best_label]
170
+ avg_conf = float(np.mean([t["confidence"] for t in winners]))
171
+ best_reason = max(winners, key=lambda x: x["confidence"])["reasoning"]
172
+ is_ambiguous = count == 1 or avg_conf < 0.45
173
+
174
+ emoji, bg, fg, desc = LABEL_INFO.get(best_label, LABEL_INFO["unknown"])
175
+ label_display = f"{emoji} {best_label.upper().replace('_', ' ')}"
176
+ conf_pct = f"{avg_conf * 100:.1f}%"
177
+ consistency = f"{count}/{int(num_trials)}"
178
+ ambig_status = "⚠️ Ambigu β€” model ragu-ragu" if is_ambiguous else "βœ… Model yakin"
179
+
180
+ label_html = f"""
181
+ <div style="
182
+ background:{bg}; color:{fg};
183
+ padding:12px 24px; border-radius:12px;
184
+ font-size:1.4rem; font-weight:700;
185
+ text-align:center; display:inline-block;
186
+ border: 2px solid {fg}30; margin:8px 0;
187
+ ">
188
+ {emoji} {best_label.upper().replace('_', ' ')}
189
+ </div>
190
+ """
191
+
192
+ trial_data = [
193
+ [
194
+ f"Trial {i+1}",
195
+ f"{LABEL_INFO.get(t['label'], LABEL_INFO['unknown'])[0]} "
196
+ f"{t['label'].upper().replace('_', ' ')}",
197
+ f"{t['confidence'] * 100:.1f}%",
198
+ t['reasoning'][:150] + "..." if len(t['reasoning']) > 150 else t['reasoning'],
199
+ ]
200
+ for i, t in enumerate(trials)
201
+ ]
202
+
203
+ gc.collect()
204
+ if torch.cuda.is_available():
205
+ torch.cuda.empty_cache()
206
+
207
+ return (
208
+ label_display, conf_pct, consistency,
209
+ ambig_status, desc, best_reason,
210
+ trial_data, label_html,
211
+ )
212
+
213
+ # ================================================================
214
+ # GRADIO UI
215
+ # ================================================================
216
+ css = """
217
+ .gradio-container { max-width: 900px !important; margin: auto; }
218
+ footer { display: none !important; }
219
+ """
220
+
221
+ with gr.Blocks(
222
+ title = "DFK Content Classifier",
223
+ theme = gr.themes.Soft(primary_hue="red", neutral_hue="slate"),
224
+ css = css,
225
+ ) as demo:
226
+
227
+ gr.HTML("""
228
+ <div style="text-align:center;padding:1.5rem 0 0.5rem">
229
+ <h1 style="font-size:2rem;font-weight:800;color:#1e293b;margin:0">
230
+ πŸ›‘οΈ DFK Content Classifier
231
+ </h1>
232
+ <p style="color:#64748b;margin:8px 0 4px">
233
+ Deteksi Disinformasi Β· Fitnah Β· Ujaran Kebencian Β· Fakta
234
+ </p>
235
+ <p style="color:#94a3b8;font-size:0.85rem;margin:0">
236
+ Model: <b>Ministral-3-8B</b> + LoRA Fine-tuning Β· Bahasa Indonesia
237
+ </p>
238
+ <div style="background:#fef9c3;color:#854d0e;padding:6px 16px;border-radius:8px;font-size:0.82rem;margin:8px auto;display:inline-block">
239
+ ⏳ CPU Mode β€” estimasi waktu inference: 2-5 menit per request
240
+ </div>
241
+ <div style="display:flex;justify-content:center;gap:8px;margin:12px 0;flex-wrap:wrap">
242
+ <span style="background:#dcfce7;color:#166534;padding:3px 12px;border-radius:20px;font-size:0.82rem">🟒 Fakta</span>
243
+ <span style="background:#fee2e2;color:#991b1b;padding:3px 12px;border-radius:20px;font-size:0.82rem">πŸ”΄ Disinformasi</span>
244
+ <span style="background:#ffedd5;color:#9a3412;padding:3px 12px;border-radius:20px;font-size:0.82rem">🟠 Fitnah</span>
245
+ <span style="background:#f1f5f9;color:#1e293b;padding:3px 12px;border-radius:20px;font-size:0.82rem">⚫ Ujaran Kebencian</span>
246
+ </div>
247
+ </div>
248
+ """)
249
+
250
+ with gr.Row():
251
+ with gr.Column(scale=2):
252
+ text_input = gr.Textbox(
253
+ label = "πŸ“ Teks yang ingin diklasifikasi",
254
+ placeholder = "Masukkan klaim, berita, atau konten...",
255
+ lines = 5,
256
+ )
257
+ with gr.Row():
258
+ num_trials = gr.Slider(
259
+ minimum = 1, maximum = 5, value = 3, step = 1,
260
+ label = "Jumlah Trial",
261
+ info = "Lebih banyak = lebih akurat tapi lebih lambat",
262
+ )
263
+ temperature = gr.Slider(
264
+ minimum = 0.1, maximum = 1.0, value = 0.7, step = 0.1,
265
+ label = "Temperature",
266
+ info = "0.1 = deterministik, 1.0 = kreatif",
267
+ )
268
+ classify_btn = gr.Button(
269
+ "πŸ” Klasifikasi Sekarang",
270
+ variant = "primary",
271
+ size = "lg",
272
+ )
273
+
274
+ with gr.Column(scale=1):
275
+ gr.Examples(
276
+ examples = [
277
+ ["Air rebusan bawang putih bisa menyembuhkan virus COVID dalam 24 jam!"],
278
+ ["BPOM mengkonfirmasi vaksin COVID-19 sudah melalui uji klinis tiga fase sesuai standar WHO."],
279
+ ["Gubernur X terbukti korupsi dana bansos, ada bukti transfer ke rekening keluarganya."],
280
+ ["Orang dari suku X itu memang tidak bisa dipercaya dan selalu bikin masalah."],
281
+ ],
282
+ inputs = text_input,
283
+ label = "πŸ’‘ Contoh Teks",
284
+ )
285
+
286
+ gr.HTML("<hr style='margin:1rem 0;border-color:#e2e8f0'>")
287
+
288
+ label_html_out = gr.HTML()
289
+
290
+ with gr.Row():
291
+ label_out = gr.Textbox(
292
+ label="🏷️ Label", interactive=False,
293
+ )
294
+ conf_out = gr.Textbox(
295
+ label="🎯 Trust Score (MTLA)", interactive=False,
296
+ info="Keyakinan model via Multi-Token Logit Averaging",
297
+ )
298
+ consistency_out = gr.Textbox(
299
+ label="πŸ—³οΈ Konsistensi", interactive=False,
300
+ )
301
+ ambig_out = gr.Textbox(
302
+ label="πŸ“Š Status", interactive=False,
303
+ )
304
+
305
+ desc_out = gr.Textbox(
306
+ label="πŸ“– Deskripsi Label", interactive=False,
307
+ )
308
+ reasoning_out = gr.Textbox(
309
+ label="πŸ’¬ Reasoning Model", lines=4, interactive=False,
310
+ info="Penjelasan model tentang keputusannya",
311
+ )
312
+
313
+ with gr.Accordion("πŸ”¬ Detail Per Trial", open=False):
314
+ trial_table = gr.Dataframe(
315
+ headers = ["Trial", "Label", "Trust Score", "Reasoning"],
316
+ wrap = True,
317
+ )
318
+
319
+ with gr.Accordion("πŸ”Œ Cara Pakai via API", open=False):
320
+ gr.Markdown("""
321
+ ### Python
322
+ ```python
323
+ from gradio_client import Client
324
+
325
+ client = Client("ggapar/dfk-classifier")
326
+ result = client.predict(
327
+ text = "Teks yang ingin dicek",
328
+ num_trials = 3,
329
+ temperature = 0.7,
330
+ api_name = "/classify_dfk"
331
+ )
332
+ # result[0] = Label, result[1] = Trust Score, result[5] = Reasoning
333
+ print(result[0], result[1], result[5])
334
+ ```
335
+
336
+ ### Install
337
+ ```bash
338
+ pip install gradio_client
339
+ ```
340
+ """)
341
+
342
+ gr.HTML("""
343
+ <div style="text-align:center;color:#94a3b8;font-size:0.78rem;margin-top:1rem">
344
+ Model: <a href="https://huggingface.co/ggapar/Ministral-3-8B-Base-2512-DFK"
345
+ target="_blank">ggapar/Ministral-3-8B-Base-2512-DFK</a> Β·
346
+ AITF Team 2025
347
+ </div>
348
+ """)
349
+
350
+ outputs = [
351
+ label_out, conf_out, consistency_out,
352
+ ambig_out, desc_out, reasoning_out,
353
+ trial_table, label_html_out,
354
+ ]
355
+ classify_btn.click(fn=classify_dfk,
356
+ inputs=[text_input, num_trials, temperature],
357
+ outputs=outputs)
358
+ text_input.submit(fn=classify_dfk,
359
+ inputs=[text_input, num_trials, temperature],
360
+ outputs=outputs)
361
+
362
+ if __name__ == "__main__":
363
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ torch>=2.0.0
2
+ transformers>=4.40.0
3
+ peft>=0.10.0
4
+ gradio>=4.0.0
5
+ numpy>=1.24.0
6
+ accelerate>=0.27.0
7
+ gradio_client>=0.6.0