/experiments/3_9_autotune/

Section 3.9: self-tuning on a tuning set vs unseen data, and three gate designs (150 eval items and the scoring code included)

Files in this folder

File / folderDescriptionSize
πŸ“ ../Up one level-
πŸ“ runs/Subfolder-
πŸ“„ README.md15.6 KB
πŸ“„ autotune.py6.5 KB
πŸ“„ baseline.py707 B
πŸ“„ baseline_en.json79.5 KB
πŸ“„ build_items.py2.4 KB
πŸ“„ copy_check.py2.1 KB
πŸ“„ curve_reeval.py1.6 KB
πŸ“„ curve_reeval_en.json4.6 KB
πŸ“„ curve_summary.py1.6 KB
πŸ“„ curve_ungated_en.json1.8 KB
πŸ“„ final_eval.py1.4 KB
πŸ“„ final_eval_en.json2.3 KB
πŸ“„ items_en.json124.6 KB
πŸ“„ lib.py3.0 KB
πŸ“„ meeting_lines_raw_en.json47.7 KB
πŸ“„ meta_prompt_en.md1.2 KB
πŸ“„ names_en.md358 B
πŸ“„ noise.py863 B
πŸ“„ noise_en.jsonl216 B
πŸ“„ noise_trace.py2.0 KB
πŸ“„ noise_trace_en.json5.9 KB
πŸ“„ run_en.sh834 B
πŸ“„ skill_v0_en.md230 B

README.md

3.9 Self-tuning loop, holdout set and gates (English runs)

About the model name. The code requests deepseek-v4-flash, the name AI Builder Space accepts (see WORKER in lib.py). When DeepSeek released V4.1 Flash on 2026-09-10, it retired V4 Flash, and requests to the old name deepseek-v4-flash are now served by DeepSeek-V4.1-Flash (DeepSeek API Change Log). These runs took place in October 2026, so the text calls the worker deepseek-v4.1-flash.

This folder holds the English eval set for Module 3 and the self-tuning experiment built on it. It supplies:

All numbers below come from the English runs, finished on 2026-10-20.

The question

Data and metric

Items (items_en.json). 150 items, each with a raw ASR transcript (raw) and the reference text (truth):

The truth texts contain 225 mentions of names and terms (memos 53, meeting 172; people 142, terms 83). 39 items contain none.

Names list (names_en.md). 9 people (Jaxon, Saoirse, Kaitlynn, Xochitl, Aoife, Kaveh, Priya, Mairead, Tadhg) and 11 terms (Koyeb, vLLM, KV cache, LoRA, MLX, AI Builder Space, Superlinear, intake, VoiceFlow, Codex, prefill), 20 entries in total. The worker receives this list together with the skill.

Starting skill (skill_v0_en.md, 37 words, 229 characters): "You will receive a raw speech-to-text transcript. Using the list of names and terms below, correct any names and terms that were transcribed wrong. Do not change anything else. Output only the corrected text, with no explanation."

Metric (score_item and aggregate in lib.py). Only the 20 entries in the names list count. For each entry, if the truth has n mentions and the output has k, then correct = min(n, k) and wrong insertions = k βˆ’ n when k > n. Score = (correct βˆ’ wrong insertions) / total expected mentions. Matching is case-insensitive and whole-word; multi-word terms also match with a hyphen or without the space ("KV-cache", "KVcache"), and possessives ("Aoife's") count. A score of 0.78 means about 78 net correct per 100 names and terms that should be there. A model that inserts names freely can score below zero.

Baseline and noise (used in 3.2 and 3.4)

baseline.py en β†’ baseline_en.json (one run):

correct wrong insertions score
Raw ASR 101/225 0 0.4489
skill_v0_en.md + deepseek-v4.1-flash 199/225 24 0.7778

All 24 wrong insertions come from meeting lines, and 21 of them from a single item, M036. In 7 of the 150 outputs (M012, M018, M036, M070, M080, M084, M086), the worker treated the transcript as a message to answer: it said there was no transcript to correct and responded to the content instead. On M036 it listed the entire names list while doing so. Without M036, the v0 score would be (199 βˆ’ 3)/225 = 0.8711.

noise.py en <skill> β†’ noise_en.jsonl (same skill, all 150 items, 3 runs; temperature 0 throughout):

skill run 1 run 2 run 3 items whose pass/fail flips across runs
v0 (skill_v0_en.md) 0.7244 0.7244 0.6533 3
round-2 skill (runs/en_ungated_1/skill_r02.md, 782 words) 0.76 0.8089 0.7556 1

Together with the baseline run, four v0 runs span 0.6533–0.7778. The round-2 skill is not stable either: one flipping item moves the 150-item total by about 0.05.

Self-tuning without a gate

Setup (autotune.py).

Name and term mentions per split:

seed tune holdout test unseen (holdout + test)
1 39 91 95 186
2 35 95 95 190
3 45 87 93 180

Without a gate (runs/en_ungated_{1,2,3}/), every proposal is accepted for 8 rounds. Because single runs are noisy, each round's skill was run 3 times on the tune set and 3 times on the 126 unseen items (the first run is in log.json; curve_reeval.py adds two more, stored in curve_reeval_en.json). curve_summary.py averages them over the three seeds into curve_ungated_en.json:

