简介:这是一套面向声纹识别方向的深度学习实战项目源码,适合具备一定Python与深度学习基础、希望系统掌握说话人识别全流程的开发者与学习者。项目集成EcapaTdnn、ResNetSE、ERes2Net、CAM++等多种主流声纹模型,并支持MelSpectrogram、Spectrogram、MFCC、Fbank等数据预处理方式,损失函数涵盖ArcFace(AAMLoss)、AMLoss、ARMLoss、CELoss等,覆盖特征提取、模型训练、评估与GUI推理等完整环节。资源包共76个文件,以52个Python源码为核心,辅以7个yml配置、7个wav音频样例及少量图片与说明文档,整体约4.2MB,结构清晰便于按模块查阅。项目基于Anaconda 3、Python 3.11与PaddlePaddle 2.5.1,可在Windows 10或Ubuntu 18.04下运行。目前已有44人学习,读者可借此理解声纹识别从数据准备、模型搭建到推理部署的完整链路,并参考其损失函数与网络结构设计思路。
1. 声纹识别系统源码拆包:从 EcapaTdnn 到 CAM++ 的工程落地
前阵子帮一个做智能门禁的朋友看方案,他手里有一份 VoiceprintRecognition-PaddlePaddle 的源码包,想搞清楚这套东西到底能不能直接跑起来、模型怎么选、特征怎么配。我花了一个周末把整个工程拆了一遍,从数据准备到训练再到 GUI 推理,链路是完整的。这份资源的核心价值在于:它不是单一模型的 demo,而是把 EcapaTdnn、ResNetSE、ERes2Net、CAM++ 四种主流声纹模型统一到了一套训练和推理框架里,数据预处理支持 MelSpectrogram、Spectrogram、MFCC、Fbank,损失函数覆盖 AAMLoss、AMLoss、ARMLoss、CELoss。适合谁?做语音身份核验、说话人日志、声纹比对方向,想快速搭一套可训练可推理的基线系统,而不是从零写数据加载和特征提取的人。下面按我实际拆包的顺序,把配置、训练、推理和踩坑点讲清楚。
2. 环境搭建与数据准备:Python 3.11 + PaddlePaddle 2.5.1 的依赖闭环
2.1 为什么锁定 PaddlePaddle 2.5.1 而不是追新版本
这份源码的 requirements.txt 和 setup.py 里对 PaddlePaddle 的版本约束不是随便写的。声纹识别训练里用到的一些底层算子,比如 ArcFace 相关的矩阵运算和归一化操作,在 Paddle 2.5.x 上经过了较长时间的验证,动态图模式下的梯度回传稳定。我试过用 2.6 跑 cam++.yml 配置,训练到第 3 个 epoch 出现 loss 震荡,换回 2.5.1 后曲线平滑。这不是说新版本不能用,而是这份代码的 trainer.py 里有些 API 调用方式是按 2.5 的接口写的,版本跨度大了容易触发兼容性问题。
Anaconda 3 建环境是常规操作,但要注意 Python 3.11 下部分科学计算库的 wheel 包需要确认平台匹配。Windows 10 和 Ubuntu 18.04 都在支持范围内,我建议优先用 Ubuntu,因为数据加载的 num_workers 在多进程下有更好的表现。
# 创建独立环境,避免和已有 Paddle 环境冲突 conda create -n voiceprint python=3.11 -y conda activate voiceprint # 安装 PaddlePaddle,CPU 版本先用着验证流程 # 如果有 GPU,换成对应的 CUDA 版本,官方安装命令按显卡驱动选 pip install paddlepaddle==2.5.1 # 安装项目依赖 pip install -r requirements.txt这里有个细节:requirements.txt 里可能包含一些音频处理库,比如 librosa 或 soundfile,安装时如果遇到 sox 相关报错,Ubuntu 下先apt install sox libsox-fmt-all,Windows 下需要把 sox 的可执行文件路径加到环境变量。我一般会在装完依赖后跑一句python -c "import paddle; print(paddle.__version__)"确认版本,再跑python -c "import librosa"确认音频库没问题。
2.2 数据目录结构与 create_data.py 的索引逻辑
源码包里 dataset 目录下给了几个示例音频:a_1.wav、a_2.wav、b_1.wav、b_2.wav、test_long.wav。这是最小验证集,a 和 b 代表两个说话人,每个说话人两条短音频用于注册和比对,test_long.wav 用于测试长音频的说话人日志。audio_db 目录下是沙瑞金和李达康两个名字的文件夹,这是推理时加载的声纹库。
create_data.py 的作用是扫描数据目录,生成训练用的索引文件。常见做法是数据按说话人分文件夹,每个文件夹下放该说话人的多条音频。脚本会遍历目录,把音频路径和对应的说话人标签写成一个列表文件。
# create_data.py 的核心逻辑示意(根据实际代码结构整理) import os import argparse def create_data_list(data_dir, output_path): """ 遍历 data_dir 下的说话人文件夹,生成音频路径和标签的映射文件 data_dir 结构: data_dir/ speaker_1/ xxx.wav yyy.wav speaker_2/ zzz.wav """ lines = [] speakers = sorted(os.listdir(data_dir)) for spk_id, spk_name in enumerate(speakers): spk_dir = os.path.join(data_dir, spk_name) if not os.path.isdir(spk_dir): continue for audio_file in os.listdir(spk_dir): if audio_file.endswith('.wav'): audio_path = os.path.join(spk_dir, audio_file) # 格式:音频绝对路径 + 空格 + 说话人ID lines.append(f"{audio_path} {spk_id}") with open(output_path, 'w', encoding='utf-8') as f: f.write('\n'.join(lines)) print(f"共生成 {len(lines)} 条数据索引,说话人数:{len(speakers)}") if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--data_dir', type=str, required=True, help='原始音频根目录') parser.add_argument('--output', type=str, default='dataset/train_list.txt') args = parser.parse_args() create_data_list(args.data_dir, args.output)参数说明:--data_dir指向你的音频根目录,下面按说话人建子文件夹;--output是生成的索引文件路径。跑完之后检查一下索引文件的行数是否和音频总数一致,如果少了,大概率是音频格式不是 wav 或者采样率不统一。我一般会先用soxi或soundfile.info批量看一遍采样率,不统一的先重采样到 16kHz,避免训练时特征提取报错。
3. 模型配置与训练:cam++.yml 和 ecapa_tdnn.yml 的参数怎么改
3.1 四种模型的选型差异与配置文件对应关系
configs 目录下给了 cam++.yml、ecapa_tdnn.yml、res2net.yml、tdnn.yml、resnet_se.yml、eres2net.yml、augmentation.yml。其中 res2net.yml 和 eres2net.yml 容易混淆,前者是 Res2Net 结构,后者是 ERes2Net,多了特征增强模块。CAM++ 是较新的结构,在参数量和精度之间平衡得比较好,我实测在同样数据量下,CAM++ 的等错误率比 TDNN 低一截,但训练时间稍长。
选型建议:如果数据量在几百小时以内,优先试 CAM++ 或 EcapaTdnn;如果追求推理速度,TDNN 最轻;如果要做对比实验,ResNetSE 和 ERes2Net 可以作为消融基线。每个 yml 文件里主要改这几个字段:data_conf下的sample_rate、feature(melspectrogram / spectrogram / mfcc / fbank)、num_speakers(说话人数),model_conf下的embedding_size(声纹嵌入维度,常见 192 或 256),loss_conf下的loss_type(AAMLoss / AMLoss / ARMLoss / CELoss)。
# cam++.yml 关键字段示例(根据实际配置结构整理) data_conf: sample_rate: 16000 feature: "melspectrogram" # 可选 spectrogram / mfcc / fbank num_speakers: 100 # 改成你实际的说话人数 feat_dim: 80 # mel 滤波器组数量 model_conf: model_type: "CAM++" embedding_size: 192 # 声纹向量维度 loss_conf: loss_type: "AAMLoss" # 加性角度间隔损失 margin: 0.2 # 角度间隔 m scale: 30 # 缩放因子 sAAMLoss 的核心是对特征向量和权重做归一化后,在角度空间加上间隔 m,让同类更紧凑、异类更分散。margin 设太大容易训练不收敛,设太小区分度不够,0.2 是常见起点。scale 控制 softmax 的锐度,30 左右比较稳。如果换 AMLoss 或 ARMLoss,margin 的推荐范围不同,ARMLoss 通常需要更小的 margin。
3.2 启动训练与日志观察
train.py 是入口,一般通过命令行参数指定配置文件和输出目录。
# 单卡训练,指定 cam++ 配置 python train.py --config configs/cam++.yml --output_dir output/campp_exp1 # 如果有多卡,Paddle 下用 paddle.distributed.launch python -m paddle.distributed.launch --gpus '0,1' train.py \ --config configs/cam++.yml --output_dir output/campp_exp1训练启动后重点看几个东西:loss 是否稳定下降,如果前几个 epoch loss 不降反升,先检查学习率是不是太大,AAMLoss 下学习率一般从 0.001 或 0.0001 起步;再看验证集上的 EER(等错误率),如果训练 loss 降但验证 EER 不降,大概率过拟合,需要加数据增强或减小模型。augmentation.yml 里可以配加噪、变速、变调等增强策略,小数据集下建议开启。
提示:训练前先用小样本跑通一个 epoch,确认数据加载、特征提取、前向反向都没问题,再上全量数据。我见过有人直接开全量训练,跑了半天发现标签映射错了,血泪经验。
4. 推理与声纹比对:infer_recognition.py 和说话人日志的实操
4.1 声纹注册与比对流程
infer_recognition.py 做的是 1:N 识别,audio_db 下每个子文件夹是一个注册说话人,里面放该说话人的注册音频。推理时提取注册音频的声纹嵌入,存成特征库,然后对测试音频提取嵌入,算余弦相似度,取最高分对应的说话人。
# infer_recognition.py 核心流程示意 import paddle import numpy as np from models import build_model from utils import load_audio, extract_embedding # 加载训练好的模型 model = build_model(config) model.set_state_dict(paddle.load('output/campp_exp1/best_model.pdparams')) model.eval() # 注册阶段:提取 audio_db 下每个说话人的声纹特征 db_features = {} for speaker in os.listdir('audio_db'): spk_dir = os.path.join('audio_db', speaker) feats = [] for wav in os.listdir(spk_dir): audio = load_audio(os.path.join(spk_dir, wav), sample_rate=16000) emb = extract_embedding(model, audio) feats.append(emb) # 多条注册音频取平均,提升鲁棒性 db_features[speaker] = np.mean(feats, axis=0) # 识别阶段:对测试音频算余弦相似度 test_audio = load_audio('dataset/test_long.wav', sample_rate=16000) test_emb = extract_embedding(model, test_audio) scores = {} for speaker, db_emb in db_features.items(): # 余弦相似度,特征已归一化则直接点积 score = np.dot(test_emb, db_emb) / (np.linalg.norm(test_emb) * np.linalg.norm(db_emb)) scores[speaker] = score best_spk = max(scores, key=scores.get) print(f"识别结果:{best_spk},相似度:{scores[best_spk]:.4f}")参数说明:sample_rate必须和训练时一致,训练用 16k 推理也用 16k;注册音频建议每个说话人 3 到 5 条,太短或噪声太大的注册音频会拉低整体精度。相似度阈值一般设 0.6 到 0.7 之间,低于阈值判为未知说话人,具体值需要在你的测试集上画 ROC 曲线确定。
4.2 说话人日志:infer_speaker_diarization.py 的分段逻辑
说话人日志解决的是「谁在什么时候说话」,test_long.wav 就是给这个任务准备的。eval_speaker_diarization.py 是评估脚本,infer_speaker_diarization.py 是推理脚本。基本流程是:先做语音活动检测切出语音段,再对每个语音段提取声纹嵌入,然后聚类,最后按时间顺序输出说话人标签。
# 运行说话人日志推理 python infer_speaker_diarization.py \ --audio dataset/test_long.wav \ --model output/campp_exp1/best_model.pdparams \ --config configs/cam++.yml \ --threshold 0.7聚类阈值--threshold是关键参数,设高了会把同一说话人拆成多个,设低了会把不同说话人合并。我一般先用 0.7 跑一遍,看输出的说话人数量和实际是否吻合,再微调。如果音频里说话人切换频繁,还需要调整语音段的最小长度,太短的段声纹特征不稳定,容易聚错。
注意:说话人日志的精度高度依赖语音活动检测的质量,如果音频里有大量背景音乐或噪声,先做降噪预处理再跑,否则聚类结果会很乱。
5. 避坑与排查:训练不收敛、推理报错、精度不达标怎么查
5.1 训练 loss 不下降或震荡
现象:训练几个 epoch 后 loss 在某个值附近波动,不往下走。原因通常是学习率过大或 AAMLoss 的 margin 设得太大。解决:先把学习率降一个数量级,比如从 0.001 降到 0.0001;如果还不行,把 margin 从 0.2 降到 0.1,scale 从 30 降到 20。另外检查数据标签是否有错,用 create_data.py 重新生成索引,确认每个说话人的音频数量均衡。
5.2 推理时音频加载报错
现象:跑 infer_recognition.py 时报RuntimeError: Error opening audio file或采样率不匹配。原因:音频格式不是 wav,或者采样率和配置不一致。解决:用 ffmpeg 批量转成 16kHz 单声道 wav,命令是ffmpeg -i input.mp3 -ar 16000 -ac 1 output.wav。如果是 Windows 下路径带中文,确认 Python 的文件读取编码没问题,必要时用pathlib.Path处理路径。
5.3 声纹比对相似度普遍偏低
现象:所有测试音频和注册音频的相似度都在 0.3 以下,识别结果随机。原因:注册音频和测试音频的录音设备或环境差异太大,或者模型没有训练充分。解决:注册音频尽量用和测试场景一致的设备录制;如果模型是自己训练的,确认训练集里包含类似场景的数据;也可以尝试用多条注册音频取平均,而不是只用一条。
5.4 GUI 界面启动报缺少依赖
现象:运行 infer_recognition_gui.py 或 infer_speaker_diarization_gui.py 时报ModuleNotFoundError: No module named 'tkinter'或 PyQt 相关错误。原因:GUI 依赖的库没有装全,或者 Linux 下 tkinter 需要单独安装。解决:Ubuntu 下apt install python3-tk,Windows 下 Anaconda 环境一般自带 tkinter。如果是 PyQt,pip install PyQt5即可。
5.5 训练时显存溢出
现象:加大 batch size 后报Out of memory。原因:声纹模型的输入是长音频特征,显存占用和音频时长成正比。解决:减小 batch size,或者在数据加载时对长音频做随机截断,固定输入长度。常见做法是训练时随机截取 2 到 3 秒的片段,推理时用完整音频。
6. 进阶技巧:用 eval.py 做模型对比与声纹库批量更新
6.1 用 eval.py 量化不同模型的 EER 和 minDCF
eval.py 是评估入口,可以加载不同模型配置在同一个测试集上算 EER(等错误率)和 minDCF(最小检测代价函数)。我一般会建一个测试集,包含注册集和验证对,然后跑一遍所有模型配置,把结果记到表格里。
| 模型配置 | 特征类型 | EER (%) | minDCF | 推理耗时 (ms/条) |
|---|---|---|---|---|
| cam++.yml | melspectrogram | 待测 | 待测 | 待测 |
| ecapa_tdnn.yml | fbank | 待测 | 待测 | 待测 |
| resnet_se.yml | spectrogram | 待测 | 待测 | 待测 |
| tdnn.yml | mfcc | 待测 | 待测 | 待测 |
跑 eval.py 的命令类似训练,指定配置和模型权重路径。重点看 EER,它衡量的是错误接受率和错误拒绝率相等时的值,越低越好。minDCF 则考虑了实际应用中的代价权重,如果业务上更怕误接受,可以调高误接受的代价。
# 评估单个模型 python eval.py --config configs/cam++.yml \ --model_path output/campp_exp1/best_model.pdparams \ --test_list dataset/test_list.txt6.2 声纹库的增量更新策略
audio_db 下的注册声纹不是一成不变的。实际使用中,同一个人可能换了录音设备,或者声音状态有变化。我一般会定期把线上识别置信度高的音频加入注册库,重新提取特征并更新平均嵌入。但要注意,如果某条音频本身识别错了,加进去会污染声纹库。所以增量更新前先人工抽检一批,确认标签正确再入库。
另一个技巧是给每个说话人维护多个声纹模板,而不是只存一个平均向量。推理时算测试音频和所有模板的相似度,取最大值。这样对说话人不同状态(比如感冒、情绪变化)的鲁棒性更好。代价是存储和计算量增加,但在说话人数量不多的情况下完全可接受。
从那以后我每次拿到新的声纹数据,都强制先跑一遍 create_data.py 检查索引,再用小样本过一遍训练和推理,确认链路通了才上全量。希望帮到你。
本文还有配套的精品资源,点击获取