@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.
Files changed (57) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/{define-pipeline-D6I-jAE_.js → define-pipeline-DEkjmOte.js} +2 -1
  3. package/dist/define-pipeline-DEkjmOte.js.map +1 -0
  4. package/dist/index.js +2 -2
  5. package/dist/index.js.map +1 -1
  6. package/dist/load-project.d.ts +1 -0
  7. package/dist/load-project.d.ts.map +1 -1
  8. package/dist/models/presets.d.ts +2 -0
  9. package/dist/models/presets.d.ts.map +1 -1
  10. package/dist/models/sequence/export-parity.d.ts +16 -0
  11. package/dist/models/sequence/export-parity.d.ts.map +1 -0
  12. package/dist/models/sequence/fit-sequence.d.ts +25 -3
  13. package/dist/models/sequence/fit-sequence.d.ts.map +1 -1
  14. package/dist/models/sequence/prepare-supervision.d.ts +3 -3
  15. package/dist/models/sequence/prepare-supervision.d.ts.map +1 -1
  16. package/dist/models/sequence/run-sequence.d.ts +9 -1
  17. package/dist/models/sequence/run-sequence.d.ts.map +1 -1
  18. package/dist/models/sequence/types.d.ts +2 -0
  19. package/dist/models/sequence/types.d.ts.map +1 -1
  20. package/dist/native/fit.d.ts +4 -2
  21. package/dist/native/fit.d.ts.map +1 -1
  22. package/dist/pipeline/define-pipeline.d.ts +1 -0
  23. package/dist/pipeline/define-pipeline.d.ts.map +1 -1
  24. package/dist/project/artifact.d.ts +21 -1
  25. package/dist/project/artifact.d.ts.map +1 -1
  26. package/dist/project/config.d.ts +2 -0
  27. package/dist/project/config.d.ts.map +1 -1
  28. package/dist/project/index.js +1 -1
  29. package/dist/{project-BR13wXHr.js → project-CgNwV9Dj.js} +6 -4
  30. package/dist/project-CgNwV9Dj.js.map +1 -0
  31. package/dist/{run-PB_jiKnw.js → run-MgK9OCQv.js} +3 -3
  32. package/dist/{run-PB_jiKnw.js.map → run-MgK9OCQv.js.map} +1 -1
  33. package/dist/run.d.ts +9 -1
  34. package/dist/run.d.ts.map +1 -1
  35. package/dist/{sequence-DGoGU7kG.js → sequence-NevJFVwZ.js} +90 -37
  36. package/dist/sequence-NevJFVwZ.js.map +1 -0
  37. package/dist/train.d.ts +9 -1
  38. package/dist/train.d.ts.map +1 -1
  39. package/docs/contributing.md +36 -0
  40. package/docs/reference/configuration.md +11 -11
  41. package/docs/reference/pipeline.md +28 -0
  42. package/docs/reference/supervision.md +8 -1
  43. package/docs/reference/training.md +2 -0
  44. package/docs/research/sequence-context.json +12632 -0
  45. package/docs/research/sequence-context.md +66 -0
  46. package/package.json +3 -3
  47. package/prebuilds/darwin-arm64/node.napi.node +0 -0
  48. package/prebuilds/darwin-x64/node.napi.node +0 -0
  49. package/prebuilds/linux-arm64/node.napi.glibc.node +0 -0
  50. package/prebuilds/linux-arm64/node.napi.musl.node +0 -0
  51. package/prebuilds/linux-x64/node.napi.glibc.node +0 -0
  52. package/prebuilds/linux-x64/node.napi.musl.node +0 -0
  53. package/prebuilds/win32-arm64/node.napi.node +0 -0
  54. package/prebuilds/win32-x64/node.napi.node +0 -0
  55. package/dist/define-pipeline-D6I-jAE_.js.map +0 -1
  56. package/dist/project-BR13wXHr.js.map +0 -1
  57. 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
- const vocabulary = [...new Set(examples.flatMap((row) => tokenize(row.input, recipe.tokenizer).map((token) => token.key)))].sort();
25
- const radius = 1;
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 inputs = [];
34
- const labels = [];
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
- inputs.push(window);
45
- labels.push(id);
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) => !labels.includes(id))) throw new Error("Every label needs supervised training examples.");
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 = inputs.length - 1; index > 0; index--) {
96
+ for (let index = count - 1; index > 0; index--) {
55
97
  const other = Math.floor(random() * (index + 1));
56
- [inputs[index], inputs[other]] = [inputs[other], inputs[index]];
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: 3,
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
- let maxConfidenceError = 0;
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
- maxConfidenceError = Math.max(maxConfidenceError, Math.abs(confidence - token.confidence));
108
- if (recipe.labels[row.indexOf(confidence)] !== token.label) labelDisagreements++;
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
- maxConfidenceError,
122
- labelDisagreements
170
+ ...parity.report()
123
171
  },
124
- supervisedTokens: inputs.length
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-DGoGU7kG.js.map
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
- examples: number;
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;
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAKrE"}
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"}
@@ -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 | 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 }`. | 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.