N-gram模型以及Go应用

278 阅读4分钟

语言模型

语言模型在实际应用中可以解决非常多的问题,例如判断一个句子的质量:

  • the house is big ! good
  • house big is the ! bad
  • the house is xxl ! worse

可以用于词的排序,比如the house is small优于small the is house;可以用于词的选择,I am going ___ (home/house),其中I am going home优于I am going house,除此之外,还有许多其他用途:

  • 语音识别
  • 机器翻译
  • 字符识别
  • 手写字体识别
  • ......

概率语言模型

假设词串W=w1,w2,...,wnW=w_{1},w_{2},...,w_{n},以p(W)表示该词串可能出现的概率,那么从概率的角度上,

p(W)=p(w1,w2,...,wn)p(W)=p(w_{1},w_{2},...,w_{n})

要计算p(W),根据链式法则有:

p(W)=p(w1)p(w2w1)...p(wnw1,w2,...,wn1)p(W)=p(w_{1})p(w_{2}|w_{1})...p(w_{n}|w_{1},w_{2},...,w_{n-1})

其中w1,w2,...,wi1w_{1},w_{2},...,w_{i-1}为第i个词的历史词。 例句:likely connects audiences with content

p(likelyconnectsaudienceswithcontent)=p(likelysentencestart)×p(connectslikely)×p(audienceslikely,connects)×p(withlikely,connects,audience)×p(contentlikely,connects,audience,with)p(likely\,connects\,audiences\,with\,content)\\=p(likely|sentence\,start)\\\times p(connects|likely)\\\times p(audiences|likely,connects)\\\times p(with|likely,connects,audience)\\\times p(content|likely, connects, audience, with)

那么如何用给定的语料库来训练N-gram语言模型呢?

极大似然估计

最简单的方式就是采用极大似然估计(Maximum Likelihood Estimation, MLE)。

p(wmw1,w2,...,wm1)=C(w1...wm1,wm)C(w1...wm1)C(w1...wm1)=wjWC(w1...wm1,wj)p(w_{m}|w_{1},w_{2},...,w_{m-1})=\frac{C(w_{1}...w_{m-1},w_{m})}{C(w_{1}...w_{m-1})}\\ C(w_{1}...w_{m-1})=\sum_{w_{j}\in W}^{}C(w_{1}...w_{m-1},w_{j})

例如,采用MLE估计:

p(contentlikely,connects,audience,with)=C(likelyconnectsaudienceswithcontent)C(likelyconnectsaudienceswith)p(content|likely, connects, audience, with)\\=\frac{C(likely\,connects\,audiences\,with\,content)}{C(likely\,connects\,audiences\,with)}

该估计方法依赖于假设:当前词出现的概率依赖于前面的词。然而,如果前面词的个数很大,一方面,语料库中C(w1,w2,...,wm)C(w_{1},w_{2},...,w_{m})C(w1,w2,...,wm1)C(w_{1},w_{2},...,w_{m-1})极有可能为0,就导致条件概率为0或无法计算。另一方面,随着历史长度的增城,不同历史数目会按指数级增长。 在此基础上,提出一个可行的方案:当前词仅依赖于较短的历史词。 p(contentconnects,audience,with)\rightarrow p(content|connects, audience, with)

  • 进行如上统计
  • 如果C(connectsaudienceswithcontent)=0C(connects\,audiences\,with\,content)=0

p(contentaudience,with)\rightarrow p(content|audience, with)

  • ...

p(contentwith)\rightarrow p(content|with)

  • ...

p(content)\rightarrow p(content)

马尔科夫假设

位于某个特定状态的概率取决于前n-1个状态,即n-1阶马尔科夫链

马尔科夫假设应用于语言模型:假设每个词的出现概率只取决于前n-1个词。 p(wiw1,w2,...,wi1)p(wiwin+1,...,wi1)p(w_{i}|w_{1},w_{2},...,w_{i-1})\approx p(w_{i}|w_{i-n+1},...,w_{i-1}) 这种语言模型也被称为N-gram模型(n元语法和n元文法)。

N-gram模型

1-gram模型(unigram)

p(w1,w2,...,wn)p(w1)p(w2)p(w3)...p(wn)p(w_{1},w_{2},...,w_{n})\approx p(w_{1})p(w_{2})p(w_{3})...p(w_{n})

2-gram模型(bigram)

p(w1,w2,...,wn)p(w1)p(w2w1)p(w3w2)...p(wnwn1)p(w_{1},w_{2},...,w_{n})\approx p(w_{1})p(w_{2}|w_{1})p(w_{3}|w_{2})...p(w_{n}|w_{n-1}) (此处wn1w_{n-1}被称为历史词)

