CampPlusEmbedder: embeddings differ from reference CAM++ (fbank not mean-normalized, Hamming window, pooling divisor)
维护者通常 1 天内回复
还没有人认领这个 Issue。
评估
- 难度
- 4/5
- 预计耗时
- 3-5 天
- 新手友好度
- 52/100
- Issue 类型
- 缺陷
- 描述清晰度
- 基本清楚
- 活跃度
- 活跃
- 技术栈
- swift
调研方向
Start with CampPlusEmbedder.embed and the CamPlusPreprocessor and CamPlusPlus model paths, then run the supplied comparison script against the reference CAM++ pipeline. Check the fbank normalization and window settings, followed by segment pooling behavior for partial segments. Done means the shipped embeddings match the reference across the reported clips and pooling lengths.
由索引模型根据 Issue 内容生成。
描述
CampPlusEmbedder (0.17.5, same on main) doesn't produce the embeddings of the CAM++ model it was converted from. I compared it with the PyTorch reference: campplus_cn_common.bin from iic/speech_campplus_sv_zh-cn_16k-common in 3D-Speaker's CAMPPlus, features as in FunASR's extract_feature. On four 16 kHz clips the shipped pipeline reaches a cosine of 0.35 to 0.59 against the reference embedding of the same clip.
Three causes, largest first:
1. The fbank isn't mean-normalized. FunASR subtracts the per-utterance mean from the fbank (feature - feature.mean(dim=0, keepdim=True) in funasr/models/campplus/utils.py), and so does 3D-Speaker (FBank(mean_nor=True)). Neither CoreML model does it: CamPlusPreprocessor ends at log, and CamPlusPlus casts and transposes feats straight into the first conv. The model card says "CAM++ normalizes the fbank internally", but this graph doesn't. Subtracting the mean in embed raises the cosine to 0.989–0.993.
2. Hamming window. The preprocessor's window constant is Hamming (0.08 at the edges). Kaldi.fbank defaults to Povey, which is what FunASR uses. The reference fbank (Povey, mean-normalized) run through CamPlusPlus gives 0.9975–0.9998, so the window costs roughly another 0.005–0.01.
3. Segment pooling divisor. seg_pooling in CAM++ is F.avg_pool1d(kernel_size=100, stride=100, ceil_mode=True), and PyTorch divides the last, partial segment by the frames it actually holds. The converted avg_pool ops have ceil_mode = true and exclude_padding_from_average = false, so they divide by 100. Fed identical features, CoreML matches torch only when the frame count after the stride-2 TDNN is a multiple of 100:
| fbank frames | after TDNN | cosine CoreML vs torch |
|---|---|---|
| 400 | 200 | 0.9999 |
| 450 | 225 | 0.9831 |
| 500 | 250 | 0.9757 |
| 600 | 300 | 0.9999 |
| 650 | 325 | 0.9734 |
The missing normalization also pulls different speakers together. Cosines between macOS say voices, shipped / with mean subtraction / torch reference:
| pair | shipped | mean subtracted | reference |
|---|---|---|---|
| Daniel – Thomas | 0.778 | 0.505 | 0.509 |
| Anna – Samantha | 0.836 | 0.741 | 0.713 |
| Samantha – Thomas | 0.479 | 0.399 | 0.366 |
We ran into this in a cross-meeting speaker-recognition benchmark on AMI, where CampPlusEmbedder scored different people as the same speaker.
Fixes, in order of impact: subtract the per-utterance fbank mean (in the preprocessor graph or in embed), use a Povey window, and divide the last pooling segment by its real length. I haven't tested whether exclude_padding_from_average = true does that for the ceil-mode overhang.
Comparison script
Needs torch torchaudio coremltools numpy soundfile, the two .mlmodelc folders from FluidInference/campplus-coreml, campplus_cn_common.bin, and DTDNN.py + layers.py from 3D-Speaker under speakerlab/models/campplus/.
import sys, glob, numpy as np, torch, soundfile as sf, coremltools as ct
import torchaudio.compliance.kaldi as K
sys.path.insert(0, '.')
from speakerlab.models.campplus.DTDNN import CAMPPlus
torch.set_grad_enabled(False)
ref = CAMPPlus(feat_dim=80, embedding_size=192)
ref.load_state_dict(torch.load('campplus_cn_common.bin', map_location='cpu')); ref.eval()
pre = ct.models.CompiledMLModel('CamPlusPreprocessor.mlmodelc', compute_units=ct.ComputeUnit.CPU_ONLY)
net = ct.models.CompiledMLModel('CamPlusPlus.mlmodelc', compute_units=ct.ComputeUnit.CPU_ONLY)
cos = lambda a, b: float(np.dot(a, b) / np.linalg.norm(a) / np.linalg.norm(b))
def ref_feats(w, window='povey'):
f = K.fbank(torch.from_numpy(w)[None], num_mel_bins=80, sample_frequency=16000, dither=0.0, window_type=window)
return f - f.mean(0, keepdim=True)
ref_emb = lambda f: ref(f[None])[0].numpy()
cml_emb = lambda f: net.predict({'feats': np.asarray(f, np.float32)[None]})['embedding'].reshape(-1)
fluid_feats = lambda w: pre.predict({'waveform': (w * 32768).astype(np.float32)[None]})['features'][0] # as CampPlusEmbedder
for path in sorted(glob.glob('audio/*.wav')): # 16 kHz mono
w, _ = sf.read(path, dtype='float32')
r, ff = ref_emb(ref_feats(w)), fluid_feats(w)
print(path,
'shipped', round(cos(cml_emb(ff), r), 4),
'mean subtracted', round(cos(cml_emb(ff - ff.mean(0, keepdims=True)), r), 4),
'reference fbank', round(cos(cml_emb(ref_feats(w).numpy()), r), 4))
w, _ = sf.read(sorted(glob.glob('audio/*.wav'))[0], dtype='float32')
f = ref_feats(w)
for T in (400, 450, 500, 600, 650):
g = f[:T] - f[:T].mean(0, keepdim=True)
print(T, round(cos(cml_emb(g.numpy()), ref_emb(g)), 4))
- 主要语言
- Swift
- 星标
- 3k
- 派生
- 451
- 平均合并
- 2 天 6 小时
- 30 天内合并 PR
- 81
环境准备
这个项目没有提供开发容器、Dockerfile 或贡献指南,环境需要你自己搭建:先看它的 README,通用步骤见我们的新手贡献指南。
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
FluidInference/FluidAudio 的其他 Issue
-
难度 4/5 3-5 天 新手友好度 52/100
FluidInference/FluidAudio#1112 ·
维护者通常 1 天内回复
-
ASR long-form: sentence-final period lost when a window ends inside the pause before the next sentence (both paths)可能已有人在做 @maboa 今天认领。 未关闭
难度 4/5 3-5 天 新手友好度 55/100
FluidInference/FluidAudio#1110 ·
维护者通常 1 天内回复
-
难度 4/5 3-5 天 新手友好度 35/100
FluidInference/FluidAudio#1105 ·
维护者通常 1 天内回复
-
难度 5/5 一周以上 新手友好度 12/100
FluidInference/FluidAudio#1007 · 2 条评论 ·
维护者通常 1 天内回复
-
Model Support Requests可能已有人在做 @BrandonWeng 于 461 天前认领。 未关闭enhancement question
FluidInference/FluidAudio#49 · 92 条评论 · 6 个 reaction · 已指派 1 人 ·
维护者通常 1 天内回复
查看 FluidInference/FluidAudio 的全部 Issue
相似的 Issue
-
feat: 앱 정보에 개인정보처리방침과 고객지원 연결可能已有人在做 @xrpjwk8 今天认领。 未关闭
难度 2/5 1-3 小时 新手友好度 62/100
DeveloperAcademy-POSTECH/2026-C6-A13-Hear-I-am#120 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 76/100
swiftlang/swift-java#955 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 78/100
michaelnisi/hanstholm#246 · 1 条评论 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 65/100
jaywcjlove/quick-rss#448 ·
-
难度 2/5 1-3 小时 新手友好度 76/100
VictorKildahl/cra#205 · 1 条评论 ·
维护者通常 1 天内回复