• 人工智能
  • 语音
  • 音频

【免费下载链接】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.

项目地址: https://gitcode.com/gh_mirrors/pa/PaddleSpeech
点击查看 免费下载

导读

本文围绕 PaddleSpeech 仓库中 paddleaudio.metric 包及其核心子模块 paddleaudio.metric.eer 展开,深入讲解声纹(说话人)验证系统最常用的两项评估指标——等错误率(Equal Error Rate, EER)最小检测代价函数(minimum Detection Cost Function, minDCF) 的定义、数学原理与 Paddle 源码实现。文中会逐行拆解 eer.pycompute_eercompute_minDCF 的实现细节,并结合 ECAPA-TDNN 声纹验证测试脚本VoxCeleb sv0 示例 说明这两项指标在真实声纹验证流程中的落地方式。读完本文,你将能够理解 EER/minDCF 的推导逻辑、掌握其调用方法与参数含义,并能读懂 PaddleSpeech 声纹模型的评估输出。

一、模块定位:paddleaudio.metric 包结构

在 PaddleSpeech 仓库中,audio 目录下维护着独立的 paddleaudio 音频处理包,paddleaudio.metric 正是其中的评估指标子包,用于支撑音频/声纹类任务的模型评测。文档页 paddleaudio.metric.rstpaddleaudio.metric.eer.rst 通过 Sphinx 的 automodule 指令,将模块内的公开成员(含 docstring)自动渲染为 API 文档,其实际内容来源即下面的源码文件:

__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 - tprtpr 即真正例率)。

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

核心逻辑只有四步,但每一步都值得展开:

  1. 输入约定labelsscores 都是一维数组,长度 N 为 trial(验证对)数量。labels 是真实标记(1 表示同一说话人,0 表示不同说话人),scores 是模型输出的相似度分数。文件头部还引入了 numpypaddlesklearn.metrics.roc_curve,其中 ROC 曲线的计算直接复用了 scikit-learn 的成熟实现。

  2. 计算 ROC 曲线roc_curve(y_true=labels, y_score=scores) 返回三个数组——fpr(误接受率/假正例率)、tpr(真正例率/召回率)以及对应的 threshold(阈值序列)。sklearn 会对所有分数候选阈值逐一计算 TPR/FPR,从而刻画"区分能力"的全貌。

  3. 推导误拒绝率fnr = 1 - tpr。由于 TPR 是"同一说话人被正确接受"的概率,其补集 1 - tpr 正是"同一说话人被错误拒绝"的概率,即 FRR(误拒绝率)。

  4. 寻找交点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 中明确注明改写自 SpeechBrainmetric_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.minpaddle.argmin 取出最小代价 c_min 及其对应的阈值下标,最终返回 (float(c_min), float(thresholds[min_index]))——即最小检测代价与取得该代价的最优阈值

函数的三个代价参数均有默认值:c_miss=1.0c_fa=1.0p_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):

  1. 设置运行设备(--device 可选 cpu/gpu)与随机种子;
  2. 构建骨干网络 EcapaTdnnconfig.model 中的通道数、核大小、空洞率等均来自 yaml 配置);
  3. SpeakerIdetification 包装骨干模型并加载预训练 checkpoint;
  4. 构造 enroll(注册)与 test(测试)两个 CSVDataset,特征为 80 维 mel 频谱,经 batch_feature_normalize 做均值归一化;
  5. 模型切换为 eval() 模式;
  6. config.global_embedding_norm 决定是否做全局 embedding 均值/方差归一化——注释明确指出全局归一化可相对降低约 10% 的 EER
  7. 若配置了 score_norm(如 s-norm),额外加载训练集构造 imposters cohort 用于分数归一化;
  8. 对 enroll/test 数据集跑两轮 embedding 提取(多轮取均值使 embedding 归一化更稳定);
  9. 计算余弦分数,可选基于 cohort 做 s-norm 分数校准(源码支持 s-norm、t-norm、z-norm 等);
  10. 汇总全部 trial 的分数与标签;
  11. 调用 EER 计算并输出日志。

4.2 EER 计算与日志输出

值得说明的是,paddlespeech/vector/exps/ecapa_tdnn/test.py内联了一份与 paddleaudio.metric.eer.compute_eer 完全等价的实现test.py),同样基于 roc_curvenanargmin(|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_filedata/vox1/veri_test2.txt官方 VoxCeleb1 trial 列表,评测用正/负样本对来源
global_embedding_normTrue是否做全局 embedding 归一化,可显著降低 EER
embedding_mean_norm / embedding_std_normTrue / False归一化时是否减均值、除方差
score_norms-norm分数归一化策略(s-norm/t-norm/z-norm),需 imposters cohort
cohort_size20000归一化 cohort 中冒名(imposter)话语数量
n_train_snts400000用于计算归一化统计量的训练语句数
num_speakers7205训练说话人数(Vox1 的 1211 + Vox2 的 5994)

此外,模型结构与前端特征(sr=16000n_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 EERCosine + S-Norm EER
ECAPA-TDNN85M0.2.1192test0.8188%0.7815%

可以看到,开启 s-norm 分数归一化后 EER 从 0.8188% 降至 0.7815%,这也印证了 test.py 中引入 imposters cohort 做分数归一化的动机。需要注意的是,该数值基于 VoxCeleb1 + VoxCeleb2 联合训练、VoxCeleb1 官方 trial 评测,换用其他数据集或 trial 列表时结果会不同。

六、使用注意事项与依赖环境

  1. 依赖compute_eer 依赖 numpysklearnroc_curve);compute_minDCF 依赖 paddle(张量运算)与 numpy。若在纯推理/部署环境使用,需保证这些依赖已安装。
  2. 输入形状:两个函数都要求一维输入(labels/scores 长度一致,positive_scores/negative_scores 为一维或可 squeeze 的张量),传入批量二维分数前需先展平。
  3. 分数方向约定compute_eerscores 越大代表"越可能是同一说话人"(与 roc_curve 的默认行为一致);compute_minDCFp_miss<= 判定、p_fa> 判定,同样遵循"高分 = 接受"的相似度分数惯例。若你的模型输出的是距离/负相似度,需要先取反。
  4. 阈值返回价值:两个函数都返回"最优操作点对应的分数阈值",这一返回值可直接复用于线上系统设定判定门限,而不仅是离线报告的指标。
  5. minDCF 的代价参数c_missc_fap_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.

项目地址: https://gitcode.com/gh_mirrors/pa/PaddleSpeech
点击查看 免费下载
Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