0、写在前面

学习NLP也有一段时间了,对其中一些算法也有了比较系统的了解,所以最近就打算阅读一些nlp领域的开源代码,一方面是想查漏补缺完善一下自然语言处理的一些基础技术以及实现;另一方面是学习学习coding的规范以及tricks。关于源码平时使用较多的是python语言的jieba库,这原本是主打中文分词的一个库,但是现在的功能可远不止分词。所以在nlp技术上应该还是比较全面的,就决定是它啦。

1、jieba系统简介

“jieba”中文分词:做最好的Python中文分词组件。(这是它的广告词...)

1.1、特点:

  • 支持三种分词模式:

    • 精确模式,试图将句子最精确地切开,适合文本分析;
    • 全模式,把句子中所有的可以成词的词语都扫描出来, 速度非常快,但是不能解决歧义;
    • 搜索引擎模式,在精确模式的基础上,对长词再次切分,提高召回率,适合用于搜索引擎分词。
  • 支持繁体分词

  • 支持自定义词典

  • MIT 授权协议

1.2、主要算法:

  1. 基于前缀词典实现词图扫描,生成句子中汉字所有可能成词情况所构成的有向无环图(DAG),采用动态规划查找最大概率路径,找出基于词频的最大切分组合;
  2. 对于未登录词,采用了基于汉字成词能力的 HMM模型,采用Viterbi算法进行计算;
  3. 基于Viterbi算法的词性标注;
  4. 分别基于tfidf和textrank模型抽取关键词;

1.3、代码框架:

2、基于前缀词典及动态规划实现分词

jieba分词主要是基于统计词典,构造一个前缀词典;然后利用前缀词典对输入句子进行切分,得到所有的切分可能,根据切分位置,构造一个有向无环图(DAG);通过动态规划算法,计算得到最大概率路径,也就得到了最终的切分形式。

2.1、举个栗子

以“去北京大学玩”为例,作为待分词的输入文本。

离线统计的词典形式如下,每一行有三列,第一列是词,第二列是词频,第三列是词性。

... 北京大学 2053 nt

大学 20025 n

去 123402 v

玩 4207 v

北京 34488 ns

北 17860 ns

京 6583 ns

大 144099 a

学 17482 n .

..

 2.2、前缀词典的构建

首先是基于统计词典构造前缀词典,如统计词典中的词“北京大学”的前缀分别是“北”、“北京”、“北京大”;词“大学”的前缀是“大”。统计词典中所有的词形成的前缀词典如下所示,你也许会注意到“北京大”作为“北京大学”的前缀,但是它的词频却为0,这是为了便于后面有向无环图的构建。

...

北京大学 2053

北京大 0

大学 20025

去 123402

玩 4207

北京 34488

北 17860

京 6583

大 144099

学 17482

...

 2.3、有向无环图的构建

然后基于前缀词典,对输入文本进行切分,对于“去”,没有前缀,那么就只有一种划分方式;对于“北”,则有“北”、“北京”、“北京大学”三种划分方式;对于“京”,也只有一种划分方式;对于“大”,则有“大”、“大学”两种划分方式,依次类推,可以得到每个字开始的前缀词的划分方式。

在jieba分词中,对每个字都是通过在文本中的位置来标记的,因此可以构建一个以位置为key,相应划分的末尾位置构成的列表为value的映射,如下所示,

0: [0]

1: [1,2,4]

2: [2]

3: [3,4]

4: [4]

5: [5]

对于0: [0],表示位置0对应的词,就是0 ~ 0,就是“去”;对于1: [1,2,4],表示位置1开始,在1,2,4位置都是词,就是1 ~ 1,1 ~ 2,1 ~ 4,即“北”,“北京”,“北京大学”这三个词。

对于每一种划分,都将相应的首尾位置相连,例如,对于位置1,可以将它与位置1、位置2、位置4相连接,最终构成一个有向无环图,如下所示,

 2.4、最大概率路径计算

在得到所有可能的切分方式构成的有向无环图后,我们发现从起点到终点存在多条路径,多条路径也就意味着存在多种分词结果,例如,

# 路径1 0 -> 1 -> 2 -> 3 -> 4 -> 5

# 分词结果1 去 / 北 / 京 / 大 / 学 / 玩

# 路径2 0 -> 1 , 2 -> 3 -> 4 -> 5

# 分词结果2 去 / 北京 / 大 / 学 / 玩

# 路径3 0 -> 1 , 2 -> 3 , 4 -> 5

# 分词结果3 去 / 北京 / 大学 / 玩

# 路径4 0 -> 1 , 2 , 3 , 4 -> 5

# 分词结果4 去 / 北京大学 / 玩 ...

因此,我们需要计算最大概率路径,也即按照这种方式切分后的分词结果的概率最大。在计算最大概率路径时,jieba分词采用从后往前这种方式进行计算。为什么采用从后往前这种方式计算呢?因为,我们这个有向无环图的方向是从前向后指向,对于一个节点,我们只知道这个节点会指向后面哪些节点,但是我们很难直接知道有哪些前面的节点会指向这个节点。

