jieba分词源码阅读

jieba是一个开源的中文分词库,这几天看了下源码,就做下记录。

下载jieba后,tree得到主要部分的目录树结构如下:

├── 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

analyse目录下是几个提取文本关键词的算法实现。

dict.txt是总的词库,每行记录了一个词和这个词的词频及词性。

__init__.py是jieba的主要入口

finalseg是如果使用hmm,那么在初步分词之后还要调用这里的代码,主要是对hmm的实现。

然后介绍下主要接口__init__.py中的几个函数:

def gen_pfdict(self, f):
    lfreq = {}
    ltotal = 0
    f_name = resolve_filename(f)
    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 xrange(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

gen_pfdict加载dict.txt生成字典树,lfreq存储dict.txt每个词出现了多少次,以及每个词的所有前缀,前缀的频数置为0,ltotal是所有词出现的总次数。得到的字典树存在self.FREQ中。例如对于词"不拘一格",在字典树lfreq中是这样的{"不" : 0, "不拘" : ,"不拘一" , 0, "不拘一格" : freq} 其中的freq就是在dict.txt中记录的词频。

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

FREQ是根据dict.txt生成的字典树,get_DAG函数是根据FREQ对于每个句子sentence生成一个有向无环图,图信息存在字典DAG中,其中DAG[pos]是一个列表[a, b, c...],pos从0到len(sentence) - 1,表示sentence[pos : a + 1],sentence[pos, b + 1]...这些单词出现在了dict中。

例如以“但也并不是那么出乎意料或难以置信”这句话作为输入,生成的DAG如下,简单的讲就是把句子中词的位置标记出来

0 [0] 但

1 [1] 也

2 [2] 并

3 [3, 4] 不是

4 [4] 是

5 [5, 6] 那么

6 [6] 么

7 [7, 8, 10] 出乎意料

8 [8] 乎

9 [9, 10] 意料

10 [10] 料

11 [11] 或

12 [12, 13, 15] 难以置信

13 [13] 以

14 [14, 15] 置信

15 [15] 信

接下来就是对句子的切分,即jieba.cut。具体的分词流程概括起来如下:

1. 给定待分词的句子, 使用正则(re_han)获取匹配的中文字符(和英文字符)切分成的短语列表;

2. 利用get_DAG(sentence)函数获得待切分句子的DAG,首先检测(check_initialized)进程是否已经加载词库,若未初始化词库则调用initialize函数进行初始化,initialize中判断有无已经缓存的前缀词典cache_file文件,若有相应的cache文件则直接使用 marshal.load 方法加载前缀词典,若无则通过gen_pfdict对指定的词库dict.txt进行计算生成前缀词典,到jieba进程的初始化工作完成后就调用get_DAG获得句子的DAG;

3. 根据cut_block指定具体的方法(__cut_all,__cut_DAG,__cut_DAG_NO_HMM)对每个短语使用DAG进行分词 ,如cut_block=__cut_DAG时则使用DAG(查字典)和动态规划, 得到最大概率路径, 对DAG中那些没有在字典中查到的字, 组合成一个新的片段短语, 使用HMM模型进行分词, 也就是作者说的识别新词, 即识别字典外的新词;

4. 使用python的yield 语法生成一个词语生成器, 逐词语返回;

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

__cut_all是全模式切分,其实就是把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])

calc函数根据出现的概率来计算最可能的切词结果,其中单词A的概率为A的出现次数除以所有单词出现的总次数。通过dp计算最大概率的切词方式,route[i][0]表示sentence[i : len]的最大概率,route[i][1]表示sentence[i : len]这个子串的第一个切分位置在哪。

def __cut_DAG_NO_HMM(self, sentence):
        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 = ''

__cut_DAG_NO_HMM是不使用hmm的精确模式,首先调用calc,得到的route[i][1]中保存的是切分的位置信息,然后遍历输出切分方式。

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):
                        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)
            for t in recognized:
                yield t
        else:
            for elem in buf:
                yield elem

__cut_DAG同时使用最大概率路径和hmm,对于利用动态规划计算出的最大概率切分后,用buf将连续的单字收集以及未登录词收集起来,再调用finalseg.cut利用hmm进行分词。

然后是final_seg的__init__.py

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算法的实现函数,是viterbi算法给定了模型参数和观察序列之后求隐藏状态序列,其中对于分词,观察序列就是句子本身,而隐藏序列就是一个由{B, M, E, S}组成的序列,B表示词的开始,M表示词的中间,E表示词的结尾,S表示单字成词。

函数的输入参数中obs是输入的观察序列,即句子本身,states表示隐藏状态的集合,即{B, M, E, S},start_p表示第一个字分别处于{B, M, E, S}这几个隐藏状态的概率,trans_p是状态转移矩阵,记录了隐藏状态之间的转化概率,例如trans_p[‘B’][‘E’]代表的含义就是从状态B转移到状态E的概率,emit_p是发射概率矩阵,表示从一个隐藏状态转移到一个观察状态的概率,例如P[‘B’][‘\u4e00’]代表的含义就是’B’状态下观测的字为’\u4e00’(对应的汉字为’一’)的概率大小。

V是一个列表,V[i][j]表示对于子观察序列obs[0 ~ i],在第i个位置时隐藏状态为j的最大概率,其实就是个简单dp。

path是记录了状态转移的路径。

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:]

通过调用viterbi算法得到概率和path之后,对sentence进行分词。

参考:

