File size: 9,106 Bytes
6ae7efa db88907 6ae7efa 7155827 6ae7efa 7155827 6ae7efa 7155827 6ae7efa 7155827 6ae7efa 7155827 6ae7efa | 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 | """Validate published regression checkpoints against their fixed evaluation data.
Paths supplied on the command line are relative to the repository root. Output
directories must be new, so validation cannot overwrite archived evidence.
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parent
REGRESSION_BASE = "RNASeek-regression-pretrained"
DEFAULTS = {
"efficiency": {
"checkpoint": "efficiency_figure2/qwen_regression_ckpt/clean_cosine_restart_besthp_preview_fixed-wd-0.9_reproduce/checkpoint-304419",
"data": "efficiency_figure2/evenBetterDataFolded-vl.json",
"metric": "r2",
"target": 0.389,
},
"stability": {
"checkpoint": "regression_stability_functionalviral/rnaseek_abs20_tuning/hpcc_best_wave29_copyback/best_model",
"data": "regression_stability_functionalviral/rnaseek_abs20_tuning/hpcc_best_wave29_copyback/valid_predictions.tsv",
"metric": "spearman_r",
"target": 0.342,
},
}
def local_path(value):
path = Path(value).expanduser()
return path.resolve() if path.is_absolute() else (ROOT / path).resolve()
def load_regression_checkpoint(checkpoint, tokenizer_path=None, *, initialize_head=False):
"""Strictly load a predictor, or explicitly initialize a training-only head."""
import torch
from torch import nn
from safetensors.torch import load_file
from transformers import AutoConfig, AutoModel, AutoTokenizer
checkpoint = local_path(checkpoint)
config = AutoConfig.from_pretrained(checkpoint, local_files_only=True)
tokenizer = AutoTokenizer.from_pretrained(
local_path(tokenizer_path) if tokenizer_path else checkpoint,
local_files_only=True,
)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
# Strict tensor loading supports both archived layouts: a standalone
# backbone plus head, or a single state dict prefixed with backbone.
index = checkpoint / "model.safetensors.index.json"
shards = sorted(set(json.loads(index.read_text())["weight_map"].values())) if index.exists() else ["model.safetensors"]
state = {}
for shard in shards:
state.update(load_file(str(checkpoint / shard)))
if any(key.startswith("backbone.") for key in state):
head_state = {key.removeprefix("regression_head."): value for key, value in state.items() if key.startswith("regression_head.")}
backbone_state = {key.removeprefix("backbone."): value for key, value in state.items() if key.startswith("backbone.")}
unexpected = [key for key in state if not key.startswith(("backbone.", "regression_head."))]
if unexpected:
raise ValueError("Unexpected tensors in combined checkpoint")
elif initialize_head and not (checkpoint / "regression_head.pt").exists() and any(
name.endswith("ForCausalLM") for name in (config.architectures or [])
):
unexpected = [key for key in state if not key.startswith("model.") and key != "lm_head.weight"]
if unexpected:
raise ValueError("Unexpected tensors in pretrained causal-language-model checkpoint")
backbone_state = {key.removeprefix("model."): value for key, value in state.items() if key.startswith("model.")}
head_state = None
else:
backbone_state = state
if not (checkpoint / "regression_head.pt").exists():
raise ValueError("This checkpoint has no trained regression head. Use train_regression.py to fine-tune the pretrained base first.")
head_state = torch.load(checkpoint / "regression_head.pt", map_location="cpu", weights_only=True)
linear_index = 2 if head_state is None or "net.2.weight" in head_state else 1
head = nn.Module()
layers = [nn.LayerNorm(config.hidden_size)]
if linear_index == 2:
layers.append(nn.Dropout(0.1))
layers.append(nn.Linear(config.hidden_size, 1))
head.net = nn.Sequential(*layers)
if head_state is not None:
head.load_state_dict(head_state, strict=True)
backbone = AutoModel.from_config(config, attn_implementation="sdpa")
backbone.load_state_dict(backbone_state, strict=True)
del state, backbone_state, head_state
backbone.config.use_cache = False
return backbone, head, tokenizer
def evaluate(args):
import numpy as np
import pandas as pd
import torch
from safetensors.torch import load_file
from scipy.stats import pearsonr, spearmanr
from sklearn.metrics import r2_score
from torch import nn
from transformers import AutoConfig, AutoModel, AutoTokenizer
spec = DEFAULTS[args.task]
checkpoint = local_path(args.checkpoint or spec["checkpoint"])
data = local_path(args.data or spec["data"])
output = local_path(args.output)
if output.exists():
raise FileExistsError("Validation requires a new output directory.")
backbone, head, tokenizer = load_regression_checkpoint(checkpoint, args.tokenizer)
device = torch.device(args.device)
dtype = torch.bfloat16 if args.precision == "bfloat16" else torch.float32
backbone.to(device=device, dtype=dtype).eval()
head.to(device=device, dtype=dtype).eval()
if args.task == "efficiency":
examples = json.loads(data.read_text())
texts, labels = list(examples), list(examples.values())
add_special_tokens = True
else:
frame = pd.read_csv(data, sep="\t")
texts, labels = frame["text"].tolist(), frame["label"].tolist()
add_special_tokens = False
if args.limit:
texts, labels = texts[:args.limit], labels[:args.limit]
predictions = []
with torch.inference_mode(), torch.autocast(device_type=device.type, dtype=torch.bfloat16, enabled=args.precision == "autocast"):
for start in range(0, len(texts), args.batch_size):
batch = tokenizer(texts[start:start + args.batch_size], padding=True, pad_to_multiple_of=8, truncation=True, max_length=512, return_tensors="pt", add_special_tokens=add_special_tokens)
batch = {key: batch[key].to(device) for key in ("input_ids", "attention_mask")}
hidden = backbone(**batch).last_hidden_state
positions = torch.arange(hidden.shape[1], device=device)[None, :].expand(hidden.shape[:2])
last = positions.masked_fill(~batch["attention_mask"].bool(), -1).max(dim=1).values
if (last < 0).any():
raise ValueError("Empty tokenized sequence")
pooled = hidden[torch.arange(hidden.shape[0], device=device), last]
predictions.extend(head.net(pooled).flatten().float().cpu().tolist())
if start % (args.batch_size * 20) == 0:
print(f"{args.task}: {min(start + args.batch_size, len(texts))}/{len(texts)} evaluated", flush=True)
labels, predictions = np.asarray(labels), np.asarray(predictions)
if not np.isfinite(predictions).all():
raise ValueError("Non-finite model predictions")
metrics = {
"task": args.task, "n": len(labels), "precision": args.precision,
"r2": float(r2_score(labels, predictions)),
"pearson_r": float(pearsonr(labels, predictions).statistic),
"spearman_r": float(spearmanr(labels, predictions).statistic),
"full_validation": not bool(args.limit),
"target_metric": spec["metric"], "target": spec["target"],
}
metrics["allowed_decrease"] = args.allowed_decrease
metrics["target_passed"] = not args.limit and metrics[spec["metric"]] >= spec["target"] - args.allowed_decrease
output.mkdir(parents=True, exist_ok=False)
pd.DataFrame({"label": labels, "prediction": predictions}).to_csv(output / "predictions.tsv", sep="\t", index=False)
(output / "metrics.json").write_text(json.dumps(metrics, indent=2) + "\n")
print(json.dumps(metrics, indent=2), flush=True)
if not args.limit and not metrics["target_passed"]:
raise SystemExit("Metric fell below the reproduction tolerance; inspect results before publishing.")
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("task", choices=DEFAULTS)
parser.add_argument("--checkpoint")
parser.add_argument("--tokenizer")
parser.add_argument("--data")
parser.add_argument("--output", required=True)
parser.add_argument("--device", default="cuda:0")
parser.add_argument("--precision", choices=["float32", "bfloat16", "autocast"], default="float32")
parser.add_argument("--batch-size", type=int, default=16)
parser.add_argument("--limit", type=int, default=0)
parser.add_argument("--allowed-decrease", type=float, default=0.01,
help="Maximum absolute decrease from the reference metric (default: 0.01).")
args = parser.parse_args()
if args.batch_size < 1 or args.limit < 0 or args.allowed_decrease < 0:
parser.error("batch-size must be positive; limit and allowed-decrease must be nonnegative")
evaluate(args)
if __name__ == "__main__":
main()
|