一 、 写 在 前 面
做语音转写项目时,说话人聚类(Speaker Clustering / Speaker Diarization)往往比 ASR 本身更容易踩坑。
安静的两个人轮流讲话,效果通常不会太差。但一旦进入真实会议、多人讨论、电话录音或者嘈杂环境,问题马上就暴露出来:
- A 和 B 同时说话时,只识别出了 A;
- 一个人刚说完,下一句话被错误归到了另一个人;
- 同一个人在不同时间段被拆成了多个 Speaker;
- 3 个人的会议被聚类成 4~5 个人;
- 说话时间比较短的人,经常被归到其他人的类别;
- 背景噪声、键盘声、笑声加入后,聚类结果明显变差。
这类问题不能简单归结为"换一个聚类算法"就能解决。
实际工程中,说话人聚类更像是一条完整流水线:

现在一些成熟的说话人识别方案已经开始重点优化"说话人分配"和"重叠语音"问题。例如 pyannote 的新版本在说话人分配和聚类方面引入了 VBx,并提供了 exclusive diarization 来降低声纹时间戳和 ASR 时间戳之间的对齐难度。
下面结合实际工程中比较常见的问题,整理一套比较容易落地的优化思路。
二、问题一:重叠语音导致说话人识别错误
这是最棘手的问题之一。
比如会议中出现:
Speaker A:这个方案我觉得------
Speaker B:我同意,而且还可以------
Speaker A:对,所以我们可以继续......
如果直接把音频切成固定窗口,然后提取 Speaker Embedding,那么中间这一段实际上包含两个说话人的声音。
假设得到:
Embedding = f(A + B)
这个向量既不像 A,也不像 B。
如果直接送进聚类算法,就很容易出现:
A + B → Cluster 1 甚至出现:
A → Cluster 1
B → Cluster 1
A → Cluster 2
最终表现出来就是"串人"。
解决方案:增加 OSD(Overlapped Speech Detection)
不要把所有 Speech Activity 都当成普通单人语音。
可以把语音区域进一步划分:
工程上建议至少保留一个 overlap 标记:
segment =` `{`
`"start":` `12.35,`
`"end":` `14.82,`
`"speaker_count":` `2,`
`"overlap":` `True`
`}`
`
这样后面的聚类就不会把所有音频"一视同仁"。
pyannote 的现有 pipeline 也提供了对 overlap 的处理能力,并且其实现中可以在提取 embedding 时排除重叠区域,从而降低混合语音对 Speaker Embedding 的污染。
三、不要直接使用重叠区域生成声纹向量
这是实际优化中非常有效的一步。
假设:
0s ───────────── 10s
A A A A A
A+B
B B B
不要直接:
embedding = encoder(audio[0:10])`
`
而应该优先选择:
A 的纯净区域 → embedding_A B 的纯净区域 → embedding_B 代码可以简单写成:
import numpy as np`
`def` `remove_overlap(mask, overlap_mask):`
`"""`
` 删除重叠语音帧`
` mask: 当前说话人的语音 mask`
` overlap_mask: overlap mask`
` """`
` clean_mask = mask.copy()`
` clean_mask[overlap_mask >` `0]` `=` `0`
`return clean_mask`
`
然后只使用:
clean_audio = audio[clean_mask]`
`embedding = speaker_encoder(clean_audio)`
`
这样得到的 embedding 通常会比"整段语音直接提取"稳定。
四、问题二:固定窗口切分,容易把两个人切到一起
很多项目一开始会采用:
5 秒一个窗口 10 秒一个窗口 例如:
window_size =` `5`
`for start in` `range(0,` `len(audio), window_size):`
` chunk = audio[start:start + window_size]`
`
这种方法实现简单,但存在明显问题。
假设:
0~3s A
3~5s B
那么第二个窗口:
0~5s = A + B
最终得到的 embedding 就是混合向量。
更合理的方法:滑动窗口 + 重叠
例如:
窗口:2s 步长:0.5s 形成:
0.0 ── 2.0
0.5 ── 2.5
1.0 ── 3.0
1.5 ── 3.5
...
这样可以提高时间分辨率。
pyannote 的 pipeline 同样采用滑动窗口进行 segmentation,并允许控制 segmentation step。
不过这里还有一个关键点:
窗口越短不一定越好。
窗口太短:
0.3s
0.5s
声纹特征不够稳定。
窗口太长:
10s
20s
又容易混入多个说话人。
实际项目通常需要根据录音类型,在"时间分辨率"和"Embedding 稳定性"之间做平衡。
五、问题三:Speaker Embedding 质量直接决定聚类上限
说话人聚类本质上不是直接比较音频,而是在比较:
Speaker Embedding 例如:
A.wav → 0.12, -0.23, 0.44, ... B.wav → 0.18, -0.19, 0.40, ... C.wav → -0.51, 0.31, -0.12, ... 常见的 Speaker Embedding 模型包括:
- ECAPA-TDNN
- x-vector
- ResNet-based Speaker Encoder
- WavLM
- CAM++
- ERes2Net 等
工程上常用余弦相似度:

Python 实现:
import numpy as np`
`def` `cosine_similarity(x, y):`
` x = np.asarray(x)`
` y = np.asarray(y)`
`return np.dot(x, y)` `/` `(`
` np.linalg.norm(x)` `* np.linalg.norm(y)`
`)`
`
例如:
score = cosine_similarity(embedding_a, embedding_b)`
`if score >` `0.75:`
`print("可能属于同一说话人")`
`
但这里不要迷信固定阈值。
不同模型、不同录音设备、不同噪声环境,对应的最佳阈值可能完全不同。
六、问题四:聚类算法本身也会影响结果
最常见的是:
Agglomerative Clustering 即层次聚类。
基本思想比较简单:

例如:
from sklearn.cluster import AgglomerativeClustering`
`model = AgglomerativeClustering(`
` n_clusters=3,`
` metric="cosine",`
` linkage="average"`
`)`
`labels = model.fit_predict(embeddings)`
`
但对于复杂会议场景,仅靠传统 HAC 往往不够。
目前一些较新的 diarization pipeline 已经采用 VBx 等聚类方法改善 speaker assignment 和 counting。pyannote 的 Community-1 就将原有方案中的层次聚类切换到了 VBx。
因此,如果项目长期存在:
Speaker 数量判断错误 Speaker ID 不稳定 相似音色容易串人 可以重点考虑:

逐步进行实验。
七、问题五:复杂场景下不能只看"声纹距离"
真实会议里,经常出现这种情况:

如果只按照 embedding 距离判断,很容易出现:
A → 0
B → 1
A → 2
C → 3
实际上:
0 和 2 都是 A 这时候需要加入时间连续性约束 。
可以设计一个简单的综合评分:

其中:
- (S_{embedding}):声纹相似度
- (S_{temporal}):时间连续性
- (S_{energy}):能量特征
- (\alpha,\beta,\gamma):权重
例如:
def` `speaker_score(`
` embedding_score,`
` temporal_score,`
` energy_score,`
` alpha=0.7,`
` beta=0.2,`
` gamma=0.1`
`):`
`return` `(`
` alpha * embedding_score +`
` beta * temporal_score +`
` gamma * energy_score`
`)`
`
这样可以减少"同一个人被切成多个 Speaker"的情况。
八、问题六:短语音是聚类里的一个大坑
比如:
Speaker A:嗯。
Speaker B:好的。
Speaker A:对。
只有:
0.2~0.5 秒 这样的音频,通常不适合单独建立一个新的 Speaker Cluster。
建议增加最小语音时长:
MIN_DURATION =` `0.8`
`def` `valid_segment(start, end):`
`return` `(end - start)` `>= MIN_DURATION`
`
短语音可以:

这比简单删除短语音更加合理。
九、问题七:同一个人被识别成多个 Speaker
这种情况在长会议里很常见。
例如:
Speaker_0
Speaker_1
Speaker_0
Speaker_3
Speaker_1
实际上可能只有:
A B A C B 解决办法之一是增加 Cluster Merge 。
计算两个 Cluster 的中心向量:

然后计算:

如果:
similarity > merge_threshold:`
` merge(cluster_i, cluster_j)`
`
示例:
def` `cluster_center(embeddings):`
` center = np.mean(embeddings, axis=0)`
`return center / np.linalg.norm(center)`
`def` `should_merge(c1, c2, threshold=0.82):`
`return cosine_similarity(c1, c2)` `>= threshold`
`
这里仍然建议通过真实业务数据进行阈值标定,而不是直接照搬某个项目的数字。
十、复杂环境下增加音频前处理
如果原始音频质量差,再好的聚类模型也很难发挥。
建议增加:

例如统一到:
16 kHz Mono PCM Python 可以简单检查:
import soundfile as sf`
`audio, sr = sf.read("meeting.wav")`
`print("sample rate:", sr)`
`print("shape:", audio.shape)`
`
如果输入设备很多,还要特别注意:
- 麦克风距离;
- 左右声道音量差;
- 自动增益;
- 回声;
- 混响;
- 背景音乐;
- 键盘声;
- 空调噪声。
实际项目中,采集质量往往比后面调几个聚类参数更加重要。
十一、一个比较实用的工程架构
如果让我重新设计一套多人会议说话人识别流程,我会倾向于下面这种结构:

