151 lines
5.5 KiB
Python
151 lines
5.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Classify GSM8K problems by missing substrate category.
|
|
|
|
Tranche 1 — broad base-layer foundations.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
from typing import Sequence
|
|
|
|
from language_packs.scalar_equivalence import list_unsupported_surfaces
|
|
from language_packs.unit_dimensions import classify_dimension
|
|
from language_packs.loader import lookup_container
|
|
from generate.process_frames import all_frames
|
|
|
|
|
|
def classify_missing_substrate(problem_text: str) -> tuple[str, ...]:
|
|
"""Return sorted tuple of missing substrate labels for a problem.
|
|
|
|
Inspects problem text using substrate facades to identify gaps.
|
|
"""
|
|
labels = set()
|
|
text_lower = problem_text.lower()
|
|
|
|
# 1. missing_scalar_equivalence
|
|
# If the text has unsupported surfaces like ".5" or "1 / 2"
|
|
for unsup in list_unsupported_surfaces():
|
|
if unsup in text_lower:
|
|
labels.add("missing_scalar_equivalence")
|
|
|
|
# Look for digit-slash-digit with spaces
|
|
if re.search(r"\b\d+\s+/\s+\d+\b", problem_text) or re.search(r"\b\.\d+\b", problem_text):
|
|
labels.add("missing_scalar_equivalence")
|
|
|
|
# 2. missing_unit_dimension
|
|
# Extract words following digits (e.g. "5 widgets")
|
|
matches = re.findall(r"\b\d+(?:\.\d+)?\s+([a-zA-Z]+)\b", problem_text)
|
|
for word in matches:
|
|
word_lower = word.lower()
|
|
if word_lower in {
|
|
"more", "less", "times", "percent", "percentage", "of", "and", "or",
|
|
"the", "a", "an", "in", "to", "for", "with", "at", "by", "from",
|
|
}:
|
|
continue
|
|
if classify_dimension(word_lower) is None and lookup_container(word_lower) is None:
|
|
labels.add("missing_unit_dimension")
|
|
|
|
# 3. missing_process_frame
|
|
has_triggers = False
|
|
for frame in all_frames():
|
|
for trigger in frame.trigger_surfaces:
|
|
if f" {trigger} " in f" {text_lower} " or text_lower.startswith(trigger) or text_lower.endswith(trigger):
|
|
has_triggers = True
|
|
break
|
|
if has_triggers:
|
|
if "give" in text_lower or "gave" in text_lower or "gives" in text_lower:
|
|
labels.add("missing_process_frame")
|
|
|
|
# 4. missing_part_whole_frame
|
|
if any(w in text_lower for w in ["split", "divide", "share", "partition", "rest of", "portion"]):
|
|
labels.add("missing_part_whole_frame")
|
|
|
|
# 5. missing_container_frame
|
|
if any(w in text_lower for w in ["box", "pack", "bag", "fill", "contain", "crate", "carton"]):
|
|
labels.add("missing_container_frame")
|
|
|
|
# 6. missing_temporal_frame
|
|
if any(w in text_lower for w in ["hour", "minute", "day", "week", "month", "year", "work", "earn", "salary", "wage"]):
|
|
labels.add("missing_temporal_frame")
|
|
|
|
# 7. missing_route_frame
|
|
if any(w in text_lower for w in ["drive", "walk", "run", "travel", "miles per hour", "mph", "trip", "journey"]):
|
|
labels.add("missing_route_frame")
|
|
|
|
# 8. missing_question_target
|
|
if "?" not in problem_text and "how many" not in text_lower and "how much" not in text_lower:
|
|
labels.add("missing_question_target")
|
|
|
|
# 9. blocked_ambiguity_hazard
|
|
for hazard_surf in [
|
|
"half", "quarter", "third", "percent", "percentage points", "times",
|
|
"more than", "less than", "of", "per", "each", "some", "remaining",
|
|
"left", "total", "altogether"
|
|
]:
|
|
if f" {hazard_surf} " in f" {text_lower} " or text_lower.startswith(hazard_surf) or text_lower.endswith(hazard_surf):
|
|
labels.add("blocked_ambiguity_hazard")
|
|
|
|
# 10. blocked_provenance_gap
|
|
if "leap year" in text_lower or "calendar" in text_lower or "world fact" in text_lower:
|
|
labels.add("blocked_provenance_gap")
|
|
|
|
return tuple(sorted(labels))
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description="Classify GSM8K problems by missing substrate.")
|
|
parser.add_argument("--cases", type=str, help="Path to JSONL cases file")
|
|
parser.add_argument("--out", type=str, help="Path to write classified output JSONL")
|
|
parser.add_argument("--limit", type=int, help="Limit number of cases to process")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if not args.cases:
|
|
print("No cases path provided.")
|
|
return
|
|
|
|
cases_path = Path(args.cases)
|
|
if not cases_path.exists():
|
|
print(f"Cases file not found at {args.cases}")
|
|
return
|
|
|
|
out_lines = []
|
|
count = 0
|
|
with cases_path.open("r", encoding="utf-8") as f:
|
|
for line in f:
|
|
if not line.strip():
|
|
continue
|
|
case = json.loads(line)
|
|
problem_text = case.get("question") or case.get("problem_text") or ""
|
|
if not problem_text:
|
|
continue
|
|
|
|
labels = classify_missing_substrate(problem_text)
|
|
case_id = case.get("case_id") or f"case_{count}"
|
|
out_lines.append({
|
|
"case_id": case_id,
|
|
"problem_text": problem_text,
|
|
"missing_substrate_labels": labels
|
|
})
|
|
|
|
count += 1
|
|
if args.limit and count >= args.limit:
|
|
break
|
|
|
|
if args.out:
|
|
out_path = Path(args.out)
|
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
with out_path.open("w", encoding="utf-8") as f:
|
|
for item in out_lines:
|
|
f.write(json.dumps(item) + "\n")
|
|
print(f"Wrote {len(out_lines)} classified cases to {args.out}")
|
|
else:
|
|
for item in out_lines:
|
|
print(json.dumps(item))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|