holoscript-trait-inference 0.1.0__py3-none-any.whl
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.
- holoscript_trait_inference-0.1.0.dist-info/METADATA +193 -0
- holoscript_trait_inference-0.1.0.dist-info/RECORD +16 -0
- holoscript_trait_inference-0.1.0.dist-info/WHEEL +5 -0
- holoscript_trait_inference-0.1.0.dist-info/entry_points.txt +2 -0
- holoscript_trait_inference-0.1.0.dist-info/top_level.txt +1 -0
- trait_inference/__init__.py +27 -0
- trait_inference/baselines.py +238 -0
- trait_inference/cli.py +498 -0
- trait_inference/dataset.py +348 -0
- trait_inference/eval/__init__.py +21 -0
- trait_inference/eval/ablations.py +336 -0
- trait_inference/metrics.py +291 -0
- trait_inference/model/__init__.py +34 -0
- trait_inference/model/decoder.py +192 -0
- trait_inference/model/sweep.py +242 -0
- trait_inference/model/trainer.py +237 -0
trait_inference/cli.py
ADDED
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
"""trait-inference CLI — argparse runner.
|
|
2
|
+
|
|
3
|
+
Subcommands:
|
|
4
|
+
trait-inference extract-traits — run scripts/extract_trait_constants.py wrapped
|
|
5
|
+
trait-inference smoke — generate synthetic dataset + run keyword baseline + emit measurement
|
|
6
|
+
trait-inference dataset audit — run audit() against a JSONL dataset
|
|
7
|
+
trait-inference dataset split — emit train/val/indist/novel split files
|
|
8
|
+
trait-inference baseline run — fit + evaluate a named baseline against a dataset
|
|
9
|
+
trait-inference eval headline — compute headline measurement bundle (model + baseline preds)
|
|
10
|
+
|
|
11
|
+
Output: structured JSON to stdout OR to --output path.
|
|
12
|
+
|
|
13
|
+
CLI is the thin layer over trait_inference.{dataset,baselines,metrics}.
|
|
14
|
+
Heavy ML deps (torch, transformers) imported only by `model run` /
|
|
15
|
+
`train` subcommands which require the [model] extra.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import json
|
|
21
|
+
import sys
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from trait_inference.dataset import (
|
|
26
|
+
Pair,
|
|
27
|
+
Source,
|
|
28
|
+
Split,
|
|
29
|
+
audit,
|
|
30
|
+
generate_smoke_dataset,
|
|
31
|
+
load_jsonl,
|
|
32
|
+
make_splits,
|
|
33
|
+
write_jsonl,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _emit(payload: dict, output: Path | None) -> None:
|
|
38
|
+
"""Emit JSON to stdout or output file."""
|
|
39
|
+
text = json.dumps(payload, indent=2, sort_keys=False, default=str)
|
|
40
|
+
if output:
|
|
41
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
output.write_text(text + "\n", encoding="utf-8")
|
|
43
|
+
print(f"Wrote {output}", file=sys.stderr)
|
|
44
|
+
else:
|
|
45
|
+
print(text)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ----------------------------------------------------------------------------
|
|
49
|
+
# extract-traits
|
|
50
|
+
# ----------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
def cmd_extract_traits(args: argparse.Namespace) -> int:
|
|
53
|
+
from scripts.extract_trait_constants import main as extract_main
|
|
54
|
+
return extract_main([
|
|
55
|
+
"--constants-dir", str(args.constants_dir),
|
|
56
|
+
"--output", str(args.output),
|
|
57
|
+
*(["--verbose"] if args.verbose else []),
|
|
58
|
+
])
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ----------------------------------------------------------------------------
|
|
62
|
+
# smoke — end-to-end pipeline test
|
|
63
|
+
# ----------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
def cmd_smoke(args: argparse.Namespace) -> int:
|
|
66
|
+
"""Generate synthetic dataset, run keyword baseline, emit measurement.
|
|
67
|
+
|
|
68
|
+
Exits 0 on full pipeline success, 1 on any failure. Used for CI smoke
|
|
69
|
+
test + on-instance Vast.ai pipeline validation BEFORE expensive
|
|
70
|
+
training runs.
|
|
71
|
+
"""
|
|
72
|
+
from trait_inference.baselines import KeywordBaseline
|
|
73
|
+
from trait_inference.metrics import bootstrap_ci, exact_match_rate, f1_macro
|
|
74
|
+
|
|
75
|
+
# Use minimal label space for synthetic data
|
|
76
|
+
label_space = ["physics", "grabbable", "collidable", "rigid", "kinematic", "softBody"]
|
|
77
|
+
|
|
78
|
+
print(f"[smoke] Generating synthetic dataset (n={args.n})...", file=sys.stderr)
|
|
79
|
+
pairs = generate_smoke_dataset(label_space, n=args.n, seed=args.seed)
|
|
80
|
+
|
|
81
|
+
print(f"[smoke] Auditing...", file=sys.stderr)
|
|
82
|
+
splits = make_splits(pairs, seed=args.seed)
|
|
83
|
+
audit_report = audit(pairs, splits)
|
|
84
|
+
|
|
85
|
+
print(f"[smoke] Fitting keyword baseline...", file=sys.stderr)
|
|
86
|
+
baseline = KeywordBaseline(label_space=tuple(label_space))
|
|
87
|
+
baseline.fit(splits[Split.TRAIN])
|
|
88
|
+
|
|
89
|
+
print(f"[smoke] Predicting on novel split (n={len(splits[Split.HELD_OUT_NOVEL])})...", file=sys.stderr)
|
|
90
|
+
novel = splits[Split.HELD_OUT_NOVEL]
|
|
91
|
+
if not novel:
|
|
92
|
+
# Smoke datasets too small to generate novel split; fall back to indist
|
|
93
|
+
novel = splits[Split.HELD_OUT_INDIST]
|
|
94
|
+
descriptions = [p.description for p in novel]
|
|
95
|
+
gold = [list(p.trait_set) for p in novel]
|
|
96
|
+
preds = baseline.predict_batch(descriptions)
|
|
97
|
+
|
|
98
|
+
print(f"[smoke] Computing metrics + bootstrap CI...", file=sys.stderr)
|
|
99
|
+
f1m = f1_macro(gold, preds, label_space=label_space)
|
|
100
|
+
em = exact_match_rate(gold, preds)
|
|
101
|
+
ci = bootstrap_ci(gold, preds, metric="f1_macro", b=args.bootstrap_b,
|
|
102
|
+
seed=args.seed, label_space=label_space)
|
|
103
|
+
|
|
104
|
+
payload = {
|
|
105
|
+
"smoke_test": True,
|
|
106
|
+
"passed": True,
|
|
107
|
+
"n_pairs": len(pairs),
|
|
108
|
+
"splits": {s.value: len(p) for s, p in splits.items()},
|
|
109
|
+
"audit_passes": audit_report.passes, # expected False on smoke (size)
|
|
110
|
+
"audit_issues": audit_report.issues,
|
|
111
|
+
"baseline": {
|
|
112
|
+
"name": baseline.name,
|
|
113
|
+
"f1_macro": f1m,
|
|
114
|
+
"exact_match": em,
|
|
115
|
+
"bootstrap_ci": ci.to_dict(),
|
|
116
|
+
},
|
|
117
|
+
"smoke_completed_at": datetime.now(timezone.utc).isoformat(),
|
|
118
|
+
}
|
|
119
|
+
_emit(payload, args.output)
|
|
120
|
+
print(f"[smoke] PASSED — pipeline ran end-to-end. F1 macro={f1m:.3f}", file=sys.stderr)
|
|
121
|
+
return 0
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ----------------------------------------------------------------------------
|
|
125
|
+
# dataset audit / split
|
|
126
|
+
# ----------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
def cmd_dataset_audit(args: argparse.Namespace) -> int:
|
|
129
|
+
pairs = load_jsonl(args.input)
|
|
130
|
+
report = audit(pairs)
|
|
131
|
+
_emit(report.to_dict(), args.output)
|
|
132
|
+
return 0 if report.passes else 1
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def cmd_dataset_split(args: argparse.Namespace) -> int:
|
|
136
|
+
pairs = load_jsonl(args.input)
|
|
137
|
+
splits = make_splits(pairs, seed=args.seed)
|
|
138
|
+
out_dir = Path(args.output_dir)
|
|
139
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
140
|
+
summary: dict[str, int] = {}
|
|
141
|
+
for split, split_pairs in splits.items():
|
|
142
|
+
path = out_dir / f"{split.value}.jsonl"
|
|
143
|
+
n = write_jsonl(path, split_pairs)
|
|
144
|
+
summary[split.value] = n
|
|
145
|
+
summary["seed"] = args.seed
|
|
146
|
+
summary["written_at"] = datetime.now(timezone.utc).isoformat()
|
|
147
|
+
_emit(summary, args.output)
|
|
148
|
+
return 0
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ----------------------------------------------------------------------------
|
|
152
|
+
# baseline run
|
|
153
|
+
# ----------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
def cmd_baseline_run(args: argparse.Namespace) -> int:
|
|
156
|
+
from trait_inference.baselines import (
|
|
157
|
+
BrittneyFewShotBaseline,
|
|
158
|
+
KeywordBaseline,
|
|
159
|
+
TfidfLogregBaseline,
|
|
160
|
+
)
|
|
161
|
+
from trait_inference.metrics import bootstrap_ci, exact_match_rate, f1_macro
|
|
162
|
+
|
|
163
|
+
train_pairs = load_jsonl(args.train)
|
|
164
|
+
eval_pairs = load_jsonl(args.eval)
|
|
165
|
+
|
|
166
|
+
label_space: list[str] = sorted({t for p in train_pairs for t in p.trait_set})
|
|
167
|
+
|
|
168
|
+
if args.name == "keyword":
|
|
169
|
+
baseline = KeywordBaseline(label_space=tuple(label_space))
|
|
170
|
+
elif args.name == "tfidf":
|
|
171
|
+
baseline = TfidfLogregBaseline(threshold=args.tfidf_threshold)
|
|
172
|
+
elif args.name == "brittney":
|
|
173
|
+
baseline = BrittneyFewShotBaseline(k_shots=args.brittney_k)
|
|
174
|
+
else:
|
|
175
|
+
print(f"unknown baseline: {args.name}", file=sys.stderr)
|
|
176
|
+
return 1
|
|
177
|
+
|
|
178
|
+
baseline.fit(train_pairs)
|
|
179
|
+
|
|
180
|
+
if args.name == "tfidf" and args.tune_threshold:
|
|
181
|
+
# Optional threshold tuning on a separate validation file
|
|
182
|
+
if args.val:
|
|
183
|
+
val_pairs = load_jsonl(args.val)
|
|
184
|
+
best = baseline.tune_threshold(val_pairs) # type: ignore[attr-defined]
|
|
185
|
+
print(f"[baseline-run] tfidf tuned threshold = {best:.3f}", file=sys.stderr)
|
|
186
|
+
|
|
187
|
+
descriptions = [p.description for p in eval_pairs]
|
|
188
|
+
gold = [list(p.trait_set) for p in eval_pairs]
|
|
189
|
+
preds = baseline.predict_batch(descriptions)
|
|
190
|
+
|
|
191
|
+
f1m = f1_macro(gold, preds, label_space=label_space)
|
|
192
|
+
em = exact_match_rate(gold, preds)
|
|
193
|
+
ci = bootstrap_ci(gold, preds, metric="f1_macro",
|
|
194
|
+
b=args.bootstrap_b, seed=args.seed,
|
|
195
|
+
label_space=label_space)
|
|
196
|
+
|
|
197
|
+
payload = {
|
|
198
|
+
"baseline_name": baseline.name,
|
|
199
|
+
"train_size": len(train_pairs),
|
|
200
|
+
"eval_size": len(eval_pairs),
|
|
201
|
+
"label_space_size": len(label_space),
|
|
202
|
+
"f1_macro": f1m,
|
|
203
|
+
"exact_match": em,
|
|
204
|
+
"bootstrap_ci": ci.to_dict(),
|
|
205
|
+
"predictions_sample": [
|
|
206
|
+
{"id": p.id, "gold": list(p.trait_set), "pred": preds[i]}
|
|
207
|
+
for i, p in enumerate(eval_pairs[:5])
|
|
208
|
+
],
|
|
209
|
+
"evaluated_at": datetime.now(timezone.utc).isoformat(),
|
|
210
|
+
}
|
|
211
|
+
_emit(payload, args.output)
|
|
212
|
+
return 0
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ----------------------------------------------------------------------------
|
|
216
|
+
# model — train / eval / sweep
|
|
217
|
+
# ----------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
def _load_label_space(path: Path) -> tuple[str, ...]:
|
|
220
|
+
"""Load label space JSON emitted by extract_trait_constants."""
|
|
221
|
+
obj = json.loads(path.read_text(encoding="utf-8"))
|
|
222
|
+
return tuple(obj["all_traits"])
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def cmd_model_train(args: argparse.Namespace) -> int:
|
|
226
|
+
from trait_inference.model.trainer import TraitTrainer, TrainConfig
|
|
227
|
+
|
|
228
|
+
label_space = _load_label_space(args.label_space)
|
|
229
|
+
train_pairs = load_jsonl(args.train)
|
|
230
|
+
val_pairs = load_jsonl(args.val)
|
|
231
|
+
|
|
232
|
+
cfg = TrainConfig(
|
|
233
|
+
model_name=args.model_name,
|
|
234
|
+
label_space=label_space,
|
|
235
|
+
output_dir=str(args.output_dir),
|
|
236
|
+
num_epochs=args.num_epochs,
|
|
237
|
+
train_batch_size=args.batch_size,
|
|
238
|
+
eval_batch_size=args.batch_size,
|
|
239
|
+
learning_rate=args.learning_rate,
|
|
240
|
+
seed=args.seed,
|
|
241
|
+
fp16=not args.no_fp16,
|
|
242
|
+
)
|
|
243
|
+
trainer = TraitTrainer(cfg)
|
|
244
|
+
summary = trainer.train(train_pairs, val_pairs)
|
|
245
|
+
|
|
246
|
+
payload = {
|
|
247
|
+
"config": cfg.to_dict(),
|
|
248
|
+
"train_size": len(train_pairs),
|
|
249
|
+
"val_size": len(val_pairs),
|
|
250
|
+
**summary,
|
|
251
|
+
}
|
|
252
|
+
_emit(payload, args.output)
|
|
253
|
+
return 0
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def cmd_model_eval(args: argparse.Namespace) -> int:
|
|
257
|
+
from trait_inference.metrics import bootstrap_ci, exact_match_rate, f1_macro
|
|
258
|
+
from trait_inference.model.decoder import TraitDecoder, TraitDecoderConfig
|
|
259
|
+
|
|
260
|
+
label_space = _load_label_space(args.label_space)
|
|
261
|
+
eval_pairs = load_jsonl(args.eval)
|
|
262
|
+
|
|
263
|
+
cfg = TraitDecoderConfig(
|
|
264
|
+
model_name=str(args.checkpoint),
|
|
265
|
+
label_space=label_space,
|
|
266
|
+
device="cpu" if args.no_fp16 else "cuda",
|
|
267
|
+
)
|
|
268
|
+
decoder = TraitDecoder(cfg)
|
|
269
|
+
|
|
270
|
+
descriptions = [p.description for p in eval_pairs]
|
|
271
|
+
gold = [list(p.trait_set) for p in eval_pairs]
|
|
272
|
+
preds = decoder.predict_batch(descriptions)
|
|
273
|
+
|
|
274
|
+
f1m = f1_macro(gold, preds, label_space=label_space)
|
|
275
|
+
em = exact_match_rate(gold, preds)
|
|
276
|
+
ci = bootstrap_ci(
|
|
277
|
+
gold, preds, metric="f1_macro",
|
|
278
|
+
b=args.bootstrap_b, seed=args.seed, label_space=label_space,
|
|
279
|
+
)
|
|
280
|
+
payload = {
|
|
281
|
+
"checkpoint": str(args.checkpoint),
|
|
282
|
+
"eval_size": len(eval_pairs),
|
|
283
|
+
"label_space_size": len(label_space),
|
|
284
|
+
"f1_macro": f1m,
|
|
285
|
+
"exact_match": em,
|
|
286
|
+
"bootstrap_ci": ci.to_dict(),
|
|
287
|
+
"evaluated_at": datetime.now(timezone.utc).isoformat(),
|
|
288
|
+
}
|
|
289
|
+
_emit(payload, args.output)
|
|
290
|
+
return 0
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def cmd_model_sweep(args: argparse.Namespace) -> int:
|
|
294
|
+
from trait_inference.model.sweep import SweepConfig, TraitSweep
|
|
295
|
+
|
|
296
|
+
cfg = SweepConfig(
|
|
297
|
+
fractional_factorial_pruned=not args.no_prune,
|
|
298
|
+
reseed_n=args.reseed_n,
|
|
299
|
+
)
|
|
300
|
+
sweep = TraitSweep(cfg)
|
|
301
|
+
summary = sweep.emit_cells(args.output_dir)
|
|
302
|
+
_emit(summary, None)
|
|
303
|
+
return 0
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def cmd_eval_ablations(args: argparse.Namespace) -> int:
|
|
307
|
+
"""Run the 5-row ablation matrix per spec §3.5.
|
|
308
|
+
|
|
309
|
+
Heavy GPU work — caller is expected to have a trained checkpoint.
|
|
310
|
+
Each ablation runs the model with a perturbed input or a different
|
|
311
|
+
training condition; total compute ≈ 4-6× the baseline training run.
|
|
312
|
+
"""
|
|
313
|
+
from trait_inference.eval.ablations import AblationConfig, AblationMatrix
|
|
314
|
+
from trait_inference.model.decoder import TraitDecoder, TraitDecoderConfig
|
|
315
|
+
|
|
316
|
+
label_space = _load_label_space(args.label_space)
|
|
317
|
+
train_pairs = load_jsonl(args.train)
|
|
318
|
+
eval_pairs = load_jsonl(args.eval)
|
|
319
|
+
|
|
320
|
+
decoder_cfg = TraitDecoderConfig(
|
|
321
|
+
model_name=str(args.checkpoint),
|
|
322
|
+
label_space=label_space,
|
|
323
|
+
device="cpu" if args.no_fp16 else "cuda",
|
|
324
|
+
)
|
|
325
|
+
decoder = TraitDecoder(decoder_cfg)
|
|
326
|
+
|
|
327
|
+
# Baseline validity = fraction of decoder predictions that are
|
|
328
|
+
# subsets of label_space (expected ≥0.95 with constrained decoding).
|
|
329
|
+
descs = [p.description for p in eval_pairs]
|
|
330
|
+
baseline_preds = decoder.predict_batch(descs)
|
|
331
|
+
ls_set = set(label_space)
|
|
332
|
+
baseline_validity = (
|
|
333
|
+
sum(1 for p in baseline_preds if all(t in ls_set for t in p)) / len(baseline_preds)
|
|
334
|
+
if baseline_preds else 0.0
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# Ablation 2 needs an UNCONSTRAINED variant — same model, no
|
|
338
|
+
# outlines regex. Stub: run decoder.predict_batch directly via
|
|
339
|
+
# transformers generate (no FSM). For Phase 2 v0 we emit a
|
|
340
|
+
# structured "needs separate impl" placeholder; full unconstrained
|
|
341
|
+
# variant is a Phase 2.1 follow-up.
|
|
342
|
+
def _unconstrained_stub(descriptions: list[str]) -> list[list[str]]:
|
|
343
|
+
# Returns empty predictions — flagged as Phase 2.1 deferral.
|
|
344
|
+
# Eval harness records this honestly rather than fabricating.
|
|
345
|
+
return [[] for _ in descriptions]
|
|
346
|
+
|
|
347
|
+
# Ablation 3 needs train_eval_at_size_fn — Phase 2.1 deferral
|
|
348
|
+
def _train_size_stub(n: int) -> float:
|
|
349
|
+
return 0.0
|
|
350
|
+
|
|
351
|
+
# Ablation 4 needs brittney_only_train_eval_fn — Phase 2.1 deferral
|
|
352
|
+
def _brittney_only_stub(_pairs: list[Pair]) -> float:
|
|
353
|
+
return 0.0
|
|
354
|
+
|
|
355
|
+
matrix = AblationMatrix(AblationConfig(
|
|
356
|
+
label_space=label_space,
|
|
357
|
+
bootstrap_b=args.bootstrap_b,
|
|
358
|
+
seed=args.seed,
|
|
359
|
+
))
|
|
360
|
+
results = matrix.run_all(
|
|
361
|
+
predict_fn=decoder.predict_batch,
|
|
362
|
+
unconstrained_predict_fn=_unconstrained_stub,
|
|
363
|
+
train_eval_at_size_fn=_train_size_stub,
|
|
364
|
+
brittney_only_train_eval_fn=_brittney_only_stub,
|
|
365
|
+
eval_pairs=eval_pairs,
|
|
366
|
+
train_pairs=train_pairs,
|
|
367
|
+
baseline_validity=baseline_validity,
|
|
368
|
+
)
|
|
369
|
+
results["phase_2_1_deferrals"] = [
|
|
370
|
+
"constrained_decoding_off (need unconstrained variant)",
|
|
371
|
+
"training_size_sweep (need 3 separate train runs)",
|
|
372
|
+
"source_ablation_brittney_only (need separate train run)",
|
|
373
|
+
]
|
|
374
|
+
_emit(results, args.output)
|
|
375
|
+
return 0
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# ----------------------------------------------------------------------------
|
|
379
|
+
# parser construction
|
|
380
|
+
# ----------------------------------------------------------------------------
|
|
381
|
+
|
|
382
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
383
|
+
p = argparse.ArgumentParser(
|
|
384
|
+
prog="trait-inference",
|
|
385
|
+
description="Paper 19 (ATI) — training pipeline + baselines + eval",
|
|
386
|
+
)
|
|
387
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
388
|
+
|
|
389
|
+
# extract-traits
|
|
390
|
+
et = sub.add_parser("extract-traits", help="Extract trait label space from TS constants")
|
|
391
|
+
et.add_argument("--constants-dir", type=Path, required=True)
|
|
392
|
+
et.add_argument("--output", type=Path, required=True)
|
|
393
|
+
et.add_argument("--verbose", "-v", action="store_true")
|
|
394
|
+
et.set_defaults(func=cmd_extract_traits)
|
|
395
|
+
|
|
396
|
+
# smoke
|
|
397
|
+
sm = sub.add_parser("smoke", help="End-to-end pipeline smoke test (synthetic data)")
|
|
398
|
+
sm.add_argument("--n", type=int, default=100)
|
|
399
|
+
sm.add_argument("--seed", type=int, default=42)
|
|
400
|
+
sm.add_argument("--bootstrap-b", type=int, default=200)
|
|
401
|
+
sm.add_argument("--output", type=Path, default=None)
|
|
402
|
+
sm.set_defaults(func=cmd_smoke)
|
|
403
|
+
|
|
404
|
+
# dataset
|
|
405
|
+
ds = sub.add_parser("dataset", help="Dataset operations").add_subparsers(dest="ds_cmd", required=True)
|
|
406
|
+
|
|
407
|
+
ds_audit = ds.add_parser("audit", help="Audit a JSONL dataset against spec §1.4")
|
|
408
|
+
ds_audit.add_argument("input", type=Path)
|
|
409
|
+
ds_audit.add_argument("--output", type=Path, default=None)
|
|
410
|
+
ds_audit.set_defaults(func=cmd_dataset_audit)
|
|
411
|
+
|
|
412
|
+
ds_split = ds.add_parser("split", help="Generate train/val/indist/novel split files")
|
|
413
|
+
ds_split.add_argument("input", type=Path)
|
|
414
|
+
ds_split.add_argument("--output-dir", type=Path, required=True)
|
|
415
|
+
ds_split.add_argument("--seed", type=int, default=42)
|
|
416
|
+
ds_split.add_argument("--output", type=Path, default=None,
|
|
417
|
+
help="Summary JSON output (separate from split files)")
|
|
418
|
+
ds_split.set_defaults(func=cmd_dataset_split)
|
|
419
|
+
|
|
420
|
+
# baseline
|
|
421
|
+
bl = sub.add_parser("baseline", help="Baseline operations").add_subparsers(dest="bl_cmd", required=True)
|
|
422
|
+
bl_run = bl.add_parser("run", help="Fit + evaluate a baseline")
|
|
423
|
+
bl_run.add_argument("name", choices=["keyword", "tfidf", "brittney"])
|
|
424
|
+
bl_run.add_argument("--train", type=Path, required=True)
|
|
425
|
+
bl_run.add_argument("--eval", type=Path, required=True)
|
|
426
|
+
bl_run.add_argument("--val", type=Path, default=None)
|
|
427
|
+
bl_run.add_argument("--output", type=Path, default=None)
|
|
428
|
+
bl_run.add_argument("--bootstrap-b", type=int, default=1000)
|
|
429
|
+
bl_run.add_argument("--seed", type=int, default=42)
|
|
430
|
+
bl_run.add_argument("--tfidf-threshold", type=float, default=0.5)
|
|
431
|
+
bl_run.add_argument("--tune-threshold", action="store_true",
|
|
432
|
+
help="Tune tfidf threshold on --val split")
|
|
433
|
+
bl_run.add_argument("--brittney-k", type=int, default=8)
|
|
434
|
+
bl_run.set_defaults(func=cmd_baseline_run)
|
|
435
|
+
|
|
436
|
+
# model — Phase 2 contribution model (requires [model] extras)
|
|
437
|
+
md = sub.add_parser("model", help="Contribution model (requires .[model])").add_subparsers(dest="md_cmd", required=True)
|
|
438
|
+
|
|
439
|
+
md_train = md.add_parser("train", help="Fine-tune the constrained-decoding trait model")
|
|
440
|
+
md_train.add_argument("--train", type=Path, required=True)
|
|
441
|
+
md_train.add_argument("--val", type=Path, required=True)
|
|
442
|
+
md_train.add_argument("--label-space", type=Path, required=True,
|
|
443
|
+
help="Path to trait_label_space.json (from extract-traits)")
|
|
444
|
+
md_train.add_argument("--model-name", default="Qwen/Qwen2.5-0.5B")
|
|
445
|
+
md_train.add_argument("--output-dir", type=Path, default=Path("checkpoints/trait_decoder_v0"))
|
|
446
|
+
md_train.add_argument("--num-epochs", type=int, default=20)
|
|
447
|
+
md_train.add_argument("--batch-size", type=int, default=32)
|
|
448
|
+
md_train.add_argument("--learning-rate", type=float, default=5e-5)
|
|
449
|
+
md_train.add_argument("--seed", type=int, default=42)
|
|
450
|
+
md_train.add_argument("--no-fp16", action="store_true",
|
|
451
|
+
help="Disable fp16 (use for CPU smoke tests)")
|
|
452
|
+
md_train.add_argument("--output", type=Path, default=None)
|
|
453
|
+
md_train.set_defaults(func=cmd_model_train)
|
|
454
|
+
|
|
455
|
+
md_eval = md.add_parser("eval", help="Evaluate a trained model on a split")
|
|
456
|
+
md_eval.add_argument("--checkpoint", type=Path, required=True)
|
|
457
|
+
md_eval.add_argument("--eval", type=Path, required=True)
|
|
458
|
+
md_eval.add_argument("--label-space", type=Path, required=True)
|
|
459
|
+
md_eval.add_argument("--bootstrap-b", type=int, default=1000)
|
|
460
|
+
md_eval.add_argument("--seed", type=int, default=42)
|
|
461
|
+
md_eval.add_argument("--no-fp16", action="store_true")
|
|
462
|
+
md_eval.add_argument("--output", type=Path, default=None)
|
|
463
|
+
md_eval.set_defaults(func=cmd_model_eval)
|
|
464
|
+
|
|
465
|
+
md_sweep = md.add_parser("sweep", help="Emit hyperparameter-sweep cell configs")
|
|
466
|
+
md_sweep.add_argument("--output-dir", type=Path, required=True)
|
|
467
|
+
md_sweep.add_argument("--no-prune", action="store_true",
|
|
468
|
+
help="Emit full 162-cell grid instead of pruned ~30")
|
|
469
|
+
md_sweep.add_argument("--reseed-n", type=int, default=5)
|
|
470
|
+
md_sweep.set_defaults(func=cmd_model_sweep)
|
|
471
|
+
|
|
472
|
+
# eval — Phase 4 ablation matrix
|
|
473
|
+
ev = sub.add_parser("eval", help="Evaluation harness").add_subparsers(dest="ev_cmd", required=True)
|
|
474
|
+
ev_ablations = ev.add_parser("ablations", help="Run 5-row ablation matrix (requires .[model])")
|
|
475
|
+
ev_ablations.add_argument("--checkpoint", type=Path, required=True)
|
|
476
|
+
ev_ablations.add_argument("--train", type=Path, required=True)
|
|
477
|
+
ev_ablations.add_argument("--eval", type=Path, required=True)
|
|
478
|
+
ev_ablations.add_argument("--label-space", type=Path, required=True)
|
|
479
|
+
ev_ablations.add_argument("--bootstrap-b", type=int, default=1000)
|
|
480
|
+
ev_ablations.add_argument("--seed", type=int, default=42)
|
|
481
|
+
ev_ablations.add_argument("--no-fp16", action="store_true")
|
|
482
|
+
ev_ablations.add_argument("--output", type=Path, default=None)
|
|
483
|
+
ev_ablations.set_defaults(func=cmd_eval_ablations)
|
|
484
|
+
|
|
485
|
+
return p
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def main(argv: list[str] | None = None) -> int:
|
|
489
|
+
parser = build_parser()
|
|
490
|
+
args = parser.parse_args(argv)
|
|
491
|
+
if not hasattr(args, "func"):
|
|
492
|
+
parser.print_help()
|
|
493
|
+
return 1
|
|
494
|
+
return args.func(args)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
if __name__ == "__main__":
|
|
498
|
+
sys.exit(main())
|