在NLP任务当中,我们第一步要处理的问题,往往是分词问题。
不同于英文等语言中天然的以空格为分隔的分词方式,中文的分词本身就需要针对语意进行理解,这使得其分词便成为了一个复杂的问题。
当然,在一些中文的nlp任务中,可以直接采用字级别的分词方式,即直接以单字作为输入单元,这样的方式可以绕过分词问题,而且其所需的词表也往往会大幅减小,事实上bert的中文模型也是基本依赖于字级别的分词处理方式。
但是,这种处理方式会带来一个问题,即它会先天的丢失掉很多词汇固有的信息,比如上校与学校,两者都有校这个单字,但是其内在的语意却千差万别。
因此,更多的情况下,我们需要保留分词这一个步骤,而后基于分词的结果进行中文nlp任务的训练。
下面,我们就来介绍几种常用的中文分词工具。
jieba分词大约是中文分词工具中最为常用的一个分词工具了。
给出jieba分词的github链接如下:
他是基于词频统计的一个分词模型,因此其优点在于轻量级且运行速度非常快,而且可以达到相对较好的分词效果。
其使用文档也就位于其GitHub项目之中,而且是中文文档,阅读起来也非常方便,非常容易上手。
下面,我们给出jieba分词的基本用法说明如下。
jieba分词的使用方法非常简单,我们只需要仿照下述代码即可满足大部分的使用需求:
import jieba
text = "今天天气真好"
jieba.cut(text)
但是需要注意的是,jieba的cut函数生成的结果会是一个生成器,要获取其真正的分词结果,我们还需要做一步额外的取用操作。
因此,更多情况下,事实上我们可以直接调用jieba.lcut()
方法直接生成分词结果的list。
给出代码样例如下:
import jieba
text = "今天天气真好"
jieba.lcut(text) # ['今天天气', '真', '好']
除了上述基本的分词用法之外,jieba事实上还提供了其他更多的高阶分词功能,比如:
下面,我们来考察一下其实现方法:
jieba分词的默认方式为精准匹配分词,但是这种方式只会生成最大概率的分词结果,有时会导致错误。
为了尽可能地获取更多可能的分词信息从而规避掉分词错误,jieba可以采用全模式的分词来获取所有可能的分词形式。
下面,给出其实现方法如下:
import jieba
jieba.lcut("今天天气真好", cut_all=True)
# ['今天', '今天天气', '天天', '天气', '真好']
此外,由于jieba分词是在通用领域进行生成的,因此,针对特定领域的分词效果往往会出现一定的性能下降,尤其是针对领域专有名词,jieba往往会失效。
因此,jieba允许用户人为的传入特定的领域关键词词表来针对性地对分词效果进行优化。
其实现方法包含以下两种:
特别地,需要注意的是,jieba的外部词汇加入方式为强加入方法,即如果加入的词汇出现于待分词句子当中,则这个词一定会被独立分词出来。
因此,这种方式特别适用于专有名字的加入,但是却不太适合一些较为泛用的可能有多种分词方式的词汇的加入。
此外,jieba还提供了tf-idf以及TextRank文本关键词的抽取功能。
这里,我们暂时只给出其实现代码样例,有关其原理部分,回头大概会写篇文章专门来进行一下整理,这里暂时就不详述了,如果有兴趣的话也可以自行网上搜索一下,相关的内容还是非常多的。
基于tf-idf的关键词抽取实现代码样例如下:
import jieba.analyse
text = "衬衫的价格为9磅15便士"
jieba.analyse.extract_tags(text, topK=3)
# ['15', '便士', '衬衫']
另一方面,基于TextRank的关键词抽取实现代码样例如下:
import jieba.analyse
text = "衬衫的价格为9磅15便士"
jieba.analyse.textrank(text, topK=3)
# ['衬衫', '价格']
pyltp库是哈工大出品的中文分词工具库。更确切的说,他事实上包含了nlp在语意层面的各种轻量级的功能模型实现,包括命名实体识别(NER)、词性分析(POS)、语义角色标注(SRL)以及依存句法分析(DP)。
有关pyltp库的具体内容可以参考其官方文档:
这里,我们就简单地就上述各个部分的调用方法进行一下简单的介绍。
给出pyltp库的分词模块的基本调用方法如下:
from pyltp import Segmentor
pyltp_segmentor = Segmentor()
pyltp_segmentor.load(cws_model_path) # 载入分词模型
pyltp_segmentor.segment("今天天气真好")
需要注意的是,pytlp分词的结果类型为pyltp.VectorOfString
,要想要显示其结果,我们同样需要将其通过类型转换变成一个list。
另一方面,pyltp同样可以输入额外的领域词表来优化垂直领域的分词效果。
其外部词表的添加方式如下:
from pyltp import Segmentor
segmentor = Segmentor() # 初始化实例
segmentor.load_with_lexicon(cws_model_path, '/path/to/your/lexicon')
但是,需要特别注意的是,pyltp的分词模型不同于jieba分词,他是一个轻量级的模型,因此,加入额外的词表可以改变其输出表达,却无法保证一定会输出额外词表中的专有名词。
因此,如果需要输出所有匹配到的领域词表中的词汇,建议使用jieba分词而不是pyltp分词。
pyltp的pos模块的使用方法同样简单,给出其官网上的基本代码实现如下:
import os
LTP_DATA_DIR = '/path/to/your/ltp_data' # ltp模型目录的路径
pos_model_path = os.path.join(LTP_DATA_DIR, 'pos.model') # 词性标注模型路径,模型名称为`pos.model`
from pyltp import Postagger
postagger = Postagger() # 初始化实例
postagger.load(pos_model_path) # 加载模型
words = ['元芳', '你', '怎么', '看'] # 分词结果
postags = postagger.postag(words) # 词性标注
print '\t'.join(postags)
postagger.release() # 释放模型
其对应的词性标签含义详见下述文档:
类似的,我们也可以快速给出pyltp模块的ner分词功能实现如下:
import os
LTP_DATA_DIR = '/path/to/your/ltp_data' # ltp模型目录的路径
ner_model_path = os.path.join(LTP_DATA_DIR, 'ner.model') # 命名实体识别模型路径,模型名称为`pos.model`
from pyltp import NamedEntityRecognizer
recognizer = NamedEntityRecognizer() # 初始化实例
recognizer.load(ner_model_path) # 加载模型
words = ['元芳', '你', '怎么', '看']
postags = ['nh', 'r', 'r', 'v']
netags = recognizer.recognize(words, postags) # 命名实体识别
print '\t'.join(netags)
recognizer.release() # 释放模型
可以看到,ner模型的输入包含分词内容和pos标签,但是,本质来说,还是挺简单的。
此外,pyltp的ner模块较为简单,仅仅识别人名、地名以及机构名三类实体。
依存句法分析的pyltp模块实现方法与ner模块相似,我们就直接给出其官方调用代码样例如下:
import os
LTP_DATA_DIR = '/path/to/your/ltp_data' # ltp模型目录的路径
par_model_path = os.path.join(LTP_DATA_DIR, 'parser.model') # 依存句法分析模型路径,模型名称为`parser.model`
from pyltp import Parser
parser = Parser() # 初始化实例
parser.load(par_model_path) # 加载模型
words = ['元芳', '你', '怎么', '看']
postags = ['nh', 'r', 'r', 'v']
arcs = parser.parse(words, postags) # 句法分析
print "\t".join("%d:%s" % (arc.head, arc.relation) for arc in arcs)
parser.release() # 释放模型
输出结果的读法可以参考博客:中文句法分析及LTP使用,这里就不再展开说明了。
最后,有关pyltp的srl模块实现,其输入除了分词信息以及pos信息之外,我们还需要输入依存句法分析的结果。
其官网上给出的调用代码样例如下:
import os
LTP_DATA_DIR = '/path/to/your/ltp_data' # ltp模型目录的路径
srl_model_path = os.path.join(LTP_DATA_DIR, 'srl') # 语义角色标注模型目录路径,模型目录为`srl`。注意该模型路径是一个目录,而不是一个文件。
from pyltp import SementicRoleLabeller
labeller = SementicRoleLabeller() # 初始化实例
labeller.load(srl_model_path) # 加载模型
words = ['元芳', '你', '怎么', '看']
postags = ['nh', 'r', 'r', 'v']
# arcs 使用依存句法分析的结果
roles = labeller.label(words, postags, arcs) # 语义角色标注
# 打印结果
for role in roles:
print role.index, "".join(
["%s:(%d,%d)" % (arg.name, arg.range.start, arg.range.end) for arg in role.arguments])
labeller.release() # 释放模型
同样的,srl结果的输出结果读法可以参考博客:中文句法分析及LTP使用,这里就不再展开说明了。
上述两者都是基于词的常用中文分词工具库。
如前所述,其优点在于可以包含词汇信息,优化模型效果,但是,它同样存在一些缺陷,除了分词的效果问题之外,它还有一个巨大的问题在于所需的词表巨大(可以达到百万量级),且会有大量非常用词的词频非常的低,因此在实际使用中往往会导致大量的OOV问题。
因此,更多情况下,我们需要结合字形式的分词方法,而其中最常用的两种分词方法就是sentencepiece以及byte pair encoding(bpe)方法。
这里,我们主要介绍一下sentencepiece分词方法。
与jieba还有pyltp分词模型不同的是,sentencepiece模块并没有提供内置的分词模型,因此,在使用sentencepiece分词之前,我们首先必须要使用一个大语料训练一个sentencepiece的分词模型。
下面,我们给出sentencepiece模块的分词模型训练代码样例如下:
import sentencepiece as spm
spm.SentencePieceTrainer.Train('--input={} --model_prefix={} --vocab_size=32000 --character_coverage=0.9995'.format(corpus, model_prefix))
其中,input
为训练语料文件,而model_prefix
为训练得到的模型路径及名称,例如model_path/model_name
,则运行后脚本会在model_path
路径下生成一个model_name.model
的分词模型文件以及一个model_name.vocab
的词表文件。
现在,我们来看sentencepiece分词模块的调用方法。
我们同样给出代码样例如下:
import sentencepiece as spm
segmentor = spm.SentencePieceProcessor()
segmentor.Load("spm_model.model") # 载入模型
text = "今天天气真好"
tokens = segmentor.EncodeAsPieces(text) # 分词
re_text = segmentor.DecodePieces(tokens) # 恢复原文本
sentencepiece分词不同于词方式的分词,本质上来说,他还是一种基于字符匹配方式的词频统计分词,但是,不同于纯字级别的分词,sentencepiece可以手动控制切分力度(词表大小)和词汇覆盖率。
通过调整这两者,我们可以兼顾字的上下文信息以及切割词表的覆盖率,因此,可以在几乎消除OOV的情况下尽可能地保留词汇信息,从而提升模型的效果。
但是,相对的,由于sentencepiece模型仅仅是一种基于词频统计的模型,因此,其切分结果往往会出现一些出现词频较高,但是事实上并非是一个合法词汇的结果,这种情况在设置词表较大时会尤为明显。
bert中文分词事实上算不上是一个真实的分词工具,但是由于他是bert中文模型的默认分词方法,因此,纵使其在分词效果的意义上性能并不好,但是它依然具有极其广泛的应用。
因此,这里,我们同样来介绍一下bert的分词方法。
bert的分词方法本质上来说就是一个基于字的分词(没办法,你不能指望老外能够对中文的分词有多少深刻的理解。。。),因此其本身没有什么值得多说的,我们下面仅就其调用方法进行说明。
首先我们去bert的开源github仓库下载bert的代码与模型。
下面,我们给出其调用方法如下:
from tokenization import FullTokenizer
bert_tokenizer = FullTokenizer(vocab_file="models/bert/chinese_L-12_H-768_A-12/vocab.txt", do_lower_case=True)
bert_tokenizer.tokenize("今天天气真好")
# ['今', '天', '天', '气', '真', '好']
最后,我们对各个分词工具进行性能比较。
我们以莎士比亚的《哈姆雷特》作为测试文本,其大小为216kB。
在不同的分词工具中,我们测试得到各自的耗时如下:
segmentor | cost time(ms) |
---|---|
jieba | 381 ms |
jieba_fast | 180 ms |
pyltp | 511 ms |
sentencepiece | 325 ms |
bert tokenizer | 532 ms |
其中,jieba_fast库是jieba库的一个优化版本,其分词效果和jieba库完全一致,但是速度上会有较大幅度的提升。
综上,我们可以看到: