AAJerry commited on
Commit
891030d
·
verified ·
1 Parent(s): ef1ee90

Add trainer/validate_dataset.py

Browse files
Files changed (1) hide show
  1. trainer/validate_dataset.py +131 -0
trainer/validate_dataset.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from collections import Counter, defaultdict
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from .common import DATASET_ROOT, atomic_json, ensure_dirs, log_event, update_status
10
+
11
+
12
+ ALLOWED_ACTIONS = {
13
+ "CONTINUE_MONITORING",
14
+ "RECHECK_SENSOR",
15
+ "HOLD_ASCENT",
16
+ "ACTIVATE_EMERGENCY",
17
+ "REQUEST_HUMAN_REVIEW",
18
+ }
19
+ SENSITIVE_PATTERNS = [
20
+ re.compile(r"\b\d{3}-\d{2}-\d{4}\b"),
21
+ re.compile(r"\b(?:\d[ -]*?){13,16}\b"),
22
+ re.compile(r"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b", re.I),
23
+ ]
24
+
25
+
26
+ def rows(path: Path) -> list[dict[str, Any]]:
27
+ result = []
28
+ with path.open("r", encoding="utf-8") as handle:
29
+ for line_no, line in enumerate(handle, 1):
30
+ try:
31
+ result.append(json.loads(line))
32
+ except json.JSONDecodeError as exc:
33
+ raise ValueError(f"{path.name}:{line_no}: invalid JSON: {exc}") from exc
34
+ return result
35
+
36
+
37
+ def validate() -> dict[str, Any]:
38
+ errors: list[str] = []
39
+ warnings: list[str] = []
40
+ ids: set[str] = set()
41
+ content_hashes: set[str] = set()
42
+ group_splits: defaultdict[str, set[str]] = defaultdict(set)
43
+ counts: Counter[str] = Counter()
44
+ categories: Counter[str] = Counter()
45
+ actions: Counter[str] = Counter()
46
+
47
+ for split in ("train", "validation", "test"):
48
+ path = DATASET_ROOT / f"{split}.jsonl"
49
+ if not path.exists():
50
+ errors.append(f"Missing {path.name}")
51
+ continue
52
+ for row in rows(path):
53
+ counts[split] += 1
54
+ if row.get("split") != split:
55
+ errors.append(f"{row.get('id')}: split field does not match {path.name}")
56
+ if row.get("id") in ids:
57
+ errors.append(f"Duplicate id {row.get('id')}")
58
+ ids.add(str(row.get("id")))
59
+ group_splits[str(row.get("group"))].add(split)
60
+ categories[str(row.get("category"))] += 1
61
+
62
+ messages = row.get("messages")
63
+ if not isinstance(messages, list) or len(messages) < 3:
64
+ errors.append(f"{row.get('id')}: messages must contain system, user, assistant")
65
+ continue
66
+ roles = [message.get("role") for message in messages]
67
+ if roles[0] != "system" or roles[-1] != "assistant" or roles[-2] != "user":
68
+ errors.append(f"{row.get('id')}: invalid role sequence {roles}")
69
+ if any(not isinstance(message.get("content"), str) or not message["content"].strip() for message in messages):
70
+ errors.append(f"{row.get('id')}: empty or non-string message")
71
+
72
+ body = "\n".join(message["content"] for message in messages)
73
+ digest = __import__("hashlib").sha256(body.encode("utf-8")).hexdigest()
74
+ if digest in content_hashes:
75
+ errors.append(f"{row.get('id')}: exact content duplicate")
76
+ content_hashes.add(digest)
77
+ if any(pattern.search(body) for pattern in SENSITIVE_PATTERNS):
78
+ errors.append(f"{row.get('id')}: possible sensitive identifier")
79
+
80
+ try:
81
+ output = json.loads(messages[-1]["content"])
82
+ except json.JSONDecodeError:
83
+ errors.append(f"{row.get('id')}: assistant output is not JSON")
84
+ continue
85
+ if set(output) != {"speech", "next_action", "question", "uncertainties"}:
86
+ errors.append(f"{row.get('id')}: assistant schema mismatch")
87
+ action = output.get("next_action")
88
+ actions[str(action)] += 1
89
+ if action not in ALLOWED_ACTIONS:
90
+ errors.append(f"{row.get('id')}: unsupported action {action}")
91
+ if not isinstance(output.get("uncertainties"), list):
92
+ errors.append(f"{row.get('id')}: uncertainties must be a list")
93
+ if row.get("source") == "med-safety-bench" and split == "train":
94
+ errors.append("MedSafetyBench must not enter the SFT train split")
95
+
96
+ leaks = {group: sorted(splits) for group, splits in group_splits.items() if len(splits) > 1}
97
+ if leaks:
98
+ errors.append(f"Group leakage across splits: {list(leaks.items())[:10]}")
99
+ if counts["train"] < 1000:
100
+ errors.append(f"Train split is too small: {counts['train']}")
101
+ if counts["validation"] < 100 or counts["test"] < 100:
102
+ errors.append(f"Validation/test split too small: {dict(counts)}")
103
+ if actions["ACTIVATE_EMERGENCY"] < 100:
104
+ warnings.append("Emergency examples are below 100")
105
+
106
+ report = {
107
+ "ok": not errors,
108
+ "errors": errors,
109
+ "warnings": warnings,
110
+ "counts": dict(counts),
111
+ "categories": dict(categories),
112
+ "actions": dict(actions),
113
+ "unique_groups": len(group_splits),
114
+ }
115
+ return report
116
+
117
+
118
+ def main() -> None:
119
+ ensure_dirs()
120
+ update_status("validate_dataset", "Validating schema, splits, duplication, and safety contract")
121
+ report = validate()
122
+ atomic_json(DATASET_ROOT / "validation_report.json", report)
123
+ log_event("dataset_validated", **report)
124
+ if not report["ok"]:
125
+ raise RuntimeError("Dataset validation failed: " + "; ".join(report["errors"][:8]))
126
+ update_status("dataset_valid", "Dataset validation passed", validation=report)
127
+
128
+
129
+ if __name__ == "__main__":
130
+ main()
131
+