oev 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.
- oev/__init__.py +7 -0
- oev/benchmark.py +70 -0
- oev/benchmark_ext.py +213 -0
- oev/calibrate.py +22 -0
- oev/convert.py +61 -0
- oev/convert_banking77.py +88 -0
- oev/convert_typed.py +124 -0
- oev/data_gen.py +199 -0
- oev/dataset.py +90 -0
- oev/ensemble.py +49 -0
- oev/evaluate.py +124 -0
- oev/infer.py +63 -0
- oev/model.py +67 -0
- oev/presets.py +140 -0
- oev/rlcd.py +113 -0
- oev/serve.py +117 -0
- oev/tokenizer.py +26 -0
- oev/tokenizer_hf.py +29 -0
- oev/train.py +114 -0
- oev-0.1.0.dist-info/METADATA +442 -0
- oev-0.1.0.dist-info/RECORD +25 -0
- oev-0.1.0.dist-info/WHEEL +5 -0
- oev-0.1.0.dist-info/entry_points.txt +2 -0
- oev-0.1.0.dist-info/licenses/LICENSE +216 -0
- oev-0.1.0.dist-info/top_level.txt +1 -0
oev/__init__.py
ADDED
oev/benchmark.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import torch
|
|
3
|
+
import torch.nn.functional as F
|
|
4
|
+
from torch.utils.data import DataLoader
|
|
5
|
+
from oev.dataset import OEVDataset, collate
|
|
6
|
+
from oev.model import HFBackboneOEV
|
|
7
|
+
from oev.evaluate import load_model, ece
|
|
8
|
+
from oev.calibrate import fit_temperature_from_logits
|
|
9
|
+
from oev.tokenizer_hf import HFTokenPacker
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def stack_ragged(rows):
|
|
13
|
+
kmax = max(r.numel() for r in rows)
|
|
14
|
+
out = torch.full((len(rows), kmax), -1e4)
|
|
15
|
+
for i, r in enumerate(rows):
|
|
16
|
+
out[i, : r.numel()] = r
|
|
17
|
+
return out
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def evaluate_benchmark(checkpoint, data_dir, calibrate=True, batch_size=64):
|
|
21
|
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
22
|
+
model = load_model(checkpoint, device)
|
|
23
|
+
if isinstance(model, HFBackboneOEV):
|
|
24
|
+
packer = HFTokenPacker(model.cfg["backbone"])
|
|
25
|
+
max_len = model.cfg["max_len"]
|
|
26
|
+
else:
|
|
27
|
+
packer = None
|
|
28
|
+
max_len = model.cfg.max_len
|
|
29
|
+
dl = DataLoader(OEVDataset(f"{data_dir}/test.jsonl", max_len, packer=packer), batch_size=batch_size, collate_fn=collate)
|
|
30
|
+
confs, corr = [], []
|
|
31
|
+
val_logits, val_labels = [], []
|
|
32
|
+
if calibrate:
|
|
33
|
+
vdl = DataLoader(OEVDataset(f"{data_dir}/valid.jsonl", max_len, packer=packer), batch_size=batch_size, collate_fn=collate)
|
|
34
|
+
with torch.no_grad():
|
|
35
|
+
for batch in vdl:
|
|
36
|
+
logits = model(batch["ids"].to(device), batch["pad_mask"].to(device), batch["anchor_pos"].to(device)) + batch["logits_mask"].to(device)
|
|
37
|
+
keep = batch["anchor_valid"].sum(dim=1)
|
|
38
|
+
for i in range(logits.size(0)):
|
|
39
|
+
row = logits[i][: keep[i]]
|
|
40
|
+
if row.numel() >= 2:
|
|
41
|
+
val_logits.append(row)
|
|
42
|
+
val_labels.append(batch["labels"][i])
|
|
43
|
+
temperature = fit_temperature_from_logits(stack_ragged(val_logits), torch.stack(val_labels))
|
|
44
|
+
else:
|
|
45
|
+
temperature = 1.0
|
|
46
|
+
with torch.no_grad():
|
|
47
|
+
for batch in dl:
|
|
48
|
+
logits = model(batch["ids"].to(device), batch["pad_mask"].to(device), batch["anchor_pos"].to(device)) + batch["logits_mask"].to(device)
|
|
49
|
+
probs = F.softmax(logits / temperature, dim=-1)
|
|
50
|
+
conf, pred = probs.max(dim=-1)
|
|
51
|
+
for i in range(len(batch["labels"])):
|
|
52
|
+
confs.append(conf[i].item())
|
|
53
|
+
corr.append(int(pred[i].item() == batch["labels"][i].item()))
|
|
54
|
+
return {
|
|
55
|
+
"accuracy": sum(corr) / len(corr),
|
|
56
|
+
"n": len(corr),
|
|
57
|
+
"ece": ece(confs, corr),
|
|
58
|
+
"temperature": temperature,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == "__main__":
|
|
63
|
+
p = argparse.ArgumentParser()
|
|
64
|
+
p.add_argument("--checkpoint", required=True)
|
|
65
|
+
p.add_argument("--data-dir", required=True)
|
|
66
|
+
p.add_argument("--no-calibrate", action="store_true")
|
|
67
|
+
args = p.parse_args()
|
|
68
|
+
r = evaluate_benchmark(args.checkpoint, args.data_dir, calibrate=not args.no_calibrate)
|
|
69
|
+
for k, v in r.items():
|
|
70
|
+
print(f"{k}: {v:.4f}" if isinstance(v, float) else f"{k}: {v}")
|
oev/benchmark_ext.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Extended benchmark metrics for typed-decisions: soft accuracy, Brier score,
|
|
2
|
+
score MAE, and per-workflow / per-primitive breakdowns.
|
|
3
|
+
|
|
4
|
+
Mirrors the metric set published in Laya's typed-decisions table so results
|
|
5
|
+
are directly comparable column-for-column.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python -m oev.benchmark_ext --checkpoint CKPT --data-dir data/typed
|
|
9
|
+
Ensembles:
|
|
10
|
+
python -m oev.benchmark_ext --ckpts a.pt,b.pt,c.pt --data-dir data/typed
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
|
|
16
|
+
import torch
|
|
17
|
+
import torch.nn.functional as F
|
|
18
|
+
|
|
19
|
+
from oev.evaluate import load_model
|
|
20
|
+
from oev.tokenizer_hf import HFTokenPacker
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _predict_probs(model, packer, state, question, device, gamma=1.0):
|
|
24
|
+
pq = {
|
|
25
|
+
"name": question["name"],
|
|
26
|
+
"type": question["type"],
|
|
27
|
+
"instructions": question.get("instructions", question["type"]),
|
|
28
|
+
"options": question["options"],
|
|
29
|
+
"answer": question["answer"],
|
|
30
|
+
}
|
|
31
|
+
ids, anchors, label = packer.pack(state, pq, model.cfg["max_len"])
|
|
32
|
+
tids = torch.tensor([ids], device=device)
|
|
33
|
+
pmask = torch.zeros(1, len(ids), dtype=torch.bool, device=device)
|
|
34
|
+
apos = torch.tensor([anchors], device=device)
|
|
35
|
+
with torch.no_grad():
|
|
36
|
+
logits = model(tids, pmask, apos)
|
|
37
|
+
probs = torch.softmax(logits[0].float(), dim=-1)
|
|
38
|
+
if gamma != 1.0:
|
|
39
|
+
probs = probs.clamp_min(1e-9) ** gamma
|
|
40
|
+
probs = probs / probs.sum()
|
|
41
|
+
return probs, label, question["type"], question.get("target")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def ece_metric(confs, corrs, n_bins=10):
|
|
45
|
+
"""Expected Calibration Error: weighted |confidence - accuracy| over bins."""
|
|
46
|
+
if not confs:
|
|
47
|
+
return 0.0
|
|
48
|
+
bins = [[] for _ in range(n_bins)]
|
|
49
|
+
for c, o in zip(confs, corrs):
|
|
50
|
+
b = min(int(c * n_bins), n_bins - 1)
|
|
51
|
+
bins[b].append((c, o))
|
|
52
|
+
e = 0.0
|
|
53
|
+
for b in bins:
|
|
54
|
+
if not b:
|
|
55
|
+
continue
|
|
56
|
+
acc = sum(o for _, o in b) / len(b)
|
|
57
|
+
conf = sum(c for c, _ in b) / len(b)
|
|
58
|
+
e += (len(b) / len(confs)) * abs(conf - acc)
|
|
59
|
+
return e
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def evaluate_metrics(checkpoints, data_dir, device="cuda", gamma=1.0, latency=False):
|
|
63
|
+
if device == "cuda" and not torch.cuda.is_available():
|
|
64
|
+
device = "cpu"
|
|
65
|
+
models = [load_model(c, device) for c in checkpoints]
|
|
66
|
+
packers = [HFTokenPacker(m.cfg["backbone"]) for m in models]
|
|
67
|
+
|
|
68
|
+
rows = [json.loads(l) for l in open(f"{data_dir}/test.jsonl", encoding="utf-8")]
|
|
69
|
+
|
|
70
|
+
n = correct = 0
|
|
71
|
+
soft_acc_sum = 0.0
|
|
72
|
+
brier_sum = 0.0
|
|
73
|
+
score_mae_sum = 0.0
|
|
74
|
+
score_n = 0
|
|
75
|
+
confs = []
|
|
76
|
+
corrs = []
|
|
77
|
+
by_type = {}
|
|
78
|
+
by_domain = {}
|
|
79
|
+
|
|
80
|
+
with torch.no_grad():
|
|
81
|
+
for r in rows:
|
|
82
|
+
for q in r["questions"]:
|
|
83
|
+
probs_sum = None
|
|
84
|
+
label = None
|
|
85
|
+
target = None
|
|
86
|
+
qtype = None
|
|
87
|
+
for m, p in zip(models, packers):
|
|
88
|
+
probs, label, qtype, target = _predict_probs(m, p, r["state"], q, device, gamma=gamma)
|
|
89
|
+
probs_sum = probs if probs_sum is None else probs_sum + probs
|
|
90
|
+
probs = probs_sum / len(models)
|
|
91
|
+
|
|
92
|
+
n += 1
|
|
93
|
+
hit = int(probs.argmax().item() == label)
|
|
94
|
+
correct += hit
|
|
95
|
+
confs.append(probs.max().item())
|
|
96
|
+
corrs.append(hit)
|
|
97
|
+
|
|
98
|
+
# soft accuracy: the probability mass the model put on the gold answer
|
|
99
|
+
soft_acc_sum += probs[label].item()
|
|
100
|
+
|
|
101
|
+
# Brier score against the gold one-hot (lower is better)
|
|
102
|
+
oh = torch.zeros_like(probs)
|
|
103
|
+
oh[label] = 1.0
|
|
104
|
+
brier_sum += ((probs - oh) ** 2).sum().item()
|
|
105
|
+
|
|
106
|
+
# score MAE: |expected level - gold level| for score questions
|
|
107
|
+
if qtype == "score":
|
|
108
|
+
levels = torch.arange(probs.numel(), dtype=torch.float32, device=probs.device)
|
|
109
|
+
ev = (probs * levels).sum().item()
|
|
110
|
+
score_mae_sum += abs(ev - label)
|
|
111
|
+
score_n += 1
|
|
112
|
+
|
|
113
|
+
agg = by_type.setdefault(qtype, [0, 0, 0.0])
|
|
114
|
+
agg[0] += hit
|
|
115
|
+
agg[1] += 1
|
|
116
|
+
agg[2] += probs[label].item()
|
|
117
|
+
|
|
118
|
+
agg = by_domain.setdefault(r.get("domain", "typed"), [0, 0, 0.0])
|
|
119
|
+
agg[0] += hit
|
|
120
|
+
agg[1] += 1
|
|
121
|
+
agg[2] += probs[label].item()
|
|
122
|
+
|
|
123
|
+
result = {
|
|
124
|
+
"accuracy": correct / n,
|
|
125
|
+
"soft_acc": soft_acc_sum / n,
|
|
126
|
+
"brier": brier_sum / n,
|
|
127
|
+
"score_mae": (score_mae_sum / score_n) if score_n else None,
|
|
128
|
+
"ece": ece_metric(confs, corrs),
|
|
129
|
+
"n": n,
|
|
130
|
+
}
|
|
131
|
+
print(f"accuracy : {result['accuracy']:.4f}")
|
|
132
|
+
print(f"soft acc : {result['soft_acc']:.4f}")
|
|
133
|
+
print(f"brier : {result['brier']:.4f}")
|
|
134
|
+
if result["score_mae"] is not None:
|
|
135
|
+
print(f"score MAE: {result['score_mae']:.4f}")
|
|
136
|
+
print(f"ece : {result['ece']:.4f}")
|
|
137
|
+
print(f"n : {result['n']}")
|
|
138
|
+
|
|
139
|
+
print("\nper primitive:")
|
|
140
|
+
for t, (c, tot, s) in sorted(by_type.items()):
|
|
141
|
+
print(f" {t:<7} acc {c / tot:.4f} soft acc {s / tot:.4f} (n={tot})")
|
|
142
|
+
print("per workflow:")
|
|
143
|
+
for d, (c, tot, s) in sorted(by_domain.items()):
|
|
144
|
+
print(f" {d:<28} acc {c / tot:.4f} soft acc {s / tot:.4f} (n={tot})")
|
|
145
|
+
|
|
146
|
+
if latency:
|
|
147
|
+
measure_latency(models[0], packers[0], rows, device)
|
|
148
|
+
return result
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def measure_latency(model, packer, rows, device, n_single=50, n_batch=200, batch_size=32):
|
|
152
|
+
"""Latency protocol: p50 single-question and batched per-question throughput.
|
|
153
|
+
Model must already be on `device` and warmed up by at least a few calls."""
|
|
154
|
+
import time
|
|
155
|
+
|
|
156
|
+
qs = [(r["state"], q) for r in rows for q in r["questions"]]
|
|
157
|
+
|
|
158
|
+
def run_one(s, q):
|
|
159
|
+
pq = {"name": q["name"], "type": q["type"], "instructions": q.get("instructions", q["type"]), "options": q["options"], "answer": q["answer"]}
|
|
160
|
+
ids, an, _ = packer.pack(s, pq, model.cfg["max_len"])
|
|
161
|
+
t0 = time.perf_counter()
|
|
162
|
+
with torch.no_grad():
|
|
163
|
+
model(torch.tensor([ids], device=device), torch.zeros(1, len(ids), dtype=torch.bool, device=device), torch.tensor([an], device=device))
|
|
164
|
+
if device == "cuda":
|
|
165
|
+
torch.cuda.synchronize()
|
|
166
|
+
return (time.perf_counter() - t0) * 1000
|
|
167
|
+
|
|
168
|
+
for s, q in qs[:3]: # warmup
|
|
169
|
+
run_one(s, q)
|
|
170
|
+
times = sorted(run_one(s, q) for s, q in qs[:n_single])
|
|
171
|
+
print(f"\nlatency single-question p50: {times[len(times) // 2]:.1f} ms (n={n_single}, warmup 3)")
|
|
172
|
+
|
|
173
|
+
# batched throughput: pack n_batch questions, pad to common length, one forward per batch
|
|
174
|
+
items = qs[:n_batch]
|
|
175
|
+
t0 = time.perf_counter()
|
|
176
|
+
done = 0
|
|
177
|
+
for i in range(0, len(items), batch_size):
|
|
178
|
+
chunk = items[i : i + batch_size]
|
|
179
|
+
packed = []
|
|
180
|
+
for s, q in chunk:
|
|
181
|
+
pq = {"name": q["name"], "type": q["type"], "instructions": q.get("instructions", q["type"]), "options": q["options"], "answer": q["answer"]}
|
|
182
|
+
packed.append(packer.pack(s, pq, model.cfg["max_len"]))
|
|
183
|
+
L = max(len(p[0]) for p in packed)
|
|
184
|
+
A = max(len(p[1]) for p in packed)
|
|
185
|
+
B = len(packed)
|
|
186
|
+
ids = torch.zeros(B, L, dtype=torch.long, device=device)
|
|
187
|
+
pmask = torch.ones(B, L, dtype=torch.bool, device=device)
|
|
188
|
+
apos = torch.zeros(B, A, dtype=torch.long, device=device)
|
|
189
|
+
for j, (pid, pan, _) in enumerate(packed):
|
|
190
|
+
ids[j, : len(pid)] = torch.tensor(pid, device=device)
|
|
191
|
+
pmask[j, : len(pid)] = False
|
|
192
|
+
apos[j, : len(pan)] = torch.tensor(pan, device=device)
|
|
193
|
+
with torch.no_grad():
|
|
194
|
+
model(ids, pmask, apos)
|
|
195
|
+
done += B
|
|
196
|
+
if device == "cuda":
|
|
197
|
+
torch.cuda.synchronize()
|
|
198
|
+
total_ms = (time.perf_counter() - t0) * 1000
|
|
199
|
+
print(f"latency batched: {total_ms / done:.1f} ms/question (n={done}, batch={batch_size})")
|
|
200
|
+
print(f"throughput: {done / (total_ms / 1000):.0f} questions/sec")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
if __name__ == "__main__":
|
|
204
|
+
p = argparse.ArgumentParser()
|
|
205
|
+
p.add_argument("--checkpoint", default=None)
|
|
206
|
+
p.add_argument("--ckpts", default=None, help="comma-separated checkpoints (ensemble)")
|
|
207
|
+
p.add_argument("--data-dir", default="data/typed")
|
|
208
|
+
p.add_argument("--sharpen", type=float, default=1.0, help="confidence exponent gamma; >1 sharpens distributions")
|
|
209
|
+
p.add_argument("--latency", action="store_true", help="also measure single p50 and batched throughput")
|
|
210
|
+
args = p.parse_args()
|
|
211
|
+
ckpts = ([c.strip() for c in args.ckpts.split(",") if c.strip()]
|
|
212
|
+
if args.ckpts else [args.checkpoint])
|
|
213
|
+
evaluate_metrics(ckpts, args.data_dir, gamma=args.sharpen, latency=args.latency)
|
oev/calibrate.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def apply_temperature(logits, temperature):
|
|
5
|
+
return logits / temperature
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def fit_temperature_from_logits(logits, labels, max_iter=200, lr=0.05):
|
|
9
|
+
logits = logits.detach().float().cpu()
|
|
10
|
+
labels = labels.detach().cpu()
|
|
11
|
+
log_t = torch.zeros(1, requires_grad=True)
|
|
12
|
+
opt = torch.optim.LBFGS([log_t], max_iter=max_iter, lr=lr)
|
|
13
|
+
labels = labels.long()
|
|
14
|
+
|
|
15
|
+
def closure():
|
|
16
|
+
opt.zero_grad()
|
|
17
|
+
loss = torch.nn.functional.cross_entropy(logits / log_t.exp(), labels)
|
|
18
|
+
loss.backward()
|
|
19
|
+
return loss
|
|
20
|
+
|
|
21
|
+
opt.step(closure)
|
|
22
|
+
return float(log_t.exp().item())
|
oev/convert.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
AG_LABELS = ["world", "sports", "business", "sci/tech"]
|
|
5
|
+
EMOTION_LABELS = ["sadness", "joy", "love", "anger", "fear", "surprise"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def convert_rows(rows, labels, domain, qname, instructions):
|
|
9
|
+
for i, r in enumerate(rows):
|
|
10
|
+
yield {
|
|
11
|
+
"id": f"{domain}-{i:06d}",
|
|
12
|
+
"domain": domain,
|
|
13
|
+
"state": r["text"],
|
|
14
|
+
"questions": [
|
|
15
|
+
{
|
|
16
|
+
"name": qname,
|
|
17
|
+
"type": "choice",
|
|
18
|
+
"options": list(labels),
|
|
19
|
+
"answer": labels[r["label"]],
|
|
20
|
+
"instructions": instructions,
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def download_ag_news():
|
|
27
|
+
from datasets import load_dataset
|
|
28
|
+
|
|
29
|
+
return load_dataset("fancyzhx/ag_news")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def download_emotion():
|
|
33
|
+
from datasets import load_dataset
|
|
34
|
+
|
|
35
|
+
return load_dataset("dair-ai/emotion")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def build_domain(hf_split_rows, labels, domain, qname, instructions):
|
|
39
|
+
return list(convert_rows(hf_split_rows, labels, domain, qname, instructions))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def write_jsonl(rows, path):
|
|
43
|
+
p = Path(path)
|
|
44
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
with open(p, "w", encoding="utf-8") as f:
|
|
46
|
+
for r in rows:
|
|
47
|
+
f.write(json.dumps(r) + "\n")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
ag = download_ag_news()
|
|
52
|
+
em = download_emotion()
|
|
53
|
+
ag_train = build_domain(ag["train"], AG_LABELS, "ag_news", "topic", "Which topic does this news article belong to?")
|
|
54
|
+
write_jsonl(ag_train[:-2000], "data/ag_news/train.jsonl")
|
|
55
|
+
write_jsonl(ag_train[-2000:], "data/ag_news/valid.jsonl")
|
|
56
|
+
write_jsonl(build_domain(ag["test"], AG_LABELS, "ag_news", "topic", "Which topic does this news article belong to?"), "data/ag_news/test.jsonl")
|
|
57
|
+
em_train = build_domain(em["train"], EMOTION_LABELS, "emotion", "emotion", "Which emotion does this text express?")
|
|
58
|
+
write_jsonl(em_train[:-1000], "data/emotion/train.jsonl")
|
|
59
|
+
write_jsonl(em_train[-1000:], "data/emotion/valid.jsonl")
|
|
60
|
+
write_jsonl(build_domain(em["test"], EMOTION_LABELS, "emotion", "emotion", "Which emotion does this text express?"), "data/emotion/test.jsonl")
|
|
61
|
+
print("ag_news train/valid/test written; emotion train/valid/test written")
|
oev/convert_banking77.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Convert PolyAI/banking77 into OEV format.
|
|
2
|
+
|
|
3
|
+
Banking77: 13,083 customer-service queries, 77 fine-grained intents.
|
|
4
|
+
This is the high-cardinality test: 77 options in ONE choice question.
|
|
5
|
+
Laya's published number is 0.425 (head token budget squeezes each label
|
|
6
|
+
to 3-4 tokens); Jev publishes 0.870. Our anchor mechanism gives every
|
|
7
|
+
option full tokens, so this is our most winnable remaining benchmark row.
|
|
8
|
+
|
|
9
|
+
One choice question per state, all 77 intents as options with their
|
|
10
|
+
human-readable intent names as option text.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
python -m oev.convert_banking77
|
|
14
|
+
Writes:
|
|
15
|
+
data/banking77/train.jsonl (~10k cases, 1 question each)
|
|
16
|
+
data/banking77/valid.jsonl (1,000 cases)
|
|
17
|
+
data/banking77/test.jsonl (3,000 cases)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def download_banking77():
|
|
25
|
+
from datasets import load_dataset
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
ds = load_dataset("PolyAI/banking77")
|
|
29
|
+
except RuntimeError:
|
|
30
|
+
# new datasets versions dropped script support; use the auto-converted parquet branch
|
|
31
|
+
ds = load_dataset("PolyAI/banking77", revision="refs/convert/parquet")
|
|
32
|
+
return ds["train"], ds["test"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def intent_names():
|
|
36
|
+
"""Official BANKING77 intent names in the dataset's label-id order,
|
|
37
|
+
fetched from the dataset's schema so it can never drift from the ids."""
|
|
38
|
+
import json
|
|
39
|
+
import urllib.request
|
|
40
|
+
|
|
41
|
+
url = "https://huggingface.co/datasets/PolyAI/banking77/resolve/main/dataset_infos.json"
|
|
42
|
+
data = json.load(urllib.request.urlopen(url, timeout=30))
|
|
43
|
+
names = list(data.values())[0]["features"]["label"]["names"]
|
|
44
|
+
assert len(names) == 77, f"expected 77 official intents, got {len(names)}"
|
|
45
|
+
return names
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def write_jsonl(rows, path):
|
|
49
|
+
p = Path(path)
|
|
50
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
with open(p, "w", encoding="utf-8") as f:
|
|
52
|
+
for r in rows:
|
|
53
|
+
f.write(json.dumps(r) + "\n")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def convert(rows, names):
|
|
57
|
+
"""One case per query: a single choice question with ALL 77 intents."""
|
|
58
|
+
n = len(names)
|
|
59
|
+
for r in rows:
|
|
60
|
+
label = int(r["label"])
|
|
61
|
+
if not (0 <= label < n):
|
|
62
|
+
continue
|
|
63
|
+
yield {
|
|
64
|
+
"id": f"b77-{r.get('id', label)}",
|
|
65
|
+
"domain": "banking77",
|
|
66
|
+
"state": r["text"],
|
|
67
|
+
"questions": [{
|
|
68
|
+
"name": "intent",
|
|
69
|
+
"type": "choice",
|
|
70
|
+
"instructions": "Which banking intent does this customer query belong to? Choose the single closest match.",
|
|
71
|
+
"options": names,
|
|
72
|
+
"answer": names[label],
|
|
73
|
+
}],
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
train, test = download_banking77()
|
|
79
|
+
names = intent_names()
|
|
80
|
+
assert len(names) == 77, f"expected 77 intents, got {len(names)}"
|
|
81
|
+
|
|
82
|
+
train_rows = list(convert(train, names))
|
|
83
|
+
# carve a validation split off the train pool
|
|
84
|
+
write_jsonl(train_rows[:-1000], "data/banking77/train.jsonl")
|
|
85
|
+
write_jsonl(train_rows[-1000:], "data/banking77/valid.jsonl")
|
|
86
|
+
test_rows = list(convert(test, names))
|
|
87
|
+
write_jsonl(test_rows, "data/banking77/test.jsonl")
|
|
88
|
+
print(f"banking77 train/valid/test written: {len(train_rows) - 1000}/{1000}/{len(test_rows)} cases (77 options each)")
|
oev/convert_typed.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
NOUL_OPTIONS = ["no", "yes"]
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def render_state(raw):
|
|
8
|
+
try:
|
|
9
|
+
obj = json.loads(raw) if isinstance(raw, str) else raw
|
|
10
|
+
except (json.JSONDecodeError, TypeError):
|
|
11
|
+
return str(raw)
|
|
12
|
+
if isinstance(obj, str):
|
|
13
|
+
return obj
|
|
14
|
+
if isinstance(obj, dict):
|
|
15
|
+
return "\n".join(f"{k}: {v}" for k, v in obj.items())
|
|
16
|
+
return str(obj)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def score_levels(gold_q, criteria):
|
|
20
|
+
probs = gold_q.get("probabilities", {})
|
|
21
|
+
int_levels = [int(k) for k in probs if str(k).lstrip("-").isdigit()]
|
|
22
|
+
n = len(criteria) if isinstance(criteria, list) else 4
|
|
23
|
+
label = int(gold_q["label"])
|
|
24
|
+
return max(n, label + 1, (max(int_levels) + 1 if int_levels else 0))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def soft_target(gold_q, options):
|
|
28
|
+
probs = gold_q.get("probabilities", {})
|
|
29
|
+
total = 0.0
|
|
30
|
+
target = []
|
|
31
|
+
for o in options:
|
|
32
|
+
p = probs.get(o)
|
|
33
|
+
if p is None and o == "no":
|
|
34
|
+
p = probs.get("false")
|
|
35
|
+
elif p is None and o == "yes":
|
|
36
|
+
p = probs.get("true")
|
|
37
|
+
p = float(p) if p is not None else 0.0
|
|
38
|
+
target.append(p)
|
|
39
|
+
total += p
|
|
40
|
+
if total <= 0.0:
|
|
41
|
+
return None
|
|
42
|
+
return [p / total for p in target]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _enrich(option, desc):
|
|
46
|
+
d = (desc or "").strip()
|
|
47
|
+
return f"{option}: {d}" if d else option
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def convert_question(qname, q, gold_q):
|
|
51
|
+
t = q["type"]
|
|
52
|
+
crit = q.get("criteria", {})
|
|
53
|
+
base = {"name": qname, "type": t, "instructions": q.get("instructions", t)}
|
|
54
|
+
if t == "choice":
|
|
55
|
+
keys = list(crit.keys())
|
|
56
|
+
if str(gold_q["label"]) not in keys:
|
|
57
|
+
return None
|
|
58
|
+
target = soft_target(gold_q, keys)
|
|
59
|
+
options = [_enrich(k, crit[k]) for k in keys]
|
|
60
|
+
answer = options[keys.index(str(gold_q["label"]))]
|
|
61
|
+
elif t == "noul":
|
|
62
|
+
options = NOUL_OPTIONS
|
|
63
|
+
target = soft_target(gold_q, options)
|
|
64
|
+
answer = "yes" if str(gold_q["label"]).lower() == "true" else "no"
|
|
65
|
+
elif t == "score":
|
|
66
|
+
n = score_levels(gold_q, crit)
|
|
67
|
+
raw = [str(i) for i in range(n)]
|
|
68
|
+
target = soft_target(gold_q, raw)
|
|
69
|
+
descs = crit if isinstance(crit, list) else []
|
|
70
|
+
options = [_enrich(str(i), descs[i] if i < len(descs) else "") for i in range(n)]
|
|
71
|
+
label = int(gold_q["label"])
|
|
72
|
+
if label >= n:
|
|
73
|
+
return None
|
|
74
|
+
answer = options[label]
|
|
75
|
+
else:
|
|
76
|
+
return None
|
|
77
|
+
cq = {**base, "options": options, "answer": answer}
|
|
78
|
+
if target is not None:
|
|
79
|
+
cq["target"] = target
|
|
80
|
+
return cq
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def convert_rows_typed(rows):
|
|
84
|
+
for row in rows:
|
|
85
|
+
questions = json.loads(row["questions"]) if isinstance(row["questions"], str) else row["questions"]
|
|
86
|
+
gold = json.loads(row["gold"]) if isinstance(row["gold"], str) else row["gold"]
|
|
87
|
+
qds = []
|
|
88
|
+
for qname, q in questions.items():
|
|
89
|
+
if qname not in gold:
|
|
90
|
+
continue
|
|
91
|
+
cq = convert_question(qname, q, gold[qname])
|
|
92
|
+
if cq is not None:
|
|
93
|
+
qds.append(cq)
|
|
94
|
+
if not qds:
|
|
95
|
+
continue
|
|
96
|
+
yield {
|
|
97
|
+
"id": f"typed-{row['id']}",
|
|
98
|
+
"domain": row.get("workflow", "typed"),
|
|
99
|
+
"state": render_state(row["state"]),
|
|
100
|
+
"questions": qds,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def download_typed():
|
|
105
|
+
from datasets import load_dataset
|
|
106
|
+
|
|
107
|
+
return load_dataset("LocalLLaMA/typed-decisions", "all")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def write_jsonl(rows, path):
|
|
111
|
+
p = Path(path)
|
|
112
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
113
|
+
with open(p, "w", encoding="utf-8") as f:
|
|
114
|
+
for r in rows:
|
|
115
|
+
f.write(json.dumps(r) + "\n")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if __name__ == "__main__":
|
|
119
|
+
ds = download_typed()
|
|
120
|
+
train_rows = list(convert_rows_typed(ds["train"]))
|
|
121
|
+
write_jsonl(train_rows[:-200], "data/typed/train.jsonl")
|
|
122
|
+
write_jsonl(train_rows[-200:], "data/typed/valid.jsonl")
|
|
123
|
+
write_jsonl(list(convert_rows_typed(ds["test"])), "data/typed/test.jsonl")
|
|
124
|
+
print(f"typed train/valid/test written: {len(train_rows) - 200}/{200}/{len(ds['test'])} cases")
|