当前位置:   article > 正文

jieba分词原理整理_jieba 分段

jieba 分段

一 工具简介

jieba 是一个基于Python的中文分词工具:https://github.com/fxsjy/jieba

对于一长段文字,其分词原理大体可分为三部:

1.首先用正则表达式将中文段落粗略的分成一个个句子。

2.将每个句子构造成有向无环图,之后寻找最佳切分方案。

3.最后对于连续的单字,采用HMM模型将其再次划分。

二 模式介绍

jieba分词分为“默认模式”(cut_all=False),“全模式”(cut_all=True)以及搜索引擎模式(该篇博文未涉及该模式)。对于“默认模式”,又可以选择是否使用 HMM 模型(HMM=True,HMM=False)。下面是一段代码实例:

  1. import jieba
  2. seg_list = jieba.cut("单手补扣+8板阿联只是微微一笑,他已赢回主帅信任", cut_all=True)
  3. print("Mode1: " + "/ ".join(seg_list))
  4. seg_list = jieba.cut("单手补扣+8板阿联只是微微一笑,他已赢回主帅信任", cut_all=False, HMM=False)
  5. print("Mode2: " + "/ ".join(seg_list))
  6. seg_list = jieba.cut("单手补扣+8板阿联只是微微一笑,他已赢回主帅信任", cut_all=False, HMM=True)
  7. print("Mode3: " + "/ ".join(seg_list))
 输出结果为:
  1. Mode1: 单手/ 补/ 扣/ +8/ 板/ 阿联/ 只是/ 微微/ 微微一笑/ / / 他/ 已/ 赢回/ 主帅/ 信任
  2. Mode2: 单手/ 补/ 扣/ +/ 8/ 板/ 阿联/ 只是/ 微微一笑/ ,/ 他/ 已/ 赢回/ 主帅/ 信任
  3. Mode3: 单手/ 补扣/ +/ 8/ 板/ 阿联/ 只是/ 微微一笑/ ,/ 他/ 已/ 赢回/ 主帅/ 信任

首先从总体上看,都是将一个整句划分为了不同的词,但是细看有以下需要注意的地方:

1.全模式下,“微微”出现了两次,这意味着全模式是把所有的可能分词都列举了出来。

2.默认模式下,“+8”被分为了两个词"+"和"8",这是由于两种模式在分割句子时采用的正则表达式不同,后面会详细说。

3.当采用HMM 模型时,“补扣”被当作是一个词。这是因为“补扣”在词典中不被认为是一个词,因此切分方案将”补”和“扣”分为两个词。采用HMM 模型后,会对连续单字进行检测,将单字”补”和“扣”合为了一个词。

  1.        if cut_all:
  2. re_han = re_han_cut_all
  3. re_skip = re_skip_cut_all
  4. else:
  5. re_han = re_han_default
  6. re_skip = re_skip_default
  7. if cut_all:
  8. cut_block = self.__cut_all
  9. elif HMM:
  10. cut_block = self.__cut_DAG
  11. else:
  12. cut_block = self.__cut_DAG_NO_HMM

从上面的代码里可以看到主程序关于各种模式的分支处理。

第一个分支处理是根据模式选取了不同的正则表示式,re_han用来识别汉字,re_skip则是识别其他各种符合。

第二个分支处理是根据模式选取了不同切分方法,cut_block是一个函数指针,后续将通过cut_block去完成实际的切分。

三 正则表达式处理

正则表达式(regular expression)描述了一种字符串匹配的模式(pattern),可以用来检查一个串是否含有某种子串、将匹配的子串替换或者从某个串中取出符合某个条件的子串等。对应到程序里,主要是通过正则表示实现对长段文字的分割。假设现在用正则表达式“[\u4E00-\u9FD5]+”进行分割,\u4E00\u9FD5这两个unicode值正好是Unicode表中的汉字的头和尾,因此该正则表达式表示任意一个或多个汉字。对应到原来的句子,就是用红色标注的部分:

单手补扣+8板阿联只是微微一笑他已赢回主帅信任

