☰
使用 KenLM 在 PaddleSpeech 中训练中文 N-gram 语言模型:从字符/词级建模到 ASR 解码集成
2026/9/25 2:44:27 网站建设 项目流程
  • 人工智能
  • 语音
  • 音频

【免费下载链接】PaddleSpeech

Easy-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 仓库中的 examples/other/ngram_lm 示例为主线,系统讲解如何基于 KenLM 从零训练中文字符级(char)与词级(word)N-gram 语言模型,完成文本预处理、ARPA 模型训练、二进制模型量化压缩,并最终将模型接入 CTC 束搜索解码作为外部语言模型打分器。读完本文,你将掌握一套可直接复跑的中文 N-gram 语言模型构建流程,以及 KenLM 评分 API 在语音识别解码中的实际用法。

一、示例定位:N-gram 语言模型在语音识别中的作用

在语音识别(ASR)系统中,声学模型负责将音频映射为字符或子词的候选序列,而语言模型(LM)负责评估候选序列在语言上的合理性。N-gram 语言模型统计"前 N-1 个词/字符条件下出现当前词/字符"的条件概率,是一种轻量、高效、无需 GPU 即可训练的传统统计语言模型。

PaddleSpeech 将这一需求封装为独立示例 examples/other/ngram_lm,其顶层 README 仅有一句话导航(s0 为 KenLM ngram lm),真正的技术内容集中在 s0/README.md 及其配套脚本中:

examples/other/ngram_lm/ ├── README.md # 顶层导航:s0 - kenlm ngram lm └── s0/ ├── README.md # 核心说明:训练流程与结果展示 ├── run.sh # 三段式(stage 0/1/2)主流程 ├── path.sh # 环境变量初始化 ├── requirements.txt # jieba 分词依赖 ├── data/ # 训练语料与纠错混淆词典 └── local/ ├── build_zh_lm.sh # 预处理 + 调 ngram_train.sh ├── download_lm_zh.sh # 下载现成中文 LM └── kenlm_score_test.py # KenLM API 功能验证

该示例的产出(ARPA 文本模型 +.klm.bin二进制模型)在仓库中至少有两处直接消费场景:一是在 examples/aishell/asr0 的 CTC 束搜索解码配置中作为lang_model_path使用;二是在 paddlespeech/s2t/decoders/scorers/ngram.py 中被封装为解码器的 N-gram Scorer。这两条集成路径会在后文详述。

二、环境准备:安装 KenLM 与 jieba

示例要求两套 Python 依赖:

  1. KenLM:语言模型训练(lmplz、build_binary命令行工具)与推理(kenlmPython 包)的核心。运行run.sh时会先做检查:
python3 -c 'import kenlm;' || { echo "kenlm package not install!"; exit -1; }
  1. jieba:中文分词器,用于词级模型的文本切分,也用于评分测试脚本中的"按词打分"。依赖声明见 examples/other/ngram_lm/s0/requirements.txt:
jieba>=0.39

此外,utils/zh_tn.py 还会用到zhon(汉字 Unicode 常量库)。在执行前先初始化示例环境(见 s0/path.sh):

. path.sh bash run.sh

path.sh的核心作用是:

export MAIN_ROOT=`realpath ${PWD}/../../../../` # 指向仓库根目录 export PATH=${MAIN_ROOT}:${MAIN_ROOT}/utils:${PATH} export LC_ALL=C export PYTHONIOENCODING=UTF-8 # 规避 LC_ALL=C 下的 UnicodeDecodeError export PYTHONPATH=${MAIN_ROOT}:${PYTHONPATH} export LD_LIBRARY_PATH=/usr/local/lib/:${LD_LIBRARY_PATH}

MAIN_ROOT指向仓库根目录,后续所有build_zh_lm.sh、ngram_train.sh都会通过它引用仓库 utils 目录下的公共工具脚本;PYTHONIOENCODING=UTF-8与LC_ALL=C的搭配是为了保证中文语料在管道传输时不出现编码解码异常。

三、快速开始:run.sh 的三段式流程

主流程脚本 s0/run.sh 采用 PaddleSpeech 标准的stage/stop_stage控制范式(参数解析由 utils/parse_options.sh 完成),共分三个阶段:

