语言模型
语言模型在实际应用中可以解决非常多的问题,例如判断一个句子的质量:
- 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
,除此之外,还有许多其他用途:
- 语音识别
- 机器翻译
- 字符识别
- 手写字体识别
- ......
概率语言模型
假设词串,以p(W)表示该词串可能出现的概率,那么从概率的角度上,
要计算p(W),根据链式法则有:
其中为第i个词的历史词。
例句:likely connects audiences with content
那么如何用给定的语料库来训练N-gram语言模型呢?
极大似然估计
最简单的方式就是采用极大似然估计(Maximum Likelihood Estimation, MLE)。
例如,采用MLE估计:
该估计方法依赖于假设:当前词出现的概率依赖于前面的词。然而,如果前面词的个数很大,一方面,语料库中或极有可能为0,就导致条件概率为0或无法计算。另一方面,随着历史长度的增城,不同历史数目会按指数级增长。 在此基础上,提出一个可行的方案:当前词仅依赖于较短的历史词。
- 进行如上统计
- 如果
- 则
- ...
- ...
马尔科夫假设
位于某个特定状态的概率取决于前n-1个状态,即n-1阶马尔科夫链
马尔科夫假设应用于语言模型:假设每个词的出现概率只取决于前n-1个词。 这种语言模型也被称为N-gram模型(n元语法和n元文法)。
N-gram模型
1-gram模型(unigram)
2-gram模型(bigram)
(此处被称为历史词)
3-gram模型(trigram)
由于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