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
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: holoscript-trait-inference
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Paper 19 (ATI) — Automated Trait Inference for HoloScript .hsplus. Phase 3 training pipeline + baselines + eval harness.
|
|
5
|
+
Author: HoloScript Core
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: holoscript,trait-inference,paper-19,ml
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: numpy>=1.24
|
|
11
|
+
Requires-Dist: scikit-learn>=1.3
|
|
12
|
+
Requires-Dist: scipy>=1.11
|
|
13
|
+
Requires-Dist: pandas>=2.0
|
|
14
|
+
Provides-Extra: model
|
|
15
|
+
Requires-Dist: torch>=2.1; extra == "model"
|
|
16
|
+
Requires-Dist: transformers>=4.40; extra == "model"
|
|
17
|
+
Requires-Dist: sentence-transformers>=2.6; extra == "model"
|
|
18
|
+
Requires-Dist: outlines>=0.0.40; extra == "model"
|
|
19
|
+
Requires-Dist: accelerate>=0.30; extra == "model"
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-cov>=4.1; extra == "dev"
|
|
23
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
24
|
+
Requires-Dist: mypy>=1.7; extra == "dev"
|
|
25
|
+
|
|
26
|
+
# trait-inference — Paper 19 (ATI) Phase 3 Pipeline
|
|
27
|
+
|
|
28
|
+
Python package implementing the **frozen** Paper 19 (Automated Trait
|
|
29
|
+
Inference) Phase 3 training pipeline + baselines + eval harness, per:
|
|
30
|
+
|
|
31
|
+
- Spec: `ai-ecosystem/research/paper-19-trait-inference/phase-1-spec.md`
|
|
32
|
+
- Pre-registration: `ai-ecosystem/research/paper-19-trait-inference/preregistration.md`
|
|
33
|
+
- Brain: `ai-ecosystem/compositions/trait-inference-brain.hsplus`
|
|
34
|
+
- GPU-claim ticket: `task_1777072040695_mrr3`
|
|
35
|
+
|
|
36
|
+
**Status (2026-04-24)**: Phase 1 (CPU pipeline) shipped — dataset
|
|
37
|
+
loader/audit/splits + 3 baselines (keyword + TF-IDF + Brittney-stub) +
|
|
38
|
+
eval metrics with bootstrap CI + CLI runner + Vast.ai launcher.
|
|
39
|
+
**Phase 2 (model module)** — sentence-transformer encoder + constrained-
|
|
40
|
+
decoder LLM, requires `[model]` extra — pending follow-up commit.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Quick start
|
|
45
|
+
|
|
46
|
+
### 1. Install (CPU baselines + eval only)
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
cd packages/trait-inference
|
|
50
|
+
pip install -e .
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 2. Smoke test (synthetic data, end-to-end)
|
|
54
|
+
|
|
55
|
+
Validates the pipeline runs without needing real data or GPU. ~2 min.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
trait-inference smoke --n 200 --bootstrap-b 200
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Should emit a JSON measurement bundle to stdout with `"smoke_test": true,
|
|
62
|
+
"passed": true`. Use this to validate a fresh install before committing
|
|
63
|
+
to a Vast.ai run.
|
|
64
|
+
|
|
65
|
+
### 3. Extract trait label space from HoloScript core
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
trait-inference extract-traits \
|
|
69
|
+
--constants-dir ../core/src/traits/constants/ \
|
|
70
|
+
--output trait_inference/data/trait_label_space.json \
|
|
71
|
+
--verbose
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Reads the 113 TS constant files, extracts string-array exports, writes
|
|
75
|
+
a single JSON consumed by the dataset + model modules.
|
|
76
|
+
|
|
77
|
+
### 4. Audit a real dataset
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
trait-inference dataset audit data/atimark.jsonl --output measurements/audit.json
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Returns exit 0 if the dataset passes spec §1.4 acceptance (≥2k pairs,
|
|
84
|
+
≥300 novel combinations, ≥500 each major source, ≥200 negatives, no
|
|
85
|
+
novelty leak); exit 1 with `issues` list otherwise.
|
|
86
|
+
|
|
87
|
+
### 5. Run baselines
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
trait-inference dataset split data/atimark.jsonl --output-dir splits/ --seed 42
|
|
91
|
+
trait-inference baseline run keyword --train splits/train.jsonl --eval splits/held_out_novel.jsonl --output measurements/keyword.json
|
|
92
|
+
trait-inference baseline run tfidf --train splits/train.jsonl --eval splits/held_out_novel.jsonl --val splits/val.jsonl --tune-threshold --output measurements/tfidf.json
|
|
93
|
+
trait-inference baseline run brittney --train splits/train.jsonl --eval splits/held_out_novel.jsonl --output measurements/brittney.json
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Each emits `f1_macro`, `exact_match`, `bootstrap_ci`, sample predictions.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Vast.ai GPU launch
|
|
101
|
+
|
|
102
|
+
Orchestration script: `scripts/vast-launch-paper-19.ps1` (PowerShell;
|
|
103
|
+
mirrors the existing `ai-ecosystem/scripts/vast-bench-runner.ps1`
|
|
104
|
+
pattern).
|
|
105
|
+
|
|
106
|
+
```powershell
|
|
107
|
+
# Cheapest end-to-end pipeline validation (~$0.30, ~5 min)
|
|
108
|
+
.\scripts\vast-launch-paper-19.ps1 -Phase smoke -Label paper19-smoke
|
|
109
|
+
|
|
110
|
+
# Run all 3 baselines on the real dataset (~$0.30, ~10 min)
|
|
111
|
+
.\scripts\vast-launch-paper-19.ps1 -Phase baseline `
|
|
112
|
+
-DatasetPath data/atimark.jsonl -Label paper19-baselines
|
|
113
|
+
|
|
114
|
+
# Full training run (REQUIRES preregistration.md frozen + Phase 2 model module shipped)
|
|
115
|
+
.\scripts\vast-launch-paper-19.ps1 -Phase train -GpuName RTX_4090 `
|
|
116
|
+
-DatasetPath data/atimark.jsonl -Label paper19-headline-cell-1
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Pre-flight: requires `vastai set api-key` configured (see
|
|
120
|
+
`ai-ecosystem/.env` `VAST_API_KEY`); requires `~/.ssh/id_rsa` with the
|
|
121
|
+
matching public key registered on the Vast.ai account; requires
|
|
122
|
+
≥$0.50 credit for `train`.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Cost estimate (per
|
|
127
|
+
|
|
128
|
+
`ai-ecosystem/research/paper-19-trait-inference/README.md` Phase 2-4 task table + GPU-claim ticket `_mrr3`)
|
|
129
|
+
|
|
130
|
+
| Job | GPU | Hours | Cost |
|
|
131
|
+
| --------------------------------------------- | -------- | ------------------------------: | -----: |
|
|
132
|
+
| Smoke test | RTX 4090 | 0.1 | $0.03 |
|
|
133
|
+
| Baselines (CPU-bound) | RTX 4090 | 0.2 | $0.06 |
|
|
134
|
+
| Single training cell | RTX 4090 | ~6 | ~$1.80 |
|
|
135
|
+
| Full sweep (30 cells × N=5 reseed = 150 runs) | RTX 4090 | ~900 (parallel: 30 GPUs × 30hr) | ~$240 |
|
|
136
|
+
|
|
137
|
+
(A100 estimates are roughly 4-8× higher; A100 supply is also tighter.
|
|
138
|
+
4090 is sufficient for ≤1B-param decoder per spec §3.1.)
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Per-spec deliverable map
|
|
143
|
+
|
|
144
|
+
| Spec section | Module | Status |
|
|
145
|
+
| ------------------------------------ | ------------------------------------------------------------------- | ------------------------------------------------ |
|
|
146
|
+
| §1.1 Sourcing 3-source mix | `dataset.py` Pair + Source | done (loader; data construction is Phase 2 task) |
|
|
147
|
+
| §1.2 Schema | `dataset.py` Pair dataclass | done |
|
|
148
|
+
| §1.3 Splits (train/val/indist/novel) | `dataset.py` make_splits | done |
|
|
149
|
+
| §1.4 Audit protocol | `dataset.py` audit + AuditReport | done |
|
|
150
|
+
| §2.1 Keyword baseline | `baselines.py` KeywordBaseline | done |
|
|
151
|
+
| §2.2 TF-IDF + LogReg baseline | `baselines.py` TfidfLogregBaseline | done |
|
|
152
|
+
| §2.3 Brittney few-shot baseline | `baselines.py` BrittneyFewShotBaseline | stub (real impl needs Brittney API integration) |
|
|
153
|
+
| §3.1 Constrained-decoder model | `model/` (Phase 2 commit) | pending |
|
|
154
|
+
| §3.2 Conditioning fields | `model/` (Phase 2 commit) | pending |
|
|
155
|
+
| §3.3 Hyperparameter sweep | `model/sweep.py` (Phase 2 commit) | pending |
|
|
156
|
+
| §4.1 Metric definitions | `metrics.py` f1_macro, f1_micro, exact_match_rate, bootstrap_ci | done |
|
|
157
|
+
| §4.2 Statistical protocol | `metrics.py` bootstrap_ci, evaluate_headline | done |
|
|
158
|
+
| §4.3 Ablation matrix | `eval/ablations.py` (Phase 2 commit) | pending |
|
|
159
|
+
| §4.4 Required user study | (separate UX-research task) | pending |
|
|
160
|
+
| §4.5 Pre-registration freeze | `ai-ecosystem/research/paper-19-trait-inference/preregistration.md` | FROZEN (do not edit) |
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Anti-pattern guards (binding — inherited from
|
|
165
|
+
|
|
166
|
+
`compositions/trait-inference-brain.hsplus`)
|
|
167
|
+
|
|
168
|
+
- **No train-set evaluation.** Headline metric on novel-combination split only.
|
|
169
|
+
- **No easy-split-only F1.** Reports include both indist (sanity) and novel (headline).
|
|
170
|
+
- **No single-source dataset.** Audit rejects datasets <500 from any of {existing, brittney, community}.
|
|
171
|
+
- **No optional user study.** §4.4 is required not optional (per F.031).
|
|
172
|
+
- **No after-the-fact threshold-shopping.** preregistration.md is frozen before any Phase 3 board task is filed.
|
|
173
|
+
- **No qualitative-only claims.** ML venue requires numbers; pipeline emits structured measurements.
|
|
174
|
+
- **No validity gap as "scoped contribution"** — constrained-decoding architecture (Phase 2 module) bakes ≥90% validity into the decoder, not into a post-filter.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Known limitations / future work
|
|
179
|
+
|
|
180
|
+
- Brittney few-shot baseline is a stub returning empty predictions; real impl needs HoloScript MCP integration (separate task).
|
|
181
|
+
- Constrained-decoder model module (`model/`) is the Phase 2 deliverable — not in this commit.
|
|
182
|
+
- Training loop + ablation matrix runner pending Phase 2.
|
|
183
|
+
- User study (Phase 4 §4.4) is a separate UX-research deliverable.
|
|
184
|
+
- The PowerShell Vast.ai launcher targets Windows; a bash equivalent for macOS/Linux is a follow-up.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Provenance
|
|
189
|
+
|
|
190
|
+
- Authored by `trait-inference-brain` (`compositions/trait-inference-brain.hsplus`).
|
|
191
|
+
- GPU-claim ticket: `task_1777072040695_mrr3` (live on team_1775935947314_f0noxi board).
|
|
192
|
+
- Capability-build provenance commit: `fc294af` (lean-theorist-brain — sibling).
|
|
193
|
+
- F.031 pre-emptions baked into spec; constrained decoding ships in Phase 2 model module.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
trait_inference/__init__.py,sha256=xDTQIAVanZUfWnPGIR75AVMt5wyFIkQKM_nUltM39UE,860
|
|
2
|
+
trait_inference/baselines.py,sha256=5T9gGDd-ZWSvS9dSJenPIeIDN4gGYBXuRKs7-x8SxvU,9430
|
|
3
|
+
trait_inference/cli.py,sha256=qOqA8UKjWFhuuBhoOlxsxatVFgboWVDmLgD3wW_w0EQ,20011
|
|
4
|
+
trait_inference/dataset.py,sha256=yeWf-wRnIX8gicy2Ej_XHwVhXLc-f-cfpd14zYWoePA,12644
|
|
5
|
+
trait_inference/metrics.py,sha256=azwMBnbevZXGV51GIczbCbVWkf9YiOGeg1xoZV0W19U,9934
|
|
6
|
+
trait_inference/eval/__init__.py,sha256=vb3NIkjYFdoeLsozAudUskKTzXL2Gf1ydpb2xebTyUE,659
|
|
7
|
+
trait_inference/eval/ablations.py,sha256=tB7ktuHT6q1cJGvAuo9MsP4HllvwiX6t69mIAES4TcQ,13960
|
|
8
|
+
trait_inference/model/__init__.py,sha256=2NMRfCqfec3prig_URrkuvfADnNu_mtVBBUynBMKcfA,1310
|
|
9
|
+
trait_inference/model/decoder.py,sha256=phpasQIa4Y3ebws3dNWMif4hp94BPADl-R0_lsaIBcM,7840
|
|
10
|
+
trait_inference/model/sweep.py,sha256=OmUA34J3AZaJ8iXUYmXcMx3ye2cLxay3kN7SAfcv6VQ,9016
|
|
11
|
+
trait_inference/model/trainer.py,sha256=Nz9anThl_Uwk2ZOfZZqL41pzl0t_6l_SNpkImyhnLcc,9164
|
|
12
|
+
holoscript_trait_inference-0.1.0.dist-info/METADATA,sha256=dsOQ1ZEL6hj3E6yek57WFXck4Ea50ERtt1xfQkEtabA,10062
|
|
13
|
+
holoscript_trait_inference-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
14
|
+
holoscript_trait_inference-0.1.0.dist-info/entry_points.txt,sha256=IO3AzHJLf9aNRzlM8PObHGJGX61AA2MZU-hYvQZCoVk,61
|
|
15
|
+
holoscript_trait_inference-0.1.0.dist-info/top_level.txt,sha256=qwYoGcHApqn612UU2QXZSfYFdIe0uEgydsZ8LV7gY3M,16
|
|
16
|
+
holoscript_trait_inference-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
trait_inference
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""trait-inference — Paper 19 (ATI) training pipeline + baselines + eval.
|
|
2
|
+
|
|
3
|
+
See ai-ecosystem/research/paper-19-trait-inference/{phase-1-spec,
|
|
4
|
+
preregistration}.md for the frozen spec.
|
|
5
|
+
|
|
6
|
+
Modules:
|
|
7
|
+
dataset — schema, JSONL loader, splits, audit (CPU-only)
|
|
8
|
+
baselines — keyword + TF-IDF + few-shot baselines (CPU-only)
|
|
9
|
+
metrics — F1 macro, exact-match, validity rate, bootstrap CI
|
|
10
|
+
cli — argparse runner; entrypoint `trait-inference`
|
|
11
|
+
|
|
12
|
+
Optional (install [model] extra):
|
|
13
|
+
model — sentence-transformer encoder + constrained-decoder LLM
|
|
14
|
+
trainer — training loop (PyTorch + transformers)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
__version__ = "0.1.0"
|
|
18
|
+
|
|
19
|
+
# Re-export commonly used types so callers can `from trait_inference import Pair`.
|
|
20
|
+
from trait_inference.dataset import ( # noqa: F401
|
|
21
|
+
Pair,
|
|
22
|
+
Source,
|
|
23
|
+
Split,
|
|
24
|
+
audit,
|
|
25
|
+
load_jsonl,
|
|
26
|
+
write_jsonl,
|
|
27
|
+
)
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Baselines — Paper 19 ATI.
|
|
2
|
+
|
|
3
|
+
Per `phase-1-spec.md` §2 — ≥3 baselines required:
|
|
4
|
+
1. Keyword match (trivial; CPU; ~30-55% F1 expected on novel split)
|
|
5
|
+
2. TF-IDF + multi-label LogReg (classical; CPU; ~50-65% F1 expected)
|
|
6
|
+
3. Brittney few-shot (strong; needs Brittney API; ~60-75% F1 expected)
|
|
7
|
+
|
|
8
|
+
Per `trait-inference-brain` priority 3 + `phase-1-spec.md` §2:
|
|
9
|
+
> Build baselines BEFORE contribution model. If a baseline already
|
|
10
|
+
> hits the gate, the contribution is null.
|
|
11
|
+
|
|
12
|
+
Each baseline implements:
|
|
13
|
+
fit(train_pairs) -> None # may be no-op for keyword
|
|
14
|
+
predict(description) -> list[str]
|
|
15
|
+
predict_batch(descs) -> list[list[str]]
|
|
16
|
+
|
|
17
|
+
This module is CPU-only. Brittney baseline imports lazily so missing
|
|
18
|
+
API key doesn't break keyword/tfidf usage.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
from collections.abc import Iterable
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from typing import Protocol
|
|
26
|
+
|
|
27
|
+
import numpy as np
|
|
28
|
+
from sklearn.feature_extraction.text import TfidfVectorizer
|
|
29
|
+
from sklearn.linear_model import LogisticRegression
|
|
30
|
+
from sklearn.multiclass import OneVsRestClassifier
|
|
31
|
+
from sklearn.preprocessing import MultiLabelBinarizer
|
|
32
|
+
|
|
33
|
+
from trait_inference.dataset import Pair
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ----------------------------------------------------------------------------
|
|
37
|
+
# Baseline protocol
|
|
38
|
+
# ----------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
class Baseline(Protocol):
|
|
41
|
+
"""Common interface for all baselines."""
|
|
42
|
+
name: str
|
|
43
|
+
|
|
44
|
+
def fit(self, train_pairs: list[Pair]) -> None: ...
|
|
45
|
+
def predict(self, description: str) -> list[str]: ...
|
|
46
|
+
def predict_batch(self, descriptions: list[str]) -> list[list[str]]: ...
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ----------------------------------------------------------------------------
|
|
50
|
+
# Baseline 1: Keyword match (trivial)
|
|
51
|
+
# ----------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class KeywordBaseline:
|
|
55
|
+
"""Trivial baseline: predict trait T if T's name (or known synonym)
|
|
56
|
+
appears in the description.
|
|
57
|
+
|
|
58
|
+
Per `phase-1-spec.md` §2.1 — expected F1 ≈ 30-55% on novel split.
|
|
59
|
+
"""
|
|
60
|
+
name: str = "keyword"
|
|
61
|
+
label_space: tuple[str, ...] = ()
|
|
62
|
+
synonyms: dict[str, list[str]] = field(default_factory=dict)
|
|
63
|
+
_patterns: dict[str, re.Pattern[str]] = field(default_factory=dict, init=False)
|
|
64
|
+
|
|
65
|
+
def __post_init__(self) -> None:
|
|
66
|
+
self._compile()
|
|
67
|
+
|
|
68
|
+
def _compile(self) -> None:
|
|
69
|
+
"""Build per-trait word-boundary regex from name + synonyms."""
|
|
70
|
+
self._patterns = {}
|
|
71
|
+
for trait in self.label_space:
|
|
72
|
+
# Strip @-prefix and _-separators for matching.
|
|
73
|
+
normalized = trait.lstrip("@").replace("_", " ")
|
|
74
|
+
tokens = [normalized] + self.synonyms.get(trait, [])
|
|
75
|
+
# Word-boundary regex; case-insensitive.
|
|
76
|
+
pattern = r"\b(" + "|".join(re.escape(t) for t in tokens) + r")\b"
|
|
77
|
+
self._patterns[trait] = re.compile(pattern, re.IGNORECASE)
|
|
78
|
+
|
|
79
|
+
def fit(self, train_pairs: list[Pair]) -> None:
|
|
80
|
+
# Could optionally derive synonyms from train co-occurrence here.
|
|
81
|
+
# Phase 1: synonyms come from constructor (caller-supplied).
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
def predict(self, description: str) -> list[str]:
|
|
85
|
+
hits = [t for t, p in self._patterns.items() if p.search(description)]
|
|
86
|
+
return sorted(hits)
|
|
87
|
+
|
|
88
|
+
def predict_batch(self, descriptions: list[str]) -> list[list[str]]:
|
|
89
|
+
return [self.predict(d) for d in descriptions]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ----------------------------------------------------------------------------
|
|
93
|
+
# Baseline 2: TF-IDF + multi-label Logistic Regression (classical)
|
|
94
|
+
# ----------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class TfidfLogregBaseline:
|
|
98
|
+
"""TF-IDF vectorizer + one-vs-rest LogReg per trait.
|
|
99
|
+
|
|
100
|
+
Per `phase-1-spec.md` §2.2 — expected F1 ≈ 50-65% on novel split.
|
|
101
|
+
Captures bag-of-words associations beyond lexical match but cannot
|
|
102
|
+
generalize across novel combinations because training only sees
|
|
103
|
+
existing combinations.
|
|
104
|
+
"""
|
|
105
|
+
name: str = "tfidf_logreg"
|
|
106
|
+
threshold: float = 0.5 # decision threshold for trait inclusion
|
|
107
|
+
max_features: int = 10000
|
|
108
|
+
ngram_range: tuple[int, int] = (1, 2)
|
|
109
|
+
_vectorizer: TfidfVectorizer | None = None
|
|
110
|
+
_classifier: OneVsRestClassifier | None = None
|
|
111
|
+
_binarizer: MultiLabelBinarizer | None = None
|
|
112
|
+
_label_space: list[str] = field(default_factory=list)
|
|
113
|
+
|
|
114
|
+
def fit(self, train_pairs: list[Pair]) -> None:
|
|
115
|
+
if not train_pairs:
|
|
116
|
+
raise ValueError("cannot fit on empty training set")
|
|
117
|
+
|
|
118
|
+
descriptions = [p.description for p in train_pairs]
|
|
119
|
+
trait_sets = [list(p.trait_set) for p in train_pairs]
|
|
120
|
+
|
|
121
|
+
# Establish label space from training data
|
|
122
|
+
self._binarizer = MultiLabelBinarizer()
|
|
123
|
+
Y = self._binarizer.fit_transform(trait_sets)
|
|
124
|
+
self._label_space = list(self._binarizer.classes_)
|
|
125
|
+
|
|
126
|
+
self._vectorizer = TfidfVectorizer(
|
|
127
|
+
max_features=self.max_features,
|
|
128
|
+
ngram_range=self.ngram_range,
|
|
129
|
+
sublinear_tf=True,
|
|
130
|
+
stop_words="english",
|
|
131
|
+
)
|
|
132
|
+
X = self._vectorizer.fit_transform(descriptions)
|
|
133
|
+
|
|
134
|
+
# Multi-label via one-vs-rest. LogReg's predict_proba enables
|
|
135
|
+
# threshold tuning on the validation split.
|
|
136
|
+
self._classifier = OneVsRestClassifier(
|
|
137
|
+
LogisticRegression(max_iter=500, C=1.0, solver="liblinear"),
|
|
138
|
+
n_jobs=-1,
|
|
139
|
+
)
|
|
140
|
+
self._classifier.fit(X, Y)
|
|
141
|
+
|
|
142
|
+
def _ensure_fit(self) -> None:
|
|
143
|
+
if self._vectorizer is None or self._classifier is None or self._binarizer is None:
|
|
144
|
+
raise RuntimeError("call fit() before predict()")
|
|
145
|
+
|
|
146
|
+
def predict(self, description: str) -> list[str]:
|
|
147
|
+
return self.predict_batch([description])[0]
|
|
148
|
+
|
|
149
|
+
def predict_batch(self, descriptions: list[str]) -> list[list[str]]:
|
|
150
|
+
self._ensure_fit()
|
|
151
|
+
assert self._vectorizer is not None and self._classifier is not None
|
|
152
|
+
assert self._binarizer is not None
|
|
153
|
+
X = self._vectorizer.transform(descriptions)
|
|
154
|
+
# predict_proba returns shape (n_samples, n_labels)
|
|
155
|
+
proba = self._classifier.predict_proba(X)
|
|
156
|
+
out: list[list[str]] = []
|
|
157
|
+
for row in proba:
|
|
158
|
+
chosen = [
|
|
159
|
+
self._label_space[i]
|
|
160
|
+
for i, p in enumerate(row)
|
|
161
|
+
if p >= self.threshold
|
|
162
|
+
]
|
|
163
|
+
out.append(sorted(chosen))
|
|
164
|
+
return out
|
|
165
|
+
|
|
166
|
+
def tune_threshold(
|
|
167
|
+
self,
|
|
168
|
+
val_pairs: list[Pair],
|
|
169
|
+
candidates: Iterable[float] = (0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8),
|
|
170
|
+
) -> float:
|
|
171
|
+
"""Find the threshold that maximizes F1 macro on the validation
|
|
172
|
+
split. Updates self.threshold and returns it."""
|
|
173
|
+
from trait_inference.metrics import f1_macro
|
|
174
|
+
|
|
175
|
+
descriptions = [p.description for p in val_pairs]
|
|
176
|
+
gold = [list(p.trait_set) for p in val_pairs]
|
|
177
|
+
|
|
178
|
+
best_thr, best_f1 = self.threshold, -1.0
|
|
179
|
+
for thr in candidates:
|
|
180
|
+
self.threshold = thr
|
|
181
|
+
preds = self.predict_batch(descriptions)
|
|
182
|
+
score = f1_macro(gold, preds, label_space=self._label_space)
|
|
183
|
+
if score > best_f1:
|
|
184
|
+
best_thr, best_f1 = thr, score
|
|
185
|
+
self.threshold = best_thr
|
|
186
|
+
return best_thr
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ----------------------------------------------------------------------------
|
|
190
|
+
# Baseline 3: Brittney few-shot (strong; lazy import — needs API access)
|
|
191
|
+
# ----------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
@dataclass
|
|
194
|
+
class BrittneyFewShotBaseline:
|
|
195
|
+
"""In-context-learn from K=8 stratified examples + Brittney prompt.
|
|
196
|
+
|
|
197
|
+
Per `phase-1-spec.md` §2.3 — expected F1 ≈ 60-75% on novel split.
|
|
198
|
+
Strongest baseline — represents "what you get for free from the
|
|
199
|
+
existing system."
|
|
200
|
+
|
|
201
|
+
Phase 3 implementation deferred — depends on Brittney API surface
|
|
202
|
+
decision (HoloScript MCP `brittney_*` tools). This stub captures the
|
|
203
|
+
interface so the eval harness can be tested end-to-end with mock
|
|
204
|
+
behavior; the real implementation lands when training Phase 3 starts.
|
|
205
|
+
"""
|
|
206
|
+
name: str = "brittney_fewshot"
|
|
207
|
+
k_shots: int = 8
|
|
208
|
+
_examples: list[Pair] = field(default_factory=list)
|
|
209
|
+
|
|
210
|
+
def fit(self, train_pairs: list[Pair]) -> None:
|
|
211
|
+
# Stratified sampling: pick K examples that maximize trait diversity
|
|
212
|
+
# in the prompt context. Greedy: each next example adds the most
|
|
213
|
+
# not-yet-seen traits.
|
|
214
|
+
if not train_pairs:
|
|
215
|
+
self._examples = []
|
|
216
|
+
return
|
|
217
|
+
seen: set[str] = set()
|
|
218
|
+
chosen: list[Pair] = []
|
|
219
|
+
remaining = list(train_pairs)
|
|
220
|
+
while len(chosen) < self.k_shots and remaining:
|
|
221
|
+
best_idx, best_new = 0, -1
|
|
222
|
+
for i, p in enumerate(remaining):
|
|
223
|
+
new_traits = len(set(p.trait_set) - seen)
|
|
224
|
+
if new_traits > best_new:
|
|
225
|
+
best_idx, best_new = i, new_traits
|
|
226
|
+
picked = remaining.pop(best_idx)
|
|
227
|
+
chosen.append(picked)
|
|
228
|
+
seen.update(picked.trait_set)
|
|
229
|
+
self._examples = chosen
|
|
230
|
+
|
|
231
|
+
def predict(self, description: str) -> list[str]:
|
|
232
|
+
# Phase 1 stub: return empty (will be replaced by Brittney API call
|
|
233
|
+
# in Phase 3). Empty prediction is the most honest non-call default
|
|
234
|
+
# — the eval harness will record this baseline as needing impl.
|
|
235
|
+
return []
|
|
236
|
+
|
|
237
|
+
def predict_batch(self, descriptions: list[str]) -> list[list[str]]:
|
|
238
|
+
return [self.predict(d) for d in descriptions]
|