资讯动态

PESQ语音质量评估实战:从原理到批量评测与回归看门狗

发布时间:2026/10/25 19:03:35 来源:尧图企业网站定制
简介这份资源围绕ITU-T P.862标准展开即感知语音质量评价PESQ面向从事语音编码、网络通信与音频设备测试的开发者及研究人员帮助其以客观方式评估语音信号质量。压缩包共8个文件约332KB包含2个wav与2个raw语音样本、2个m脚本、2个txt说明文档其中m文件为PESQ评估函数及配套测试脚本wav与raw提供原始和处理后的音频素材txt则涵盖使用说明与许可条款。已有643人学习下载。读者可借助MATLAB脚本直接计算两段语音之间的PESQ得分结合示例音频验证算法正确性并理解频谱、时域与声学参数差异如何转化为1.0至4.5区间的质量分值从而为编码器优化、网络性能监测、设备测试及语音增强技术评估提供可复用的参考实现与排错思路。1. PESQ 不是玄学为什么你的语音质量打分总对不上主观感受做语音算法这行的大概都经历过这种场面降噪模型跑完客观指标 PESQ 从 2.1 涨到 2.8团队群里一片欢呼结果拉了几个真人一听反馈是“怎么比原来还闷”。反过来也有明明听着干净不少PESQ 却掉了 0.2。于是有人开始说 PESQ 是玄学。其实不是它玄学是大多数人只把它当成一个黑盒函数在调没搞清楚它到底在算什么。ITU-T P.862 就是 PESQPerceptual Evaluation of Speech Quality感知语音质量评价这个标准的正式编号。它解决的问题很具体给一段参考语音和一段经过编解码、降噪、丢包等处理后的退化语音输出一个 1 到 4.5 之间的分数越接近 4.5 说明退化越小。它替代的是更早的 PSQM在时延变化、滤波失真、短时局部损伤这些场景下更稳。适合谁用做语音增强、编解码器选型、VoIP 链路质量监控、TTS 音质回归测试的工程师基本都绕不开它。但前提是你得知道它的边界在哪不然那个分数真的会骗你。2. PESQ 的计算链路从时间对齐到扰动聚合2.1 为什么不能直接逐帧比频谱很多人第一反应是参考语音和处理语音都做 STFT逐帧算个谱距离不就行了。真这么干结果会惨不忍睹。原因在于经过网络传输或编解码的语音整体可能有几十毫秒的时延甚至时延是波动的。你逐帧对齐第一帧可能就错位了后面全乱。PESQ 的第一步不是比频谱而是把两段语音在时间轴上“拉齐”。它的处理链路大致是这样先做电平归一化把两段语音的响度拉到同一基准避免增益差异干扰判断然后过一个类似电话听筒的输入滤波器模拟人耳接收语音的频响特性接着做时间对齐这一步用的是感知域上的包络互相关允许一定范围内的时延偏移对齐之后才做短时 FFT把信号转到频域在频域上计算一个叫“扰动密度”的东西它综合了响度差和频率差最后把每帧的扰动聚合成一个全局分数再映射到 MOS 量表上。这里有个关键点PESQ 算的不是“差异有多大”而是“差异在人耳听起来有多明显”。所以它对某些类型的失真敏感对另一些类型迟钝。比如它对时延抖动、局部丢包造成的咔嗒声很敏感但对某些平稳的频谱包络偏移反而没那么敏感。这就是为什么降噪后 PESQ 涨了人耳却觉得闷——降噪可能引入了平稳的谱失真PESQ 没怎么扣分但人耳对音色变化很在意。2.2 参考语音和处理语音的采样率约束PESQ 标准本身定义在 8kHz 和 16kHz 两种采样率上。8kHz 对应窄带电话场景16kHz 对应宽带场景。你拿 48kHz 的语音直接喂进去大多数实现会报错或者内部重采样但重采样滤波器如果不匹配分数会偏。常见做法是在调用 PESQ 之前自己先把两段语音统一到 16kHz用高质量的 resampler比如 soxr 或 scipy 的 resample_poly别用简单的线性插值。还有一个容易翻车的点参考语音和处理语音的采样率必须一致长度也要基本一致。如果处理语音比参考语音短了一截比如因为丢包被截断了PESQ 实现通常会报长度不匹配的错误。这时候你得先做长度对齐要么补零要么截断到较短的长度但补零本身会引入失真影响分数。我一般会先检查长度差如果超过 5%就先排查处理链路是不是有丢帧而不是硬算。2.3 用 Python 跑通第一个 PESQ 分数理论说再多不如跑一遍。目前最省事的方案是pesq这个 Python 包它底层封装了 ITU-T 的参考 C 代码。安装很简单pip install pesq注意这个包在 Windows 上可能需要编译工具链Linux 和 macOS 一般直接装。装完之后准备两段 16kHz 的单声道 wav 文件参考语音叫ref.wav处理语音叫deg.wav。下面是最小可运行代码import soundfile as sf from pesq import pesq # 读取音频soundfile 返回的是 float64 数组范围 [-1, 1] ref, sr_ref sf.read(ref.wav) deg, sr_deg sf.read(deg.wav) # 强制检查采样率一致且必须是 8000 或 16000 assert sr_ref sr_deg, f采样率不一致: {sr_ref} vs {sr_deg} assert sr_ref in (8000, 16000), fPESQ 只支持 8k/16k当前 {sr_ref} # 如果是双声道取左声道 if ref.ndim 1: ref ref[:, 0] if deg.ndim 1: deg deg[:, 0] # 长度对齐截断到较短的长度避免报错 min_len min(len(ref), len(deg)) ref ref[:min_len] deg deg[:min_len] # mode 参数wb 对应 16kHz 宽带nb 对应 8kHz 窄带 score pesq(sr_ref, ref, deg, wb) print(fPESQ MOS: {score:.3f})这段代码里几个参数值得说清楚。mode必须和采样率匹配16kHz 用wb8kHz 用nb写反了会直接抛异常。pesq函数返回的是一个浮点数宽带模式下范围大概是 -0.5 到 4.5窄带模式下范围是 -0.5 到 4.5 但实际有效区间更窄。如果你拿到一个负数别慌说明退化非常严重已经超出 MOS 量表的正常范围了。跑通之后你可以试着把处理语音加一点白噪声看分数怎么变。信噪比 30dB 的时候PESQ 可能还有 4.0 以上降到 10dB可能就掉到 2.5 左右了。这个手感需要多跑几组才能建立起来。3. 把 PESQ 接进评测流水线批量跑、对齐、出报告3.1 批量评测的目录结构和脚本单条跑通只是第一步实际项目里你面对的是几百上千条测试集。这时候需要一套批量脚本。我一般会约定一个目录结构ref/放参考语音deg/放对应的处理语音文件名一一对应比如ref/001.wav对应deg/001.wav。然后写一个批量脚本import os import glob import soundfile as sf import pandas as pd from pesq import pesq REF_DIR ref DEG_DIR deg SR_TARGET 16000 MODE wb results [] for ref_path in sorted(glob.glob(os.path.join(REF_DIR, *.wav))): fname os.path.basename(ref_path) deg_path os.path.join(DEG_DIR, fname) if not os.path.exists(deg_path): print(f跳过 {fname}找不到对应的处理语音) continue ref, sr_r sf.read(ref_path) deg, sr_d sf.read(deg_path) # 采样率不一致时用 soxr 重采样到目标采样率 if sr_r ! SR_TARGET or sr_d ! SR_TARGET: import soxr if sr_r ! SR_TARGET: ref soxr.resample(ref, sr_r, SR_TARGET) if sr_d ! SR_TARGET: deg soxr.resample(deg, sr_d, SR_TARGET) sr_r sr_d SR_TARGET if ref.ndim 1: ref ref[:, 0] if deg.ndim 1: deg deg[:, 0] min_len min(len(ref), len(deg)) ref ref[:min_len] deg deg[:min_len] try: score pesq(SR_TARGET, ref, deg, MODE) except Exception as e: print(f{fname} 计算失败: {e}) score None results.append({file: fname, pesq: score}) df pd.DataFrame(results) df.to_csv(pesq_report.csv, indexFalse) print(df[pesq].describe())这个脚本里我加了soxr重采样因为实际拿到的数据采样率经常不统一。soxr的质量比 scipy 默认的 resample 好对 PESQ 分数影响更小。另外用try/except包住计算避免某一条数据异常导致整个批量任务挂掉。最后输出 CSV方便后续用 pandas 做统计。3.2 分数聚合均值、中位数和置信区间拿到一批分数之后别只看均值。语音质量评测里均值容易被极端样本拉偏。我一般会同时看中位数和 5% 分位数。如果中位数和均值差很多说明分布有长尾可能有几条样本退化特别严重。这时候要单独把那几条拎出来听看看是不是处理链路有 bug。另外如果测试集本身有分类比如按信噪比分档、按说话人分性别那最好分组统计。下面这个表是我常用的报告格式分组样本数PESQ 均值PESQ 中位数PESQ 5% 分位整体5003.123.202.45高信噪比2003.853.903.50低信噪比2002.602.652.10含丢包1002.953.002.30这张表能告诉你很多信息。比如低信噪比组的 5% 分位只有 2.10说明最差的那批样本已经接近不可用了需要重点优化。而含丢包组的均值和中位数差距不大说明丢包的影响比较均匀没有特别极端的个例。3.3 和主观 MOS 的校准别让 PESQ 替你拍板PESQ 再准它也是客观指标。ITU-T 的建议是PESQ 和主观 MOS 的相关系数大概在 0.9 左右但这是在特定测试条件下。你的场景如果和标准测试条件差很远相关性会掉。所以我的习惯是每次做重大算法变更除了跑 PESQ还要拉 3 到 5 个人做一次快速主观打分不用很正式就听几组对比样本给个 1 到 5 分。然后把主观分和 PESQ 分放一起看如果趋势一致那 PESQ 可以继续用如果趋势相反那就得怀疑 PESQ 在这个场景下是不是失效了。常见的一种失效场景是处理算法引入了非线性失真比如削波。PESQ 对削波的惩罚可能不够重但人耳听起来非常刺耳。这时候 PESQ 可能只掉 0.1但主观分掉 1 分以上。遇到这种情况我会在流水线里额外加一个削波检测指标比如统计样本中超过 0.99 幅度的比例超过阈值就告警。4. 避坑与排查PESQ 分数对不上时先查这五件事4.1 现象分数异常高接近 4.5但人耳听着明显不对原因最常见的是参考语音和处理语音被意外对齐了比如处理语音其实就是参考语音的副本或者处理链路里有个直通分支。另一个可能是采样率不匹配导致的重采样把差异抹平了。解决先确认ref.wav和deg.wav不是同一个文件用md5sum比对一下。然后检查处理链路的输出是不是真的经过了算法模块。如果确认不是直通那就把两段语音的波形画出来叠在一起看肉眼就能看出是不是完全重合。4.2 现象报错ValueError: Sampling rate must be 8000 or 16000原因输入音频的采样率不是 8k 或 16k比如是 44.1k 或 48k。pesq包不会自动重采样它直接拒绝。解决在调用pesq之前用soxr或librosa重采样到 16k。注意重采样要同时作用于参考语音和处理语音且用同一个重采样器避免引入额外差异。重采样之后再检查一下音频长度因为重采样会改变样本数。4.3 现象分数波动很大同一组参数跑两次结果不一样原因PESQ 本身是确定性的同样的输入一定得到同样的输出。波动大通常是因为输入音频本身有随机性比如处理链路里用了随机噪声注入或者测试集里混入了不同采样率的文件重采样时引入了不一致。解决固定随机种子确保处理链路可复现。然后在批量脚本里加一个校验步骤打印每条音频的采样率和长度看看有没有异常值。如果发现某几条音频的采样率和其他不一样单独处理。4.4 现象窄带模式下分数普遍偏低但宽带模式下正常原因窄带模式8kHz本身的有效 MOS 范围就比宽带窄而且窄带语音的高频信息被截掉了很多在宽带下不明显的失真在窄带下会被放大。如果你的测试集是宽带语音强行降采样到 8k 再算窄带 PESQ分数低是正常的。解决确认你的业务场景是窄带还是宽带。VoIP 电话通常是窄带音乐和现代语音通信通常是宽带。不要混用。如果必须跨带宽比较那就统一到宽带或统一到窄带别拿窄带分数和宽带分数直接比。4.5 现象PESQ 分数和主观 MOS 趋势相反原因前面提过PESQ 对某些类型的失真不敏感比如平稳的谱包络偏移、某些非线性失真。如果你的算法恰好引入了这类失真PESQ 可能不降反升但人耳能听出来。解决引入补充指标。比如用 STOI 看可懂度用 POLQA 看宽带质量如果条件允许或者自己算一个谱失真指标。更重要的是建立一个小规模的主观听测集每次算法变更都跑一遍把主观分作为最终判据。PESQ 只是参考不是圣旨。5. 进阶用 PESQ 做消融实验和回归看门狗5.1 消融实验里怎么用 PESQ 定位问题模块假设你的语音增强链路有降噪、去混响、自动增益三个模块。你想知道哪个模块对最终质量影响最大。做法是跑四组实验分别是全链路、去掉降噪、去掉去混响、去掉自动增益。每组都算 PESQ然后看分数变化。但这里有个坑去掉某个模块后整体增益可能变了导致 PESQ 因为电平差异而波动。所以每组实验前都要做一遍电平归一化把输出语音的 RMS 调到和参考语音一致。下面这个函数可以帮你做归一化import numpy as np def rms_normalize(x, target_rms0.05): 把音频的 RMS 调到目标值避免增益差异干扰 PESQ current_rms np.sqrt(np.mean(x ** 2)) if current_rms 1e-8: return x gain target_rms / current_rms # 限制增益范围避免过度放大噪声 gain np.clip(gain, 0.1, 10.0) return x * gain这个函数里target_rms设成 0.05 是一个经验值对应大概 -26 dBFS。np.clip是防止某段几乎静音的音频被放大成噪声。归一化之后再算 PESQ分数变化就更能反映算法本身的贡献而不是增益差异。5.2 把 PESQ 做成回归看门狗算法迭代最怕的是“修了一个 bug引入两个新 bug”。我的做法是在 CI 里加一个 PESQ 回归测试每次代码合并前自动跑一遍固定的测试集和基线分数对比。如果整体均值下降超过 0.05或者任何一条样本下降超过 0.3就阻断合并。实现上可以用 pytest 写一个测试用例import pytest import pandas as pd BASELINE { 001.wav: 3.85, 002.wav: 3.42, 003.wav: 2.91, } def test_pesq_regression(): df pd.read_csv(pesq_report.csv) for _, row in df.iterrows(): fname row[file] score row[pesq] if fname in BASELINE: drop BASELINE[fname] - score assert drop 0.3, f{fname} PESQ 下降 {drop:.2f}超过阈值 mean_score df[pesq].mean() baseline_mean sum(BASELINE.values()) / len(BASELINE) assert baseline_mean - mean_score 0.05, f整体均值下降 {baseline_mean - mean_score:.2f}这个测试用例里BASELINE是手动维护的基线分数每次确认新版本确实更好之后就更新基线。阈值 0.3 和 0.05 可以根据你的业务容忍度调整。如果测试集很大逐条断言可能太严格那就只对关键样本做逐条断言整体只做均值断言。5.3 一个容易忽略的细节音频文件格式PESQ 对音频格式本身不敏感但对读取方式敏感。如果你用librosa.load默认会重采样到 22050Hz那就错了。soundfile.read不会重采样读出来是什么采样率就是什么所以更安全。另外wav 文件的位深也有影响16bit 和 32bit float 读出来数值范围不一样虽然 PESQ 内部会做归一化但为了可复现建议统一用 16bit PCM wav 作为测试集格式。从那以后我每次跑 PESQ 之前都强制走一遍检查采样率是不是 16k、声道是不是单声道、长度差是不是小于 5%、有没有直流偏移。这四步花不了两分钟但能省掉后面几个小时的排查。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