在采用动态规划计算最大概率路径时,每到达一个节点,它前面的节点到终点的最大路径概率已经计算出来。

3、源码分析

3.1、算法流程

jieba分词的主函数cut(self, sentence, cut_all=False, HMM=True)位于jieba.__init__.py文件中

  • 使用__cut__DAG(self, sentence)函数构建前缀词典;
  • 使用get_DAG(self, sentence)函数构建有向无环图;
  • 使用calc(self, sentence, DAG, route)基于最大概率路径进行分词,如果遇到未登陆词,则调用HMM模型进行切分。

3.2、前缀词典的构建

    """构建前缀词典,解析离线统计词典文本文件f(每一行分别对应着词,词频,词性),将词和词频提取出来,形成key-value对,加入到前缀词典中lfreq对于每个词,再分别获得它的前缀词,如果前缀词已经存在于前缀词典中,则不处理;如果不在,则将词频设为0便于后续DAG构建"""def gen_pfdict(self, f):#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] = freqltotal += freq#获取该词的所有前缀词for ch in xrange(len(word)):wfrag = word[:ch + 1]#如果wfrag不在前缀词中,则将词频设为0if wfrag not in lfreq:lfreq[wfrag] = 0except ValueError:raise ValueError('invalid dictionary entry in %s at Line %s: %s' % (f_name, lineno, line))f.close()return lfreq, ltotal

PS:为什么jieba没有使用trie树作为前缀词典存储的数据结构?

对于get_DAG()函数来说,用Trie数据结构,特别是在Python环境,内存使用量过大。经实验,可构造一个前缀集合解决问题。该集合储存词语及其前缀,如set(['数', '数据', '数据结', '数据结构'])。在句子中按字正向查找词语,在前缀列表中就继续查找,直到不在前缀列表中或超出句子范围。大约比原词库增加40%词条。

3.3、有向无环图的构建

关于DAG的实现,jieba这里用了python的字典结构,最终的效果是{k : [k , j , ..] , m : [m , p , q] , ...},其中k和m为词S在文本中的位置,k对应的列表存放的是文本中以k开始且词S[k: j+1]在前缀词典中以k开始以j结尾的词的列表,即列表存档的是S中以k开始的可能的词语的结束位置。

    """从前往后依次遍历文本的每个位置,对于位置k,首先形成一个片段,这个片段只包含位置k的字,然后就判断该片段是否在前缀词典中,如果这个片段在前缀词典中,1.1 如果词频大于0,就将这个位置i追加到以k为key的一个列表中;1.2 如果词频等于0,如同前面提到的“北京大”,则表明前缀词典存在这个前缀,但是统计词典并没有这个词,继续循环;如果这个片段不在前缀词典中,则表明这个片段已经超出统计词典中该词的范围,则终止循环;然后该位置加1,然后就形成一个新的片段,该片段在文本的索引为[k:i+1],继续判断这个片段是否在前缀词典中。"""def get_DAG(self, sentence):#检查是否初始化self.check_initialized()#DAG存储数据结构是dictDAG = {}N = len(sentence)#依次遍历文本中的每个位置for k in xrange(N):tmplist = []i = k#位置k形成的片段frag = sentence[k]#判断片段是否在前缀词典中,如果不在则跳出循环,#即该片段已经超出统计词典中该词的长度(即不是同一个词)while i < N and frag in self.FREQ:#如果在词典中:#词频大于0则将该片段加入到DAG中。否则循环继续if self.FREQ[frag]:tmplist.append(i)#片段末尾位置加1i += 1#新的片段较旧的片段右边新增一个字frag = sentence[k:i + 1]if not tmplist:tmplist.append(k)DAG[k] = tmplistreturn DAG

如上节中“去北京大学玩”,最终形成的DAG为:

DAG = {0:[0], 1:[1,2,4], 2:[2], 3:[3, 4], 4:[4], 5:[]5}

3.4、最大概率计算

在上一小节当中,我们构建出的DAG的每个节点,都是带权的。对于在前缀词典中的词语,其权重就是它的词频。

我们想要求得route=(w1,w2,...,wn),使得∑weight(wi)最大。对于该问题可以使用动态规划来解。

    """函数是一个自底向上的动态规划问题,它从sentence的最后一个字(N-1)开始倒序遍历sentence的每个字(idx)的方式,计算子句sentence[idx ~ N-1]的概率对数得分。然后将概率对数得分最高的情况以(概率对数,词语最后一个位置)这样的元组保存在route中。"""def calc(self, sentence, DAG, route):N = len(sentence)#初始化末尾为0route[N] = (0, 0)#logtotal为构建前缀词频时所有的词频之和的对数值,#这里的计算使用概率对数值,可以有效防止下溢问题。logtotal = log(self.total)for idx in xrange(N - 1, -1, -1):#这里max函数的参数是元祖,比较的标准的第一个元素的大小#!!!!下面代码看不太懂 TTroute[idx] = max((log(self.FREQ.get(sentence[idx:x + 1]) or 1) -logtotal + route[x + 1][0], x) for x in DAG[idx])

