N-Gram语言模型初探

语言模型(Language Model,LM)在自然语言处理中占有十分重要的地位,尤其在基于统计的语音识别、机器翻译、分词和 query纠错等相关应用中有着广泛的应用。目前主要采用的是 n 元语法模型(n-gram model)。笔者在工作用中应用到了 query改写和 query 的纠错,均起到了不错的应用效果,本文将从一下几点介绍 n-gram 语言模型。

  • n-gram 语言模型基本概念
  • n-gram 语言模型的工具 — kenlm
  • n-gram 语言模型的应用

一、n-gram 语言模型基本概念

1.1、n-gram 概念

语言模型通常是一句话或者一个词 的概念分布 ,这里 试图反映的是 出现的概率。例如,一个人说话的每 个句子中大约有 句是 “这个需求什么时候上线?”(大概是产品经理?),则可以认为:


这个需求什么时候上线

而对于句子 “这个需求马上上线!”(大概是程序员?),几乎没有程序员说这样的话,我们认为程序员说这句话的概率为 。所以需要注意的是,语言模型与句子是否合法是没有相关性的,即使一个句子符合语法逻辑,我们仍然可以认为它出现的概率为 ,这个概率和你训练模型的数据是相关的。对于由 个词(这个词可以是字、词或短语等)组成的句子 ,其概率计算公式为:

1.2、n-gram 的产生

上式中,产生第 个词的概率由已经产生的 个词 序列 (称作 history)决定的。这种方法计算概率有一个弊端,如果history的长度为 ,那么就有 种不同的history( 为词表的长度),我们要计算所有 种不同history情况下产生第 个词的概率,这样的情况下,模型中就有 个自由参数  ,假设 , 自由参数的数目就是 亿个,这对于我们训练模型和应用模型来说是几乎不可能实现的。为了解决这个问题,可以将history 序列按照某种法则映射到等价类 ,而等价类的数目远远小于不同history的数目,假定:

那么,自由参数的数目将会大大减少。有很多种方法可以将history划分为等价类,一种比较实际的做法是,将两个history 序列和 映射到同一个等价类,当且仅当这两个history最近的 ()个词相同,即如果 ,当且仅当 。满足以上条件的语言模型称为 元语法或者 元文法(n-gram)。通常情况下,的取值不能太大,否则等价类太多,自由参数过多的问题仍然不能解决,因此在实际应用中, 取值为 的情况较多。

  • 当 时,即出现在第位上出现的词 独立于history。一元文法计作 unigram或者uni-gram。
  • 当 时,即出现在第位上出现的词 仅与前面一个history词 有关。二元文法计作 bigram或者bi-gram。
  • 当 时,即出现在第位上出现的词 仅与前面两个history词 有关。三元文法计作 trigram或者tri-gram。

1.3、n-gram 计算示例

以二元语法模型为例,根据前面的解释,我们可以近似地认为,一个词的概率只依赖于它前面的一个词,那么,

另外,有两点值得注意:

  • 为了使得 对于 有意义,在句子的开头加上句首标记 $,即假设w_{0}就是$。
  • 为了使得所有句子的概率之和 等于 1,需要在句子结尾加上一个句尾标记 $$,并包含在概率计算公式(5-3)的乘积中。

例如,我们计算概率 我爱工作,可以这样计算:

$$\begin{align} p(我爱读书)=p(我|)p(爱|我)p(读|爱)p(书|读)p(|书) \nonumber \end{align} $$


为了估计 条件概率,可以简单的计算二元语法 在训练语料中出现的概率,然后归一化。如果用 表示二元语法 在给定文本中出现的次数,我们采用最大似然估计计算条件概率的公式如下:

对于 的 元语法模型,条件概率中要考虑前面 个词的概率,为了使得公式 (1.8) 对于 成立,取:

其中, 表示词 ,那么 到 为 $,w_{m+1}为$。同样的用最大似然估计计算条件概率:

具体的计算实例可以参考宗成庆老师著《统计自然语言处理(第2版)》

二、n-gram 语言模型的工具 — kenlm

限于篇幅,这里我们仅仅介绍如何安装和使用kenlm,详细信息参考kenlm官网,后续另开一文详细介绍这个工具中n-gram分数和ppl_socre(语句通顺度)的计算过程。

2.1、安装

笔者成功在macOS和centos上成功安装并使用了kenlm,windows下使用需要用cygwin 64模拟linux环境使用。需要安装Boost 和zlib以及gcc。

yum -y install gcc gcc-c++ boost boost-devel zlib zlib-devel

yum -y install gcc gcc-c++ boost boost-devel zlib zlib-devel

安装好环境后就可以安装编译kenlm了:

git clone https://github.com/kpu/kenlm.git
mkdir -p build
cd build
cmake ..
make -j 4

git clone https://github.com/kpu/kenlm.git
mkdir -p build
cd build
cmake ..
make -j 4

2.2、训练模型

训练

/bin/lmplz -o 3 --verbose_header --text corpus.txt --arpa kenlm.arpa

/bin/lmplz -o 3 --verbose_header --text corpus.txt --arpa kenlm.arpa

由于.arpa文件较大,可以转化为二进制文件

bin/build_binary kenlm.arpa kenlm.klm

bin/build_binary kenlm.arpa kenlm.klm

2.3、应用

通过安装 kenlm 的 python sdk 后我们就可以使用了。

import kenlm
strings = ["蒙 牛 纯 牛 奶", "蒙 牛 咖 啡 奶", "蒙 牛 存 牛 奶"]
kn_model = kenlm.Model('language_model_char.klm')
for i in range(len(strings)):
    print("query: ", strings[i])
    print("ngram_score: ", kn_model.score(strings[i], bos=True, eos=True))
    print("ppl_score: ", kn_model.perplexity(strings[i]))
    print("========================================")

import kenlm
strings = ["蒙 牛 纯 牛 奶", "蒙 牛 咖 啡 奶", "蒙 牛 存 牛 奶"]
kn_model = kenlm.Model('language_model_char.klm')
for i in range(len(strings)):
    print("query: ", strings[i])
    print("ngram_score: ", kn_model.score(strings[i], bos=True, eos=True))
    print("ppl_score: ", kn_model.perplexity(strings[i]))
    print("========================================")

得到结果如下:

query:  蒙 牛 纯 牛 奶
ngram_score:  -5.382442951202393
ppl_score:  7.889942264061641
========================================
query:  蒙 牛 咖 啡 奶
ngram_score:  -10.560464859008789
ppl_score:  57.554260281408254
========================================
query:  蒙 牛 存 牛 奶
ngram_score:  -15.350200653076172
ppl_score:  361.7152136927609
========================================

query:  蒙 牛 纯 牛 奶
ngram_score:  -5.382442951202393
ppl_score:  7.889942264061641
========================================
query:  蒙 牛 咖 啡 奶
ngram_score:  -10.560464859008789
ppl_score:  57.554260281408254
========================================
query:  蒙 牛 存 牛 奶
ngram_score:  -15.350200653076172
ppl_score:  361.7152136927609
========================================

需要补充的是,为了方便计算条件概率的乘积过程,我们将概率取对数,那么求积就转化成求和。从结果中我们可以得到结论:

  • 越是常见的 query ,其对应的取对数后的概率(ngram_score)就越小,对应的条件概率越大,符合第一节我们的推理过程。
  • 同样的,越是常见的query,其对应的语言通顺度 (ppl_score)也越小,至于ppl_score计算过程依赖于ngram_score,详情可参考官网论文。

三、n-gram 语言模型的应用

正如在前沿中所述,语言模型在基于统计的语音识别、机器翻译、分词和 query纠错等应用中有广泛应用,这里我们介绍一下其在query改错中的应用。

tansformer encoder 语言模型 ngram语言模型训练_等价类

通常的流程是:

  • query 分词。一般常用多种方式(分词工具 + ngram)切分出尽量多的词组。
  • 检测疑似错别字。主要是自定义的纠错逻辑以及分词后未登录词和通过n-gram平滑得到疑似错误字词。
  • 疑似错别字寻找其正确候选集。主要是通过同音近形字、编辑距离词以及自定义混淆词,并通过词表过滤掉不在此表中的词,最终得到候选集。
  • 语言模型纠正。自定义混淆词可以不用这一步,直接替换;其他几种情况通过语言模型计算语义通顺度,得到得分最小的作为正确词(通常需要有一个最小分词的阈值逻辑判断是否修改正确)。

笔者这里有一些纠错案例:

origin: 月饼莲蓉 | score: 234.13980266961732 ----> corrected: 莲蓉月饼 | score: 36.40022878394902

origin: 好奇纯绵棉柔巾 | score: 122.08895880088059 ----> corrected: 好奇纯棉棉柔巾 | score: 43.72525151069142

origin: 硬毛芽刷 | score: 3071.285575052603 ----> corrected: 硬毛牙刷 | score: 87.96634329493364

origin: 伊立牛奶 | score: 275.74163855910786 ----> corrected: 伊利牛奶 | score: 18.14028069230779

origin: 利伊牛奶 | score: 1009.111608062079 ----> corrected: 伊利牛奶 | score: 18.14028069230779

origin: 同人堂洗发液 | score: 646.2303979080308 ----> corrected: 同仁堂洗发液 | score: 27.881235951440328

origin: 军乐宝酸奶 | score: 244.22472729364367 ----> corrected: 君乐宝酸奶 | score: 12.32559814126351

origin: 伊犁牛奶 | score: 167.27450985853162 ----> corrected: 伊利牛奶 | score: 18.14028069230779

origin: 酒精噴雾 | score: 8307.144176007858 ----> corrected: 酒精喷雾 | score: 55.69452559695505

origin: 猎头肉 | score: 1074.2781682994482 ----> corrected: 猪头肉 | score: 45.298965075390726

origin: 月饼莲蓉 | score: 234.13980266961732 ----> corrected: 莲蓉月饼 | score: 36.40022878394902

origin: 好奇纯绵棉柔巾 | score: 122.08895880088059 ----> corrected: 好奇纯棉棉柔巾 | score: 43.72525151069142

origin: 硬毛芽刷 | score: 3071.285575052603 ----> corrected: 硬毛牙刷 | score: 87.96634329493364

origin: 伊立牛奶 | score: 275.74163855910786 ----> corrected: 伊利牛奶 | score: 18.14028069230779

origin: 利伊牛奶 | score: 1009.111608062079 ----> corrected: 伊利牛奶 | score: 18.14028069230779

origin: 同人堂洗发液 | score: 646.2303979080308 ----> corrected: 同仁堂洗发液 | score: 27.881235951440328

origin: 军乐宝酸奶 | score: 244.22472729364367 ----> corrected: 君乐宝酸奶 | score: 12.32559814126351

origin: 伊犁牛奶 | score: 167.27450985853162 ----> corrected: 伊利牛奶 | score: 18.14028069230779

origin: 酒精噴雾 | score: 8307.144176007858 ----> corrected: 酒精喷雾 | score: 55.69452559695505

origin: 猎头肉 | score: 1074.2781682994482 ----> corrected: 猪头肉 | score: 45.298965075390726

最终笔者在实际应用中,纠错准确率能达到95%以上。

参考

  • 《统计自然语言处理(第2版)》
  • kenlm官网
  • pycorrector