decidekit 0.1.0 → 0.2.0
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.
- package/CHANGELOG.md +30 -0
- package/README.md +37 -29
- package/dist/head.js +45 -1
- package/dist/index.d.ts +17 -6
- package/dist/index.js +20 -9
- package/package.json +93 -93
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0 (unreleased)
|
|
4
|
+
|
|
5
|
+
### Training
|
|
6
|
+
|
|
7
|
+
- From 8 examples per label (mean across labels), `train()` fits the head with full-batch Adam, starting from the same centroid and label-name point. Test accuracy with all data: BANKING77 90.6% to 93.1%, CLINC150 94.6% to 96.1%, HWU64 90.15% to 91.17% (one seed, on cached embeddings; through train() in eval/results-h14.json, BANKING77 93.1% and CLINC150 96.3%). At 5 per label nothing changes. At 10 per label: BANKING77 84.4% to 87.5%, CLINC150 92.2% to 92.5%, HWU64 85.50% to 85.38%. Adam's settings come from jimothy's `src/linear.ts` (MIT, Andrew Prifer).
|
|
8
|
+
- New option `solver`. Setting `learningRate` or `l2` without it keeps SGD, so tuned calls behave as before. The default `epochs` is 100 with Adam.
|
|
9
|
+
- Saved heads keep working unchanged. Retrained heads can score differently, so pick `minConfidence` thresholds again: the CLINC150 out-of-scope threshold moved from 0.9 to 0.98.
|
|
10
|
+
- Training takes longer: in Node, BANKING77 went from 84 s to 123 s on cached embeddings. In Chrome, BANKING77 took 208 s with a longest page stall of 19 ms.
|
|
11
|
+
|
|
12
|
+
## 0.1.1 (2026-11-02)
|
|
13
|
+
|
|
14
|
+
### Models
|
|
15
|
+
|
|
16
|
+
- The default model id is `debuffjp/decide-small` (Decide Small). Before 0.1.1 it was `Xenova/bge-small-en-v1.5`. The
|
|
17
|
+
weights are identical, so decisions don't change. For other languages the id is `debuffjp/decide-multilingual`
|
|
18
|
+
(Decide Multilingual), identical to `Xenova/multilingual-e5-small`.
|
|
19
|
+
- The old ids and heads recording them keep working. They resolve to the new ids, so `d.model` returns the new id. A
|
|
20
|
+
head trained on 0.1.1 records the new id, so a 0.1.0 decider needs that id passed as `model` to use it.
|
|
21
|
+
- Downloads of both models are pinned to a revision.
|
|
22
|
+
- New option `modelHost` serves model files from `<modelHost>/<model>/` instead of Hugging Face. It applies to the
|
|
23
|
+
whole page or process.
|
|
24
|
+
|
|
25
|
+
### Docs
|
|
26
|
+
|
|
27
|
+
- Label names count as 1 example in every doc, and the advice to turn them off with bge-small is gone: it applied to the
|
|
28
|
+
old weight of 3.
|
|
29
|
+
- Calibration, out-of-scope and zero-shot numbers are now bge-small's or labelled with the model they were measured on.
|
|
30
|
+
- `decide()` documents that a described label scores by the better of its name and description.
|
|
31
|
+
- The API reference covers `decidekit/typesafe`.
|
|
32
|
+
|
|
3
33
|
## 0.1.0 (2026-11-02)
|
|
4
34
|
|
|
5
35
|
The first release. Numbers are on public test splits unless noted; the scripts that produced them are in `eval/`.
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/decidekit) [](https://github.com/debuffjp/decidekit/actions/workflows/ci.yml) [](LICENSE) [](https://discord.gg/jshbDHSpCY)
|
|
4
4
|
|
|
5
|
-
**[Try it in your browser](https://debuff.jp/
|
|
5
|
+
**[Try it in your browser](https://debuff.jp/decide/playground/)** · [Watch it learn from an LLM](https://debuff.jp/decide/lanes/) · [Launch post](https://debuff.jp/blog/introducing-decide) · [Research: when the LLM is wrong](https://debuff.jp/research/learning-from-a-noisy-llm)
|
|
6
6
|
|
|
7
7
|
A decision model for the browser and Node: it picks one of your labels for a piece of text, with confidence you can trust, and learns from an LLM when it isn't sure. Your labels are the type. No server, no API key.
|
|
8
8
|
|
|
@@ -16,8 +16,8 @@ const r = await d.decide("my card never arrived", ["card_arrival", "refund", "ot
|
|
|
16
16
|
r.choice; // "card_arrival" | "refund" | "other"
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
Give it 5 examples per label and it reaches
|
|
20
|
-
reaches
|
|
19
|
+
Give it 5 examples per label and it reaches 84% on BANKING77 (77 intents); give it the full training set and it
|
|
20
|
+
reaches 93.1%, with each answer taking about 8 ms (median) in Chrome. Training runs in the user's tab, so their data never
|
|
21
21
|
leaves the device.
|
|
22
22
|
|
|
23
23
|
## Zero-shot
|
|
@@ -29,8 +29,8 @@ await d.decide(text, ["question", "bug_report", "praise"]);
|
|
|
29
29
|
await d.decide(text, { refund: "wants their money back", bug: "something is broken" });
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
Zero-shot is for trying things out:
|
|
33
|
-
Its confidence came out close to calibrated on those datasets (error 0.
|
|
32
|
+
Zero-shot is for trying things out: 66% on BANKING77 and 73% on CLINC150 (dev splits), against 90-94% trained.
|
|
33
|
+
Its confidence came out close to calibrated on those datasets (error 0.03-0.04), but check
|
|
34
34
|
it on your own labels before relying on a threshold.
|
|
35
35
|
|
|
36
36
|
## Train a head
|
|
@@ -146,23 +146,36 @@ That lets wasm use several threads: in our runs with all-MiniLM-L6-v2 it cut p95
|
|
|
146
146
|
|
|
147
147
|
Download on first visit: the model (34 MB) and onnxruntime's wasm (23.6 MB, 5.8 MB gzipped), both cached after.
|
|
148
148
|
|
|
149
|
-
## Models
|
|
150
|
-
|
|
151
|
-
decidekit runs the Decide models, each a sentence encoder plus a calibrated head:
|
|
152
|
-
|
|
153
|
-
| Model |
|
|
154
|
-
|
|
155
|
-
| Decide Small (default) | `
|
|
156
|
-
| Decide Multilingual | `
|
|
157
|
-
|
|
158
|
-
Pass the
|
|
159
|
-
|
|
149
|
+
## Models
|
|
150
|
+
|
|
151
|
+
decidekit runs the Decide models, each a sentence encoder plus a calibrated head:
|
|
152
|
+
|
|
153
|
+
| Model | Id | Download | Reads | Upstream |
|
|
154
|
+
|---|---|---|---|---|
|
|
155
|
+
| Decide Small (default) | [`debuffjp/decide-small`](https://huggingface.co/debuffjp/decide-small) | 34 MB | English | BAAI's [bge-small-en-v1.5](https://huggingface.co/BAAI/bge-small-en-v1.5), identical to `Xenova/bge-small-en-v1.5` |
|
|
156
|
+
| Decide Multilingual | [`debuffjp/decide-multilingual`](https://huggingface.co/debuffjp/decide-multilingual) | about 118 MB | 76% on Japanese and Chinese intents | intfloat's [multilingual-e5-small](https://huggingface.co/intfloat/multilingual-e5-small), identical to `Xenova/multilingual-e5-small` |
|
|
157
|
+
|
|
158
|
+
Pass the id as `model` to `createDecider()`. The files download from Hugging Face on first use, pinned to a revision, and
|
|
159
|
+
are then cached. To serve them yourself, pass `modelHost`: see [self-hosting](docs/guide-browser.md#host-the-model-files-yourself).
|
|
160
|
+
|
|
161
|
+
Before 0.1.1 the default id was `Xenova/bge-small-en-v1.5`. The old ids and heads recording them keep working: they
|
|
162
|
+
resolve to the new ids, so `d.model` returns the new id.
|
|
163
|
+
|
|
160
164
|
## Numbers
|
|
161
165
|
|
|
162
|
-
bge-small-en-v1.5
|
|
166
|
+
Decide Small (bge-small-en-v1.5, the default) at q8, without label names, Ryzen 7 260 laptop, Node 24 and headless Chrome 156, 2026-11-01. Accuracy
|
|
163
167
|
and calibration error (ECE, lower is better) on each dataset's test split, mean of 3 seeds (one run for "all"):
|
|
164
168
|
[eval/results-bge-small.json](eval/results-bge-small.json).
|
|
165
169
|
|
|
170
|
+
| Examples per label | 5 | 10 | 25 | all |
|
|
171
|
+
|---|---|---|---|---|
|
|
172
|
+
| BANKING77 (77 labels) | 83.7% | 87.5% | 90.2% | 93.1% |
|
|
173
|
+
| CLINC150 (150 labels) | 90.2% | 92.8% | 94.3% | 96.3% |
|
|
174
|
+
| ECE, BANKING77 | 0.050 | 0.015 | 0.013 | 0.013 |
|
|
175
|
+
| ECE, CLINC150 | 0.020 | 0.015 | 0.016 | 0.008 |
|
|
176
|
+
|
|
177
|
+
Those are 0.2, with label names, from [eval/results-h14.json](eval/results-h14.json). HWU64 with all data goes from 90.15% to 91.17%. Before 0.2 (SGD at every size, no label names, `eval/results-bge-small.json`):
|
|
178
|
+
|
|
166
179
|
| Examples per label | 5 | 10 | 25 | all |
|
|
167
180
|
|---|---|---|---|---|
|
|
168
181
|
| BANKING77 (77 labels) | 82.3% | 83.8% | 88.7% | 90.7% |
|
|
@@ -185,7 +198,7 @@ queries, [eval/tiny.ts](eval/tiny.ts), measured with all-MiniLM-L6-v2), 1 exampl
|
|
|
185
198
|
a threshold there.
|
|
186
199
|
|
|
187
200
|
**In Chrome** (BANKING77, full training set, worker, cross-origin isolated, 16 threads): 7.7 ms median and 13.8 ms
|
|
188
|
-
p95 per decision, 228 s to train on 10,003 examples with no page stall longer than 18 ms. all-MiniLM-L6-v2 in the
|
|
201
|
+
p95 per decision before 0.2, 228 s to train on 10,003 examples with no page stall longer than 18 ms. A 0.2 run on the same machine took 208 s, with a longest stall of 19 ms and 93.05% test accuracy (runs on this laptop vary: another 0.1 run took 147 s). all-MiniLM-L6-v2 in the
|
|
189
202
|
same run: 4.2 ms median, 7.7 ms p95. In Node: 3.2 ms median for a single text, 1.1 ms per text batched (MiniLM 2.0
|
|
190
203
|
and 0.7). Earlier MiniLM run: [bench/results.json](bench/results.json).
|
|
191
204
|
|
|
@@ -196,16 +209,16 @@ in-scope examples only, with `minConfidence` 0.93 picked on the validation split
|
|
|
196
209
|
|
|
197
210
|
**The smaller model.** `createDecider({ model: "Xenova/all-MiniLM-L6-v2" })` (23.7 MB, the pre-release default) is
|
|
198
211
|
about 1.8x faster per decision in Chrome and 3 to 5 points less accurate with few examples, about the same with all
|
|
199
|
-
the data. Pass it to keep using heads trained on it. Before
|
|
200
|
-
`{ labelNames: false }` with bge-small
|
|
212
|
+
the data. Pass it to keep using heads trained on it. Before 0.1.0 a label name counted as 3 examples and the README
|
|
213
|
+
suggested `{ labelNames: false }` with bge-small. It now counts as 1, which helps.
|
|
201
214
|
|
|
202
215
|
Reproduce: `npm run eval:suite` (Node), `npm run bench` then `npm run bench:run` (Chrome).
|
|
203
216
|
|
|
204
217
|
## Playground
|
|
205
218
|
|
|
206
|
-
Try it at [debuff.jp/
|
|
219
|
+
Try it at [debuff.jp/decide](https://debuff.jp/decide/). To run it locally: `npm run playground`, then serve `playground/dist` cross-origin isolated (`node bench/serve.mjs` style headers).
|
|
207
220
|
Try zero-shot labels, the ready-made heads, a head trained in the page and "none of these".
|
|
208
|
-
[Two lanes](https://debuff.jp/
|
|
221
|
+
[Two lanes](https://debuff.jp/decide/lanes/) shows `withFallback` live: decidekit starts from twelve label names,
|
|
209
222
|
asks a small LLM when it's unsure, learns from each answer, and replays the first 20 messages at the end.
|
|
210
223
|
|
|
211
224
|
## Ready-made heads
|
|
@@ -231,14 +244,9 @@ and offline apps, and text that can't leave the device. Details, code and poor f
|
|
|
231
244
|
| MediaPipe text classifier | yes | no, offline | no | no |
|
|
232
245
|
| LLM structured output (AI SDK `Output.choice`) | no, API | n/a | yes | no |
|
|
233
246
|
|
|
234
|
-
jimothy reports 92.4% on BANKING77, above our 90.7%. Pick it if you train in Node and want the higher number. Pick
|
|
235
|
-
decidekit if training has to happen in the page, from the user's own data.
|
|
236
|
-
|
|
237
|
-
[system-one](https://github.com/lukeramsden/system-one) is a different kind of tool: a typed client for hosted System One models (Jev and Laya). It runs no model itself.
|
|
238
|
-
|
|
239
247
|
## Docs
|
|
240
248
|
|
|
241
|
-
[docs/README.md](docs/README.md) indexes the guides (training, confidence and thresholds, the browser), the
|
|
249
|
+
[debuff.jp/decide/docs](https://debuff.jp/decide/docs/) (also in [docs/README.md](docs/README.md)) indexes the guides (training, confidence and thresholds, the browser), the
|
|
242
250
|
API reference, how it works and the FAQ.
|
|
243
251
|
|
|
244
252
|
## Community
|
|
@@ -249,6 +257,6 @@ issue template for them. See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
|
249
257
|
|
|
250
258
|
## Status
|
|
251
259
|
|
|
252
|
-
0.1. English with the default model, other languages with
|
|
260
|
+
0.1. English with the default model, other languages with Decide Multilingual ([FAQ](docs/faq.md#which-languages-does-it-support)); short texts (about a paragraph). Not done yet: WebGPU in the worker and fine-tuning the encoder (the likely route past about 91%).
|
|
253
261
|
|
|
254
262
|
Made by the [debuff](https://debuff.jp) team. MIT.
|
package/dist/head.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
const BATCH = 64;
|
|
2
2
|
const CENTROID_SCALE = 20;
|
|
3
|
+
// At 5 per label Adam lost 0.29 points on CLINC150, at 10 it gained on BANKING77 and CLINC150 (eval/spike). The cutoff
|
|
4
|
+
// between them wasn't measured on its own.
|
|
5
|
+
const ADAM_FROM = 8;
|
|
6
|
+
// Without a better holdout loss for this many epochs, stop the probe run.
|
|
7
|
+
const ADAM_PATIENCE = 30;
|
|
3
8
|
// Lets a tab paint and handle input between training batches. A message-channel task, unlike setTimeout, isn't
|
|
4
9
|
// throttled to once a second (or minute) when the tab is in the background.
|
|
5
10
|
function yieldToPage() {
|
|
@@ -56,11 +61,23 @@ function batchGradient(ids, X, y, W, B, gW, gb, z, C, dim) {
|
|
|
56
61
|
}
|
|
57
62
|
}
|
|
58
63
|
}
|
|
64
|
+
// One Adam step from the summed gradient. Adam's settings follow jimothy's src/linear.ts (MIT, Andrew Prifer).
|
|
65
|
+
function adamStep(p, g, m, v, rows, l2, lr, epoch) {
|
|
66
|
+
const c1 = 1 - 0.9 ** epoch, c2 = 1 - 0.999 ** epoch;
|
|
67
|
+
for (let k = 0; k < p.length; k++) {
|
|
68
|
+
const d = g[k] / rows + l2 * p[k];
|
|
69
|
+
m[k] = 0.9 * m[k] + 0.1 * d;
|
|
70
|
+
v[k] = 0.999 * v[k] + 0.001 * d * d;
|
|
71
|
+
p[k] -= (lr * m[k]) / c1 / (Math.sqrt(v[k] / c2) + 1e-8);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
59
74
|
/** Fits weights, biases and temperature to embeddings `X` with label indices `y`. */
|
|
60
75
|
export async function fitHead(X, y, labels, opts = {}, priors, priorWeight = 1, centroidScale = CENTROID_SCALE, smallFloor = 1, calibration) {
|
|
61
76
|
// smallFloor: the lowest temperature a head with under 50 held-out answers may get; createDecider picks it per model.
|
|
62
77
|
// calibration: labelled examples known to be right to fit the temperature on instead of the training examples.
|
|
63
|
-
|
|
78
|
+
// A set learningRate or l2 was tuned for SGD, so it keeps SGD unless the solver is named.
|
|
79
|
+
const adam = opts.solver ? opts.solver === "adam" : opts.learningRate === undefined && opts.l2 === undefined && X.length / labels.length >= ADAM_FROM;
|
|
80
|
+
const { epochs = adam ? 100 : 60, learningRate = adam ? 0.05 : 2, l2 = adam ? 0 : 1e-4, holdout = 0.1, seed = 7, onEpoch } = opts;
|
|
64
81
|
const dim = X[0].length, C = labels.length;
|
|
65
82
|
// One flat buffer, so the inner loops read typed memory instead of an array of arrays.
|
|
66
83
|
const Xf = new Float64Array(X.length * dim);
|
|
@@ -92,6 +109,33 @@ export async function fitHead(X, y, labels, opts = {}, priors, priorWeight = 1,
|
|
|
92
109
|
let best = { acc: check ? acc() : 0, weights: head.weights.slice(), bias: head.bias.slice(), epochs: 0 };
|
|
93
110
|
const gW = new Float64Array(C * dim), gb = new Float64Array(C), z = new Float64Array(C);
|
|
94
111
|
const W = head.weights, B = head.bias;
|
|
112
|
+
if (adam) {
|
|
113
|
+
const mW = new Float64Array(C * dim), vW = new Float64Array(C * dim), mB = new Float64Array(C), vB = new Float64Array(C);
|
|
114
|
+
let bestLoss = Infinity;
|
|
115
|
+
for (let ep = 0; ep < nEpochs; ep++) {
|
|
116
|
+
gW.fill(0);
|
|
117
|
+
gb.fill(0);
|
|
118
|
+
for (let b = 0; b < rows.length; b += BATCH) {
|
|
119
|
+
batchGradient(rows.slice(b, b + BATCH), Xf, y, W, B, gW, gb, z, C, dim);
|
|
120
|
+
await yieldToPage();
|
|
121
|
+
}
|
|
122
|
+
adamStep(W, gW, mW, vW, rows.length, l2, learningRate, ep + 1);
|
|
123
|
+
adamStep(B, gb, mB, vB, rows.length, 0, learningRate, ep + 1);
|
|
124
|
+
if (!check)
|
|
125
|
+
continue;
|
|
126
|
+
const loss = check.reduce((sum, i) => sum - Math.log(softmax(logits(head, X[i]), 1)[y[i]] + 1e-300), 0);
|
|
127
|
+
if (onEpoch)
|
|
128
|
+
onEpoch(ep, acc());
|
|
129
|
+
if (loss < bestLoss * (1 - 1e-7)) {
|
|
130
|
+
bestLoss = loss;
|
|
131
|
+
best = { acc: 0, weights: W.slice(), bias: B.slice(), epochs: ep + 1 };
|
|
132
|
+
}
|
|
133
|
+
if (ep + 1 - best.epochs >= ADAM_PATIENCE)
|
|
134
|
+
break;
|
|
135
|
+
await yieldToPage();
|
|
136
|
+
}
|
|
137
|
+
return check ? { ...head, weights: best.weights, bias: best.bias, epochs: best.epochs } : { ...head, epochs: nEpochs };
|
|
138
|
+
}
|
|
95
139
|
const order = [...rows];
|
|
96
140
|
for (let ep = 0; ep < nEpochs; ep++) {
|
|
97
141
|
order.sort(() => rand() - 0.5);
|
package/dist/index.d.ts
CHANGED
|
@@ -51,11 +51,16 @@ export interface TrainOptions<L extends string = string> {
|
|
|
51
51
|
* can still be chosen. Needs `labelNames`.
|
|
52
52
|
*/
|
|
53
53
|
labels?: readonly L[];
|
|
54
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* How to fit the head. By default `adam` from 8 examples per label (mean across labels), `sgd` below that. Setting
|
|
56
|
+
* `learningRate` or `l2` without this also picks `sgd`.
|
|
57
|
+
*/
|
|
58
|
+
solver?: "adam" | "sgd";
|
|
59
|
+
/** Most passes over the examples. Default 100 with `adam`, 60 with `sgd`; the best holdout epoch is kept. */
|
|
55
60
|
epochs?: number;
|
|
56
|
-
/** Default
|
|
61
|
+
/** Default 0.05 with `adam`, 2 with `sgd`. */
|
|
57
62
|
learningRate?: number;
|
|
58
|
-
/** Weight decay. Default 1e-4
|
|
63
|
+
/** Weight decay. Default 0 with `adam`, 1e-4 with `sgd`. */
|
|
59
64
|
l2?: number;
|
|
60
65
|
/** Share of examples held out for early stopping and temperature fitting. Default 0.1. */
|
|
61
66
|
holdout?: number;
|
|
@@ -72,13 +77,19 @@ export interface TrainOptions<L extends string = string> {
|
|
|
72
77
|
}
|
|
73
78
|
export interface DeciderOptions {
|
|
74
79
|
/**
|
|
75
|
-
* A Hugging Face sentence-embedding model with ONNX weights. Default `
|
|
76
|
-
*
|
|
80
|
+
* A Hugging Face sentence-embedding model with ONNX weights. Default `debuffjp/decide-small` (Decide Small, 34 MB at q8).
|
|
81
|
+
* Before 0.1.1 the default id was `Xenova/bge-small-en-v1.5`, the same weights; that id and heads recording it still work.
|
|
82
|
+
* Pre-release builds defaulted to `Xenova/all-MiniLM-L6-v2` (23.7 MB): smaller and about 1.8x faster in Chrome, 3 to 5 points less
|
|
77
83
|
* accurate. Pass it to keep using heads trained on it.
|
|
78
84
|
*/
|
|
79
85
|
model?: string;
|
|
80
86
|
/** Defaults to transformers.js' choice: wasm in browsers, cpu in Node. */
|
|
81
87
|
device?: "wasm" | "webgpu" | "cpu";
|
|
88
|
+
/**
|
|
89
|
+
* Serve model files from your own host instead of Hugging Face, as `<modelHost>/<model>/`, for example
|
|
90
|
+
* `https://cdn.example.com/models/debuffjp/decide-small/onnx/model_quantized.onnx`. Applies to the whole page or process.
|
|
91
|
+
*/
|
|
92
|
+
modelHost?: string;
|
|
82
93
|
/** Weight precision. Default `q8`. */
|
|
83
94
|
dtype?: "q8" | "fp16" | "fp32";
|
|
84
95
|
/** Called as model files download (first load only; later loads come from cache). */
|
|
@@ -98,7 +109,7 @@ export interface DeciderOptions {
|
|
|
98
109
|
* `train()`. In a browser, prefer `createWorkerDecider()` from `decidekit/browser`: the encoder blocks the thread
|
|
99
110
|
* it runs on.
|
|
100
111
|
*/
|
|
101
|
-
export declare function createDecider({ model, device, dtype, temperature, onProgress }?: DeciderOptions): Promise<{
|
|
112
|
+
export declare function createDecider({ model: requested, device, dtype, temperature, onProgress, modelHost }?: DeciderOptions): Promise<{
|
|
102
113
|
model: string;
|
|
103
114
|
decide: {
|
|
104
115
|
<const L extends string>(text: string, using: Choices<L>, options?: {
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// decide(text, options): typed, calibrated decisions on device. Zero-shot compares the text with each option's
|
|
2
2
|
// embedding; a trained head (a softmax layer over the same embeddings, 156 KB for 77 labels) is far more
|
|
3
3
|
// accurate. Both return the option literals as the choice type.
|
|
4
|
-
import { pipeline } from "@huggingface/transformers";
|
|
4
|
+
import { env, pipeline } from "@huggingface/transformers";
|
|
5
5
|
import { fitHead, logits, softmax } from "./head.js";
|
|
6
6
|
function isHead(c) {
|
|
7
7
|
if (Array.isArray(c) || !("weights" in c))
|
|
@@ -23,9 +23,15 @@ const LABEL_NAME_WEIGHT = 1;
|
|
|
23
23
|
// multilingual-e5-small's similarities sit close together, so it needs far lower temperatures: on MASSIVE in English
|
|
24
24
|
// and Japanese, 0.0105 for label names (calibration error 0.03 to 0.04; descriptions not yet measured) and a floor of
|
|
25
25
|
// 0.4 (eval/e5-settings.ts). It also expects "query: " before every text.
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
const
|
|
26
|
+
// The Decide models are unmodified copies of these upstream ids, so the old ids and heads trained on them still work.
|
|
27
|
+
const SMALL = "debuffjp/decide-small", MULTILINGUAL = "debuffjp/decide-multilingual";
|
|
28
|
+
const SAME_MODEL = { "Xenova/bge-small-en-v1.5": SMALL, "Xenova/multilingual-e5-small": MULTILINGUAL };
|
|
29
|
+
const canonical = (model) => SAME_MODEL[model] ?? model;
|
|
30
|
+
// Pinned so a later change on Hugging Face can't change what an installed version decides.
|
|
31
|
+
const REVISIONS = { [SMALL]: "188d690f86190863e3b4e44e29c88348b10230c4", [MULTILINGUAL]: "a3e353d26d846b2163db8acad65ef3d43893592e" };
|
|
32
|
+
const SMALL_HEAD_FLOOR = { [SMALL]: 0.5, [MULTILINGUAL]: 0.4 };
|
|
33
|
+
const TEMPERATURES = { [SMALL]: [0.03, 0.025], "Xenova/all-MiniLM-L6-v2": [0.05, 0.05], [MULTILINGUAL]: [0.0105, 0.0105] };
|
|
34
|
+
const PREFIX = { [MULTILINGUAL]: "query: " };
|
|
29
35
|
function toDecision(labels, p) {
|
|
30
36
|
let best = 0;
|
|
31
37
|
for (let i = 1; i < p.length; i++)
|
|
@@ -39,9 +45,14 @@ function toDecision(labels, p) {
|
|
|
39
45
|
* `train()`. In a browser, prefer `createWorkerDecider()` from `decidekit/browser`: the encoder blocks the thread
|
|
40
46
|
* it runs on.
|
|
41
47
|
*/
|
|
42
|
-
export async function createDecider({ model =
|
|
48
|
+
export async function createDecider({ model: requested = SMALL, device, dtype = "q8", temperature, onProgress, modelHost } = {}) {
|
|
49
|
+
const model = canonical(requested);
|
|
50
|
+
if (modelHost) {
|
|
51
|
+
env.remoteHost = modelHost.endsWith("/") ? modelHost : modelHost + "/";
|
|
52
|
+
env.remotePathTemplate = "{model}/";
|
|
53
|
+
}
|
|
43
54
|
const embed = await pipeline("feature-extraction", model, {
|
|
44
|
-
device, dtype,
|
|
55
|
+
device, dtype, revision: REVISIONS[model],
|
|
45
56
|
progress_callback: onProgress && ((p) => p.status === "progress" && onProgress({ file: p.file, loaded: p.loaded, total: p.total })),
|
|
46
57
|
});
|
|
47
58
|
const optionCache = new Map(), prefix = PREFIX[model] ?? "";
|
|
@@ -58,7 +69,7 @@ export async function createDecider({ model = "Xenova/bge-small-en-v1.5", device
|
|
|
58
69
|
async function score(q, using, minConfidence) {
|
|
59
70
|
let d;
|
|
60
71
|
if (isHead(using)) {
|
|
61
|
-
if (using.model !== model)
|
|
72
|
+
if (canonical(using.model) !== model)
|
|
62
73
|
throw new Error(`decidekit: this head was trained on ${using.model}, but the decider uses ${model}. Pass { model: "${using.model}" } to createDecider.`);
|
|
63
74
|
d = toDecision(using.labels, softmax(logits(using, q), using.temperature));
|
|
64
75
|
}
|
|
@@ -92,8 +103,8 @@ export async function createDecider({ model = "Xenova/bge-small-en-v1.5", device
|
|
|
92
103
|
/**
|
|
93
104
|
* Trains a head on labelled examples: mini-batch gradient descent on the embeddings, keeping the best holdout
|
|
94
105
|
* epoch, then a temperature fitted on the holdout so `confidence` is calibrated. Every label needs at least a few
|
|
95
|
-
* examples. Each label starts at the mean of its examples and its name, so 5 per label already gave
|
|
96
|
-
*
|
|
106
|
+
* examples. Each label starts at the mean of its examples and its name, so 5 per label already gave 82.3% on BANKING77 and
|
|
107
|
+
* 89.6% on CLINC150 (see the README).
|
|
97
108
|
*/
|
|
98
109
|
async function train(examples, opts = {}) {
|
|
99
110
|
const bad = examples.findIndex((e) => typeof e?.text !== "string" || typeof e?.label !== "string" || !e.label);
|
package/package.json
CHANGED
|
@@ -1,93 +1,93 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "decidekit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A System One model in the browser: typed, calibrated text decisions on the user's device. Learns from an LLM when it isn't sure.",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"repository": "github:debuffjp/decidekit",
|
|
7
|
-
"keywords": [
|
|
8
|
-
"decision model",
|
|
9
|
-
"system one",
|
|
10
|
-
"classification",
|
|
11
|
-
"intent",
|
|
12
|
-
"zero-shot",
|
|
13
|
-
"calibration",
|
|
14
|
-
"llm",
|
|
15
|
-
"embeddings",
|
|
16
|
-
"browser",
|
|
17
|
-
"typescript",
|
|
18
|
-
"text classification",
|
|
19
|
-
"transformers.js",
|
|
20
|
-
"on-device",
|
|
21
|
-
"llm fallback"
|
|
22
|
-
],
|
|
23
|
-
"type": "module",
|
|
24
|
-
"exports": {
|
|
25
|
-
".": {
|
|
26
|
-
"types": "./dist/index.d.ts",
|
|
27
|
-
"default": "./dist/index.js"
|
|
28
|
-
},
|
|
29
|
-
"./browser": {
|
|
30
|
-
"types": "./dist/browser.d.ts",
|
|
31
|
-
"default": "./dist/browser.js"
|
|
32
|
-
},
|
|
33
|
-
"./react": {
|
|
34
|
-
"types": "./dist/react.d.ts",
|
|
35
|
-
"default": "./dist/react.js"
|
|
36
|
-
},
|
|
37
|
-
"./switch": {
|
|
38
|
-
"types": "./dist/switch.d.ts",
|
|
39
|
-
"default": "./dist/switch.js"
|
|
40
|
-
},
|
|
41
|
-
"./typesafe": {
|
|
42
|
-
"types": "./dist/typesafe.d.ts",
|
|
43
|
-
"default": "./dist/typesafe.js"
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
"files": [
|
|
47
|
-
"dist",
|
|
48
|
-
"CHANGELOG.md"
|
|
49
|
-
],
|
|
50
|
-
"scripts": {
|
|
51
|
-
"build": "tsc -p tsconfig.build.json",
|
|
52
|
-
"prepack": "npm run build",
|
|
53
|
-
"typecheck": "tsc --noEmit",
|
|
54
|
-
"eval": "node --experimental-strip-types eval/banking77.ts",
|
|
55
|
-
"bench": "esbuild src/worker.ts src/browser.ts --bundle --format=esm --platform=browser --outdir=bench --alias:onnxruntime-web/webgpu=onnxruntime-web/wasm && node bench/serve.mjs",
|
|
56
|
-
"playground": "node playground/build.mjs",
|
|
57
|
-
"heads": "node --experimental-strip-types heads/build.ts",
|
|
58
|
-
"bench:run": "node bench/run.mjs",
|
|
59
|
-
"eval:suite": "node --experimental-strip-types eval/suite.ts",
|
|
60
|
-
"eval:hill": "node --experimental-strip-types eval/hill.ts",
|
|
61
|
-
"test": "node --test --experimental-strip-types \"test/*.test.ts\"",
|
|
62
|
-
"lint:prose": "node scripts/lint-prose.mjs",
|
|
63
|
-
"smoke": "node scripts/smoke.mjs"
|
|
64
|
-
},
|
|
65
|
-
"dependencies": {
|
|
66
|
-
"@huggingface/transformers": "4.2.0"
|
|
67
|
-
},
|
|
68
|
-
"devDependencies": {
|
|
69
|
-
"@types/node": "22.15.3",
|
|
70
|
-
"@types/react": "19.3.0",
|
|
71
|
-
"esbuild": "0.25.10",
|
|
72
|
-
"playwright-core": "1.64.1",
|
|
73
|
-
"react": "19.3.1",
|
|
74
|
-
"spryte": "0.11.0",
|
|
75
|
-
"typescript": "5.9.3"
|
|
76
|
-
},
|
|
77
|
-
"engines": {
|
|
78
|
-
"node": ">=22.18"
|
|
79
|
-
},
|
|
80
|
-
"homepage": "https://debuff.jp/
|
|
81
|
-
"bugs": "https://github.com/debuffjp/decidekit/issues",
|
|
82
|
-
"bin": {
|
|
83
|
-
"decidekit": "./dist/cli.js"
|
|
84
|
-
},
|
|
85
|
-
"peerDependencies": {
|
|
86
|
-
"react": ">=18"
|
|
87
|
-
},
|
|
88
|
-
"peerDependenciesMeta": {
|
|
89
|
-
"react": {
|
|
90
|
-
"optional": true
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "decidekit",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "A System One model in the browser: typed, calibrated text decisions on the user's device. Learns from an LLM when it isn't sure.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": "github:debuffjp/decidekit",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"decision model",
|
|
9
|
+
"system one",
|
|
10
|
+
"classification",
|
|
11
|
+
"intent",
|
|
12
|
+
"zero-shot",
|
|
13
|
+
"calibration",
|
|
14
|
+
"llm",
|
|
15
|
+
"embeddings",
|
|
16
|
+
"browser",
|
|
17
|
+
"typescript",
|
|
18
|
+
"text classification",
|
|
19
|
+
"transformers.js",
|
|
20
|
+
"on-device",
|
|
21
|
+
"llm fallback"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"default": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./browser": {
|
|
30
|
+
"types": "./dist/browser.d.ts",
|
|
31
|
+
"default": "./dist/browser.js"
|
|
32
|
+
},
|
|
33
|
+
"./react": {
|
|
34
|
+
"types": "./dist/react.d.ts",
|
|
35
|
+
"default": "./dist/react.js"
|
|
36
|
+
},
|
|
37
|
+
"./switch": {
|
|
38
|
+
"types": "./dist/switch.d.ts",
|
|
39
|
+
"default": "./dist/switch.js"
|
|
40
|
+
},
|
|
41
|
+
"./typesafe": {
|
|
42
|
+
"types": "./dist/typesafe.d.ts",
|
|
43
|
+
"default": "./dist/typesafe.js"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"files": [
|
|
47
|
+
"dist",
|
|
48
|
+
"CHANGELOG.md"
|
|
49
|
+
],
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsc -p tsconfig.build.json",
|
|
52
|
+
"prepack": "npm run build",
|
|
53
|
+
"typecheck": "tsc --noEmit",
|
|
54
|
+
"eval": "node --experimental-strip-types eval/banking77.ts",
|
|
55
|
+
"bench": "esbuild src/worker.ts src/browser.ts --bundle --format=esm --platform=browser --outdir=bench --alias:onnxruntime-web/webgpu=onnxruntime-web/wasm && node bench/serve.mjs",
|
|
56
|
+
"playground": "node playground/build.mjs",
|
|
57
|
+
"heads": "node --experimental-strip-types heads/build.ts",
|
|
58
|
+
"bench:run": "node bench/run.mjs",
|
|
59
|
+
"eval:suite": "node --experimental-strip-types eval/suite.ts",
|
|
60
|
+
"eval:hill": "node --experimental-strip-types eval/hill.ts",
|
|
61
|
+
"test": "node --test --experimental-strip-types \"test/*.test.ts\"",
|
|
62
|
+
"lint:prose": "node scripts/lint-prose.mjs",
|
|
63
|
+
"smoke": "node scripts/smoke.mjs"
|
|
64
|
+
},
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"@huggingface/transformers": "4.2.0"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@types/node": "22.15.3",
|
|
70
|
+
"@types/react": "19.3.0",
|
|
71
|
+
"esbuild": "0.25.10",
|
|
72
|
+
"playwright-core": "1.64.1",
|
|
73
|
+
"react": "19.3.1",
|
|
74
|
+
"spryte": "0.11.0",
|
|
75
|
+
"typescript": "5.9.3"
|
|
76
|
+
},
|
|
77
|
+
"engines": {
|
|
78
|
+
"node": ">=22.18"
|
|
79
|
+
},
|
|
80
|
+
"homepage": "https://debuff.jp/decide/",
|
|
81
|
+
"bugs": "https://github.com/debuffjp/decidekit/issues",
|
|
82
|
+
"bin": {
|
|
83
|
+
"decidekit": "./dist/cli.js"
|
|
84
|
+
},
|
|
85
|
+
"peerDependencies": {
|
|
86
|
+
"react": ">=18"
|
|
87
|
+
},
|
|
88
|
+
"peerDependenciesMeta": {
|
|
89
|
+
"react": {
|
|
90
|
+
"optional": true
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|