vegaml 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vegaml/__init__.py +192 -0
- vegaml/_api.py +222 -0
- vegaml/_common.py +1027 -0
- vegaml/_vision.py +172 -0
- vegaml/hub.py +33 -0
- vegaml/py.typed +0 -0
- vegaml/ttt.py +85 -0
- vegaml-0.1.0.dist-info/METADATA +251 -0
- vegaml-0.1.0.dist-info/RECORD +12 -0
- vegaml-0.1.0.dist-info/WHEEL +5 -0
- vegaml-0.1.0.dist-info/licenses/LICENSE +191 -0
- vegaml-0.1.0.dist-info/top_level.txt +1 -0
vegaml/__init__.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""vegaml — typed decisions from a frozen language model and a small physics engine.
|
|
2
|
+
|
|
3
|
+
import vegaml
|
|
4
|
+
|
|
5
|
+
v = vegaml.load("nandakishorm/vega-08b-public-intents")
|
|
6
|
+
v.decide("I was billed twice, refund me or I cancel.",
|
|
7
|
+
{"team": {"type": "choice", "instructions": "Which team handles this?",
|
|
8
|
+
"criteria": {"billing": "payments and refunds", "technical": "bugs"}}})
|
|
9
|
+
|
|
10
|
+
Two readouts over the same frozen features, chosen with `mode`:
|
|
11
|
+
|
|
12
|
+
* ``"engine"`` (the default) — the physics engine and its adapters. Zero-shot: it answers a question
|
|
13
|
+
it has never seen, and every answer carries a calibrated probability, a conformal set and an
|
|
14
|
+
abstain flag. Every benchmark figure published for this model is measured in this mode.
|
|
15
|
+
* ``"ttt"`` — a small head fitted here, on your labelled examples, over the same features. Sharper
|
|
16
|
+
where your label space matches the examples, and worthless outside them. It carries no calibration
|
|
17
|
+
and no abstain flag, so it will answer confidently when it should not.
|
|
18
|
+
* ``"both"`` — both, side by side, which is the only honest way to pick between them.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from typing import Dict, List, Mapping, Optional, Sequence, Tuple
|
|
23
|
+
|
|
24
|
+
from .hub import fetch
|
|
25
|
+
from .ttt import TTTHead
|
|
26
|
+
|
|
27
|
+
__all__ = ["load", "Vega", "TTTHead", "fetch", "__version__"]
|
|
28
|
+
__version__ = "0.1.0"
|
|
29
|
+
|
|
30
|
+
MODES = ("engine", "ttt", "both")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Vega:
|
|
34
|
+
"""A loaded checkpoint, with both readouts over one set of frozen features.
|
|
35
|
+
|
|
36
|
+
`mode` sets the default readout for :meth:`decide`; "engine" unless you change it.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, engine, mode: str = "engine"):
|
|
40
|
+
if mode not in MODES:
|
|
41
|
+
raise ValueError("mode must be one of %s, got %r" % (", ".join(MODES), mode))
|
|
42
|
+
self.engine = engine
|
|
43
|
+
self.mode = mode
|
|
44
|
+
self.heads: Dict[str, Tuple[TTTHead, Optional[float]]] = {}
|
|
45
|
+
|
|
46
|
+
# ------------------------------------------------------------------ loading
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_pretrained(cls, repo_or_path: str, backbone: Optional[str] = None,
|
|
49
|
+
device: Optional[str] = None, adapters: str = "auto",
|
|
50
|
+
mode: str = "engine", revision: Optional[str] = None,
|
|
51
|
+
token: Optional[str] = None, max_len: Optional[int] = None) -> "Vega":
|
|
52
|
+
# Imported here, not at module import: torch and transformers are only needed once a
|
|
53
|
+
# checkpoint is actually being loaded.
|
|
54
|
+
from ._api import Vega as _Engine
|
|
55
|
+
|
|
56
|
+
path = fetch(repo_or_path, revision=revision, token=token)
|
|
57
|
+
return cls(_Engine(path, backbone=backbone, device=device, adapters=adapters,
|
|
58
|
+
max_len=max_len), mode=mode)
|
|
59
|
+
|
|
60
|
+
# ------------------------------------------------------------------ reading
|
|
61
|
+
def features(self, state, question: Mapping):
|
|
62
|
+
"""Pooled frozen features for one (state, question) — what both readouts see."""
|
|
63
|
+
import numpy as np
|
|
64
|
+
import torch
|
|
65
|
+
|
|
66
|
+
from ._common import build_prompt, option_texts, serialize_state
|
|
67
|
+
|
|
68
|
+
v = self.engine
|
|
69
|
+
qi = v._internal(question)
|
|
70
|
+
k = len(option_texts(qi))
|
|
71
|
+
text = state if isinstance(state, str) else serialize_state(state)
|
|
72
|
+
ids, spans = build_prompt(v.perc.tok, text, qi, v.cfg["max_len"],
|
|
73
|
+
option_order=list(range(k)),
|
|
74
|
+
opt_max_tokens=v.cfg["opt_max_tokens"],
|
|
75
|
+
query_first_max_state=v.cfg.get("query_first_max_state", 0))
|
|
76
|
+
with torch.no_grad():
|
|
77
|
+
f, _ = v.perc.pooled([{"ids": np.asarray(ids, dtype=np.int32), "spans": spans}],
|
|
78
|
+
prefix_min=v.cfg.get("prefix_min_tokens", 4096))
|
|
79
|
+
return f[0].float().cpu().numpy().ravel()
|
|
80
|
+
|
|
81
|
+
# ------------------------------------------------------------------ fitting
|
|
82
|
+
def fit(self, examples: Sequence[Tuple], questions: Mapping, folds: int = 5,
|
|
83
|
+
min_examples: int = 6) -> Dict[str, Dict]:
|
|
84
|
+
"""Fit one TTT head per question from labelled examples.
|
|
85
|
+
|
|
86
|
+
`examples` is a sequence of ``(state, {question_id: label})``. One forward pass is run per
|
|
87
|
+
example per question, so the cost is ``len(examples) × len(questions)`` reads of the frozen
|
|
88
|
+
backbone — seconds on a GPU for a few dozen examples, and paid once.
|
|
89
|
+
|
|
90
|
+
Returns, per question id, the number of examples used and a cross-validated accuracy. **Read
|
|
91
|
+
that number.** A head at or below chance has learned nothing, and its answers are noise
|
|
92
|
+
dressed up as probabilities.
|
|
93
|
+
"""
|
|
94
|
+
report: Dict[str, Dict] = {}
|
|
95
|
+
for qid, q in questions.items():
|
|
96
|
+
X, y = [], []
|
|
97
|
+
for state, labels in examples:
|
|
98
|
+
if qid in labels:
|
|
99
|
+
X.append(self.features(state, q))
|
|
100
|
+
y.append(str(labels[qid]))
|
|
101
|
+
if len(X) < min_examples:
|
|
102
|
+
report[qid] = {"fitted": False, "examples": len(X),
|
|
103
|
+
"reason": "needs at least %d labelled examples" % min_examples}
|
|
104
|
+
continue
|
|
105
|
+
labels_for_q = label_ids(q)
|
|
106
|
+
head = TTTHead(labels_for_q).fit(X, y)
|
|
107
|
+
cv = head.cross_val_accuracy(X, y, folds=folds)
|
|
108
|
+
chance = 1.0 / max(len(labels_for_q), 2)
|
|
109
|
+
self.heads[qid] = (head, cv)
|
|
110
|
+
# cv is None when there are too few examples to cross-validate. That is "unknown", not
|
|
111
|
+
# "fine": reporting False there would read as a pass the data cannot support.
|
|
112
|
+
report[qid] = {"fitted": True, "examples": len(X), "cv_accuracy": cv,
|
|
113
|
+
"chance": round(chance, 3),
|
|
114
|
+
"at_or_below_chance": None if cv is None else cv <= chance}
|
|
115
|
+
if cv is None:
|
|
116
|
+
report[qid]["cv_note"] = ("too few examples for %d-fold cross-validation, so the "
|
|
117
|
+
"head's accuracy is unknown; %d or more would measure it"
|
|
118
|
+
% (folds, 2 * folds))
|
|
119
|
+
return report
|
|
120
|
+
|
|
121
|
+
# ------------------------------------------------------------------ deciding
|
|
122
|
+
def decide(self, state, questions: Mapping, mode: Optional[str] = None, alpha: str = "0.1",
|
|
123
|
+
views: int = 1) -> Dict:
|
|
124
|
+
"""Answer every question from one read of the state.
|
|
125
|
+
|
|
126
|
+
With ``mode="engine"`` the result is the engine's own, unchanged. With ``"ttt"`` each answer
|
|
127
|
+
comes from the fitted head for that question. With ``"both"`` the result carries
|
|
128
|
+
``answers`` (engine) and ``ttt`` side by side.
|
|
129
|
+
"""
|
|
130
|
+
mode = mode or self.mode
|
|
131
|
+
if mode not in MODES:
|
|
132
|
+
raise ValueError("mode must be one of %s, got %r" % (", ".join(MODES), mode))
|
|
133
|
+
if mode in ("ttt", "both"):
|
|
134
|
+
missing = [q for q in questions if q not in self.heads]
|
|
135
|
+
if missing and mode == "ttt":
|
|
136
|
+
raise RuntimeError(
|
|
137
|
+
"no TTT head for %s. Call fit(examples, questions) first, or use "
|
|
138
|
+
"mode='engine'." % ", ".join(sorted(missing)))
|
|
139
|
+
if mode == "ttt":
|
|
140
|
+
return {"mode": "ttt", "answers": self._ttt_answers(state, questions)}
|
|
141
|
+
out = self.engine.decide(state, questions, alpha=alpha, views=views)
|
|
142
|
+
if mode == "engine":
|
|
143
|
+
out["mode"] = "engine"
|
|
144
|
+
return out
|
|
145
|
+
out["mode"] = "both"
|
|
146
|
+
out["ttt"] = self._ttt_answers(state, questions, skip_missing=True)
|
|
147
|
+
return out
|
|
148
|
+
|
|
149
|
+
def _ttt_answers(self, state, questions: Mapping, skip_missing: bool = False) -> Dict:
|
|
150
|
+
answers: Dict[str, Dict] = {}
|
|
151
|
+
for qid, q in questions.items():
|
|
152
|
+
hv = self.heads.get(qid)
|
|
153
|
+
if hv is None:
|
|
154
|
+
if skip_missing:
|
|
155
|
+
continue
|
|
156
|
+
raise RuntimeError("no TTT head for %r" % qid)
|
|
157
|
+
head, cv = hv
|
|
158
|
+
probs = head.predict(self.features(state, q), allowed=label_ids(q))
|
|
159
|
+
best = max(probs, key=probs.get)
|
|
160
|
+
answers[qid] = {"probs": probs, "choice": best, "confidence": probs[best],
|
|
161
|
+
"cv_accuracy": cv}
|
|
162
|
+
return answers
|
|
163
|
+
|
|
164
|
+
# ------------------------------------------------------------------ images
|
|
165
|
+
def decide_image(self, image, questions: Mapping, **kw) -> Dict:
|
|
166
|
+
"""The same typed interface over a picture, through the backbone's frozen vision encoder."""
|
|
167
|
+
if getattr(self, "_vision", None) is None:
|
|
168
|
+
from ._vision import VegaVision
|
|
169
|
+
|
|
170
|
+
self._vision = VegaVision(self.engine.model_dir,
|
|
171
|
+
backbone=self.engine.cfg.get("backbone_name"),
|
|
172
|
+
device=str(getattr(self.engine, "device", "cpu")), **kw)
|
|
173
|
+
return self._vision.decide_image(image, questions)
|
|
174
|
+
|
|
175
|
+
def __repr__(self) -> str:
|
|
176
|
+
return "Vega(mode=%r, heads=%d, max_len=%d)" % (
|
|
177
|
+
self.mode, len(self.heads), self.engine.cfg.get("max_len", 0))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def label_ids(question: Mapping) -> List[str]:
|
|
181
|
+
"""The option ids a readout must span, for any of the three answer types."""
|
|
182
|
+
t = question["type"]
|
|
183
|
+
if t == "noul":
|
|
184
|
+
return ["false", "true"]
|
|
185
|
+
if t == "score":
|
|
186
|
+
return [str(i) for i in range(len(question.get("criteria") or []))]
|
|
187
|
+
return [str(k) for k in question["criteria"]]
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def load(repo_or_path: str, **kw) -> Vega:
|
|
191
|
+
"""Load a checkpoint. See :meth:`Vega.from_pretrained` for the arguments."""
|
|
192
|
+
return Vega.from_pretrained(repo_or_path, **kw)
|
vegaml/_api.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""Vega inference: decide(state, questions) -> calibrated typed answers, with the physics behind each one.
|
|
2
|
+
|
|
3
|
+
d = Vega("/kaggle/working/vega_model") # backbone read from vega_config.json (or backbone=...)
|
|
4
|
+
d.decide("I was billed twice, refund me or I cancel.",
|
|
5
|
+
{"team": {"type": "choice", "instructions": "Which team handles this?",
|
|
6
|
+
"criteria": {"billing": "payments and refunds", "technical": "bugs", "sales": "pricing"}},
|
|
7
|
+
"churn": {"type": "noul", "instructions": "Does the customer threaten to leave?"}})
|
|
8
|
+
|
|
9
|
+
Every answer carries: probabilities (thermally calibrated), confidence, a split-conformal prediction set, whether to
|
|
10
|
+
abstain at the configured risk, whether the particle ended unbound (far from every well: likely out of
|
|
11
|
+
distribution), how many integration steps it took to settle, and its final energy.
|
|
12
|
+
|
|
13
|
+
Adapters (model_dir/adapters/*.safetensors + .json) extend the frozen engine to new decision families without
|
|
14
|
+
changing it. adapters="auto" (default): a learned gate per question decides whether an adapter applies, and each
|
|
15
|
+
answer uses that adapter's calibration or the base one; "on": every adapter always on; "off": the base engine only.
|
|
16
|
+
Answers then also carry "adapter" (name or None) and "gate".
|
|
17
|
+
"""
|
|
18
|
+
import glob
|
|
19
|
+
import hashlib
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import time
|
|
23
|
+
|
|
24
|
+
import numpy as np
|
|
25
|
+
import torch
|
|
26
|
+
|
|
27
|
+
from ._common import (QTYPES, VegaEngine, Perception, build_prompt, load_adapter, option_texts, prose_state,
|
|
28
|
+
route_adapters, serialize_state, softmax_np, thermal_features, thermal_tau)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Vega:
|
|
32
|
+
def __init__(self, model_dir, backbone=None, device=None, halt=True, calibration="default", adapters="auto",
|
|
33
|
+
perception=None, max_len=None):
|
|
34
|
+
"""calibration="teacher" loads the teacher-matching calibration of a typed-decisions fine-tune (if present)
|
|
35
|
+
instead of the default one fitted to the correct answers. perception: an already loaded Perception to share.
|
|
36
|
+
max_len: prompt token limit, overriding the trained one (e.g. 65536 to read long records; the engine was
|
|
37
|
+
trained at its configured length, so accuracy beyond it has to be measured)."""
|
|
38
|
+
from safetensors.torch import load_file
|
|
39
|
+
with open(os.path.join(model_dir, "vega_config.json")) as f:
|
|
40
|
+
self.cfg = json.load(f)
|
|
41
|
+
if max_len:
|
|
42
|
+
self.cfg["trained_max_len"] = self.cfg["max_len"]
|
|
43
|
+
self.cfg["max_len"] = int(max_len)
|
|
44
|
+
self.model_dir = model_dir
|
|
45
|
+
self.calibration = calibration
|
|
46
|
+
self.device = torch.device(device or ("cuda" if torch.cuda.is_available() else "cpu"))
|
|
47
|
+
bb = backbone or self.cfg["backbone"]
|
|
48
|
+
local = os.path.join(model_dir, "backbone")
|
|
49
|
+
if backbone is None and os.path.isdir(local):
|
|
50
|
+
bb = local
|
|
51
|
+
self.perc = perception or Perception(bb, self.device, self.cfg["feature_layers"], self.cfg["option_layers"])
|
|
52
|
+
assert self.perc.feature_layers == self.cfg["feature_layers"], "backbone depth differs from training"
|
|
53
|
+
self.engine = VegaEngine(self.cfg["d_model"], len(self.cfg["feature_layers"]), len(self.cfg["option_layers"]),
|
|
54
|
+
self.cfg)
|
|
55
|
+
self.engine.load_state_dict(load_file(os.path.join(model_dir, "engine.safetensors")))
|
|
56
|
+
self.engine.to(self.device).eval()
|
|
57
|
+
self.halt = halt
|
|
58
|
+
cal = self.cfg.get("calibration_teacher" if calibration == "teacher" else "calibration") or {}
|
|
59
|
+
if calibration == "teacher" and not cal:
|
|
60
|
+
raise ValueError("this model has no teacher calibration block (only typed-decisions fine-tunes do)")
|
|
61
|
+
self.theta = np.array(cal.get("theta", np.zeros((3, 5))), dtype=np.float64)
|
|
62
|
+
self.conformal = cal.get("conformal", {})
|
|
63
|
+
self.abstain = cal.get("abstain_threshold", {})
|
|
64
|
+
self.risk = cal.get("default_risk", "0.1")
|
|
65
|
+
self.unbound = cal.get("unbound_threshold", float("inf"))
|
|
66
|
+
if not cal:
|
|
67
|
+
print("vega: no calibration block in vega_config.json (run evaluate.py); probabilities are raw")
|
|
68
|
+
self.adapters = []
|
|
69
|
+
self._sha = None
|
|
70
|
+
assert adapters in ("auto", "on", "off"), adapters
|
|
71
|
+
if adapters != "off":
|
|
72
|
+
for js in sorted(glob.glob(os.path.join(model_dir, "adapters", "*.json"))):
|
|
73
|
+
meta = json.load(open(js))
|
|
74
|
+
if adapters == "auto" and not meta.get("gate_trained"):
|
|
75
|
+
print("vega: adapter '%s' has no trained gate (fitted without replay data); load it with "
|
|
76
|
+
"adapters=\"on\" to use it" % meta["name"])
|
|
77
|
+
continue
|
|
78
|
+
if meta.get("base_engine_sha256") and meta["base_engine_sha256"] != self.base_sha256():
|
|
79
|
+
print("vega: adapter '%s' was trained on a different base engine - skipped" % meta["name"])
|
|
80
|
+
continue
|
|
81
|
+
self.add_adapter(load_file(js[:-len(".json")] + ".safetensors"), meta)
|
|
82
|
+
self.set_adapters(adapters)
|
|
83
|
+
|
|
84
|
+
def base_sha256(self):
|
|
85
|
+
if self._sha is None:
|
|
86
|
+
h = hashlib.sha256()
|
|
87
|
+
with open(os.path.join(self.model_dir, "engine.safetensors"), "rb") as f:
|
|
88
|
+
for chunk in iter(lambda: f.read(1 << 22), b""):
|
|
89
|
+
h.update(chunk)
|
|
90
|
+
self._sha = h.hexdigest()
|
|
91
|
+
return self._sha
|
|
92
|
+
|
|
93
|
+
def add_adapter(self, state, meta):
|
|
94
|
+
"""Attach one adapter (tensors + its json) to the engine; returns its index."""
|
|
95
|
+
j = load_adapter(self.engine, state, meta["rank"], meta["alpha"])
|
|
96
|
+
self.engine.to(self.device).eval()
|
|
97
|
+
cal = (meta.get("calibration_teacher") if self.calibration == "teacher" else None) or meta["calibration"]
|
|
98
|
+
self.adapters.append({"name": meta["name"], "meta": meta, "cal": {
|
|
99
|
+
"theta": np.array(cal["theta"], dtype=np.float64), "conformal": cal.get("conformal", {}),
|
|
100
|
+
"abstain": cal.get("abstain_threshold", {}), "unbound": cal.get("unbound_threshold", float("inf"))},
|
|
101
|
+
"threshold": meta.get("gate_threshold", 0.5)})
|
|
102
|
+
return j
|
|
103
|
+
|
|
104
|
+
def set_adapters(self, mode):
|
|
105
|
+
""""auto" (gated), "on" (all adapters always on) or "off" (base engine exactly)."""
|
|
106
|
+
assert mode in ("auto", "on", "off"), mode
|
|
107
|
+
if mode == "on" and len(self.adapters) > 1:
|
|
108
|
+
print("vega: %d adapters all on at once; \"auto\" lets each question pick" % len(self.adapters))
|
|
109
|
+
self.engine.adapter_mode = mode if self.adapters else "off"
|
|
110
|
+
|
|
111
|
+
def _cal(self, r):
|
|
112
|
+
if r < 0:
|
|
113
|
+
return {"theta": self.theta, "conformal": self.conformal, "abstain": self.abstain, "unbound": self.unbound}
|
|
114
|
+
return self.adapters[r]["cal"]
|
|
115
|
+
|
|
116
|
+
@staticmethod
|
|
117
|
+
def _internal(qdef):
|
|
118
|
+
t = qdef["type"]
|
|
119
|
+
crit = qdef.get("criteria")
|
|
120
|
+
if t == "choice" and isinstance(crit, list):
|
|
121
|
+
crit = {c: None for c in crit}
|
|
122
|
+
ins = qdef["instructions"] if isinstance(qdef["instructions"], str) else json.dumps(qdef["instructions"])
|
|
123
|
+
return {"t": t, "ins": ins, "crit": crit}
|
|
124
|
+
|
|
125
|
+
def _view(self, text, qs, template, orders):
|
|
126
|
+
"""One reading of the state under one surface form. Returns calibrated probabilities per question in ORIGINAL
|
|
127
|
+
option order, plus the engine output / route / raw logits (in prompt order) for the physics fields."""
|
|
128
|
+
items = []
|
|
129
|
+
for q, order in zip(qs, orders):
|
|
130
|
+
ids, spans = build_prompt(self.perc.tok, text, q, self.cfg["max_len"], template=template, option_order=order,
|
|
131
|
+
opt_max_tokens=self.cfg["opt_max_tokens"],
|
|
132
|
+
query_first_max_state=self.cfg.get("query_first_max_state", 0))
|
|
133
|
+
items.append({"ids": np.asarray(ids, dtype=np.int32), "spans": spans})
|
|
134
|
+
C = self.engine.memory_chunks if self.engine.memory is not None else 0
|
|
135
|
+
pm = self.cfg.get("prefix_min_tokens", 4096)
|
|
136
|
+
if C: # long states are read once for all questions; chunk summaries feed the memory reader
|
|
137
|
+
feats, opt_flat, chunks, cmask = self.perc.pooled(items, chunks=C, prefix_min=pm,
|
|
138
|
+
chunk_min_tokens=self.cfg.get("memory_min_tokens", 2048),
|
|
139
|
+
chunk_tokens=self.cfg.get("memory_chunk_tokens", 128))
|
|
140
|
+
else:
|
|
141
|
+
(feats, opt_flat), chunks, cmask = self.perc.pooled(items, prefix_min=pm), None, None
|
|
142
|
+
K = max(len(it["spans"]["options"]) for it in items)
|
|
143
|
+
B = len(items)
|
|
144
|
+
opts = torch.zeros((B, K) + opt_flat.shape[1:], device=self.device)
|
|
145
|
+
mask = torch.zeros((B, K), dtype=torch.bool, device=self.device)
|
|
146
|
+
o = 0
|
|
147
|
+
for b, it in enumerate(items):
|
|
148
|
+
k = len(it["spans"]["options"])
|
|
149
|
+
opts[b, :k] = opt_flat[o:o + k]
|
|
150
|
+
mask[b, :k] = True
|
|
151
|
+
o += k
|
|
152
|
+
qtype = torch.tensor([QTYPES[q["t"]] for q in qs], device=self.device)
|
|
153
|
+
out = self.engine(feats, opts, mask, qtype, halt=self.halt, return_traj=False, chunks=chunks, chunk_mask=cmask)
|
|
154
|
+
logits = out["logits"].float().cpu().numpy()
|
|
155
|
+
phys = thermal_features(out, mask, self.engine.max_steps).cpu().numpy()
|
|
156
|
+
qt = qtype.cpu().numpy()
|
|
157
|
+
gates = out["gates"].float().cpu().numpy() if out.get("gates") is not None else None
|
|
158
|
+
route = route_adapters(gates, self.adapters[0]["threshold"] if self.adapters else 0.5)
|
|
159
|
+
route = np.full(B, -1) if route is None else route
|
|
160
|
+
P = np.zeros_like(logits, dtype=np.float64)
|
|
161
|
+
for rt in np.unique(route): # each question calibrated by the adapter it went through (or the base)
|
|
162
|
+
sel = route == rt
|
|
163
|
+
P[sel] = softmax_np(logits[sel], thermal_tau(self._cal(rt)["theta"], qt[sel], phys[sel]))
|
|
164
|
+
P_orig = []
|
|
165
|
+
for r, order in enumerate(orders):
|
|
166
|
+
po = np.zeros(len(order))
|
|
167
|
+
po[order] = P[r, :len(order)]
|
|
168
|
+
P_orig.append(po)
|
|
169
|
+
return {"P": P_orig, "out": out, "route": route, "gates": gates, "logits": logits, "mask": mask,
|
|
170
|
+
"n_tokens": int(sum(len(it["ids"]) for it in items))}
|
|
171
|
+
|
|
172
|
+
@torch.no_grad()
|
|
173
|
+
def decide(self, state, questions, alpha="0.1", risk=None, views=1):
|
|
174
|
+
"""views = 1: one reading. views = 2 adds a reading with every choice / yes-no question's options in reverse
|
|
175
|
+
order; views = 3 also adds the state rendered as prose with the second template. Probabilities are averaged
|
|
176
|
+
(test-time averaging over surface forms: steadier answers, about views x the cost)."""
|
|
177
|
+
t0 = time.time()
|
|
178
|
+
qids = list(questions)
|
|
179
|
+
qs = [self._internal(questions[k]) for k in qids]
|
|
180
|
+
n_opt = [len(option_texts(q)) for q in qs]
|
|
181
|
+
ident = [list(range(k)) for k in n_opt]
|
|
182
|
+
text = serialize_state(state)
|
|
183
|
+
runs = [self._view(text, qs, 0, ident)]
|
|
184
|
+
if views >= 2:
|
|
185
|
+
rev = [o[::-1] if q["t"] != "score" else o for q, o in zip(qs, ident)]
|
|
186
|
+
runs.append(self._view(text, qs, 0, rev))
|
|
187
|
+
if views >= 3:
|
|
188
|
+
runs.append(self._view(prose_state(state) if not isinstance(state, str) else text, qs, 1, ident))
|
|
189
|
+
base = runs[0]
|
|
190
|
+
out, route, gates, logits, mask = base["out"], base["route"], base["gates"], base["logits"], base["mask"]
|
|
191
|
+
risk = str(risk) if risk is not None else self.risk
|
|
192
|
+
answers = {}
|
|
193
|
+
for r, (qid, q) in enumerate(zip(qids, qs)):
|
|
194
|
+
k = n_opt[r]
|
|
195
|
+
p = np.mean([run["P"][r] for run in runs], 0)
|
|
196
|
+
cal = self._cal(int(route[r]))
|
|
197
|
+
labels = list(q["crit"].keys()) if q["t"] == "choice" else list(range(k)) if q["t"] == "score" else [False, True]
|
|
198
|
+
conf = float(p.max())
|
|
199
|
+
th = cal["conformal"].get(q["t"], {}).get(alpha)
|
|
200
|
+
best = int(p.argmax())
|
|
201
|
+
# LAC set; the top option is always kept so the set is never empty
|
|
202
|
+
pset = [labels[i] for i in range(k) if th is not None and (p[i] >= max(1.0 - th, 1e-12) or i == best)]
|
|
203
|
+
a = {"probs": {str(labels[i]): float(p[i]) for i in range(k)}, "confidence": conf,
|
|
204
|
+
"prediction_set": pset if th is not None else None,
|
|
205
|
+
"abstain": bool(conf < cal["abstain"].get(risk, 0.0)),
|
|
206
|
+
"unbound": bool(float(out["nearest"][r]) > cal["unbound"]),
|
|
207
|
+
"settle_steps": int(out["steps_used"][r]), "energy": float(-logits[r, :k].max())}
|
|
208
|
+
if len(runs) > 1:
|
|
209
|
+
a["view_agreement"] = float(np.mean([int(run["P"][r].argmax()) == best for run in runs]))
|
|
210
|
+
if self.adapters:
|
|
211
|
+
a["adapter"] = self.adapters[int(route[r])]["name"] if route[r] >= 0 else None
|
|
212
|
+
a["gate"] = float(gates[r].max()) if gates is not None else 0.0
|
|
213
|
+
if q["t"] == "choice":
|
|
214
|
+
a["choice"] = labels[best]
|
|
215
|
+
elif q["t"] == "score":
|
|
216
|
+
a["score"] = best
|
|
217
|
+
a["expected_score"] = float((np.arange(k) * p).sum())
|
|
218
|
+
else:
|
|
219
|
+
a["noul"] = float(p[1])
|
|
220
|
+
answers[qid] = a
|
|
221
|
+
return {"answers": answers, "latency_ms": round(1000 * (time.time() - t0), 1),
|
|
222
|
+
"n_tokens": sum(run["n_tokens"] for run in runs), "views": len(runs)}
|