Upload pusula_perturb.py with huggingface_hub
Browse files- pusula_perturb.py +150 -0
pusula_perturb.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
pusula_perturb.py — Turkce sorgular icin kontrollu, tekrarlanabilir bozulmalar.
|
| 3 |
+
|
| 4 |
+
Her bozulma: f(text, rng, **params) -> text
|
| 5 |
+
Rastgelelik sorgu kimligi + bozulma adindan turetilir: calisma sirasi sonucu degistirmez.
|
| 6 |
+
"""
|
| 7 |
+
import hashlib
|
| 8 |
+
import re
|
| 9 |
+
|
| 10 |
+
import numpy as np
|
| 11 |
+
|
| 12 |
+
# ═══ Turkce harf yardimcilari (uzunluk koruyan) ═══
|
| 13 |
+
ASCII_MAP = str.maketrans({
|
| 14 |
+
"ç": "c", "Ç": "C", "ğ": "g", "Ğ": "G", "ı": "i", "İ": "I",
|
| 15 |
+
"ö": "o", "Ö": "O", "ş": "s", "Ş": "S", "ü": "u", "Ü": "U",
|
| 16 |
+
"â": "a", "Â": "A", "î": "i", "Î": "I", "û": "u", "Û": "U",
|
| 17 |
+
})
|
| 18 |
+
DIACRITICS = set("çÇğĞıİöÖşŞüÜâÂîÎûÛ")
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def tr_lower(s):
|
| 22 |
+
return s.replace("I", "ı").replace("İ", "i").lower()
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def tr_upper(s):
|
| 26 |
+
return s.replace("i", "İ").replace("ı", "I").upper()
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
# ═══ Turkce Q klavye komsuluklari (yazim hatasi icin) ═══
|
| 30 |
+
_ROWS = ["qwertyuıopğü", "asdfghjklşi", "zxcvbnmöç"]
|
| 31 |
+
KEY_NEIGHBORS = {}
|
| 32 |
+
for r, row in enumerate(_ROWS):
|
| 33 |
+
for c, ch in enumerate(row):
|
| 34 |
+
nb = set()
|
| 35 |
+
for dr in (-1, 0, 1):
|
| 36 |
+
rr = r + dr
|
| 37 |
+
if 0 <= rr < len(_ROWS):
|
| 38 |
+
for dc in (-1, 0, 1):
|
| 39 |
+
cc = c + dc
|
| 40 |
+
if (dr or dc) and 0 <= cc < len(_ROWS[rr]):
|
| 41 |
+
nb.add(_ROWS[rr][cc])
|
| 42 |
+
KEY_NEIGHBORS[ch] = sorted(nb)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def make_rng(query_id, perturbation, seed=0):
|
| 46 |
+
h = hashlib.sha256(f"{seed}|{perturbation}|{query_id}".encode()).digest()
|
| 47 |
+
return np.random.default_rng(int.from_bytes(h[:8], "little"))
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
# ═══ Bozulmalar ═══
|
| 51 |
+
def ascii_full(text, rng=None):
|
| 52 |
+
"""Tum Turkce karakterler ASCII karsiligina (Ingilizce klavye)."""
|
| 53 |
+
return text.translate(ASCII_MAP)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def ascii_partial(text, rng, p=0.5):
|
| 57 |
+
"""Her Turkce karakter p olasilikla ASCII'ye (karisik yazim)."""
|
| 58 |
+
return "".join(ch.translate(ASCII_MAP) if ch in DIACRITICS and rng.random() < p else ch for ch in text)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def upper(text, rng=None):
|
| 62 |
+
"""Turkce klavyeyle BUYUK HARF."""
|
| 63 |
+
return tr_upper(text)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def _typo_word(word, rng):
|
| 67 |
+
if len(word) < 4:
|
| 68 |
+
return word
|
| 69 |
+
i = int(rng.integers(1, len(word))) # ilk harfe dokunma
|
| 70 |
+
op = rng.choice(["sub", "del", "swap", "dup"])
|
| 71 |
+
ch = word[i]
|
| 72 |
+
if op == "sub":
|
| 73 |
+
nbs = KEY_NEIGHBORS.get(tr_lower(ch))
|
| 74 |
+
if not nbs:
|
| 75 |
+
return word
|
| 76 |
+
rep = rng.choice(nbs)
|
| 77 |
+
rep = tr_upper(rep) if ch != tr_lower(ch) else rep
|
| 78 |
+
return word[:i] + rep + word[i + 1:]
|
| 79 |
+
if op == "del":
|
| 80 |
+
return word[:i] + word[i + 1:]
|
| 81 |
+
if op == "swap" and i < len(word) - 1:
|
| 82 |
+
return word[:i] + word[i + 1] + word[i] + word[i + 2:]
|
| 83 |
+
return word[:i] + ch + word[i:] # dup (swap son harfteyse de buraya duser)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def typo(text, rng, rate=0.2):
|
| 87 |
+
"""Kelime basina rate olasilikla tek bir klavye hatasi (komsu tus, silme, yer degistirme, cift basma)."""
|
| 88 |
+
return re.sub(r"\w+", lambda m: _typo_word(m.group(0), rng) if rng.random() < rate else m.group(0), text)
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
# Gundelik yazim: -yor + kisi eki birlikte kisalir (kullaniyorum -> kullaniyom, geliyorsun -> geliyosun)
|
| 92 |
+
_YOR_SUFFIX = {"": "", "um": "m", "uz": "z", "sun": "sun", "sunuz": "sunuz", "lar": "lar"}
|
| 93 |
+
|
| 94 |
+
# (desen, yerine [str veya fonksiyon], aciklama). Her eslesme rate olasilikla uygulanir.
|
| 95 |
+
COLLOQUIAL_RULES = [
|
| 96 |
+
(r"\bnerede\b", "nerde", "nerede → nerde"),
|
| 97 |
+
(r"\bneredeyse\b", "nerdeyse", "neredeyse → nerdeyse"),
|
| 98 |
+
(r"\bbir\b", "bi", "bir → bi"),
|
| 99 |
+
(r"\bdeğil\b", "diil", "değil → diil"),
|
| 100 |
+
(r"\bne yapacağım\b", "napcam", "ne yapacağım → napcam"),
|
| 101 |
+
(r"\bne yapıyorsun\b", "napıyosun", "ne yapıyorsun → napıyosun"),
|
| 102 |
+
(r"(\w+?[ıiuü])yor(sunuz|sun|um|uz|lar)?\b",
|
| 103 |
+
lambda m: m.group(1) + "yo" + _YOR_SUFFIX[m.group(2) or ""], "-yor(um) → -yo(m)"),
|
| 104 |
+
(r"(\w+)acağım\b", r"\1acam", "-acağım → -acam"),
|
| 105 |
+
(r"(\w+)eceğim\b", r"\1ecem", "-eceğim → -ecem"),
|
| 106 |
+
(r"(\w+)\s(m[ıiuü](?:s[ıiuü]n|y[ıiuü]m|d[ıiuü]r)?)\b", r"\1\2", "soru eki bitişik (var mı → varmı)"),
|
| 107 |
+
(r"\b(ben|sen|biz|siz|o|onlar)\s(de|da)\b", r"\1\2", "zamir + de/da bitişik (ben de → bende)"),
|
| 108 |
+
]
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def _keep_case(original, replaced):
|
| 112 |
+
"""Eslesmenin ilk harfi buyukse sonucun ilk harfini de buyut (Nerede -> Nerde)."""
|
| 113 |
+
if original[:1] and original[:1] != tr_lower(original[:1]) and replaced:
|
| 114 |
+
return tr_upper(replaced[0]) + replaced[1:]
|
| 115 |
+
return replaced
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def colloquial(text, rng, rate=0.8):
|
| 119 |
+
out = text
|
| 120 |
+
for pattern, repl, _ in COLLOQUIAL_RULES:
|
| 121 |
+
def sub(m, repl=repl):
|
| 122 |
+
if rng.random() >= rate:
|
| 123 |
+
return m.group(0)
|
| 124 |
+
new = repl(m) if callable(repl) else m.expand(repl)
|
| 125 |
+
return _keep_case(m.group(0), new)
|
| 126 |
+
out = re.sub(pattern, sub, out, flags=re.IGNORECASE)
|
| 127 |
+
return out
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def mobile(text, rng, typo_rate=0.15):
|
| 131 |
+
"""Aceleyle Ingilizce klavyeli telefondan yazilmis sorgu: kucuk harf + gundelik + hata + ASCII.
|
| 132 |
+
ASCII en SONDA: yazim hatasi Turkce komsu tus (ğ, ş) uretse bile cikti tutarli bicimde ASCII olur."""
|
| 133 |
+
out = colloquial(tr_lower(text), rng)
|
| 134 |
+
return ascii_full(typo(out, rng, rate=typo_rate))
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
PERTURBATIONS = {
|
| 138 |
+
"clean": (lambda t, rng: t, "Temiz (referans)"),
|
| 139 |
+
"ascii_full": (lambda t, rng: ascii_full(t), "Tüm Türkçe karakterler ASCII"),
|
| 140 |
+
"ascii_partial": (lambda t, rng: ascii_partial(t, rng), "Türkçe karakterlerin yarısı ASCII"),
|
| 141 |
+
"upper": (lambda t, rng: upper(t), "BÜYÜK HARF"),
|
| 142 |
+
"typo": (lambda t, rng: typo(t, rng), "Kelime başına %20 klavye hatası"),
|
| 143 |
+
"colloquial": (lambda t, rng: colloquial(t, rng), "Gündelik yazım (nerde, geliyo, varmı)"),
|
| 144 |
+
"mobile": (lambda t, rng: mobile(t, rng), "Hepsi: küçük harf + gündelik + ASCII + hata"),
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def perturb(text, query_id, name, seed=0):
|
| 149 |
+
fn, _ = PERTURBATIONS[name]
|
| 150 |
+
return fn(text, make_rng(query_id, name, seed))
|