AUBIN-E4B-Web / code /aubin /learn.py
emrevrg's picture
AUBIN-Learn: instant self-learning memory, fast skills, self-calibration + reports
d12b611 verified
Raw History Blame Contribute Delete
18.9 kB
"""AUBIN-Learn — Norovox öz-öğrenme çekirdeğinin karar modeline entegrasyonu.
Norovox Alpha Mini ilkesi: bilgi ağırlıkta değil, DIŞ BELLEKTE tutulur; model düşünür, bellek hatırlar.
Burada bu ilke AUBIN'in hızlı karar sürecine taşınır:
learn(...) doğrulanmış bir vakayı belleğe yazar — gradyan yok, eğitim yok, ~milisaniye; bir sonraki soruda etkili.
recall(...) aynı soru tipindeki (kaynak + seçenek kümesi) en benzer çözülmüş vakalardan seçeneklere oy dağılımı.
fuse(...) modelin log-olasılıkları + bellek oyları (log-doğrusal); ağırlık kaynak-başına, YALNIZ dev'de ayarlanır.
AubinLearner modeli sarar: emin değilse belleğe bakar; bellekte karşılığı yoksa araştırma kancası (Norovox
Retrieval: yerel bilgi tabanı → web) kanıt getirir; geri bildirim gelince learn() ile eksiğini kapatır.
Gömme (embedding) takılabilir: varsayılan sözcük+karakter n-gram karma vektörü (bağımlılıksız, CPU'da µs-ms),
istenirse cümle gömme modeli (ör. BAAI/bge-small-en-v1.5) `embed_fn` olarak verilir.
"""
from __future__ import annotations
import hashlib
import math
import re
import time
from collections import defaultdict
import numpy as np
DIM = int(__import__("os").environ.get("AUBIN_HASH_DIM", 2 ** 13))
def hash_embed(texts, dim: int = DIM) -> np.ndarray:
"""Sözcük (1-2 gram) + karakter 4-gram karma (hashing) vektörü, L2-normalize. Model indirmeden çalışır."""
out = np.zeros((len(texts), dim), dtype=np.float32)
for r, t in enumerate(texts):
t = re.sub(r"\s+", " ", str(t).lower())
ws = re.findall(r"[a-z0-9çğıöşü']+", t)
feats = ws + [a + "_" + b for a, b in zip(ws, ws[1:])] + [t[i:i + 4] for i in range(0, max(0, len(t) - 3), 2)]
for f in feats:
h = int.from_bytes(hashlib.blake2b(f.encode(), digest_size=8).digest(), "little")
out[r, h % dim] += 1.0 if (h >> 63) & 1 else -1.0
n = np.linalg.norm(out[r])
if n:
out[r] /= n
return out
def sig(src, keys) -> str:
"""Bellek bölümü. ≤26 seçenek: kaynak + seçenek kümesinin tamamı. Geniş sorularda (banking77: 77–78 seçenek, testte
seçenek kümesi değişebiliyor) yalnız kaynak — oylar etiket ADIYLA mevcut seçeneklere eşlenir."""
if len(keys) > 26:
return f"{src}|wide"
return f"{src}|{len(keys)}|" + hashlib.md5("\x1f".join(map(str, keys)).encode()).hexdigest()[:10]
class DecisionMemory:
"""Soru tipine göre bölümlenmiş vaka belleği. Ekleme O(1); arama bölüm-içi kosinüs (matris çarpımı)."""
def __init__(self, embed_fn=hash_embed):
self.embed_fn = embed_fn
self.vecs: dict[str, list] = defaultdict(list) # imza → vektör listesi (yeni eklenenler)
self.mats: dict[str, np.ndarray] = {} # imza → donmuş matris (toplu arama için)
self.labels: dict[str, list] = defaultdict(list) # imza → etiket ADI listesi (seçenek kümesinden bağımsız)
self.n = 0
def _freeze(self, s):
if self.vecs[s]:
new = np.stack(self.vecs[s])
self.mats[s] = new if s not in self.mats else np.concatenate([self.mats[s], new])
self.vecs[s] = []
def learn(self, text, src, keys, y, vec=None):
"""Tek vaka öğren (anında etkili). Dönüş: geçen süre (ms)."""
t0 = time.perf_counter()
s = sig(src, keys)
v = vec if vec is not None else self.embed_fn([text])[0]
self.vecs[s].append(v.astype(np.float32)); self.labels[s].append(str(keys[int(y)])); self.n += 1
return (time.perf_counter() - t0) * 1e3
def learn_many(self, texts, srcs, keyss, ys, batch=2048):
for i in range(0, len(texts), batch):
self.learn_vectors(self.embed_fn(texts[i:i + batch]), srcs[i:i + batch], keyss[i:i + batch], ys[i:i + batch])
def learn_vectors(self, V, srcs, keyss, ys):
"""Önceden gömülmüş vakaları toplu öğren (gömme bir kez hesaplanır, farklı bellekler için yeniden kullanılır)."""
for v, sr, ks, y in zip(V, srcs, keyss, ys):
s = sig(sr, ks); self.vecs[s].append(np.asarray(v, dtype=np.float32)); self.labels[s].append(str(ks[int(y)])); self.n += 1
for s in list(self.vecs):
self._freeze(s)
def recall(self, text, src, keys, k=16, tau=0.05, vec=None, min_sim=0.0):
"""Seçenekler üzerinde bellek dağılımı (benzerlik-ağırlıklı oy) + en yüksek benzerlik. Bellek boşsa (None, 0).
min_sim: bu benzerliğin altındaki komşular oy vermez (bellek küçükken alakasız tek vakanın her soruya oy vermesini önler)."""
s = sig(src, keys)
self._freeze(s)
M = self.mats.get(s)
if M is None or not len(M):
return None, 0.0
q = vec if vec is not None else self.embed_fn([text])[0]
sims = M @ q
kk = min(k, len(sims))
top = np.argpartition(-sims, kk - 1)[:kk]
top = top[sims[top] >= min_sim]
if not len(top):
return None, float(sims.max())
w = np.exp((sims[top] - sims[top].max()) / tau)
p = vote([self.labels[s][i] for i in top], w, keys)
return p, (float(sims[top].max()) if p is not None else 0.0)
def vote(labels, weights, keys):
"""Komşu etiket adlarını mevcut seçeneklere eşle (seçenek kümesi değişse de çalışır). Eşleşen yoksa None."""
pos = {str(k): i for i, k in enumerate(keys)}
p = np.zeros(len(keys), dtype=np.float64)
for lab, wi in zip(labels, weights):
j = pos.get(lab)
if j is not None:
p[j] += wi
return p / p.sum() if p.sum() > 0 else None
def fuse(lp_model, p_mem, w, T=1.0, eps=1e-3):
"""Log-doğrusal birleştirme: log p_model/T + w·log(p_mem + ε). p_mem yoksa model aynen kalır."""
lp = np.asarray(lp_model, dtype=np.float64) / T
if p_mem is None or w == 0:
return lp - np.logaddexp.reduce(lp)
z = lp + w * np.log(np.asarray(p_mem) + eps)
return z - np.logaddexp.reduce(z)
class SelfCalibrator:
"""Öz-kalibrasyon (Hedge / çarpımsal ağırlıklar): her kaynak için uzmanlar — model, bellek, birleşik — arasında güveni
geri bildirimle kendisi ayarlar. Kayıp = log-kayıp; π_e ← π_e·exp(−η·kayıp_e). Pişmanlık sınırı: zamanla en iyi uzmana yaklaşır,
yani bellek bir kaynakta işe yaramıyorsa ona olan güven kendiliğinden söner (kendi eksiğini kendisi kapatır)."""
EXPERTS = ("model", "memory", "fused")
def __init__(self, eta=1.0, prior=(0.6, 0.1, 0.3)):
self.eta, self.prior = eta, np.log(np.asarray(prior, dtype=np.float64))
self.logw = defaultdict(lambda: self.prior.copy())
@staticmethod
def experts(lp_model, p_mem, w_fuse=1.0):
pm = np.exp(np.asarray(lp_model, dtype=np.float64) - np.logaddexp.reduce(lp_model))
pk = pm if p_mem is None else 0.98 * np.asarray(p_mem) + 0.02 / len(pm)
pf = np.exp(fuse(lp_model, p_mem, w_fuse))
return np.stack([pm, pk, pf])
def predict(self, src, E):
pi = np.exp(self.logw[src] - np.logaddexp.reduce(self.logw[src]))
return pi @ E, pi
def update(self, src, E, y):
self.logw[src] = self.logw[src] - self.eta * (-np.log(E[:, y] + 1e-12))
class Probe:
"""Bağımlılıksız çok sınıflı lojistik regresyon (numpy, L2, Adam, tam yığın) — beceri sınıflandırıcısı."""
def __init__(self, C=10.0, iters=300, lr=0.05):
self.C, self.iters, self.lr = C, iters, lr
def fit(self, X, y, classes):
self.classes = list(classes); idx = {c: i for i, c in enumerate(self.classes)}
X = np.asarray(X, dtype=np.float32); n, d = X.shape; k = len(self.classes)
Y = np.zeros((n, k), dtype=np.float32); Y[np.arange(n), [idx[c] for c in y]] = 1
W, b = np.zeros((d, k), np.float32), np.zeros(k, np.float32)
st = [np.zeros_like(W), np.zeros_like(W), np.zeros_like(b), np.zeros_like(b)]
lam = 1.0 / (self.C * n)
for t in range(1, self.iters + 1):
Z = X @ W + b; Z -= Z.max(1, keepdims=True); P = np.exp(Z); P /= P.sum(1, keepdims=True)
G = (P - Y) / n
for g, m, v, p in ((X.T @ G + lam * W, st[0], st[1], W), (G.sum(0), st[2], st[3], b)):
m *= 0.9; m += 0.1 * g; v *= 0.999; v += 0.001 * g * g
p -= self.lr * (m / (1 - 0.9 ** t)) / (np.sqrt(v / (1 - 0.999 ** t)) + 1e-8)
self.W, self.b = W, b
return self
def proba(self, x, keys):
z = np.asarray(x, np.float32) @ self.W + self.b; z = np.exp(z - z.max()); z /= z.sum()
pos = {c: j for j, c in enumerate(self.classes)}
p = np.array([z[pos[str(k)]] if str(k) in pos else 0.0 for k in keys], dtype=np.float64)
return p / p.sum() if p.sum() > 0 else None
class SkillLibrary:
"""Kendine yetenek ekleme: soru tipi (qid) başına veriden öğrenilen beceri + KENDİNİ SINAMA.
acquire(...) örneklerin bir kısmını ayırır, beceriyi kalanla öğrenir, ayrılan kısımda model-tek vs model+beceri karşılaştırır;
beceri yalnız doğruluk ≥ min_gain soru artar VE log-kayıp düşerse AÇILIR (bozmadan deneyerek öğrenme). Rapor döner."""
WGRID = (0.1, 0.2, 0.35, 0.5, 0.75, 1.0, 1.5, 2.0, 3.0)
def __init__(self, embed_fn=hash_embed):
self.embed, self.skills = embed_fn, {} # qid -> {"probe", "w", "report"}
def probs(self, qid, text, keys):
s = self.skills.get(qid)
if not s or s["w"] <= 0:
return None, 0.0
return s["probe"].proba(self.embed([text])[0], keys), s["w"]
def acquire(self, qid, texts, answers, keys_list, model_lp, holdout=0.25, min_gain=2, T=1.0, seed=0):
"""texts/answers/keys_list: örnekler; model_lp(i) → i. örnek için modelin log-olasılıkları (yalnız ayrılan kısımda çağrılır)."""
n = len(texts); rng = np.random.default_rng(seed); order = rng.permutation(n)
h = max(10, int(n * holdout)); te, tr = order[:h], order[h:]
X = self.embed([texts[i] for i in range(n)])
classes = sorted({str(a) for a in answers})
t0 = time.perf_counter()
probe = Probe().fit(X[tr], [str(answers[i]) for i in tr], classes)
fit_ms = (time.perf_counter() - t0) * 1e3
rows = []
for i in te:
keys = [str(k) for k in keys_list[i]]
if str(answers[i]) not in keys:
continue
rows.append((np.asarray(model_lp(i), dtype=np.float64), probe.proba(X[i], keys), keys.index(str(answers[i]))))
def ev(w):
nl = ac = 0
for lp, p, y in rows:
f = fuse(lp, p, w, T); nl -= f[y]; ac += int(np.argmax(f) == y)
return nl / max(1, len(rows)), ac
(n0, a0) = ev(0.0)
w = min(self.WGRID, key=lambda w: ev(w)[0]); (n1, a1) = ev(w)
on = a1 >= a0 + min_gain and n1 < n0
rep = {"qid": qid, "train": len(tr), "self_test": len(rows), "fit_ms": round(fit_ms, 1),
"self_test_acc": [round(a0 / max(1, len(rows)), 4), round(a1 / max(1, len(rows)), 4)],
"self_test_nll": [round(float(n0), 4), round(float(n1), 4)], "weight": float(w) if on else 0.0, "enabled": bool(on)}
if on: # kanıtlandı → tüm veriyle yeniden öğren, aç
probe = Probe().fit(X, [str(a) for a in answers], classes)
self.skills[qid] = {"probe": probe, "w": w if on else 0.0, "report": rep}
return rep
class AubinLearning:
"""Ürün katmanı: herhangi bir AUBIN modelini (Aubin / AubinEnsemble — .logprobs(state, questions) sunan) öz-öğrenen hale getirir.
smart = AubinLearning(Aubin("emrevrg/AUBIN-12B"))
out = smart.decide(state, questions) # bellek + öz-kalibrasyonlu karar
smart.learn(state, questions, {"q1": "refund"}) # doğru cevap → anında öğrenilir (eğitim yok)
smart.save("mem.npz"); smart.load("mem.npz") # bellek kalıcı
Bellek bölümü = soru kimliği + seçenek kümesi (geniş sorularda etiket ADI eşlemesi)."""
def __init__(self, model, memory=None, calibrator=None, k=16, tau=0.05, w_fuse=1.0, min_sim=0.5):
self.min_sim = min_sim # yalnız yeterince benzer geçmiş vakalar oy verir
self.model, self.mem = model, (memory or DecisionMemory())
self.cal = calibrator or SelfCalibrator()
self.k, self.tau, self.w = k, tau, w_fuse
self.skills = SkillLibrary(self.mem.embed_fn)
self._last = {}
def acquire_skill(self, qid, examples, holdout=0.25, min_gain=2):
"""Yeni yetenek: examples = [(state, questions, {qid: doğru anahtar}), ...] aynı soru tipinden.
Model yalnız kendini sınama kısmında çağrılır; beceri ancak kazanç kanıtlanırsa açılır. Dönüş: rapor."""
from .core import options
texts, answers, keys_list, items = [], [], [], []
for st, qs, ans in examples:
keys, _ = options(qs[qid]); texts.append(self._text(st, qs[qid])); answers.append(str(ans[qid]))
keys_list.append([str(k) for k in keys]); items.append((st, qs))
def model_lp(i):
st, qs = items[i]
lp = self.model.logprobs(st, {qid: qs[qid]})[qid][1]
return lp.detach().cpu().numpy() if hasattr(lp, "detach") else lp
return self.skills.acquire(qid, texts, answers, keys_list, model_lp, holdout=holdout, min_gain=min_gain)
@staticmethod
def _text(state, q):
import json as _j
st = state if isinstance(state, str) else _j.dumps(state, ensure_ascii=False, sort_keys=True)
return f"{st} || {(q or {}).get('instructions', '')}"
def decide(self, state, questions):
lps = self.model.logprobs(state, questions)
out = {}
for qid, (keys, lp) in lps.items():
lp = np.asarray(lp.detach().cpu() if hasattr(lp, "detach") else lp, dtype=np.float64)
ps, ws = self.skills.probs(qid, self._text(state, questions[qid]), [str(k) for k in keys])
if ps is not None: # kanıtlanmış beceri → modele katılır (log-doğrusal)
lp = fuse(lp, ps, ws)
p, smax = self.mem.recall(self._text(state, questions[qid]), qid, keys, self.k, self.tau, min_sim=self.min_sim)
E = SelfCalibrator.experts(lp, p, self.w)
mix, pi = self.cal.predict(qid, E)
self._last[(qid, self._text(state, questions[qid]))] = E
j = int(np.argmax(mix))
out[qid] = {"answer": keys[j], "confidence": round(float(mix[j]), 4),
"probabilities": {str(k): round(float(v), 4) for k, v in zip(keys, mix)},
"memory": {"similar_case": round(smax, 3), "trust_model_memory_fused": [round(float(x), 3) for x in pi]}}
return out
def learn(self, state, questions, answers):
"""answers: {qid: doğru seçenek anahtarı}. Belleğe yazar + (son decide varsa) güveni günceller. Dönüş: ms."""
from .core import options
t0 = time.perf_counter()
for qid, ans in answers.items():
keys, _ = options(questions[qid])
keys = [str(k) for k in keys]
if str(ans) not in keys:
continue
y = keys.index(str(ans)); txt = self._text(state, questions[qid])
E = self._last.pop((qid, txt), None)
if E is not None:
self.cal.update(qid, E, y)
self.mem.learn(txt, qid, keys, y)
return round((time.perf_counter() - t0) * 1e3, 3)
def save(self, path):
for s in list(self.mem.vecs):
self.mem._freeze(s)
np.savez_compressed(path, **{f"M::{s}": m for s, m in self.mem.mats.items()},
**{f"L::{s}": np.asarray(self.mem.labels[s]) for s in self.mem.mats})
def load(self, path):
Z = np.load(path, allow_pickle=False)
for f in Z.files:
if f.startswith("M::"):
s = f[3:]; self.mem.mats[s] = Z[f]; self.mem.labels[s] = [str(x) for x in Z["L::" + s]]
self.mem.n = sum(len(v) for v in self.mem.labels.values())
class AubinLearner:
"""Karar modeli + öz-öğrenme belleği + araştırma kancası.
score_fn(item) → seçenek log-olasılıkları (ör. kev_llm.Scorer.score); item: {"state","q","keys","texts","src"}.
research_fn(query) → kanıt metinleri listesi (ör. norovox_mini.retrieval.Retrieval().fetch).
"""
def __init__(self, score_fn, memory=None, weights=None, margin=99.0, research_fn=None, k=16, tau=0.05, calibrator=None):
self.score_fn, self.mem = score_fn, (memory or DecisionMemory())
self.weights = weights or {} # kaynak → bellek birleştirme ağırlığı (dev'de ayarlanır); yoksa "*" ya da 0.5
self.margin, self.research_fn, self.k, self.tau = margin, research_fn, k, tau
self.cal = calibrator if calibrator is not None else SelfCalibrator()
self.log, self._last = [], {}
@staticmethod
def text(it):
return f"{it['state']} || {it['q']}"
def decide(self, it):
t0 = time.perf_counter()
lp = np.asarray(self.score_fn(it), dtype=np.float64)
srt = np.sort(lp)[::-1]
unsure = len(lp) > 1 and (srt[0] - srt[1]) < self.margin
p, smax, evidence = self.mem.recall(self.text(it), it["src"], it["keys"], self.k, self.tau) + ([],)
if p is None and unsure and self.research_fn is not None: # bellekte karşılığı yok: dışarıdan kanıt (Norovox Retrieval)
try:
evidence = self.research_fn(f"{it['q']} {str(it['state'])[:300]}")[:3]
except Exception:
evidence = []
E = SelfCalibrator.experts(lp, p, self.weights.get(it["src"], self.weights.get("*", 0.5)))
mix, pi = self.cal.predict(it["src"], E)
self._last[id(it)] = E
ms = (time.perf_counter() - t0) * 1e3
self.log.append({"src": it["src"], "unsure": bool(unsure), "memory_sim": round(smax, 3), "trust": [round(float(x), 3) for x in pi],
"evidence": len(evidence), "ms": round(ms, 2)})
return int(np.argmax(mix)), np.log(mix + 1e-12), evidence
def feedback(self, it, y):
"""Doğru cevap gelince: (1) belleğe yaz — bilgi eksiği anında kapanır; (2) öz-kalibrasyon — bu kaynakta model mi bellek mi
daha isabetli, güven kendiliğinden kayar. Dönüş: ms."""
t0 = time.perf_counter()
E = self._last.pop(id(it), None)
if E is not None:
self.cal.update(it["src"], E, int(y))
self.mem.learn(self.text(it), it["src"], it["keys"], y)
return (time.perf_counter() - t0) * 1e3