本篇文章将主要介绍在《分词算法的原理及简单实现(一)》中提及的分词相关的各种算法。包括但不仅限于结巴分词。
注:下文中提及的jieba分词源码指的是 v0.39 版本的代码。
前缀词典
jieba分词中,构建前缀词典相关的源码:
def gen_pfdict(self, f):
lfreq = {}
ltotal = 0
f_name = f.name
for lineno, line in enumerate(f, 1):
try:
line = line.strip().decode('utf-8')
word, freq = line.split(' ')[:2]
freq = int(freq)
lfreq[word] = freq
ltotal += freq
for ch in range(len(word)):
wfrag = word[:ch + 1]
if wfrag not in lfreq:
lfreq[wfrag] = 0
except ValueError:
raise ValueError(
'invalid dictionary entry in %s at Line %s: %s' % (f_name, lineno, line))
f.close()
return lfreq, ltotal
看着感觉简单粗暴,据测试在 python 中效率比Trie树要好一点?这个有待验证,应该跟语言特性,词典中词汇数量,词汇长短,单字种类有关。
Trie树
jieba 分词中原来的Trie树生成算法,已被上面的方法取代。
def gen_trie(f_name):
lfreq = {}
trie = {}
ltotal = 0.0
with open(f_name, 'rb') as f:
lineno = 0
for line in f.read().rstrip().decode('utf-8').split('\n'):
lineno += 1
try:
word,freq,_ = line.split(' ')
freq = float(freq)
lfreq[word] = freq
ltotal += freq
p = trie
for c in word:
if c not in p:
p[c] ={}
p = p[c]
p['']='' #ending flag
except ValueError as e:
logger.debug('%s at line %s %s' % (f_name, lineno, line))
raise e
return trie, lfreq, ltotal
字典树这个数据结构的用途还是很多的,尤其在搜索领域,下面是一些相关的例题:
有向无环图
有向无环图的生成算法,其中 FREQ 即为上面提到的前缀词典。
def get_DAG(self, sentence):
self.check_initialized()
DAG = {}
N = len(sentence)
for k in range(N):
tmplist = []
i = k
frag = sentence[k]
while i < N and frag in self.FREQ:
# 如果词频大于0,就将这个位置i追加到以k为key的一个列表中
if self.FREQ[frag]:
tmplist.append(i)
# 如果词频等于0,则表明前缀词典存在这个前缀,但是统计词典并没有这个词,继续循环
i += 1
frag = sentence[k:i + 1]
if not tmplist:
tmplist.append(k)
DAG[k] = tmplist
return DAG
查找最大概率路径
def calc(self, sentence, DAG, 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.FREQ.get(sentence[idx:x + 1]) or 1) -
logtotal + route[x + 1][0], x) for x in DAG[idx])
def __cut_DAG_NO_HMM(self, sentence):
re_eng = re.compile('[a-zA-Z0-9]', re.U)
DAG = self.get_DAG(sentence)
route = {}
self.calc(sentence, DAG, route)
x = 0
N = len(sentence)
buf = ''
while x < N:
y = route[x][1] + 1
l_word = sentence[x:y]
if re_eng.match(l_word) and len(l_word) == 1:
buf += l_word
x = y
else:
if buf:
yield buf
buf = ''
yield l_word
x = y
if buf:
yield buf
buf = ''
设想一个场景,用户在不断的输入,每当用户输入一个字符之后,需要对用户的完整输入进行一遍分词。如果只是单纯的调用分词接口,那么每输入一个字符就要进行一次分词运算。如果了解了“最大概率路径”的算法,就会发现,其实每输入一个字符只要在原有的分词算法上进行增量的计算即可。当然,这么做在减小计算量的同时,会增加一点内存占用。所以在实际应用中是否采用这样的方式,有待考量吧。
从上面的算法中可以看出,每当用户输入的查询词新增一个字符时,实际上是在有向无环图后面增加一个节点,根据构建的词典判断从这个节点出发的有向边。再利用动态规划刷新一下最大概率路径即可。
HMM
关于HMM的内容在文章《HMM的知识点》中进行详细的介绍。