那么我们将红色的作为单独的一块,剩余的黑色部分也作为一块,最终分割结果过:

['', '单手补扣', '+8', '板阿联只是微微一笑', ',', '他已赢回主帅信任', '']

以上是全模式下的将段落分割成小句的方案。而在默认模式下,正则表达式变为了“[ \u4E00-\u9FD5 a-z A-Z 0-9+#&\._]+”, a-z表示小写字母, A-Z表示大写 0-9字母,表示任意数字,后面黑色部分表示字符‘+’‘#’‘&’‘.’‘_’。全模式下只能识别汉字,而默认模式下还可以识别字母数字符号等等,其对应的分割结果如下。


单手补扣+8板阿联只是微微一笑他已赢回主帅信任

['', '单手补扣+8板阿联只是微微一笑', ',', '他已赢回主帅信任', '']


四 默认模式

前面提到的原理三大步,第一步分割小句已经完成了,下一步就是根据小句构造有向无环图(DAG)了。假设当前要处理的句子是:“他已赢回主帅信任”,jieba中构造DAG的代码如下:

  1. def get_DAG(self, sentence):
  2. self.check_initialized()
  3. DAG = {}
  4. N = len(sentence)
  5. for k in xrange(N): # 遍历block中的每个字符
  6. tmplist = [] # 用于存储可能词
  7. i = k # 该字符所在位置
  8. frag = sentence[k]
  9. while i < N \
  10. and frag in self.FREQ: # 从字符位置开始遍历,直至末尾或字符不在字典内
  11. if self.FREQ[frag]: # 如果次数大于0,就将该位加入
  12. tmplist.append(i)
  13. i += 1
  14. frag = sentence[k:i + 1]
  15. if not tmplist:
  16. tmplist.append(k)
  17. DAG[k] = tmplist
  18. return DAG

上述代码最终得到DAG(return DAG):{0: [0], 1: [1], 2: [2, 3], 3: [3], 4: [4, 5], 5: [5], 6: [6, 7], 7: [7]}

可以看出该DAG就是一个key-value表,key值是一个坐标,value是一个列表。可以用下面的图形象化表示:


上述get_DAG函数的执行过程,就是沿着橘黄色的箭头遍历每个字符,当遍历到某个字符时,从该字符开始不断往后拓展,如果能成为一个词(通过查看字典),那么就在节点间连一个蓝色的箭头。比如对于“主”(key:2)来说,它自己本身(2-2)可以构成一个词,“主帅”(2-3)也能构成一个词。

有了图以后,我们要找最好的整体划分方案,也就是说“主”和“主帅”之间我们只能选一种,而具体的选择要从整体看,找到一个全局最佳方案,实现代码如下:

  1. def calc(self, sentence, DAG, route):
  2. N = len(sentence)
  3. route[N] = (0, 0)
  4. logtotal = log(self.total)
  5. for idx in xrange(N - 1, -1, -1):
  6. route[idx] = max((log(self.FREQ.get(sentence[idx:x + 1]) or 1) -
  7. logtotal + route[x + 1][0], x) for x in DAG[idx])

上面的代码是使用route存储最佳分词位置及分数,分数越大越好。使用动态规划计算最好的分词方式, 其实就是从末尾开始遍历,遍历到某个节点时,计算以该节点为起始位置,以最后一个节点为结束位置,这之间那段字符串的最佳分词方案。

当我们得到最佳分词方案后,就可以输出了,代码如下:

  1. def __cut_DAG(self, sentence):
  2. DAG = self.get_DAG(sentence) # 得到的有向无环图
  3. route = {}
  4. self.calc(sentence, DAG, route) # 根据有向无欢图就算最佳分词方案
  5. x = 0
  6. buf = ''
  7. N = len(sentence)
  8. while x < N:
  9. y = route[x][1] + 1
  10. l_word = sentence[x:y] # 依顺序取出每个分词
  11. if y - x == 1:
  12. buf += l_word # 若是单字先存入buf
  13. else:
  14. if buf:
  15. if len(buf) == 1:
  16. yield buf
  17. buf = ''# 若buf中有只有一个字符,则输出并清空buf
  18. else:
  19. if not self.FREQ.get(buf):
  20. recognized = finalseg.cut(buf)
  21. for t in recognized:
  22. yield t
  23. else:
  24. for elem in buf:
  25. yield elem
  26. buf = ''
  27. yield l_word # 输出非单个字符
  28. x = y # 指向下一个分词首字位置
  29.          if buf:
  30. if len(buf) == 1:
  31. yield buf
  32. elif not self.FREQ.get(buf):
  33. recognized = finalseg.cut(buf)
  34. for t in recognized:
  35. yield t
  36. else:
  37. for elem in buf:
  38. yield elem

上述代码就是根据得到的分词方案去将每个词输出,只是处理上有些小细节。如果当前要输出的是单字,不会马上输出,而是存入一个buf里,直至下一个词不是单字时,将会对buf内的所有单字组成的句子处理,其实也就是利用HMM识别未登录词。若不采用HMM模式,那么这里不需要对单字进行处理,而是直接输出。

利用HMM识别未登录词需要了解的知识请看这篇:https://blog.csdn.net/liujianfei526/article/details/50640176

最终目标是用{B,M,E,S}为连续的单字字符进行标注,标注完成后就得到了分词方案。


jieba中关于HMM的代码如下:

  1. def viterbi(obs, states, start_p, trans_p, emit_p):
  2. V = [{}] # tabular
  3. path = {}
  4. for y in states: # init
  5. V[0][y] = start_p[y] + emit_p[y].get(obs[0], MIN_FLOAT)
  6. path[y] = [y]
  7. for t in xrange(1, len(obs)):
  8. V.append({})
  9. newpath = {}
  10. for y in states:
  11. em_p = emit_p[y].get(obs[t], MIN_FLOAT)
  12. (prob, state) = max(
  13. [(V[t - 1][y0] + trans_p[y0].get(y, MIN_FLOAT) + em_p, y0) for y0 in PrevStatus[y]])
  14. V[t][y] = prob
  15. newpath[y] = path[state] + [y]
  16. path = newpath
  17. (prob, state) = max((V[len(obs) - 1][y], y) for y in 'ES')
  18. return (prob, path[state])

上述代码就是viterbi算法了,将观测字符存入obs,V是一个二维数组,第一维表示第几个字符,第二维表示第几个状态,数组中存储的值V[pos][status]可以理解为第pos位字符是status状态的概率。

首先对第一个字符单独特殊处理,因为之后都是状态->状态,而第一个是起始->状态。我们需要计算每个状态产生该字符的概率P(字符|状态),还要计算由起始状态转为每个状态的概率P(状态|起始),两者相乘既得到V(字符,状态)=P(状态|起始)*P(字符|状态)。

之后就是遍历剩余的字符,处理每个位置字符时,从所有可能的前置状态中找一个使V(上个字符,前置状态)*P(状态|前置状态)*P(字符|状态)最大,并将该最大值作为V(字符,状态)。同时用path记录一个序列,每次把新确定的状态放到末尾。计算完最后一个字符后,我们去找以E或S结尾且概率最大的状态序列。

五 全模式

  1. def __cut_all(self, sentence):
  2. dag = self.get_DAG(sentence)
  3. old_j = -1
  4. for k, L in iteritems(dag):
  5. if len(L) == 1 and k > old_j:
  6. yield sentence[k:L[0] + 1]
  7. old_j = L[0]
  8. else:
  9. for j in L:
  10. if j > k:
  11. yield sentence[k:j + 1]
  12. old_j = j
上述代码就是全模式下的算法,与默认模式的区别在于需要将所有的分词都输出。所以可以不必去理最佳分词方案,而是把所有的分词都输出。即从头开始遍历DAG,在每次遍历时都得到一个(k,L),其中k代表每个字符,L是一个列表,里面存储了一些坐标,从k位到L中的每个坐标可以构成一个词,所以每次遍历时,就把这些词都输出。唯一需要注意的是通过old_j防止重复输出。
声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
推荐阅读
相关标签
  

闽ICP备14008679号