rudeparis commited on
Commit
fbce9f8
·
verified ·
1 Parent(s): 4dd405a

Upload build_canonical_v2.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. build_canonical_v2.py +159 -0
build_canonical_v2.py ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Build canonical-v2 corpus by adding V9 hard-negatives + augmentation.
2
+
3
+ Inputs:
4
+ - canonical-v1 base: poc/llm-finetune/training/data_canonical_v1/{train,valid,test}.jsonl
5
+ - V9 failures from poc/llm-finetune/training/eval_dump_v9_local.json
6
+ - Each failure becomes a corrected gold example, repeated 3x for emphasis
7
+ - Plus near-duplicate text mutations (substitute callsign, swap runway numbers)
8
+
9
+ Output: poc/llm-finetune/training/data_canonical_v2/{train,valid,test}.jsonl
10
+
11
+ Run after V9 eval completes:
12
+ uv run python poc/llm-finetune/training/build_canonical_v2.py \\
13
+ --v9-dump poc/llm-finetune/training/eval_dump_v9_local.json \\
14
+ --emphasis 3
15
+ """
16
+ from __future__ import annotations
17
+ import argparse
18
+ import json
19
+ import random
20
+ import re
21
+ from collections import defaultdict
22
+ from pathlib import Path
23
+
24
+ ROOT = Path(__file__).resolve().parents[3]
25
+ import sys
26
+ sys.path.insert(0, str(ROOT / "poc/llm-finetune/training"))
27
+ from build_canonical_v1 import SYSTEM_PROMPT, to_chat, INTENTS_50, INTENTS_51
28
+
29
+ CALLSIGN_VARIATIONS = [
30
+ "November One Seven X-ray X-ray",
31
+ "Cessna Three Four Papa",
32
+ "Piper Five Six Bravo",
33
+ "Skyhawk Seven Eight Charlie",
34
+ "Bonanza One Two Delta",
35
+ ]
36
+
37
+ RUNWAY_VARIATIONS = [
38
+ ("two seven", "three one"), ("three one", "two seven"),
39
+ ("zero seven", "one nine"), ("one nine", "zero seven"),
40
+ ("two four right", "two four left"), ("zero nine", "two seven"),
41
+ ]
42
+
43
+
44
+ def mutate_text(text: str, rng: random.Random) -> str:
45
+ """Light text mutation: callsign substitution + runway swap."""
46
+ out = text
47
+ # callsign substitution
48
+ for cs in CALLSIGN_VARIATIONS:
49
+ if cs.lower() in out.lower():
50
+ new_cs = rng.choice([c for c in CALLSIGN_VARIATIONS if c != cs])
51
+ out = re.sub(re.escape(cs), new_cs, out, flags=re.IGNORECASE)
52
+ break
53
+ # runway swap
54
+ for r_from, r_to in RUNWAY_VARIATIONS:
55
+ if f"runway {r_from}" in out.lower():
56
+ out = re.sub(rf"runway {r_from}", f"runway {r_to}", out, flags=re.IGNORECASE)
57
+ break
58
+ return out
59
+
60
+
61
+ def load_v9_failures(dump_path: Path) -> list[dict]:
62
+ """Extract failures from V9 dump as corrected gold examples."""
63
+ if not dump_path.exists():
64
+ return []
65
+ dump = json.loads(dump_path.read_text())
66
+ failures = []
67
+ for r in dump["rows"]:
68
+ gold = r.get("gold") or {}
69
+ pred = r.get("parsed") or {}
70
+ gold_segs = gold.get("segments", [])
71
+ pred_segs = pred.get("segments") or []
72
+ # Failure if intent or count differs
73
+ intent_ordered_match = (
74
+ len(pred_segs) == len(gold_segs)
75
+ and [s.get("intent") for s in pred_segs] == [s.get("intent") for s in gold_segs]
76
+ )
77
+ if intent_ordered_match:
78
+ continue
79
+ failures.append({
80
+ "text": r["text"],
81
+ "segments": gold_segs,
82
+ "compound": r.get("compound", False),
83
+ })
84
+ return failures
85
+
86
+
87
+ def main():
88
+ p = argparse.ArgumentParser(description=__doc__)
89
+ p.add_argument("--v9-dump", default="poc/llm-finetune/training/eval_dump_v9_local.json")
90
+ p.add_argument("--emphasis", type=int, default=3, help="how many copies of each failure to add")
91
+ p.add_argument("--mutate", type=int, default=2, help="how many mutated near-duplicates per failure")
92
+ p.add_argument("--seed", type=int, default=42)
93
+ args = p.parse_args()
94
+
95
+ rng = random.Random(args.seed)
96
+ out = ROOT / "poc/llm-finetune/training/data_canonical_v2"
97
+ out.mkdir(parents=True, exist_ok=True)
98
+
99
+ rows = []
100
+ counts = defaultdict(int)
101
+
102
+ # 1. Inherit canonical-v1
103
+ v1_dir = ROOT / "poc/llm-finetune/training/data_canonical_v1"
104
+ for split in ("train", "valid", "test"):
105
+ path = v1_dir / f"{split}.jsonl"
106
+ if not path.exists():
107
+ continue
108
+ for line in path.read_text().splitlines():
109
+ if not line.strip():
110
+ continue
111
+ row = json.loads(line)
112
+ payload = json.loads(row["messages"][2]["content"])
113
+ text = row["messages"][1]["content"]
114
+ rows.append({"text": text, "segments": payload["segments"], "_source": f"v1-{split}"})
115
+ counts[f"v1-{split}"] += 1
116
+
117
+ # 2. V9 hard negatives (corrected gold), repeated for emphasis
118
+ failures = load_v9_failures(ROOT / args.v9_dump)
119
+ print(f"V9 failures: {len(failures)}")
120
+ for fail in failures:
121
+ for _ in range(args.emphasis):
122
+ rows.append({"text": fail["text"], "segments": fail["segments"], "_source": "hardneg"})
123
+ counts["hardneg"] += 1
124
+ for _ in range(args.mutate):
125
+ mutated_text = mutate_text(fail["text"], rng)
126
+ if mutated_text != fail["text"]:
127
+ rows.append({"text": mutated_text, "segments": fail["segments"], "_source": "hardneg-mutated"})
128
+ counts["hardneg-mutated"] += 1
129
+
130
+ print(f"\nraw counts: {dict(counts)}")
131
+ print(f"total: {len(rows)}")
132
+
133
+ # Dedupe
134
+ seen = set()
135
+ dedup = []
136
+ for r in rows:
137
+ if r["text"] in seen:
138
+ continue
139
+ seen.add(r["text"])
140
+ dedup.append(r)
141
+ print(f"deduped: {len(dedup)}")
142
+
143
+ rng.shuffle(dedup)
144
+ n = len(dedup)
145
+ n_valid = max(20, int(n * 0.05))
146
+ n_test = max(20, int(n * 0.05))
147
+ n_train = n - n_valid - n_test
148
+ splits = {"train": dedup[:n_train], "valid": dedup[n_train:n_train+n_valid], "test": dedup[n_train+n_valid:]}
149
+
150
+ for name, items in splits.items():
151
+ path = out / f"{name}.jsonl"
152
+ with path.open("w") as f:
153
+ for r in items:
154
+ f.write(json.dumps(to_chat(r["text"], r["segments"]), ensure_ascii=False) + "\n")
155
+ print(f"{name}: {len(items)} -> {path}")
156
+
157
+
158
+ if __name__ == "__main__":
159
+ main()