关于未登陆词的分词采用的是HMM模型,写在下一篇里吧~

以上~

jieba源码分析(一)相关推荐

  1. jieba tfidf_【NLP】【三】jieba源码分析之关键字提取(TF-IDF/TextRank)

    [一]综述 利用jieba进行关键字提取时,有两种接口.一个基于TF-IDF算法,一个基于TextRank算法.TF-IDF算法,完全基于词频统计来计算词的权重,然后排序,在返回TopK个词作为关键字 ...

  2. jieba源码分析(二)

    0.写在前面 在jieba源码分析(一)里面已经jieba分词的一部分进行了分析,本文主要解决分词的另一块:未登陆词,也就是我们常说的新词.对于这些新词,我们前面所说的前缀词典中是不存在的,那么之前的 ...

  3. java jieba tfidf_【NLP】【三】jieba源码分析之关键字提取(TF-IDF/TextRank)

    [一]综述 利用jieba进行关键字提取时,有两种接口.一个基于TF-IDF算法,一个基于TextRank算法.TF-IDF算法,完全基于词频统计来计算词的权重,然后排序,在返回TopK个词作为关键字 ...

  4. python词云需要导入什么包_[python] 词云:wordcloud包的安装、使用、原理(源码分析)、中文词云生成、代码重写...

    词云,又称文字云.标签云,是对文本数据中出现频率较高的"关键词"在视觉上的突出呈现,形成关键词的渲染形成类似云一样的彩色图片,从而一眼就可以领略文本数据的主要表达意思.常见于博客. ...

  5. python生成中文词云的代码_[python] 基于词云的关键词提取:wordcloud的使用、源码分析、中文词云生成和代码重写...

    1. 词云简介 词云,又称文字云.标签云,是对文本数据中出现频率较高的"关键词"在视觉上的突出呈现,形成关键词的渲染形成类似云一样的彩色图片,从而一眼就可以领略文本数据的主要表达意 ...

  6. 【Golang源码分析】Go Web常用程序包gorilla/mux的使用与源码简析

    目录[阅读时间:约10分钟] 一.概述 二.对比: gorilla/mux与net/http DefaultServeMux 三.简单使用 四.源码简析 1.NewRouter函数 2.HandleF ...

  7. SpringBoot-web开发(四): SpringMVC的拓展、接管(源码分析)

    [SpringBoot-web系列]前文: SpringBoot-web开发(一): 静态资源的导入(源码分析) SpringBoot-web开发(二): 页面和图标定制(源码分析) SpringBo ...

  8. SpringBoot-web开发(二): 页面和图标定制(源码分析)

    [SpringBoot-web系列]前文: SpringBoot-web开发(一): 静态资源的导入(源码分析) 目录 一.首页 1. 源码分析 2. 访问首页测试 二.动态页面 1. 动态资源目录t ...

  9. SpringBoot-web开发(一): 静态资源的导入(源码分析)

    目录 方式一:通过WebJars 1. 什么是webjars? 2. webjars的使用 3. webjars结构 4. 解析源码 5. 测试访问 方式二:放入静态资源目录 1. 源码分析 2. 测 ...

最新文章

  1. python的redis数据库连接与使用
  2. Ceph Upstream 添加 InfiniBand RDMA 互联支持
  3. stm32中断优先级分组
  4. switch注意事项和细节讨论
  5. Java Maximum Subarray debug
  6. python浪漫代码_五行Python代码实现批量抠图
  7. hadoop启动后,9000端口无法连接,netstat -tpnl中找不到该端口
  8. 下列不是unix linux,下列软件中,不是操作系统的是______。A) LinuxB) UNIXC) MS-DOSD) MS-OfficeA.B.C.D._考题宝...
  9. 大数组情况下栈溢出解决
  10. Python全栈之路——运算符(Day 02)
  11. 应用实战:从Redis到Aerospike,我们踩了这些坑
  12. iRedmail配置手册
  13. 学会IDEA REST Client后,postman就可以丢掉了...
  14. 神州战神系列装系统过程,其他电脑大同小异
  15. 问题解决:java.sql.SQLException: No suitable driver found for jdbc:mysql
  16. 内容管理系统CMS简介
  17. 【案例】畅捷通T+无生产管理模块情况下按产成品统计直接材料成本
  18. 2022-2027年中国巴西鲷鱼养殖行业市场调研及未来发展趋势预测报告
  19. js实现表格列的位置拖拽
  20. 音频提取 4K YouTube to MP3

热门文章

  1. 通透!数据仓库领域常见建模方法及实例演示
  2. 暑假周进度总结报告3
  3. DataTable 数字排序问题
  4. javaweb基础 - Servlet
  5. Vagrant搭建虚拟化开发环境(五)虚拟机优化 PHP升级 打包分发
  6. 让div跟着鼠标移动
  7. C++反汇编第一讲,认识构造函数,析构函数,以及成员函数
  8. html 文件动态加载.PDI 流程图
  9. 编程—休息片刻的好处
  10. Linq 中的 left join