【中文分词】常见工具及算法

我始终认为,学习新事物的时候,若能先有一个感性的认识,再去学具体的抽象知识和原理会有更深的体会。因此,本文首先介绍中文分词常用的工具及基本用法,然后再说明分词常用算法的原理或给出学习链接,最后总结中文分词的主要难点。

常见工具及用法

工具1:pyltp

分句

使用 pyltp 进行分句示例如下

1
2
3
4
# -*- coding: utf-8 -*-
from pyltp import SentenceSplitter
sents = SentenceSplitter.split('元芳你怎么看?我就趴窗口上看呗!') # 分句
print '\n'.join(sents)

结果如下

1
2
元芳你怎么看?
我就趴窗口上看呗!

分词

使用 pyltp 进行分词示例如下

1
2
3
4
5
6
7
8
9
10
11
# -*- coding: utf-8 -*-
import os
LTP_DATA_DIR = '/path/to/your/ltp_data' # ltp模型目录的路径
cws_model_path = os.path.join(LTP_DATA_DIR, 'cws.model') # 分词模型路径,模型名称为`cws.model`

from pyltp import Segmentor
segmentor = Segmentor() # 初始化实例
segmentor.load(cws_model_path) # 加载模型
words = segmentor.segment('元芳你怎么看') # 分词
print '\t'.join(words)
segmentor.release() # 释放模型

结果如下

1
元芳  你       怎么      看

words = segmentor.segment('元芳你怎么看') 的返回值类型是native的VectorOfString类型,可以使用list转换成Python的列表类型,例如

1
2
3
4
5
6
7
8
9
...
>>> words = segmentor.segment('元芳你怎么看')
>>> type(words)
<class 'pyltp.VectorOfString'>
>>> words_list = list(words)
>>> type(words_list)
<type 'list'>
>>> print words_list
['\xe5\xae\xa2\xe6\x9c\x8d', '\xe5\xa4\xaa', '\xe7\xb3\x9f\xe7\xb3\x95', '\xe4\xba\x86']

使用分词外部词典

pyltp 分词支持用户使用自定义词典。分词外部词典本身是一个文本文件(plain text),每行指定一个词,编码同样须为 UTF-8,样例如下所示

1
2
苯并芘
亚硝酸盐

示例如下

1
2
3
4
5
6
7
8
9
10
11
# -*- coding: utf-8 -*-
import os
LTP_DATA_DIR = '/path/to/your/ltp_data' # ltp模型目录的路径
cws_model_path = os.path.join(LTP_DATA_DIR, 'cws.model') # 分词模型路径,模型名称为`cws.model`

from pyltp import Segmentor
segmentor = Segmentor() # 初始化实例
segmentor.load_with_lexicon(cws_model_path, '/path/to/your/lexicon') # 加载模型,第二个参数是您的外部词典文件路径
words = segmentor.segment('亚硝酸盐是一种化学物质')
print '\t'.join(words)
segmentor.release()

使用个性化分词模型

个性化分词是 LTP 的特色功能。个性化分词为了解决测试数据切换到如小说、财经等不同于新闻领域的领域。 在切换到新领域时,用户只需要标注少量数据。 个性化分词会在原有新闻数据基础之上进行增量训练。 从而达到即利用新闻领域的丰富数据,又兼顾目标领域特殊性的目的。

pyltp 支持使用用户训练好的个性化模型。关于个性化模型的训练需使用 LTP,详细介绍和训练方法请参考 个性化分词

在 pyltp 中使用个性化分词模型的示例如下

1
2
3
4
5
6
7
8
9
10
11
# -*- coding: utf-8 -*-
import os
LTP_DATA_DIR = '/path/to/your/ltp_data' # ltp模型目录的路径
cws_model_path = os.path.join(LTP_DATA_DIR, 'cws.model') # 分词模型路径,模型名称为`cws.model`

from pyltp import CustomizedSegmentor
customized_segmentor = CustomizedSegmentor() # 初始化实例
customized_segmentor.load(cws_model_path, '/path/to/your/customized_model') # 加载模型,第二个参数是您的增量模型路径
words = customized_segmentor.segment('亚硝酸盐是一种化学物质')
print '\t'.join(words)
customized_segmentor.release()

同样,使用个性化分词模型的同时也可以使用外部词典

1
2
3
4
5
6
7
8
9
10
11
# -*- coding: utf-8 -*-
import os
LTP_DATA_DIR = '/path/to/your/ltp_data' # ltp模型目录的路径
cws_model_path = os.path.join(LTP_DATA_DIR, 'cws.model') # 分词模型路径,模型名称为`cws.model`

