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
Download scripts/33_token_modality_relevance.py from RomanCohort/RNAQwen: direct link, hf CLI and curl.
- Browser
- Download file 7.64 kB
-
https://huggingface.co/RomanCohort/RNAQwen/resolve/main/scripts/33_token_modality_relevance.py
- Command line
-
hf download hf://RomanCohort/RNAQwen/scripts/33_token_modality_relevance.py
-
curl -L -o 33_token_modality_relevance.py https://huggingface.co/RomanCohort/RNAQwen/resolve/main/scripts/33_token_modality_relevance.py
7.64 kB
| """逐 token 模态相关性探针(MAPO 的仪器的 RNA 版)。 | |
| MAPO(arXiv 2605.27741)的做法:拿一个 **ablated reference** 和真实策略比, | |
| 用跨模态差分熵找出"哪些 token 真的依赖源模态",然后据此加权策略梯度。 | |
| 我们的对应物: | |
| 参照 = 编码器置零的前向(encoder-ablated) | |
| 度量 = 答案位置上两个分布的 **KL(真实 || 置零)** | |
| 粒度 = **逐 token**,不是一个全局标量 | |
| 为什么必须升级到逐 token: | |
| 全局 Δloss 只说"整体上依赖变少了",说不出**哪里**变少。 | |
| 而模态崩溃的关键信息恰恰在分布上——是"所有位置均匀地依赖下降"(= 整体遗忘), | |
| 还是"只有套话位置不依赖、关键事实位置仍然依赖"(= 正常的任务结构)? | |
| 这两种情况的全局标量可能一样,但对下一步该改什么,结论完全相反。 | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import dataclasses | |
| 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 | |
| import torch.nn.functional as F # noqa: E402 | |
| from rnaqwen.data.records import read_jsonl # noqa: E402 | |
| from rnaqwen.models.alignment import Stage1Aligner, Stage1Config # noqa: E402 | |
| from rnaqwen.models.encoder import EncoderOutput # noqa: E402 | |
| def answer_kl(model, seq: str, tgt: str, device) -> dict: | |
| """返回该样本答案位置上,真实 RNA 与置零 RNA 两个分布的逐 token KL。""" | |
| # ⚠️ build_batch 必须在**补丁内部**重新调用。 | |
| # 踩过:如果在补丁之前先算好 inputs,置零那个 forward 用的还是旧 inputs, | |
| # 补丁根本没参与计算 —— 结果是 KL 全 0.0000(包括 max),看起来像"模态完全崩了", | |
| # 实际是我的探针没生效。全零且 max 也是零,这就是信号:先怀疑自己。 | |
| def fwd(zero: bool): | |
| orig = model.encoder.forward | |
| if zero: | |
| def zf(seqs): | |
| o = orig(seqs) | |
| return EncoderOutput(hidden=torch.zeros_like(o.hidden), | |
| mask=o.mask, lengths=o.lengths) | |
| model.encoder.forward = zf | |
| try: | |
| inputs, attn, labels = model.build_batch([seq], [tgt], device) | |
| out = model.llm(inputs_embeds=inputs, attention_mask=attn, labels=labels) | |
| finally: | |
| model.encoder.forward = orig | |
| return out.logits, labels | |
| lr, labels = fwd(False) | |
| lz, _ = fwd(True) | |
| lr, lz = lr.float(), lz.float() | |
| # 只在答案位置比:labels != -100 且需要 shift 对齐 | |
| shift_lab = labels[:, 1:] | |
| mask = shift_lab != -100 | |
| if not bool(mask.any()): | |
| return {} | |
| pr = F.log_softmax(lr[:, :-1][mask], dim=-1) | |
| pz = F.log_softmax(lz[:, :-1][mask], dim=-1) | |
| kl = (pr.exp() * (pr - pz)).sum(-1) # 逐 token KL(real||zero) | |
| idx = shift_lab[mask] | |
| dlogp = (pr.gather(-1, idx.unsqueeze(-1)) - pz.gather(-1, idx.unsqueeze(-1))).squeeze(-1) | |
| return {"kl": kl.detach().cpu(), "dlogp": dlogp.detach().cpu(), "n": int(mask.sum())} | |
| def main() -> int: | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--ckpt", type=Path, required=True) | |
| ap.add_argument("--data", type=Path, default=ROOT / "data/processed/rnaqa_rnacentral_all.jsonl") | |
| ap.add_argument("--n", type=int, default=16) | |
| ap.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu") | |
| ap.add_argument("--out", type=Path, default=ROOT / "reports/token_modality_relevance.json") | |
| args = ap.parse_args() | |
| blob = torch.load(args.ckpt, map_location="cpu", weights_only=False) | |
| known = {f.name for f in dataclasses.fields(Stage1Config)} | |
| cfg = Stage1Config(**{k: v for k, v in (blob.get("config") or {}).items() if k in known}) | |
| model = Stage1Aligner(cfg) | |
| model.projection.load_state_dict(blob["projection"]) | |
| if model.compression is not None: | |
| model.compression.load_state_dict(blob["compression"]) | |
| for m in (model.encoder, model.llm, model.projection): | |
| m.to(args.device) | |
| if model.compression is not None: | |
| model.compression.to(args.device) | |
| model.eval() | |
| print(f"[tok] ckpt={args.ckpt.name} step={blob.get('step')} arch={model.encoder.arch.name}") | |
| recs = [r.as_dict() for r in read_jsonl(args.data)][: args.n] | |
| all_kl, all_dlogp, per_sample = [], [], [] | |
| for i, r in enumerate(recs): | |
| seq = r.get("sequence") or "" | |
| tgt = (r.get("annotation") or "").strip() | |
| if not seq or not tgt: | |
| continue | |
| try: | |
| d = answer_kl(model, seq, tgt, args.device) | |
| except Exception as e: | |
| print(f" [{i}] 失败 {type(e).__name__}: {str(e)[:60]}") | |
| continue | |
| if not d: | |
| continue | |
| kl, dl = d["kl"], d["dlogp"] | |
| all_kl.append(kl); all_dlogp.append(dl) | |
| per_sample.append({ | |
| "i": i, "n_tok": d["n"], | |
| "kl_mean": float(kl.mean()), "kl_median": float(kl.median()), | |
| "kl_max": float(kl.max()), "kl_p90": float(kl.quantile(0.9)), | |
| "kl_frac_above_0.1": float((kl > 0.1).float().mean()), | |
| "dlogp_mean": float(dl.mean()), | |
| "dlogp_neg_frac": float((dl < 0).float().mean()), | |
| }) | |
| if not all_kl: | |
| print("[fail] 没有有效样本") | |
| return 1 | |
| K = torch.cat(all_kl); D = torch.cat(all_dlogp) | |
| print(f"\n 样本 {len(per_sample)} 答案 token {K.numel()}") | |
| print(f" KL(real||zero) mean {K.mean():.4f} median {K.median():.4f} " | |
| f" p90 {K.quantile(0.9):.4f} max {K.max():.4f}") | |
| print(f" 依赖占比:KL>0.1 的 token 占 {100*float((K>0.1).float().mean()):.1f}%") | |
| print(f" KL>0.5 的 token 占 {100*float((K>0.5).float().mean()):.1f}%") | |
| print(f" Δlogp(正确token) mean {D.mean():+.4f} **负值占比 {100*float((D<0).float().mean()):.1f}%**") | |
| print() | |
| print(" %4s %7s %9s %9s %9s %11s" % ("idx", "ntok", "KL均值", "KL中位", "KL_p90", "KL>0.1占比")) | |
| for s in per_sample[:12]: | |
| print(" %4d %7d %9.4f %9.4f %9.4f %10.1f%%" | |
| % (s["i"], s["n_tok"], s["kl_mean"], s["kl_median"], s["kl_p90"], | |
| 100 * s["kl_frac_above_0.1"])) | |
| print() | |
| frac = float((K > 0.1).float().mean()) | |
| neg = float((D < 0).float().mean()) | |
| if frac > 0.5: | |
| print(" -> **大部分答案 token 都依赖 RNA**:模型确实在逐位置读序列。模态未崩。") | |
| elif frac > 0.1: | |
| print(" -> **依赖集中在少数 token**:典型结构——套话位置不依赖,事实位置依赖。") | |
| print(" 这不算崩溃;要看的是那几个关键位置**随训练有没有掉**。") | |
| else: | |
| print(" -> **几乎没有 token 依赖 RNA**:模态已经崩了。") | |
| if neg > 0.3: | |
| print(f" -> ⚠️ {100*neg:.0f}% 的 token 上,置零后正确 token 的 logp **反而更高**。") | |
| print(" 即编码器在这些位置的贡献是**负的**,不只是没用。") | |
| args.out.parent.mkdir(parents=True, exist_ok=True) | |
| args.out.write_text(json.dumps( | |
| {"ckpt": str(args.ckpt), "step": blob.get("step"), | |
| "kl_mean": float(K.mean()), "kl_median": float(K.median()), | |
| "frac_kl_gt_0.1": frac, "dlogp_neg_frac": neg, | |
| "per_sample": per_sample}, indent=2, ensure_ascii=False), encoding="utf-8") | |
| print(f"\n[tok] -> {args.out}") | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |