Laya
Safetensors
English
biology
dna
protein
laya-bio-models / scripts /audit_laya_tasks.py
marisming's picture
Publish frozen Laya-Bio assets, batch 4/5
d35dd87 verified
Raw History Blame Contribute Delete
17.8 kB
#!/usr/bin/env python3
"""Audit Laya's two local pilot tasks without emitting biological sequences.
Standard library only. Raw JSONL files are read only. The proposed pilot split
is based on train/val only; the test split is audited but never used to decide
which train/val examples to keep. Exact sequence separation is not homology
separation. Outputs contain aggregate counts and SHA-256 hashes, not sequences.
"""
from __future__ import annotations
import argparse
from collections import Counter, defaultdict
from datetime import datetime, timezone
import hashlib
import itertools
import json
from pathlib import Path
TASKS = {
"lg_promoter_detection": {
"task_id": "promoter_detection", "modality": "dna",
"canonical": "ACGT", "allowed": "ACGTRYSWKMBDHVN-",
"choices": ["Non-promoter", "promoter"],
},
"lg_fold_class": {
"task_id": "fold_class", "modality": "protein",
"canonical": "ACDEFGHIKLMNPQRSTVWY", "allowed": "ACDEFGHIKLMNPQRSTVWYBXZJUO*-",
"choices": ["All Alpha", "All Beta", "Alpha and Beta", "Alpha plus Beta",
"Multi-domain Proteins", "Mixed Structures", "Small Proteins and Peptides"],
},
}
SPLITS = ("train", "val", "test")
def sha256(text):
return hashlib.sha256(text.encode("utf-8")).hexdigest()
def sorted_counts(values):
return dict(sorted(values.items()))
def length_summary(values):
values = sorted(values)
if not values:
return {"count": 0}
return {
"count": len(values), "min": values[0], "max": values[-1],
"median": values[(len(values) - 1) // 2],
"p95": values[int((len(values) - 1) * .95)],
"p99": values[int((len(values) - 1) * .99)],
}
def sequence_from_user(record):
users = [m.get("content") for m in record.get("messages", [])
if isinstance(m, dict) and m.get("role") == "user"]
if len(users) != 1 or not isinstance(users[0], str):
return None, None
lines = [line.strip() for line in users[0].splitlines() if line.strip()]
if len(lines) < 2:
return None, None
sequence = lines[-1].upper()
# This is deliberately stricter than searching for the longest alphabetic
# substring: there may be no silent extraction of just part of a sequence.
if not sequence or not all(c.isascii() and (c.isalpha() or c in "*-") for c in sequence):
return None, None
return sequence, sha256("\n".join(lines[:-1]))
def audit_task(root, name, expected):
path = root / "data/03_sft_biopaws2/jsonl" / f"{name}.jsonl"
file_hash = hashlib.sha256()
counts, errors, splits, alphabets, instructions, choice_variants = (Counter() for _ in range(6))
labels_by_split = defaultdict(Counter)
lengths_by_split = defaultdict(list)
groups = defaultdict(list)
row_hashes, identifiers = set(), set()
with path.open("rb") as handle:
for line_number, raw in enumerate(handle, 1):
file_hash.update(raw)
counts["physical_lines"] += 1
if not raw.strip():
counts["blank_lines"] += 1
continue
try:
row = json.loads(raw)
except (json.JSONDecodeError, UnicodeDecodeError):
errors["json_parse_errors"] += 1
continue
if not isinstance(row, dict):
errors["non_object_rows"] += 1
continue
counts["rows"] += 1
row_errors = []
split = str(row.get("split"))
splits[split] += 1
if split not in SPLITS:
row_errors.append("unexpected_split")
if row.get("task_id") != expected["task_id"]:
row_errors.append("unexpected_task_id")
if expected["modality"] not in row.get("modality", []):
row_errors.append("missing_sequence_modality")
identifier = row.get("id")
if not isinstance(identifier, str) or not identifier:
row_errors.append("invalid_identifier")
else:
counts["duplicate_id_extra_rows"] += identifier in identifiers
identifiers.add(identifier)
row_hash = sha256(json.dumps(row, sort_keys=True, separators=(",", ":")))
counts["duplicate_full_extra_rows"] += row_hash in row_hashes
row_hashes.add(row_hash)
choices, gold = row.get("choices"), row.get("answer_short")
choice_variants[json.dumps(choices, ensure_ascii=False)] += 1
if choices != expected["choices"]:
row_errors.append("choices_not_expected_order_and_values")
if not isinstance(choices, list) or len(choices) != len(set(map(str, choices))):
row_errors.append("choices_invalid_or_duplicated")
if not isinstance(choices, list) or gold not in choices:
row_errors.append("gold_not_in_choices")
labels_by_split[split][str(gold)] += 1
assistants = [m.get("content") for m in row.get("messages", [])
if isinstance(m, dict) and m.get("role") == "assistant"]
if len(assistants) != 1 or assistants[0] != gold:
row_errors.append("assistant_not_exactly_gold")
sequence, instruction_hash = sequence_from_user(row)
if sequence is None:
row_errors.append("user_last_line_extraction_failure")
else:
counts["extracted_sequences"] += 1
counts[f"extracted_sequences_{split}"] += 1
instructions[instruction_hash] += 1
alphabets.update(sequence)
lengths_by_split[split].append(len(sequence))
if not set(sequence) <= set(expected["allowed"]):
row_errors.append("unexpected_alphabet")
if not set(sequence) <= set(expected["canonical"]):
counts["rows_with_noncanonical_characters"] += 1
groups[sha256(sequence)].append({
"split": split, "gold": str(gold), "line": line_number,
"valid": not row_errors,
})
errors.update(row_errors)
counts["valid_rows_before_duplicate_conflict_filter"] += not row_errors
seq_sets = {split: {h for h, rows in groups.items() if any(r["split"] == split for r in rows)}
for split in SPLITS}
exact_duplicates = {}
for split in SPLITS:
repeated = {h: sum(r["split"] == split for r in rows) for h, rows in groups.items()
if sum(r["split"] == split for r in rows) > 1}
exact_duplicates[split] = {
"unique_sequences": len(seq_sets[split]), "repeated_sequence_hashes": len(repeated),
"extra_rows": sum(n - 1 for n in repeated.values()),
"rows_in_repeated_groups": sum(repeated.values()),
}
cross_split = {}
for left, right in itertools.combinations(SPLITS, 2):
overlap = seq_sets[left] & seq_sets[right]
cross_split[f"{left}__{right}"] = {
"shared_sequence_hashes": len(overlap),
"left_rows": sum(sum(r["split"] == left for r in groups[h]) for h in overlap),
"right_rows": sum(sum(r["split"] == right for r in groups[h]) for h in overlap),
"conflicting_label_hashes": sum(
len({r["gold"] for r in groups[h] if r["split"] in (left, right)}) > 1
for h in overlap),
}
all_conflicts = {h for h, rows in groups.items() if len({r["gold"] for r in rows}) > 1}
trainval_conflicts = {h for h, rows in groups.items()
if len({r["gold"] for r in rows if r["split"] in ("train", "val")}) > 1}
conflicts_by_split = {}
for split in SPLITS:
hashes = {h for h, rows in groups.items()
if len({r["gold"] for r in rows if r["split"] == split}) > 1}
conflicts_by_split[split] = {
"conflicting_sequence_hashes": len(hashes),
"rows_in_conflicting_groups": sum(sum(r["split"] == split for r in groups[h]) for h in hashes),
}
# Deterministic pilot view only. All train/val conflicts are removed, then
# first valid occurrence per sequence is retained in each split; train wins
# any remaining train/val exact overlap. No test labels influence this view.
retained, pilot_details = {}, {}
for split in ("train", "val"):
candidates = sorted(((r["line"], h, r) for h, rows in groups.items()
for r in rows if r["split"] == split), key=lambda x: x[0])
selected, excluded, selected_labels = {}, Counter(), Counter()
for _, h, row in candidates:
if not row["valid"]:
excluded["invalid_rows"] += 1
elif h in trainval_conflicts:
excluded["train_val_label_conflict_rows"] += 1
elif split == "val" and h in retained["train"]:
excluded["exact_overlap_with_clean_train_rows"] += 1
elif h in selected:
excluded["same_split_exact_duplicate_rows"] += 1
else:
selected[h] = row
selected_labels[row["gold"]] += 1
retained[split] = selected
pilot_details[split] = {
"eligible_rows": len(selected), "labels": sorted_counts(selected_labels),
"excluded_rows": sorted_counts(excluded),
"retained_sequence_hash_list_sha256": sha256("\n".join(sorted(selected))),
}
return {
"relative_path": str(path.relative_to(root)), "file_sha256": file_hash.hexdigest(),
"expected_task": expected["task_id"], "expected_modality": expected["modality"],
"expected_choices": expected["choices"], "counts": sorted_counts(counts),
"errors": sorted_counts(errors), "split_rows": sorted_counts(splits),
"labels_by_split": {s: sorted_counts(c) for s, c in labels_by_split.items()},
"choice_variants": sorted_counts(choice_variants),
"instruction_template_sha256_counts": sorted_counts(instructions),
"character_counts": sorted_counts(alphabets),
"unexpected_characters": sorted(set(alphabets) - set(expected["allowed"])),
"lengths": length_summary([v for values in lengths_by_split.values() for v in values]),
"lengths_by_split": {s: length_summary(v) for s, v in lengths_by_split.items()},
"exact_duplicates": exact_duplicates, "cross_split_exact_overlap": cross_split,
"label_conflicts": {
"all_splits_conflicting_sequence_hashes": len(all_conflicts),
"all_splits_conflicting_rows": sum(len(groups[h]) for h in all_conflicts),
"within_split": conflicts_by_split,
"train_val_conflicting_sequence_hashes": len(trainval_conflicts),
"train_val_conflicting_hashes": sorted(trainval_conflicts),
},
"pilot": {
"decision": "eligible_for_small_supervised_train_dev_pilot" if
all(pilot_details[s]["eligible_rows"] > 0 for s in ("train", "val"))
and not errors else "inspect_errors_before_pilot",
"policy": ["Normalize user last non-empty line with strip + uppercase; hash with SHA-256.",
"Reject invalid rows and remove every sequence with conflicting labels in train+val.",
"Retain the first valid source-line occurrence per sequence in each split.",
"Remove validation sequences also present in the retained training split.",
"Test labels and test membership do not decide train/val exclusions.",
"Do not select examples by current model loss or validation performance."],
"splits": pilot_details,
"post_cleanup_train_val_shared_hashes": len(set(retained["train"]) & set(retained["val"])),
"test_used_for_pilot_filter": False,
"homology_independence_verified": False,
},
}
def markdown_report(report):
lines = ["# Laya 两任务数据审计", "", f"审计时间:{report['generated_at_utc']}", "",
"只读扫描两个原始 JSONL;不修改数据、不安装依赖、不使用 GPU。结果文件只保存汇总与 SHA-256 哈希。",
"", "## 输入与标签", "",
"| 任务 | 总行数 | train / val / test | 末行序列覆盖 | 序列长度 min / median / p95 / max | 数据格式错误 |",
"|---|---:|---|---:|---|---:|"]
for name, task in report["tasks"].items():
c, le = task["counts"], task["lengths"]
lines.append(f"| {name} | {c['rows']:,} | " + " / ".join(f"{task['split_rows'].get(s, 0):,}" for s in SPLITS)
+ f" | {c.get('extracted_sequences', 0):,}/{c['rows']:,} | "
+ " / ".join(str(le[k]) for k in ("min", "median", "p95", "max"))
+ f" | {sum(task['errors'].values())} |")
lines += ["", "长度单位为 DNA 碱基或蛋白氨基酸字符,并非 tokenizer token。"
"检查 choices 顺序和值、gold 属于 choices、assistant 与 gold 完全一致、task_id、modality、split 和字母表。", "",
"## 重复、跨划分重叠与冲突", "",
"| 任务 | train / val / test 内重复额外行 | train–val / train–test / val–test 共享序列 | 全部划分同序列多标签 | train+val 同序列多标签 |",
"|---|---|---|---:|---:|"]
for name, task in report["tasks"].items():
dup, overlap, conflicts = task["exact_duplicates"], task["cross_split_exact_overlap"], task["label_conflicts"]
lines.append(f"| {name} | " + " / ".join(str(dup[s]["extra_rows"]) for s in SPLITS)
+ " | " + " / ".join(str(overlap[p]["shared_sequence_hashes"]) for p in ("train__val", "train__test", "val__test"))
+ f" | {conflicts['all_splits_conflicting_sequence_hashes']} | {conflicts['train_val_conflicting_sequence_hashes']} |")
lines += ["", "共享序列和标签冲突均按规范化序列 SHA-256 计数;同序列标签冲突不通过多数投票修复。", "",
"## 少量监督 pilot 的建议视图", "",
"先从 train+val 中完全删除标签冲突序列,再在各 split 内保留同序列第一条有效原始行,最后从 val 删除与清洗后 train 完全重复的序列。"
"这个规则只使用 train/val 信息;test 不参与 pilot 过滤、调参或结果选择。原始数据不改动。", "",
"| 任务 | 清洗后 train | 清洗后 val | train 删除行 | val 删除行 | 清洗后 train–val 共享序列 |",
"|---|---:|---:|---:|---:|---:|"]
for name, task in report["tasks"].items():
pilot = task["pilot"]
tr, va = pilot["splits"]["train"], pilot["splits"]["val"]
lines.append(f"| {name} | {tr['eligible_rows']:,} | {va['eligible_rows']:,} | "
f"{sum(tr['excluded_rows'].values()):,} | {sum(va['excluded_rows'].values()):,} | "
f"{pilot['post_cleanup_train_val_shared_hashes']} |")
lines += ["", "建议对上述 train/dev 视图固定 seed 后按类别抽取小批量,验证训练 loss、数值稳定性和词表扩展后的输入兼容性。"
"小样本 pilot 不能提供论文最终效能结论。", "", "## 范围限制", "",
"- 本次验证的是完全相同序列的重复;没有执行同源聚类、相似性搜索或 DNA 反向互补等价检查,不能声称同源独立。",
"- 未验证来源许可、原始采样方式和生物标签是否正确;文件中的 license 字段不等于独立许可核验。",
"- 未使用 tokenizer,本报告不保证 1024 token 内完整覆盖;需要另行验证候选、指令和序列共同占用的长度。",
"- 全划分冲突仅作审计统计;pilot 清洗不使用 test 标签。正式论文评估仍需独立确定泄漏控制与标签冲突处理方案。",
"", "复现命令:", "", "```bash", "python scripts/audit_laya_tasks.py", "```", ""]
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1])
args = parser.parse_args()
root = args.root.resolve()
report = {
"audit_version": 1,
"generated_at_utc": datetime.now(timezone.utc).isoformat(),
"sequence_hash_normalization": "single user message, last nonempty line, strip, uppercase, SHA-256 UTF-8",
"tasks": {name: audit_task(root, name, expected) for name, expected in TASKS.items()},
}
json_path = root / "artifacts/laya_two_task_audit.json"
markdown_path = root / "research/laya_two_task_audit.md"
json_path.parent.mkdir(parents=True, exist_ok=True)
markdown_path.parent.mkdir(parents=True, exist_ok=True)
json_path.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n")
markdown_path.write_text(markdown_report(report))
for name, task in report["tasks"].items():
pilot = task["pilot"]
print(json.dumps({"task": name, "rows": task["counts"]["rows"], "errors": task["errors"],
"pilot": {split: pilot["splits"][split]["eligible_rows"] for split in ("train", "val")},
"train_val_conflict_hashes": task["label_conflicts"]["train_val_conflicting_sequence_hashes"],
"decision": pilot["decision"]}, ensure_ascii=False))
print(f"Wrote {json_path.relative_to(root)} and {markdown_path.relative_to(root)}")
if __name__ == "__main__":
main()