stage内容关键命令
0下载现成中文 LM,并验证 KenLM 的score/full_scores接口local/download_lm_zh.sh+local/kenlm_score_test.py
1训练中文字符级5-gram 模型local/build_zh_lm.sh --order 5 --prune "0 1 2 4 4" ... char
2训练中文词级3-gram 模型local/build_zh_lm.sh --order 3 --prune "0 0 0" ... word

脚本开头会对 kenlm 做存在性检查,并准备产物目录:

mkdir -p exp cp data/text_correct.txt exp/text

训练语料 data/text_correct.txt 为网购咨询类口语短句(例如"少先队员因该为老人让坐""祛痘印可以吗?有效果吗?"),每行一句,共 200 余行。该文件源自 pycorrector 的测试语料(来源说明见 s0/data/README.md),同目录的 custom_confusion.txt 是"变体→本体"的错别字混淆表,供后续文本纠错场景参考,本示例的训练流程本身并不消费它。

如需只跑某一阶段,可用 PaddleSpeech 通用的方式指定,例如仅训练字符级模型:

bash run.sh --stage 1 --stop_stage 1

四、Stage 0:下载现成中文 LM 与 KenLM API 验证

4.1 下载 5-gram 中文二进制模型

local/download_lm_zh.sh 会从 Paddle 官方对象存储下载一个训练于中文 Gigaword 语料、经过 prune01244 剪枝的字符级 5-gram 二进制模型:

DIR=data/lm mkdir -p ${DIR} URL='https://deepspeech.bj.bcebos.com/zh_lm/zh_giga.no_cna_cmn.prune01244.klm' MD5="29e02312deb2e59b3c8686c7966d4fe3" TARGET=${DIR}/zh_giga.no_cna_cmn.prune01244.klm

下载工具封装在 utils/utility.sh 的download函数中,会校验 MD5;文件已存在则直接跳过。该模型同时被 ASR 解码示例复用——examples/aishell/asr0/local/download_lm_ch.sh 下载的是同一 URL。

4.2 KenLM 评分 API 详解

local/kenlm_score_test.py 是理解 KenLM Python 接口的最佳入口,它覆盖了四个核心能力:

1.model.score(sentence)——整句对数概率

返回整句的 log10 概率(内部自动拼接<s>与</s>):

model = kenlm.Model(language_model_path) # 加载二进制模型 print(model.score(sentence)) # 未分词的整句 print(model.score(sentence_char_split)) # 字符级切分(空格分隔) print(model.score(sentence_word_split)) # jieba 词级切分

测试脚本还校验了一个关键行为:full_scores返回的条目数应等于"空格切分后的 token 数 + 1",即包含句首<s>状态:

assert split_size == len(sentence_char_split.split()) + 1, "error split size."

2.model.full_scores(sentence)——逐 token 打分

full_scores逐 token 返回三元组(log10_prob, ngram_length, is_oov),可用于观察每个字符实际命中了多长的 n-gram 上下文:

words = ['<s>'] + list(sentence) + ['</s>'] for i, (prob, length, oov) in enumerate(model.full_scores(sentence_char_split)): print('{0} {1}: {2}'.format(prob, length, ' '.join(words[i + 2 - length: i + 2]))) if oov: print('\t"{0}" is an OOV'.format(words[i + 1]))

3. OOV(词表外)检测

通过w not in model判断 token 是否在词表中。由于下载的zh_giga.no_cna_cmn.prune01244.klm是字符级模型,词级切分后几乎全部成词都会 OOV:

# 字符级切分下,OOV 应为三个特殊字符 assert oov == ["❗", "️", "!"], 'error oov' # 词级切分下,绝大多数词 OOV("盘点"、"不怕"、"网站"……) assert oov == ["盘点", "不怕", "网站", "❗", "️", "海淘", "向来", "便宜", "保真", "!"], 'error oov'

4.perplexity(sentence)与 BOS/EOS 控制

full_scores(sentence, bos=False, eos=False)可以去掉句首/句尾标记,此时返回条数与输入 token 数严格相等;model.perplexity()计算句子困惑度,测试脚本用"先救挨饿的人"与"先就挨饿的人"两组同音易混句对比 PPL,验证模型对正确写法应给出更低困惑度:

