Instructions to use RomanCohort/RNAQwen with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use RomanCohort/RNAQwen with Transformers:
# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("RomanCohort/RNAQwen", device_map="auto") - Notebooks
- Google Colab
- Kaggle
File size: 8,728 Bytes
acd280c | 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 | """P1 验收:**置零编码器探针**(PLAN §3 P1 的完成判据)。
判据原文:
"置零编码器后,对齐指标明显下降"
这条是整个计划里最重要的自检 —— 它同时挡两个坑(PLAN §5 风险登记):
* **投影塌缩 / 编码器被绕过**:loss 降了,但置零毫无影响 → 模型压根没用上序列;
* **QA 里复述了序列**:指标虚高 → 置零后分数不掉,就是这个症状。
做法:
对同一批样本跑两次前向(teacher forcing),一次用真实 RNA 表征、一次把表征置零,
比较 loss / token 准确率。真对齐的模型,置零后 loss 必须**明显**上升。
跑法:
python scripts/22_zero_encoder_probe.py --ckpt checkpoints/stage1/projection_final.pt \
--data data/processed/annotations_50.jsonl --limit 20
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "src"))
import rnaqwen # noqa: E402,F401
import torch # noqa: E402
from rnaqwen.data.records import read_jsonl # noqa: E402
from rnaqwen.models.alignment import Stage1Aligner, Stage1Config # noqa: E402
from rnaqwen.paths import default_encoder, describe_encoder # noqa: E402
@torch.no_grad()
def evaluate(model: Stage1Aligner, samples: list[dict], device: torch.device,
zero_encoder: bool) -> dict:
"""返回平均 loss 与答案 token 的逐 token 准确率。"""
total_loss, total_tok, correct = 0.0, 0, 0
orig_forward = model.encoder.forward
if zero_encoder:
from rnaqwen.models.encoder import EncoderOutput
def zero_forward(seqs): # noqa: ANN001
out = orig_forward(seqs)
return EncoderOutput(hidden=torch.zeros_like(out.hidden), mask=out.mask, lengths=out.lengths)
model.encoder.forward = zero_forward # type: ignore[assignment]
try:
for s in samples:
inputs, attn, labels = model.build_batch([s["sequence"]], [s["target"]], device)
out = model.llm(inputs_embeds=inputs, attention_mask=attn, labels=labels)
total_loss += float(out.loss)
shift_logits = out.logits[:, :-1]
shift_labels = labels[:, 1:]
mask = shift_labels != -100
if mask.any():
pred = shift_logits[mask].argmax(-1)
correct += int((pred == shift_labels[mask]).sum())
total_tok += int(mask.sum())
finally:
model.encoder.forward = orig_forward # type: ignore[assignment]
n = max(len(samples), 1)
return {
"loss": total_loss / n,
"token_accuracy": correct / total_tok if total_tok else None,
"n_samples": len(samples),
"n_answer_tokens": total_tok,
}
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--ckpt", type=Path, required=True, help="Stage1 产出的 projection_*.pt")
ap.add_argument("--data", type=Path, required=True)
ap.add_argument("--llm", default="models/Qwen3-8B")
ap.add_argument("--encoder", default=default_encoder(),
help="不填就走 E 轨默认(ERNIE-RNA)。R 轨复现请显式给 models/rnafm")
ap.add_argument("--target-field", default="annotation", choices=["annotation", "literature"])
ap.add_argument("--limit", type=int, default=20)
ap.add_argument("--device", default="cuda")
ap.add_argument("--out", type=Path, default=ROOT / "reports" / "zero_encoder_probe.json")
# E 轨 checkpoint 要对齐压缩配置(默认 None = 读 checkpoint 里存的)
ap.add_argument("--compress-latent", type=int, default=None,
help="覆盖 checkpoint 里的 compress_latent(E 轨 checkpoint 一般不用填,会自动读到)")
ap.add_argument("--compress-method", default=None,
help="覆盖 checkpoint 里的 compress_method")
# 判据阈值。
# ⚠️ 实测:E 轨 run1 的 Δloss 从峰值 0.273 衰减到 0.047,但 ROUGE-1 反而从 0.208 涨到 0.303。
# 说明 Δloss 是**逐 token 平均**,答案变长/变准会把它摊薄,低 Δloss ≠ 编码器没用。
# 所以这个探针的结论要和 31_stage1_rouge_curve.py 一起读,别单看这里的 PASS/FAIL。
ap.add_argument("--pass-delta", type=float, default=0.5)
ap.add_argument("--weak-delta", type=float, default=0.1)
args = ap.parse_args()
rows = [r.as_dict() for r in read_jsonl(args.data)]
samples = []
for r in rows:
t = r.get(args.target_field) or ""
if r.get("sequence") and t.strip():
samples.append({"sequence": r["sequence"], "target": t})
if args.limit and len(samples) >= args.limit:
break
if not samples:
print("[fail] 没有样本")
return 1
blob = torch.load(args.ckpt, map_location="cpu")
# checkpoint 里存的 config 是**训练时**的完整快照,字段集合和 Stage1Config 不完全一致
# (多出 collapse_delta 之类的旧字段,且 llm_path/encoder_path 会和命令行参数撞名)。
# 以前直接 **blob["config"] 会 TypeError;这里只挑 dataclass 认识的字段,
# 且命令行显式给的路径优先。
import dataclasses
known = {f.name for f in dataclasses.fields(Stage1Config)}
saved = {k: v for k, v in (blob.get("config") or {}).items() if k in known}
dropped = sorted(set(blob.get("config") or {}) - known)
if dropped:
print(f"[zero-probe] 忽略 checkpoint 里的未知配置字段:{dropped}")
saved["llm_path"] = args.llm
saved["encoder_path"] = args.encoder
# 允许命令行强制覆盖(E 轨 checkpoint 靠这个对齐压缩配置)
if args.compress_latent is not None:
saved["compress_latent"] = args.compress_latent
if args.compress_method:
saved["compress_method"] = args.compress_method
cfg = Stage1Config(**saved)
model = Stage1Aligner(cfg)
model.projection.load_state_dict(blob["projection"])
# E 轨 checkpoint 必须把压缩器一起装回去,否则 build_batch 的 token 数对不上。
if cfg.compress_latent:
if "compression" not in blob:
print(f"[fail] 配置要求 compress_latent={cfg.compress_latent},"
f"但 checkpoint 里没有 compression 权重")
return 2
model.compression.load_state_dict(blob["compression"])
print(f"[zero-probe] 已装回压缩器 {blob.get('compression_spec')}")
device = torch.device(args.device)
model.encoder.to(device)
model.llm.to(device)
model.projection.to(device)
if model.compression is not None:
model.compression.to(device)
model.eval()
print(f"[zero-probe] {len(samples)} 条样本;投影 {model.projection.describe()}")
real = evaluate(model, samples, device, zero_encoder=False)
print(f"[zero-probe] 真实编码器 : loss={real['loss']:.4f} tok_acc={real['token_accuracy']}")
zero = evaluate(model, samples, device, zero_encoder=True)
print(f"[zero-probe] 置零编码器 : loss={zero['loss']:.4f} tok_acc={zero['token_accuracy']}")
d_loss = zero["loss"] - real["loss"]
acc_drop = ((real["token_accuracy"] or 0) - (zero["token_accuracy"] or 0))
if d_loss > args.pass_delta:
verdict = "PASS"
elif d_loss > args.weak_delta:
verdict = "WEAK"
else:
verdict = "FAIL"
print(f"\n[zero-probe] Δloss = +{d_loss:.4f} Δtoken_acc = -{acc_drop:.4f} -> {verdict}")
print(" ⚠️ 判据提醒:Δloss 是逐 token 均值,答案越准越长它越低。")
print(" 最终以 scripts/31_stage1_rouge_curve.py 的 ROUGE 为准。")
if verdict == "FAIL":
print(" 置零几乎没影响 = 模型没在用序列(投影塌缩 / 抄文本),回去查。")
report = {"checkpoint": str(args.ckpt), "data": str(args.data), "n": len(samples),
"real": real, "zeroed": zero, "delta_loss": d_loss,
"delta_token_accuracy": acc_drop, "verdict": verdict,
"compress_latent": cfg.compress_latent, "compress_method": cfg.compress_method,
"criterion": "PLAN §3 P1:置零编码器后对齐指标必须明显下降"
"(Δloss 仅作参考,最终判据见 ROUGE 曲线)"}
args.out.parent.mkdir(parents=True, exist_ok=True)
args.out.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
print(f"[zero-probe] 报告 -> {args.out}")
return 0 if verdict == "PASS" else 1
if __name__ == "__main__":
raise SystemExit(main())
|