from pyltp import CustomizedSegmentor
customized_segmentor = CustomizedSegmentor() # 初始化实例
customized_segmentor.load_with_lexicon(cws_model_path, '/path/to/your/customized_model', '/path/to/your/lexicon') # 加载模型
words = customized_segmentor.segment('亚硝酸盐是一种化学物质')
print '\t'.join(words)
customized_segmentor.release()

分词标注集

标记 含义 举例
B 词首
I 词中
E 词尾
S 单字成词

工具2:jieba

分词

  • jieba.cut 方法接受三个输入参数: 需要分词的字符串;cut_all 参数用来控制是否采用全模式;HMM 参数用来控制是否使用 HMM 模型
  • jieba.cut_for_search 方法接受两个参数:需要分词的字符串;是否使用 HMM 模型。该方法适合用于搜索引擎构建倒排索引的分词,粒度比较细
  • 待分词的字符串可以是 unicode 或 UTF-8 字符串、GBK 字符串。注意:不建议直接输入 GBK 字符串,可能无法预料地错误解码成 UTF-8
  • jieba.cut 以及 jieba.cut_for_search 返回的结构都是一个可迭代的 generator,可以使用 for 循环来获得分词后得到的每一个词语(unicode),或者用
  • jieba.lcut 以及 jieba.lcut_for_search 直接返回 list
  • jieba.Tokenizer(dictionary=DEFAULT_DICT) 新建自定义分词器,可用于同时使用不同词典。jieba.dt 为默认分词器,所有全局分词相关函数都是该分词器的映射。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# encoding=utf-8
import jieba

seg_list = jieba.cut("我来到北京清华大学", cut_all=True)
print("Full Mode: " + "/ ".join(seg_list)) # 全模式

seg_list = jieba.cut("我来到北京清华大学", cut_all=False)
print("Default Mode: " + "/ ".join(seg_list)) # 精确模式

seg_list = jieba.cut("他来到了网易杭研大厦") # 默认是精确模式
print(", ".join(seg_list))

seg_list = jieba.cut_for_search("小明硕士毕业于中国科学院计算所,后在日本京都大学深造") # 搜索引擎模式
print(", ".join(seg_list))

输出:

1
2
3
4
5
6
7
【全模式】: 我/ 来到/ 北京/ 清华/ 清华大学/ 华大/ 大学

【精确模式】: 我/ 来到/ 北京/ 清华大学

【新词识别】:他, 来到, 了, 网易, 杭研, 大厦 (此处,“杭研”并没有在词典中,但是也被Viterbi算法识别出来了)

【搜索引擎模式】: 小明, 硕士, 毕业, 于, 中国, 科学, 学院, 科学院, 中国科学院, 计算, 计算所, 后, 在, 日本, 京都, 大学, 日本京都大学, 深造

添加自定义词典


载入词典

  • 开发者可以指定自己自定义的词典,以便包含 jieba 词库里没有的词。虽然 jieba 有新词识别能力,但是自行添加新词可以保证更高的正确率
  • 用法: jieba.load_userdict(file_name) # file_name 为文件类对象或自定义词典的路径
  • 词典格式和 dict.txt 一样,一个词占一行;每一行分三部分:词语、词频(可省略)、词性(可省略),用空格隔开,顺序不可颠倒。file_name 若为路径或二进制方式打开的文件,则文件必须为 UTF-8 编码。
  • 词频省略时使用自动计算的能保证分出该词的词频。

例如:

1
2
3
4
创新办 3 i
云计算 5
凱特琳 nz
台中

调整词典

  • 使用 add_word(word, freq=None, tag=None)del_word(word) 可在程序中动态修改词典。
  • 使用 suggest_freq(segment, tune=True) 可调节单个词语的词频,使其能(或不能)被分出来。
  • 注意:自动计算的词频在使用 HMM 新词发现功能时可能无效。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
>>> print('/'.join(jieba.cut('如果放到post中将出错。', HMM=False)))
如果/放到/post/中将/出错/。
>>> jieba.suggest_freq(('中', '将'), True)
494
>>> print('/'.join(jieba.cut('如果放到post中将出错。', HMM=False)))
如果/放到/post/中/将/出错/。
>>> print('/'.join(jieba.cut('「台中」正确应该不会被切开', HMM=False)))
「/台/中/」/正确/应该/不会/被/切开
>>> jieba.suggest_freq('台中', True)
69
>>> print('/'.join(jieba.cut('「台中」正确应该不会被切开', HMM=False)))
「/台中/」/正确/应该/不会/被/切开

工具3:thulac-python

接口使用示例

  • python版

    1
    2
    3
    4
    5
    6
    代码示例1
    import thulac

    thu1 = thulac.thulac() #默认模式
    text = thu1.cut("我爱北京天安门", text=True) #进行一句话分词
    print(text)
    1
    2
    3
    代码示例2
    thu1 = thulac.thulac(seg_only=True) #只进行分词,不进行词性标注
    thu1.cut_f("input.txt", "output.txt") #对input.txt文件内容进行分词,输出到output.txt

