89NGRAM_SIZES: tuple[int, ...] = (1, 2, 3, 4)
125 """The L2-normalised hashed n-gram counts of *command*, plus a bias column."""
127 padded = f
"{START_SENTINEL}{command}{END_SENTINEL}"
128 counts: dict[int, float] = {}
129 for size
in NGRAM_SIZES:
130 for start
in range(len(padded) - size + 1):
132 counts[index] = counts.get(index, 0.0) + 1.0
133 norm = math.sqrt(sum(value * value
for value
in counts.values()))
or 1.0
134 scaled = {index: value / norm
for index, value
in counts.items()}
135 scaled[0] = scaled.get(0, 0.0) + 1.0
173 rows: list[dict[str, str]],
174 epochs: int = DEFAULT_EPOCHS,
175 seed: int = DEFAULT_SEED,
176 learning_rate: float = DEFAULT_LEARNING_RATE,
177 l2: float = DEFAULT_L2,
178 batch: int = DEFAULT_BATCH,
180 """Fit the softmax by minibatch gradient descent over *rows*.
182 Features are extracted once up front rather than per epoch, which is the
183 difference between a run of seconds and a run of minutes.
187 _dbg(
"OC_CHAT.MAIN", func, f
"{len(rows)} rows, {epochs} epochs, batch {batch}", 2)
188 label_of = {name: index
for index, name
in enumerate(LABELS)}
189 examples = [(
features(row[
"command"]), label_of[row[
"truth"]])
for row
in rows]
191 weights=[[0.0] * HASH_SPACE
for _
in LABELS],
192 biases=[0.0] * len(LABELS),
194 rng = random.Random(seed)
195 order = list(range(len(examples)))
196 for epoch
in range(epochs):
198 rate = learning_rate / (1.0 + epoch)
201 for start
in range(0, len(order), batch):
202 chunk = order[start:start + batch]
203 gradients: list[dict[int, float]] = [{}
for _
in LABELS]
204 bias_gradients = [0.0] * len(LABELS)
205 for position
in chunk:
206 vector, truth = examples[position]
207 probabilities = model.probabilities(vector)
208 loss -= math.log(max(probabilities[truth], 1e-12))
209 hits += int(max(range(len(LABELS)),
210 key=probabilities.__getitem__) == truth)
211 for label
in range(len(LABELS)):
212 error = probabilities[label] - (1.0
if label == truth
else 0.0)
215 bias_gradients[label] += error
216 target = gradients[label]
217 for index, value
in vector.items():
218 target[index] = target.get(index, 0.0) + error * value
219 step = rate / len(chunk)
220 for label
in range(len(LABELS)):
221 row = model.weights[label]
222 for index, gradient
in gradients[label].items():
223 row[index] -= step * (gradient + l2 * row[index])
224 model.biases[label] -= step * bias_gradients[label]
225 _dbg(
"OC_CHAT.MAIN", func,
226 f
"epoch {epoch + 1}: loss {loss / len(examples):.4f} "
227 f
"train accuracy {hits / len(examples):.4f}", 3)
228 _dbg(
"OC_CHAT.MAIN", func,
"training complete", 2)
232def evaluate(model: Model, cases: list[dict[str, str]]) -> list[dict[str, object]]:
233 """Predict every case, timing each decision in process."""
236 rows: list[dict[str, object]] = []
238 started = time.perf_counter()
239 prediction, probabilities = model.predict(case[
"command"])
240 elapsed_ms = (time.perf_counter() - started) * 1000.0
242 "command": case[
"command"],
243 "truth": case[
"truth"],
244 "slice": case[
"slice"],
246 "probs": probabilities,
248 "reason": case[
"reason"],
250 hits = sum(row[
"pred"] == row[
"truth"]
for row
in rows)
251 _dbg(
"OC_CHAT.MAIN", func, f
"{hits}/{len(rows)} agreement", 3)
264 """Construct the CLI argument parser."""
266 parser = argparse.ArgumentParser(
267 prog=
"python bench/argv_policy/baselines/token_classifier.py",
268 description=
"Train the hashed n-gram floor and score both case sets.",
270 parser.add_argument(
"--epochs", type=int, default=DEFAULT_EPOCHS,
271 help=f
"Passes over the corpus (default: {DEFAULT_EPOCHS}).")
272 parser.add_argument(
"--seed", type=int, default=DEFAULT_SEED,
273 help=f
"Shuffle seed (default: {DEFAULT_SEED}).")
274 parser.add_argument(
"--learning-rate", type=float, default=DEFAULT_LEARNING_RATE,
275 help=f
"Initial step size (default: {DEFAULT_LEARNING_RATE}).")
276 parser.add_argument(
"--l2", type=float, default=DEFAULT_L2,
277 help=f
"L2 penalty (default: {DEFAULT_L2}).")
278 parser.add_argument(
"--batch", type=int, default=DEFAULT_BATCH,
279 help=f
"Minibatch size (default: {DEFAULT_BATCH}).")
280 parser.add_argument(
"--out-dir", default=
None,
281 help=
"Where to write result files (default: ../results).")
285def main(argv: list[str] |
None =
None) -> int:
286 """CLI entry point: train the floor and write both result files."""
290 bench_dir = Path(__file__).resolve().parent.parent
291 out_dir = Path(args.out_dir)
if args.out_dir
else bench_dir /
"results"
292 out_dir.mkdir(parents=
True, exist_ok=
True)
294 corpus = [json.loads(line)
for line
295 in (bench_dir /
"corpus.jsonl").read_text(encoding=
"utf-8").splitlines()
if line]
296 validation = [json.loads(line)
for line
297 in (bench_dir /
"corpus_val.jsonl").read_text(encoding=
"utf-8").splitlines()
299 print(f
"token_classifier: training on {len(corpus)} commands, "
300 f
"{args.epochs} epochs, hash space {HASH_SPACE}")
301 started = time.perf_counter()
302 model =
train(corpus, args.epochs, args.seed, args.learning_rate, args.l2, args.batch)
303 train_seconds = time.perf_counter() - started
304 train_accuracy =
_accuracy(model, corpus)
305 validation_accuracy =
_accuracy(model, validation)
306 print(f
"token_classifier: trained in {train_seconds:.1f}s; "
307 f
"train {train_accuracy:.4f}, validation {validation_accuracy:.4f}")
309 for condition, case_file
in ((
"hand",
"cases.json"), (
"hard",
"cases_hard.json")):
310 cases = json.loads((bench_dir / case_file).read_text(encoding=
"utf-8"))
312 hits = sum(row[
"pred"] == row[
"truth"]
for row
in rows)
314 "model":
"token_classifier",
315 "condition": condition,
318 "route":
"CPU, standard library, hashed character n-grams",
319 "case_file": case_file,
320 "train_rows": len(corpus),
321 "validation_rows": len(validation),
322 "epochs": args.epochs,
324 "learning_rate": args.learning_rate,
327 "hash_space": HASH_SPACE,
328 "ngram_sizes": list(NGRAM_SIZES),
329 "train_seconds": train_seconds,
330 "train_accuracy": train_accuracy,
331 "validation_accuracy": validation_accuracy,
334 target = out_dir / f
"token_classifier_{condition}.json"
335 target.write_text(json.dumps(payload, indent=1, sort_keys=
True) +
"\n", encoding=
"utf-8")
336 print(f
" {condition:5} {hits}/{len(rows)} agreement -> {target.name}")
337 _dbg(
"OC_CHAT.MAIN", func,
"exit 0", 2)
Model train(list[dict[str, str]] rows, int epochs=DEFAULT_EPOCHS, int seed=DEFAULT_SEED, float learning_rate=DEFAULT_LEARNING_RATE, float l2=DEFAULT_L2, int batch=DEFAULT_BATCH)