n = model.perplexity(sentence_char_split1) # '先 救 挨 饿 的 人 , 然 后 治 疗 病 人 。' n = model.perplexity(sentence_char_split2) # '先 就 挨 饿 的 人 , 然 后 治 疗 病 人 。'

这些接口正是 PaddleSpeech 解码器内部使用 KenLM 的底层依据(详见第七节)。

五、Stage 1 & 2:训练字符级与词级中文 N-gram 模型

5.1 参数速查与产物命名规则

run.sh中两个 stage 的核心参数如下:

参数字符级(stage 1)词级(stage 2)含义
token_typecharword建模粒度
order53N-gram 阶数
prune0 1 2 4 40 0 0各阶剪枝计数阈值
a2222二进制模型指针压缩位数
q88概率量化位数
b88二值化位数

产物文件名把关键超参全部编码进名字,便于管理多版本模型:

text_zh_char_o5_p0_1_2_4_4_a22_q8_b8.arpa text_zh_char_o5_p0_1_2_4_4_a22_q8_b8.arpa.klm.bin text_zh_word_o3_p0_0_0_a22_q8_b8.arpa text_zh_word_o3_p0_0_0_a22_q8_b8.arpa.klm.bin

命名规则为输入名_zh_<char|word>_o<order>_p<prune>_a<a>_q<q>_b<b>.arpa。其中p0_1_2_4_4即prune="0 1 2 4 4"去掉空格后的紧凑写法,含义是 1-gram 剪枝阈值 0、2-gram 阈值 1、3-gram 阈值 2、4-gram 阈值 4、5-gram 阈值 4。

5.2 文本预处理:zh_tn.py

local/build_zh_lm.sh 首先调用仓库公共脚本 utils/zh_tn.py 做中文文本正则化(Text Normalization,TN)+ 切分:

python3 ${MAIN_ROOT}/utils/zh_tn.py --token_type ${type} ${text} ${text}.${type}.tn

zh_tn.py是一份 1400 余行的中文 TN 实现(源自 speechio/chinese_text_normalization),负责:

  • 数字转中文:如"13斤"→"十三斤"(CHINESE_DIGIS、大/小写数字系统、POSITIVE/NEGATIVE/POINT等常量控制转换规则);
  • 标点归一化:基于zhon库的hanzi常量区分离散标点(CHINESE_PUNC_STOP如!?。。)与非停标点(CHINESE_PUNC_NON_STOP),统一全半角;
  • 语气词、儿化音等白名单处理:如ER_WHITELIST保护"女儿、儿童、托儿所"等词不被误切;
  • token 级切分:--token_type char时逐字以空格分隔输出,--token_type word时调用jieba.lcut按词分隔。

预处理前后对比(取自 s0/README.md 的结果展示):

==> exp/text <== 少先队员因该为老人让坐 祛痘印可以吗?有效果吗? 我家宝宝13斤用多大码的 ==> exp/text.char.tn <== 少 先 队 员 因 该 为 老 人 让 坐 祛 痘 印 可 以 吗 有 效 果 吗 我 家 宝 宝 十 三 斤 用 多 大 码 的 ==> exp/text.word.tn <== 少先队员 因该 为 老人 让 坐 祛痘 印 可以 吗 有 效果 吗 我家 宝宝 十三斤 用多大码 的

可以看到"13斤"在字符级被展开为"十三斤"、"用多大码"在词级被 jieba 整体切分为一个词。切分后的文本才是lmplz的训练输入——KenLM 要求训练语料以空格分隔的 token 序列形式按行输入。

5.3 训练与压缩:ngram_train.sh

预处理完成后,build_zh_lm.sh 调用仓库公共脚本 utils/ngram_train.sh 完成建模与二进制化:

# 1) lmplz:从分词文本估计 n-gram 计数并输出 ARPA 格式 lmplz -o ${order} -S ${mem} --prune ${prune} < ${text} > ${arpa} # 2) build_binary:将 ARPA 压缩为 trie 二进制模型 build_binary -a ${a} -q ${q} -b ${b} trie ${arpa} ${lmbin}

