@matchbox-ai/train 0.1.0 → 0.2.1
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 +36 -0
- package/README.md +1 -1
- package/THIRD-PARTY-NOTICES.md +1 -1
- package/dist/define-pipeline-D6I-jAE_.js +20 -0
- package/dist/define-pipeline-D6I-jAE_.js.map +1 -0
- package/dist/encoders/index.d.ts +0 -1
- package/dist/encoders/index.d.ts.map +1 -1
- package/dist/evaluate-sequence-ZCwyomzQ.js.map +1 -1
- package/dist/evaluation/evaluate-sequence.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -11
- package/dist/index.js.map +1 -1
- package/dist/load-project.d.ts +0 -4
- package/dist/load-project.d.ts.map +1 -1
- package/dist/models/presets.d.ts +6 -6
- package/dist/models/presets.d.ts.map +1 -1
- package/dist/models/record/fit.d.ts +6 -99
- package/dist/models/record/fit.d.ts.map +1 -1
- package/dist/models/record/run.d.ts +1 -57
- package/dist/models/record/run.d.ts.map +1 -1
- package/dist/models/record/verify-export.d.ts +1 -2
- package/dist/models/record/verify-export.d.ts.map +1 -1
- package/dist/models/sequence/fit-sequence.d.ts +9 -116
- package/dist/models/sequence/fit-sequence.d.ts.map +1 -1
- package/dist/models/sequence/prepare-supervision.d.ts +10 -0
- package/dist/models/sequence/prepare-supervision.d.ts.map +1 -0
- package/dist/models/sequence/run-sequence.d.ts +3 -71
- package/dist/models/sequence/run-sequence.d.ts.map +1 -1
- package/dist/models/sequence/types.d.ts +4 -0
- package/dist/models/sequence/types.d.ts.map +1 -1
- package/dist/native/fit.d.ts +15 -0
- package/dist/native/fit.d.ts.map +1 -0
- package/dist/native/index.d.ts +3 -0
- package/dist/native/index.d.ts.map +1 -0
- package/dist/native/record.d.ts +12 -0
- package/dist/native/record.d.ts.map +1 -0
- package/dist/package-model-CPj0qnQs.js.map +1 -1
- package/dist/packaging/package-model.d.ts.map +1 -1
- package/dist/pipeline/define-pipeline.d.ts +2 -5
- package/dist/pipeline/define-pipeline.d.ts.map +1 -1
- package/dist/project/artifact.d.ts +21 -67
- package/dist/project/artifact.d.ts.map +1 -1
- package/dist/project/config.d.ts +1 -4
- package/dist/project/config.d.ts.map +1 -1
- package/dist/project/discover.d.ts +2 -0
- package/dist/project/discover.d.ts.map +1 -1
- package/dist/project/entry.d.ts +5 -0
- package/dist/project/entry.d.ts.map +1 -0
- package/dist/project/index.d.ts +1 -1
- package/dist/project/index.d.ts.map +1 -1
- package/dist/project/index.js +2 -2
- package/dist/{project-BP331m20.js → project-BR13wXHr.js} +51 -18
- package/dist/project-BR13wXHr.js.map +1 -0
- package/dist/{record-BTZ5LWHF.js → record-MCYVi9O6.js} +60 -114
- package/dist/record-MCYVi9O6.js.map +1 -0
- package/dist/{run-Cwa7vD_T.js → run-PB_jiKnw.js} +4 -6
- package/dist/run-PB_jiKnw.js.map +1 -0
- package/dist/run.d.ts +4 -128
- package/dist/run.d.ts.map +1 -1
- package/dist/sequence-DGoGU7kG.js +219 -0
- package/dist/sequence-DGoGU7kG.js.map +1 -0
- package/dist/train.d.ts +4 -128
- package/dist/train.d.ts.map +1 -1
- package/dist/types.d.ts +0 -2
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +12 -7
- package/docs/agent-skill.md +8 -4
- package/docs/cli.md +85 -28
- package/docs/contributing.md +43 -0
- package/docs/dataset-format.md +4 -2
- package/docs/evaluation.md +66 -5
- package/docs/example-evaluation.md +47 -0
- package/docs/example-results.json +280 -0
- package/docs/examples/money.md +19 -0
- package/docs/examples/time.md +43 -0
- package/docs/getting-started.md +48 -54
- package/docs/native-packages.md +31 -0
- package/docs/neural-training.md +1 -1
- package/docs/parser-api.md +16 -16
- package/docs/pipelines.md +5 -6
- package/docs/primitives/README.md +12 -11
- package/docs/project-structure.md +17 -3
- package/docs/react.md +49 -7
- package/docs/reference/README.md +25 -0
- package/docs/reference/configuration.md +43 -0
- package/docs/reference/evaluation.md +21 -0
- package/docs/reference/pipeline.md +43 -0
- package/docs/reference/runtime.md +56 -0
- package/docs/reference/supervision.md +87 -0
- package/docs/reference/training.md +26 -0
- package/docs/reference/vite.md +20 -0
- package/docs/releases.md +6 -16
- package/docs/repository-design.md +2 -2
- package/docs/runtime-backends.md +8 -11
- package/docs/training.md +73 -0
- package/docs/website-deployment.md +11 -0
- package/native.cjs +1 -0
- package/package.json +10 -5
- 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-Colvwt7u.js +0 -32
- package/dist/define-pipeline-Colvwt7u.js.map +0 -1
- package/dist/encoders/word-tokens.d.ts +0 -5
- package/dist/encoders/word-tokens.d.ts.map +0 -1
- package/dist/models/sequence/create-network.d.ts +0 -3
- package/dist/models/sequence/create-network.d.ts.map +0 -1
- package/dist/project-BP331m20.js.map +0 -1
- package/dist/record-BTZ5LWHF.js.map +0 -1
- package/dist/run-Cwa7vD_T.js.map +0 -1
- package/dist/sequence-C_WfVGO1.js +0 -251
- package/dist/sequence-C_WfVGO1.js.map +0 -1
- package/docs/filter-research.md +0 -63
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
import { n as sameOutput, t as evaluateSequence } from "./evaluate-sequence-ZCwyomzQ.js";
|
|
2
|
-
import { t as packageModel } from "./package-model-CPj0qnQs.js";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
import { readSequenceArtifact, tensorPredictor, tokenize, windows } from "@matchbox-ai/core/internal";
|
|
5
|
-
import { readFile } from "node:fs/promises";
|
|
6
|
-
import { dirname, relative, resolve } from "node:path";
|
|
7
|
-
import { pathToFileURL } from "node:url";
|
|
8
|
-
import { createParser } from "@matchbox-ai/core/runtime";
|
|
9
|
-
import { createHash } from "node:crypto";
|
|
10
|
-
import * as tf from "@tensorflow/tfjs-node";
|
|
11
|
-
//#region src/models/sequence/create-network.ts
|
|
12
|
-
function createNetwork(vocabularySize, labelCount) {
|
|
13
|
-
return tf.sequential({ layers: [
|
|
14
|
-
tf.layers.embedding({
|
|
15
|
-
inputDim: vocabularySize + 2,
|
|
16
|
-
outputDim: 8,
|
|
17
|
-
inputLength: 3,
|
|
18
|
-
embeddingsInitializer: tf.initializers.randomUniform({
|
|
19
|
-
minval: -.1,
|
|
20
|
-
maxval: .1,
|
|
21
|
-
seed: 42
|
|
22
|
-
})
|
|
23
|
-
}),
|
|
24
|
-
tf.layers.flatten(),
|
|
25
|
-
tf.layers.dense({
|
|
26
|
-
units: 16,
|
|
27
|
-
activation: "tanh",
|
|
28
|
-
kernelInitializer: tf.initializers.glorotUniform({ seed: 43 })
|
|
29
|
-
}),
|
|
30
|
-
tf.layers.dense({
|
|
31
|
-
units: labelCount,
|
|
32
|
-
activation: "softmax",
|
|
33
|
-
kernelInitializer: tf.initializers.glorotUniform({ seed: 44 })
|
|
34
|
-
})
|
|
35
|
-
] });
|
|
36
|
-
}
|
|
37
|
-
//#endregion
|
|
38
|
-
//#region src/models/sequence/fit-sequence.ts
|
|
39
|
-
async function fitSequence(examples, recipe, metadata, probes = [], shuffleLabels = false, progress) {
|
|
40
|
-
await tf.setBackend("tensorflow");
|
|
41
|
-
await tf.ready();
|
|
42
|
-
const vocabulary = [...new Set(examples.flatMap((row) => tokenize(row.input, recipe.tokenizer).map((token) => token.key)))].sort();
|
|
43
|
-
const radius = 1;
|
|
44
|
-
const inputs = [];
|
|
45
|
-
const labels = [];
|
|
46
|
-
for (const example of examples) {
|
|
47
|
-
const tokens = tokenize(example.input, recipe.tokenizer);
|
|
48
|
-
const annotations = recipe.annotate(example, tokens);
|
|
49
|
-
if (annotations.length !== tokens.length) throw new Error(`Annotation length mismatch: ${example.input}`);
|
|
50
|
-
windows(tokens, vocabulary, radius).forEach((window, position) => {
|
|
51
|
-
const label = annotations[position];
|
|
52
|
-
if (label === null) return;
|
|
53
|
-
const id = recipe.labels.indexOf(label);
|
|
54
|
-
if (id < 0) throw new Error(`Unknown annotation ${label}: ${example.input}`);
|
|
55
|
-
inputs.push(window);
|
|
56
|
-
labels.push(id);
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
if (recipe.labels.some((_, id) => !labels.includes(id))) throw new Error("Every label needs supervised training examples.");
|
|
60
|
-
if (shuffleLabels) {
|
|
61
|
-
let state = 173;
|
|
62
|
-
for (let index = labels.length - 1; index > 0; index--) {
|
|
63
|
-
state = Math.imul(state, 1664525) + 1013904223 >>> 0;
|
|
64
|
-
const other = state % (index + 1);
|
|
65
|
-
[labels[index], labels[other]] = [labels[other], labels[index]];
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
const model = createNetwork(vocabulary.length, recipe.labels.length);
|
|
69
|
-
const optimizer = tf.train.adam(.02);
|
|
70
|
-
model.compile({
|
|
71
|
-
optimizer,
|
|
72
|
-
loss: "categoricalCrossentropy"
|
|
73
|
-
});
|
|
74
|
-
const x = tf.tensor2d(inputs, [inputs.length, 3], "int32");
|
|
75
|
-
const y = tf.tidy(() => tf.oneHot(tf.tensor1d(labels, "int32"), recipe.labels.length));
|
|
76
|
-
function exportModel(precision) {
|
|
77
|
-
const weights = model.getWeights().map((tensor, index) => {
|
|
78
|
-
const values = Array.from(tensor.dataSync());
|
|
79
|
-
const scale = precision === "int8" ? Math.max(...values.map(Math.abs)) / 127 || 1 : 1;
|
|
80
|
-
return {
|
|
81
|
-
name: model.weights[index].originalName,
|
|
82
|
-
shape: tensor.shape,
|
|
83
|
-
scale,
|
|
84
|
-
values: precision === "int8" ? values.map((value) => Math.round(value / scale)) : values
|
|
85
|
-
};
|
|
86
|
-
});
|
|
87
|
-
return {
|
|
88
|
-
formatVersion: 2,
|
|
89
|
-
kind: "sequence-parser",
|
|
90
|
-
architecture: "embedding-window-mlp",
|
|
91
|
-
...metadata,
|
|
92
|
-
modelTopology: JSON.parse(model.toJSON()),
|
|
93
|
-
tokenizer: recipe.tokenizer,
|
|
94
|
-
readout: recipe.readout,
|
|
95
|
-
vocabulary,
|
|
96
|
-
labels: [...recipe.labels],
|
|
97
|
-
radius,
|
|
98
|
-
threshold: .75,
|
|
99
|
-
precision,
|
|
100
|
-
weights
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
const untrained = exportModel("float32");
|
|
104
|
-
const history = [];
|
|
105
|
-
try {
|
|
106
|
-
await model.fit(x, y, {
|
|
107
|
-
epochs: 55,
|
|
108
|
-
batchSize: 128,
|
|
109
|
-
shuffle: false,
|
|
110
|
-
verbose: 0,
|
|
111
|
-
callbacks: { onEpochEnd(epoch, logs) {
|
|
112
|
-
history.push(Number(logs?.loss));
|
|
113
|
-
progress?.(epoch + 1, Number(logs?.loss));
|
|
114
|
-
} }
|
|
115
|
-
});
|
|
116
|
-
const float = exportModel("float32");
|
|
117
|
-
const quantized = exportModel("int8");
|
|
118
|
-
let maxConfidenceError = 0;
|
|
119
|
-
let labelDisagreements = 0;
|
|
120
|
-
const checkedInputs = [...examples.slice(0, 16).map((row) => row.input), ...probes];
|
|
121
|
-
const nativeProbabilities = checkedInputs.map((inputText) => tf.tidy(() => {
|
|
122
|
-
const tokens = tokenize(inputText, recipe.tokenizer);
|
|
123
|
-
const input = tf.tensor2d(windows(tokens, vocabulary, radius), [tokens.length, 3], "int32");
|
|
124
|
-
return model.predict(input).arraySync();
|
|
125
|
-
}));
|
|
126
|
-
const portable = await tensorPredictor(float);
|
|
127
|
-
try {
|
|
128
|
-
checkedInputs.forEach((inputText, probeIndex) => {
|
|
129
|
-
portable.sequence(inputText).forEach((token, index) => {
|
|
130
|
-
const scores = nativeProbabilities[probeIndex][index];
|
|
131
|
-
const maximum = Math.max(...scores);
|
|
132
|
-
maxConfidenceError = Math.max(maxConfidenceError, Math.abs(maximum - token.confidence));
|
|
133
|
-
if (recipe.labels[scores.indexOf(maximum)] !== token.label) labelDisagreements++;
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
} finally {
|
|
137
|
-
portable.dispose();
|
|
138
|
-
await tf.setBackend("tensorflow");
|
|
139
|
-
}
|
|
140
|
-
if (labelDisagreements || maxConfidenceError > 1e-5) throw new Error("Exported runtime disagrees with TensorFlow.js.");
|
|
141
|
-
return {
|
|
142
|
-
untrained,
|
|
143
|
-
float,
|
|
144
|
-
quantized,
|
|
145
|
-
history,
|
|
146
|
-
parity: {
|
|
147
|
-
examples: checkedInputs.length,
|
|
148
|
-
maxConfidenceError,
|
|
149
|
-
labelDisagreements
|
|
150
|
-
},
|
|
151
|
-
supervisedTokens: inputs.length
|
|
152
|
-
};
|
|
153
|
-
} finally {
|
|
154
|
-
x.dispose();
|
|
155
|
-
y.dispose();
|
|
156
|
-
model.dispose();
|
|
157
|
-
optimizer.dispose();
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
//#endregion
|
|
161
|
-
//#region src/models/sequence/run-sequence.ts
|
|
162
|
-
const hash = (value) => createHash("sha256").update(value).digest("hex");
|
|
163
|
-
async function runSequence(command, project, progress) {
|
|
164
|
-
const { task, config } = project;
|
|
165
|
-
const root = project.root;
|
|
166
|
-
const sequence = config.sequence;
|
|
167
|
-
if (!sequence) throw new Error("A sequence pipeline needs both recipe and decoder modules.");
|
|
168
|
-
const savedArtifact = command === "eval" ? readSequenceArtifact(JSON.parse(await readFile(project.output, "utf8"))) : null;
|
|
169
|
-
const decoderPath = savedArtifact ? resolve(dirname(project.output), savedArtifact.decoderModule) : resolve(root, sequence.decoder);
|
|
170
|
-
const decode = (await import(pathToFileURL(decoderPath).href)).default;
|
|
171
|
-
const evaluateSequence$1 = (parser, examples) => evaluateSequence(parser, examples, (value) => task.validateOutput(value).success).finally(() => {
|
|
172
|
-
if ("dispose" in parser && typeof parser.dispose === "function") parser.dispose();
|
|
173
|
-
});
|
|
174
|
-
const parser = (artifact) => createParser(artifact, task, decode);
|
|
175
|
-
if (savedArtifact) return { evaluation: await evaluateSequence$1(parser(savedArtifact), project.evaluation) };
|
|
176
|
-
const recipe = (await import(pathToFileURL(resolve(root, sequence.recipe)).href)).default;
|
|
177
|
-
const modulePath = (path) => {
|
|
178
|
-
const value = relative(dirname(project.output), path).replaceAll("\\", "/");
|
|
179
|
-
return value.startsWith(".") ? value : `./${value}`;
|
|
180
|
-
};
|
|
181
|
-
for (const example of project.train) {
|
|
182
|
-
const tokens = tokenize(example.input, recipe.tokenizer);
|
|
183
|
-
const labels = recipe.annotate(example, tokens);
|
|
184
|
-
const value = decode(tokens.map((token, index) => ({
|
|
185
|
-
...token,
|
|
186
|
-
label: labels[index] ?? "O",
|
|
187
|
-
confidence: 1
|
|
188
|
-
})), example.input);
|
|
189
|
-
if (!sameOutput(value, example.output)) throw new Error(`Training annotations do not decode to the supplied output: ${example.input}`);
|
|
190
|
-
}
|
|
191
|
-
const started = performance.now();
|
|
192
|
-
const fit = await fitSequence(project.train, recipe, {
|
|
193
|
-
taskMetadata: task.toJSON(),
|
|
194
|
-
taskModule: modulePath(project.taskPath),
|
|
195
|
-
decoderModule: modulePath(resolve(root, sequence.decoder))
|
|
196
|
-
}, project.validation.map((row) => row.input), false, progress);
|
|
197
|
-
const validation = await evaluateSequence$1(parser(fit.quantized), project.validation);
|
|
198
|
-
const bytes = Buffer.byteLength(JSON.stringify(fit.quantized));
|
|
199
|
-
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))}`);
|
|
200
|
-
const shuffledControl = recipe.readout === "last" ? await fitSequence(project.train, recipe, fit.quantized, [], true) : null;
|
|
201
|
-
const challenges = config.challenges ? z.array(z.strictObject({
|
|
202
|
-
input: z.string(),
|
|
203
|
-
output: z.null()
|
|
204
|
-
})).min(1).parse(JSON.parse(await readFile(resolve(root, config.challenges), "utf8"))) : null;
|
|
205
|
-
const report = {
|
|
206
|
-
formatVersion: 1,
|
|
207
|
-
architecture: fit.quantized.architecture,
|
|
208
|
-
backend: "TensorFlow native CPU",
|
|
209
|
-
seed: 42,
|
|
210
|
-
artifactSha256: hash(JSON.stringify(fit.quantized)),
|
|
211
|
-
bytes,
|
|
212
|
-
parameters: fit.quantized.weights.reduce((sum, weight) => sum + weight.values.length, 0),
|
|
213
|
-
datasetSha256: project.sources.map((source) => ({
|
|
214
|
-
source: source.source,
|
|
215
|
-
sha256: hash(source.text)
|
|
216
|
-
})),
|
|
217
|
-
examples: {
|
|
218
|
-
train: project.train.length,
|
|
219
|
-
validation: project.validation.length,
|
|
220
|
-
eval: project.evaluation.length
|
|
221
|
-
},
|
|
222
|
-
shuffledLabelsUngated: shuffledControl ? await evaluateSequence$1(parser({
|
|
223
|
-
...shuffledControl.quantized,
|
|
224
|
-
threshold: 0
|
|
225
|
-
}), project.evaluation) : null,
|
|
226
|
-
supervisedTokens: fit.supervisedTokens,
|
|
227
|
-
loss: fit.history,
|
|
228
|
-
exportParity: fit.parity,
|
|
229
|
-
validation,
|
|
230
|
-
untrainedUngated: await evaluateSequence$1(parser({
|
|
231
|
-
...fit.untrained,
|
|
232
|
-
threshold: 0
|
|
233
|
-
}), project.evaluation),
|
|
234
|
-
untrained: await evaluateSequence$1(parser(fit.untrained), project.evaluation),
|
|
235
|
-
float: await evaluateSequence$1(parser(fit.float), project.evaluation),
|
|
236
|
-
quantized: await evaluateSequence$1(parser(fit.quantized), project.evaluation),
|
|
237
|
-
challenges: challenges ? await evaluateSequence$1(parser(fit.quantized), challenges) : null,
|
|
238
|
-
baseline: project.baseline ? await evaluateSequence$1(project.baseline, project.evaluation) : null,
|
|
239
|
-
trainingMs: performance.now() - started,
|
|
240
|
-
notes: "Validation gates export. Eval labels do not influence selection. Scores are uncalibrated. Unknown tokens abstain. JSON int8 arrays are portable but not a packed binary format."
|
|
241
|
-
};
|
|
242
|
-
await packageModel(project.output, fit.quantized, report);
|
|
243
|
-
return {
|
|
244
|
-
report,
|
|
245
|
-
output: project.output
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
//#endregion
|
|
249
|
-
export { runSequence };
|
|
250
|
-
|
|
251
|
-
//# sourceMappingURL=sequence-C_WfVGO1.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sequence-C_WfVGO1.js","names":["evaluateSequence","evaluate"],"sources":["../src/models/sequence/create-network.ts","../src/models/sequence/fit-sequence.ts","../src/models/sequence/run-sequence.ts"],"sourcesContent":["import * as tf from \"@tensorflow/tfjs-node\";\nexport function createNetwork(vocabularySize: number, labelCount: number) {\n return tf.sequential({\n layers: [\n tf.layers.embedding({\n inputDim: vocabularySize + 2,\n outputDim: 8,\n inputLength: 3,\n embeddingsInitializer: tf.initializers.randomUniform({\n minval: -0.1,\n maxval: 0.1,\n seed: 42,\n }),\n }),\n tf.layers.flatten(),\n tf.layers.dense({\n units: 16,\n activation: \"tanh\",\n kernelInitializer: tf.initializers.glorotUniform({ seed: 43 }),\n }),\n tf.layers.dense({\n units: labelCount,\n activation: \"softmax\",\n kernelInitializer: tf.initializers.glorotUniform({ seed: 44 }),\n }),\n ],\n });\n}\n","import { createNetwork } from \"./create-network.js\";\nimport * as tf from \"@tensorflow/tfjs-node\";\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 shuffleLabels = false,\n progress?: (epoch: number, loss: number) => void,\n) {\n await tf.setBackend(\"tensorflow\");\n await tf.ready();\n const vocabulary = [\n ...new Set(\n examples.flatMap((row) => tokenize(row.input, recipe.tokenizer).map((token) => token.key)),\n ),\n ].sort();\n const radius = 1;\n const inputs: number[][] = [];\n const labels: number[] = [];\n for (const example of examples) {\n const tokens = tokenize(example.input, recipe.tokenizer);\n const annotations = recipe.annotate(example, tokens);\n if (annotations.length !== tokens.length)\n throw new Error(`Annotation length mismatch: ${example.input}`);\n windows(tokens, vocabulary, radius).forEach((window, position) => {\n const label = annotations[position];\n if (label === null) return;\n const id = recipe.labels.indexOf(label!);\n if (id < 0) throw new Error(`Unknown annotation ${label}: ${example.input}`);\n inputs.push(window);\n labels.push(id);\n });\n }\n if (recipe.labels.some((_, id) => !labels.includes(id)))\n throw new Error(\"Every label needs supervised training examples.\");\n if (shuffleLabels) {\n let state = 173;\n for (let index = labels.length - 1; index > 0; index--) {\n state = (Math.imul(state, 1664525) + 1013904223) >>> 0;\n const other = state % (index + 1);\n [labels[index], labels[other]] = [labels[other]!, labels[index]!];\n }\n }\n const model = createNetwork(vocabulary.length, recipe.labels.length);\n const optimizer = tf.train.adam(0.02);\n model.compile({ optimizer, loss: \"categoricalCrossentropy\" });\n const x = tf.tensor2d(inputs, [inputs.length, 3], \"int32\");\n const y = tf.tidy(() => tf.oneHot(tf.tensor1d(labels, \"int32\"), recipe.labels.length));\n function exportModel(precision: \"float32\" | \"int8\"): SequenceArtifact {\n const weights = model.getWeights().map((tensor, index) => {\n const values = Array.from(tensor.dataSync());\n const scale = precision === \"int8\" ? Math.max(...values.map(Math.abs)) / 127 || 1 : 1;\n return {\n name: model.weights[index]!.originalName,\n shape: tensor.shape,\n scale,\n values: precision === \"int8\" ? values.map((value) => Math.round(value / scale)) : values,\n };\n }) as SequenceArtifact[\"weights\"];\n return {\n formatVersion: 2,\n kind: \"sequence-parser\",\n architecture: \"embedding-window-mlp\",\n ...metadata,\n modelTopology: JSON.parse(model.toJSON() as string),\n tokenizer: recipe.tokenizer,\n readout: recipe.readout,\n vocabulary,\n labels: [...recipe.labels],\n radius,\n threshold: 0.75,\n precision,\n weights,\n };\n }\n const untrained = exportModel(\"float32\");\n const history: number[] = [];\n try {\n await model.fit(x, y, {\n epochs: 55,\n batchSize: 128,\n shuffle: false,\n verbose: 0,\n callbacks: {\n onEpochEnd(epoch, logs) {\n history.push(Number(logs?.loss));\n progress?.(epoch + 1, Number(logs?.loss));\n },\n },\n });\n const float = exportModel(\"float32\");\n const quantized = exportModel(\"int8\");\n let maxConfidenceError = 0;\n let labelDisagreements = 0;\n const checkedInputs = [...examples.slice(0, 16).map((row) => row.input), ...probes];\n const nativeProbabilities = checkedInputs.map((inputText) =>\n tf.tidy(() => {\n const tokens = tokenize(inputText, recipe.tokenizer);\n const input = tf.tensor2d(windows(tokens, vocabulary, radius), [tokens.length, 3], \"int32\");\n return (model.predict(input) as tf.Tensor).arraySync() as number[][];\n }),\n );\n const portable = await tensorPredictor(float);\n try {\n checkedInputs.forEach((inputText, probeIndex) => {\n portable.sequence(inputText).forEach((token, index) => {\n const scores = nativeProbabilities[probeIndex]![index]!;\n const maximum = Math.max(...scores);\n maxConfidenceError = Math.max(maxConfidenceError, Math.abs(maximum - token.confidence));\n if (recipe.labels[scores.indexOf(maximum)] !== token.label) labelDisagreements++;\n });\n });\n } finally {\n portable.dispose();\n await tf.setBackend(\"tensorflow\");\n }\n if (labelDisagreements || maxConfidenceError > 1e-5)\n throw new Error(\"Exported runtime disagrees with TensorFlow.js.\");\n return {\n untrained,\n float,\n quantized,\n history,\n parity: { examples: checkedInputs.length, maxConfidenceError, labelDisagreements },\n supervisedTokens: inputs.length,\n };\n } finally {\n x.dispose();\n y.dispose();\n model.dispose();\n optimizer.dispose();\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) throw new Error(\"A sequence pipeline needs both recipe and decoder modules.\");\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\") parser.dispose();\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 for (const example of project.train) {\n const tokens = tokenize(example.input, recipe.tokenizer);\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 const started = performance.now();\n const fit = await fitSequence(\n project.train,\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 false,\n progress,\n );\n const validation = await evaluateSequence(parser(fit.quantized), project.validation);\n const bytes = Buffer.byteLength(JSON.stringify(fit.quantized));\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 const shuffledControl =\n recipe.readout === \"last\"\n ? await fitSequence(project.train, recipe, fit.quantized, [], true)\n : null;\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: 1,\n architecture: fit.quantized.architecture,\n backend: \"TensorFlow native CPU\",\n seed: 42,\n artifactSha256: hash(JSON.stringify(fit.quantized)),\n bytes,\n parameters: fit.quantized.weights.reduce((sum, weight) => sum + weight.values.length, 0),\n datasetSha256: project.sources.map((source) => ({\n source: source.source,\n sha256: hash(source.text),\n })),\n examples: {\n train: project.train.length,\n validation: project.validation.length,\n eval: project.evaluation.length,\n },\n shuffledLabelsUngated: shuffledControl\n ? await evaluateSequence(\n parser({ ...shuffledControl.quantized, threshold: 0 }),\n project.evaluation,\n )\n : null,\n supervisedTokens: fit.supervisedTokens,\n loss: fit.history,\n exportParity: fit.parity,\n validation,\n untrainedUngated: await evaluateSequence(\n parser({ ...fit.untrained, threshold: 0 }),\n project.evaluation,\n ),\n untrained: await evaluateSequence(parser(fit.untrained), project.evaluation),\n float: await evaluateSequence(parser(fit.float), project.evaluation),\n quantized: await evaluateSequence(parser(fit.quantized), project.evaluation),\n challenges: challenges ? await evaluateSequence(parser(fit.quantized), challenges) : null,\n baseline: project.baseline\n ? await evaluateSequence(project.baseline, project.evaluation)\n : null,\n trainingMs: performance.now() - started,\n notes:\n \"Validation gates export. Eval labels do not influence selection. Scores are uncalibrated. Unknown tokens abstain. JSON int8 arrays are portable but not a packed binary format.\",\n };\n await packageModel(project.output, fit.quantized, report);\n return { report, output: project.output };\n}\n"],"mappings":"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"}
|
package/docs/filter-research.md
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
# Historical filter proof findings
|
|
2
|
-
|
|
3
|
-
This document records BOO-46 before the TensorFlow migration. BOO-47 removes these handwritten trainers and routes filters through the shared neural training path. Current results and limitations are in the [neural training guide](neural-training.md).
|
|
4
|
-
|
|
5
|
-
The first full toolchain works: examples train a small local model, validation selects a candidate, packaging generates a typed file import, and React uses it to filter a table. This experiment does not yet establish a compelling advantage over a reasonable deterministic parser.
|
|
6
|
-
|
|
7
|
-
## Reproduction and data
|
|
8
|
-
|
|
9
|
-
Run `bun run train`, `bun run eval`, `bun run research`, and `bun run test:browser`. The artifact report is generated beside `examples/filters/.matchbox/filters/model.matchbox`; challenge results are in `challenges.report.json`. Browser results are written under `test-results/` and uploaded by CI.
|
|
10
|
-
|
|
11
|
-
The fixed corpus contains 176 single-clause training examples, 32 validation examples, and 32 evaluation examples across 13 output classes. These cover three statuses, churn exclusion, four countries, five numeric operators, and explicit AND/OR combinations. Examples contain no missing labels and must satisfy the application schema before fitting. All three splits are separate files. Exact input overlap across splits is rejected.
|
|
12
|
-
|
|
13
|
-
The corpus was assembled locally from a small set of controlled phrase variations. Validation and evaluation contain distinct inputs and unseen combinations/amounts, but share vocabulary and phrase families with training. This is a software integration benchmark, not an independent linguistic generalization study. BOO-35's broader requirement for independent eval templates, dates, ownership, ranges, and adversarial coverage remains open.
|
|
14
|
-
|
|
15
|
-
The eight additional challenge queries are diagnostic examples, not a second hidden test set. We used them to discover and fix mixed-field partial parses and unsupported negation. They cannot be used as unbiased evidence of the resulting parser's generalization.
|
|
16
|
-
|
|
17
|
-
## Models and selection
|
|
18
|
-
|
|
19
|
-
Both candidates use 212 normalized word/bigram features and 13 predicate classes, giving 2,756 weights. Numeric spans are replaced by a shared feature during fitting; their actual values are normalized after recognition. Field-exclusive tokens learned from training reject detected mixed-field clauses. That guard is a heuristic, not a proof that all input semantics were consumed.
|
|
20
|
-
|
|
21
|
-
Nearest centroid averages and normalizes feature vectors by label. The linear model uses deterministic full-batch softmax regression, zero initialization, 300 training iterations, and a small L2 penalty. Each candidate is quantized to signed int8 values with a shared scale before scoring. The artifact currently stores those integers as JSON, rather than a binary tensor format.
|
|
22
|
-
|
|
23
|
-
Selection uses validation exact match and serialized artifact size. The initial exploratory 80% requirement chose the centroid model. We raised the example's requirement to 95% after inspecting validation failures, selecting linear regression at a modest size increase. The held-out report is now visible during development, so these results should not be treated as preregistered research.
|
|
24
|
-
|
|
25
|
-
| Approach | Validation exact | Eval exact | Eval abstention | Invalid output | Serialized artifact |
|
|
26
|
-
| ---------------- | ---------------------: | ---------: | --------------: | -------------: | ------------------: |
|
|
27
|
-
| Nearest centroid | 81.25% | 84.375% | 15.625% | 0% | 14,597 bytes |
|
|
28
|
-
| Linear softmax | 100% | 100% | 0% | 0% | 17,090 bytes |
|
|
29
|
-
| Rules | Not used for selection | 100% | 0% | 0% | No model weights |
|
|
30
|
-
|
|
31
|
-
The selected artifact SHA-256 is `03ef341142e8b2779a48d1033f58f08ded5e036101bcf72a2a36277f40027cfb`. The generated report records the corresponding data hashes. Package changes or project paths can change the artifact hash; rerun the commands to obtain current results.
|
|
32
|
-
|
|
33
|
-
Zero invalid output is enforced by deterministic validation. It does not establish semantic accuracy. Confidence bins and individual failures are included in the report. The score uses classification margin and vocabulary coverage; it has not been calibrated as a probability.
|
|
34
|
-
|
|
35
|
-
## Browser measurements
|
|
36
|
-
|
|
37
|
-
A local macOS arm64 run used Playwright Chromium, 20 warm-up parses followed by 300 measured parses over three fixed queries per approach. All 300 parses were accepted for each candidate. The desktop result was:
|
|
38
|
-
|
|
39
|
-
| Approach | Warm p50 | Warm p95 | Module load and initialization |
|
|
40
|
-
| -------------------------- | ---------------------: | -------: | -----------------------------: |
|
|
41
|
-
| Nearest centroid | 0.1 ms | 0.2 ms | 7.3 ms |
|
|
42
|
-
| Linear comparison artifact | 0.1 ms | 0.2 ms | 9.2 ms |
|
|
43
|
-
| Rules | Below timer resolution | 0.1 ms | 7.4 ms |
|
|
44
|
-
|
|
45
|
-
The selected parser's initial lazy module load took 39.5 ms; its warm p50/p95 were 0.1/0.2 ms. The mobile viewport run reported 41.3 ms initial loading and the same warm quantiles. Mobile emulation uses the same host CPU and is not a phone hardware benchmark. Comparison modules load later with shared dependencies already warm, so their load times are not directly comparable to initial application loading. Timings include local static asset delivery and are affected by timer precision, JIT, and concurrent test workers.
|
|
46
|
-
|
|
47
|
-
The Vite build emits the selected model module at about 3.16 KB gzip, with shared runtime and application code separately. The application entry is about 109.4 KB gzip and includes React, Zod, the task definition, and UI code. The raw artifact size is not the total runtime cost. A 64,000-byte artifact limit and a generous 50 ms selected-parser browser p95 limit are enforced as regression budgets.
|
|
48
|
-
|
|
49
|
-
## The rule comparison matters
|
|
50
|
-
|
|
51
|
-
The baseline has three anchored recognition regexes after common-word normalization: status, country, and numeric comparison. It uses explicit country/operator maps, three recognition branches, and the same boolean compiler and schema validation as the learned parser. Its current implementation is under 100 lines. It was expanded to cover the authored training language rather than left as a weak straw baseline.
|
|
52
|
-
|
|
53
|
-
The baseline matches the model's supported eval accuracy and runs faster. On the challenge set it also handles `not active customers`, which the model abstains on because only churn exclusion was trained. Both approaches abstain on the implicit multi-field flagship query. The linear model's successful import and tiny weights do not by themselves demonstrate that it should replace these rules.
|
|
54
|
-
|
|
55
|
-
## What this establishes
|
|
56
|
-
|
|
57
|
-
A developer can define a typed task, edit ordinary JSONL examples, run one Bun command, and import a local learned parser without handling tensors, model architecture, or runtime prompts. Quantized learned inference is fast enough here that adding WebGPU has no demonstrated payoff. The API and file-import packaging are ready to support a better parser without changing application call sites.
|
|
58
|
-
|
|
59
|
-
The next substantive research step is independently authored language data and semantic span classification. It should target implicit conjunctions, operator/value attachment, negation, ranges, dates, and ownership, and measure false acceptance as well as exact match. We should compare that against this clause model and the rule baseline before generalizing the framework or claiming a regex replacement advantage.
|
|
60
|
-
|
|
61
|
-
## Ticket coverage
|
|
62
|
-
|
|
63
|
-
BOO-46 delivers this integration proof. It implements the bounded AST, rule baseline, two learned candidates, local runtime, generated wrapper/import, React integration, and training command. It also establishes initial eval and report commands. It leaves BOO-35's independent broad dataset, BOO-37's span-versus-generation comparison, BOO-38's proper calibration, and BOO-43/44's unified component/latency reporting unfinished. The broader tickets should remain open where their acceptance criteria exceed this proof.
|