@matchbox-ai/train 0.2.1 → 0.3.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 +20 -0
- package/dist/{define-pipeline-D6I-jAE_.js → define-pipeline-DEkjmOte.js} +2 -1
- package/dist/define-pipeline-DEkjmOte.js.map +1 -0
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/load-project.d.ts +1 -0
- package/dist/load-project.d.ts.map +1 -1
- package/dist/models/presets.d.ts +2 -0
- package/dist/models/presets.d.ts.map +1 -1
- package/dist/models/sequence/export-parity.d.ts +16 -0
- package/dist/models/sequence/export-parity.d.ts.map +1 -0
- package/dist/models/sequence/fit-sequence.d.ts +25 -3
- package/dist/models/sequence/fit-sequence.d.ts.map +1 -1
- package/dist/models/sequence/prepare-supervision.d.ts +3 -3
- package/dist/models/sequence/prepare-supervision.d.ts.map +1 -1
- package/dist/models/sequence/run-sequence.d.ts +9 -1
- package/dist/models/sequence/run-sequence.d.ts.map +1 -1
- package/dist/models/sequence/types.d.ts +2 -0
- package/dist/models/sequence/types.d.ts.map +1 -1
- package/dist/native/fit.d.ts +4 -2
- package/dist/native/fit.d.ts.map +1 -1
- package/dist/pipeline/define-pipeline.d.ts +1 -0
- package/dist/pipeline/define-pipeline.d.ts.map +1 -1
- package/dist/project/artifact.d.ts +21 -1
- package/dist/project/artifact.d.ts.map +1 -1
- package/dist/project/config.d.ts +2 -0
- package/dist/project/config.d.ts.map +1 -1
- package/dist/project/index.js +1 -1
- package/dist/{project-BR13wXHr.js → project-CgNwV9Dj.js} +6 -4
- package/dist/project-CgNwV9Dj.js.map +1 -0
- package/dist/{run-PB_jiKnw.js → run-MgK9OCQv.js} +3 -3
- package/dist/{run-PB_jiKnw.js.map → run-MgK9OCQv.js.map} +1 -1
- package/dist/run.d.ts +9 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/{sequence-DGoGU7kG.js → sequence-NevJFVwZ.js} +90 -37
- package/dist/sequence-NevJFVwZ.js.map +1 -0
- package/dist/train.d.ts +9 -1
- package/dist/train.d.ts.map +1 -1
- package/docs/contributing.md +36 -0
- package/docs/reference/configuration.md +11 -11
- package/docs/reference/pipeline.md +28 -0
- package/docs/reference/supervision.md +8 -1
- package/docs/reference/training.md +2 -0
- package/docs/research/sequence-context.json +12632 -0
- package/docs/research/sequence-context.md +66 -0
- package/package.json +3 -3
- package/prebuilds/darwin-arm64/node.napi.node +0 -0
- package/prebuilds/darwin-x64/node.napi.node +0 -0
- package/prebuilds/linux-arm64/node.napi.glibc.node +0 -0
- package/prebuilds/linux-arm64/node.napi.musl.node +0 -0
- package/prebuilds/linux-x64/node.napi.glibc.node +0 -0
- package/prebuilds/linux-x64/node.napi.musl.node +0 -0
- package/prebuilds/win32-arm64/node.napi.node +0 -0
- package/prebuilds/win32-x64/node.napi.node +0 -0
- package/dist/define-pipeline-D6I-jAE_.js.map +0 -1
- package/dist/project-BR13wXHr.js.map +0 -1
- package/dist/sequence-DGoGU7kG.js.map +0 -1
|
@@ -11,18 +11,55 @@ import { createHash } from "node:crypto";
|
|
|
11
11
|
//#region src/native/fit.ts
|
|
12
12
|
const require = createRequire(import.meta.url);
|
|
13
13
|
function predict(config, weights, inputs) {
|
|
14
|
-
return require("#native").predict(JSON.stringify(config), Buffer.from(weights), Int32Array.from(inputs.flat()));
|
|
14
|
+
return require("#native").predict(JSON.stringify(config), Buffer.from(weights), inputs instanceof Int32Array ? inputs : Int32Array.from(inputs.flat()));
|
|
15
15
|
}
|
|
16
16
|
function fit(config, inputs, labels, progress) {
|
|
17
|
-
return require("#native").fit(JSON.stringify(config), Int32Array.from(inputs.flat()), Int32Array.from(labels), (error, value) => {
|
|
17
|
+
return require("#native").fit(JSON.stringify(config), inputs instanceof Int32Array ? inputs : Int32Array.from(inputs.flat()), labels instanceof Int32Array ? labels : Int32Array.from(labels), (error, value) => {
|
|
18
18
|
if (!error) progress?.(value[0], value[1]);
|
|
19
19
|
});
|
|
20
20
|
}
|
|
21
21
|
//#endregion
|
|
22
|
+
//#region src/models/sequence/export-parity.ts
|
|
23
|
+
/** Float32 probability tolerance, independent of the model's acceptance threshold. */
|
|
24
|
+
const confidenceTolerance = 1e-4;
|
|
25
|
+
function sequenceParity(threshold) {
|
|
26
|
+
let tokens = 0;
|
|
27
|
+
let maxConfidenceError = 0;
|
|
28
|
+
let labelDisagreements = 0;
|
|
29
|
+
let acceptanceDisagreements = 0;
|
|
30
|
+
return {
|
|
31
|
+
add(native, portable) {
|
|
32
|
+
if (![native.confidence, portable.confidence].every(Number.isFinite)) throw new Error("Burn export parity received non-finite confidence.");
|
|
33
|
+
tokens++;
|
|
34
|
+
maxConfidenceError = Math.max(maxConfidenceError, Math.abs(native.confidence - portable.confidence));
|
|
35
|
+
if (native.label !== portable.label) labelDisagreements++;
|
|
36
|
+
if (native.confidence >= threshold !== portable.confidence >= threshold) acceptanceDisagreements++;
|
|
37
|
+
},
|
|
38
|
+
report() {
|
|
39
|
+
const report = {
|
|
40
|
+
tokens,
|
|
41
|
+
maxConfidenceError,
|
|
42
|
+
confidenceTolerance,
|
|
43
|
+
labelDisagreements,
|
|
44
|
+
acceptanceDisagreements
|
|
45
|
+
};
|
|
46
|
+
if (labelDisagreements || acceptanceDisagreements || maxConfidenceError > confidenceTolerance) throw new Error(`Burn native and WASM predictions disagree: ${JSON.stringify(report)}`);
|
|
47
|
+
return report;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
22
52
|
//#region src/models/sequence/prepare-supervision.ts
|
|
23
|
-
function prepareSupervision(examples, recipe) {
|
|
24
|
-
|
|
25
|
-
|
|
53
|
+
function prepareSupervision(examples, recipe, radius = 1) {
|
|
54
|
+
if (!Number.isInteger(radius) || radius < 1 || radius > 16) throw new Error("contextRadius must be an integer between 1 and 16.");
|
|
55
|
+
if (recipe.casing !== void 0 && !["preserve", "lowercase"].includes(recipe.casing)) throw new Error("casing must be preserve or lowercase.");
|
|
56
|
+
const keys = /* @__PURE__ */ new Set();
|
|
57
|
+
let tokenCount = 0;
|
|
58
|
+
for (const example of examples) for (const token of tokenize(example.input, recipe.tokenizer, recipe.casing)) {
|
|
59
|
+
keys.add(token.key);
|
|
60
|
+
tokenCount++;
|
|
61
|
+
}
|
|
62
|
+
const vocabulary = [...keys].sort();
|
|
26
63
|
const dropout = recipe.tokenDropout ?? 0;
|
|
27
64
|
if (!Number.isFinite(dropout) || dropout < 0 || dropout > .5) throw new Error("tokenDropout must be between 0 and 0.5.");
|
|
28
65
|
let maskingSeed = 7043;
|
|
@@ -30,10 +67,19 @@ function prepareSupervision(examples, recipe) {
|
|
|
30
67
|
maskingSeed = Math.imul(maskingSeed, 1664525) + 1013904223 >>> 0;
|
|
31
68
|
return maskingSeed / 4294967296;
|
|
32
69
|
};
|
|
33
|
-
const
|
|
34
|
-
const
|
|
70
|
+
const width = radius * 2 + 1;
|
|
71
|
+
const capacity = tokenCount * (dropout > 0 ? 2 : 1);
|
|
72
|
+
const inputs = new Int32Array(capacity * width);
|
|
73
|
+
const labels = new Int32Array(capacity);
|
|
74
|
+
const observed = /* @__PURE__ */ new Set();
|
|
75
|
+
let count = 0;
|
|
76
|
+
const append = (window, id) => {
|
|
77
|
+
inputs.set(window, count * width);
|
|
78
|
+
labels[count++] = id;
|
|
79
|
+
observed.add(id);
|
|
80
|
+
};
|
|
35
81
|
for (const example of examples) {
|
|
36
|
-
const tokens = tokenize(example.input, recipe.tokenizer);
|
|
82
|
+
const tokens = tokenize(example.input, recipe.tokenizer, recipe.casing);
|
|
37
83
|
const annotations = recipe.annotate(example, tokens);
|
|
38
84
|
if (annotations.length !== tokens.length) throw new Error(`Annotation length mismatch: ${example.input}`);
|
|
39
85
|
windows(tokens, vocabulary, radius).forEach((window, position) => {
|
|
@@ -41,39 +87,40 @@ function prepareSupervision(examples, recipe) {
|
|
|
41
87
|
if (label === null) return;
|
|
42
88
|
const id = recipe.labels.indexOf(label);
|
|
43
89
|
if (id < 0) throw new Error(`Unknown annotation ${label}: ${example.input}`);
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (dropout > 0) {
|
|
47
|
-
inputs.push(window.map((id) => id > 1 && random() < dropout ? 1 : id));
|
|
48
|
-
labels.push(id);
|
|
49
|
-
}
|
|
90
|
+
append(window, id);
|
|
91
|
+
if (dropout > 0) append(window.map((id) => id > 1 && random() < dropout ? 1 : id), id);
|
|
50
92
|
});
|
|
51
93
|
}
|
|
52
|
-
if (recipe.labels.some((_, id) => !
|
|
94
|
+
if (recipe.labels.some((_, id) => !observed.has(id))) throw new Error("Every label needs supervised training examples.");
|
|
53
95
|
maskingSeed = 9187;
|
|
54
|
-
for (let index =
|
|
96
|
+
for (let index = count - 1; index > 0; index--) {
|
|
55
97
|
const other = Math.floor(random() * (index + 1));
|
|
56
|
-
|
|
98
|
+
for (let offset = 0; offset < width; offset++) {
|
|
99
|
+
const left = index * width + offset;
|
|
100
|
+
const right = other * width + offset;
|
|
101
|
+
[inputs[left], inputs[right]] = [inputs[right], inputs[left]];
|
|
102
|
+
}
|
|
57
103
|
[labels[index], labels[other]] = [labels[other], labels[index]];
|
|
58
104
|
}
|
|
59
105
|
return {
|
|
60
106
|
vocabulary,
|
|
61
107
|
radius,
|
|
62
108
|
dropout,
|
|
63
|
-
inputs,
|
|
64
|
-
labels
|
|
109
|
+
inputs: inputs.subarray(0, count * width),
|
|
110
|
+
labels: labels.subarray(0, count)
|
|
65
111
|
};
|
|
66
112
|
}
|
|
67
113
|
//#endregion
|
|
68
114
|
//#region src/models/sequence/fit-sequence.ts
|
|
69
|
-
async function fitSequence(examples, recipe, metadata, probes = [], progress) {
|
|
70
|
-
const { vocabulary, radius, dropout, inputs, labels } = prepareSupervision(examples, recipe);
|
|
115
|
+
async function fitSequence(examples, recipe, metadata, probes = [], progress, contextRadius = 1) {
|
|
116
|
+
const { vocabulary, radius, dropout, inputs, labels } = prepareSupervision(examples, recipe, contextRadius);
|
|
71
117
|
const result = await fit({
|
|
72
118
|
vocabularySize: vocabulary.length + 2,
|
|
73
|
-
labelCount: recipe.labels.length
|
|
119
|
+
labelCount: recipe.labels.length,
|
|
120
|
+
contextRadius: radius
|
|
74
121
|
}, inputs, labels, progress);
|
|
75
122
|
const artifact = (weights) => ({
|
|
76
|
-
formatVersion:
|
|
123
|
+
formatVersion: 4,
|
|
77
124
|
engine: "burn-0.21",
|
|
78
125
|
kind: "sequence-parser",
|
|
79
126
|
architecture: "embedding-window-mlp",
|
|
@@ -83,6 +130,7 @@ async function fitSequence(examples, recipe, metadata, probes = [], progress) {
|
|
|
83
130
|
vocabulary,
|
|
84
131
|
labels: [...recipe.labels],
|
|
85
132
|
radius,
|
|
133
|
+
casing: recipe.casing ?? "lowercase",
|
|
86
134
|
unknownTokens: dropout > 0 ? "predict" : "abstain",
|
|
87
135
|
threshold: .75,
|
|
88
136
|
precision: "float32",
|
|
@@ -91,37 +139,37 @@ async function fitSequence(examples, recipe, metadata, probes = [], progress) {
|
|
|
91
139
|
const model = artifact(result.weights);
|
|
92
140
|
const checked = [...examples.slice(0, 16).map((row) => row.input), ...probes];
|
|
93
141
|
const portable = await tensorPredictor(model);
|
|
94
|
-
|
|
95
|
-
let labelDisagreements = 0;
|
|
142
|
+
const parity = sequenceParity(model.threshold);
|
|
96
143
|
try {
|
|
97
144
|
for (const input of checked) {
|
|
98
|
-
const tokens = tokenize(input, recipe.tokenizer);
|
|
145
|
+
const tokens = tokenize(input, recipe.tokenizer, recipe.casing);
|
|
99
146
|
if (!tokens.length) continue;
|
|
100
147
|
const scores = predict({
|
|
101
148
|
vocabularySize: vocabulary.length + 2,
|
|
102
|
-
labelCount: recipe.labels.length
|
|
149
|
+
labelCount: recipe.labels.length,
|
|
150
|
+
contextRadius: radius
|
|
103
151
|
}, result.weights, windows(tokens, vocabulary, radius));
|
|
104
152
|
portable.sequence(input).forEach((token, index) => {
|
|
105
153
|
const row = scores.slice(index * recipe.labels.length, (index + 1) * recipe.labels.length);
|
|
106
154
|
const confidence = Math.max(...row);
|
|
107
|
-
|
|
108
|
-
|
|
155
|
+
parity.add({
|
|
156
|
+
label: recipe.labels[row.indexOf(confidence)],
|
|
157
|
+
confidence
|
|
158
|
+
}, token);
|
|
109
159
|
});
|
|
110
160
|
}
|
|
111
161
|
} finally {
|
|
112
162
|
portable.dispose();
|
|
113
163
|
}
|
|
114
|
-
if (labelDisagreements || maxConfidenceError > 1e-5) throw new Error("Burn native and WASM predictions disagree.");
|
|
115
164
|
return {
|
|
116
165
|
model,
|
|
117
166
|
parameters: result.parameters,
|
|
118
167
|
history: result.loss,
|
|
119
168
|
parity: {
|
|
120
169
|
examples: checked.length,
|
|
121
|
-
|
|
122
|
-
labelDisagreements
|
|
170
|
+
...parity.report()
|
|
123
171
|
},
|
|
124
|
-
supervisedTokens:
|
|
172
|
+
supervisedTokens: labels.length
|
|
125
173
|
};
|
|
126
174
|
}
|
|
127
175
|
//#endregion
|
|
@@ -154,7 +202,7 @@ async function runSequence(command, project, progress) {
|
|
|
154
202
|
for (const example of training) {
|
|
155
203
|
if (!task.validateInput(example.input).success) throw new Error(`Invalid training input: ${example.input}`);
|
|
156
204
|
if (heldOut.has(example.input.trim().toLowerCase())) throw new Error(`Training input overlaps evaluation: ${example.input}`);
|
|
157
|
-
const tokens = tokenize(example.input, recipe.tokenizer);
|
|
205
|
+
const tokens = tokenize(example.input, recipe.tokenizer, recipe.casing);
|
|
158
206
|
const labels = recipe.annotate(example, tokens);
|
|
159
207
|
const value = decode(tokens.map((token, index) => ({
|
|
160
208
|
...token,
|
|
@@ -168,7 +216,7 @@ async function runSequence(command, project, progress) {
|
|
|
168
216
|
taskMetadata: task.toJSON(),
|
|
169
217
|
taskModule: modulePath(project.taskPath),
|
|
170
218
|
decoderModule: modulePath(resolve(root, sequence.decoder))
|
|
171
|
-
}, project.validation.map((row) => row.input), progress);
|
|
219
|
+
}, project.validation.map((row) => row.input), progress, sequence.contextRadius);
|
|
172
220
|
const validation = await evaluateSequence$1(parser(fit.model), project.validation);
|
|
173
221
|
const bytes = Buffer.byteLength(JSON.stringify(fit.model));
|
|
174
222
|
if (validation.exactAccuracy < config.minAccuracy || bytes > config.maxBytes) throw new Error(`Sequence model failed validation/size requirements (${validation.exactAccuracy}, ${bytes} bytes). ${JSON.stringify(validation.failures.slice(0, 10))}`);
|
|
@@ -179,6 +227,11 @@ async function runSequence(command, project, progress) {
|
|
|
179
227
|
const report = {
|
|
180
228
|
formatVersion: 2,
|
|
181
229
|
architecture: fit.model.architecture,
|
|
230
|
+
encoding: {
|
|
231
|
+
tokenizer: fit.model.tokenizer,
|
|
232
|
+
casing: fit.model.casing
|
|
233
|
+
},
|
|
234
|
+
contextRadius: fit.model.radius,
|
|
182
235
|
backend: "Burn native CPU",
|
|
183
236
|
seed: 42,
|
|
184
237
|
artifactSha256: hash(JSON.stringify(fit.model)),
|
|
@@ -196,7 +249,7 @@ async function runSequence(command, project, progress) {
|
|
|
196
249
|
},
|
|
197
250
|
supervisionSha256: hash(JSON.stringify(training.map((row) => ({
|
|
198
251
|
...row,
|
|
199
|
-
labels: recipe.annotate(row, tokenize(row.input, recipe.tokenizer))
|
|
252
|
+
labels: recipe.annotate(row, tokenize(row.input, recipe.tokenizer, recipe.casing))
|
|
200
253
|
})))),
|
|
201
254
|
supervisedTokens: fit.supervisedTokens,
|
|
202
255
|
loss: fit.history,
|
|
@@ -216,4 +269,4 @@ async function runSequence(command, project, progress) {
|
|
|
216
269
|
//#endregion
|
|
217
270
|
export { runSequence };
|
|
218
271
|
|
|
219
|
-
//# sourceMappingURL=sequence-
|
|
272
|
+
//# sourceMappingURL=sequence-NevJFVwZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sequence-NevJFVwZ.js","names":["evaluateSequence","evaluate"],"sources":["../src/native/fit.ts","../src/models/sequence/export-parity.ts","../src/models/sequence/prepare-supervision.ts","../src/models/sequence/fit-sequence.ts","../src/models/sequence/run-sequence.ts"],"sourcesContent":["import { createRequire } from \"node:module\";\ninterface NativeResult {\n parameters: number;\n weights: Uint8Array;\n loss: number[];\n}\nconst require = createRequire(import.meta.url);\nexport function predict(\n config: { vocabularySize: number; labelCount: number; contextRadius?: number },\n weights: Uint8Array,\n inputs: number[][] | Int32Array,\n): number[] {\n const native = require(\"#native\") as {\n predict(config: string, weights: Buffer, inputs: Int32Array): number[];\n };\n return native.predict(\n JSON.stringify(config),\n Buffer.from(weights),\n inputs instanceof Int32Array ? inputs : Int32Array.from(inputs.flat()),\n );\n}\nexport function fit(\n config: { vocabularySize: number; labelCount: number; contextRadius?: number },\n inputs: number[][] | Int32Array,\n labels: number[] | Int32Array,\n progress?: (epoch: number, loss: number) => void,\n): Promise<NativeResult> {\n const native = require(\"#native\") as {\n fit(\n config: string,\n inputs: Int32Array,\n labels: Int32Array,\n progress: (error: Error | null, value: number[]) => void,\n ): Promise<NativeResult>;\n };\n return native.fit(\n JSON.stringify(config),\n inputs instanceof Int32Array ? inputs : Int32Array.from(inputs.flat()),\n labels instanceof Int32Array ? labels : Int32Array.from(labels),\n (error, value) => {\n if (!error) {\n progress?.(value[0]!, value[1]!);\n }\n },\n );\n}\n","type Prediction = { label: string; confidence: number };\n\n/** Float32 probability tolerance, independent of the model's acceptance threshold. */\nconst confidenceTolerance = 1e-4;\n\nexport function sequenceParity(threshold: number) {\n let tokens = 0;\n let maxConfidenceError = 0;\n let labelDisagreements = 0;\n let acceptanceDisagreements = 0;\n return {\n add(native: Prediction, portable: Prediction) {\n if (![native.confidence, portable.confidence].every(Number.isFinite)) {\n throw new Error(\"Burn export parity received non-finite confidence.\");\n }\n tokens++;\n maxConfidenceError = Math.max(\n maxConfidenceError,\n Math.abs(native.confidence - portable.confidence),\n );\n if (native.label !== portable.label) {\n labelDisagreements++;\n }\n if (native.confidence >= threshold !== portable.confidence >= threshold) {\n acceptanceDisagreements++;\n }\n },\n report() {\n const report = {\n tokens,\n maxConfidenceError,\n confidenceTolerance,\n labelDisagreements,\n acceptanceDisagreements,\n };\n if (\n labelDisagreements ||\n acceptanceDisagreements ||\n maxConfidenceError > confidenceTolerance\n ) {\n throw new Error(`Burn native and WASM predictions disagree: ${JSON.stringify(report)}`);\n }\n return report;\n },\n };\n}\n","import { tokenize, windows } from \"@matchbox-ai/core/internal\";\nimport type { DatasetExample } from \"@matchbox-ai/core\";\nimport type { SequenceRecipe } from \"./types.js\";\nexport function prepareSupervision(\n examples: readonly DatasetExample<unknown>[],\n recipe: SequenceRecipe,\n radius = 1,\n) {\n if (!Number.isInteger(radius) || radius < 1 || radius > 16) {\n throw new Error(\"contextRadius must be an integer between 1 and 16.\");\n }\n if (recipe.casing !== undefined && ![\"preserve\", \"lowercase\"].includes(recipe.casing)) {\n throw new Error(\"casing must be preserve or lowercase.\");\n }\n const keys = new Set<string>();\n let tokenCount = 0;\n for (const example of examples) {\n for (const token of tokenize(example.input, recipe.tokenizer, recipe.casing)) {\n keys.add(token.key);\n tokenCount++;\n }\n }\n const vocabulary = [...keys].sort();\n const dropout = recipe.tokenDropout ?? 0;\n if (!Number.isFinite(dropout) || dropout < 0 || dropout > 0.5) {\n throw new Error(\"tokenDropout must be between 0 and 0.5.\");\n }\n let maskingSeed = 7043;\n const random = () => {\n maskingSeed = (Math.imul(maskingSeed, 1664525) + 1013904223) >>> 0;\n return maskingSeed / 4294967296;\n };\n const width = radius * 2 + 1;\n const capacity = tokenCount * (dropout > 0 ? 2 : 1);\n const inputs = new Int32Array(capacity * width);\n const labels = new Int32Array(capacity);\n const observed = new Set<number>();\n let count = 0;\n const append = (window: number[], id: number) => {\n inputs.set(window, count * width);\n labels[count++] = id;\n observed.add(id);\n };\n for (const example of examples) {\n const tokens = tokenize(example.input, recipe.tokenizer, recipe.casing);\n const annotations = recipe.annotate(example, tokens);\n if (annotations.length !== tokens.length) {\n throw new Error(`Annotation length mismatch: ${example.input}`);\n }\n windows(tokens, vocabulary, radius).forEach((window, position) => {\n const label = annotations[position];\n if (label === null) {\n return;\n }\n const id = recipe.labels.indexOf(label!);\n if (id < 0) {\n throw new Error(`Unknown annotation ${label}: ${example.input}`);\n }\n append(window, id);\n if (dropout > 0) {\n append(\n window.map((id) => (id > 1 && random() < dropout ? 1 : id)),\n id,\n );\n }\n });\n }\n if (recipe.labels.some((_, id) => !observed.has(id))) {\n throw new Error(\"Every label needs supervised training examples.\");\n }\n // Mix currency/template blocks deterministically before minibatch optimization.\n maskingSeed = 9187;\n for (let index = count - 1; index > 0; index--) {\n const other = Math.floor(random() * (index + 1));\n for (let offset = 0; offset < width; offset++) {\n const left = index * width + offset;\n const right = other * width + offset;\n [inputs[left], inputs[right]] = [inputs[right]!, inputs[left]!];\n }\n [labels[index], labels[other]] = [labels[other]!, labels[index]!];\n }\n return {\n vocabulary,\n radius,\n dropout,\n inputs: inputs.subarray(0, count * width),\n labels: labels.subarray(0, count),\n };\n}\n","import { sequenceParity } from \"./export-parity.js\";\nimport { prepareSupervision } from \"./prepare-supervision.js\";\nimport { fit, predict } from \"../../native/index.js\";\nimport { tensorPredictor, tokenize, windows } from \"@matchbox-ai/core/internal\";\nimport type { SequenceArtifact } from \"@matchbox-ai/core/internal\";\nimport type { DatasetExample } from \"@matchbox-ai/core\";\nimport type { SequenceRecipe } from \"./types.js\";\nexport async function fitSequence(\n examples: readonly DatasetExample<unknown>[],\n recipe: SequenceRecipe,\n metadata: Pick<SequenceArtifact, \"taskModule\" | \"taskMetadata\" | \"decoderModule\">,\n probes: readonly string[] = [],\n progress?: (epoch: number, loss: number) => void,\n contextRadius = 1,\n) {\n const { vocabulary, radius, dropout, inputs, labels } = prepareSupervision(\n examples,\n recipe,\n contextRadius,\n );\n const result = await fit(\n {\n vocabularySize: vocabulary.length + 2,\n labelCount: recipe.labels.length,\n contextRadius: radius,\n },\n inputs,\n labels,\n progress,\n );\n const artifact = (weights: Uint8Array): SequenceArtifact => ({\n formatVersion: 4,\n engine: \"burn-0.21\",\n kind: \"sequence-parser\",\n architecture: \"embedding-window-mlp\",\n ...metadata,\n tokenizer: recipe.tokenizer,\n readout: recipe.readout,\n vocabulary,\n labels: [...recipe.labels],\n radius,\n casing: recipe.casing ?? \"lowercase\",\n unknownTokens: dropout > 0 ? \"predict\" : \"abstain\",\n threshold: 0.75,\n precision: \"float32\",\n weights: Buffer.from(weights).toString(\"base64\"),\n });\n const model = artifact(result.weights);\n const checked = [...examples.slice(0, 16).map((row) => row.input), ...probes];\n const portable = await tensorPredictor(model);\n const parity = sequenceParity(model.threshold);\n try {\n for (const input of checked) {\n const tokens = tokenize(input, recipe.tokenizer, recipe.casing);\n if (!tokens.length) {\n continue;\n }\n const scores = predict(\n {\n vocabularySize: vocabulary.length + 2,\n labelCount: recipe.labels.length,\n contextRadius: radius,\n },\n result.weights,\n windows(tokens, vocabulary, radius),\n );\n portable.sequence(input).forEach((token, index) => {\n const row = scores.slice(index * recipe.labels.length, (index + 1) * recipe.labels.length);\n const confidence = Math.max(...row);\n parity.add({ label: recipe.labels[row.indexOf(confidence)]!, confidence }, token);\n });\n }\n } finally {\n portable.dispose();\n }\n return {\n model,\n parameters: result.parameters,\n history: result.loss,\n parity: { examples: checked.length, ...parity.report() },\n supervisedTokens: labels.length,\n };\n}\n","import { sameOutput } from \"../../evaluation/same-output.js\";\nimport { z } from \"zod\";\nimport { createHash } from \"node:crypto\";\nimport { readFile } from \"node:fs/promises\";\nimport { dirname, relative, resolve } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport { createParser } from \"@matchbox-ai/core/runtime\";\nimport { readSequenceArtifact, tokenize } from \"@matchbox-ai/core/internal\";\nimport type { DatasetExample } from \"@matchbox-ai/core\";\nimport type { MatchboxParser, SequenceDecoder } from \"@matchbox-ai/core/runtime\";\nimport type { loadProject } from \"../../load-project.js\";\nimport { packageModel } from \"../../packaging/package-model.js\";\nimport { fitSequence } from \"./fit-sequence.js\";\nimport { evaluateSequence as evaluate } from \"../../evaluation/evaluate-sequence.js\";\nimport type { SequenceRecipe } from \"./types.js\";\nconst hash = (value: string) => createHash(\"sha256\").update(value).digest(\"hex\");\nexport async function runSequence(\n command: \"train\" | \"eval\",\n project: Awaited<ReturnType<typeof loadProject>>,\n progress?: (epoch: number, loss: number) => void,\n) {\n const { task, config } = project;\n const root = project.root;\n const sequence = config.sequence;\n if (!sequence) {\n throw new Error(\"A sequence pipeline needs both recipe and decoder modules.\");\n }\n const savedArtifact =\n command === \"eval\"\n ? readSequenceArtifact(JSON.parse(await readFile(project.output, \"utf8\")))\n : null;\n const decoderPath = savedArtifact\n ? resolve(dirname(project.output), savedArtifact.decoderModule)\n : resolve(root, sequence.decoder);\n const decode: SequenceDecoder = (await import(pathToFileURL(decoderPath).href)).default;\n const evaluateSequence = (\n parser: MatchboxParser<unknown>,\n examples: readonly DatasetExample<unknown>[],\n ) =>\n evaluate(parser, examples, (value) => task.validateOutput(value).success).finally(() => {\n if (\"dispose\" in parser && typeof parser.dispose === \"function\") {\n parser.dispose();\n }\n });\n const parser = (artifact: unknown) => createParser(artifact, task, decode);\n if (savedArtifact) {\n return { evaluation: await evaluateSequence(parser(savedArtifact), project.evaluation) };\n }\n const recipe: SequenceRecipe = (await import(pathToFileURL(resolve(root, sequence.recipe)).href))\n .default;\n const modulePath = (path: string) => {\n const value = relative(dirname(project.output), path).replaceAll(\"\\\\\", \"/\");\n return value.startsWith(\".\") ? value : `./${value}`;\n };\n const rejections = z\n .array(z.strictObject({ input: z.string(), output: z.null() }))\n .parse(recipe.rejections ?? []);\n const training = [...project.train, ...rejections];\n const heldOut = new Set(\n [...project.validation, ...project.evaluation].map((row) => row.input.trim().toLowerCase()),\n );\n for (const example of training) {\n if (!task.validateInput(example.input).success) {\n throw new Error(`Invalid training input: ${example.input}`);\n }\n if (heldOut.has(example.input.trim().toLowerCase())) {\n throw new Error(`Training input overlaps evaluation: ${example.input}`);\n }\n const tokens = tokenize(example.input, recipe.tokenizer, recipe.casing);\n const labels = recipe.annotate(example, tokens);\n const value = decode(\n tokens.map((token, index) => ({ ...token, label: labels[index] ?? \"O\", confidence: 1 })),\n example.input,\n );\n if (!sameOutput(value, example.output)) {\n throw new Error(\n `Training annotations do not decode to the supplied output: ${example.input}`,\n );\n }\n }\n const started = performance.now();\n const fit = await fitSequence(\n training,\n recipe,\n {\n taskMetadata: task.toJSON(),\n taskModule: modulePath(project.taskPath),\n decoderModule: modulePath(resolve(root, sequence.decoder)),\n },\n project.validation.map((row) => row.input),\n progress,\n sequence.contextRadius,\n );\n const validation = await evaluateSequence(parser(fit.model), project.validation);\n const bytes = Buffer.byteLength(JSON.stringify(fit.model));\n if (validation.exactAccuracy < config.minAccuracy || bytes > config.maxBytes) {\n throw new Error(\n `Sequence model failed validation/size requirements (${validation.exactAccuracy}, ${bytes} bytes). ${JSON.stringify(validation.failures.slice(0, 10))}`,\n );\n }\n const challenges = config.challenges\n ? z\n .array(z.strictObject({ input: z.string(), output: z.null() }))\n .min(1)\n .parse(JSON.parse(await readFile(resolve(root, config.challenges), \"utf8\")))\n : null;\n const report = {\n formatVersion: 2,\n architecture: fit.model.architecture,\n encoding: { tokenizer: fit.model.tokenizer, casing: fit.model.casing },\n contextRadius: fit.model.radius,\n backend: \"Burn native CPU\",\n seed: 42,\n artifactSha256: hash(JSON.stringify(fit.model)),\n bytes,\n parameters: fit.parameters,\n datasetSha256: project.sources.map((source) => ({\n source: source.source,\n sha256: hash(source.text),\n })),\n examples: {\n train: project.train.length,\n rejections: recipe.rejections?.length ?? 0,\n validation: project.validation.length,\n eval: project.evaluation.length,\n },\n supervisionSha256: hash(\n JSON.stringify(\n training.map((row) => ({\n ...row,\n labels: recipe.annotate(row, tokenize(row.input, recipe.tokenizer, recipe.casing)),\n })),\n ),\n ),\n supervisedTokens: fit.supervisedTokens,\n loss: fit.history,\n exportParity: fit.parity,\n validation,\n evaluation: await evaluateSequence(parser(fit.model), project.evaluation),\n challenges: challenges ? await evaluateSequence(parser(fit.model), challenges) : null,\n trainingMs: performance.now() - started,\n notes:\n \"Float32 weights in a Burn binary record, base64-encoded in the artifact. Validation gates export. Eval labels do not influence selection. Scores are uncalibrated.\",\n };\n await packageModel(project.output, fit.model, report);\n return { report, output: project.output };\n}\n"],"mappings":"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"}
|
package/dist/train.d.ts
CHANGED
|
@@ -23,6 +23,11 @@ export declare function train(target: string, options?: {
|
|
|
23
23
|
report: {
|
|
24
24
|
formatVersion: number;
|
|
25
25
|
architecture: "embedding-window-mlp";
|
|
26
|
+
encoding: {
|
|
27
|
+
tokenizer: "characters" | "words";
|
|
28
|
+
casing: "lowercase" | "preserve" | undefined;
|
|
29
|
+
};
|
|
30
|
+
contextRadius: number;
|
|
26
31
|
backend: string;
|
|
27
32
|
seed: number;
|
|
28
33
|
artifactSha256: string;
|
|
@@ -42,9 +47,12 @@ export declare function train(target: string, options?: {
|
|
|
42
47
|
supervisedTokens: number;
|
|
43
48
|
loss: number[];
|
|
44
49
|
exportParity: {
|
|
45
|
-
|
|
50
|
+
tokens: number;
|
|
46
51
|
maxConfidenceError: number;
|
|
52
|
+
confidenceTolerance: number;
|
|
47
53
|
labelDisagreements: number;
|
|
54
|
+
acceptanceDisagreements: number;
|
|
55
|
+
examples: number;
|
|
48
56
|
};
|
|
49
57
|
validation: {
|
|
50
58
|
examples: number;
|
package/dist/train.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"train.d.ts","sourceRoot":"","sources":["../src/train.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,wBAAsB,KAAK,CACzB,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAAO
|
|
1
|
+
{"version":3,"file":"train.d.ts","sourceRoot":"","sources":["../src/train.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,wBAAsB,KAAK,CACzB,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAKrE"}
|
package/docs/contributing.md
CHANGED
|
@@ -41,3 +41,39 @@ bun run eval:examples
|
|
|
41
41
|
Generated native binaries, WASM output, Cargo build output and model artifacts are ignored by Git. `Cargo.lock` pins Rust dependencies. `bun run check` includes Rust formatting, Clippy and engine tests.
|
|
42
42
|
|
|
43
43
|
See [native package distribution](native-packages.md) for platform coverage and release verification.
|
|
44
|
+
|
|
45
|
+
## Diagnose sequence exports
|
|
46
|
+
|
|
47
|
+
To inspect native/WASM prediction differences with an installed consumer's binaries, run:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
bun scripts/diagnose-sequence-export.ts \
|
|
51
|
+
/path/to/application \
|
|
52
|
+
/path/to/application/matchbox/task \
|
|
53
|
+
/path/to/dataset \
|
|
54
|
+
.matchbox/export-diagnostic
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The dataset directory must contain `train.jsonl` and `validation.jsonl`. An optional final integer limits training records. The runner refuses an existing output directory and saves weights and fit metadata before comparing predictions. Reports include checked tokens, differing labels and confidence drift without source text. This contributor tool uses framework internals; it does not package an application model. Generated files stay ignored.
|
|
58
|
+
|
|
59
|
+
## Sequence context experiments
|
|
60
|
+
|
|
61
|
+
The framework research tools under `scripts/sequence-research/` exercise a local native/WASM build against an independently prepared task and frozen corpus. They do not replace testing the published packages in a consumer application. Run `bun run build:packages` first.
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
bun scripts/sequence-research/train.ts "$TASK" "$CORPUS" "$NEW_RUN" preserve 1
|
|
65
|
+
bun scripts/sequence-research/assess.ts "$TASK" "$NEW_RUN/model.json" "$CORPUS/validation.jsonl" "$NEW_RUN/assessment.json"
|
|
66
|
+
bun scripts/sequence-research/browser.ts "$NEW_RUN/model.json" "$CORPUS/validation.jsonl" "$NEW_RUN/browser.json"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`TASK` contains the parser, recipe, and decoder. `CORPUS` contains frozen `train.jsonl` and `validation.jsonl`. `NEW_RUN` must be a new directory under ignored `.matchbox/`; its parent must exist. The research recipe must support deriving labels from held-out input/output pairs as well as training examples. Training reads only the training and validation files. Assessment defaults to validation. After choosing a model, pass `test.jsonl` and a final `test` argument to `assess.ts` for a separate final report. Do not adjust the model based on that result.
|
|
70
|
+
|
|
71
|
+
Compare lowercase/radius-one, preserved-case/radius-one, and lowercase/wider-radius models before testing their combination. Keep data, supervision, optimizer, seed, epochs, and evaluation method fixed. The vocabulary is fitted on training data only. Save weights before evaluation so an export or measurement failure does not require another training run.
|
|
72
|
+
|
|
73
|
+
Reports distinguish diagnostic non-whitespace token accuracy from whole-parser acceptance and exact output accuracy. Native/WASM checks retain the normal confidence tolerance, label agreement, and acceptance agreement requirements. Browser timings measure diagnostic inference on fixed validation prefixes with five warmups and thirty calls per length; they exclude decoding and must not be presented as accepted-result throughput. Initialization excludes JavaScript import and model fetch.
|
|
74
|
+
|
|
75
|
+
For a paired document-level bootstrap interval, run `bun scripts/sequence-research/compare.ts "$BASELINE_ASSESSMENT" "$CANDIDATE_ASSESSMENT" "$NEW_COMPARISON"`. It requires identical evaluation hashes and token counts. Its uncertainty interval covers document sampling, not training-seed variation.
|
|
76
|
+
|
|
77
|
+
Training reports record preparation and native fit wall time separately, data hashes, model size, loss, and machine details. Use an operating-system resource monitor to record peak resident memory and total process time. Single-seed runs and single-machine timings establish an initial comparison, not a production quality or speed guarantee. Preserve immutable measurements and keep run commentary outside the repository.
|
|
78
|
+
|
|
79
|
+
See the [character encoding and context measurements](research/sequence-context.md) for a full-corpus example of this evaluation protocol.
|
|
@@ -13,17 +13,17 @@ export default {
|
|
|
13
13
|
|
|
14
14
|
Paths resolve relative to the config's directory, normally `matchbox/<task>/`. Task config overrides pipeline acceptance values, which override defaults.
|
|
15
15
|
|
|
16
|
-
| Field | Type
|
|
17
|
-
| ------------- |
|
|
18
|
-
| `task` | String path.
|
|
19
|
-
| `train` | String path.
|
|
20
|
-
| `validation` | String path.
|
|
21
|
-
| `eval` | String path.
|
|
22
|
-
| `output` | Path ending in `.matchbox`.
|
|
23
|
-
| `minAccuracy` | Number from 0 to 1.
|
|
24
|
-
| `maxBytes` | Positive number.
|
|
25
|
-
| `challenges` | Optional JSON path.
|
|
26
|
-
| `sequence` | `{ recipe: string, decoder: string }`. | Derived from the token pipeline. This is a legacy configuration route; prefer `pipeline.ts`. |
|
|
16
|
+
| Field | Type | Default |
|
|
17
|
+
| ------------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
18
|
+
| `task` | String path. | `parser.ts` or `parser/parser.ts`. |
|
|
19
|
+
| `train` | String path. | `./data/train.jsonl`. |
|
|
20
|
+
| `validation` | String path. | `./evals/validation.jsonl`. |
|
|
21
|
+
| `eval` | String path. | `./evals/test.jsonl`. |
|
|
22
|
+
| `output` | Path ending in `.matchbox`. | Project-level `.matchbox/<task>/model.matchbox`. |
|
|
23
|
+
| `minAccuracy` | Number from 0 to 1. | `0.95`, unless set in the pipeline. |
|
|
24
|
+
| `maxBytes` | Positive number. | `64000`, unless set in the pipeline. |
|
|
25
|
+
| `challenges` | Optional JSON path. | `./evals/challenges.json` if present. |
|
|
26
|
+
| `sequence` | `{ recipe: string, decoder: string, contextRadius?: number }`. | Derived from the token pipeline. This is a legacy configuration route; prefer `pipeline.ts`. |
|
|
27
27
|
|
|
28
28
|
Unknown properties are rejected. JSON configuration files are not supported. Challenge JSON is an array of `{ input: string, output: null }` used by sequence reports.
|
|
29
29
|
|
|
@@ -41,3 +41,31 @@ export default definePipeline({
|
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
The recipe owns tokenization and training supervision. The decoder ships to the browser. See [their contracts](supervision.md). Schemas do not select encodings, dictionaries, or normalization rules.
|
|
44
|
+
|
|
45
|
+
## Token context
|
|
46
|
+
|
|
47
|
+
`tokenClassifier({ contextRadius: 4 })` gives each prediction four tokens on either side of the current token, nine tokens total. Radius is an integer from 1 through 16 and defaults to 1. Tokens are Unicode code points in character mode and tokenizer units in word mode. Positions outside the current input are padded; context never crosses an example boundary.
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
export default definePipeline({
|
|
51
|
+
prediction: tokenClassifier({ contextRadius: 4 }),
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
This remains a fixed-window classifier. Burn embeds the window and learns a small feedforward network; there is no recurrent state, attention, or awareness beyond that window. Wider context increases training memory, model size, and compute, and may not improve held-out accuracy. Choose it using independent validation examples, then report results on an untouched test set.
|
|
56
|
+
|
|
57
|
+
Casing belongs in the [recipe](supervision.md), independently of context size:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import type { SequenceRecipe } from "@matchbox-ai/train";
|
|
61
|
+
|
|
62
|
+
export default {
|
|
63
|
+
tokenizer: "characters",
|
|
64
|
+
casing: "preserve",
|
|
65
|
+
readout: "all",
|
|
66
|
+
labels,
|
|
67
|
+
annotate,
|
|
68
|
+
} satisfies SequenceRecipe;
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Retrain after either change. New sequence artifacts use format version 4 and require a compatible runtime. The runtime still reads version 3 artifacts with their original lowercase, radius-one behavior. These choices do not change the 512 UTF-16-unit parser limit, whole-result abstention, or confidence calibration.
|
|
@@ -17,6 +17,7 @@ import type { SequenceRecipe } from "@matchbox-ai/train";
|
|
|
17
17
|
| Property | Type | Meaning |
|
|
18
18
|
| ----------- | -------------------------------------------------- | --------------------------------------------------- |
|
|
19
19
|
| `tokenizer` | `"words"` or `"characters"` | Chooses token boundaries. |
|
|
20
|
+
| `casing` | `"lowercase"` or `"preserve"` | Controls token keys; defaults to `"lowercase"`. |
|
|
20
21
|
| `readout` | `"all"` or `"last"` | Predicts at every position or the final position. |
|
|
21
22
|
| `labels` | `readonly string[]` | The label vocabulary. |
|
|
22
23
|
| `annotate` | `(example, tokens) => readonly (string or null)[]` | Returns one label or unsupervised `null` per token. |
|
|
@@ -46,7 +47,13 @@ import { tokenize } from "@matchbox-ai/train";
|
|
|
46
47
|
const tokens = tokenize("for 90 minutes", "words");
|
|
47
48
|
```
|
|
48
49
|
|
|
49
|
-
`tokenize(input: string, mode: "words" | "characters"): Token[]` is shared with annotation generators. Word mode separates numbers, letter runs, and punctuation; numeric token keys become `<number>`. Character mode iterates Unicode code points. Original text and offsets are preserved.
|
|
50
|
+
`tokenize(input: string, mode: "words" | "characters", casing?: "lowercase" | "preserve"): Token[]` is shared with annotation generators. Word mode separates numbers, letter runs, and punctuation; numeric token keys become `<number>`. Character mode iterates Unicode code points. Original text and offsets are preserved. Keys are lowercased by default. Set `casing: "preserve"` in a recipe when distinctions such as `User` versus `user` carry meaning. Pass the same third argument to `tokenize` in an annotation generator:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
const tokens = tokenize("User user", "characters", "preserve");
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The vocabulary is still learned only from training data. Case preservation creates distinct vocabulary entries and can increase unknown-token abstention. It does not add a language dictionary or Unicode normalization. In word mode, numeric keys remain `<number>` regardless of casing. Retrain after changing the encoding; the artifact stores the choice for inference.
|
|
50
57
|
|
|
51
58
|
```ts
|
|
52
59
|
interface Token {
|
|
@@ -24,3 +24,5 @@ Report format 2 contains `architecture`, `backend`, `seed`, `bytes`, `parameters
|
|
|
24
24
|
`TrainingConfig` is the optional task configuration type. See [every field and default](configuration.md).
|
|
25
25
|
|
|
26
26
|
Use [evaluate](evaluation.md) to score an existing parser without training it.
|
|
27
|
+
|
|
28
|
+
For sequence models, `exportParity` records the checked token count, maximum absolute confidence difference, a `0.0001` numerical tolerance, label disagreements and acceptance disagreements. Export fails on any label disagreement, any crossing of the model acceptance threshold, or confidence drift above that tolerance. This tolerance does not lower model confidence requirements.
|