语言模型理论与实战

原创:李孟启

1、前言

什么是语言模型(language model)?简单地说,语言模型就是用来计算一个句子的概率的模型,也就是判断一句话是否是人话的概率。

语言模型的应用比较广泛,可⽤于提升语⾳识别和机器翻译的性能。例如,在语音识别的过程中,给定一条语音“厨房里食油用完了”的语音,机器可能会把语音输出为“厨房⾥⻝油⽤完了”和“厨房⾥⽯油⽤完了”这两个读音相同的文本序列,如果通过提前训练好的语言模型,对识别的这两个句子进行概率计算可以判断出前者的概率大于后者的概率,我们就可以认定为句子概率较大的“厨房⾥⻝油⽤完了”文本序列是最终输出;在机器翻译中,如果对英⽂“you go first”逐词翻译成中⽂的话,可能得到“你⾛先”、“你先⾛”等排列⽅式的⽂本序列。如果语⾔模型判断出“你先⾛”的概率⼤于其他排列⽅式的⽂本序列的概率,我们就可以把“you go first”翻译成“你先⾛”。

2、语言模型的计算

我们用w_{1},w_{2},...,w_{T},表示一个长度为T的文本序列,可以将计算该序列(句子)的概率表示为:P\left(w_{1}, w_{2}, \ldots, w_{T}\right).

那该如何计算一个句子的概率呢?假设序列w_{1}, w_{2}, \ldots, w_{T}中的每个词是依次⽣成的,我们可以这样计算句子的概率P\left(w_{1}, w_{2}, \ldots, w_{T}\right)=\prod_{t=1}^{T} P\left(w_{t} \mid w_{1}, \ldots, w_{t-1}\right)

例如,⼀段含有4个词的⽂本序列的概率P\left(w_{1}, w_{2}, w_{3}, w_{4}\right)=P\left(w_{1}\right) P\left(w_{2} \mid w_{1}\right) P\left(w_{3} \mid w_{1}, w_{2}\right) P\left(w_{4} \mid w_{1}, w_{2}, w_{3}\right).

为了计算语⾔模型,我们需要计算词的概率,以及⼀个词在给定前⼏个词的情况下的条件概率,即语⾔模型参数。设训练数据集为⼀个⼤型⽂本语料库,如维基百科的所有条⽬。词的概率可以通过该词在训练数据集中的相对词频来计算。例如,P\left(w_{1}\right) 可以计算为w_{1}在训练数据集中的词频(词出现的次数)与训练数据集的总词数之⽐。因此,根据条件概率定义,⼀个词在给定前⼏个词的情况下的条件概率也可以通过训练数据集中的相对词频计算。例如, P\left(w_{2} \mid w_{1}\right)可以计算为 w_{1}, w_{2}两词相邻的频率与w_{1}词频的⽐值,因为该⽐值即 P\left(w_{1}, w_{2}\right)P\left(w_{1}\right)之⽐;⽽ P\left(w_{3} \mid w_{1}, w_{2}\right)同理可以计算为 w_{1}, w_{2} ,w_{3}和三词相邻的频率与 w_{1}w_{2} 两词相邻的频率的⽐值。以此类推。

3、n元语法

当序列⻓度增加时,计算和存储多个词共同出现的概率的复杂度会呈指数级增加。 n元语法通过⻢尔可夫假设(虽然并不⼀定成⽴)简化了语⾔模型的计算。这⾥的⻢尔可夫假设是指⼀个词的出现只与前⾯n个词相关,即n阶⻢尔可夫链( Markov chain of order n)。如果n=1,那么有P\left(w_{3} \mid w_{1}, w_{2}\right)=P\left(w_{3} \mid w_{2}\right)。如果基于n-1阶⻢尔可夫链,我们可以将语⾔模型改写为P\left(w_{1}, w_{2}, \ldots, w_{T}\right) \approx \prod_{t=1}^{T} P\left(w_{t} \mid w_{t-(n-1)}, \ldots, w_{t-1}\right).

以上也叫n元语法( n-grams)。它是基于n-1阶⻢尔可夫链的概率语⾔模型。当n分别为1、2和3时,我们将其分别称作⼀元语法(unigram)、⼆元语法(bigram)和三元语法(trigram)。例如,⻓度为4的序列w_{1}, w_{2}, w_{3}, w_{4}在⼀元语法、⼆元语法和三元语法中的概率分别为

P\left(w_{1}, w_{2}, w_{3}, w_{4}\right)=P\left(w_{1}\right) P\left(w_{2}\right) P\left(w_{3}\right) P\left(w_{4}\right)

P\left(w_{1}, w_{2}, w_{3}, w_{4}\right)=P\left(w_{1}\right) P\left(w_{2} \mid w_{1}\right) P\left(w_{3} \mid w_{2}\right) P\left(w_{4} \mid w_{3}\right)

P\left(w_{1}, w_{2}, w_{3}, w_{4}\right)=P\left(w_{1}\right) P\left(w_{2} \mid w_{1}\right) P\left(w_{3} \mid w_{1}, w_{2}\right) P\left(w_{4} \mid w_{2}, w_{3}\right).

n较⼩时,n元语法往往并不准确。例如,在⼀元语法中,由三个词组成的句⼦“你⾛先”和“你先⾛”的

概率是⼀样的。然⽽,当n较⼤时, n元语法需要计算并存储⼤量的词频和多词相邻频率。

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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容