Model 3B untuk masking data pribadi (NAMA, NIK, NOMOR_HP) di teks administratif Bahasa Indonesia. Fine-tuned dari Qwen2.5-3B-Instruct dengan SFT + DPO (QLoRA 4-bit). Adapter only - butuh base model Qwen/Qwen2.5-3B-Instruct.
Diberikan teks Bahasa Indonesia yang mengandung PII, model mengembalikan teks yang sama dengan setiap PII diganti tag:
1import torch
2from peft import PeftModel
3from transformers import AutoModelForCausalLM, AutoTokenizer
4
5# 1. Load base model (fp16 - ~6GB VRAM)
6base = AutoModelForCausalLM.from_pretrained(
7 "Qwen/Qwen2.5-3B-Instruct",
8 torch_dtype=torch.float16,
9 device_map="auto",
10)
11tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-3B-Instruct")
12
13# 2. Load DPO adapter
14model = PeftModel.from_pretrained(base, "qrizan/pii-masking-qwen2.5-3b-dpo_beta0_5")
15model.eval()
16
17# 3. System prompt
18SYSTEM_PROMPT = (
19 "Anda adalah sistem masking PII (data pribadi) untuk teks Bahasa Indonesia.\n"
20 "Tugas Anda: kembalikan teks yang SAMA PERSIS, tetapi ganti setiap data pribadi "
21 "dengan tag yang sesuai.\n\n"
22 "Aturan masking:\n"
23 "- Nama orang -> [NAMA]\n"
24 "- NIK (16 digit) -> [NIK]\n"
25 "- Nomor HP (diawali 08) -> [NOMOR_HP]\n\n"
26 "JANGAN di-mask (biarkan apa adanya): nomor rekening bank, nomor transaksi, "
27 "kode referensi, dan angka lain yang bukan data pribadi.\n\n"
28 "Ketentuan keluaran:\n"
29 "- Keluarkan HANYA teks hasil masking.\n"
30 "- Jangan tambahkan kalimat pengantar, penjelasan, catatan, atau tanda kutip.\n"
31 "- Pertahankan seluruh kata, tanda baca, dan spasi lain seperti aslinya.\n"
32 "- Jika sebuah data pribadi ditulis dengan spasi/pemisah di tengah digit, tetap "
33 "ganti seluruhnya dengan satu tag."
34)
35
36def mask_pii(text: str, max_new_tokens: int = 256) -> str:
37 messages = [
38 {"role": "system", "content": SYSTEM_PROMPT},
39 {"role": "user", "content": f"Mask PII pada teks berikut:\n{text}"},
40 ]
41 inputs = tokenizer.apply_chat_template(
42 messages, tokenize=True, add_generation_prompt=True,
43 return_tensors="pt", return_dict=True,
44 ).to(model.device)
45
46 with torch.no_grad():
47 output_ids = model.generate(
48 **inputs,
49 max_new_tokens=max_new_tokens,
50 do_sample=False,
51 pad_token_id=tokenizer.eos_token_id,
52 )
53
54 prompt_len = inputs["input_ids"].shape[1]
55 result = tokenizer.decode(output_ids[0][prompt_len:], skip_special_tokens=True)
56 return result.strip()
57
58
59# Contoh
60print(mask_pii("Halo, saya Budi Santoso, NIK 3273010101010001, HP 081234567890."))
61# Output: Halo, saya [NAMA], NIK [NIK], HP [NOMOR_HP].
62
63print(mask_pii("Transfer ke rekening 1234567890 atas nama Siti Rahayu."))
64# Output: Transfer ke rekening 1234567890 atas nama [NAMA].
65# (Nomor rekening tidak di-mask - ini distraktor, bukan PII)
DPO menurunkan over-mask di template glued-text dari 100% → 0%, tapi 3/7 record masih punya tag hallucination minor (mis. [NAMA] → [FAMA]) - bukan leak literal, tapi tidak exact match. Tidak ada regresi di 12 template lain (non-holdout tetap 100% exact match).