Buckets:

illusion-brain/brain-domains / scripts /exp76_variant_structure.py
redmingwei's picture
download
raw
17.1 kB
#!/usr/bin/env python3
"""exp76_variant_structure.py — 阶段 D 的**结构勘查**(零训练,前置)。
## 为什么必须先跑这一步
教训 #24:**先验证结构是否存在,再构建结构**。
本项目的组装器(`assembler.py`)走的是「拆成零件 → 重建整条链」的路子,
但它有一条致命限制已经写在代码注释里:
> CBraMod 的 Criss-Cross 是**卷积**实现,我们的 `CrissCross2D` 是**注意力**实现
> —— 结构不同、权重**不可搬**,只能标 `arch-only`。
⇒ 如果拿 cbramod 当宿主去「重建整条链」,产出的其实是一个
**随机初始化的 transformer**(只有 patch embed 是真的)⇒ 跑 RSA 必然接近 0,
测到的是「随机初始化的 Transformer 没有脑对齐」,不是「重组有没有用」。
所以本脚本先把**真实结构**取出来,让「能不能嫁接」变成一个**本地可判定**的问题:
1. 每个模型的 block 列表(名字 / mixer 类 / FFN 类);
2. 每个 block 的 mixer / FFN **逐键权重形状**;
3. 每个 block 的**输出 token 形状** (B, N, D) ⇒ 深度与 token 维;
4. patch embed(token 化入口)的输出形状。
拿到这些之后,嫁接分三种结论,每种都有明确处置:
| 结论 | 含义 | 处置 |
|---|---|---|
| **形状兼容** | 供体与宿主的对应权重**逐键同形** | 真嫁接:把供体权重搬进宿主槽位(零训练、逐键可核) |
| **需投影** | 维度不同但结构同族 | 零训练下**不做**(学习投影是随机初始化 ⇒ 白搬)⇒ 记为负结果 |
| **结构不同** | 供体是 conv 注意力、宿主是 MHA 之类 | 不可搬 ⇒ 走**表征级拼接**(D1 路线) |
## 输出
- 日志:每个模型一行 `###MODEL### {json}`(便于从 `hf jobs logs` 解析);
- 文件:`/domains/runs/illusionbrain-align/structure/<version>.json`。
## 用法
云端:
hf jobs run "hf.co/spaces/redmingwei/IllusionBrain" \
python -u /domains/scripts/exp76_variant_structure.py \
-v hf://buckets/illusion-brain/training-data:/training:ro \
-v hf://buckets/illusion-brain/brain-domains:/domains \
--flavor cpu-upgrade --timeout 30m -d --bd-version stable
本地自检(合成数据,不碰网络):
python exp76_variant_structure.py --self-test
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
import traceback
from pathlib import Path
import numpy as np
EXP_DIR = Path(__file__).resolve().parent
# models 包的位置:本地 = brain_domains/models;云端 = /domains/scripts/models
_PKG_CANDIDATES = [EXP_DIR / "models", EXP_DIR.parent / "models", EXP_DIR]
PKG_DIR = next((c for c in _PKG_CANDIDATES if (c / "registry.py").exists()),
_PKG_CANDIDATES[0])
for _p in (str(EXP_DIR), str(PKG_DIR)):
if _p not in sys.path:
sys.path.insert(0, _p)
import base_specs as BS # noqa: E402
import domain_adapters as DA # noqa: E402
MODELS_DIR = Path("/training/models")
OUT_ROOT = Path("/domains/runs/illusionbrain-align/structure")
# dev 版 braindecode 的完整版本串。
# ⚠️ `ensure_braindecode_version` 收的是**完整版本串**,不是 "dev" 这个标签 ——
# 传 "dev" 会让 pip 去装 `braindecode[hub]==dev`(不存在)⇒ 直接 RuntimeError。
BD_DEV_VERSION = "1.8.1.dev184031250"
def _bd_install_target(v):
"""把版本标签翻译成 `ensure_braindecode_version` 要的**完整版本串**。"""
v = (v or "stable").strip()
if v == "stable":
return "stable"
if v == "dev":
return BD_DEV_VERSION
return v
def _ver_tag(v):
"""模型选择用的版本标签(规格表里是 "stable" / "dev")。"""
return "stable" if (v or "stable").strip() == "stable" else "dev"
def log(*a):
print(*a, flush=True)
def _as_tokens(t):
"""把各种 block 输出统一成 (B, N, D) 的 tensor(拿不到就 None)。"""
import torch
if t is None:
return None
if hasattr(t, "tokens"): # TokenBatch
t = t.tokens
if isinstance(t, (tuple, list)):
t = t[0] if t else None
if t is None:
return None
if not torch.is_tensor(t):
return None
if t.ndim == 3:
return t
if t.ndim == 2:
return t.unsqueeze(1)
if t.ndim == 4:
# ⚠️ 4 维统一交给 registry 的 `normalize_token_tensor`:
# 它的约定是**通道在前** (B, D, H, W) ⇒ 折成 (B, H*W, D)。
# (CBraMod 的 block 输出就是 (B, D, H, W);exp75 用的也是这个函数
# ⇒ 这里必须与它同口径,不能自己按通道在后折。)
import registry as REG
return REG.normalize_token_tensor(t)
return None
def _shapes(mod):
"""模块的权重形状表 {键: [shape]}(只取浮点张量,避免 BN 的 int buffer)。"""
import torch
out = {}
for k, v in mod.state_dict().items():
if torch.is_tensor(v) and v.is_floating_point():
out[k] = list(v.shape)
return out
def _cls(mod):
return type(mod).__name__
def _find_tokenizer2(model, x):
"""找 token 化入口(patch embed),**接受 3 维与 4 维输出**。
为什么不能直接用 assembler 的 `find_tokenizer`:它只收 `t.ndim == 3`,
而 **CBraMod 的 patch embed 输出是 4 维**(B, rows, cols, D)的二维网格
⇒ 那个函数在 cbramod 上**必然返回 None**(实测 d_model=None)。
这里把 4 维也认下来,并按同样的「谁能独立吃原始 EEG」判据筛。
"""
import torch
cands = []
def hook(nm):
def _h(mod, inp, o):
t = o
if isinstance(t, (tuple, list)):
t = t[0] if t else None
if torch.is_tensor(t) and t.ndim in (3, 4) and t.shape[1] > 1:
cands.append(nm)
return _h
handles = [m.register_forward_hook(hook(nm)) for nm, m in model.named_modules() if nm]
try:
with torch.no_grad():
model(x)
finally:
for h in handles:
h.remove()
cand_set = set(cands)
for nm, mod in model.named_modules():
if nm not in cand_set:
continue
try:
with torch.no_grad():
out = mod(x)
except Exception:
continue
t = out[0] if isinstance(out, (tuple, list)) else out
if torch.is_tensor(t) and t.ndim in (3, 4) and t.shape[1] > 1:
return nm, mod
return None, None
LAYERISH = re.compile(r"(layers?\.\d+|blocks?\.\d+|encoder\.layer\d+|transformer\.\d+)", re.I)
def _layer_detail(model):
"""每个「像层」的模块:类名 + 直接子模块的类名与权重形状。
这是**嫁接可行性**的关键数据:要知道宿主的槽位是
「卷积注意力」还是「MultiheadAttention」、FFN 的 Linear 形状是多少。
"""
import torch
out = []
for nm, mod in model.named_modules():
if not LAYERISH.search(nm):
continue
kids = []
for cn, cm in mod.named_children():
sh = _shapes(cm)
kids.append({"name": cn, "cls": type(cm).__name__,
"n_params": int(sum(p.numel() for p in cm.parameters())),
"shapes": sh})
out.append({"name": nm, "cls": type(mod).__name__,
"n_params": int(sum(p.numel() for p in mod.parameters())),
"children": kids})
return out
def _module_tree(model):
"""完整模块树 [名字, 类名](嫁接要精确定位路径,光有 block 列表不够)。"""
return [[nm, type(m).__name__] for nm, m in model.named_modules() if nm]
def probe_model(name, handle, have_chans=None):
"""勘查一个基模:block 结构 + token 形状 + 权重形状 + 模块树。"""
import torch
import registry as REG
import assembler as ASM
spec = handle.spec
out = {"model": name, "ok": False}
# --- 1. 造一个真实形状的输入(走正式的域适配路径)---
dummy = np.zeros((1, 63, 250), dtype=np.float32)
try:
adapted, info = DA.adapt(dummy, spec, have_chans or BS.THINGS_EEG_CHANS)
except Exception as e:
out["error"] = f"域适配失败:{type(e).__name__}: {e}"
return out
out["adapted_shape"] = list(adapted.shape)
out["adapt"] = info.to_dict()
mdt = next((p.dtype for p in handle.model.parameters()), None)
x = torch.from_numpy(np.ascontiguousarray(adapted)).to(dtype=mdt or torch.float32)
# --- 2. 找 block 与 token 化入口 ---
blocks = ASM.find_block_modules(handle.model)
out["n_blocks"] = len(blocks)
out["block_names"] = [b["name"] for b in blocks]
tok_name, tok_mod = _find_tokenizer2(handle.model, x)
out["tokenizer"] = {"name": tok_name, "cls": _cls(tok_mod) if tok_mod else None}
# --- 3. 挂 hook 收每个 block / token 化入口 的输出形状 ---
got = {}
def mk(nm):
def hook(mod, inp, o):
t = _as_tokens(o)
if t is not None:
got[nm] = list(t.shape)
return hook
handles = []
for b in blocks:
handles.append(b["module"].register_forward_hook(mk(b["name"])))
if tok_mod is not None:
handles.append(tok_mod.register_forward_hook(mk("__tokenizer__")))
try:
with torch.no_grad():
handle.model(x)
except Exception as e:
out["error"] = f"前向失败:{type(e).__name__}: {e}"
out["trace"] = traceback.format_exc()[-600:]
return out
finally:
for h in handles:
h.remove()
# 最终输出(表里那一行用的表征)
try:
with torch.no_grad():
o = handle.model(x)
ft, _how = REG.tokens_from_output(o, 2)
if ft is not None:
got["__final__"] = list(ft.shape)
except Exception:
pass
out["tokenizer"]["out_shape"] = got.get("__tokenizer__")
out["final_shape"] = got.get("__final__")
out["d_model"] = (out["tokenizer"]["out_shape"] or [None, None, None])[-1]
out["n_tokens"] = (out["tokenizer"]["out_shape"] or [None, None])[-2]
out["final_dim"] = (out["final_shape"] or [None, None, None])[-1]
# --- 4. 逐 block 的 mixer / FFN 结构与权重形状 ---
binfo = []
for i, b in enumerate(blocks):
attn_name, attn_mod = b["attn"]
ffn_name, ffn_mod = b["ffn"]
binfo.append({
"index": i,
"name": b["name"],
"mixer": {
"path": f"{b['name']}.{attn_name}",
"cls": _cls(attn_mod),
"is_mha": isinstance(attn_mod, torch.nn.MultiheadAttention),
"shapes": _shapes(attn_mod),
},
"ffn": {
"path": f"{b['name']}.{ffn_name}",
"cls": _cls(ffn_mod),
"shapes": _shapes(ffn_mod),
},
"out_shape": got.get(b["name"]),
})
out["blocks"] = binfo
# --- 5. 「像层」的模块明细 + 完整模块树 ---
out["layer_detail"] = _layer_detail(handle.model)
out["module_tree"] = _module_tree(handle.model)
# --- 6. 布局推断 ---
lay = REG.infer_layout(out["n_tokens"] or 0, handle.cfg, handle.meta, handle.model)
out["layout"] = {"layout": list(lay["layout"]) if lay["layout"] else None,
"n_global": lay["n_global"],
"confidence": lay["confidence"], "reason": lay["reason"]}
out["ok"] = True
return out
def run(args):
import registry as REG
REG.ensure_braindecode_version(_bd_install_target(args.bd_version))
try:
import braindecode.models as bdmodels
except Exception as e:
log(f"✗ braindecode 不可用:{e!r}")
return 1
if args.models in ("all", "auto"):
names = BS.models_for_version(_ver_tag(args.bd_version))
else:
names = [s.strip() for s in args.models.split(",") if s.strip()]
log(f"本趟(braindecode {args.bd_version})模型:{names}")
results = []
for nm in names:
sp = BS.BASE_SPECS.get(nm)
h = REG.load_base_model(nm, models_dir=MODELS_DIR, braindecode_models=bdmodels,
spec=sp, verbose=False)
if not h.ok:
rec = {"model": nm, "ok": False, "error": h.error}
results.append(rec)
log(f"###MODEL### {json.dumps(rec, ensure_ascii=False)}")
continue
try:
rec = probe_model(nm, h)
except Exception as e:
rec = {"model": nm, "ok": False,
"error": f"{type(e).__name__}: {e}",
"trace": traceback.format_exc()[-600:]}
rec["n_params"] = h.n_params
rec["class"] = type(h.model).__name__
rec["license"] = h.attribution.get("license")
results.append(rec)
log(f"###MODEL### {json.dumps(rec, ensure_ascii=False)}")
if rec.get("ok"):
log(f"✓ {nm:<16} {rec.get('class')} "
f"blocks={rec.get('n_blocks')} "
f"d_model={rec.get('d_model')} n_tokens={rec.get('n_tokens')} "
f"layout={rec.get('layout', {}).get('layout')} "
f"({rec.get('layout', {}).get('confidence')})")
else:
log(f"✗ {nm:<16} {rec.get('error')}"[:250])
if args.out:
p = Path(args.out)
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(json.dumps({"bd_version": args.bd_version, "models": results},
indent=2, ensure_ascii=False, default=str),
encoding="utf-8")
log(f"结构报告写入 {p}")
return 0 if all(r.get("ok") for r in results) else 1
# ---------------------------------------------------------------------------
# 自测(合成模型,不碰网络)
# ---------------------------------------------------------------------------
def self_test():
import torch
from torch import nn
import interfaces as IF
import registry as REG
log("=== 自测:结构勘查链路 ===")
class PatchEmbed(nn.Module):
def __init__(self, d):
super().__init__()
self.proj = nn.Linear(9 * 25, d)
def forward(self, x):
B, C, T = x.shape
p = x.unfold(-1, 25, 25).unfold(1, 9, 9) # (B, cols, rows, 9, 25)
p = p.permute(0, 2, 1, 3, 4).reshape(B, -1, 9 * 25)
return self.proj(p)
class Blk(nn.Module):
def __init__(self, d):
super().__init__()
self.attn = nn.MultiheadAttention(d, 4, batch_first=True)
self.ffn = nn.Sequential(nn.Linear(d, 2 * d), nn.GELU(), nn.Linear(2 * d, d))
self.n1 = nn.LayerNorm(d)
def forward(self, x):
h = self.n1(x)
a, _ = self.attn(h, h, h, need_weights=False)
return x + a + self.ffn(h)
class Fake(nn.Module):
def __init__(self, d=32, L=2):
super().__init__()
self.patch = PatchEmbed(d)
self.blocks = nn.ModuleList([Blk(d) for _ in range(L)])
def forward(self, x):
t = self.patch(x)
for b in self.blocks:
t = b(t)
return {"embedding": t}
class NS:
pass
ns = NS()
ns.CBraMod = Fake
import tempfile
with tempfile.TemporaryDirectory() as td:
d = Path(td) / "cbramod"
d.mkdir()
(d / "config.json").write_text(json.dumps(
{"n_chans": 63, "n_times": 250, "patch_size": [9, 25], "d_patch": 32}),
encoding="utf-8")
(d / "ATTRIBUTION.txt").write_text("Source: synthetic\nLicense: MIT\n",
encoding="utf-8")
h = REG.load_base_model("cbramod", models_dir=Path(td), braindecode_models=ns)
assert h.ok, h.error
rec = probe_model("cbramod", h, BS.THINGS_EEG_CHANS)
assert rec["ok"], rec.get("error")
assert rec["n_blocks"] == 2, rec
assert rec["d_model"] == 32, rec
assert rec["blocks"][0]["mixer"]["is_mha"] is True
assert rec["blocks"][0]["out_shape"][-1] == 32, rec["blocks"][0]
log(f" ✓ 勘查:blocks={rec['n_blocks']} d_model={rec['d_model']} "
f"n_tokens={rec['n_tokens']}")
log(f" ✓ mixer 权重键:{list(rec['blocks'][0]['mixer']['shapes'])}")
log("=== 自测通过 ===")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--models", default="all")
ap.add_argument("--bd-version", default="stable",
help="stable | dev(1.8.1.dev184031250)")
ap.add_argument("--out", default="")
ap.add_argument("--self-test", action="store_true")
args = ap.parse_args()
if args.self_test:
self_test()
return
if not args.out:
args.out = str(OUT_ROOT / f"{args.bd_version}.json")
raise SystemExit(run(args))
if __name__ == "__main__":
main()

Xet Storage Details

Size:
17.1 kB
·
Xet hash:
912d212f66cc18b660435cb9d542f65ee477370211411b1d7a2300aa63d3e9df

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.