lmplz参数说明:

参数默认值作用
-o5N-gram 阶数,决定上下文长度
-S80%训练内存上限,可为绝对值(如2G)或百分比
--prune0各阶剪枝阈值,计数低于阈值的 n-gram 被丢弃,用于控制模型体积与过拟合

build_binary参数说明:

参数默认值作用
trie—存储结构,trie在内存与速度间取得平衡,另有probing可选
-a22trie 指针压缩位数,值越小模型越小、可能略增查询开销
-q8概率量化位数(对数概率的存储精度)
-b8二值化位数

实现细节值得注意:build_zh_lm.sh虽接收--a/--q/--b参数(由 parse_options 解析),但脚本内部只把--order/--mem/--prune转发给ngram_train.sh;实际生效的是 ngram_train.sh 中的默认值a=22、q=8、b=8,恰好与产物文件名a22_q8_b8一致。另外 run.sh 中传给build_zh_lm.sh的写法是--q ${a}(值为 22),由于该参数不被转发,不影响最终结果。读者如自行修改a/q/b期望生效,需要同步修改ngram_train.sh的传参链路。

5.4 剪枝的意义:从结果看参数选择

从 s0/README.md 给出的 ARPA 统计可以直观看到剪枝效果。字符级 5-gram(prune "0 1 2 4 4"):

\data\ ngram 1=587 ngram 2=395 ngram 3=100 ngram 4=2 ngram 5=0

而词级 3-gram(prune "0 0 0",不剪枝):

\data\ ngram 1=689 ngram 2=1398 ngram 3=1506

两组数字对比明显:同样的 200 余行小语料,字符级 5-gram 在剪枝后 5-gram 数量被压到 0(高阶计数全部低于阈值 4),4-gram 仅剩 2 条;词级 3-gram 不剪枝时 3-gram 高达 1506 条。这解释了示例选择"字符 5-gram 配剪枝、词 3-gram 不剪枝"的搭配逻辑——字符组合爆炸快,必须剪枝控制体积;词级在小语料下 3 阶已能覆盖多数上下文。ARPA 文件头部还展示了概率格式(log10):

\1-grams: -3.272324 <unk> 0 0 <s> -0.36706257

每行格式为log10概率 <token> [回退权重],<unk>、<s>、</s>是 KenLM 约定的特殊标记。

六、产物解读:exp 目录与文件清单

按 s0/README.md 展示,完整运行run.sh后exp/目录共 7 个文件:

exp/ ├── text # 原始语料副本 ├── text.char.tn # 字符级 TN 结果 ├── text.word.tn # 词级 TN 结果(jieba 分词) ├── text_zh_char_o5_p0_1_2_4_4_a22_q8_b8.arpa # 字符 5-gram ARPA ├── text_zh_char_o5_p0_1_2_4_4_a22_q8_b8.arpa.klm.bin # 字符 5-gram 二进制 ├── text_zh_word_o3_p0_0_0_a22_q8_b8.arpa # 词 3-gram ARPA └── text_zh_word_o3_p0_0_0_a22_q8_b8.arpa.klm.bin # 词 3-gram 二进制

README 同时给出了各文件的 MD5 校验值(如3ae083627b9b6cef1a82d574d8483f97 exp/text),可用于核对产物是否一致。ARPA 文本模型方便人读与二次加工(如合并、插值),.klm.bin二进制模型体积小、加载快,是实际解码时的首选。

七、接入 ASR 解码:NgramScorer 与解码配置

本示例训练的 LM 并非孤立产物,PaddleSpeech 已内置完整的消费链路。

7.1 解码器侧的 N-gram Scorer

paddlespeech/s2t/decoders/scorers/ngram.py 实现了基于 KenLM 的 N-gram 打分器,类图结构为:

  • Ngrambase:抽象基类,持有kenlm.LanguageModel与kenlm.State,负责将 PaddleSpeech 词表映射为 KenLM token(<eos>映射为</s>);
  • NgramFullScorer:继承BatchScorerInterface,对词表全部 token 打分,用于全集打分;
  • NgramPartScorer:继承PartialScorerInterface,对候选子集打分,用于部分展开。

