File size: 23,961 Bytes
c83c617 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 | """Review, freeze, package and import the reusable standard IME benchmark."""
import argparse
import collections
import copy
import json
import re
import shutil
import sys
import uuid
import zipfile
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "src"))
from vimeml.benchmarks.ajimee import convert_items
from vimeml.benchmarks.expanded_ime import CONVERTER
from vimeml.benchmarks.standard_ime import FORMAT, read, load_standard_export, reviewed_and_frozen
from vimeml.training.data import write_json
DRAFT = ROOT / "artifacts/benchmarks/ime-standard-ja-v1-draft"
PREPARED_ROLES = ("development", "blind", "regression")
def fresh(path):
if path.exists():
raise ValueError(f"Preserve existing output: {path}; choose a fresh path.")
path.mkdir(parents=True)
def track_files(path, items):
path.mkdir(parents=True, exist_ok=True)
cli, mapping, stats = convert_items(items)
for name, value in (("evaluation_items.json", items), ("ajimee-input.json", cli), ("case-map.json", mapping), ("stats.json", stats)):
write_json(path / name, value)
def notice(output):
text = """# Source attribution and use scope
Local research benchmark. WRIME conversion spans and readings are derived
from WRIME v2: https://github.com/ids-cv/wrime (CC BY-NC-ND 4.0).
This preparation does not grant permission to redistribute an adapted WRIME
benchmark. Restricted data and review packages remain outside Git. Public code
and methodology may be shared separately; data publication requires permission.
JMultiWOZ: https://github.com/nu-dialogue/jmultiwoz (CC BY-SA 4.0).
Authors: Atsumoto Ohashi, Ryu Hirai, Shinya Iizuka, Ryuichiro Higashinaka (2024).
WRIME: cite Suzuki et al., LREC 2022 and Kajiwara et al., NAACL 2021.
Per-case IDs/rows and pinned revisions are preserved in provenance/manifest.
Legacy regression: previously exposed FineWeb2 Edu Japanese development cases,
not independent or blind. Existing per-case web URLs/provenance remain intact.
FineWeb2 packaging: ODC-By 1.0 and upstream Common Crawl terms; underlying text
retains its original rights. Original frozen references are unchanged.
Source: https://huggingface.co/datasets/hotchpotch/fineweb-2-edu-japanese
Candidate collection performs no LM scoring. Human review pages show no model
outputs or candidate ordering. AI expert review and dictionary checks are
separately recorded from human native gold. No credentials or trained weights are bundled.
"""
(output / "NOTICE.md").write_text(text, encoding="utf-8")
for source in ("wrime", "jmultiwoz"):
shutil.copyfile(ROOT / f"datasets/ime-standard-ja-v1/{source}/LICENSE", output / f"LICENSE-{source}.txt")
def make_review(args):
if args.refresh:
if read(args.output / "manifest.json")["version"] != read(args.prepared / "manifest.json")["version"]:
raise ValueError("Review directory belongs to a different draft.")
for role in ("development", "blind"):
existing = [json.loads(line) for line in (args.output / f"{role}-review.jsonl").read_text(encoding="utf-8").splitlines()]
if any(r["decision"] != "pending" for r in existing):
raise ValueError("Preserve edited reviews; choose a new output.")
else:
fresh(args.output)
notice(args.output)
manifest = read(args.prepared / "manifest.json")
write_json(args.output / "manifest.json", manifest)
template = (ROOT / "templates/benchmarks/standard-ime-review.html").read_text(encoding="utf-8")
from vimeml.benchmarks.readings import DualReading
reader = DualReading()
pairs = [("分か", "わか"), ("可愛い", "かわいい"), ("可愛く", "かわいく"),
("下さい", "ください"), ("出来", "でき"), ("良い", "よい"), ("良く", "よく"),
("ご飯", "ごはん"), ("眼鏡", "めがね", "メガネ"), ("子供", "子ども"),
("頑張", "がんば"), ("事", "こと"), ("為", "ため"), ("無い", "ない"),
("無く", "なく"), ("買い取り", "買取", "買取り"), ("かもしれ", "かも知れ"),
("我々", "われわれ"), ("言え", "いえ"), ("有り", "あり"), ("無理", "むり")]
for role in ("development", "blind"):
reviews = [json.loads(line) for line in (args.prepared / role / "review.jsonl").read_text(encoding="utf-8").splitlines()]
for row in reviews:
forms = {row["target"]}
for group in pairs:
for form in sorted(forms)[:16]:
for current in group:
if current in form:
forms.update(form.replace(current, alt) for alt in group)
forms = set(sorted(forms)[:32]) | {row["target"]}
suggestions = []
for form in sorted(forms - {row["target"]})[:32]:
check = reader.analyse(form)
if (not check["unknown"] and check["unidic"] == row["proposed_reading"] and
check["sudachi"] == row["proposed_reading"]):
suggestions.append(form)
row["spelling_suggestions"] = suggestions[:15]
row["suggestion_policy"] = "Pre-score orthographic proposals, dictionary-matched; not accepted answers until reviewer confirms"
blob = json.dumps({"version": manifest["version"], "role": role, "cases": reviews}, ensure_ascii=False).replace("<", "\\u003c")
(args.output / f"{role}-review.html").write_text(template.replace("__PAYLOAD__", blob), encoding="utf-8")
(args.output / f"{role}-review.jsonl").write_text("".join(json.dumps(r, ensure_ascii=False)+"\n" for r in reviews), encoding="utf-8")
(args.output / "README.md").write_text(
"# 标签审核\n\n打开development-review.html或blind-review.html即可离线审核。"
"核对给定左文下的读音、转换边界和全部合理表记;信息不足则标为exclude并写原因。"
"不要根据任何模型结果改答案。审核者和母语审核声明必须如实填写,AI初审保留false。"
"下载的两个JSONL交给freeze入口;未审核项目不会进入正式发布。"
"原文存在右侧文字时,只用于追溯,不把未提供给模型的右文作为唯一消歧依据。\n",
encoding="utf-8")
with zipfile.ZipFile(args.output.with_suffix(".zip"), "w", zipfile.ZIP_DEFLATED) as package:
for path in sorted(args.output.iterdir()):
package.write(path, args.output.name + "/" + path.name)
def freeze(args):
if args.output.exists():
raise ValueError("Preserve prior releases.")
manifest = read(args.prepared / "manifest.json")
if reviewed_and_frozen(manifest) and (args.version == manifest["version"] or not args.change_note.strip()):
raise ValueError("Corrections to a reviewed release require a new version and --change-note.")
tracks, quarantine, decisions = {}, [], {}
for role in ("development", "blind"):
original = read(args.prepared / role / "evaluation_items.json")
reviews = [json.loads(line) for line in (args.reviews / f"{role}-review.jsonl").read_text(encoding="utf-8").splitlines() if line.strip()]
by_id = {r["id"]: r for r in reviews}
if len(by_id) != len(reviews) or set(by_id) != {r["index"] for r in original}:
raise ValueError("Review IDs do not match the prepared draft.")
selected = []
for row in original:
review = by_id[row["index"]]
if (review["left_context"] != row["context_text"] or
review["target"] != row["expected_output"][0] or review["source"] != row["provenance"]["source"]):
raise ValueError("Review context/source changed; prepare a new version.")
if review["decision"] == "exclude" and review["notes"].strip() and review["reviewer"].strip():
quarantine.append({"case": row, "review": review})
continue
if review["decision"] != "accept" or not review["reviewer"].strip() or not review["notes"].strip():
raise ValueError(f"Pending review not complete: {row['index']}")
native = args.review_mode == "human-native"
if native and review["native_japanese_review"] is not True:
raise ValueError(f"Native review not complete: {row['index']}")
if not native and (review.get("review_type") != "ai_expert" or review["native_japanese_review"] is not False):
raise ValueError("AI expert review must be identified explicitly, without a native-human claim.")
answers = review["accepted_answers"]
if not isinstance(answers, list) or not answers or any(not isinstance(a, str) or not a.strip() for a in answers) or len(set(answers)) != len(answers):
raise ValueError("Accepted forms must be a nonempty unique string list.")
if not isinstance(review["proposed_reading"], str) or not re.fullmatch(r"[ァ-ヺー。、!?「」『』()・:,,.!?]{4,64}", review["proposed_reading"]):
raise ValueError("Reviewed input must follow the declared 4-64-character orthographic katakana protocol.")
frozen = copy.deepcopy(row)
frozen["input"] = review["proposed_reading"]
frozen["expected_output"] = answers
frozen["review"] = {"status": "native_reviewed" if native else "ai_expert_reviewed",
"human_native_review": native, "not_formal_gold": not native,
"reviewer": review["reviewer"], "notes": review["notes"], "acceptable_forms_exhaustive": False}
selected.append(frozen)
if {r["provenance"]["source"] for r in selected} != {"wrime", "jmultiwoz"}:
raise ValueError("Both sources must remain in each external split.")
tracks[role] = selected
decisions[role] = reviews
tracks["regression"] = read(args.prepared / "regression/evaluation_items.json")
# Edited readings must not create exact input/context leakage across splits.
dev_keys = {(r['context_text'], r['input']) for r in tracks['development']}
if dev_keys & {(r['context_text'], r['input']) for r in tracks['blind']}:
raise ValueError("Reviewed development/blind input/context pairs overlap.")
fresh(args.output)
for role, rows in tracks.items():
track_files(args.output / role, rows)
manifest.update(parent_release_id=manifest.get("release_id"), change_note=args.change_note,
frozen_utc=datetime.now(timezone.utc).isoformat(),
version=args.version, release_id=uuid.uuid4().hex, status="labels_frozen_awaiting_candidates",
labels_formal_gold=args.review_mode == "human-native",
native_review_completed=args.review_mode == "human-native",
labels_frozen=True, review_completed=True,
label_quality="human_native_reviewed" if args.review_mode == "human-native" else "ai_expert_reviewed",
review_policy="Frozen pre-score AI expert references; not human native gold" if args.review_mode == "ai-expert" else "Frozen native-human-reviewed references",
legacy_labels_formal_gold=False, splits={r: len(v) for r,v in tracks.items()},
label_change_policy="Immutable published release; corrections require a new release ID and an explicit changelog")
write_json(args.output / "manifest.json", manifest)
write_json(args.output / "review-decisions.json", decisions)
write_json(args.output / "quarantine.json", quarantine)
notice(args.output)
def package(args):
manifest = read(args.prepared / "manifest.json")
fresh(args.output)
for role in ("development", "blind"):
dest = args.output / role
dest.mkdir()
for filename in ("evaluation_items.json", "ajimee-input.json", "case-map.json", "stats.json"):
shutil.copyfile(args.prepared / role / filename, dest / filename)
write_json(args.output / "manifest.json", manifest)
notice(args.output)
shutil.copyfile(ROOT / "scripts/benchmarks/export_azookey_v21.sh", args.output / "export_azookey.sh")
shutil.copyfile(ROOT / "scripts/benchmarks/run_standard_ime_mac.sh", args.output / "run_mac.sh")
handoff_note = (ROOT / "templates/benchmarks/standard-ime-mac.md").read_text(encoding="utf-8")
for key, value in {"VERSION": manifest["version"], "DEVELOPMENT": manifest["splits"]["development"],
"BLIND": manifest["splits"]["blind"], "TOTAL": manifest["splits"]["development"] + manifest["splits"]["blind"],
"LABEL_QUALITY": manifest.get("label_quality", "unreviewed_draft"),
"FORMAL_GOLD": manifest["labels_formal_gold"]}.items():
handoff_note = handoff_note.replace(f"__{key}__", str(value))
(args.output / "README_CN.md").write_text(handoff_note, encoding="utf-8")
(args.output / "README.md").write_text(
f"# Standard IME candidate collection\n\n{manifest['splits']['development']} development / {manifest['splits']['blind']} blind.\n\n"
"```bash\nbash export_azookey.sh /path/to/AzooKeyKanaKanjiConverter\nzip -r azookey-results.zip azookey-results\n```\n\n"
f"Converter: {CONVERTER}; existing n-best20 dictionary/flags unchanged. "
"Legacy FineWeb candidates are reused locally, so this package exports only new sources. "
"Exporting a blind candidate pool is permitted; no LM scoring is performed. "
"Reviewed label-only changes can reuse candidates when query/context/IDs are identical; original outputs remain preserved. "
"Reading, case exclusion or context changes require a fresh package/export. "
f"Label quality: {manifest.get('label_quality', 'unreviewed_draft')}; formal human gold: {manifest['labels_formal_gold']}.\n", encoding="utf-8")
with zipfile.ZipFile(args.output.with_suffix(".zip"), "w", zipfile.ZIP_DEFLATED) as archive:
for path in sorted(args.output.rglob("*")):
if path.is_file():
archive.write(path, str(path.relative_to(args.output.parent)))
def import_candidates(args):
manifest = read(args.prepared / "manifest.json")
if args.output.exists():
raise ValueError("Preserve prior imported versions.")
payload = {}
files = ("ajimee-input.json", "case-map.json", "azookey-candidates.json", "converter-version.txt",
"dictionary-versions.txt", "swift-version.txt", "export-flags.txt", "completed.txt", "export.log")
with zipfile.ZipFile(args.archive) as archive:
names = archive.namelist()
for role in ("development", "blind"):
payload[role] = {}
for filename in files:
suffix = f"azookey-results/{role}/{filename}"
matches = [n for n in names if n == suffix or n.endswith("/"+suffix)]
if len(matches) != 1:
raise ValueError(f"Expected exactly one {suffix}.")
payload[role][filename] = archive.read(matches[0])
label_changed = False
for filename, answer_key in (("ajimee-input.json", "answer"), ("case-map.json", "answers")):
returned = json.loads(payload[role][filename])
expected = read(args.prepared / role / filename)
if (len(returned) != len(expected) or any(
{k:v for k,v in a.items() if k != answer_key} != {k:v for k,v in b.items() if k != answer_key}
for a,b in zip(returned, expected))):
raise ValueError("Query/context/IDs changed; create a fresh candidate export.")
label_changed |= returned != expected
if label_changed:
if not reviewed_and_frozen(manifest):
raise ValueError("Label-only rebasing requires a reviewed frozen release.")
original_inputs = json.loads(payload[role]["ajimee-input.json"])
inputs = read(args.prepared / role / "ajimee-input.json")
raw = json.loads(payload[role]["azookey-candidates.json"])
if len(raw["items"]) != len(inputs):
raise ValueError("Candidate count differs from the handoff.")
for item, old_input, current in zip(raw["items"], original_inputs, inputs):
if (item["query"] != old_input["query"] or item["left_context"] != old_input["left_context"] or
item["answers"] != old_input["answer"]):
raise ValueError("Original candidate output differs from the original CLI input.")
item["answers"] = current["answer"]
item["max_rank"] = next((i for i,c in enumerate(item["outputs"]) if c["text"] in current["answer"]), -1)
raw["stat"]["ranks"] = dict(collections.Counter(str(r["max_rank"]) for r in raw["items"]))
for filename in ("ajimee-input.json", "case-map.json", "azookey-candidates.json"):
payload[role][filename.replace(".json", "-original.json")] = payload[role][filename]
payload[role]["azookey-candidates.json"] = (json.dumps(raw,ensure_ascii=False,indent=2)+"\n").encode("utf-8")
for filename in ("ajimee-input.json", "case-map.json"):
payload[role][filename] = (args.prepared / role / filename).read_bytes()
fresh(args.output)
manifest.setdefault("release_id", "draft-" + uuid.uuid4().hex)
for role in PREPARED_ROLES:
rows = read(args.prepared / role / "evaluation_items.json")
dest = args.output / role
track_files(dest, rows)
exported = dest / "ajimee-results"
exported.mkdir()
if role != "regression":
for filename, content in payload[role].items():
(exported / filename).write_bytes(content)
else:
old = ROOT / "artifacts/benchmarks/ime-expanded-v21-candidates-v1/development"
old_items = read(old / "evaluation_items.json")
positions = {row["index"]: i for i,row in enumerate(old_items)}
raw = read(old / "ajimee-results/azookey-candidates.json")
raw["items"] = [raw["items"][positions[row["index"]]] for row in rows]
raw["stat"]["query_count"] = len(rows)
raw["stat"]["ranks"] = dict(collections.Counter(str(item["max_rank"]) for item in raw["items"]))
write_json(exported / "azookey-candidates.json", raw)
for filename in files:
if filename not in ("ajimee-input.json", "case-map.json", "azookey-candidates.json"):
shutil.copyfile(old / "ajimee-results" / filename, exported / filename)
for filename in ("ajimee-input.json", "case-map.json"):
shutil.copyfile(dest / filename, exported / filename)
stats = read(dest / "stats.json")
imported = {**manifest, "format": FORMAT, "status": "complete", "split": role, "stats": stats,
"labels_formal_gold": bool(manifest["labels_formal_gold"] and role != "regression"),
"label_quality": "legacy_historical_draft" if role == "regression" else manifest.get("label_quality", "unreviewed_draft"),
"labels_frozen": bool(manifest.get("labels_frozen") and role != "regression"),
"review_completed": bool(manifest.get("review_completed") and role != "regression"),
"native_review_completed": bool(manifest.get("native_review_completed") and role != "regression"),
"source_corpus_split": "independent_source" if role != "regression" else "historical_development",
"candidate_provenance": "New Mac export" if role != "regression" else "Subset of frozen original 2000-case export; aggregate subset ranks recomputed, original files unchanged"}
imported["reviewed_labels_rebased"] = "azookey-candidates-original.json" in payload.get(role, {})
write_json(dest / "manifest.json", imported)
load_standard_export(dest)
manifest.update(status="candidates_imported", blind_lm_scored=False)
write_json(args.output / "manifest.json", manifest)
notice(args.output)
def plan(args):
manifest = read(args.benchmark / "blind/manifest.json")
if not reviewed_and_frozen(manifest):
raise ValueError("Freeze reviewed labels before committing a blind plan.")
consumed = (args.benchmark / "blind-consumption.json").exists()
if args.output.exists() or consumed and not args.reference_test:
raise ValueError("Preserve the existing plan/consumed blind release.")
if args.reference_test and not consumed:
raise ValueError("Reference-test mode is for a previously opened blind release.")
import torch
models = []
for path in args.checkpoint:
saved = torch.load(path, map_location="cpu", weights_only=True)
models.append({"checkpoint": str(path.resolve()), "bytes": path.stat().st_size,
"mtime_ns": path.stat().st_mtime_ns, "step": saved["step"],
"training_config": saved["config"], "signatures": saved["signatures"]})
del saved
write_json(args.output, {"plan_id": uuid.uuid4().hex, "benchmark_release_id": manifest["release_id"],
"created_utc": datetime.now(timezone.utc).isoformat(), "models": models,
"primary_metric": "FP32 suffix logP sum: per-source top1, independent micro and equal-source macro",
"selection_completed_before_blind": not args.reference_test,
"fresh_blind_evidence": not args.reference_test,
"role": "reference_test_after_consumption" if args.reference_test else "blind",
"label_quality": manifest.get("label_quality", "human_native_reviewed"),
"checkpoint_identity_policy": "Path/bytes/step/config/signatures; no repeated weight-file SHA256"})
def main():
parser = argparse.ArgumentParser(description=__doc__)
subs = parser.add_subparsers(dest="command", required=True)
for name in ("review-ui", "freeze", "package", "import"):
sub = subs.add_parser(name)
sub.add_argument("--prepared", type=Path, default=DRAFT)
sub.add_argument("--output", type=Path, required=True)
if name == "freeze":
sub.add_argument("--reviews", type=Path, required=True)
sub.add_argument("--version", default="ime-standard-ja-v1")
sub.add_argument("--change-note", default="Initial native review of the unscored draft")
sub.add_argument("--review-mode", choices=("human-native", "ai-expert"), default="human-native")
if name == "import":
sub.add_argument("--archive", type=Path, required=True)
if name == "review-ui":
sub.add_argument("--refresh", action="store_true", help="Refresh generated templates only while all on-disk reviews remain pending")
sub = subs.add_parser("plan")
sub.add_argument("--benchmark", type=Path, required=True)
sub.add_argument("--checkpoint", type=Path, nargs="+", required=True)
sub.add_argument("--output", type=Path, required=True)
sub.add_argument("--reference-test", action="store_true", help="Reuse an opened release as a fixed reference test, not as fresh blind evidence")
args = parser.parse_args()
{"review-ui": make_review, "freeze": freeze, "package": package, "import": import_candidates, "plan": plan}[args.command](args)
print(f"Completed {args.command}: {args.output}")
if __name__ == "__main__":
main()
|