File size: 6,093 Bytes
cddc722
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""路线 C 的定方向实验:修公式 + 换解码器。

两个改动:
  ① 修 FSQ 公式(v3 的 l=2 恒等输出零,是 bug 不是发现)
     v3:  q = round(tanh(z) * (l-1)/2) / ((l-1)/2)      -> l=2 时 tanh(z)*0.5 ∈(-0.5,0.5),round 恒 0
     v4:  k = round((tanh(z)+1)/2 * (l-1))              -> k ∈ {0..l-1},正好 l 档
          q = k/(l-1)*2 - 1                              -> 落在 [-1,1]
  ② 解码器 Linear -> MLP
     假设:"err 0.64 是解码器太弱,不是词表不够大"
     若换 MLP 后 err 大降 -> 瓶颈是解码器(好消息,词表可以小)
     若几乎不动       -> 真的是词表容量不足,路线要重估

判据(决定路线成不成):
    err  —— 能不能留住信息(< 1 才比"输出均值"好)
    词内方差 —— 同一个码对应的输入有多像(越低 = 码字越有意义)
    最大码占比 —— 塌缩检测(RiboSphere: 有选择性的稀疏是好的,塌是坏的)
"""
from __future__ import annotations

import json
import math
import sys
import time
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "src"))

import rnaqwen  # noqa: F401,E402
import torch  # noqa: E402
import torch.nn as nn  # noqa: E402
import torch.nn.functional as F  # noqa: E402

from rnaqwen.models.encoder import RnaEncoder  # noqa: E402

DEV = "cuda" if torch.cuda.is_available() else "cpu"
N_SEQ, STEPS = 64, 2000
DATA = ROOT / "data/processed/rnaqa_n6_merged.jsonl"

CONFIGS = [
    (4096, 6, 4), (4096, 4, 8),
    (65536, 8, 4), (65536, 4, 16),
]


def fsq(z, levels):
    """v4 公式:正好 levels 档,l=2 时是 {0,1},不退化。"""
    k = torch.round((torch.tanh(z) + 1) / 2 * (levels - 1))
    q = k / (levels - 1) * 2 - 1
    q = q + ((torch.tanh(z) + 1) / 2 * (levels - 1) - k).detach() * 0  # 纯 STE
    q = q + (((torch.tanh(z) + 1) / 2 * (levels - 1)) - ((torch.tanh(z) + 1) / 2 * (levels - 1)).detach())
    return q, k.long()


class AE(nn.Module):
    def __init__(self, dim, latent, levels, dec="linear"):
        super().__init__()
        self.enc = nn.Linear(dim, latent)
        self.levels = levels
        if dec == "linear":
            self.dec = nn.Linear(latent, dim)
        else:
            self.dec = nn.Sequential(
                nn.Linear(latent, 128), nn.GELU(),
                nn.Linear(128, 256), nn.GELU(),
                nn.Linear(256, dim))

    def forward(self, x):
        q, k = fsq(self.enc(x), self.levels)
        return self.dec(q), k


def main() -> int:
    recs = []
    with open(DATA, encoding="utf-8") as f:
        for line in f:
            if line.strip():
                recs.append(json.loads(line))
    recs.sort(key=lambda r: len(r.get("sequence") or ""))
    pick = [recs[int(i * (len(recs) - 1) / (N_SEQ - 1))] for i in range(N_SEQ)]
    seqs = [r["sequence"] for r in pick if r.get("sequence")]
    enc = RnaEncoder(str(ROOT / "models/ernierna"), dtype=torch.float32).to(DEV).eval()
    with torch.no_grad():
        o = enc(seqs)
        x = o.hidden.float().to(DEV)[o.mask.to(DEV).squeeze(-1) > 0]
    N, D = x.shape
    var_tot = float(x.var(dim=0).sum())
    print(f"[v4] {len(seqs)} 条序列,位置 {N},维度 {D}")

    res = []
    for vocab, latent, levels in CONFIGS:
        assert levels ** latent == vocab
        for dec in ("linear", "mlp"):
            torch.manual_seed(0)
            ae = AE(D, latent, levels, dec).to(DEV)
            opt = torch.optim.AdamW(ae.parameters(), lr=3e-3)
            t0 = time.time()
            for _ in range(STEPS):
                p = torch.randperm(N, device=DEV)[:512]
                xb = x[p]
                rec, _ = ae(xb)
                loss = F.mse_loss(rec, xb) * D
                opt.zero_grad(); loss.backward(); opt.step()
            ae.eval()
            with torch.no_grad():
                rec, k = ae(x)
                err = float(((rec - x) ** 2).sum(1).mean() / var_tot)
                code = torch.zeros(N, dtype=torch.long, device=DEV)
                for d_ in range(latent):
                    code = code * levels + k[:, d_]
                uniq, inv, cnt = torch.unique(code, return_inverse=True, return_counts=True)
                sums = torch.zeros(uniq.numel(), D, device=DEV)
                sums.index_add_(0, inv, x)
                means = sums / cnt.unsqueeze(1)
                within = float(((x - means[inv]) ** 2).sum(1).mean() / var_tot)
                p_ = cnt.float() / N
                ent = float(-(p_ * (p_ + 1e-12).log()).sum()) / math.log(vocab)
                top1 = float(cnt.max()) / N
            res.append({"vocab": vocab, "latent": latent, "levels": levels, "dec": dec,
                        "err": err, "codes_used": int(uniq.numel()),
                        "util": float(uniq.numel()) / vocab, "within_var": within,
                        "entropy_norm": ent, "top1_share": top1, "sec": round(time.time()-t0)})
            print(f"  vocab {vocab:>6} latent={latent:>2} l={levels:>2} dec={dec:<6} "
                  f"err {err:.4f}  码字 {int(uniq.numel()):>5} ({100*uniq.numel()/vocab:>5.1f}%)  "
                  f"词内方差 {within:.4f}  归一熵 {ent:.3f}  最大码 {100*top1:.2f}%  {time.time()-t0:.0f}s")

    out = ROOT / "reports/fsq_v4_decoder.json"
    out.parent.mkdir(parents=True, exist_ok=True)
    out.write_text(json.dumps(res, indent=2, ensure_ascii=False), encoding="utf-8")
    print(f"\n[v4] -> {out}")

    print()
    print("  === 解码器是不是瓶颈 ===")
    for vocab, latent, levels in CONFIGS:
        a = next(r for r in res if r["vocab"] == vocab and r["dec"] == "linear")
        b = next(r for r in res if r["vocab"] == vocab and r["dec"] == "mlp")
        drop = 1 - b["err"] / max(a["err"], 1e-9)
        print(f"  vocab {vocab:>6} latent={latent} l={levels}: "
              f"linear {a['err']:.4f} -> mlp {b['err']:.4f}  ({100*drop:+.1f}%)")
    print()
    print("  === l=2 是否还退化(修公式的验证)===")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())