PaddleSpeech 声纹验证评估指标全解析:paddleaudio.metric 模块的 EER 与 minDCF 实现
【免费下载链接】PaddleSpeech
Easy-to-use Speech Toolkit including Self-Supervised Learning model, SOTA/Streaming ASR with punctuation, Streaming TTS with text frontend, Speaker Verification System, End-to-End Speech Translation and Keyword Spotting. Won NAACL2022 Best Demo Award.
导读
本文围绕 PaddleSpeech 仓库中 paddleaudio.metric 包及其核心子模块 paddleaudio.metric.eer 展开,深入讲解声纹(说话人)验证系统最常用的两项评估指标——等错误率(Equal Error Rate, EER) 与 最小检测代价函数(minimum Detection Cost Function, minDCF) 的定义、数学原理与 Paddle 源码实现。文中会逐行拆解 eer.py 中 compute_eer 与 compute_minDCF 的实现细节,并结合 ECAPA-TDNN 声纹验证测试脚本 与 VoxCeleb sv0 示例 说明这两项指标在真实声纹验证流程中的落地方式。读完本文,你将能够理解 EER/minDCF 的推导逻辑、掌握其调用方法与参数含义,并能读懂 PaddleSpeech 声纹模型的评估输出。
一、模块定位:paddleaudio.metric 包结构
在 PaddleSpeech 仓库中,audio 目录下维护着独立的 paddleaudio 音频处理包,paddleaudio.metric 正是其中的评估指标子包,用于支撑音频/声纹类任务的模型评测。文档页 paddleaudio.metric.rst 与 paddleaudio.metric.eer.rst 通过 Sphinx 的 automodule 指令,将模块内的公开成员(含 docstring)自动渲染为 API 文档,其实际内容来源即下面的源码文件:
- audio/paddleaudio/metric/init.py —— 包导出入口;
- audio/paddleaudio/metric/eer.py —— 指标核心实现,包含两个公开函数。
从 __init__.py 可以看出,包对外仅导出两个 API:
from .eer import compute_eer
from .eer import compute_minDCF
也就是说,paddleaudio.metric 的全部技术内涵集中在 eer.py 中:一个用于计算 EER(等错误率),一个用于计算 minDCF(最小检测代价函数)。两者都是声纹验证(Speaker Verification)领域的事实标准指标,前者衡量"两类错误相等时的最小错误率",后者则在引入代价权重与目标先验后,衡量系统在最优阈值下的加权检测代价。
二、compute_eer:等错误率(EER)的原理与实现
2.1 EER 的指标含义
在声纹验证任务中,系统对每一对"待测音频 vs 注册说话人"输出一个相似度分数(score),并设定一个判定阈值:分数高于阈值判为"同一人"(接受,positive),低于阈值判为"不同人"(拒绝,negative)。由此产生两类错误:
- 误接受率 FAR(False Acceptance Rate):不同说话人被错误接受的比率,对应 ROC 曲线中的
fpr; - 误拒绝率 FRR(False Rejection Rate):同一说话人被错误拒绝的比率,对应
1 - tpr(tpr即真正例率)。
FAR 与 FRR 随阈值朝相反方向变化:阈值放宽则 FAR 上升、FRR 下降;阈值收紧则反之。EER 就是 FAR 与 FRR 恰好相等时的那一点错误率,此时曲线交点对应的阈值可作为"均衡"操作点。EER 越小,说明系统在同等两类错误下区分能力越强,因此它是声纹验证论文与工程报告中最常引用的指标之一。
2.2 源码逐行解析
compute_eer 的实现如下:
def compute_eer(labels: np.ndarray, scores: np.ndarray) -> List[float]:
"""Compute EER and return score threshold.
Args:
labels (np.ndarray): the trial label, shape: [N], one-dimension, N refer to the samples num
scores (np.ndarray): the trial scores, shape: [N], one-dimension, N refer to the samples num
Returns:
List[float]: eer and the specific threshold
"""
fpr, tpr, threshold = roc_curve(y_true=labels, y_score=scores)
fnr = 1 - tpr
eer_threshold = threshold[np.nanargmin(np.absolute((fnr - fpr)))]
eer = fpr[np.nanargmin(np.absolute((fnr - fpr)))]
return eer, eer_threshold
核心逻辑只有四步,但每一步都值得展开:
-
输入约定:
labels与scores都是一维数组,长度N为 trial(验证对)数量。labels是真实标记(1 表示同一说话人,0 表示不同说话人),scores是模型输出的相似度分数。文件头部还引入了numpy、paddle与sklearn.metrics.roc_curve,其中 ROC 曲线的计算直接复用了 scikit-learn 的成熟实现。 -
计算 ROC 曲线:
roc_curve(y_true=labels, y_score=scores)返回三个数组——fpr(误接受率/假正例率)、tpr(真正例率/召回率)以及对应的threshold(阈值序列)。sklearn 会对所有分数候选阈值逐一计算 TPR/FPR,从而刻画"区分能力"的全貌。 -
推导误拒绝率:
fnr = 1 - tpr。由于 TPR 是"同一说话人被正确接受"的概率,其补集1 - tpr正是"同一说话人被错误拒绝"的概率,即 FRR(误拒绝率)。 -
寻找交点:
np.absolute(fnr - fpr)计算每条 ROC 点上 FAR 与 FRR 之差的绝对值,np.nanargmin返回差值最小(即两条曲线最接近相交)处的下标。由于 ROC 曲线的采样是离散的,这里取的是最接近交点的点,并同时返回:eer = fpr[idx]:等错误率本身;eer_threshold = threshold[idx]:该操作点对应的分数阈值。
返回值是一个二元列表 [eer, eer_threshold],即同时给出"错误率"与"达到该错误率的判定阈值",后者可以直接用于线上系统设置判定门限。
三、compute_minDCF:最小检测代价函数的原理与实现
3.1 minDCF 的指标含义
EER 只关注 FAR 与 FRR 相等的一个特殊点,而在真实业务(如门禁、金融风控)中,两类错误造成的损失往往不对等——漏报(错过真实用户)与误报(放行冒名者)的成本差异很大。minDCF 正是为此设计:在引入误拒代价 c_miss、误接代价 c_fa 与目标先验概率 p_target 之后,系统的检测代价定义为:
C_det = c_miss * p_miss * p_target + c_fa * p_fa * (1 - p_target)
其中 p_miss 为漏检概率(同一说话人被拒绝),p_fa 为虚警概率(不同说话人被接受)。minDCF 即遍历全部候选阈值后 C_det 的最小值,它刻画了系统在最优阈值下、考虑代价权重后的综合风险。p_target = 0.01 的默认值也反映了声纹验证场景中"目标(本人)出现概率很低、绝大多数 trial 是冒名者"的典型先验。
3.2 源码逐行解析
compute_minDCF 的实现相对更长,其作者在 docstring 中明确注明改写自 SpeechBrain 的 metric_stats.py(源码注释中保留了 SpeechBrain 的原始链接),并完整给出了 C_det 公式。整体流程分为四个阶段:
(1)输入预处理
if len(positive_scores.shape) > 1:
positive_scores = positive_scores.squeeze()
if len(negative_scores.shape) > 1:
negative_scores = negative_scores.squeeze()
positive_scores 是同类别(同一说话人) trial 的分数张量,negative_scores 是不同类别 trial 的分数张量,两者均支持 Paddle Tensor 输入;若存在冗余维度则先 squeeze() 去掉,确保后续拼接与广播正确。
(2)构造候选阈值集合
thresholds = paddle.sort(paddle.concat([positive_scores, negative_scores]))
thresholds = paddle.unique(thresholds)
interm_thresholds = (thresholds[0:-1] + thresholds[1:]) / 2
thresholds = paddle.sort(paddle.concat([thresholds, interm_thresholds]))
这一步把正、负样本的全部分数合并、排序并去重作为基础候选阈值,再对相邻阈值取中点生成中间阈值一并纳入候选集。这样处理的原因是:ROC 曲线本质是阶梯函数,真正的最优代价点往往落在相邻两个分数之间的"分界"位置,插入中间阈值能显著提高 C_det 最小值的搜索精度。
(3)批量计算 p_miss 与 p_fa
positive_scores = paddle.concat(
len(thresholds) * [positive_scores.unsqueeze(0)])
pos_scores_threshold = positive_scores.transpose(perm=[1, 0]) <= thresholds
p_miss = (pos_scores_threshold.sum(0)).astype("float32") / positive_scores.shape[1]
这里的技巧是:将正样本分数沿新轴复制 len(thresholds) 份并转置为 (N_pos, T) 形状(T 为候选阈值数),随后一次性与全部阈值做 <= 比较——同一说话人的分数低于等于阈值,即被判为"拒绝",构成漏检;按列求和再除以正样本数即得每个阈值下的 p_miss。负样本的 p_fa 逻辑对称:
negative_scores = paddle.concat(
len(thresholds) * [negative_scores.unsqueeze(0)])
neg_scores_threshold = negative_scores.transpose(perm=[1, 0]) > thresholds
p_fa = (neg_scores_threshold.sum(0)).astype("float32") / negative_scores.shape[1]
不同说话人的分数高于阈值即被判为"接受",构成虚警,按列统计占比得到 p_fa。这种"向量化广播比较"避免了 Python 层逐阈值循环,充分发挥了 Paddle 的张量并行计算能力;源码中还通过 del 及时释放中间张量,控制长序列评测时的显存/内存占用。
(4)计算代价并取最小值
c_det = c_miss * p_miss * p_target + c_fa * p_fa * (1 - p_target)
c_min = paddle.min(c_det, axis=0)
min_index = paddle.argmin(c_det, axis=0)
return float(c_min), float(thresholds[min_index])
按公式向量化计算全部候选阈值下的 C_det,用 paddle.min 与 paddle.argmin 取出最小代价 c_min 及其对应的阈值下标,最终返回 (float(c_min), float(thresholds[min_index]))——即最小检测代价与取得该代价的最优阈值。
函数的三个代价参数均有默认值:c_miss=1.0、c_fa=1.0、p_target=0.01。默认情况下两者代价相等,因此 p_target 直接决定了系统在"漏报"与"误报"之间的倾向性;业务中若冒名攻击风险更高,可调大 c_fa,反之调大 c_miss。
四、仓库中的真实应用:ECAPA-TDNN 声纹验证评测流程
paddleaudio.metric 作为独立音频包提供的指标工具,与 paddlespeech 主包中的声纹验证流程相互印证。在 paddlespeech/vector/exps/ecapa_tdnn/test.py 中,可以看到一条完整的"embedding 提取 → 余弦打分 → EER 评估"链路。
4.1 全流程十一个阶段
该测试脚本将声纹验证评测组织为清晰的 stage 流程(test.py):
- 设置运行设备(
--device可选cpu/gpu)与随机种子; - 构建骨干网络
EcapaTdnn(config.model中的通道数、核大小、空洞率等均来自 yaml 配置); - 以
SpeakerIdetification包装骨干模型并加载预训练 checkpoint; - 构造 enroll(注册)与 test(测试)两个
CSVDataset,特征为 80 维 mel 频谱,经batch_feature_normalize做均值归一化; - 模型切换为
eval()模式; - 按
config.global_embedding_norm决定是否做全局 embedding 均值/方差归一化——注释明确指出全局归一化可相对降低约 10% 的 EER; - 若配置了
score_norm(如s-norm),额外加载训练集构造 imposters cohort 用于分数归一化; - 对 enroll/test 数据集跑两轮 embedding 提取(多轮取均值使 embedding 归一化更稳定);
- 计算余弦分数,可选基于 cohort 做 s-norm 分数校准(源码支持 s-norm、t-norm、z-norm 等);
- 汇总全部 trial 的分数与标签;
- 调用 EER 计算并输出日志。
4.2 EER 计算与日志输出
值得说明的是,paddlespeech/vector/exps/ecapa_tdnn/test.py 中内联了一份与 paddleaudio.metric.eer.compute_eer 完全等价的实现(test.py),同样基于 roc_curve 与 nanargmin(|fnr - fpr|),体现了"指标算法跨包复用、口径一致"的设计。评测末尾的实际调用为:
scores = paddle.to_tensor(scores)
EER, threshold = compute_eer(np.asarray(labels), scores.numpy())
logger.info(
f'EER of verification test: {EER*100:.4f}%, score threshold: {threshold:.5f}'
)
即把模型输出的 scores 转为 numpy 数组、labels 一并喂给 compute_eer,随后打印百分比格式的 EER 与对应的分数阈值——这也是你在训练日志中看到 EER of verification test: 0.xxxx% 一行日志的来源。从调用形态看,compute_eer 接受 (labels, scores) 两个一维数组、返回 (eer, threshold),与 eer.py 中的 docstring 完全一致。
五、配套实操:VoxCeleb 声纹验证示例的配置与运行
5.1 配置文件中与评估相关的关键项
examples/voxceleb/sv0/conf/ecapa_tdnn.yaml 是 ECAPA-TDNN 声纹验证的完整配置,其中与"评测 + 指标"直接相关的参数如下:
| 配置项 | 示例值 | 作用 |
|---|---|---|
verification_file | data/vox1/veri_test2.txt | 官方 VoxCeleb1 trial 列表,评测用正/负样本对来源 |
global_embedding_norm | True | 是否做全局 embedding 归一化,可显著降低 EER |
embedding_mean_norm / embedding_std_norm | True / False | 归一化时是否减均值、除方差 |
score_norm | s-norm | 分数归一化策略(s-norm/t-norm/z-norm),需 imposters cohort |
cohort_size | 20000 | 归一化 cohort 中冒名(imposter)话语数量 |
n_train_snts | 400000 | 用于计算归一化统计量的训练语句数 |
num_speakers | 7205 | 训练说话人数(Vox1 的 1211 + Vox2 的 5994) |
此外,模型结构与前端特征(sr=16000、n_mels=80、25ms 窗长 window_size=400、10ms 帧移 hop_size=160)同样会间接影响最终 EER 表现。
5.2 运行评测
按照 examples/voxceleb/sv0/README.md 中的 Stage 2(模型测试),训练完成后执行:
bash run.sh --stage 2 --stop_stage 2
也可以直接运行底层脚本(仅用 CPU 时令 CUDA_VISIBLE_DEVICES 为空):
source path.sh
CUDA_VISIBLE_DEVICES= ./local/test.sh ./data/ exp/ecapa-tdnn-vox12-big/ conf/ecapa_tdnn.yaml
脚本内部会按上文十一个阶段的流程提取 embedding、计算余弦分数(可选 s-norm),并最终以 compute_eer 输出验证结果。
5.3 仓库记录的参考结果
仓库在 examples/voxceleb/sv0/RESULT.md 中记录了该示例的评测结果:
| Model | 参数规模 | 发布版本 | dim | 测试集 | Cosine EER | Cosine + S-Norm EER |
|---|---|---|---|---|---|---|
| ECAPA-TDNN | 85M | 0.2.1 | 192 | test | 0.8188% | 0.7815% |
可以看到,开启 s-norm 分数归一化后 EER 从 0.8188% 降至 0.7815%,这也印证了 test.py 中引入 imposters cohort 做分数归一化的动机。需要注意的是,该数值基于 VoxCeleb1 + VoxCeleb2 联合训练、VoxCeleb1 官方 trial 评测,换用其他数据集或 trial 列表时结果会不同。
六、使用注意事项与依赖环境
- 依赖:
compute_eer依赖numpy、sklearn(roc_curve);compute_minDCF依赖paddle(张量运算)与numpy。若在纯推理/部署环境使用,需保证这些依赖已安装。 - 输入形状:两个函数都要求一维输入(
labels/scores长度一致,positive_scores/negative_scores为一维或可 squeeze 的张量),传入批量二维分数前需先展平。 - 分数方向约定:
compute_eer中scores越大代表"越可能是同一说话人"(与roc_curve的默认行为一致);compute_minDCF中p_miss用<=判定、p_fa用>判定,同样遵循"高分 = 接受"的相似度分数惯例。若你的模型输出的是距离/负相似度,需要先取反。 - 阈值返回价值:两个函数都返回"最优操作点对应的分数阈值",这一返回值可直接复用于线上系统设定判定门限,而不仅是离线报告的指标。
- minDCF 的代价参数:
c_miss、c_fa、p_target应根据业务风险偏好调整;p_target默认0.01对应"目标出现概率低"的验证场景,在注册/登录类场景中通常保持较小值。
七、总结
paddleaudio.metric 是 PaddleSpeech 音频工具链中轻量但关键的评估指标模块。compute_eer 通过 ROC 曲线与 FAR/FRR 交点给出等错误率及其阈值,compute_minDCF 则在代价权重与目标先验下扫描候选阈值(含中间阈值插值)得到最小检测代价及其最优阈值。二者共同覆盖了声纹验证评估的"均衡视角"与"风险加权视角",其实现与 ECAPA-TDNN 评测脚本、VoxCeleb sv0 示例 一脉相承,是理解 PaddleSpeech 声纹验证评测输出、复现实验与部署阈值选型的直接入口。
【免费下载链接】PaddleSpeech
Easy-to-use Speech Toolkit including Self-Supervised Learning model, SOTA/Streaming ASR with punctuation, Streaming TTS with text frontend, Speaker Verification System, End-to-End Speech Translation and Keyword Spotting. Won NAACL2022 Best Demo Award.
火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。
更多推荐
所有评论(0)