lhc-0-brain / code /features_v3.py
sayed125's picture
update code/features_v3.py (post-review release)
b3142d8 verified
Raw History Blame Contribute Delete
9.16 kB
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""features_v3.py — تجزئة متجهة متكافئة بايت-بايت مع phase_p2_slot.hashed_features_slot.
الاستراتيجية (بدل MD5 لكل ميزة في كل استدعاء):
1) مخزون لكل كلمة: وحداتها + جميع ثلاثيات أحرفها (u: و c:) بمصطلحات (bucket, ±sign).
2) مخزون لكل ثنائية كلمات (b:) — المفتاح نصي "t1\x00t2".
3) مخزون لكل رقم للنوافذ N:<num>#0/1.
4) التجميع بnumpy: np.unique(return_index) لترتيب أول-ظهور + np.bincount للمجاميع
(القيم كلها صحيحة: مجموع إشارات ±1 ⇒ مطابقة تامة بلا أخطاء عائمة).
أمان الترتيب: مرحلتان (أساس ثم أرقام) مطابقةً لدلالات الدالة الأصلية (التي تُسقط
الأصفار الأساسية قبل إضافة ميزات الأرقام).
"""
import hashlib
import re
import numpy as np
_FIXED_F = 1 << 15
_TOK = re.compile(r"\w+", re.UNICODE)
_NUM = re.compile(r"\d+")
_TOK_CACHE = {}
_BI_CACHE = {}
_NUM_CACHE = {}
def _h32(f):
"""int(md5(f).hexdigest()[:8], 16) — نفس القيمة بقراءة digest مباشرة."""
return int.from_bytes(hashlib.md5(f.encode()).digest()[:4], "big")
def _entry(feats, F):
"""يبني تسلسل (b،s) مسطّحًا كقائمة [b0,s0,b1,s1,...] لاستدعاء extend واحدة."""
out = []
for f in feats:
h = _h32(f)
out.append(h % F)
out.append(1 if (h >> 16) & 1 else -1)
return out
def _tok_entry(t, F):
"""[وحدات u:t، ثلاثيات #t#، قاموس الثنائيات التالية] — واحد لأن مستهلكًا متسلسلًا."""
u = _entry(["u:" + t], F)
w = "#" + t + "#"
cs = []
for j in range(len(w) - 2):
cs += _entry(["c:" + w[j:j + 3]], F)
return [u, cs, {}]
def _bi_entry(t1, t2, F):
return _entry(["b:" + t1 + "_" + t2], F)
def _num_entry(t, F):
return _entry(["N:" + t + "#0", "N:" + t + "#1"], F)
def _accumulate(flat):
"""flat = [b0,s0,b1,s1,...] → (idx, val) بترتيب أول-ظهور، متطابق مع dict الأصلي."""
B = np.array(flat[0::2], dtype=np.int64)
S = np.array(flat[1::2], dtype=np.int64)
if B.size == 0:
return np.array([], dtype=np.int64), np.array([], dtype=np.float32)
u, first = np.unique(B, return_index=True)
order = np.argsort(first, kind="stable")
idx = u[order]
vals = np.bincount(B, weights=S)[idx]
nz = vals != 0
return idx[nz].astype(np.int64), vals[nz].astype(np.float32)
def hashed_features_slot_v3(text, F=2 ** 15):
"""بديل متكافئ بايت-بايت لـhashed_features_slot (نفس الترتيب ونفس القيم)."""
if F != _FIXED_F:
raise ValueError("features_v3 مُحسَّنة لـF=32768 فقط (قيمة كل الأوزان المنشورة)")
toks = _TOK.findall(text.lower())
flat = []
ext = flat.extend
tc = _TOK_CACHE
for i, t in enumerate(toks):
e = tc.get(t)
if e is None:
e = _tok_entry(t, F)
tc[t] = e
ext(e[0]) # u:t
if i + 1 < len(toks):
bd = e[2]
nxt = toks[i + 1]
e2 = bd.get(nxt)
if e2 is None:
e2 = _bi_entry(t, nxt, F)
bd[nxt] = e2
ext(e2) # b: (قبل الأحرف — كالأصل)
ext(e[1]) # c:#t#[...]
idx, val = _accumulate(flat)
nums = _NUM.findall(text)
if not nums:
return idx, val
# المرحلة 2: مطابقة دلالات الأصل (إسقاط الأصفار الأساسية قبل إضافة N:)
flat2 = [0] * (2 * len(idx))
flat2[0::2] = idx.tolist()
flat2[1::2] = val.tolist()
ext2 = flat2.extend
nc = _NUM_CACHE
for t in nums:
e = nc.get(t)
if e is None:
e = _num_entry(t, F)
nc[t] = e
ext2(e)
return _accumulate(flat2)
def cache_stats():
return {"tokens": len(_TOK_CACHE), "bigrams": len(_BI_CACHE), "numbers": len(_NUM_CACHE)}
def clear_caches():
_TOK_CACHE.clear(); _BI_CACHE.clear(); _NUM_CACHE.clear()
# ------------------------------------------------------------------ المُرمِّز الدفعي
def flat_all_with_counts(texts):
"""قائمة مسطّحة واحدة [b0,s0,b1,s1,...] لكل النصوص + عدّ الميزات لكل نص."""
flat = []
counts = np.empty(len(texts), dtype=np.int64)
tc = _TOK_CACHE
for idx, text in enumerate(texts):
mark = len(flat)
ext = flat.extend
toks = _TOK.findall(text.lower())
n = len(toks)
for i, t in enumerate(toks):
e = tc.get(t)
if e is None:
e = _tok_entry(t, _FIXED_F)
tc[t] = e
ext(e[0])
if i + 1 < n:
bd = e[2]
nxt = toks[i + 1]
e2 = bd.get(nxt)
if e2 is None:
e2 = _bi_entry(t, nxt, _FIXED_F)
bd[nxt] = e2
ext(e2)
ext(e[1])
counts[idx] = (len(flat) - mark) >> 1
return flat, counts
def _accumulate_flat(flat, counts):
"""تراكم دفعة كاملة من قائمة مسطّحة واحدة: يُرجع (text, bucket, value) مرتّبة
حسب (النص، أول-ظهور) — مكافئ رياضيًا لتجميع كل نص على حدة (القيم صحيحة)."""
if not flat:
z64 = np.empty(0, np.int64); z32 = np.empty(0, np.float32)
return z64, z64.copy(), z32
arr = np.fromiter(flat, dtype=np.int64, count=len(flat))
B = arr[0::2].astype(np.int32)
S = arr[1::2]
T = np.repeat(np.arange(len(counts), dtype=np.int32), counts)
key = T.astype(np.int64) * _FIXED_F + B
sorter = np.argsort(key, kind="stable")
sk = key[sorter]
fm = np.empty(sk.size, dtype=bool)
fm[0] = True
np.not_equal(sk[1:], sk[:-1], out=fm[1:])
u = sk[fm]
first = sorter[fm]
sums = np.add.reduceat(S[sorter], np.flatnonzero(fm))
order = np.lexsort((first, u // _FIXED_F))
return (u // _FIXED_F)[order], (u % _FIXED_F)[order], sums[order].astype(np.float32)
CHUNK = 4000 # حد أقصى للنصوص في الدفعة الواحدة (ذاكرة محكومة)
def encode_batch_v3(enc, texts):
"""بديل دفعي متكافئ بايت-بايت لـenc.encode_batch (يعيد مصفوفة (n، d) مُطبَّعة).
يعالج على دفعات CHUNK نصًا للحفاظ على الذاكرة — المخزون يستمر عبر الدفعات."""
if int(enc.F) != _FIXED_F:
raise ValueError("features_v3 مُحسَّنة لـF=32768")
if len(texts) > CHUNK:
parts = [encode_batch_v3(enc, texts[i:i + CHUNK]) for i in range(0, len(texts), CHUNK)]
return np.concatenate(parts, axis=0)
n = len(texts)
W = enc.W
d = enc.d
flat, counts = flat_all_with_counts(texts)
t_s, b_s, v_s = _accumulate_flat(flat, counts)
m = v_s != 0
t_s, b_s, v_s = t_s[m], b_s[m], v_s[m]
counts = np.bincount(t_s, minlength=n)
offs = np.zeros(n + 1, dtype=np.int64)
np.cumsum(counts, out=offs[1:])
num_texts = [i for i in range(n) if _NUM.search(texts[i])]
if num_texts:
flat2 = []
c2 = np.empty(len(num_texts), dtype=np.int64)
for j, i in enumerate(num_texts):
mark = len(flat2)
a, bnd = int(offs[i]), int(offs[i + 1])
tlen = bnd - a
if tlen:
seg = np.empty(2 * tlen, dtype=np.int64)
seg[0::2] = b_s[a:bnd]
seg[1::2] = v_s[a:bnd]
flat2.extend(seg.tolist())
ex = flat2.extend
for tk in _NUM.findall(texts[i]):
e = _NUM_CACHE.get(tk)
if e is None:
e = _num_entry(tk, _FIXED_F)
_NUM_CACHE[tk] = e
ex(e)
c2[j] = (len(flat2) - mark) // 2
t2s, b2s, v2s = _accumulate_flat(flat2, c2)
m2 = v2s != 0
t2s, b2s, v2s = t2s[m2], b2s[m2], v2s[m2]
counts2 = np.bincount(t2s, minlength=len(num_texts))
offs2 = np.zeros(len(num_texts) + 1, dtype=np.int64)
np.cumsum(counts2, out=offs2[1:])
loc = {i: j for j, i in enumerate(num_texts)}
else:
loc = {}
V = np.zeros((n, d), dtype=np.float32)
for i in range(n):
if i in loc:
j = loc[i]
a, bnd = int(offs2[j]), int(offs2[j + 1])
ii, vv = b2s[a:bnd], v2s[a:bnd]
else:
a, bnd = int(offs[i]), int(offs[i + 1])
ii, vv = b_s[a:bnd], v_s[a:bnd]
if ii.size == 0:
continue
z = vv @ W[ii]
nz = float(np.linalg.norm(z))
V[i] = (z / nz).astype(np.float32) if nz > 0 else z.astype(np.float32)
return V