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: 6,787 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 | """原生文本路线的预处理:把 RNA 序列变成"普通文本"。
这是 PLAN_v2 §2.2 里唯一需要新写的东西(几十行)。产出两份:
① data/processed/pretrain_rna.jsonl 序列继续预训练语料(每行一条 RNA 的纯文本)
② data/processed/sft_text_stats.json 为②扫一遍:序列进文本后,prompt 会多长
为什么先做这两个:
§2.3 里定的是"**先跑②(直接 SFT),不够再补①**"。所以①可以做出来备用,
但真正卡住臂 B 的是"RNA 当文本进模板之后,prompt 长度是多少、会不会超上下文"。
②就是为了先把这个数摆出来。
关键取舍(都在代码里注释了):
* 只保留 ACGU 四条碱基 —— 实测全语料 1,557,973 个碱基里非 ACGUN 的是 **0 个**,
所以这不会丢数据;但它挡住了脏数据(空格、换行、数字混进序列)
* 按序列前 60 字符去重 —— 重复会让继续预训练把某些序列背下来
* 长度上限取 **1022 nt**:ERNIE-RNA 的实测上限。换到原生文本后理论上更长也行
(Qwen3 上下文 32K),但**保持和臂 A 同长度上限**,否则对比里多一个变量
"""
from __future__ import annotations
import argparse
import json
import random
from collections import Counter
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MERGED = ROOT / "data/processed/rnaqa_n6_merged.jsonl"
PARQUET = ROOT / "data/external/rnacentral_1024.parquet"
OUT_PRETRAIN = ROOT / "data/processed/pretrain_rna.jsonl"
OUT_STATS = ROOT / "data/processed/sft_text_stats.json"
BASES = set("ACGU")
MAX_NT = 1022 # 与臂 A 对齐
MIN_NT = 20
SEED = 20261010
def clean(seq: str) -> str | None:
s = (seq or "").upper().replace("T", "U").strip()
if not s or len(s) < MIN_NT or len(s) > MAX_NT:
return None
if not set(s) <= BASES: # 混进 N / 空格 / 数字的一律丢
return None
return s
def from_merged():
recs = []
with open(MERGED, encoding="utf-8") as f:
for line in f:
if line.strip():
recs.append(json.loads(line))
out = []
for r in recs:
s = clean(r.get("sequence"))
if s:
out.append({"seq": s, "rna_type": r.get("rna_type") or "",
"source": "rnaqa_n6_merged"})
return out, recs
def from_parquet(n_want: int, rng: random.Random):
import pyarrow.parquet as pq
f = pq.ParquetFile(PARQUET)
out = []
# 流式读,按行组抽;不一次进内存(3250 万行)
for rg in range(f.metadata.num_row_groups):
if len(out) >= n_want:
break
t = f.read_row_group(rg, columns=["sequence", "type"])
d = t.to_pydict()
for s, ty in zip(d["sequence"], d["type"]):
if len(out) >= n_want:
break
c = clean(s)
if c:
out.append({"seq": c, "rna_type": ty or "", "source": "rnacentral_1024"})
rng.shuffle(out)
return out
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--source", default="both", choices=["merged", "parquet", "both"])
ap.add_argument("--n-parquet", type=int, default=200_000,
help="从 3250 万条里抽多少做继续预训练语料")
ap.add_argument("--out", type=Path, default=OUT_PRETRAIN)
args = ap.parse_args()
rng = random.Random(SEED)
rows, recs = [], []
if args.source in ("merged", "both"):
rows, recs = from_merged()
print(f"[prep] merged: {len(rows)} 条干净序列(原始 {len(recs)} 条)")
if args.source in ("parquet", "both"):
p = from_parquet(args.n_parquet, rng)
print(f"[prep] parquet: 抽到 {len(p)} 条干净序列")
rows += p
# 去重(按前 60 字符)
seen, uniq = set(), []
for r in rows:
k = r["seq"][:60]
if k in seen:
continue
seen.add(k)
uniq.append(r)
print(f"[prep] 去重后 {len(uniq)} 条(去掉 {len(rows)-len(uniq)} 条重复)")
lens = sorted(len(r["seq"]) for r in uniq)
lp = sorted((len(r["seq"]) + 2) // 2 + 8 for r in uniq) # 实测 0.526 tok/nt,向上取整
print(f"[prep] 序列长度 min {lens[0]} 中位 {lens[len(lens)//2]} max {lens[-1]}")
print(f"[prep] 对应 token 数(按 0.526 tok/nt)min {lp[0]} 中位 {lp[len(lp)//2]} p99 {lp[int(len(lp)*0.99)]} max {lp[-1]}")
args.out.parent.mkdir(parents=True, exist_ok=True)
with open(args.out, "w", encoding="utf-8") as f:
for r in uniq:
f.write(json.dumps(r, ensure_ascii=False) + "\n")
print(f"[prep] -> {args.out} ({args.out.stat().st_size/1e6:.1f} MB)")
# ---- ② 臂 B 的 prompt 长度估算 ----
# 模板(Llama-3 风格,和 Stage 2 一致)+ 序列 + 问题 + 答案
if recs:
import statistics
prompt_toks, ans_toks = [], []
for r in recs:
s = clean(r.get("sequence")) or ""
n_seq = int(len(s) / 0.526)
for q in (r.get("qa") or []):
qt = q.get("question") or ""
at = q.get("answer") or ""
prompt_toks.append(n_seq + int(len(qt) / 3.6) + 24) # 24 ≈ 模板开销
ans_toks.append(int(len(at) / 3.6) + 2)
if prompt_toks:
pt = sorted(prompt_toks)
print()
print("[prep] 臂 B(RNA 当文本)的 prompt token 数估算:")
print(f" 中位 {pt[len(pt)//2]} p90 {pt[int(len(pt)*0.9)]} max {pt[-1]}"
f" (对应 1.9 nt/token,答案另计)")
over = sum(1 for x in pt if x > 4096)
print(f" 超过 4096 token 的样本:{over} / {len(pt)}"
f"({100*over/len(pt):.2f}%)")
print(f" -> Qwen3 上下文远大于此,臂 B 不需要截断序列")
json_stats = {
"n_sequences": len(uniq),
"seq_len": {"min": lens[0], "median": lens[len(lens)//2], "max": lens[-1]},
"prompt_tokens": {"median": pt[len(pt)//2], "p90": pt[int(len(pt)*0.9)],
"max": pt[-1], "over_4096": over,
"n_samples": len(pt)},
"template": "llama3 + RNA 原生文本",
"max_nt": MAX_NT, "seed": SEED,
}
OUT_STATS.write_text(json.dumps(json_stats, indent=2, ensure_ascii=False),
encoding="utf-8")
print(f"[prep] -> {OUT_STATS}")
else:
print("[prep] 没有 QA 样本,跳过 prompt 长度估算")
return 0
if __name__ == "__main__":
raise SystemExit(main())
|