声音是看不见摸不着的气流震动,把两段声音放在一起比较“像不像”,就像想用天平称出两道菜的味道谁更接近,根本没法直接量。声纹识别系统想出一招:先让听觉模型把每个人的声音“冲印”成一张数字指纹卡,也就是一串固定长度的数字——声纹向量(embedding)。接下来判断两段声音是否来自同一人,就变成比较两张数字卡片有多接近。声纹相似度度量,正是这套“给声音指纹比对打分”的方法论:有拿角度尺量方向的余弦相似度,有拿直尺量远近的欧氏距离,有经验老到的 PLDA 老师傅,还有专门特训出来的度量学习尺子,以及最后统一考场的评分归一化。本文用“声音指纹鉴定所”这个故事,把量化两段声音有多像的全链路讲明白。
对 AI 从业者而言,相似度度量是声纹比对、说话人聚类、声纹检索系统的“最后一公里”。掌握这些度量方法与评分校准,你就能把一个声纹模型真正落地成可对外输出的产品能力。
可视化演示
展示声纹嵌入提取、说话人验证余弦相似度计算与多说话人聚类分布,交互切换查看声纹度量核心。
本节配有交互动效(进入视口后自动加载,约 3KB)。
声纹嵌入提取 音频经特征提取映射到高维嵌入向量空间 x-vector/ECAPA-TDNN编码说话人身份特征 Emb = ECAPA(audio) 调节阈值查看嵌入维度与相似度变化
声音指纹档案
指纹鉴定所里每位来建档的人,都会被要求对着麦克风说一句话。鉴定师不会保存原始录音,而是让一台“声纹冲印机”把语音变成一张卡片——上面填满了几十上百个数字栏目,每个栏目刻画一种音色属性,比如喉腔共振、语速节奏、发音习惯等。这张数字卡片就是声纹 embedding(嵌入向量)。它把一段不定长的语音,压成固定长度的数字串,比如 192 维或 256 维。
为什么不能直接拿语音波形比较?因为两句话的长度、停顿、响度都不一样,就像两份字迹潦草的便条,直接比对每个字的形状毫无意义。冲印机只关心“谁在说”,不关心“说了什么”,所以它把语音中与说话人身份相关的线索提炼成一个个数字。这串数字被放在一个抽象的“特征空间”里,每个数字是一个维度。
有趣的现象是:同一个人今天感冒、明天兴奋、后天在嘈杂马路上打电话,同一张嘴冲印出的指纹卡并不会完全一样,而是在特征空间里散落成一小团——就像同一个人在不同场合按下的十枚指纹,形状相似但不完全重合。不同人的指纹团则会隔得比较远。因此,声纹相似度度量本质上比的是“两个点在特征空间里的亲疏关系”——这决定了我们后面要研究的所有尺子。
角度尺余弦相似度
鉴定所里最常用的第一把尺子是“角度尺”。它不关心指纹卡的长度,只关心两张卡“指向”是否一致。想象两张卡都被画成从原点出发的箭头,箭头越长只代表登记时的音量越大、环境增益越高,这些跟身份无关;真正能说明身份的是箭头的方向。余弦相似度就是测量两个箭头夹角的余弦值:夹角为 0° 时余弦等于 1,代表完全同向,几乎可以断定是同一人;夹角为 90° 时余弦为 0,代表毫不相关;夹角超过 90° 时余弦为负,代表背道而驰。
在实际工程中,系统会提前把每张指纹卡缩放到单位长度(模长为 1),这时余弦值就等于简单的向量内积,在 GPU 上可以极快地完成千万级的声纹检索排序,所以它成了指纹鉴定所里的“快枪手”。
- 分子对应栏目乘积求和:把两张指纹卡的每个数字栏目一一对应相乘再加总。相同栏目同为正数时贡献正分,一正一负时贡献负分,最后得到“两个方向叠加后的一致总量”。
- 分母两张卡的模长之积:分别给两张卡的每个数字平方求和再开方,得到各自的“长度”,再相乘。这一步把分数压到与音量大小无关的可比区间。
- 取比值归一化到[-1,1]:因为方向一致时分子等于分母,比值最大为 1;方向相反时比值最小为 -1。多数声纹系统把阈值设在 0.4~0.6 之间,高于阈值判定“同人”,低于阈值判定“异人”。
直尺欧氏距离
第二把尺子是“直尺”,它最符合人的直觉:不问方向,只问两个点在特征空间里直线相隔多远。距离越近越像,距离越远越不像。如果说余弦是测量两张卡“朝向”是否一致,欧氏距离则是直接测量两张卡“落点”隔了多远。
欧氏距离有个“脾气”:它对指纹卡的长度很敏感。如果一个人说话声音大导致整张卡长了一截,直接量距离会误判为“很远”。所以使用直尺之前,通常要先做长度归一化(把模长缩放为 1)。有趣的是,如果两张卡都归一化成长度为 1,那么欧氏距离和余弦会神奇地互通:距离平方等于两倍减去两倍余弦,即 d² = 2 - 2cosθ。这说明直尺和角度尺其实是同一套刻度的两种读数,选哪个更多取决于下游任务习惯——聚类算法更喜欢距离度量,搜索引擎更喜欢余弦排序。
- 对应栏目差分平方把两张卡每个栏目相减得到差值,再平方。平方是为了抹掉正负号,让任何方向上的偏差都变成正的贡献量。
- 差值平方求和把所有维度上的偏差累加起来,得到“整张卡的总体偏差”,相当于把所有栏目的欠账一笔笔算清。
- 开方还原尺度因为前面加了平方,总量被放大了,开方可以把它还原回与原始数字一致的尺度,得到最终的直线距离。
老师傅PLDA评判
角度尺和直尺都是“看表面”的工具,只比较数字的大小与方向。鉴定所里还有一位经验老到的老师傅——PLDA(概率线性判别分析,Probabilistic Linear Discriminant Analysis)。他看得更深:同一个人同一句话连说两遍,指纹卡也不会完全一样,因为情绪、信道、距离都会带来不可避免的波动;而两个不同的人之间,又有与生俱来的天然差异。只问“差多少”是不够的,必须问“这个差距符合同一个人的波动规律,还是更像两个陌生人的差异”。
PLDA 的打分逻辑是一个概率比:它同时计算“这两张卡来自同一人”的可能性,以及“这两张卡来自两个不同人”的可能性,然后取对数比值。如果同人假设的可能性远超异人假设,分数为正且很大,判定同人;否则判定异人。这个式子形式上是一个对数似然比:
实际落地时,PLDA 打分可以展开成一个二次型表达式,只需要两次矩阵运算即可完成,速度并不慢。正因它建模了“同人波动”与“异人差异”两本账,在跨设备、跨信道场景下,老师傅的判定通常比新手拿角度尺更可靠。
- 分子同人假设下的概率:假设两段声音来自同一个人,模型允许这个人有正常的发声波动(比如感冒、语速变化),在这个假设下看到这两张指纹卡的概率是多少。
- 分母异人假设下的概率:假设两段声音分别来自两个不同的人,考虑到人跟人之间的天然差异,看到这两张卡的概率又是多少。
- 取对数得到分数用对数把两个概率的比值变成可加可比的分数。比值大于 1 时对数为正,支持同人;小于 1 时为负,支持异人;等于 1 时为 0,表示完全无法判断。
特训专用尺度量学习
余弦和欧氏都是“通用尺子”,拿来即用,但每个人手的形状不同,通用尺量出来的结果未必最合适。鉴定所决定自己打造一把“专用尺”:用大量标注好的声纹数据训练一个神经网络,让网络学到的指纹卡天生就满足“同一人贴得近、不同人离得远”的规律。这就是度量学习(metric learning)。
度量学习最经典的手法是三元组损失(triplet loss)。每次从数据里取三张卡:锚点 a(某个人的一条语音)、正样本 p(同一个人的另一条语音)、负样本 n(另一个人的语音)。训练目标很直接:让锚点与正样本的距离变小,同时让锚点与负样本的距离变大,并且两者之间至少留出一个安全间隔。损失函数写成:
与之配套的还有对比损失(contrastive loss),思路是成对比较:同人样本对压缩距离,异人样本对推过边际。训练时还需要精心构造样本对,尤其是“难样本挖掘”——专门挑那些长得像但其实是异人的配对来强化训练,否则模型容易偷懒。经过特训,新来的语音冲印成指纹卡后,天然就落在“同类簇紧、异类簇远”的空间里,再用余弦或欧氏就能取得更好的效果。
- 正样本距离平方锚点与同一个人另一条语音的距离平方。这个值越小越好,代表“同一个人应该聚在一起”。
- 负样本距离平方锚点与另一个不同人语音的距离平方。这个值越大越好,代表“不同人应该被隔开”。
- 间隔与取最大值当负样本距离比正样本距离大出 α 以上时,括号内为负,损失取 0,说明训练合格;否则损失为正,梯度会促使网络调整,把正样本拉近、负样本推远。
评分归一化与阈值
鉴定所最终要给每份报告下结论:是同人还是异人。可这里有个麻烦:不同考场(近场手机录音、远场音箱录音、嘈杂街头)考出来的分数尺度不一样。同一个人在近场测试能拿 0.8 分,在远场可能只有 0.5 分;如果拿固定的及格线去卡所有场景,要么误放坏人,要么误伤好人。评分归一化(score normalization)就是为了解决这个问题——它像高考的标准分换算,把不同考场的原始分拉到同一把尺子上。
最常用的方法是 Z-norm(零归一化):系统准备一批“路人甲”背景语音,计算他们两两之间的冒充打分的均值 μ 与标准差 σ。正式比对时,把原始分数减去 μ 再除以 σ,得到标准分数。这样无论原始分的分布是偏高还是偏低,都能换算成统一的“平时成绩”,再与同一个阈值比较。
与 Z-norm 类似还有 T-norm(测试者归一化),区别在于 T-norm 用被测试者本人的冒充分数分布做校准,适合每次只测一个人的确认场景。阈值本身也不是拍脑袋定的:门禁系统可以接受偶尔把陌生人放进来(低误拒高误纳),金融风控则必须优先拦住坏人(压低误纳),阈值根据业务容忍度在验证集上选点。有了标准分和合理阈值,鉴定所的报告才算真正公平可靠。
- 背景冒充者均值与标准差用一批非目标说话人互相打分,得到大量“冒充分数”,算出它们的均值 μ 和标准差 σ。这代表“无缘无故冒充时会得多少分”的常态水平。
- 原始分减均值除标准差把待判定的原始分减去冒充者的平均水平,再看它偏离平均水平几个标准差,得到标准分。
- 与统一阈值比较归一化之后,所有场景分享同一个经验阈值。高于阈值判定同人,低于阈值判定异人,避免各场景各设一套标准的混乱局面。
AI 应用场景
智能音箱声纹锁与说话人确认
阶段:声纹注册与实时确认阶段
解决的问题:智能音箱需要区分“这是不是家庭成员的指令”。传统密码输入在免提场景体验差,也无法区分用户。AI 做法是:注册阶段为每位家庭成员提取 embedding 指纹卡,每次唤醒后即时计算待测语音与注册模板的余弦相似度,再经过 Z-norm 校准给出确认结果,支持个性化推荐与儿童模式管控。
具体案例:某款智慧屏支持“声纹登录儿童模式”,儿童喊话时系统比对相似度高于阈值则自动切换儿童内容,家人说话则不响应。工程上还叠加了“活体检测”,防止录音重放攻击。
金融电话客服反欺诈身份核验
阶段:通话中的身份核验与风险评分阶段
解决的问题:银行电话客服办理转账、挂失等业务时,需要确认来电者是否为账户本人。传统客服靠提问验证信息,既慢又易被社会工程破解。AI 做法是:在通话前 30 秒提取声纹 embedding,使用 PLDA 打分替代简单余弦,对信道(座机/手机)和背景噪声更鲁棒;同时用大规模冒名样本做 T-norm 校准,把阈值设定在极低误纳水平。
具体案例:某股份制银行在客服热线中接入声纹核验,高风险操作前自动比对来电声纹,命中黑名单声音库时触发人工复核。PLDA 打分的引入使跨设备场景下的等错误率下降约 30%。
会议录音多说话人聚类与声纹检索
阶段:后端声纹聚类与检索阶段
解决的问题:一段数小时的会议录音有多个发言人,人工翻找“某位高管什么时候说了什么”极其低效。AI 做法是:先把录音切成短句片段,每句提取一个 embedding,再对所有 embedding 两两计算相似度(余弦或欧氏),用聚类算法把同一人的片段聚成一类,完成说话人分离;之后用户输入一句目标语音,系统用余弦相似度在海量片段索引中快速检索出该人的全部发言。
具体案例:某在线会议软件的“发言人时间轴”功能,利用 ECAPA-TDNN 提取 192 维 embedding,配合 faiss 近似最近邻索引,在 10 万条语音片段上实现毫秒级检索,聚类准确率在 20 人规模内达到 95% 以上。
Python 演示
import numpy as npfrom itertools import combinations np.random.seed(42) def cosine_sim(x, y): return float(np.dot(x, y) / (np.linalg.norm(x) * np.linalg.norm(y) + 1e-12)) def euclidean_dist(x, y): return float(np.linalg.norm(x - y)) def estimate_covs(embs_by_spk): all_emb = np.vstack(embs_by_spk) sw = np.mean([np.cov(x.T) for x in embs_by_spk], axis=0) sb = np.cov(all_emb.T) - sw sw = (sw + sw.T) / 2.0 sb = (sb + sb.T) / 2.0 return sw, sb def log_gauss(x, cov): dim = x.shape[0] inv = np.linalg.pinv(cov) sign, logdet = np.linalg.slogdet(cov) return -0.5 * (dim * np.log(2 * np.pi) + logdet + x @ inv @ x) def plda_score(x1, x2, sw, sb): y = x1 + x2 cov_same = 2 * sw + 4 * sb cov_diff = 2 * sw + 2 * sb return log_gauss(y, cov_same) - log_gauss(y, cov_diff) def z_norm(scores, imp_scores): mu, sigma = np.mean(imp_scores), np.std(imp_scores) return (np.array(scores) - mu) / (sigma + 1e-12), mu, sigma def build_embeddings(n_spk=4, n_utt=30, dim=16): centers = np.random.randn(n_spk, dim) * 1.2 return [centers[k] + np.random.randn(n_utt, dim) * 0.35 for k in range(n_spk)] def main(): train = build_embeddings(4, 30, 16) sw, sb = estimate_covs(train) enroll = train[0][0] test_same = train[0][2] test_diff = train[1][0] for name, x in [("same", test_same), ("diff", test_diff)]: cs = cosine_sim(enroll, x) ed = euclidean_dist(enroll, x) ps = plda_score(enroll, x, sw, sb) print(f"{name}: cos={cs:.3f}, euclid={ed:.3f}, plda={ps:.3f}") imp = [plda_score(np.mean(train[s], axis=0), np.mean(train[t], axis=0), sw, sb) for s, t in combinations(range(4), 2)] zn, mu, sigma = z_norm([plda_score(enroll, test_same, sw, sb), plda_score(enroll, test_diff, sw, sb)], imp) print(f"z-norm result: same={zn[0]:.3f}, diff={zn[1]:.3f}, mu={mu:.3f}, sigma={sigma:.3f}") print("observation: same-person score is higher; after z-norm it is comparable across scenes.") if __name__ == "__main__": main()
train = build_embeddings(4, 30, 16)生成 4 个说话人、每人 30 条、每条 16 维的模拟声纹指纹卡。sw, sb = estimate_covs(train)估计类内协方差(同人波动)与类间协方差(异人差异),PLDA 模型的两本账。enroll = train[0][0]取说话人 0 的第一条语音作为注册模板,模拟用户录入声纹。test_same = train[0][2]同一条说话人 0 的另一条录音,用于模拟同人测试。test_diff = train[1][0]说话人 1 的一条录音,用于模拟异人测试。cos=...余弦相似度直接用内积除以模长,同人应接近 1,异人应偏低。def plda_score(x1, x2, sw, sb)用两协方差模型计算对数似然比,y=x1+x2 是关键统计量。y = x1 + x2两段同人语音相加时说话人因子翻倍,使“和”的协方差显著增大,这是区分同人与异人的核心线索。z_norm(...)用 4 个说话人两两组合的冒充分数估计 μ 与 σ,对原始分做标准分换算。
常见误区
误区听别人说声纹比对常用 0.5 当阈值,于是每次看到余弦相似度超过 0.5 就断定是同一人,低于 0.5 就断定不是。
正解阈值是跟着模型和场景走的,没有放之四海皆准的 0.5。不同模型输出的分数分布完全不同,有的模型同人内部分布整体偏高,阈值可能要 0.7;远场环境分数整体偏低,阈值又要下调。阈值必须用验证集上的错误接受率与错误拒绝率曲线来选点,而不是拍脑袋定一个数。
为何常见:市面上很多教程为了简单只讲“分数大于阈值即同人”,却省略了阈值如何标定,初学者最容易把示例值当成通用值。
误区反正都是度量相似度,用哪个都一样,哪个顺手用哪个,调完参效果应该差不多。
正解两者对向量长度(音量、信道增益)的敏感度完全不同。余弦只看方向,对整卡缩放完全不敏感;欧氏距离会把长度差异算入距离。如果 embedding 没有做 L2 归一化,直接用欧氏距离会把响度差异误判成身份差异。正确做法是:先单位化再用欧氏,此时与余弦呈现 d²=2-2cosθ 的确定关系,才能自由互换。
为何常见:很多开源代码示例恰好使用了已归一化的 embedding,掩盖了两者在原始空间里的差异,让人误以为天然等价。
误区PLDA 更复杂、更“高级”,所以哪都该用 PLDA,余弦这种简单的尺子应该被淘汰。
正解PLDA 的优势建立在训练数据与测试场景匹配的前提下。它需要足够的同人语音和异人语音来估计类内/类间协方差;如果训练数据与部署场景的领域差异大(比如训练用近场、部署是远场),PLDA 的模型参数反而会引入误差,效果可能不如简单余弦。工程上常见做法是先做域适配或重训 PLDA,再比较离线集上的 EER 决定取舍。
为何常见:算法文章通常强调 PLDA 在评测集上的增益,却很少提它对训练数据依赖性强;实战中数据不匹配时人们才发现“老师傅也会看走眼”。
误区归一化嘛,拿手头所有测试分数算个均值和标准差,套个标准分公式就完事了。
正解Z-norm 的 μ 和 σ 必须来自“冒充分数”,也就是大量非目标说话人之间的比对得分,代表的是“陌生人冒充时系统给出的常态分数”,而不是任意分数的均值和标准差。如果拿同人分数去算 μ,校准方向就完全错了;背景说话人的构成也要贴近真实攻击人群,否则校准形同虚设。
为何常见:标准分公式人人会写,但“用哪一批分数来算 μ、σ”这个统计口径才是 Z-norm 的灵魂,初学者极易在数据选择上踩坑。
误区指纹卡上的数字栏目越多,刻画越细致,两段声音就应该比得越准,所以把模型输出维度从 128 加到 512 必然有用。
正解维度增加未必带来泛化收益。高维空间容易出现“维度灾难”:样本变得稀疏,距离区分度下降,甚至每个样本都像“孤岛”;同时模型参数量增大,需要更多训练数据防止过拟合。实验应对比不同维度的 EER 曲线,找到拐点,而不是盲目堆维度。
为何常见:人们天然觉得“特征越多信息量越大”,忽略了声纹模型的高层特征已经高度抽象化,低维压缩反而是去冗余、提升鲁棒性的常用手段。
面试题
Q1. 关于余弦相似度,下列说法正确的是?
- A.只关注两向量的方向一致性← 正确答案
- B.只关注两向量的长短差异
- C.必须先做矩阵分解才能计算
- D.输出永远是不超过十的整数
答案:A。余弦相似度的分子是向量内积,分母是模长乘积,正好把长度因素约掉,只看方向;方向一致时值为 1,相反时为 -1,输出是连续值而非整数,也不需要矩阵分解。就像角度尺只量夹角,不量卡片长短。
Q2. 声纹 embedding 可以通俗理解为?
- A.由语音提取的固定维数向量← 正确答案
- B.未做处理的原始音频波形
- C.标记音素边界的时间轴
- D.每位说话人的一段音频切片
答案:A。embedding 是神经网络从语音中提取出的固定维度指纹卡,核心特征是“固定维度、与身份相关”。原始波形未处理、时间长,音素边界是语音识别的内容信息,都不直接等价于 embedding。
Q3. 同一人多条录音的 embedding 通常会?
- A.在特征空间中聚成一小簇← 正确答案
- B.每次提取结果完全一致
- C.均匀铺满整个特征空间
- D.严格落在同一条直线上
答案:A。同一个人受情绪、信道、噪声影响,每次提取的 embedding 会有波动,但在特征空间里总体围绕一个中心聚成小簇。完全一致或呈直线分布不符合真实波动规律,均匀铺满更是异人的表现。
Q4. 嵌入向量单位化之后,欧氏距离与余弦的关系是?
- A.二者的平方存在线性换算关系← 正确答案
- B.二者完全没有任何数学关系
- C.单位化后距离永远大于一
- D.单位化后距离恒等于余弦
答案:A。当两条向量模长都为 1 时,d² = ‖x-y‖² = ‖x‖² + ‖y‖² - 2x·y = 2 - 2cosθ,所以距离平方与余弦存在确定的线性换算关系。选择 A 最准确,其余选项要么否定关系、要么数值关系错误。
Q5. PLDA 打分在本质上是计算?
- A.同人与异人假设的似然比对数← 正确答案
- B.两个向量夹角的余弦函数值
- C.特征向量各维度平方的求和
- D.语音时长与音高的加权值
答案:A。PLDA 构建了两个概率模型:两段语音来自同一说话人的概率,与来自不同说话人的概率,两者取比值再取对数得到分数。它不像余弦只看夹角,也不像 L2 只看距离,而是把“同人波动 vs 异人差异”统计进模型,故选 A。
Q6. 下列哪些因素会造成同一声纹的相似度分数下降?
- A.电话信道与近场麦克风差异← 正确答案
- B.感冒情绪引起的发声变化← 正确答案
- C.嘈杂环境带来采集噪声← 正确答案
- D.静音段被当作有效语音
答案:ABC。信道差异、发声生理变化、环境噪声都会让同一个人两次语音的 embedding 偏离更大,导致相似度下降。静音段没有有效语音信息,一般不会被提取成有效 embedding,它不是“导致分数下降”的正常因素,更多属于数据质量问题。
Q7. Z-norm 的主要作用是?
- A.用背景说话人分数做校准← 正确答案
- B.直接提升音频采样率
- C.消除语音中的环境噪声
- D.自动写入说话人的身份标签
答案:A。Z-norm 用一批冒充者比对分数估计 μ 与 σ,把原始分换算成标准分,从而统一不同环境下的分数尺度。它不改变音频信号质量,也不做降噪和打标签,故选 A。
Q8. Triplet loss 的优化目标是?
- A.让同类更近且异类更远← 正确答案
- B.让所有样本映射至原点
- C.异类越远时损失值越小
- D.只统计单个样本的模长
答案:A。三元组损失同时拉近锚点与正样本、推远锚点与负样本,并保留一个安全间隔。异类越远确实会让损失变小,但那只是半个目标;完整目标是“同类近且异类远”,因此 A 更全面准确。
Q9. 大规模声纹检索引擎中常用的加速策略有?
- A.嵌入归一化后余弦快速排序← 正确答案
- B.对全部音频重新训练模型
- C.近似最近邻索引做召回← 正确答案
- D.直接比对原始音频波形
答案:AC。工程上先把 embedding 做 L2 归一化,余弦退化为内积,可以批量矩阵乘法快速排序;再用 faiss 等近似最近邻索引做召回,避免全量两两比较。重训模型不是检索加速手段,直接比原始波形既慢又不稳定。
Q10. 两协方差 PLDA 模型中,统计量 y=x1+x2 的价值在于?
- A.同人时和的协方差变大← 正确答案
- B.向量和用于估算语音能量
- C.和与异人假设完全相同
- D.和必须满秩才有分析意义
答案:A。在同人假设下,两段语音共享同一个说话人因子,求和时因子翻倍,使 y 的协方差变为 2Sw+4Sb;异人假设下两个因子独立,协方差为 2Sw+2Sb。两组协方差不同,才让似然比具备判别力。这与能量估算无关,满秩也不是必要条件。