原创:李孟启
1、前言
什么是语言模型(language model)?简单地说,语言模型就是用来计算一个句子的概率的模型,也就是判断一句话是否是人话的概率。
语言模型的应用比较广泛,可⽤于提升语⾳识别和机器翻译的性能。例如,在语音识别的过程中,给定一条语音“厨房里食油用完了”的语音,机器可能会把语音输出为“厨房⾥⻝油⽤完了”和“厨房⾥⽯油⽤完了”这两个读音相同的文本序列,如果通过提前训练好的语言模型,对识别的这两个句子进行概率计算可以判断出前者的概率大于后者的概率,我们就可以认定为句子概率较大的“厨房⾥⻝油⽤完了”文本序列是最终输出;在机器翻译中,如果对英⽂“you go first”逐词翻译成中⽂的话,可能得到“你⾛先”、“你先⾛”等排列⽅式的⽂本序列。如果语⾔模型判断出“你先⾛”的概率⼤于其他排列⽅式的⽂本序列的概率,我们就可以把“you go first”翻译成“你先⾛”。
2、语言模型的计算
我们用表示一个长度为T的文本序列,可以将计算该序列(句子)的概率表示为:.
那该如何计算一个句子的概率呢?假设序列中的每个词是依次⽣成的,我们可以这样计算句子的概率
例如,⼀段含有4个词的⽂本序列的概率.
为了计算语⾔模型,我们需要计算词的概率,以及⼀个词在给定前⼏个词的情况下的条件概率,即语⾔模型参数。设训练数据集为⼀个⼤型⽂本语料库,如维基百科的所有条⽬。词的概率可以通过该词在训练数据集中的相对词频来计算。例如, 可以计算为在训练数据集中的词频(词出现的次数)与训练数据集的总词数之⽐。因此,根据条件概率定义,⼀个词在给定前⼏个词的情况下的条件概率也可以通过训练数据集中的相对词频计算。例如, 可以计算为 两词相邻的频率与词频的⽐值,因为该⽐值即 与之⽐;⽽ 同理可以计算为 和三词相邻的频率与 和 两词相邻的频率的⽐值。以此类推。
3、n元语法
当序列⻓度增加时,计算和存储多个词共同出现的概率的复杂度会呈指数级增加。 元语法通过⻢尔可夫假设(虽然并不⼀定成⽴)简化了语⾔模型的计算。这⾥的⻢尔可夫假设是指⼀个词的出现只与前⾯个词相关,即阶⻢尔可夫链( Markov chain of order n)。如果n=1,那么有。如果基于阶⻢尔可夫链,我们可以将语⾔模型改写为.
以上也叫元语法( n-grams)。它是基于阶⻢尔可夫链的概率语⾔模型。当分别为1、2和3时,我们将其分别称作⼀元语法(unigram)、⼆元语法(bigram)和三元语法(trigram)。例如,⻓度为4的序列在⼀元语法、⼆元语法和三元语法中的概率分别为
,
,
.
当较⼩时,元语法往往并不准确。例如,在⼀元语法中,由三个词组成的句⼦“你⾛先”和“你先⾛”的
概率是⼀样的。然⽽,当较⼤时, 元语法需要计算并存储⼤量的词频和多词相邻频率。
4、代码实战
这里借助nltk包来实现语言模型的训练。
首先,导入一些必要的包;
from nltk.util import pad_sequence
from nltk.util import bigrams
from nltk.util import ngrams
from nltk.util import everygrams
from nltk.lm.preprocessing import pad_both_ends
from nltk.lm.preprocessing import flatten
这里准备两条语料来演示一些函数的功能;
text = [['a', 'b', 'c'], ['a', 'c', 'd', 'c', 'e', 'f']]
下面是bigrams函数功能;
list(bigrams(text[0]))
输出为:
[('a', 'b'), ('b', 'c')]
也可以使用ngrams函数完成上述效果,这里参数n对应的是n元语法,示例中n=2,即2元语法。
list(ngrams(text[0], n=2))
输出为:
[('a', 'b'), ('b', 'c')]
我们注意到b同时出现在了第一个和第二个元组中,而a和c只出现在了一个元组中,如果我们能以某种形式指出句子以a开头c结束,那不是会更好吗?解决此问题的标准方法是在将句子拆分为ngram之前,将特殊的填充符号添加到句子前后,幸运的是nltk就有一个功能,下面让我们看看它是怎么做的吧;
list(pad_sequence(text[0],
pad_left=True, left_pad_symbol="<s>",
pad_right=True, right_pad_symbol="</s>",
n=2)) # The n order of n-grams, if it's 2-grams, you pad once, 3-grams pad twice, etc.
输出为:
['\<s>','a','b','c','\</s>']
我们对添加特殊符号后的句子再进行ngrams操作;
padded_sent = list(pad_sequence(text[0], pad_left=True, left_pad_symbol="<s>",
pad_right=True, right_pad_symbol="</s>", n=2))
list(ngrams(padded_sent, n=2))
输出为:
[('\<s>', 'a'), ('a', 'b'), ('b', 'c'), ('c', '\</s>')]
可以看到源句子中的每个单词都出现在了相同数量的元组中;那么在三元语法中也是这样,请看下面的示例:
list(pad_sequence(text[0],
pad_left=True, left_pad_symbol="<s>",
pad_right=True, right_pad_symbol="</s>",
n=3)) # 其中的n,如果n=2,就是2-gram,前后要各填充1个标记,同理3-gram就需要填充2个标记,以此类推。
输出为:
['\<s>', '\<s>', 'a', 'b', 'c', '\</s>', '\</s>']
padded_sent = list(pad_sequence(text[0], pad_left=True, left_pad_symbol="<s>",
pad_right=True, right_pad_symbol="</s>", n=3))
list(ngrams(padded_sent, n=3))
[('\<s>', '\<s>', 'a'), ('\<s>', 'a', 'b'), ('a', 'b', 'c'), ('b', 'c', '\</s>'), ('c', '\</s>', '\</s>')]
可以看到上面方法在对句子进行处理时,需要传递很多参数,比较麻烦,那么在nltk中还有一个方法pad_both_ends;
from nltk.lm.preprocessing import pad_both_ends
list(pad_both_ends(text[0], n=2))
输出为:
['\<s>', 'a', 'b', 'c', '\</s>']
为了使我们的模型更加健壮,我们还可以在 unigrams和 bigrams上对其进行训练。 NLTK 再次提供了一个名为everygrams 的函数。
padded_bigrams = list(pad_both_ends(text[0], n=2))
list(everygrams(padded_bigrams, max_len=2))
输出为:
[('<s>',), ('a',), ('b',), ('c',), ('</s>',), ('<s>', 'a'), ('a', 'b'), ('b', 'c'), ('c', '</s>')]
在计算 ngram之前,我们来了解怎么生成单词的词汇表。
为了创建这个词汇表,我们需要填充我们的句子(就像计算 ngrams 一样),然后将句子组合成一个扁平的单词流。
from nltk.lm.preprocessing import flatten
list(flatten(pad_both_ends(sent, n=2) for sent in text))
输出为:
['<s>', 'a', 'b', 'c', '</s>', '<s>', 'a', 'c', 'd', 'c', 'e', 'f', '</s>']
在大多数情况下,我们希望使用相同的文本作为词汇和 ngram 计数的来源。现在我们了解了这对我们的预处理意味着什么,我们可以简单地导入一个为我们做所有事情的函数。
from nltk.lm.preprocessing import padded_everygram_pipeline
training_ngrams, padded_sentences = padded_everygram_pipeline(2, text)
for ngramlize_sent in training_ngrams:
print(list(ngramlize_sent))
print()
print('#############')
list(padded_sentences)
输出为:
[('<s>',), ('a',), ('b',), ('c',), ('</s>',), ('<s>', 'a'), ('a', 'b'), ('b', 'c'), ('c', '</s>')] [('<s>',), ('a',), ('c',), ('d',), ('c',), ('e',), ('f',), ('</s>',), ('<s>', 'a'), ('a', 'c'), ('c', 'd'), ('d', 'c'), ('c', 'e'), ('e', 'f'), ('f', '</s>')] ############# ['<s>', 'a', 'b', 'c', '</s>', '<s>', 'a', 'c', 'd', 'c', 'e', 'f', '</s>']
下面让我们弄点真实数据演练一遍:
try: # 在这里使用 NLTK 的分句和分词工具sent_tokenize、sent_tokenize
from nltk import sent_tokenize, sent_tokenize
# 测试是否工作
# 有时,由于安装问题,它在某些机器上不起作用。
word_tokenize(sent_tokenize("This is a foobar sentence. Yes it is.")[0])
except: # 使用正则表达式分句
import re
from nltk.tokenize import ToktokTokenizer
# See https://stackoverflow.com/a/25736515/610569
sent_tokenize = lambda x: re.split(r'(?<=[^A-Z].[.?]) +(?=[A-Z])', x)
# 使用不需要依赖项的ToktokTokenizer来分词
toktok = ToktokTokenizer()
word_tokenize = word_tokenize = toktok.tokenize
准备一些数据:
import os
import requests
import io
# Text version of https://kilgarriff.co.uk/Publications/2005-K-lineer.pdf
if os.path.isfile('language-never-random.txt'):
with io.open('language-never-random.txt', encoding='utf8') as fin:
text = fin.read()
else:
url = "https://gist.githubusercontent.com/alvations/53b01e4076573fea47c6057120bb017a/raw/b01ff96a5f76848450e648f35da6497ca9454e4a/language-never-random.txt"
text = requests.get(url).content.decode('utf8')
with io.open('language-never-random.txt', 'w', encoding='utf8') as fout:
fout.write(text)
对文本分词:
tokenized_text = [list(map(str.lower, word_tokenize(sent)))
for sent in sent_tokenize(text)]
生成训练数据:
# 为3-gram语言模型,预处理文本
n = 3
train_data, padded_sents = padded_everygram_pipeline(n, tokenized_text)
训练N-gram模型:
from nltk.lm import MLE
model = MLE(n) # 训练一个3-gram的语言模型,上面设定了n=3
model.fit(train_data, padded_sents)
len(model.vocab)
1391
进行打分:
model.score('never', 'language is'.split()) # P('never'|'language is')
0.6363636363636364