NLP入门:基于马尔可夫链的随机百科(一)

最后一次修改于2020/3/7

去年暑假买了一本《数学之美》,里面介绍了NLP(自然语言处理)的很多东西,我感觉比较有意思,就写这篇文章来讲讲机器学习最基本的一个模型——马尔可夫链的一个无聊的应用,作为自己学习的一些笔记和一个预备入门级的教程,我会尽可能避免使用初中数学以上的概念,所以如果你拥有初中数学基础和少得可怜的编程知识(就像我),应该是看得懂的。

先来讲讲理论

关于马尔可夫模型的介绍网上有很多了,在这里不进行详细介绍。我们在这里只需要知道它假设每一个状态只与前n个状态有关,比如今天的天气只与昨天的有关,昨天是晴天,所以今天大概率也是晴天(所以在这个例子中n=1)。这样的假设不太符合实际,但我们这样假设,这样非常有助于简化计算,而且多数情况下确实有效。

那么在了解了之后,我们可以开始讲讲今天的主题了。我想要做的是一个叫做“随机百科”的文本生成器,给出前两个词,然后自动接话,说出一些符合语法甚至是符合情理的内容,随机的原因是词条大多不相关联,所以生成的一句话大体上符合语法,但一句话的主题跳跃很大,看上去比较随机。在这里,我们并不需要真的把所有语法都教给机器——那是个很大的工程,我们只需要利用一个概率模型,并向其输入大量的语料,让它自己算出“最应该说的话”。

具体怎么实现呢,开头我们说了,马尔可夫链假设每一个状态只与前n个状态有关,于是我们在这里假设一个词只与它前面两个词有关,具体为什么,后面会说,然后我们现在已经拥有了最开头的两个词了,怎么推出下一个?当然是选择一个出现在这两个词后概率最大的词,为了简便,我省去一些推导过程,给出下面这个公式:

不慌,这是本文唯一的公式,意思是:i个词跟在第i-2,i-1个词后面的概率为这三个词一起出现的次数除以第i-2,i-1个词一起出现的次数,而我们称第i-2,i-1个词构成的元组为第i个词或这三个词的前缀,第i个词为第i-2,i-1个词构成的元组或这三个词的后缀,我们只需要利用大量的语料进行统计,算出这些概率就可以了。举个例子:

“我会”是一个由“我”和“会”两个词构成的两元组,我们统计好“我会”出现的次数,再统计好以“我会”开头的三元组的出现的次数,假如我们统计出来“我会飞”出现了1次而“我会走路”出现了3次,“我会”没有在其他地方出现过,我们就可以用它们的频数除以“我会”的频数——这里是4,算出“我会”后面接“飞”的概率为1/4而接“走路”的概率是3/4,于是我们可以在前缀为“我会”时选择接上概率较大的”走路“而不选择概率较小的”飞“或其他没有出现过的结果(这里样本比较小,说频率可能更准确,但样本足够时就可以用频率来估计概率了,还有,“我会”在一些分词系统中会被看作一个词,这里只是个例子)。

现在,我们可以构建一个三元模型,存上所有词的条件概率(即每一个词跟在每一个两元前缀后的概率),这样当我们给出三元组的前两个词也就是一个前缀,就能接上概率最高的第三个词作为后缀,也就是理论上最符合情理的那个,然后我们又以这第二、三个词作为前缀继续推导,以此类推。为何不用两元或者四元五元组,相信你已经知道了,两元的准确性不够,显然假设每个词只取决于它前面一个词是不太好的,在真实语境中一个词可能与它前面许多词有关,而元越多,虽然效果确实会越好,但模型的空间复杂度也会随着元的数量上升而快速增加,占用的资源也会大幅上涨,模型效果的上升却越来越不显著,一般来说,三元或四元已经足够了。

再来写点代码

由于篇幅原因,本文只会讲关于训练的代码,至于生成文本和性能优化的内容会在后面作为单独的文章发出。

我们需要用一些语料训练一个统计语言模型,一般来说语料越多越好,但还需要根据你想做的事针对性地选择材料,我这里使用的是维基百科的部分词条,大约是16万个,来自Github上的一个仓库,我对仓库的数据稍做了处理方便训练,训练的代码如下:

我们先将所有文本都从文件读取进来,然后使用jieba包对文本进行分词,拆分成一个个单独的词语存到words数组中,接下来使用nltk包将数组中的词语组合为两元组和三元组,在这里的BigramTgram就分别是所有两元组和三元组的总体对象,我们再使用FreqDist得到两个Hash,BigramDistTgramDist,里面分别存上了所有两元组和三元组的出现次数,举个例子:

这段代码的输出是(省去部分分词过程中的无关紧要的输出):

可以看到程序输出了分词的结果——一个列表,还有两个词频统计的结果——两元组和三元组及其对应的出现次数——这里都只出现了1次。

我们继续往下看,计算概率模型在有了词频后就十分简单了,先开两个字典,model_dct是每个词的条件概率(即上面那个公式的等号左边,在这里以三元组为键),index_dct是一个索引字典,键值分别是每个两元组和它拥有的所有后缀,本来这个索引字典是没有的,但加上之后会大大优化时空占用,具体为什么会在后面的生成时讲到。遍历三元组词频Hash,model_dct的键就是三元组的字符串形式,转为字符串是方便存到json中,值就是三元组的频数除以其前缀的频数,接着就把三元组的后缀丢到索引字典里对应前缀的数组里。代码再贴一次方便阅读:

还是以text = '这是一个测试。'为例,将model_dct.items()index_dct.items()打印出来就是这样:

前缀为“这是一个”时,后缀为“测试”的概率是1.0,前缀为“一个测试”时,后缀为“。”的概率是1.0(注意标点符号也算词)。

最后,我们只需要把两个字典以json形式存到文件里方便每次进行生成时复用就可以了。

最后总结一下

本文主要讲了NLP最基本的一个模型——马尔可夫链,“随机百科”的原理以及如何训练一个概率模型,到现在为止,我们已经拥有了一个概率模型,那么要怎么使用概率模型来让机器接话呢,这就是下篇我们要讨论的关于生成的问题了。

看到这里,相信你对NLP已经有了一定了解,还是很简单的,对吧。

 

icecream

2020/2/21