File size: 9,201 Bytes
fac7535 | 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 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | #!/usr/bin/env python3
"""
classify_models.py
==================
根据 checkpoint 里的参数名 / 形状,判断 .ckpt 模型属于哪种分离模型架构
(bs_roformer / mel_band_roformer / scnet / bandit / … )。
判据来自 E:\\vs\\pymss-core\\pymss_core\\modules 下的参考实现:
bs_roformer.py / mel_band_roformer.py -> band_split(BandSplit) + mask_estimators(MaskEstimator) + layers(Transformer)
bands.py BandSplit : self.to_features = [Sequential(RMSNorm(gamma), Linear)]
bands.py MaskEstimator : self.to_freqs = [Sequential(MLP, GLU)]
transformer.py Attention : self.norm(gamma) / to_qkv / to_gates / to_out
scnet.py SCNet : self.separation_net / SDlayer / dp_modules / encoder / decoder
bandit_v2/bandit.py : self.band_split(norm_fc_modules) / tf_model(seqband) / mask_estim[stem]
bs_roformer 与 mel_band_roformer 的判别点(两者 band_split / mask_estimators 结构相同):
* BSRoformer.final_norm = RMSNorm(dim) -> state_dict 里有 "final_norm.gamma"
* MelBandRoformer.final_norm = nn.Identity() -> 没有任何 final_norm 参数
* BSRoformer 常用 use_shared_bias -> 有 "linear_62_bias_0" / "linear_64_bias_0"
* freq 带宽分布:BS 默认 DEFAULT_FREQS_PER_BANDS,mel 版本是按 mel 滤波器分带
用法
----
python classify_models.py # 扫描 ./ckpt
python classify_models.py --dir <ckpt目录>
python classify_models.py --json out.json
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
from collections import defaultdict
import torch
import model_io
HERE = os.path.dirname(os.path.abspath(__file__))
# pymss-core 里 BS-RoFormer 的默认频带划分(单声道)
BS_DEFAULT_FREQS = (2,) * 24 + (4,) * 12 + (12,) * 8 + (24,) * 8 + (48,) * 8 + (128, 129)
def idx_set(pattern: str, keys) -> set[str]:
out = set()
for k in keys:
m = re.search(pattern, k)
if m:
out.add(m.group(1))
return out
def shape_of(sd, pattern: str):
for k, v in sd.items():
if re.search(pattern, k):
return tuple(v.shape)
return None
def classify(sd: dict) -> dict:
all_keys = list(sd)
# 别名键(coreml.source.* / onnx.source.*)保留了原始 PyTorch 名。有些模型
# (例如 drumsep)的 mask estimator 只以别名形式存在,因此判定时使用全量键。
keys = all_keys
info: dict = {}
# ---- Bandit / Bandit v2 :band_split(norm_fc_modules) + tf_model(seqband) ----
if any("norm_fc_modules" in k for k in keys) or any(k.startswith("tf_model") for k in keys):
stems = sorted({m.group(1) for k in keys
if (m := re.match(r"mask_estim\.([A-Za-z0-9_]+)\.", k))})
info.update(
family="bandit",
subtype="bandit_v2" if "band_split.norm_fc" in " ".join(keys) else "bandit",
bands=len(idx_set(r"band_split\.norm_fc_modules\.(\d+)\.", keys)),
stems=stems,
stft_nfft=2048,
)
return info
# ---- SCNet ----
if any(k.startswith(("separation_net", "model.separation_net")) for k in keys) or \
any("SDlayer" in k for k in keys):
info.update(family="scnet",
encoders=len(idx_set(r"(?:^|[./])encoder\.(\d+)\.", keys)),
decoders=len(idx_set(r"(?:^|[./])decoder\.(\d+)\.", keys)))
return info
# ---- ConvNeXt U-Net generator(GAN 类降噪器)----
if any("generator.trainable" in k for k in keys):
info.update(family="convnext_gan_generator",
convnext_blocks=len(idx_set(r"convnext\.(\d+)\.", keys)),
u_blocks=len(idx_set(r"u_blocks\.(\d+)\.", keys)),
d_blocks=len(idx_set(r"d_blocks\.(\d+)\.", keys)),
snake=any(k.endswith(".alpha") for k in keys))
return info
# ---- ReDimNet 说话人编码器 ----
if any(k.startswith("backbone.stage") for k in keys):
info.update(family="redimnet",
stages=len(idx_set(r"backbone\.stage(\d+)\.", keys)))
return info
# ---- 小型打分头 ----
if any(k.startswith(("scorer.", "temporal.")) for k in keys):
info.update(family="scoring_head",
layers=len(idx_set(r"temporal\.layers\.(\d+)\.", keys)))
return info
# ---- Roformer 家族 ----
if any("band_split" in k for k in keys) and \
any(k.startswith("mask_est") or "mask_estimator" in k for k in keys):
bands = (idx_set(r"band_split\.(\d+)\.linear\.weight", keys)
or idx_set(r"band_split\.to_features\.(\d+)\.", keys))
n_bands = len(bands)
# 每带 linear 的 (out_features=dim, in_features=复数频点×声道)
widths = []
for k in keys:
m = re.fullmatch(r"band_split\.(\d+)\.linear\.weight", k)
if m:
widths.append((int(m.group(1)), sd[k].shape[0], sd[k].shape[1]))
if not widths:
for k in keys:
m = re.fullmatch(r"band_split\.to_features\.(\d+)\.1\.weight", k)
if m:
widths.append((int(m.group(1)), sd[k].shape[0], sd[k].shape[1]))
widths.sort()
per_band = [w // 2 for _, _, w in widths] # 立体声 → /2 得单声道频点数
final_norm = any("final_norm" in k for k in keys)
shared_bias = any("linear_62_bias_0" in k or "linear_64_bias_0" in k for k in keys)
branch = any(".branch." in k for k in keys)
depth = len(idx_set(r"^(?:layers|blk)\.(\d+)\.", keys))
stems_raw = (idx_set(r"(?:mask_estimators?|mask_est)\.(\d+)\.", keys)
| idx_set(r"mask_estimators?_(\d+)_", keys))
heads = shape_of(sd, r"to_gates\.weight$")
info.update(
family="bs_roformer" if final_norm else "mel_band_roformer",
bands=n_bands,
depth=depth,
dim=widths[0][1] if widths else None,
heads=heads[0] if heads else None,
stems=len(stems_raw),
final_norm=final_norm,
shared_bias=shared_bias,
attention_branch=branch,
freqs_per_band=per_band,
freq_layout="BS-DEFAULT(stereo)" if per_band == [2 * f for f in BS_DEFAULT_FREQS]
else "BS-DEFAULT(mono)" if per_band == list(BS_DEFAULT_FREQS)
else "custom",
)
return info
info.update(family="unknown")
return info
def describe(info: dict) -> str:
fam = info.get("family", "?")
if fam in ("bs_roformer", "mel_band_roformer"):
bits = [f"{info['bands']} bands", f"dim={info['dim']}", f"depth={info['depth']}",
f"{info['stems']} stem(s)", f"freq={info.get('freq_layout')}"]
if info.get("shared_bias"):
bits.append("shared_bias")
if info.get("attention_branch"):
bits.append("attn-branch")
return " ".join(bits)
if fam == "bandit":
return f"n_bands={info.get('bands')} stems={info.get('stems')}"
if fam == "scnet":
enc, dec = info.get("encoders") or 0, info.get("decoders") or 0
return f"{enc} enc / {dec} dec" if enc else f"{dec} decoder(s)"
if fam == "convnext_gan_generator":
return (f"convnext={info.get('convnext_blocks')} u_blocks={info.get('u_blocks')} "
f"d_blocks={info.get('d_blocks')} snake={info.get('snake')}")
if fam == "redimnet":
return f"stages={info.get('stages')}"
if fam == "scoring_head":
return f"temporal_layers={info.get('layers')}"
return ""
def main() -> int:
ap = argparse.ArgumentParser(description="识别 ckpt 的分离模型类型")
ap.add_argument("--dir", default=os.path.join(HERE, "..", "raw"), help="模型目录(safetensors / ckpt)")
ap.add_argument("--json", default=None, help="把结果另存为 JSON")
args = ap.parse_args()
paths = []
for root, _d, files in os.walk(args.dir):
for n in files:
if n.endswith((".ckpt", ".safetensors")):
paths.append(os.path.join(root, n))
if not paths:
print("没有找到 .ckpt / .safetensors")
return 1
results = {}
print(f"{'模型':<34}{'类型':<22}关键参数")
print("-" * 108)
for p in sorted(paths):
sd = model_io.load_any(p)
info = classify(sd)
name = os.path.splitext(os.path.basename(p))[0]
results[name] = {"type": info.get("family"), **{k: v for k, v in info.items() if k != "family"}}
print(f"{name:<34}{info.get('family', '?'):<22}{describe(info)}")
del sd
if args.json:
with open(args.json, "w", encoding="utf-8") as f:
json.dump(results, f, indent=2, ensure_ascii=False)
print(f"\n已写入 {args.json}")
return 0
if __name__ == "__main__":
sys.exit(main())
|