jieba源码阅读与思考

jieba源码阅读笔记

由于在做模型的时候需要对切词这块做些优化,jieba切词目前主要融合了基于词典的分词和HMM两种方法,阅读了一下jieba的源码,思考了几个优化方案,在此mark一下,欢迎各位大牛前来交流,如果有谬误欢迎指正~

正常使用切词方法:
# 模式一 精确模式:
import jieba
s = "我们都是好孩子"
jieba.cut(s)
# re: 我们 都 是 好孩子


# 模式二 全模式:
jieba.cut(s, cut_all=True)
# re: 我们 都 是 好孩子 孩子

# 模式三 不适用HMM:
jieba.cut(s, HMM = False)

源码阅读:
  • 进入源码里面瞅瞅
# jieba.cut  finalseg/__init__.py
    re_han_default = re.compile("([\u4E00-\u9FD5a-zA-Z0-9+#&\._%]+)", re.U)
    re_skip_default = re.compile("(\r\n|\s)", re.U)
    re_han_cut_all = re.compile("([\u4E00-\u9FD5]+)", re.U)
    re_skip_cut_all = re.compile("[^a-zA-Z0-9+#\n]", re.U)
    
    def cut(self, sentence, cut_all=False, HMM=True):
        sentence = strdecode(sentence)  
        if cut_all:
            re_han = re_han_cut_all
            re_skip = re_skip_cut_all
        else:
            re_han = re_han_default
            re_skip = re_skip_default
        if cut_all:
            cut_block = self.__cut_all
        elif HMM:
            cut_block = self.__cut_DAG
        else:
            cut_block = self.__cut_DAG_NO_HMM
        blocks = re_han.split(sentence)
        for blk in blocks:
            if not blk:
                continue
            if re_han.match(blk):
                for word in cut_block(blk):
                    yield word
            else:
                tmp = re_skip.split(blk)
                for x in tmp:
                    if re_skip.match(x):
                        yield x
                    elif not cut_all:
                        for xx in x:
                            yield xx
                    else:
                        yield x

整体流程
可见一个句子进来之后,jieba先简单实用正则表达式最一下拆分,然后根据cut_all, HMM的选项,用不同的方法切词,其中HMM默认是打开的。

  • 先看看cut_all:
# cut_all模式
    def __cut_all(self, sentence):
        dag = self.get_DAG(sentence)
        old_j = -1
        for k, L in iteritems(dag):
            if len(L) == 1 and k > old_j:
                yield sentence[k:L[0] + 1]
                old_j = L[0]
            else:
                for j in L:
                    if j > k:
                        yield sentence[k:j + 1]
                        old_j = j

该模式最简单就是把构成的DAG图,返回给前端。
dag

    def get_DAG(self, sentence):
        self.check_initialized()
        DAG = {}
        N = len(sentence)
        for k in xrange(N):
            tmplist = []
            i = k
            frag = sentence[k]
            while i < N and frag in self.FREQ:
                if self.FREQ[frag]:
                    tmplist.append(i)
                i += 1
                frag = sentence[k:i + 1]
            if not tmplist:
                tmplist.append(k)
            DAG[k] = tmplist
        return DAG

DAG的数据类似:

{0: [0], 1: [1, 2], 2: [2], 3: [3, 4], 4: [4]} 

字典中的key 是没个字对应句子的index,后面的value 是一个list就是可达的路径。比如{1:[1,2]}意思就是“来”和“来到”这两个词在词典中存在。其他的类推。

  • 再看看 __cut_DAG:
    def __cut_DAG(self, sentence):
        DAG = self.get_DAG(sentence)
        route = {}
        # 基于词典的算法,通过动态规划计算分割点
        self.calc(sentence, DAG, route)
        x = 0
        buf = ''
        N = len(sentence)
        while x < N:
            y = route[x][1] + 1
            l_word = sentence[x:y]
            if y - x == 1:
                buf += l_word
            else:
                if buf:
                    if len(buf) == 1:
                        yield buf
                        buf = ''
                    else:
                        if not self.FREQ.get(buf):
                            # 将未登陆词用HMM处理一下
                            recognized = finalseg.cut(buf)
                            for t in recognized:
                                yield t
                        else:
                            for elem in buf:
                                yield elem
                        buf = ''
                yield l_word
            x = y

        if buf:
            if len(buf) == 1:
                yield buf
            elif not self.FREQ.get(buf):
                recognized = finalseg.cut(buf)
        ......

