简介:基于PaddlePaddle的深度学习声纹识别系统完整工程,面向语音技术开发者、算法工程师及相关专业学生,可用于说话人识别、声纹比对和说话人日志等任务的落地实践。项目集成了EcapaTdnn、ResNetSE、ERes2Net、CAM++等多种主流声纹模型,支持MelSpectrogram、Spectrogram、MFCC、Fbank等多种数据预处理方式,损失函数以ArcFace Loss加性角度间隔损失为核心,同时兼容AMLoss、ARMLoss、CELoss,便于研究者横向对比与算法调优。运行环境为Anaconda 3、Python 3.11、PaddlePaddle 2.5.1,兼容Windows 10与Ubuntu 18.04,可跨平台快速复现。压缩包共76个文件,以52个Python源码为主体,覆盖数据制作、训练、评估、特征提取、推理及GUI交互等完整流程,另有7个wav音频样本、7个yml模型配置、2个Markdown文档及依赖清单,整体仅4.2MB,轻量易部署,已有44人学习浏览。配套项目文档与目录结构清晰,开箱即用,适合在此基础之上快速开展声纹识别二次开发与学术实验。
1. 拆一个完整的声纹识别工程:从EcapaTdnn到CAM++都能跑
声纹识别近几年在企业声纹库、智能客服和安防日志分析里落地不少,但很多开源示例只给你一个训练脚本,换数据就崩。这次拆的这套基于Python和PaddlePaddle 2.5.1的声纹识别系统,把训练、评估、特征提取、说话人日志和GUI推理都串起来了,模型侧覆盖EcapaTdnn、ResNetSE、ERes2Net、CAM++四种主流backbone,损失函数支持ArcFace、AMSoftmax等,数据预处理兼容MelSpectrogram、Spectrogram、MFCC、Fbank。对想快速验证深度学习声纹方案的开发者和做说话人日志需求的技术人员来说,这个工程可以直接当底座改。
2. 模型与损失函数:四个backbone、四种下采样思路怎么选
2.1 从ECAPA-TDNN到CAM++,配置文件里的一行切换
这个项目把模型封装在models/目录下,配置文件里一个model字段决定用哪个backbone。我在实际项目中一般把EcapaTdnn当默认基线,因为它把一维卷积、SE-Res2Block和多尺度特征融合组合在一起,在参数量和精度之间平衡最好。ResNetSE则是把ResNet的残差结构和Squeeze-and-Excitation通道注意力搬到语音特征上,对短语音更稳。ERes2Net通过通道分块内部的残差连接进一步提取细粒度特征,适合音色差异极小的注册场景。CAM++来自近年工业界开源工作,本项目也做了兼容,它用了更细的多尺度聚合策略,在同参数级别下通常能比ECAPA再高一两个点。
先看模型选择的配置写法:
# configs/ecapa_tdnn.yml model: EcapaTdnn # 可选: EcapaTdnn / ResNetSE / ERes2Net / CAM++ feature_method: MelSpectrogram loss: AAMLoss # 对应ArcFace embedding_size: 192 num_classes: 8000模型名称必须与models下的类名一一对应,feature_method和loss也各自来自configs/下的yml片段。换模型时我一般直接拷贝一份配置文件,只改model字段,再检查预训练权重路径,避免污染原始配置。需要说明的是,四个模型的输入维度都依赖feature_method的n_mels或n_fft参数,所以model可以换,特征维度不要乱动,否则前向传播会报shape mismatch。
除了配置,训练循环里加载模型的核心逻辑类似这样:
# train.py 中模型初始化的简化逻辑 import paddle from models import build_model from configs import load_config config = load_config("configs/ecapa_tdnn.yml") model = build_model( model_name=config["model"], num_classes=config["num_classes"], embedding_size=config["embedding_size"] )这里build_model内部根据model_name去实例化对应类,num_classes对应训练集说话人数,embedding_size是输出的说话人向量维度。如果训练集说话人数变化,要同步更新num_classes;如果只是做推理,可以加载训练好的参数再替换最后一层。embedding_size影响度量学习时的向量维度,192是常见选择,实际项目中如果类别特别多,可以提到256。
表2-1 四个模型对比
| 模型 | 核心机制 | 参数量级 | 适合场景 |
|---|---|---|---|
| EcapaTdnn | 一维卷积+SE-Res2Block+多尺度融合 | 中等 | 通用基线、中文短语音 |
| ResNetSE | 残差网络+SE注意力 | 中等偏小 | 低资源场景、在线推理 |
| ERes2Net | 通道分块残差+多尺度 | 较大 | 高精度注册、小类别数区分 |
| CAM++ | 多尺度特征聚合 | 较大 | 长时音频、跨信道场景 |
四种模型的共同点是都输出定长embedding,这是声纹识别的基础逻辑:训练时用embedding和说话人标签算损失,推理时提取embedding做余弦相似度。不同点在于特征金字塔的构建方式,ECAPA用多层特征相加,ERes2Net在通道维度内做层次拆分,CAM++在多尺度聚合上更激进,这也导致它们在不同信噪比、不同录音设备下的表现有差异。
2.2 ArcFace加了角度间隔,AMSoftmax和ARMLoss为什么还留着
ArcFace在项目里对应AAMLoss,它的思想是先把特征向量和权重都归一化,再在θ上加一个角度间隔m。相比余弦间隔,角度间隔直接作用在夹角上,对难样本的梯度影响更明确。实现上,Paddle的动态图写法通常是先计算cosθ,再用arccos还原角度,加m后重新求cos。由于加角度的计算会放大梯度,Paddle框架里一般用paddle.nn.functional里的组合算子实现,避免手写arccos导致反向传播不稳定。
项目里同时保留了AMLoss、ARMLoss和CELoss。AMSoftmax也叫AMLoss,用的是余弦间隔而非角度间隔,实现更简单、收敛更稳定;ARMLoss通过调整margin的尺度缓解类别数很大的情况;CELoss则是普通softmax交叉熵,声纹效果最弱,一般用来做基线对比。我的习惯是先在小的子集上用CELoss验证数据链路,再切到AAMLoss刷精度。
损失函数的配置在yml里改一个字段就行:
loss: AAMLoss # 也可换成 AMLoss / ARMLoss / CELoss margin: 0.2 # AAMLoss中的m,角度间隔 loss_scale: 32.0 # 增大数值稳定性的缩放因子margin通常取0.2到0.35,数值越大对同类特征压缩越强,但过大会导致训练不收敛。loss_scale是Paddle的混合精度设计里常见的缩放因子,如果不开AMP,这个参数其实不参与计算。调参时我一般先固定backbone,在验证集EER相差不大时,优先选margin小的配置,因为鲁棒性更好。
提示:换了损失函数后,模型导出的推理脚本不需要改,损失只影响训练时梯度,推理阶段都是提取embedding后做余弦相似度。
3. 特征提取与数据准备:直接喂原始wav还是先落盘
3.1 create_data.py与数据列表的约定
在拿模型跑起来之前,数据准备往往是最大的坑。这个工程从文件列表看是一个比较标准的数据管线:create_data.py负责扫描音频目录、生成训练列表,extract_features.py负责把wav转为特征并保存。常见做法是准备类似下面的目录结构:
dataset/ ├── speakerA/ │ ├── 001.wav │ └── 002.wav └── speakerB/ ├── 001.wav └── 002.wav然后执行数据列表创建命令:
python create_data.py \ --data_dir dataset \ --output_dir data_list \ --format wav这个脚本会扫描每个子目录作为说话人ID,生成三元组的tsv或json列表,记录音频路径、说话人ID和采样率。它会自动过滤掉时长小于0.5秒的音频,避免静音段污染特征。跑完后在data_list/下面会看到train_list.txt和val_list.txt,每行是音频路径\t说话人ID。如果你的目录命名不是“以说话人ID作为文件夹名”,脚本大概率会报错或者把相同说话人切成两个ID,这条约束要最先确认。
3.2 MelSpectrogram、Fbank、MFCC与Spectrogram的取舍
extract_features.py提取的四种特征,本质上是同一段语音的不同观察方式。Spectrogram直接做短时傅里叶变换,保留最多原始信息,但对噪声敏感;MelSpectrogram把频谱映射到人耳感知的Mel刻度,信息量适中,是EcapaTdnn最常用的输入;Fbank在Mel基础上取log能量,丢掉相位信息,和MelSpectrogram非常像,区别只在滤波器的归一化方式;MFCC还要再做一次DCT去相关,压缩到13维或39维,维数最低,轻量但会丢细节。对声纹任务来说,Fbank和MelSpectrogram是首选,MFCC更多用于早期GMM-UBM系统,Spectrogram则适合做数据增强对比。
配置文件的写法如下:
# configs/augmentation.yml 与主配置中的特征参数 feature_method: Fbank sample_rate: 16000 n_mels: 80 frame_length: 25 # 毫秒 frame_shift: 10 # 毫秒 dither: 1.0 # 特征抖动系数,增加鲁棒性sample_rate统一16kHz,这是声纹模型最常用的采样率,电话信道8kHz音频需要重采样后再输入;n_mels取值范围40到80,80是当前深度学习说话人识别的主流配置,本仓库中也对应Fbank的滤波器个数;frame_length和frame_shift决定了每帧的时间跨度和步进,25ms/10ms是ASR和声纹通用的帧参数。
如果音频原始采样率是48kHz,文件中的resample逻辑会先降到16kHz。如果你换了模型但保留同一个特征配置,建议检查extract_features.py里的预加重系数pre_emphasis是否一致,这个参数通常取0.97,它会影响高频分量的相对幅度。
特征提取指令可以按需预提取:
python extract_features.py \ --config configs/ecapa_tdnn.yml \ --data_list data_list/train_list.txt \ --save_dir features/train预提取的好处是训练时不再实时算FFT,GPU利用率更高。坏处是磁盘占用大,抽样显示80维Fbank的float32特征,一小时音频约占用540MB,如果总时长过长,建议用lmdb格式或者On-The-Fly提取。项目默认的训练流程也会在dataloader里动态计算特征,我通常先小规模预提取验证,再切回实时提取。
表3-1 四种特征维度与适用对比
| 特征 | 维度 | 计算量 | 声纹效果 | 适用策略 |
|---|---|---|---|---|
| Spectrogram | 257以上 | 低 | 中等,噪声敏感 | 与增强模型配合 |
| MelSpectrogram | 80 | 中 | 高 | 推荐EcapaTdnn |
| Fbank | 80 | 中 | 高 | 本工程最常用 |
| MFCC | 39 | 高 | 中下 | 传统系统兼容 |
特征参数不是越大越好。比如n_mels设为128,维度更高不代表精度一定涨,反而会让模型更容易过拟合到信道噪声上。我在实际对比中发现80维Fbank对不同麦克风的泛化最好,这也是目前VoxCeleb基线的主流配置。
4. 训练与评估:读懂配置、跑通命令、看会指标
4.1 训练入口与关键超参
这个工程把训练逻辑整体放在train.py,内部通过configs目录下的yaml读取模型、数据、优化器、学习率等信息。以ecapa_tdnn.yml为例,配置文件中除了模型字段,还要关注batch_size、learning_rate、epoch和验证频率。常见训练设置是初始学习率0.001,batch_size按显存调整到32或64,配合warmup和余弦退火。既然是PaddlePaddle 2.5.1版本,优化器用Adam或者SGD都可以,我一般对这类度量学习任务用Adam加固定weight_decay,比SGD更容易稳定。
启动训练的命令是:
python train.py --config configs/ecapa_tdnn.yml --gpus 0--gpus 0指定单卡训练,工程内部通过paddle.distributed封装多卡训练,如果改成--gpus 0,1,需要额外配置--save_dir和--resume等参数。训练过程中,train.py每完成一个epoch会在验证集上抽一批音频计算准确率,同时保存最新的checkpoint。模型参数、优化器状态、epoch数和当前学习率都会打包进.pdparams和.pdopt文件,断点续训时用--resume checkpoints/epoch_10/恢复,不会丢学习率信息。
4.2 训练日志里到底该看哪些数
日志一般长这样:
[Train] epoch 20/100, loss: 0.214, acc: 0.963, lr: 0.00031 [Val] epoch 20/100, acc: 0.972, eer: 0.0321这里的acc是闭集分类准确率,eer是等错误率,它是声纹验证的核心指标,表示把FRR和FAR调成相等时的数值。EER越低越好,0.03意味着错误接受率和错误拒绝率在3%左右,已经可以支撑门禁类业务。训练日志中的acc容易虚高,因为训练集说话人已知,模型只要学到分类边界就好;真正决定线上效果的是EER,所以每次epoch结束后在验证集上计算EER是必要的。
4.3 eval.py算出来的指标怎么用
单独跑评估用下面的命令:
python eval.py \ --config configs/ecapa_tdnn.yml \ --resume checkpoints/epoch_50/ \ --test_list data_list/val_list.txt这里--resume传的是checkpoint目录,脚本会加载最新参数并计算整个验证集的EER和ACC。eval.py同时会输出一个阈值参考,比如在某个阈值下FAR和FRR交叉,这个阈值可以直接写入后续推理脚本的threshold参数。如果不传--test_list,默认使用训练时划分的val_list,但为了对比不同模型的稳定性,我一般会额外构造一个跨设备采集的测试集,语音时长覆盖2秒到10秒,然后看EER的方差,比单点精度更能反映真实场景。
表4-1 train.py/eval.py常用参数参考
| 参数 | 含义 | 建议 |
|---|---|---|
| config | 主配置路径 | 每次实验拷贝一份 |
| gpus | 参与训练的GPU编号 | 单卡时写0 |
| resume | 断点续训目录 | 切换数据集时不要续训 |
| test_list | 测试列表 | 与训练列表说话人不重叠 |
| threshold | 推理阈值 | 由eval.py输出后回填 |
有一个容易踩的坑:训练时打乱音频,没有按照说话人分组,会导致同一个人相邻音频被分到训练和验证集,EER虚低。我会用create_data.py生成列表时按说话人哈希划分,而不是随机打乱。这个工程如果默认是全局随机划分,建议改成按说话人分组的split,否则后续部署很容易出现“验证集很准、线上很差”的现象。
5. 推理、GUI与说话人日志:把模型变成可用服务
5.1 单条音频与对比推理:确认两个声音是不是同一个人
推理脚本infer_recognition.py负责单条音频的注册和验证。它的作用是把一条新的wav提取成embedding,读取项目自带的audio_db下音频库里的声纹注册表,然后计算余弦相似度,超过阈值就判定为同一个人。命令行用法类似:
python infer_recognition.py \ --config configs/ecapa_tdnn.yml \ --resume checkpoints/epoch_50/ \ --audio_path test_long.wav \ --audio_db audio_db \ --threshold 0.62test_long.wav是项目自带的长音频测试样例,audio_db是存放注册音频的目录,里面“沙瑞金”“李达康”这类人名子目录就是注册说话人。threshold来自eval.py输出的等错误率阈值,通常设在0.55到0.7之间。如果输入音频过长,脚本会先按VAD检测语音段,再对每段提取embedding并取平均,避免静音段拉偏向量方向。
如果要绕过音频库直接比两条音频,项目里还有infer_contrast.py,它适合快速验证音色是否一致:
python infer_contrast.py \ --config configs/ecapa_tdnn.yml \ --resume checkpoints/epoch_50/ \ --wav1 a_1.wav \ --wav2 b_1.wav脚本输出的是两条语音embedding的余弦相似度,没有阈值判断,只给你一个0到1之间的分数。这个分数在调试阈值和测试数据增强效果时很直观,a_1.wav和b_1.wav是项目自带的对比样本,可以直接用来确认链路。
5.2 说话人日志:把“谁在什么时候说话”输出出来
说话人日志是声纹识别之外更接近业务的功能,infer_speaker_diarization.py负责这段长音频里有几个说话人以及各自出现的时间段。实现流程是先把长音频切成短段,去掉静音,再对短段依次提取embedding,用聚类算法把相同说话人的embedding归到一起,最后输出带时间戳的说话人段。这在电话录音质检、会议纪要场景中很有用。
执行日志推理的命令是:
python infer_speaker_diarization.py \ --config configs/ecapa_tdnn.yml \ --resume checkpoints/epoch_50/ \ --audio_path test_long.wav \ --output_dir diarization_result \ --num_speakers 2--num_speakers是可选项。如果提前知道录音里有几个人,手动指定后聚类结果会更稳定;如果不知道,脚本会用silhouette score估计最优人数。项目还提供了eval_speaker_diarization.py用于计算日志结果的DER指标,DER越低说明时间边界和说话人归属都越准。DER的优化重点往往不在聚类,而在VAD边界检测,设置vad_min_silence_duration过小会把停顿也当作说话人切换。
执行完会生成一个带时间戳的TSV文件,字段含义如下:
| 字段 | 含义 | 示例 |
|---|---|---|
| start_time | 说话段起始时间(秒) | 1.24 |
| end_time | 说话段结束时间(秒) | 3.86 |
| speaker | 聚类后的说话人ID | speaker_0 |
| score | 该段与所属聚类的相似度均值 | 0.81 |
5.3 GUI演示:让非技术人员也能验证效果
infer_recognition_gui.py把上面的推理包装成了可视化界面,启动命令:
python infer_recognition_gui.py \ --config configs/ecapa_tdnn.yml \ --resume checkpoints/epoch_50/界面通常包含“注册音频”和“识别音频”两个按钮,注册时把wav文件关联到一个名字,识别时显示Top1结果和相似度。这里的底层逻辑和命令行完全一样,只是把阈值判断、特征提取、向量比对都封装在GUI后端。演示时有一个容易忽略的细节:GUI使用的麦克风录音采样率可能是44.1kHz,而模型需要16kHz,脚本里如果没有重采样,识别结果会明显下降。我在接外部演示环境时,会先让GUI打印输入音频的采样率,再决定加不加librosa.resample。
注意:GUI和命令行推理共用同一个
infer_utils.py里的特征提取函数,修改了特征参数后,旧的音频库embedding作废,需要重新注册,否则相似度分布会发生漂移。
6. 落地阶段的调整:切换模型、选择特征、排查报错
6.1 切换模型前需要检查的三个字段
把EcapaTdnn换成CAM++不是只改yaml里的model字段就行,还有三处容易一起漏掉。第一个是num_classes,不同backbone最后一层全连接大小不一样,加载预训练参数时最后一层shape不匹配,Paddle会直接报错。第二个是embedding_size,EcapaTdnn默认192,CAM++可能是256,这会影响audio_db中向量的维度,之前注册的向量还是192维,后续对比就会碰matmul错误。第三个是feature_method,ERes2Net用Fbank比MelSpectrogram稳定,CAM++更适配80维Fbank,建议保持80维。
检查兼容性的最快方法是用一段临时命令打印输出维度:
python -c "import paddle; from models import build_model; m=build_model('CAM++',8000,256); x=paddle.randn([1,80,300]); print(m(x).shape)"这段命令在Windows 10和Ubuntu 18.04下都适用,前提是PaddlePaddle按2.5.1版本安装,且models能直接导入。输出shape和预期不一致,优先检查embedding_size和输入帧维度是否写死。
6.2 特征与损失组合建议
中文短语音注册场景,我倾向用Fbank特征 + CAM++或ERes2Net + AAMLoss;实时在线验证则用80维MelSpectrogram + EcapaTdnn + AMSoftmax,因为AMSoftmax梯度更平缓,配合小batch更稳。MFCC在CPU上能快30%,但EER通常劣化一截,只适合当对比基线。
验证集EER一直卡在0.1上下,先检查数据列表是否混入不同采样率的音频;EER稳定但不收敛到目标值,把AAMLoss的margin从0.2降到0.1,或者把embedding_size从192提到256;训练不收敛时,先关掉SpecAugment和特征抖动,用干净特征调通链路再加增强。
6.3 两个常见报错与对应环境
Windows 10下复现最常遇到The 1st dimension of input must be equal to the 1st dimension of weight,基本就是num_classes不匹配。另外get_device_count返回0,说明装成了CPU版PaddlePaddle,按pip install paddlepaddle==2.5.1重装,注意2.5.1对应CUDA 11.x,直接用在CUDA 12环境需要换新版本。
Python环境建议用Anaconda 3单独建环境,PaddlePaddle 2.5.1对Python 3.11支持正常,但librosa需要0.10以上版本,否则scipy导入会报ModuleNotFoundError。先把requirements.txt装完,再补装librosa和soundfile,然后跑python train.py --config configs/ecapa_tdnn.yml,能避开多数环境坑。把新wav放到audio_db下重新注册,再用infer_recognition_gui.py验证,等阈值得分稳定,这套系统就能接手新数据了。
本文还有配套的精品资源,点击获取