语言模型(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 概念
语言模型通常是一句话或者一个词 \(s\) 的概念分布 \(p(s)\),这里 \(p(s)\) 试图反映的是 \(s\) 出现的概率。例如,一个人说话的每 \(100\) 个句子中大约有 \(10\)句是 “这个需求什么时候上线?”(大概是产品经理),则可以认为:
\[
p(这个需求什么时候上线?)=0.1 \tag{1.1}
\]
而对于句子 “这个需求马上上线!”(大概是程序员),几乎没有程序员说这样的话,我们认为程序员说这句话的概率为 \(0%=\)。所以需要注意的是,语言模型与句子是否合法是没有相关性的,即使一个句子符合语法逻辑,我们仍然可以认为它出现的概率为 \(0\),这个概率和你训练模型的数据是相关的。
对于由 \(m\) 个词(这个词可以是字、词或短语等)组成的句子 \(s=w_{1}w_{2}...w_{m}\) ,其概率计算公式为:
\[
\begin{align}
p(s)
&=p(w_{1})p(w_{2}|w_{1})p(w_{3}|w_{1}w_{2})...p(w_{m}|w_{1}...w_{m-1}) \tag{1.2} \\
&=\prod_{i=1}^{m} p(w_{i}|w_{1}...w_{i-1}) \tag{1.3}
\end{align}
\]
1.2、n-gram 的产生
上式中,产生第 \(i\) 个词的概率由已经产生的 \(i-1\) 个词 \(w_{1}w_{2}...w_{i-1}\) 序列 (称作 history)决定的。这种方法计算概率有一个弊端,如果history的长度为 \(i-1\),那么就有\(L^{l-1}\) 种不同的history(\(L\) 为词表的长度),我们要计算所有 \(L^{l-1}\) 种不同history情况下产生第 \(i\) 个词的概率,这样的情况下,模型中就有 \(L^{l}\) 个自由参数 \(p(w_{i}|w_{1}...w_{i-1})\),假设 \(L=5000\),\(i=3\) 自由参数的数目就是 \(1250\)亿个,这对于我们训练模型和应用模型来说是几乎不可能实现的。为了解决这个问题,可以将history \(w_{1}w_{2}...w_{i-1}\) 序列按照某种法则映射到等价类 \(E(w_{1}w_{2}...w_{i-1})\),而等价类的数目远远小于不同history的数目,假定:
\[
\begin{align}
p(w_{i}|w_{1},w_{2},...,w_{i-1})=p(w_{i}|E(w_{1},w_{2},...,w_{i-1})) \tag{1.4}
\end{align}
\]
那么,自由参数的数目将会大大减少。有很多种方法可以将history划分为等价类,一种比较实际的做法是,将两个history \(w_{i-n+2}...w_{i-1}w_{i}\) 序列和\(v_{k-n+2}...v_{k-1}v_{k}\) 映射到同一个等价类,当且仅当这两个history最近的 \(n-1\)(\(1\leq{n}\leq{m}\))个词相同,即如果 \(E(w_{1}w_{2}...w_{i-1}w_{i})\) \(=\) \(E(v_{1}v_{2}...v_{i-1}v_{i})\),当且仅当 \(w_{i-n+2}...w_{i-1}w_{i}\) \(=\) \(v_{i-n+2}...v_{i-1}v_{i}\)。
满足以上条件的语言模型称为 \(n\)元语法或者 \(n\)元文法(n-gram)。通常情况下,\(n\)的取值不能太大,否则等价类太多,自由参数过多的问题仍然不能解决,因此在实际应用中,\(n\) 取值为 \(3\) 的情况较多。
当 \(n=1\)时,即出现在第\(i\)位上出现的词 \(w_{i}\) 独立于history。一元文法计作 unigram或者uni-gram。
当 \(n=2\)时,即出现在第\(i\)位上出现的词 \(w_{i}\) 仅与前面一个history词 \(w_{i_1}\) 有关。二元文法计作 bigram或者bi-gram。
当 \(n=3\)时,即出现在第\(i\)位上出现的词 \(w_{i}\) 仅与前面两个history词 \(w_{i-2}w_{i-1}\) 有关。三元文法计作 trigram或者tri-gram。
1.3、n-gram 计算示例
以二元语法模型为例,根据前面的解释,我们可以近似地认为,一个词的概率只依赖于它前面的一个词,那么,
\[
\begin{align}
p(s)
&=\prod_{i=1}^{m} p(w_{i}|w_{1}...w_{i-1}) \tag{1.5}\\
&\approx \prod_{i=1}^{m} p(w_{i}|w_{i-1}) \tag{1.6}
\end{align}
\]
另外,有两点值得注意:
为了使得 \(p(w_{i}|w_{i-1})\) 对于 \(i=1\)有意义,在句子的开头加上句首标记 \(\),即假设 \(w_{0}\) 就是 \(\)。
为了使得所有句子的概率之和 \(\sum_{i} p(s)\) 等于 1,需要在句子结尾加上一个句尾标记 \(\),并包含在概率计算公式(5-3)的乘积中。
例如,我们计算概率 \(p(我爱工作)\),可以这样计算:
\[
\begin{align}
p(我爱读书)=p(我|)p(爱|我)p(读|爱)p(书|读)p(|书) \tag{1.7}
\end{align}
\]
为了估计 \(p(w_{i}|w_{i-1})\) 条件概率,可以简单的计算二元语法 \(w_{i-1}w_{i}\) 在训练语料中出现的概率,然后归一化。如果用 \(c(w_{i-1}w_{i})\) 表示二元语法 \(w_{i-1}w_{i}\) 在给定文本中出现的次数,我们采用最大似然估计计算条件概率的公式如下:
\[
\begin{align}
p(w_{i}|w_{i_1})= \frac{c(w_{i-1}w_{i})}{\sum_{w_{i}}c(w_{i-1}w_{i})} \tag{1.8}
\end{align}
\]
对于 \(n>2\) 的 \(n\)元语法模型,条件概率中要考虑前面 \(n-1\)个词的概率,为了使得公式 (1.8) 对于 \(n>2\) 成立,取:
\[
\begin{align}
p(s)= \prod_{i=1}^{m+1} p(w_{i}|w_{i-n+1}^{i-1}) \tag{1.9}
\end{align}
\]
其中,\(w_{i}^{j}\) 表示词 \(w_{i}...w_{j}\),那么 \(w_{-n+2}\) 到 \(w_{0}\) 为 \(\),\(w_{m+1}\) 为 \(\)。同样的用最大似然估计计算条件概率:
\[
\begin{align}
p(w_{i}|w_{i-n+1}^{i-1})= \frac{c(w_{i-n+1}^{i})}{\sum_{w_{i}}c(w_{i-n+1}^{i})} \tag{1.10}
\end{align}
\]
具体的计算实例可以参考宗成庆老师著《统计自然语言处理(第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
安装好环境后就可以安装编译kenlm了:
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
由于.arpa文件较大,可以转化为二进制文件
bin/build_binary kenlm.arpa kenlm.klm
2.3、应用
通过安装 kenlm 的 python sdk 后我们就可以使用了。
import kenlm
strings = ["蒙 牛 纯 牛 奶", "蒙 牛 咖 啡 奶", "蒙 牛 存 牛 奶"]
kn_model = kenlm.Model('kenlm.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)就越小,对应的条件概率越大,符合第一节我们的推理过程。
同样的,越是常见的query,其对应的语言通顺度 (ppl_score)也越小,至于ppl_score计算过程依赖于ngram_score,详情可参考官网论文。
三、n-gram 语言模型的应用
正如在前沿中所述,语言模型在基于统计的语音识别、机器翻译、分词和 query纠错等应用中有广泛应用,这里我们介绍一下其在query改错中的应用。
通常的流程是:
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
最终笔者在实际应用中,纠错准确率能达到95%以上。
参考