目录html
在浅谈分词算法(1)分词中的基本问题中咱们探讨了分词中的基本问题,也提到了基于词典的分词方法。基于词典的分词方法是一种比较传统的方式,这类分词方法有不少,如:正向最大匹配(forward maximum matching method, FMM)、逆向最大匹配(backward maximum matching method,BMM)、双向扫描法、逐词遍历法、N-最短路径方法以及基于词的n-gram语法模型的分词方法等等。对于这类方法,词典的整理选择在其中占到了很重要的做用,本文主要介绍下基于n-gram的分词方法,这类方法在平时的分词工具中比较常见,并且性能也较好。python
浅谈分词算法(1)分词中的基本问题
浅谈分词算法(2)基于词典的分词方法
浅谈分词算法(3)基于字的分词方法(HMM)
浅谈分词算法(4)基于字的分词方法(CRF)
浅谈分词算法(5)基于字的分词方法(LSTM)git
提到基于N-Gram的几率分词方法,首先咱们就要说下伟大的贝叶斯理论,说到贝叶斯理论,先说贝叶斯公式:
贝叶斯公式也是几率论当中的基础,这里咱们再也不赘述,推荐一篇文章数学之美番外篇:平凡而又神奇的贝叶斯方法,讲的很不错。下面咱们主要关注下在分词当中怎么利用贝叶斯原理。github
咱们知道一般P(Y)是一个常数,因此在使用贝叶斯公式的时候咱们更多用以下的公式:
当贝叶斯理论应用在离散数据集上的时候,能够使用频率做为几率来进行计算,在分词算法中,在给定训练语料库中,咱们以词为单位进行统计,统计出每一个词出现的频率,当出现一句待切分的句子时,咱们将全部可能的分词结果统计出来,计算几率最大的做为切分结果。用形式化的语言描述下:
假设训练数据集为,其中词典集为D,
为长度N的句子中的第i个词,那么一句话的联合几率能够表示为:
也就是说句子当中的每一个词的几率都是一个依赖于其前面全部词的条件几率。说到这里咱们就是惯用套路,显然这东东无法计算,那怎么办呢,那就是贝叶斯理论中经常使用的,作些条件独立假设呗,这也就是所谓n-gram中n的由来。算法
通常来讲,咱们最多只看到前两个词,有研究代表,大于4个以上的模型并不会取得更好的效果(显然n越大,咱们须要找寻n元组的词出现的频率就越低,会很直接的致使数据稀疏问题),一般状况下咱们使用的是2-gram模型居多。app
假设待切分语句为:“研究生物学”,咱们要怎样进行切分呢,直观的讲咱们能够看出就这么一句简单的话包含了“研究”、“研究生”、“生物”、“生物学”多个词语,那么直观上咱们有以下几种切分方式:函数
咱们将这些切法构建为一幅有向无环图,结点为词语,边为条件几率
(摘自[4])
那么根据最大似然原理,咱们分词的过程转为了在图中求解最佳路径的问题,咱们只须要选取任意一种搜索算法,例如在结巴分词中是利用动态规划找寻最大几率路径。工具
上面说了那么多,仍是上code比较有干货,咱们以1-gram为例,来进行一个阐述,这里咱们主要参考告终巴分词。在实现的过程当中涉及到的核心问题:创建前缀字典树、根据句子创建DAG(有向无环图)、利用动态规划获得最大几率路径。性能
代码以下:学习
with open(dict_path, "rb") as f: count = 0 for line in f: try: line = line.strip().decode('utf-8') word, freq = line.split()[:2] freq = int(freq) self.wfreq[word] = freq for idx in range(len(word)): wfrag = word[:idx + 1] if wfrag not in self.wfreq: self.wfreq[wfrag] = 0 # trie: record char in word path self.total += freq count += 1 except Exception as e: print("%s add error!" % line) print(e) continue
咱们利用dict来创建这颗前缀字典树,遇到一个词时,会将词路径当中全部的子串都记录在字典树中。(其实这种方式存放是有大量冗余子串的,不过查询是会更加方便)
代码以下:
def get_DAG(self, sentence): DAG = {} N = len(sentence) for k in range(N): tmplist = [] i = k frag = sentence[k] while i < N and frag in self.wfreq: if self.wfreq[frag]: tmplist.append(i) i += 1 frag = sentence[k:i + 1] if not tmplist: tmplist.append(k) DAG[k] = tmplist return DAG
由于在载入词典的时候已经将word和word的全部前缀加入了词典,因此一旦frag not in wfreq,便可以判定frag和以frag为前缀的词不在词典里,能够跳出循环。
值得注意的是,DAG的每一个结点,都是带权的,对于在词典里面的词语,其权重为其词频,即wfreq[word]。咱们要求得route = (w1, w2, w3 ,.., wn),使得Σweight(wi)最大。
知足dp的条件有两个
咱们来分析最大几率路径问题。
重复子问题
对于结点Wi和其可能存在的多个后继Wj和Wk,有:
最优子结构
对于整个句子的最优路径Rmax和一个末端节点Wx,对于其可能存在的多个前驱Wi,Wj,Wk…,设到达Wi,Wj,Wk的最大路径分别为Rmaxi,Rmaxj,Rmaxk,有:
Rmax = max(Rmaxi,Rmaxj,Rmaxk…) + weight(Wx)
因而问题转化为:
求Rmaxi, Rmaxj, Rmaxk…
组成了最优子结构,子结构里面的最优解是全局的最优解的一部分。
很容易写出其状态转移方程:
Rmax = max{(Rmaxi,Rmaxj,Rmaxk…) + weight(Wx)}
代码以下:
def get_route(self, DAG, sentence, route): N = len(sentence) route[N] = (0, 0) logtotal = log(self.total) for idx in range(N - 1, -1, -1): route[idx] = max((log(self.wfreq.get(sentence[idx:x + 1]) or 1) - logtotal + route[x + 1][0], x) for x in DAG[idx])
这里值得注意的是在求频率时,使用了log函数,将除法变成了减法,防止溢出。
对于句子“我是中国人”,咱们能够看到以下图所示的效果:
我将完整的代码放在了git上,这里的词典用的就是结巴分词中的词典,其中好多代码都是从结巴分词复用过来的,你们须要能够瞅瞅:
https://github.com/xlturing/machine-learning-journey/tree/master/seg_ngram