3-gram模型(trigram)

p(w1,w2,...,wn)p(w1)p(w2w1)p(w3w1,w2)...p(wnwn2,wn1)p(w_{1},w_{2},...,w_{n})\approx p(w_{1})p(w_{2}|w_{1})p(w_{3}|w_{1},w_{2})...p(w_{n}|w_{n-2},w_{n-1}) 由于N-gram模型的马尔科夫假设,其对语言来说是一个“不充分”的模型,因为语言本身是长距离相依的,但却极大程度上满足了语言建模需要。 如何使用语料库来训练N-gram模型呢?假设经过预处理的语料库document为由语段组成的列表,例如:

document = [
	'欧洲杯大战在即,荷兰葡萄牙面临淘汰将背水一战',
	'金正恩为朝少年团代表安排宴会,学生发誓坚决跟随',
	'证监会:重组中股票异常交易监管将加强',
	'台媒曝岛内大学教授假发票案,涉案教授或达千人',
	'中石油创历史新低,大盘或开启短线下跌空间',
]

<s>代表每句话开头,</s>代表每句话结尾,对语料进行处理,以N作为N-gram模型中的阶数参数,可以得到最终计算时分子列表与分母列表:

for doc in document:
    split_words = ['<s>'] + list(doc) + ['</s>']
    # 分子
    [total_grams.append(tuple(split_words[i: i+N])) for i in range(len(split_words)-N+1)]
    # 分母
    [words.append(tuple(split_words[i:i+N-1])) for i in range(len(split_words)-N+2)]

至此基本的N-gram模型就训练成功了,根据实际需求添加功能就可以应用到实际案例中。

Go应用:3-gram模型预测未出现的字

我们以搜狗实验室的搜狐新闻数据(SogouCS)为原始语料库,其格式如下:

<doc>
<url>页面URL</url>
<docno>页面ID</docno>
<contenttitle>页面标题</contenttitle>
<content>页面内容</content>
</doc>

注意:content字段去除了HTML标签,保存的是新闻正文文本,对页面标题以及页面内容进行提取过滤:

if strings.HasPrefix(line, "<content>") {
   line = strings.Replace(line, "\n", "", -1)
   line = strings.Replace(line, "<content>", "", -1)
   line = strings.Replace(line, "</content>", "", -1)
   if len(line) > 1 {
      ret = append(ret, line)
   }
}

处理语料,计算分子列表wordGram与分母列表totalGram

var totalGram []string
var wordGram []string

for _, c := range content {
   splitWords := []string{"<s>"}
   for _, w := range c {
      splitWords = append(splitWords, string(w))
   }
   splitWords = append(splitWords, "</s>")

   for i := 0; i < len(splitWords)-n.gram+1; i++ {
      totalGram = append(totalGram, strings.Join(splitWords[i:i+n.gram], ""))
      wordGram = append(wordGram, strings.Join(splitWords[i:i+n.gram-1], ""))
   }
}

根据以上步骤训练出模型后,还有对模型进行改动,让其变成一个预测类模型:

totalMap := make(map[string]int)
wordMap := make(map[string]int)
for _, t := range totalGram {
   if m, ok := totalMap[t]; ok {
      totalMap[t] = m + 1
   } else {
      totalMap[t] = 1
   }
}
for _, w := range wordGram {
   if m, ok := wordMap[w]; ok {
      wordMap[w] = m + 1
   } else {
      wordMap[w] = 1
   }
}

由于该3元模型主要预测最后一个字,所以前两个字已知的情况下来求第三个字的出现概率:

for k, v := range totalMap {
   word := k[0 : len(k)/n.gram*(n.gram-1)]
   if _, ok := wordMap[word]; !ok {
      n.wordBag[word] = make([]*NextWord, 0)
   }

   nextWordProb := float32(v) / float32(wordMap[word])
   n.wordBag[word] = append(n.wordBag[word], &NextWord{
      word: k[len(k)/n.gram*(n.gram-1):],
      prob: nextWordProb,
   })
}

对于每一个key,都有相应的nextWordProb也就是它出现的概率对应,可以将这对参数作为集合,以key的前两个字为键,以集合为值构成字典,该字典也就是预测模型。

最后,对预测模型字典中的值,即上述对应集合,中的概率nextWordProb进行排序,根据输入的两个字,取出集合对,这也就完成了预测。

源码

以上源码上传至github