语音算法中,流式模型是一个重要的应用场景。写一些语音流式算法的相关经验总结。
流式场景是语音中特有的一类问题。和 vc 以及 nlp 问题不同,语音输入 or 输出具有极强的时序性, 音频是以 chunk 形式发送和接收的。因此,我们不需要等待所有 chunk 发送完毕,可以边发送边处理, 从而获得更好的实时性能和用户体验。
语音算法中的流式场景
这是几个常见的流式语音任务。
- VC(voice conversation), 音频进音频出。每次输入音频片段,输出转换后的音频。
- ASR,音频进文本出。每次输入音频片段,输出该音频片段对应文本(可以为空)。 当所有音频输入结束时(输入对应结束标记),对已经输出所有文本进行重排or整合,得到最终输出文本。
- TTS,文本进音频出。输入一段文本,依次输出每段生成音频,边播放边生成下一段音频。
- vad or AED,音频进标签出,每次输入音频片段,输出帧对应标签。
流式算法的平均指标
流式语音算法实现的前提是 rtf<1.0 即处理1s音频时所用时间小于1s,事实上由于还要考虑网络延时,rtf应该远小于1。 rtf满足要求的情况下,后续处理不会阻塞,在此前提下,我们通常使用如下两个指标来评估流式系统的性能。
- 首片延时,系统处理第一个音频片段时,从接收数据到返回结果所用的时间。该指标反映了流式系统下, 用户获得反馈的最小用时,是直观体现用户体验的核心指标。
- 每路服务cpu占用率,该指标反映了系统运行时对资源的占用情况。在多路并发场景下,该每路服务使用更少的cpu, 意味着相同资源下可以处理更多服务请求,从而节约了服务器成本。
在 asr 任务中,因为最后会对所有文本进行重排,因此我们有一个额外对测试指标:
- 尾片延时,服务接收到音频结束标志符到返回最终正确结果的延时,该指标意味着流式系统下拿到最准确asr结果所用延时, 也是反应asr系统性能的核心指标。在某些应用场景(例如客服对话系统)下,尾片延时是比首片延时更重要的指标参数。
流式算法的实现
如果一个算法是流式的,且和非流式在数值上完全相同,则需要满足(这里+表示concat的意思)
f(chunk1+chunk2+chunk3...) = f(chunk1) + f(chunk2) + f(chunk3) ...
这样边可以对每个输入片段分别计算再拼接,从而达到流式效果。
对于绝大多数模型来讲,满足如下条件,只需进行少量改动,即可满足流式需求。
- 模型不使用未来时间的数据(或者只使用有限未来时间的数据)
- 模型不适用 local 统计值,例如不适用当前特征的时间平均特征信息。
CNN 层
一个经典的CNN流式结构如下(图片来源于wenet):

CNN 处理的难点在于 chunk 边界情况的处理。可以类似 wenet 在模型内部处理,使用 cache 传递中间变量。 也可以在上层代码中处理,模型和训练保持一致。两者各有所长。
如果是前者,推理结构极为简单(如下即可),但需要在模型内部做额外但处理。如果是后者,需要在外部调用完成 process 的相关逻辑。
x, state = process(x, state)
CNN 常见三种模式,
a. same for 输入输出等长。假如输入有4帧,输入输出如(下同) [0/1->0, 0/1/2->1, 1/2/3->2, 2/3->3]
b. valid 输入输出不等长 [0/1/2->1, 1/2/3->2]
c. casual 因果时序,右下文长度为0 [0/1/2->2, 1/2/3->3]
绝大多数情况下,问题都是输入输出等长序列,此时方案 a 是的首选。 但和b/c相比,方案 a 会在边缘处有冗余计算。如果 chunk 不太大的情况下,影响可能会不可忽略。
a/b两种方案如下所示。

hifigan 声码器的流式实现
hifigan 声码器的流式实现是一个典型的例子。 hifigan 模型的 geneartor 由连续多层 cnn 构成。
一个实际上线的模型中,left_context = right_context = 19(对应时间 190 ms)。 考虑到vc任务的流式请求中, 每次音频片段不超过 500ms,context 占比相当惊人。
在实际测试中,T(cnn_a)/T(cnn_b) 大约在1/2~2/3 左右,意味着至少可以带来超过30%的性能收益。
(cnn_a 和 cnn_b 对应上述图中两种方案)
Note: TTS中的hifigan使用情况有所不同。因为tts中,vocoder的mel输入由前一模型预测而来, 不会阻塞,所以我们往往可以在几十毫秒内获得100帧以上的输入,从而规避处理超短音频的问题。
Rnn 层
- 如果有流式需求,一开始就不要用双向结构(重要)。
- 对于单向 rnn(单层 or 多层),如 lstm ,只需每次计算保存隐状态值在输出给下个计算 chunk 即可。
- 尽量不要在模型中保持隐层变量,容易导致部署时模型共享带来的内存冲突问题。 (之后展开细讲)
transoformer or Attention
- 使用 mask attention 的方案,在模型训练中限制 attention 的形式,以满足流式推理要求。
- 使用动态 chunk 可以有助收敛并提升效果。
- 也可以借鉴类似 Mocha 之类的单向 attention 的方案。
推荐阅读: wenet大神写的一个技术博客,里面有相当多笔墨在涉及流式的处理。某些方法还是很有借鉴意义。https://placebokkk.github.io/wenet/2021/06/04/asr-wenet-nn-1.html
不同模型的组合
可以设计若干个 memory, 对不同模型使用不同的处理方案。例如:
# for offline
input = ...
x1 = model_a(input)
x2 = model_b(x1)
output = model_c(x2)
# for online
input_chunk = ...
input.append(input_chunk) # add more data for queue of inp
x1_chunk = model_a(input[p_inp, p_inp + len_inp]) # compute chunk of inp
x1_valid = x1_chunk[0]
x1.append(x1_valid) # add generate data for queue of x1
x2_chunk = model_b(x1[p_x1, p_x1 + len_x1]) # compute chunk of x1
...
流式特征提取
流式特征提取是另一个流式算法设计另一个主要考虑的部分。之前花了很多时间在处理流式 pitch 的相关细节,坑太多一言难尽,这里先不展开。
一个简洁有效的处理办法是,把常用的特征提取模块模型化。所谓模型化就是用神经网络框架实现常规的模型提取过程,例如基于pytorch的mel提取函数,当然使用时要阻断反向传播。 模块化的好处是可以导出后直接使用,省去了py和cpp对齐的繁琐,而且一般特征提取部分不涉及性能上的压力。
总结
设计流式算法,核心还是对效果/延迟/计算资源的平衡,需要更多在一开始就进行整体设计。