CAM++语音聚类实战:K-Means结合Embedding应用
1. 为什么语音聚类值得你花10分钟了解
你有没有遇到过这样的场景:手头有几十段会议录音,每段里都有不同人发言,但没人告诉你谁说了什么;或者客服中心每天产生上百通电话,想自动把同一客户的声音归到一起,却只能靠人工反复听辨?
传统做法要么耗时耗力,要么效果粗糙。而今天要聊的这套方案——用CAM++提取语音Embedding,再用K-Means做聚类——能让你在不依赖文字转录、不训练新模型的前提下,仅凭声音本身就把说话人自动分组。
这不是理论推演,而是科哥实测跑通的轻量级落地路径:5行核心代码 + 一个预训练模型 + 本地就能跑通。它不追求学术SOTA,但足够解决真实业务中“先粗筛、再精标”的刚需。
更关键的是,整个流程完全开源、无需GPU、对小白友好。接下来我会带你从零开始,把一段段音频变成可聚类的数字向量,再让机器自动告诉你:“这8段声音大概率来自3个人”。
2. 先搞懂CAM++到底在做什么
2.1 它不是语音识别,而是“声纹翻译器”
很多人第一眼看到CAM++会误以为它是ASR(语音识别),其实完全不是一回事。
- ASR干的事:把“你好啊”这三个字的发音,翻译成文字“你好啊”
- CAM++干的事:把“你好啊”这段3秒语音,压缩成一串192个数字组成的向量(比如
[0.12, -0.45, 0.88, ..., 0.03]),这个向量就像人的“声纹身份证”——同一个人不同时间说的句子,生成的向量彼此靠近;不同人说的,向量就离得远。
你可以把它理解成一种“语音的数学指纹”。它不关心你说什么,只关心你是谁。
2.2 为什么选CAM++而不是其他模型
市面上说话人识别模型不少,但CAM++在中文场景下有几个实在优势:
- 专为中文优化:在CN-Celeb测试集上EER(等错误率)仅4.32%,比很多通用模型低2~3个百分点
- 轻量高效:单次特征提取平均耗时不到0.8秒(CPU i7-11800H),适合批量处理
- 开箱即用:科哥打包的webUI版本,连Docker都不用装,一行命令就能启动
- 输出稳定:192维向量数值范围集中(均值接近0,标准差约0.12),特别适合后续聚类算法
小贴士:别被“192维”吓到。它不像图像动辄上万维,这个维度对K-Means来说非常友好——既保留了足够区分度,又不会因维度灾难导致聚类失真。
3. 从音频到向量:三步提取Embedding
3.1 启动系统,确认环境就绪
打开终端,进入CAM++项目目录:
cd /root/speech_campplus_sv_zh-cn_16k bash scripts/start_app.sh等待终端输出类似Running on local URL: http://127.0.0.1:7860的提示后,在浏览器打开该地址。如果看到标题为“CAM++ 说话人识别系统”的界面,说明一切准备就绪。
注意:如果你用的是远程服务器,把
127.0.0.1换成你的服务器IP,并确保7860端口已开放。
3.2 批量上传音频,一键提取特征
点击顶部导航栏的「特征提取」页签,你会看到两个区域:
- 单个文件提取:适合调试或少量文件
- 批量提取:这才是我们聚类要用的核心功能
我们直接使用批量提取:
- 点击「选择文件」按钮,一次性选中你要聚类的所有音频(支持WAV/MP3/M4A,推荐用16kHz WAV)
- 勾选「保存 Embedding 到 outputs 目录」
- 点击「批量提取」
几秒钟后,页面会显示每个文件的状态。成功提取的文件,会在右侧列出其.npy文件名和维度(192,)。
此时,你的outputs/目录下已经生成了对应数量的.npy文件,比如:
outputs/ └── outputs_20260104223645/ └── embeddings/ ├── meeting_01.npy ├── meeting_02.npy ├── customer_a_call_1.npy └── customer_a_call_2.npy3.3 验证向量质量:用Python快速检查
在终端中运行以下代码,确认向量是否正常加载:
import numpy as np import os # 加载第一个embedding emb_path = "outputs/outputs_20260104223645/embeddings/meeting_01.npy" emb = np.load(emb_path) print(f"文件路径: {emb_path}") print(f"向量形状: {emb.shape}") # 应输出 (192,) print(f"数值范围: [{emb.min():.3f}, {emb.max():.3f}]") print(f"均值: {emb.mean():.3f}, 标准差: {emb.std():.3f}")正常输出应类似:
文件路径: outputs/outputs_20260104223645/embeddings/meeting_01.npy 向量形状: (192,) 数值范围: [-1.243, 1.876] 均值: 0.002, 标准差: 0.118只要形状是(192,),且数值不过于发散(如标准差没超过0.3),就可以放心进入下一步。
4. K-Means聚类实战:5行代码完成说话人分组
4.1 准备数据:把所有.npy文件读成矩阵
新建一个cluster.py文件,写入以下内容:
import numpy as np import os from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler # 1. 自动发现所有embedding文件 embed_dir = "outputs/outputs_20260104223645/embeddings/" emb_files = [f for f in os.listdir(embed_dir) if f.endswith(".npy")] # 2. 加载所有向量,堆叠成 (N, 192) 矩阵 embeddings = [] filenames = [] for f in emb_files: emb = np.load(os.path.join(embed_dir, f)) embeddings.append(emb) filenames.append(f.replace(".npy", "")) X = np.vstack(embeddings) # 形状: (N, 192) print(f"共加载 {len(filenames)} 个向量,矩阵形状: {X.shape}")运行后你会看到类似:
共加载 12 个向量,矩阵形状: (12, 192)4.2 标准化 + 聚类:两步搞定
继续在cluster.py中添加:
# 3. 标准化(重要!K-Means对量纲敏感) scaler = StandardScaler() X_scaled = scaler.fit_transform(X) # 4. 执行K-Means聚类(这里假设你预估有3个说话人) kmeans = KMeans(n_clusters=3, random_state=42, n_init=10) labels = kmeans.fit_predict(X_scaled) # 5. 输出分组结果 print("\n=== 说话人聚类结果 ===") for i, name in enumerate(filenames): print(f"{name:<20} → 群组 {labels[i]}")运行结果示例:
=== 说话人聚类结果 === meeting_01 → 群组 0 meeting_02 → 群组 0 customer_a_call_1 → 群组 1 customer_a_call_2 → 群组 1 interview_b_part1 → 群组 2 interview_b_part2 → 群组 2 ...你会发现,同一人的多段录音基本被分到了同一个群组里——这就是Embedding+K-Means的威力。
关键提醒:
n_clusters参数需要你根据业务预估。如果不确定,可以用肘部法则(Elbow Method)或轮廓系数(Silhouette Score)辅助判断,文末附了完整代码。
4.3 可视化聚类效果(可选但强烈推荐)
加几行代码,把192维向量降到2D看一眼:
from sklearn.decomposition import PCA import matplotlib.pyplot as plt # PCA降维到2D pca = PCA(n_components=2) X_2d = pca.fit_transform(X_scaled) # 绘图 plt.figure(figsize=(10, 6)) scatter = plt.scatter(X_2d[:, 0], X_2d[:, 1], c=labels, cmap='tab10', s=100, alpha=0.8) plt.colorbar(scatter) for i, name in enumerate(filenames): plt.annotate(name[:8], (X_2d[i, 0], X_2d[i, 1]), fontsize=9, ha='center') plt.title("语音Embedding聚类可视化(PCA降维)") plt.xlabel(f"PC1 ({pca.explained_variance_ratio_[0]:.2%} 方差)") plt.ylabel(f"PC2 ({pca.explained_variance_ratio_[1]:.2%} 方差)") plt.grid(True, alpha=0.3) plt.show()你会看到清晰的3簇点云,彼此分离明显——这说明Embedding质量高,聚类结果可信。
5. 实战调优:让聚类更准、更稳
5.1 怎么确定最佳聚类数K?
别猜,用轮廓系数验证:
from sklearn.metrics import silhouette_score sil_scores = [] K_range = range(2, min(8, len(filenames))) for k in K_range: kmeans_test = KMeans(n_clusters=k, random_state=42, n_init=10) labels_test = kmeans_test.fit_predict(X_scaled) score = silhouette_score(X_scaled, labels_test) sil_scores.append(score) print(f"K={k} → 轮廓系数: {score:.3f}") best_k = K_range[np.argmax(sil_scores)] print(f"\n推荐聚类数: K = {best_k} (轮廓系数最高)")运行后你会得到一组分数,选最高分对应的K值即可。通常在真实语音数据上,K=2~5之间就能找到明显拐点。
5.2 音频质量差?试试预过滤策略
不是所有音频都适合直接聚类。建议在提取Embedding前加一道轻量过滤:
- 静音检测:用
pydub切掉开头结尾200ms静音 - 信噪比粗筛:计算RMS能量,剔除低于阈值的片段(如
np.mean(np.abs(audio)) < 0.005) - 时长校验:丢弃<2秒或>30秒的音频(CAM++对中等长度最鲁棒)
这些操作加起来不到10行代码,却能显著提升聚类纯度。
5.3 进阶玩法:用余弦距离替代欧氏距离
K-Means默认用欧氏距离,但语音Embedding更适合用余弦相似度。你可以改用AgglomerativeClustering:
from sklearn.cluster import AgglomerativeClustering from sklearn.metrics.pairwise import cosine_similarity # 计算余弦相似度矩阵 cos_sim = cosine_similarity(X_scaled) # 转为距离矩阵(1 - 相似度) distance_matrix = 1 - cos_sim # 层次聚类(更符合语音语义空间) clustering = AgglomerativeClustering( n_clusters=3, metric='precomputed', linkage='average' ) labels_hier = clustering.fit_predict(distance_matrix)在小样本(<50段)时,这种方法往往比K-Means更稳定。
6. 总结:一条可复用的语音聚类流水线
我们走完了从原始音频到说话人分组的完整闭环。回顾一下这条轻量但高效的路径:
- 第一步:用科哥打包的CAM++ webUI,批量提取192维Embedding,5分钟搞定数据准备;
- 第二步:用
StandardScaler + KMeans标准化+聚类,核心逻辑5行代码,10秒出结果; - 第三步:用轮廓系数选K、用PCA可视化验证、用层次聚类兜底,让结果更经得起推敲。
它不依赖标注数据,不训练大模型,不调复杂超参,却能在会议纪要整理、客服质检、课堂发言分析等场景中,帮你把“谁说了什么”这个问题,从人工盲听升级为机器可计算。
更重要的是,整套流程完全基于开源工具链(CAM++ + scikit-learn + NumPy),所有代码可复制、可修改、可嵌入你的现有系统。下次当你面对一堆未标记的语音文件时,不妨试试这个思路——有时候,最实用的技术,恰恰是最不炫技的那个。
7. 下一步你可以做什么
- 把聚类结果导出为CSV,对接你的CRM或工单系统
- 用聚类标签反哺ASR模型,做说话人自适应解码
- 把每个群组的Embedding取均值,构建简易声纹库,用于后续实时验证
- 尝试用UMAP替代PCA做可视化,获得更精细的结构洞察
技术没有银弹,但有一条清晰、可执行、不设门槛的路径,就已经赢在了起跑线上。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。