rudeparis commited on
Commit
214d233
·
verified ·
1 Parent(s): 50440f4

Upload mine_hard_negs.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. mine_hard_negs.py +82 -0
mine_hard_negs.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Mine hard negatives from a V9+ eval dump for next-iteration training.
2
+
3
+ For each failure (intent_em or slots_em wrong on canonical 253):
4
+ - Output the row with corrected gold (from canonical scenario)
5
+ - Tag with failure type: wrong_intent | missing_slot | extra_slot | wrong_segment_count
6
+ - Optionally synthesize a near-duplicate by light text mutation
7
+
8
+ Output: poc/llm-finetune/training/data_canonical_v2/hard_negatives.jsonl
9
+ """
10
+ from __future__ import annotations
11
+ import argparse
12
+ import json
13
+ import re
14
+ from collections import Counter
15
+ from pathlib import Path
16
+
17
+ ROOT = Path(__file__).resolve().parents[3]
18
+
19
+
20
+ def diff_segments(pred_segs: list, gold_segs: list) -> dict:
21
+ out = {"seg_count_match": len(pred_segs) == len(gold_segs)}
22
+ out["pred_n"] = len(pred_segs)
23
+ out["gold_n"] = len(gold_segs)
24
+ pred_intents = [s.get("intent") for s in pred_segs]
25
+ gold_intents = [s.get("intent") for s in gold_segs]
26
+ out["intent_match_ordered"] = pred_intents == gold_intents
27
+ out["intent_overlap"] = list(Counter(gold_intents) & Counter(pred_intents))
28
+ return out
29
+
30
+
31
+ def main():
32
+ p = argparse.ArgumentParser(description=__doc__)
33
+ p.add_argument("dump")
34
+ p.add_argument("--out", default="poc/llm-finetune/training/data_canonical_v2/hard_negatives.jsonl")
35
+ args = p.parse_args()
36
+
37
+ dump = json.loads(Path(args.dump).read_text())
38
+ rows = dump["rows"]
39
+
40
+ failures = []
41
+ type_counter = Counter()
42
+ for r in rows:
43
+ gold = r.get("gold") or {}
44
+ pred = r.get("parsed") or {}
45
+ gold_segs = gold.get("segments", [])
46
+ pred_segs = pred.get("segments") or []
47
+ diff = diff_segments(pred_segs, gold_segs)
48
+
49
+ if diff["intent_match_ordered"]:
50
+ continue # not a failure on intent
51
+ # tag failure
52
+ if not diff["seg_count_match"]:
53
+ failure_type = f"seg_count_{diff['pred_n']}_vs_{diff['gold_n']}"
54
+ elif not diff["intent_overlap"]:
55
+ failure_type = "intent_disjoint"
56
+ else:
57
+ failure_type = "intent_order_or_partial_overlap"
58
+ type_counter[failure_type] += 1
59
+ failures.append({
60
+ "text": r["text"],
61
+ "compound": r.get("compound", False),
62
+ "gold_segments": gold_segs,
63
+ "pred_segments_for_audit": pred_segs,
64
+ "failure_type": failure_type,
65
+ })
66
+
67
+ print(f"loaded {len(rows)} rows; failures: {len(failures)}")
68
+ print("\nfailure type distribution:")
69
+ for ft, n in type_counter.most_common():
70
+ print(f" {ft:35s} {n}")
71
+
72
+ out_path = Path(args.out)
73
+ out_path.parent.mkdir(parents=True, exist_ok=True)
74
+ with out_path.open("w") as f:
75
+ for fail in failures:
76
+ f.write(json.dumps(fail, ensure_ascii=False) + "\n")
77
+ print(f"\nwrote {len(failures)} hard negatives -> {out_path}")
78
+ print(f"\nnext: feed back into build_canonical_v1.py via mine_v9_failures() helper, retrain V10")
79
+
80
+
81
+ if __name__ == "__main__":
82
+ main()