接口参数

  • thulac(user_dict=None, model_path=None, T2S=False, seg_only=False, filt=False, deli='_')初始化程序,进行自定义设置

    1
    2
    3
    4
    5
    6
    user_dict	      	设置用户词典,用户词典中的词会被打上uw标签。词典中每一个词一行,UTF8编码
    T2S 默认False, 是否将句子从繁体转化为简体
    seg_only 默认False, 时候只进行分词,不进行词性标注
    filt 默认False, 是否使用过滤器去除一些没有意义的词语,例如“可以”。
    model_path 设置模型文件所在文件夹,默认为models/
    deli 默认为‘_’, 设置词与词性之间的分隔符

    rm_space 默认为False, 是否去掉原文本中的空格后再进行分词

    1
    2

    - `cut(文本, text=False)` 对一句话进行分词

    text 默认为False, 是否返回文本,不返回文本则返回一个二维数组([[word, tag]..]),seg_only模式下tag为空字符。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    - `cut_f(输入文件, 输出文件)` 对文件进行分词

    - `run()` 命令行交互式分词(屏幕输入、屏幕输出)

    #### 命令行运行(限pip安装使用)

    直接调用

    ```python
    python -m thulac input.txt output.txt
    #从input.txt读入,并将分词和词性标注结果输出到ouptut.txt中

    #如果只需要分词功能,可在增加参数"seg_only"
    python -m thulac input.txt output.txt seg_only

fast接口

(请下载make后将得到的libthulac.so放入models文件夹同目录下)

有两个函数实现了fast接口,仅函数名改变,参数使用同普通函数

1
cut -> fast_cut, cut_f -> fast_cut_f

其他工具

工具列表

中科院计算所NLPIR http://ictclas.nlpir.org/nlpir/

ansj分词器 https://github.com/NLPchina/ansj_seg

斯坦福分词器 https://nlp.stanford.edu/software/segmenter.shtml

Hanlp分词器 https://github.com/hankcs/HanLP

KCWS分词器(字嵌入+Bi-LSTM+CRF) https://github.com/koth/kcws

ZPar https://github.com/frcchang/zpar/releases

IKAnalyzer https://github.com/wks/ik-analyzer

简单说明

哈工大的分词器:主页上给过调用接口,每秒请求的次数有限制。

清华大学THULAC:目前已经有Java、Python和C++版本,并且代码开源。

斯坦福分词器:作为众多斯坦福自然语言处理中的一个包,目前最新版本3.7.0, Java实现的CRF算法。可以直接使用训练好的模型,也提供训练模型接口。

Hanlp分词:求解的是最短路径。优点:开源、有人维护、可以解答。原始模型用的训练语料是人民日报的语料,当然如果你有足够的语料也可以自己训练。

结巴分词工具:基于前缀词典实现高效的词图扫描,生成句子中汉字所有可能成词情况所构成的有向无环图 (DAG);采用了动态规划查找最大概率路径, 找出基于词频的最大切分组合;对于未登录词,采用了基于汉字成词能力的 HMM 模型,使用了 Viterbi 算法。

字嵌入+Bi-LSTM+CRF分词器:本质上是序列标注,这个分词器用人民日报的80万语料,据说按照字符正确率评估标准能达到97.5%的准确率,各位感兴趣可以去看看。

ZPar分词器:新加坡科技设计大学开发的中文分词器,包括分词、词性标注和Parser,支持多语言,据说效果是公开的分词器中最好的,C++语言编写。

速度对比

由于分词是基础组件,其性能也是关键的考量因素。通常,分词速度跟系统的软硬件环境有相关外,还与词典的结构设计和算法复杂度相关。字嵌入+Bi-LSTM+CRF分词器,其速度相对较慢。另外,开源项目 https://github.com/ysc/cws_evaluation 曾对多款分词器速度和效果进行过对比,可供参考。

公开的分词数据集

测试数据集

1、SIGHAN Bakeoff 2005 MSR,560KB

http://sighan.cs.uchicago.edu/bakeoff2005/

2、SIGHAN Bakeoff 2005 PKU, 510KB

http://sighan.cs.uchicago.edu/bakeoff2005/

3、人民日报 2014, 65MB

https://pan.baidu.com/s/1hq3KKXe

常用分词算法及原理

基于词典

原理

也称为字符串匹配分词算法。将待匹配的字符串和已有的“充分大的”词典中的词进行匹配,若匹配成功,则识别了该词。识别出一个词,根据扫描方向的不同分为正向匹配和逆向匹配。根据不同长度优先匹配的情况,分为最大(最长)匹配和最小(最短)匹配。这是一类算法,较常见的有:正向最大匹配法、逆向最大匹配法、双向匹配分词法。

  • 最大正向匹配法(MM, MaximumMatching Method)

通常简称为MM法。其基本思想为:假定分词词典中的最长词有 i 个汉字字符,则用被处理文档的当前字串中的前 i 个字作为匹配字段,查找字典。若字典中存在这样的一个i字词,则匹配成功,匹配字段被作为一个词切分出来。如果词典中找不到这样的一个i字词,则匹配失败,将匹配字段中的最后一个字去掉,对剩下的字串重新进行匹配处理。

  • 逆向最大匹配法(ReverseMaximum Matching Method)

先将文档进行倒排处理,生成逆序文档。然后,根据逆序词典,对逆序文档用正向最大匹配法处理即可。

由于汉语中偏正结构较多,若从后向前匹配,可以适当提高精确度。所以,逆向最大匹配法比正向最大匹配法的误差要小。统计结果表明 ,单纯使用正向最大匹配的错误率为 1/169,单纯使用逆向最大匹配的错误率为 1/245。

在实际使用时,为了提高系统分词的准确度,可以采用正向最大匹配法和逆向最大匹配法相结合的分词方案。

  • 双向匹配法

先根据标点对文档进行粗切分,把文档分解成若干个句子,然后再对这些句子用正向最大匹配法和逆向最大匹配法进行扫描切分。如果两种分词方法得到的匹配结果相同,则认为分词正确,否则,按最小集处理。

特点

因为词典已经建立好,所以该类算法的特点是速度快。而且这种分词算法很直观,符合人的思维方式,是较早提出的分词方案,也因此应用范围很广泛。

缺点:严重依赖于词典,不能根据文档上下文的语义特征来切分词语,对于未登录词的分词效果差。

优化方向

很明显,要优化这种分词方法,主要的着力点在两个方面:

一是在词典方面:扩充词典,同时保证准确性;改变词表的组织结构(比如字典树、哈希索引)以提高效率。

二是优化字符串匹配算法:比如设定最大长度、字符串存储和查找的方式等等。

基于统计机器学习

这类目前常用的是算法是HMM、CRF、SVM、深度学习等算法,比如stanford、Hanlp分词工具是基于CRF算法。

以CRF为例,基本思路是对汉字进行标注训练,不仅考虑了词语出现的频率,还考虑上下文,具备较好的学习能力,因此其对歧义词和未登录词的识别都具有良好的效果。比如上面用到的工具 pyltp,底层的原理就是 CRF。

CRF(条件随机场)不是几句话可以介绍清楚的,要深入学习 CRF,可以参考:

常见的分词器都是使用机器学习算法和词典相结合,一方面能够提高分词准确率,另一方面能够改善领域适应性。

基于深度学习

随着深度学习的兴起,也出现了基于神经网络的分词器,例如有人员尝试使用 双向LSTM+CRF 实现分词器,其本质上是序列标注,所以有通用性,命名实体识别等都可以使用该模型,据报道其分词器字符准确率可高达97.5%。算法框架的思路与论文《Neural Architectures for Named Entity Recognition》类似,利用该框架可以实现中文分词。

中文分词的难点

目前中文分词难点主要有三个:

1、分词标准:比如人名,在哈工大的标准中姓和名是分开的,但在Hanlp中是合在一起的。这需要根据不同的需求制定不同的分词标准。

2、歧义:对同一个待切分字符串存在多个分词结果。

歧义又分为组合型歧义、交集型歧义和真歧义三种类型。

1) 组合型歧义:分词是有不同的粒度的,指某个词条中的一部分也可以切分为一个独立的词条。比如“中华人民共和国”,粗粒度的分词就是“中华人民共和国”,细粒度的分词可能是“中华/人民/共和国”

2) 交集型歧义:在“郑州天和服装厂”中,“天和”是厂名,是一个专有词,“和服”也是一个词,它们共用了“和”字。

3) 真歧义:本身的语法和语义都没有问题, 即便采用人工切分也会产生同样的歧义,只有通过上下文的语义环境才能给出正确的切分结果。例如:对于句子“美国会通过对台售武法案”,既可以切分成“美国/会/通过对台售武法案”,又可以切分成“美/国会/通过对台售武法案”。

一般在搜索引擎中,构建索引时和查询时会使用不同的分词算法。常用的方案是,在索引的时候使用细粒度的分词以保证召回,在查询的时候使用粗粒度的分词以保证精度。

3、新词:也称未被词典收录的词,该问题的解决依赖于人们对分词技术和汉语语言结构的进一步认识。

Reference

  1. 使用 pyltp
  2. jieba - GitHub
  3. 有哪些比较好的中文分词方案? - 竹间智能 Emotibot的回答 - 知乎
  4. 【NLP】中文分词:原理及分词算法