核心打分逻辑在score_partial_中,逐 token 维护 KenLM 状态机:

self.lm = kenlm.LanguageModel(ngram_model) state = kenlm.State() self.lm.NullContextWrite(state) # 初始化空上下文状态 ... self.lm.BaseScore(state, ys, out_state) # 基于历史状态推进 scores[i] = self.lm.BaseScore(out_state, self.chardict[j], self.tmpkenlmstate)

它依赖的ScorerInterface(见 paddlespeech/s2t/decoders/scorers/scorer_interface.py)定义了束搜索中外部打分器(长度奖励、神经网络 LM、N-gram LM 等)的统一接口契约。也就是说,KenLM 在这里以"外部打分器"身份与声学模型得分融合,从而在解码时引入语言先验。

7.2 解码配置文件中的使用方式

examples/aishell/asr0/conf/tuning/decode.yaml 展示了标准接入配置:

decode_batch_size: 128 error_rate_type: cer decoding_method: ctc_beam_search lang_model_path: data/lm/zh_giga.no_cna_cmn.prune01244.klm alpha: 2.2 beta: 4.3 beam_size: 500 cutoff_prob: 0.99 cutoff_top_n: 40 num_proc_bsearch: 10

其中与本主题直接相关的两个关键项:

  • lang_model_path:指向 KenLM 二进制模型(正是 stage 0 下载或自定义训练的.klm.bin),解码时由Ngrambase加载;
  • alpha/beta:语言模型得分与声学得分的融合权重——alpha为 LM 对数概率的缩放系数,beta为句长补偿偏置,两者需要在开发集上网格调优。

这组配置意味着:你完全可以用第五节训练的text_zh_char_o5_p0_1_2_4_4_a22_q8_b8.arpa.klm.bin替换lang_model_path,对领域语料定制语言模型,从而在特定场景(如电商咨询、口语短句)获得更贴合的解码先验。decoding_method: ctc_beam_search表明该 LM 主要服务于 CTC 束搜索解码路径。

八、参数调优建议与注意事项

  1. 阶数与语料量匹配:小语料建议 3-gram 起步;语料达到千万句量级再考虑 5-gram 以上,否则高阶 n-gram 大量稀疏,需配合剪枝(参考 5.4 节的统计对比)。
  2. 剪枝阈值从低阶到高阶递增:KenLM 的--prune按1-gram 2-gram ... N-gram顺序给出计数阈值,高阶阈值通常设得更高(示例字符级0 1 2 4 4即遵循该思路)。
  3. 量化压缩是上线标配:build_binary -a 22 -q 8 -b 8是体积与精度的常用平衡点;若追求更小体积可尝试-a 21或更低q,需在验证集上确认 PPL 与 WER 不显著劣化。
  4. 预处理一致性至关重要:训练侧(zh_tn.py的 TN 与切分)必须与解码侧的分词/正则化策略保持一致,否则 LM 打分时会大量命中 OOV(见 4.2 节的 OOV 断言示例)。
  5. 编码环境:务必按 s0/path.sh 设置LC_ALL=C与PYTHONIOENCODING=UTF-8,避免中文语料在 shell 管道中出现编码错误。

九、小结

通过 examples/other/ngram_lm 示例,PaddleSpeech 提供了一条从"原始中文文本"到"可部署语言模型"的完整流水线:zh_tn.py负责文本正则化与 char/word 切分 →lmplz负责 N-gram 计数与剪枝并输出 ARPA →build_binary负责量化为 trie 二进制 → KenLM Python API(score/full_scores/perplexity)负责推理验证 → 最终经 ngram.py 的NgramFullScorer/NgramPartScorer接入 CTC 束搜索解码,或直接填入 decode.yaml 的lang_model_path使用。掌握这条链路,即可为任意中文领域语料定制轻量语言模型,为 ASR 解码引入领域语言先验。

  • 人工智能
  • 语音
  • 音频

【免费下载链接】PaddleSpeech

Easy-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
点击查看免费下载

相关推荐

上一篇:告别繁琐配置:NAS-Tools与Synology DSM深度集成指南
下一篇:VAR模型部署到边缘设备:TensorRT INT8量化实践

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询