http://www.cnblogs.com/lrysjtu/p/4529325.html

http://blog.csdn.net/daniel_ustc/article/details/48195287

时间: 2024-10-13 16:00:47

jieba分词源码阅读的相关文章

IK分词源码讲解(一)-初始篇

IK分词全名为IK Analyzer,是由java编写的中文分词工具包,目前在lucene以及solr中用的比较多,本系列的文章主要对ik的核心源码进行解析讲解,与大家分享,如果有错误的地方还望指教. 先来个整体概况: 其实从上面的图可以看出,真实的ik的代码其实并不多,这样给我们开始接触心里压力就小的多. 先打开IKAnalzyerDemo.java文件,先大体看看IK的工作流程 //构建IK分词器,使用smart分词模式 Analyzer analyzer = new IKAnalyzer(

IK分词源码讲解(七)-TokenStream以及incrementToken属性处理

首先介绍下在lucene中attributeSource的类层次: org.apache.lucene.util.AttributeSource ·        org.apache.lucene.analysis.TokenStream (implementsjava.io.Closeable) ·        org.apache.lucene.analysis.NumericTokenStream ·        org.apache.lucene.analysis.TeeSinkT

淘宝数据库OceanBase SQL编译器部分 源码阅读--生成逻辑计划

body, td { font-family: tahoma; font-size: 10pt; } 淘宝数据库OceanBase SQL编译器部分 源码阅读--生成逻辑计划 SQL编译解析三部曲分为:构建语法树,生成逻辑计划,指定物理执行计划.第一步骤,在我的上一篇博客淘宝数据库OceanBase SQL编译器部分 源码阅读--解析SQL语法树里做了介绍,这篇博客主要研究第二步,生成逻辑计划. 一. 什么是逻辑计划?我们已经知道,语法树就是一个树状的结构组织,每个节点代表一种类型的语法含义.如

Netty源码阅读(一) ServerBootstrap启动

Netty源码阅读(一) ServerBootstrap启动 转自我的Github Netty是由JBOSS提供的一个java开源框架.Netty提供异步的.事件驱动的网络应用程序框架和工具,用以快速开发高性能.高可靠性的网络服务器和客户端程序.本文讲会对Netty服务启动的过程进行分析,主要关注启动的调用过程,从这里面进一步理解Netty的线程模型,以及Reactor模式. 这是我画的一个Netty启动过程中使用到的主要的类的概要类图,当然是用到的类比这个多得多,而且我也忽略了各个类的继承关系

【原】SDWebImage源码阅读(三)

[原]SDWebImage源码阅读(三) 本文转载请注明出处 —— polobymulberry-博客园 1.SDWebImageDownloader中的downloadImageWithURL 我们来到SDWebImageDownloader.m文件中,找到downloadImageWithURL函数.发现代码不是很长,那就一行行读.毕竟这个函数大概做什么我们是知道的.这个函数大概就是创建了一个SDWebImageSownloader的异步下载器,根据给定的URL下载image. 先映入眼帘的

CI框架源码阅读笔记2 一切的入口 index.php

上一节(CI框架源码阅读笔记1 - 环境准备.基本术语和框架流程)中,我们提到了CI框架的基本流程,这里这次贴出流程图,以备参考: 作为CI框架的入口文件,源码阅读,自然由此开始.在源码阅读的过程中,我们并不会逐行进行解释,而只解释核心的功能和实现. 1.       设置应用程序环境 define('ENVIRONMENT', 'development'); 这里的development可以是任何你喜欢的环境名称(比如dev,再如test),相对应的,你要在下面的switch case代码块中

Nutch源码阅读进程2---Generate

继之前仓促走完nutch的第一个流程Inject后,再次起航,Debug模式走起,进入第二个预热阶段Generate~~~ 上期回顾:Inject主要是将爬取列表中的url转换为指定格式<Text,CrawlDatum>存在CrawlDb中,主要做了两件事,一是读取种子列表中的url,对其进行了url过滤.规范化,当然这其中用的是hadoop的mapreduce模式提交job到jobtracker,因为没有研读hadoop源码,所以这块先放放,理清nutch的大体思路后再去啃hadoop的ma

spark.mllib源码阅读-分类算法4-DecisionTree

本篇博文主要围绕Spark上的决策树来讲解,我将分为2部分来阐述这一块的知识.第一部分会介绍一些决策树的基本概念.Spark下决策树的表示与存储.结点分类信息的存储.结点的特征选择与分类:第二部分通过一个Spark自带的示例来看看Spark的决策树的训练算法.另外,将本篇与上一篇博文"spark.mllib源码阅读bagging方法"的bagging子样本集抽样方法结合,也就理解了Spark下的决策森林树的实现过程. 第一部分: 决策树模型 分类决策树模型是一种描述对实例进行分类的树形

源码阅读系列:源码阅读方法

一.前提条件 1.纯熟扎实的语言基础 ??如果你学java,却对反射.泛型.注解一直半解,还是不要去读什么框架了,回去把java基础打扎实反而对你自身更有益. 2.UML能力 ??在软件工程中,UML在软件的不同生命周期阶段扮演着非常重要的角色,没有好的UML水平,面对大型的项目源码会束手无策. 3.对业务的理解 ??如果你要阅读的项目业务性比较强,事先对业务有一定的了解是必须的. 4.设计模式.重构的掌握 ??编程语言什么的没什么好说.着重提一个:设计模式由于Android源代码用到各种各样的