File size: 7,080 Bytes
f490edd 36aa3ab f490edd 36aa3ab 95b5e87 36aa3ab f490edd 36aa3ab f490edd 36aa3ab f490edd 36aa3ab | 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 | """Snap 1: typed decisions in one forward pass.
pip install "laya==0.3.4" torch transformers safetensors huggingface_hub
from snap import Snap
snap = Snap("Otron/snap-1")
snap.decide(
state={"message": "my card got charged twice for the same order"},
questions={
"topic": {"type": "choice", "instructions": "What is the message about?",
"criteria": {"billing": "Payments, charges, refunds", "shipping": "Delivery", "other": "Anything else"}},
"urgent": {"type": "noul", "instructions": "The customer needs a reply today"},
},
)
# {"topic": {"choice": "billing", "confidence": 0.997, "probabilities": {...}},
# "urgent": {"noul": 0.42, "confidence": 0.58}}
Questions use the common typed-decision wire format: `noul` (probability a statement is true),
`choice` (one key from `criteria`, a dict of key -> description, or a list of keys) and `score`
(one of the ordered levels in `criteria`). All questions about one state are answered together:
they are packed into as few sequences as fit, each read once by the encoder.
"""
import json
import math
import os
from typing import Dict, List, Optional, Union
import torch
from laya.agent import Agent
from laya.common import QTYPE_NAMES, QTYPES, render_options, serialize_state
NONE_TYPE = 3 # token type of [CLS], the state and the final [SEP]
OPTION_TOKENS = 48 # per-option token cap
QUESTION_TOKENS = 256 # per-question instruction cap
class Snap:
def __init__(self, repo_or_path: str = "Otron/snap-1", device: Optional[str] = None,
token: Optional[str] = None):
path = repo_or_path
if not os.path.isdir(path):
from huggingface_hub import snapshot_download
path = snapshot_download(repo_or_path, token=token, ignore_patterns=["coreml/*"])
agent = Agent(path, device=device)
self.cfg = agent.cfg
self.tok = agent.tok
self.model = agent.model.eval()
self.device = next(self.model.parameters()).device
self.max_len = int(self.cfg.get("max_len", 4096))
self.head_max_len = int(self.cfg.get("head_max_len", 3584))
# the embedding Snap gives [CLS] and state tokens in its packed layout
from safetensors.torch import load_file
none = next(iter(load_file(os.path.join(path, "extra.safetensors")).values()))
self.type_table = torch.cat([self.model.type_emb.weight.detach(), none[None].to(self.device)], 0)
self.temps = dict(self.cfg.get("temperature_by_options") or {})
self.temps.update({QTYPE_NAMES[i]: t for i, t in enumerate(self.cfg.get("temperature") or [])})
# ------------------------------------------------------------------ public
@torch.no_grad()
def decide(self, state: Union[str, dict, list], questions: Dict[str, Dict]) -> Dict[str, Dict]:
"""Answer every question about `state`. Returns {question id: answer}."""
mask = self.tok.mask_token
state_ids = self._ids(serialize_state(state).replace(mask, " "))
blocks = []
for qid, qdef in questions.items():
q = Agent._to_internal(qdef)
tokens, markers = self._block(q)
if len(tokens) <= self.head_max_len:
blocks.append((qid, q, tokens, markers))
answers = {}
for group in self._pack(blocks):
ids, types, where = [self.tok.cls_token_id], [NONE_TYPE], []
for qid, q, tokens, markers in group:
start = len(ids)
ids += tokens
types += [QTYPES[q["t"]]] * len(tokens)
where.append((qid, q, [start + m for m in markers]))
room = max(0, self.max_len - len(ids) - 1)
ids += state_ids[:room] + [self.tok.sep_token_id]
types += [NONE_TYPE] * (len(ids) - len(types))
scores = self._scores(ids, types)
for qid, q, positions in where:
answers[qid] = self._answer(q, [float(scores[p]) for p in positions])
return answers
# ------------------------------------------------------------------ internals
def _ids(self, text: str) -> List[int]:
return self.tok(text, add_special_tokens=False)["input_ids"]
def _block(self, q: Dict):
mask = self.tok.mask_token
head = self._ids("%s question: %s" % (q["t"], str(q["ins"]).replace(mask, " ")))[:QUESTION_TOKENS]
body, markers = [], []
for text in render_options(q):
markers.append(len(head) + 1 + len(body))
body += [self.tok.mask_token_id] + self._ids(" " + text.replace(mask, " "))[:OPTION_TOKENS]
return head + [self.tok.sep_token_id] + body + [self.tok.sep_token_id], markers
def _pack(self, blocks):
groups, current, used = [], [], 0
for b in blocks:
if current and used + len(b[2]) > self.head_max_len:
groups.append(current)
current, used = [], 0
current.append(b)
used += len(b[2])
if current:
groups.append(current)
return groups
def _scores(self, ids: List[int], types: List[int]) -> torch.Tensor:
m = self.model
x = torch.tensor([ids], device=self.device)
attn = torch.ones_like(x)
h = m.encoder(input_ids=x, attention_mask=attn).last_hidden_state
h = h + self.type_table[torch.tensor([types], device=self.device)].to(h.dtype)
if m.head is not None:
for layer in m.head.layers:
h = layer(h, src_key_padding_mask=~attn.bool())
return m.scorer(h).squeeze(-1)[0].float().cpu()
def _answer(self, q: Dict, logits: List[float]) -> Dict:
k = len(logits)
size = "2" if k <= 2 else "3-5" if k <= 5 else "6-10" if k <= 10 else "11+"
t = self.temps.get("%s:%s" % (q["t"], size), self.temps.get(q["t"], 1.0))
z = [v / max(1e-3, t) for v in logits]
top = max(z)
e = [math.exp(v - top) for v in z]
s = sum(e)
p = [v / s for v in e]
best = max(range(k), key=p.__getitem__)
if q["t"] == "noul":
return {"noul": p[1], "confidence": max(p)}
keys = list(q["crit"].keys()) if q["t"] == "choice" else [str(i) for i in range(k)]
out = {"confidence": p[best], "probabilities": dict(zip(keys, p))}
out["choice" if q["t"] == "choice" else "score"] = keys[best] if q["t"] == "choice" else best
return out
if __name__ == "__main__":
import sys
r = Snap(sys.argv[1] if len(sys.argv) > 1 else "Otron/snap-1")
print(json.dumps(r.decide(
{"message": "can you turn the volume down a bit"},
{"about_audio": {"type": "noul", "instructions": "The message is about sound or volume"},
"intent": {"type": "choice", "instructions": "What does the user want?",
"criteria": {"volume_down": "Lower the volume", "volume_up": "Raise the volume",
"other": "Something else"}}}), indent=1))
|