从部分结果到最终结果 — 用 WER 衡量流式 ASR 与 Whisper
目标
亲手制作 WER 计算器,用同样的语句比较流式 ASR 和离线 ASR,测量准确度和“话说完之后到出最终结果”的延迟。确认电话频段、采样率误报和噪声会如何改变 WER。
为什么重要
语音助手开始回答的时刻,取决于“最终结果什么时候出来”。流式是在说话的同时转写,所以最终结果马上就出来;离线则要等话说完才开始。准确度数字(WER)会随规范化、汇总方式和评估数据的不同而大幅变化,所以必须亲手制作计算器,才能相信这些数字。评分器会用隐藏用例测试你的 wer.py,并把同一个模型重新运行一遍,与你的结果核对。
步骤
- 创建
/root/voice/asr/wer.py,让wer(ref, hyp)返回{S, D, I, N, wer}(规范化为小写、去除标点、保留撇号)。 - 把
/opt/lab/fixtures/voice/librispeech/ls01~ls08.wav按每次 100 ms 输入流式 ASR,把部分结果、最终结果和最终延迟写入/root/voice/asr/stream.jsonl。 - 把最终结果的语料库级 WER 写入
/root/voice/asr/wer_stream.json。 - 用 Whisper tiny.en 转写同样的 8 条语句,生成
/root/voice/asr/whisper.jsonl和/root/voice/asr/wer_whisper.json。 - 把两种方式“话说完之后到出最终结果”的中位数写入
/root/voice/asr/latency.json。 - 把 8 kHz 电话录音(
phone_8k.wav)的 WER 与 16 kHz 原始录音(ls04)比较,并测量把 8 kHz 谎报为 16000 时的 WER,写入/root/voice/asr/phone.json。 - 把混入 SNR 为 20 · 10 · 0 dB 的白噪声时的语料库级 WER 写入
/root/voice/asr/noise.json。 - 汇总测量结果,把会选择哪种配置写入
/root/voice/asr/report.json。
参考
- 模型:
from voicekit.models import streaming_asr, whisper——创建一次后重复使用(读取约需 1 秒多一点)。 - 流式:
s = rec.create_stream(),每个片段s.accept_waveform(16000, x[i:i+1600]),然后while rec.is_ready(s): rec.decode_stream(s),部分结果用rec.get_result(s)。最后加入 0.66 秒静音,在s.input_finished()之后再 decode 一次。 - 常见错误:把语料库级 WER 算成逐文件 WER 的平均值,从一开始就测量最终延迟(要从输入最后一个声音之后开始测量),把采样率写成 16000 却输入 8 kHz 的声音。
- 文档:sherpa-onnx · icefall · Whisper 论文(Radford et al., 2022) · LibriSpeech(Panayotov et al., 2015)
制作 WER 计算器
在 /root/voice/asr/wer.py 中创建 wer(ref, hyp)。把两段文字转成小写,去掉撇号以外的标点,然后求以单词为单位的最小编辑距离,并返回 {"S": 치환, "D": 삭제, "I": 삽입, "N": 정답 단어 수, "wer": (S+D+I)/N}(占位符依次为替换数、删除数、插入数、正确答案的单词数;如果正确答案为空,则 wer 为 0)。评分器会用 8 个隐藏用例进行测试。
把莱文斯坦距离从字符改成单词。填完表 d[i][j] 之后,从右下角往回追溯,数出各是哪种编辑。“don't”的撇号是单词的一部分,所以必须保留。
每次输入 100 ms,并记录部分结果
把 /opt/lab/fixtures/voice/librispeech/refs.tsv 中的 ls01 到 ls08 按每次 1600 个采样点(100 ms)输入流式 ASR,每当结果改变就收集 {"audio_s": 지금까지 넣은 소리 길이, "text": 부분 결과}(占位符依次为到目前为止输入的声音长度、部分结果),并把从输入最后一个声音的那一刻起,到出现最终结果为止的 ms 测为 final_ms,以每行一条(id、partials、final、final_ms)写入 /root/voice/asr/stream.jsonl。
必须在最后一个片段之后加入 0.66 秒静音并调用 input_finished(),最后一个词才会出来。final_ms 是这一收尾所花的时间——也就是话说完之后用户要等待的那一份。
语料库级 WER
用你的 wer.py 把 8 条语句的错误数和单词数分别全部相加,求出语料库级 WER,并以 S、D、I、N、wer 写入 /root/voice/asr/wer_stream.json。
不是逐文件 WER 的平均值。为了让较长的语句得到更重的权重,要用错误之和 ÷ 单词数之和来计算。
与 Whisper tiny.en 比较
用 voicekit.models.whisper() 一次性转写同样的 8 条语句,生成 /root/voice/asr/whisper.jsonl(id、text、compute_ms、audio_s),并把语料库级 WER 写入 /root/voice/asr/wer_whisper.json。
离线识别器的用法是 s = rec.create_stream(); s.accept_waveform(sr, x); rec.decode_stream(s); s.result.text。Whisper 会附带标点和大小写,所以你的规范化要在这里发挥作用。
话说完之后到出最终结果
把 stream.jsonl 的 final_ms 和 whisper.jsonl 的 compute_ms 各自的中位数(最近秩:排序后第 ⌈0.5·n⌉ 个值),以 stream_final_ms_p50、whisper_ms_p50 写入 /root/voice/asr/latency.json。
Whisper 要等话说完才开始,所以整个 compute_ms 都是用户的等待时间。流式在说话期间已经完成了大部分,所以只需要等 final_ms。
8 kHz 电话频段——以及谎报采样率时
用流式 ASR 转写与 ls04(16 kHz)同一句话的 /opt/lab/fixtures/voice/librispeech/phone_8k.wav(8 kHz),把相对于 ls04 正确答案的 WER 作为 wer_16k、wer_8k,把以 accept_waveform(16000, …) 输入 8 kHz 采样点的结果的 WER 作为 wer_8k_as_16k,写入 /root/voice/asr/phone.json。
accept_waveform 的第一个参数是声音的采样率。如实告知 8000,sherpa-onnx 会在内部转换成 16 kHz。如果谎称 16000,8,000 个采样点会被读成 0.5 秒,声音就快了一倍。
噪声越强
对 ls01 到 ls08(按排序顺序的第 k 个文件),把 np.random.default_rng(100 + k).standard_normal(n) 噪声以 SNR 20 · 10 · 0 dB(功率比)混入,用流式 ASR(100 ms 片段)转写,并把语料库级 WER 以 {"20": …, "10": …, "0": …} 写入 /root/voice/asr/noise.json。
缩放比例与模块 1 相同:sqrt(P_信号 / 10^(SNR/10) / P_噪声)。如果对每个文件固定种子,评分器就能重新生成同样的噪声来核对。
该选哪一个
在 /root/voice/asr/report.json 中,把 wer_stream、wer_whisper、stream_final_ms_p50、whisper_ms_p50、wer_8k、wer_0db 从前面步骤的文件中搬过来,并在 choice 中从 stream、whisper、two-pass 里写下一个。
要同时看延迟、准确度和数据的相似程度。也别忘了,这份数据(LibriSpeech)与流式模型的训练数据是相同的。