round tune mean unseen mean per-seed means min–max single runs min–max prompt words (mean)
0 0.673 0.743 0.636–0.860 0.581–0.868 37
1 0.930 0.867 0.842–0.886 0.812–0.889 409
2 0.982 0.825 0.719–0.884 0.597–0.911 698
3 1.000 0.855 0.781–0.894 0.737–0.900 902
4 0.975 0.850 0.776–0.889 0.769–0.889 958
5 1.000 0.830 0.728–0.884 0.661–0.889 1,110
6 0.998 0.834 0.746–0.882 0.726–0.889 1,178
7 1.000 0.868 0.833–0.886 0.801–0.894 1,258
8 1.000 0.829 0.751–0.877 0.737–0.879 1,237

The gain on unseen items happens in round 1 (0.743 β†’ 0.867) and stops there. Rounds 2–8 move between 0.825 and 0.868 while the tune set sits at 0.975–1.0 and the prompt grows from 409 to about 1,240 words (229 β†’ 7,975 characters). Seed 1 shows the overfitting most clearly: unseen 0.636 β†’ 0.842 (round 1) β†’ 0.719 (round 2), then 0.728–0.833, while its tune score is 1.0 from round 3 on.

Copying from the tune set (copy_check.py en). After removing the 20 vocabulary entries from both texts, a tune item counts as copied if any 8 consecutive words of its raw transcript or its truth appear in the round-8 skill. Seed 1: 2 of 24 tune items (W10, M053); seed 2: 3 (W20, M018, W17); seed 3: 2 (M101, L04). Unseen items, checked the same way as a control: 0 in every seed. An example from runs/en_ungated_2/skill_r08.md, where W20 is a tune item: "For example, "remind the solicitor the app store screenshots need to be redone" becomes "remind Xochitl the app store screenshots need to be redone.""

Gates

Same seeds and splits as above. Each round's proposal takes effect only if it passes the gate; otherwise the current skill stays, and the tuner is told: "Your previous proposal was rejected by the gate: on a holdout set you cannot see, it lowered the score or broke items that used to be right. The failures below are still for the version currently in force; try a different approach." No holdout content is shown. The loop stops after 3 rejections in a row, or after 8 rounds.

An item counts as right when it has no missed and no wrongly added entries. Broken items were right under the current skill and are wrong under the proposal; fixed items are the reverse. The current skill's holdout numbers are the ones measured when it was accepted (or at round 0); they are not re-measured each round.

Gate Flag Rule
1, strict --gate-strict Holdout score does not drop, and no holdout item breaks.
2, single baseline --gate Holdout score rises by at least 0.01 (MARGIN), and broken items do not outnumber fixed ones.
3, averaged baseline --gate-avg Same as gate 2, but the holdout set is run twice and the two-run means are compared. Broken and fixed items are counted on the first run.

With 87–95 mentions in a holdout split, one mention is worth 0.0105–0.0115, so a margin of 0.01 means at least one more net correct mention. Every proposal in gates 1 and 2 was judged on a single run, so part of what the gates react to is noise.

Results (runs/en_{gatedstrict,gated,gatedavg}_{1,2,3}/log.json):

Final comparison on the test set

final_eval.py en β†’ final_eval_en.json: each stopping skill is run 3 times on that seed's 66-item test split, which no decision used, and the three scores are averaged.

version seed 1 seed 2 seed 3 mean words (mean)
v0 0.6105 0.8421 0.5591 0.671 37
ungated, round 2 0.6632 0.8632 0.8279 0.785 698
ungated, round 8 0.7509 0.8632 0.8316 0.815 1,237
gate 1, strict 0.7228 0.8316 0.5233 0.693 124 (298 / 37 / 37)
gate 2, single baseline 0.8491 0.8421 0.7921 0.828 565 (354 / 660 / 682)
gate 3, averaged baseline 0.8421 0.8316 0.4875 0.720 429 (636 / 613 / 37)

Single test runs across this table span 0.3011–0.9032; v0's runs alone span 0.3333–0.8526. Gate 2 gave the highest mean with prompts less than half the length of eight ungated rounds. On seeds 1 and 2, gate 3 is close to gate 2; its mean is pulled down by seed 3 staying at v0, which scored 0.8736 on the holdout set and 0.487 on test.

Files

Data and prompts:

Code:

Results:

How to rerun

Run the commands from this folder. The scripts need Python with httpx, and an AI_BUILDER_TOKEN for the worker calls on AI Builder Space.

export AI_BUILDER_TOKEN=...

python baseline.py en                       # -> baseline_en.json
python noise.py en skill_v0_en.md           # appends one line to noise_en.jsonl

# Self-tuning, one seed at a time (seed = 1, 2 or 3)
python autotune.py en 1                     # no gate, 8 rounds  -> runs/en_ungated_1/
python autotune.py en 1 --gate-strict       # gate 1             -> runs/en_gatedstrict_1/
python autotune.py en 1 --gate              # gate 2             -> runs/en_gated_1/
python autotune.py en 1 --gate-avg          # gate 3             -> runs/en_gatedavg_1/

python noise.py en runs/en_ungated_1/skill_r02.md
python final_eval.py en                     # needs the ungated runs for all three seeds
python curve_reeval.py en                   # -> curve_reeval_en.json
python curve_summary.py en                  # -> curve_ungated_en.json
python copy_check.py en                     # copying check on the round-8 ungated skills

run_en.sh runs the noise, autotune (three seeds in parallel per variant), final and curve steps in order and writes each step's output to runs_en_<step>.out. It skips any step that already has a done_en_<step> marker file; those markers are not included here, so a fresh copy runs everything. Set P at the top of the script to your Python interpreter before running it. It does not run baseline.py, curve_summary.py or copy_check.py.

Notes: