什么是ngram语言模型
语言模型是NLP中最最基础的模块,从传统基于统计的ngram语言模型,再到基于深度学习的CNN,RNN语言模型,再到现在基于tranformer的预训练语言模型,每次语言模型的发展都能给整个NLP领域带来巨大推动。
由于传统的ngram语言模型具备原理简单,推断速度快等特点,所以至今依然在广泛应用在众多NLP任务中,尤其在计算资源受限的移动端。本文将系统介绍ngram语言模型的内部原理,计算方法及相关工具。
ngram语言模型计算方法
给定一句话:
![\{w_{1},w_{2},w_{3},...,w_{n}\}](https://i-blog.csdnimg.cn/blog_migrate/4f7bd9e0334cddbc9f36053b75c2f95b.png)
![w_{i}](https://i-blog.csdnimg.cn/blog_migrate/ebe506140269f2d75d4a0df2f3185356.png)
![P(w_{1}^{n}) = p(w_{1}) p(w_{2}|w_{1}) p(w3|w_{1}^{2})...p(w_{n}|w_{1}^{n-1})](https://i-blog.csdnimg.cn/blog_migrate/6a8918351c7fb0bad476e8902d796f4b.png)
对于其中的每一小项
![p(w_{k}|w_{1}^{k-1})](https://i-blog.csdnimg.cn/blog_migrate/a5bdb8ed6053ede97952a15283878cb9.png)
![p({w_{k}|w_{1}^{k-1}}) = \frac{\#\{w_{1}^{k}\}}{\#\{w_{1}^{k-1}\}}](https://i-blog.csdnimg.cn/blog_migrate/a9c8cea5e449d8e0d397df5358167cfe.png)
这里
![\#\{{w_{1}^{k}}\}](https://i-blog.csdnimg.cn/blog_migrate/91b03b759bde4fd92dcecf930a420319.png)
![\{w_{1},w_{2},w_{3},...,w_{k}\}](https://i-blog.csdnimg.cn/blog_migrate/62bab778fee39891905e2f85d63f982e.png)
理论上语料足够充足,就可以很好的用频率直接估计出概率,但实际操作中对于较长的序列
![w_{1}^{k}](https://i-blog.csdnimg.cn/blog_migrate/075d496cf9183cdd74c54ed63fd1636c.png)
ngram语言模型的核心就在于一个强假设:当前词的概率分布只与前N-1个词有关,即:
![p(w_{k}|w_{1}^{k-1}) = p(w_{k}|w_{k-(N-1)}^{k-1})](https://i-blog.csdnimg.cn/blog_migrate/a5bdb8ed6053ede97952a15283878cb9.png+%3D+p%28w_%7Bk%7D%7Cw_%7Bk-%28N-1%29%7D%5E%7Bk-1%7D%29+)
本质上 N-gram 模型的假设类似于马尔可夫链当中的 N-1 阶马尔可夫性假设。通常情况下n=1,2,3。对于再高阶的4-gram,5-gram就很少见,因为需要非常大的语料才能训练充分高阶的语言模型,而且模型本身的体积也会非常大(占内存)。
- 当n=1时为unigram:当前词的概率分布与历史信息无关
- 当n=2时为bigram:当前词的概率分布只和前一个词有关
- 当n=3时为trigram:当前词的概率分布只和前两个词有关
所以假设n=2,p(处理| {我, 爱, 自然, 语言}) = p(处理| 语言) = #{语言, 处理} / #{语言} ,这样相对而言就可算了。
基于kenlm的ngram语言模型训练
运用ngram语言模型目前最便捷的工具就是kenlm,可快速实现语言模型的训练与应用。
首先准备一份语言模型训练语料(test_corpus.txt)注意每个词之前需要空格分割,如果训练基于字的语言模型,则每个字之前用空格分割。
模型
语言 模型
传统
模型
语言
关于kenlm的安装网上有很多教程,实际操作的过程中也确实有坑,为了避免踩坑,可直接采用docker来获得已安装了kenlm的环境。具体参见:GitHub - nghuyong/kenlm-docker: docker for kenlm 。下面采用kenlm训练一个bigram语言模型:
# 拉取镜像
docker pull nghuyong/kenlm
# 启动并进入容器
docker run -it -v $(pwd):/var nghuyong/kenlm bash
# 容器内训练kenlm
./lmplz -o 2 --verbose_header --text /var/test_corpus.txt --arpa /var/arpa.kenlm
这样就完成了语言模型的训练,并获得arpa模型文件。
下面是输出的apra文件
# Input file: /var/test_corpus.txt
# Token count: 6
# Smoothing: Modified Kneser-Ney
\data\
ngram 1=6
ngram 2=7
\1-grams:
-0.89085555 <unk> 0
0 <s> -0.22184873
-0.89085555 </s> 0
-0.46488678 模型 0
-0.69896996 语言 -0.30103
-0.69896996 传统 -0.30103
\2-grams:
-0.89085555 模型 </s>
-0.50267535 语言 </s>
-0.24850096 传统 </s>
-0.44889864 <s> 模型
-0.37527603 语言 模型
-0.56863624 <s> 语言
-0.6575773 <s> 传统
\end\
可以看到生成的arpa文件包括ngram的统计值以及ngram的概率。基于这份arpa文件就可以计算一句话的概率分布了。
再看一下kenlm训练过程输出的日志情况:
=== 1/5 Counting and sorting n-grams ===
Reading /data/mm64/rightyonghu/code/kenlm/build/bin/test_corpus.txt
----5---10---15---20---25---30---35---40---45---50---55---60---65---70---75---80---85---90---95--100
****************************************************************************************************
Unigram tokens 6 types 6
=== 2/5 Calculating and sorting adjusted counts ===
Chain sizes: 1:72 2:33536714342
Statistics:
1 6 D1=0.5 D2=0.5 D3+=3
2 7 D1=0.5 D2=1.25 D3+=3
Memory estimate for binary LM:
type B
probing 292 assuming -p 1.5
probing 320 assuming -r models -p 1.5
trie 226 without quantization
trie 1235 assuming -q 8 -b 8 quantization
trie 226 assuming -a 22 array pointer compression
trie 1235 assuming -a 22 -q 8 -b 8 array pointer compression and quantization
=== 3/5 Calculating and sorting initial probabilities ===
Chain sizes: 1:72 2:112
----5---10---15---20---25---30---35---40---45---50---55---60---65---70---75---80---85---90---95--100
####################################################################################################
=== 4/5 Calculating and writing order-interpolated probabilities ===
Chain sizes: 1:72 2:112
----5---10---15---20---25---30---35---40---45---50---55---60---65---70---75---80---85---90---95--100
####################################################################################################
=== 5/5 Writing ARPA model ===
----5---10---15---20---25---30---35---40---45---50---55---60---65---70---75---80---85---90---95--100
****************************************************************************************************
Name:lmplz VmPeak:33297604 kB VmRSS:3344 kB RSSMax:8928344 kB user:0.484 sys:3.636 CPU:4.12075 real:4.12281
根据日志可以看出,kenlm在训练语言模型时候分成了5个主要步骤:统计并排序ngram,计算并排序调整就计数,计算并排序初始概率,计算并写入差值概率以及生成arpa模型文件。
下面我们就将细致拆解这个几个步骤,给定语料,硬核的手算出arpa模型。
手算基于KN的语言模型
继续以test_corpus.txt为语料,手算一个bigram的语言模型。
模型
语言 模型
传统
模型
语言
1. ngram初始统计
第一步是进行ngram的统计,因为这里训练bigram的语言模型,所以需要统计unigram以及bigram的数量。
在进行统计之前需要先给语料中每句话的开始和结束加上特殊的token:<s> 和 </s>。这样语料进一步处理成:
<s> 模型 </s>
<s> 语言 模型 </s>
<s> 传统 </s>
<s> 模型 </s>
<s> 语言 </s>
根据以上语料统计ngram的数量
unigram | count |
<s> | 5 |
传统 | 1 |
语言 | 2 |
模型 | 3 |
</s> | 5 |
bigram | count |
<s> 模型 | 2 |
<s> 语言 | 2 |
<s> 传统 | 1 |
语言 模型 | 1 |
模型 </s> | 3 |
传统 </s> | 1 |
语言 </s> | 1 |
2. ngram计数调整
对于N-gram的语言模型,调整技术主要针对n<N的ngram进行计数调整。核心是将计数从原先的直接数量统计调整为可接词数量的统计。具体的计算方法如下:
![a(w_{1}^{n}) = \left \{ \begin{array} {ll} & c(w_{1}^{n}) & n=N \quad or \quad w_{1}=<s> \\ & |v:c(v:w_{1}^{n}) > 0| & otherwise \end{array} \right.](https://i-blog.csdnimg.cn/blog_migrate/de21eeb939149871f94b16afe8dd15be.png)
这里的
![c(w_{1}^{n})](https://i-blog.csdnimg.cn/blog_migrate/bfd741d1f6b2717011425e92f971e6fc.png)
![w_{1}^{n}](https://i-blog.csdnimg.cn/blog_migrate/4efe9e35003f87ef2d0aca558981225a.png)
![a(w_{1}^{n})](https://i-blog.csdnimg.cn/blog_migrate/ce64aa9d613fdb51f00adab6a5cf215c.png)
![w_{1}](https://i-blog.csdnimg.cn/blog_migrate/a2d6aba6a2bbc7585c49f96fde7953e3.png)
![w_{1}^{n}](https://i-blog.csdnimg.cn/blog_migrate/4efe9e35003f87ef2d0aca558981225a.png)
所以调整后计数的结果为:
unigram | adjust count | reason |
<s> | 5 | w1 = <s>, a = c = 5 |
传统 | 1 | | {<s>, 传统} | = 1 |
语言 | 1 | | {<s>, 语言} | = 1 |
模型 | 2 | | {<s>, 模型}, {语言, 模型} | = 2 |
</s> | 3 | | {模型, </s>}, {传统, </s>}, {语言, </s>} | = 3 |
bigram | adjust count | reason |
<s> 模型 | 2 | n = N, a = c =2 |
<s> 语言 | 2 | n = N, a = c =2 |
<s> 传统 | 1 | n = N, a = c =1 |
语言 模型 | 1 | n = N, a = c =1 |
模型 </s> | 3 | n = N, a = c =3 |
传统 </s> | 1 | n = N, a = c =1 |
语言 </s> | 1 | n = N, a = c =1 |
3. 计数打折
计数打折的思想为:对于出现频率较高的ngram减少一点对最终的概率影响不会很大,可将其加到那些未出现的ngram上;对于出现频率较低的ngram则不能减少。
具体根据Chen and Goodman提出的打折公式进行计算
![D_{n}(k) = \left \{ \begin{array}{} & 0 & k=0 \\ & k - \frac{(k+1)t_{n,1}t_{n,k+1}}{(t_{n,1}+2t_{n,2})t_{n,k}} & 1\leq k \leq 3 \\ & D_{n}(3) & k > 4 \end{array} \right.](https://i-blog.csdnimg.cn/blog_migrate/1a648a0316f38e34e8e675adf3e03b91.png)
这里的
![t_{n,k}](https://i-blog.csdnimg.cn/blog_migrate/8f3cbca7aa33f48cc6bd8b8a453bb218.png)
![k\in [1,4]](https://i-blog.csdnimg.cn/blog_migrate/b40dca0719c073ce8dfff9bd73717f8a.png)
![t_{n,k} = |w_{1}^{n}:a(w_{1}^{n} ) = k|](https://i-blog.csdnimg.cn/blog_migrate/8f3cbca7aa33f48cc6bd8b8a453bb218.png+%3D+%7Cw_%7B1%7D%5E%7Bn%7D%3Aa%28w_%7B1%7D%5E%7Bn%7D+%29+%3D+k%7C)
t_{n,k} (n=1,2; k=1,2,3,4) | value | reason |
t_{1,1} | 2 | n=1, |a(语言) , a(传统) | = 2 |
t_{1,2} | 1 | n=1, |a(模型) | = 1 |
t_{1,3} | 1 | n=1, |a(</s>)| = 1 |
t_{1,4} | 0 | n=1, 不存在a为4的unigram |
t_{2,1} | 4 | n = 2, |a({<s>, 传统}), a({语言, 模型}), a({传统, </s>}), a({语言, </s>})| = 4 |
t_{2,2} | 2 | n = 2, |a({<s>, 模型}), a({<s>, 语言})| = 2 |
t_{2,3} | 1 | n = 2, |a({模型, </s>})| = 1 |
t_{2,4} | 0 | n=2, 不存在a为4的bigram |
D_{n,k} | value |
D_{1}(1) | 1/2 |
D_{1}(2) | 1/2 |
D_{1}(3) | 3 |
D_{1}(4) | 3 |
D_{2}(1) | 1/2 |
D_{2}(2) | 5/4 |
D_{2}(3) | 3 |
D_{2}(4) | 3 |
4. 计算伪概率
伪概率的计算公式如下:
![u(w_{n}|w_{1}^{n-1}) = \frac{a(w_{1}^{n}) - D_{n}(a(w_{1}^{n}))}{ \sum_{x}a(w_{1}^{n-1}x) }](https://i-blog.csdnimg.cn/blog_migrate/f921d0d3b2f1236bc5052a846c9f5080.png)
可以看到,分子如果没有减去
![D_{n}(k)](https://i-blog.csdnimg.cn/blog_migrate/e55e80000f4f704e4a3c8b9d699d4e6a.png)
unigram | u value | reason |
<s> | 2/7 | a = 5, D_{1}(5) = 3, a(传统) + a(语言) + a(模型) + a(</s>) = 7(5-3) / 7 = 2/7 |
传统 | 1/14 | a = 1, D_{1}(1) = 1/2 (1-1/2)/7 = 1/14 |
语言 | 1/14 | a = 1, D_{1}(1) = 1/2 (1-1/2)/7 = 1/14 |
模型 | 3/14 | a = 2, D_{1}(2). = 1/2 (2-1/2)/7 = 3/14 |
</s> | 0 | a =3, D_{1}(3) = 3 (3-3) / 7 = 0 |
bigram | u value | reason |
<s> 模型 | 3/20 | a = 2, D_{2}(2) = 5/4, a({<s> 模型}) + a({<s> 语言}) + a({<s> 传统}) = 5(2-5/4)/5 = 3/20 |
<s> 语言 | 3/20 | a = 2, D_{2}(2) = 5/4 (2-5/4)/5 = 3/20 |
<s> 传统 | 1/10 | a = 1, D_{2}(1) = 1/2 (1-1/2)/5 = 1/10 |
语言 模型 | 1/4 | a = 1, D_{2}(1) = 1/2 a({语言 模型}) + a({语言 </s>}) = 2(1-1/2) / 2 = 1/4 |
模型 </s> | 0 | a = 3, D_{2}(3) = 3 a({模型 </s>}) = 3(3-3) / 3 = 0 |
传统 </s> | 1/2 | a = 1, D_{2}(1) = 1/2 a({模型 </s>}) = 1(1-1/2)/1 = 1/2 |
语言 </s> | 1/4 | a = 1, D_{2}(1) = 1/2 a({语言 </s> }) + a({语言 模型}) = 2(1-1/2)/2= 1/4 |
注意,当n=1时,计算
![\sum_{x}a(w_{1}^{n-1}x)](https://i-blog.csdnimg.cn/blog_migrate/c112f0649c57a6fe3b93947b5606f233.png)
5. 回退值计算
定义回退值为接词的能力,具体回退值的计算公式如下:
![b(w_{1}^{n-1} )= \frac{\sum_{i=1}^{3} D_{n}(i) |x:a(w_{1}^{n-1}x)=i| }{\sum_{x}a(w_{1}^{n-1}x)}](https://i-blog.csdnimg.cn/blog_migrate/06f51dcea3dc957ffacbd43c5d7d307e.png)
unigram | backoff value | reason |
<s> | 3/5 | (1/2*1 + 5/4 * 2 + 3 * 0) / 5= 3/5 |
传统 | 1/2 | (1/2*1 + 5/4*0 + 3*0) / 1 = 1/2 |
语言 | 1/2 | (1/2*2 + 5/4 *0 + 3*0) / 2 = 1/2 |
模型 | 1 | (1/2*0 + 5/4 *0 + 3*1) / 3 = 1 |
</s> | 0 | 0 |
注意</s>后面不可能接新词,所以backoff为0
6. 差值计算
差值的计算可根据递推公式:
![p(w_{n}|w_{1}^{n-1}) = u(w_{1}^{n}|w_{1}^{n-1}) + b(w_{1}^{n-1})*p(w_{n}|w_{2}^{n-1})](https://i-blog.csdnimg.cn/blog_migrate/c48faff7963f20f06c14e7e8b173c9ce.png)
根据此递推公式一定会递归到unigram,而unigram可直接由以下公式进行计算
![p(w_{n}) = u(w_{n}) + b(\epsilon) \frac{1}{|V|}](https://i-blog.csdnimg.cn/blog_migrate/26bed38f695772309bbce0d094eae061.png)
这里的
![\epsilon](https://i-blog.csdnimg.cn/blog_migrate/9d83d5a736cfe878f7d1f9052bd2ee7b.png)
![w_{1}^{0}](https://i-blog.csdnimg.cn/blog_migrate/a2d6aba6a2bbc7585c49f96fde7953e3.png%5E%7B0%7D)
![b(\epsilon)=\frac{D_{1}(1)*2 + D_{1}(2)*1 + D_{1}(3)*1}{\sum_{x\ne<s>} a(x)} = \frac{9}{14}](https://i-blog.csdnimg.cn/blog_migrate/34c3b215b5c713751e08ee9d3d7bfe5d.png)
首先计算unigram插值后的概率值,注意对于<s>的概率直接置为0
unigram | p | reason |
<s> | 0 | 0 |
传统 | 1/5 | 1/14 + 9/14 * (1/5) = 14/70 |
语言 | 1/5 | 1/14 + 9/14 * (1/5) = 14/70 |
模型 | 12/35 | 3/14 + 9/14 * (1/5) = 24/70 |
</s> | 9/70 | 0 + 9/14 * (1/5) = 9/70 |
<unk> | 9/70 | 0 + 9/14 * (1/5) = 9/70 |
再根据递推公式,进一步计算bigram插值后的概率值
bigram | p | reason |
<s> 模型 | 249/700 | 3/20 + 3/5 * 24/70 = 249/700 |
<s> 语言 | 27/100 | 3/20 + 3/5 * 1/5 = 27/100 |
<s> 传统 | 11/50 | 1/10 + 3/5 * 1/5 = 11/50 |
语言 模型 | 59/140 | 1/4 + 1/2 * 24/70 = 59/140 |
模型 </s> | 9/70 | 0 + 1 * 9/70 = 9/70 |
传统 </s> | 79/140 | 1/2 + 1/2 * 9/70 = 79/140 |
语言 </s> | 11/35 | 1/4 + 1/2 * 9/70 = 11/35 |
7. 生成语言模型
整理上文中计算的概率以及backoff,并计算log10
unigram | p | log10 p | backoff | log10 backoff |
<s> | 0 | 0 | 3/5 | -0.221849 |
传统 | 1/5 | -0.698970 | 1/2 | -0.301030 |
语言 | 1/5 | -0.698970 | 1/2 | -0.301030 |
模型 | 12/35 | -0.464887 | 1 | 0 |
</s> | 9/70 | -0.890856 | 0 | 0 |
<unk> | 9/70 | -0.890856 | 0 | 0 |
bigram | p | log10 p | ||
<s> 模型 | 249/700 | -0.448899 | ||
<s> 语言 | 27/100 | -0.568636 | ||
<s> 传统 | 11/50 | -0.657577 | ||
语言 模型 | 59/140 | -0.375276 | ||
模型 </s> | 9/70 | -0.890856 | ||
传统 </s> | 79/140 | -0.248501 | ||
语言 </s> | 11/35 | -0.502675 |
进一步整理成arpa格式, 可以发现与之前kenlm计算的结果一致
\data\
ngram 1=6
ngram 2=7
\1-grams:
-0.890856 <unk> 0
0 <s> -0.22184873
-0.69896996 传统 -0.30103
-0.69896996 语言 -0.30103
-0.46488678 模型 0
-0.89085555 </s> 0
-0.890856 <unk> 0
\2-grams:
-0.24850096 传统 </s>
-0.44889864 <s> 模型
-0.56863624 <s> 语言
-0.6575773 <s> 传统
-0.37527603 语言 模型
-0.89085555 模型 </s>
-0.50267535 语言 </s>
\end\
参考文献
Scalable Modified Kneser-Ney Language Model Estimation: https://aclanthology.org/P13-2121.pdf