这个架构有一个非常重要的思想:
不要试图让一个模型解决所有问题。
VAD 负责"有没有人在说话"。
OSD 负责"是不是有人同时说话"。
Speaker Encoder 负责"这个声音像谁"。
Clustering 负责"哪些声音属于同一个人"。
Temporal Smoothing 负责"前后是不是同一个人"。
ASR 负责"他说了什么"。
职责拆开之后,定位问题会容易很多。
十二、如何判断到底是哪一环出了问题?
实际排查时,可以建立一张错误分类表:
|--------------------|-------------------------------|
| 问题表现 | 优先检查模块 |
| 安静环境也串人 | Speaker Embedding |
| 同一个人被拆成多个 ID | Clustering |
| 两个人同时讲话只剩一个人 | OSD |
| 短句经常被归错 | Segment / Embedding |
| 前后 Speaker ID 跳变 | Temporal Smoothing |
| 噪声环境明显变差 | Audio Enhancement |
| ASR 字对了但 Speaker 错 | Diarization ↔ ASR Alignment |
| Speaker 数量经常错误 | Clustering / Speaker Counting |
这张表在工程排查中非常有用。
不要看到"说话人错了"就直接重新训练模型。
先把错误定位到具体模块,再决定是调参数、换模型还是重新训练。
十三、评价指标不要只看准确率
说话人聚类最好使用 DER(Diarization Error Rate) 。
通常可以理解为:

其中:
- Miss:应该检测到但没有检测到;
- False Alarm:非语音区域被错误检测;
- Confusion:检测到了,但 Speaker 分错了。
因此:
DER ↓
通常意味着整体 diarization 效果更好。
同时建议单独统计:
Overlap DER Speaker Confusion Miss Rate False Alarm Rate Speaker Counting Error 因为如果只看总 DER,很难知道到底是:
VAD 不行 还是:
聚类不行 或者:
重叠语音处理不行 一些公开 benchmark 也会在不跳过 overlap 的情况下报告 DER,这对于评估复杂会议场景尤其有参考意义。
十四、在实际项目中的推荐优化顺序
如果当前系统已经上线,不建议一上来就大改模型。
可以按照下面的顺序逐步处理:
第一阶段:先把数据弄干净
统一采样率
统一声道
音量归一
第二阶段:解决重叠语音
增加 OSD
排除 overlap embedding
必要时加入 speech separation
第三阶段:优化 Speaker Embedding
可以比较:
ECAPA-TDNN
CAM++
WavLM
ResNet Speaker Encoder
选择真正适合当前数据的模型。
第四阶段:优化聚类
从:
AHC 逐步尝试:
AHC + PLDA VBx
第五阶段:增加后处理
Cluster Merge
Temporal Smoothing
Short Segment Reassignment
Speaker ID Tracking
第六阶段:处理 ASR 对齐
最后再做:

这样最终得到:
00:01:23 Speaker_01:
这个方案我们可以继续讨论一下。
00:01:27 Speaker_02:
我觉得还有一个问题需要考虑。
00:01:30 Speaker_01:
对,这一点确实需要补充。
十五、和实际会议系统结合时的一点经验
在实际的离线会议转写系统中,说话人聚类通常不是独立存在的。
它前面连接:

后面连接:

因此,不能只追求某一个模块的实验室指标。
比如:
Diarization DER 降低了 但如果导致:
ASR 时间戳对不上 最终用户看到的会议纪要仍然可能是错的。
这也是为什么现在一些 diarization pipeline 会提供 exclusive diarization,用来把复杂的重叠说话结果转换成更容易和 STT 时间戳对齐的形式。
在类似熙瑾会悟这样的会议语音场景中,如果最终目标是"谁说了什么",那么评价标准就不应该只停留在声纹聚类本身,而应该关注:

这才是一套完整的工程链路。
十六、最后总结
说话人聚类遇到"重叠语音识别错误、复杂场景效果差",通常不是简单修改一个 clustering threshold 就能解决。
比较推荐的整体思路可以归纳成:

其中最值得优先处理的几个点是:
- 不要让重叠语音直接污染 Speaker Embedding;
- 不要过度依赖固定窗口切分;
- 短语音不要轻易建立新的 Speaker Cluster;
- 聚类后增加 Cluster Merge;
- 通过时间连续性减少 Speaker ID 跳变;
- 使用 DER、Confusion、Overlap Error 等指标分别定位问题;
- 复杂场景下考虑 AHC、PLDA、VBx 等不同聚类方案;
- 最终一定要和 ASR 时间戳进行联合验证。
从工程角度看,真正稳定的说话人识别系统,往往不是"换了一个更大的模型"之后突然变好了,而是把 VAD、OSD、Embedding、Clustering、后处理和 ASR 对齐 这些环节逐个拆开,再针对错误类型逐项优化。
这也是说话人聚类从 Demo 走向实际生产环境时,最值得投入精力的地方。