由以上代码可见,首先先利用基于词典的分词方法,通过动态规划求最优分割点,然后对于未登陆词,利用HMM进行切词处理。我们接下来分别来看一下这两个模块。

    # 基于词典在带权重的DAG图上进行动态规划,其中每个节点的权重为词频
    def calc(self, sentence, DAG, route):
        N = len(sentence)
        route[N] = (0, 0)
        logtotal = log(self.total)
        for idx in xrange(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])

对于结点Wi和其可能存在的多个后继Wj和Wk,有:
任意通过Wi到达Wj的路径的权重为该路径通过Wi的路径权重加上Wj的权重{Ri->j} = {Ri + weight(j)} ;
任意通过Wi到达Wk的路径的权重为该路径通过Wi的路径权重加上Wk的权重{Ri->k} = {Ri + weight(k)} ;
对于整个句子的最优路径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)} 这是一个自底向下的动态规划问题

再看看HMM:

def __cut(sentence):
    global emit_P
    prob, pos_list = viterbi(sentence, 'BMES', start_P, trans_P, emit_P)
    begin, nexti = 0, 0
    # print pos_list, sentence
    for i, char in enumerate(sentence):
        pos = pos_list[i]
        if pos == 'B':
            begin = i
        elif pos == 'E':
            yield sentence[begin:i + 1]
            nexti = i + 1
        elif pos == 'S':
            yield char
            nexti = i + 1
    if nexti < len(sentence):
        yield sentence[nexti:]

def viterbi(obs, states, start_p, trans_p, emit_p):
    V = [{}]  # tabular
    path = {}
    for y in states:  # init
        V[0][y] = start_p[y] + emit_p[y].get(obs[0], MIN_FLOAT)
        path[y] = [y]
    for t in xrange(1, len(obs)):
        V.append({})
        newpath = {}
        for y in states:
            em_p = emit_p[y].get(obs[t], MIN_FLOAT)
            (prob, state) = max(
                [(V[t - 1][y0] + trans_p[y0].get(y, MIN_FLOAT) + em_p, y0) for y0 in PrevStatus[y]])
            V[t][y] = prob
            newpath[y] = path[state] + [y]
        path = newpath

    (prob, state) = max((V[len(obs) - 1][y], y) for y in 'ES')

    return (prob, path[state])

通过预训练得到HMM的初始状态、转移矩阵、发射矩阵,然后利用Viterbi算法进行动态规划,得到词性标注,进而达到切词的目的。

  • 最后看看 __cut_DAG_NO_HMM:
    说白了就是 cut_DAG去掉了HMM部分,每个未登陆的词单独拎出来。
总结:

jieba切词默认是精确模式,先利用基于词典的算法得到分词,对于未登录词利用hmm算法进行拆分。而对于全模式,直接将jieba切词过程中产生的dag图处理后返回。
所以可见,要对分词进行优化,主要包括两个方面:

  1. 丰富词库,常用方法有引入新词库,新词发现系列等
  2. 优化HMM或者用更强的模型入CRF、LSTM等替代HMM

最后附上源码的整体目录树结构

├── jieba
    │         ├── analyse
    │         │         ├── analyzer.py
    │         │         ├── idf.txt
    │         │         ├── __init__.py
    │         │         ├── textrank.py
    │         │         └── tfidf.py
    │         ├── _compat.py
    │         ├── dict.txt
    │         ├── finalseg
    │         │         ├── __init__.py
    │         │         ├── prob_emit.p
    │         │         ├── prob_emit.py
    │         │         ├── prob_start.p
    │         │         ├── prob_start.py
    │         │         ├── prob_trans.p
    │         │         └── prob_trans.py
    │         ├── __init__.py
    │         ├── __main__.py
    │         └── posseg
    │             ├── char_state_tab.p
    │             ├── char_state_tab.py
    │             ├── __init__.py
    │             ├── prob_emit.p
    │             ├── prob_emit.py
    │             ├── prob_start.p
    │             ├── prob_start.py
    │             ├── prob_trans.p
    │             ├── prob_trans.py
    │             └── viterbi.py
    ├── LICENSE
    ├── MANIFEST.in
    ├── README.md
    ├── setup.py
    └── test
参考资料:

github jieba
统计学习方法 李航
浅谈分词算法系列
jieba源码解析

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值