File size: 18,949 Bytes
b707206
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9bc99d7
 
 
b707206
 
 
 
 
 
 
 
 
9bc99d7
 
 
b707206
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9bc99d7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b707206
 
 
 
 
 
 
 
 
9bc99d7
 
b707206
 
 
9bc99d7
b707206
 
9bc99d7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b707206
 
 
 
 
 
 
 
 
 
 
9bc99d7
 
 
 
b707206
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
"""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