PaddleSpeech 基于 SpeechX 的 C++ ASR 部署指南:U2/U2++/DeepSpeech2 模型 FP32 与 INT8 推理实战
【免费下载链接】PaddleSpeechEasy-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 仓库中的 ASR 部署示例(demos/asr_deployment/README_cn.md),系统讲解如何利用 SpeechX 运行时对 U2 / U2++ / DeepSpeech2 等主流 ASR 模型进行 C++ 端部署。文中覆盖从环境搭建、SpeechX 编译、模型与测试数据准备,到 FP32 / INT8 双精度解码的完整操作流程,并结合 runtime 目录下的引擎源码与示例脚本,深入剖析recognizer_main的流式分块推理机制、核心命令行参数含义,以及 CER / RTF 指标的计算口径。读完本文,你将能够独立在 Linux 环境(如 Ubuntu 16.04 + Docker)中跑通一套端到端的 C++ ASR 离线/流式识别管线,并理解量化部署带来的精度与性能权衡。
一、简介:为什么用 C++ 部署 ASR
在工业实践中,语音识别模型上线时往往需要脱离 Python 训练框架、以更高的吞吐和更低的延迟运行。SpeechX 是 PaddleSpeech 提供的 C++ 推理运行时,本部署方案通过它实现对U2、U2++、DeepSpeech2三类经典 ASR 模型的 C++ 部署:
- U2:统一流式与非流式(Unified Streaming and Non-streaming)两阶段模型,兼顾 CTC 前缀束搜索的流式输出与 Attention Rescoring 的精度修正;
- U2++:U2 的增强版,引入双向(left-to-right / right-to-left)解码器,重打分(rescoring)精度更高,是当前仓库部署示例的主角;
- DeepSpeech2:经典的端到端语音识别模型,同样支持该运行时部署。
从源码结构看,C++ 推理引擎整体位于 runtime/engine/asr,包含nnet(神经网络前向)、decoder(解码器)、recognizer(识别器控制器)与server(WebSocket 服务)四大子模块;对应的可运行示例位于 runtime/examples/u2pp_ol/wenetspeech,示例采用wenetspeech 上训练的 U2++ Chunk Conformer 静态模型在 AISHELL-1 测试集上评测。
二、环境准备
部署示例对编译与运行环境有明确要求,原文档列出的环境如下:
| 组件 | 版本要求 |
|---|---|
| Python | 3.7 |
| Docker 镜像 | registry.baidubce.com/paddlepaddle/paddle:2.2.2-gpu-cuda10.2-cudnn7 |
| 操作系统 | Ubuntu 16.04.7 LTS |
| 编译器 | gcc / g++ / gfortran 8.2.0 |
| CMake | 3.16.0 |
几点实操建议:
- Docker 镜像中已内置 CUDA 10.2 / cuDNN 7 与对应版本的 PaddlePaddle 运行时,
run.sh在编译阶段依赖PADDLE_LIB_PATH定位 Paddle 动态库(见 runtime/examples/u2pp_ol/wenetspeech/path.sh 中被注释的paddle.sysconfig.get_include()逻辑),因此建议直接在上述 Docker 容器内进行编译与运行,避免宿主环境库版本冲突; - 若 CPU 指令集支持
avx512_vnni,INT8 量化模型的推理会获得额外加速(本文第五节的结果即在该类 CPU 上测得); - gfortran 8.2.0 是编译 Kaldi 风格特征前端(如 OpenBLAS、线性代数依赖)所必需的,请确保与 gcc 版本一致。
三、编译 SpeechX 运行时
示例脚本run.sh在首次运行时若检测不到SPEECHX_BUILD目录(即编译产物),会自动进入SPEECHX_ROOT执行bash build.sh完成整仓编译(对应 runtime/examples/u2pp_ol/wenetspeech/run.sh):
if [ ! -d ${SPEECHX_BUILD} ]; then pushd ${SPEECHX_ROOT} bash build.sh popd fi编译完成后,产物默认位于runtime/build/Linux/x86_64/engine/asr下,并通过path.sh将以下关键二进制目录加入PATH(见 path.sh):
nnet:神经网络前向相关 bin(如u2_nnet_main、u2_onnx_nnet);decoder:CTC 前缀束搜索 / TLG 解码器 bin;recognizer:识别器主程序recognizer_main;kaldi/fstbin、kaldi/lmbin:WFST 解码与语言模型工具;frontend/audio:音频与特征前端工具。
整个引擎的 CMake 组织可参看 runtime/engine/asr/CMakeLists.txt,其依赖(openblas、openfst、kenlm、paddleinference、fastdeploy 等)由 runtime/cmake 下的脚本统一管理。
四、U2++ 识别部署完整实操
以下步骤对应原文档第三节「例子」,实际操作目录为 runtime/examples/u2pp_ol/wenetspeech。
4.1 初始化环境变量
source path.sh该脚本会校验编译产物是否存在,并将recognizer_main等 bin 与LD_LIBRARY_PATH配置好(PADDLE_LIB_PATH需在容器内由 Paddle 环境自动注入)。
4.2 下载模型、准备测试数据与 cmvn
run.sh --stage 0 --stop_stage 1注意:原文档此处写作--stage 0 --stop_stage 1,而当前仓库中run.sh的阶段划分已调整为 stage 0 负责「下载模型与数据」、stage 1 与 stage 2 分别执行 FP32 与 INT8 解码(见 run.sh),建议以仓库脚本实际为准。stage 0 将自动完成:
- 下载FP32 静态模型:
asr1_chunk_conformer_u2pp_wenetspeech_static_1.3.0.model.tar.gz,解压后得到export.jit(Paddle 静态图模型)、mean_std.json(cmvn 均值方差文件)与unit.txt(词表/符号表); - 下载INT8 量化模型:
asr1_chunk_conformer_u2pp_wenetspeech_static_quant_1.3.0.model.tar.gz; - 准备两个测试输入:单条中文测试音频
zh.wav(生成wav.scp)与 AISHELL-1 测试集aishell_test.zip(生成aishell_test.scp,每条utt_id + wav 绝对路径)。
scp文件是 Kaldi 风格的输入描述格式,例如:
BAC009S0764W0121 /workspace/PaddleSpeech/runtime/examples/u2pp_ol/wenetspeech/data/test/S0764/BAC009S0764W0121.wav BAC009S0764W0122 /workspace/PaddleSpeech/runtime/examples/u2pp_ol/wenetspeech/data/test/S0764/BAC009S0764W0122.wav如需识别单个音频,只需构造形如key path/to/wav/file的两列 scp 即可(详见 runtime/examples/u2pp_ol/wenetspeech/README.md)。
4.3 解码:FP32 与 INT8
# FP32 ./local/recognizer.sh # INT8 ./local/recognizer_quant.sh两个脚本结构完全一致(recognizer.sh 与 recognizer_quant.sh),差异仅在于:
- 模型目录:FP32 指向
asr1_chunk_conformer_u2pp_wenetspeech_static_1.3.0.model/,INT8 指向asr1_chunk_conformer_u2pp_wenetspeech_static_quant_1.3.0.model/; - INT8 脚本的
--model_path直接传模型目录名export(Paddle 量化导出产物),FP32 传export.jit; - 日志与结果文件分别写到
recognizer.log/recognizer.quant.log,结果汇总到exp/aishell_recognizer与exp/aishell.recognizer.quant.rsl。
脚本内部以nj=20并行分片(utils/run.pl JOB=1:$nj),每个 JOB 调用一次recognizer_main,最终用 utils/compute-wer.py 以--char=1(字符级)计算 CER 并打印尾部统计。
4.4 解码输出解读
原文档给出了单条音频的典型输出,这里结合源码解释每一类日志(对应 recognizer_main.cc):
I1026 16:13:24.683531 48038 u2_recognizer_main.cc:55] utt: BAC009S0916W0495 I1026 16:13:24.683578 48038 u2_recognizer_main.cc:56] wav dur: 4.17119 sec. I1026 16:13:24.683595 48038 u2_recognizer_main.cc:64] wav len (sample): 66739 I1026 16:13:25.037652 48038 u2_recognizer_main.cc:87] Pratial result: 3 这令 I1026 16:13:25.222124 48038 u2_recognizer_main.cc:87] Pratial result: 5 这令被贷款 I1026 16:13:25.608129 48038 u2_recognizer_main.cc:87] Pratial result: 9 这令被贷款的员工们请 I1026 16:13:25.804101 48038 feature_cache.h:44] set finished I1026 16:13:25.804128 48038 feature_cache.h:51] compute last feats done. I1026 16:13:26.246963 48038 u2_recognizer_main.cc:113] BAC009S0916W0495 这令被贷款的员工们请食难安utt / wav dur / wav len:读取到测试语句 ID、音频时长(秒)与采样点数;Pratial result: N ...:流式解码过程中的部分结果(N为已累积的帧/分块编号),体现 U2++ 的流式输出能力——边接收音频边给出中间识别文本;feature_cache.h: set finished / compute last feats done:整段音频输入完毕后,特征缓存完成收尾计算(对应 runtime/engine/asr/nnet/nnet_producer.cc 中的生产者-消费者缓存);- 最后一行
utt + 识别文本:经过Attention Rescoring重打分后的最终结果,由result_writer写入ark,t格式结果文件。
4.5 主程序的流式推理原理
从 recognizer_main.cc 可以看出 C++ 部署的流式处理骨架:
- 默认
--streaming_chunk=0.36(秒)、--sample_rate=16000,因此每块音频为0.36 × 16000 = 5760个采样点; - 主循环把 wav 按块切分,逐块调用
recognizer_ptr->Accept(wav_chunk)送入特征前端(fbank 提取),全部送入后调用SetInputFinished()并等待解码线程结束(nnet_producer.h 中的 Accept / Reset 接口即此缓存队列); - 随后执行
AttentionRescoring(),用解码器得分与 CTC 得分加权融合得到最终结果; - 结束时统计
total wav duration、total decode cost并打印RTF is: ...,其中RTF = 总解码耗时 / 总音频时长(含提特征与解码,更贴近端到端口径)。
五、核心参数详解
recognizer_main的关键参数定义集中在 runtime/engine/asr/decoder/param.h 与 runtime/engine/asr/recognizer/recognizer_main.cc。下表为示例脚本中实际使用的参数及其源码注释含义:
| 参数 | 示例值 | 默认值 | 源码注释/作用 |
|---|---|---|---|
--use_fbank | true | false | 使用 fbank 特征(false 表示线性特征) |
--num_bins | 80 | 161 | Mel 滤波器组个数 |
--cmvn_file | mean_std.json | "" | cmvn 均值-方差文件路径 |
--model_path | export.jit/export | avg_1.jit.pdmodel | Paddle 静态图模型路径 |
--word_symbol_table | unit.txt | "" | 词/字符号表 |
--nnet_decoder_chunk | 16 | 1 | 神经网络前向解码分块数 |
--receptive_field_length | 7 | 7 | 两个 CNN(kernel=3) 下采样模块的感受野长度 |
--subsampling_rate | 4 | 4 | 两个 CNN(kernel=3) 模块的下采样率 |
--wav_rspecifier | scp:... | "" | 测试音频 rspecifier |
--result_wspecifier | ark,t:... | "" | 识别结果 wspecifier |
5.1 解码分块参数与感受野
--nnet_decoder_chunk、--receptive_field_length、--subsampling_rate三者共同决定模型前向的输入窗口大小与步长。在 ctc_prefix_beam_search_decoder_main.cc 中可看到如下计算:
int32 chunk_size = FLAGS_receptive_field_length + (FLAGS_nnet_decoder_chunk - 1) * FLAGS_subsampling_rate; int32 chunk_stride = FLAGS_subsampling_rate * FLAGS_nnet_decoder_chunk;即:一次前向所需帧数 = 感受野长度 +(分块数 - 1)× 下采样率;相邻两次前向的帧步长 = 下采样率 × 分块数。以示例取值(7, 16, 4)为例:chunk_size = 7 + 15 × 4 = 67 帧,chunk_stride = 64 帧——这正是 Chunk Conformer 流式模型中「当前块 + 左侧缓存上下文」的典型配置,保证流式解码的因果性。subsampling_rate还会被U2Nnet读取模型属性覆盖(见 u2_nnet.cc)。
5.2 其他可调解码参数
param.h中还定义了示例脚本未显式传入、使用默认值的解码器参数,按需可追加到命令行:
--acoustic_scale=1.0:声学分数权重;--beam=15.0、--lattice_beam=7.5、--max_active=7500:束搜索规模控制;--ctc_weight=0.5、--rescoring_weight=1.0:CTC 得分与重打分得分的融合权重(ctc_prefix_beam_search_score.h中实现);--reverse_weight=0.3:仅当使用双向 Transformer 解码器时该值 > 0 才启用从右到左解码(U2++ 即属于此情形);--num_left_chunks=-1:解码时可用的左侧块数,-1 表示不限制;--blank_threshold=0.98:blank 跳过阈值。
5.3 特征输入模式
除直接解码 wav 外,示例还支持「特征输入」模式:feat.sh负责将mean_std.json转换为 Kaldi 格式的 cmvn 并抽取 fbank 特征,之后可走recognizer_main的--feature_rspecifier输入(详见 runtime/examples/u2pp_ol/wenetspeech/README.md)。--use_fbank、--num_bins等参数即在该模式下决定特征口径。
六、部署结果:CER 与 RTF 对比
原文档在「结果」一节给出了测试口径:CER 在aishell-test上计算,RTF 包含提特征与解码(更端到端)。仓库中的完整评测数据见 runtime/examples/u2pp_ol/wenetspeech/RESULTS.md。
6.1 FP32 推理
原文档(测试机器 Intel Xeon Gold 6271C)记录:
Overall -> 5.75 % N=104765 C=99035 S=5587 D=143 I=294 Mandarin -> 5.75 % N=104762 C=99035 S=5584 D=143 I=294 English -> 0.00 % N=0 C=0 S=0 D=0 I=0 Other -> 100.00 % N=3 C=0 S=3 D=0 I=0RTF is: 0.315337RESULTS.md 在 Intel Xeon Gold 6148(同样支持 avx512_vnni)上测得 FP32 RTF 为0.265234(总时长 36108.9 秒 / 总耗时 9577.31 秒)。CER 统计行中的N/C/S/D/I含义为:N总字符数、C正确、S替换、D删除、I插入,CER = (S + D + I) / N。
6.2 INT8 量化推理
原文档记录 INT8 结果(CER 5.87% 与 5.83% 在不同版本文档中略有出入,以仓库 RESULTS.md 为准):
Overall -> 5.83 % N=104765 C=98943 S=5675 D=147 I=286 Mandarin -> 5.83 % N=104762 C=98943 S=5672 D=147 I=286 English -> 0.00 % N=0 C=0 S=0 D=0 I=0 Other -> 100.00 % N=3 C=0 S=3 D=0 I=0RTF is: 0.269674INT8 相比 FP32:CER 从 5.75% 略升至 5.83%(约 +0.08 个百分点),精度损失极小;而 RTF 在支持avx512_vnni的机器上有望获得可观加速(原文档 6271C 机器上 FP32 RTF 0.315337 → INT8 RTF 0.269674,提速约 14%)。这说明在延迟敏感、对精度容错较高的工业场景中,INT8 量化是性价比很高的部署选项。
6.3 附加:TLG 解码(无 Attention Rescoring)
RESULTS.md还提供了recognizer_wfst.sh(TLG 词图解码、不做 Attention Rescoring)的参考结果:CER 4.73%(N=104765,C=100001,S=4283,D=481,I=187),RTF 0.283。该路径对应ctc_tlg_decoder(见 runtime/engine/asr/decoder/ctc_tlg_decoder.cc),适合结合外部语言模型与 WFST 做工业级解码方案。
七、总结
本文完整走通了 PaddleSpeech 中「SpeechX C++ ASR 部署」的实战链路:
- 部署对象:U2 / U2++ / DeepSpeech2 模型,以 wenetspeech 训练的 U2++ Chunk Conformer 静态模型为示例;
- 运行方式:Ubuntu 16.04 + Paddle 2.2.2 Docker 环境内编译 runtime 引擎,通过 run.sh 一键完成模型下载、数据准备与 FP32/INT8 解码;
- 原理支撑:recognizer_main.cc 展示了 0.36s 分块的流式推理 + Attention Rescoring 两阶段解码;param.h 定义了感受野、下采样率、束宽、CTC/重打分权重等全部关键参数;
- 效果衡量:AISHELL-1 测试集上 FP32 CER 5.75%、RTF 0.265~0.315;INT8 CER 5.83%、RTF 0.270,量化方案在近乎无损精度下显著提升解码速度。
对于希望进一步扩展的读者,runtime/examples/u2pp_ol 下还提供了 FastDeploy(ONNX)与 WFST 解码等变体脚本,可直接在示例目录中参考对应local/*.sh的使用方式。
【免费下载链接】PaddleSpeechEasy-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
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考