摘要:机器学习处理文字、语音、图片、视频等任务,很重要的一点就是从原始信息中提取出机器可以理解的特征。一篇文章通常由大量的词语组成,在转换为向量的过程中,首先便会遇到抽取词语的问题。对抽取出的词语,进行向量后,然后便可以计算向量之间的相似性了。
人类的语言,经过了几千年积累,已经形成了完整体系。对人而言,识别其中的意思是比较容易的。可电脑却不同,要想识别其中的字词是很困难的。
自然语言处理中,最简单的是判断两个文本的相似性。简单说,就是两段话或者两篇文档,判断他们是否表达相同的意思。又或者,发表了一篇论文,论文审核的人会去论文库里面搜索是否涉及抄袭。再比如,把10篇文档按内容描述的大意分成3个类别,即判断文档之间的相似性,把相似性高的聚在一起,这是简单的文档聚类。
机器处理文本,最重要的是提取文本的特征。扩展开来,机器学习的很多任务都需要提取特征,提取出来的特征好坏,很大程序上决定了任务结果的好坏。机器学习处理文字、语音、图片、视频等任务,很重要的就是从原始信息中提取出机器可以理解的特征,这也是基于自动特征提取的深度学习算法能火起来的主要原因。
机器只能处理数值类型的数据,首先遇到的一个问题,就是将文字的描述转换成数值类型,即后面要用到的向量。只有转换为向量后,才能通过模型来进行计算。一篇文章通常由大量的词语组成,在转换为向量的过程中,首先会遇到词语的抽取问题。对抽取出的词语,进行向量后,然后计算向量之间的相似性。
02 中文分词
中文最小的基本单位为字,词由字组成,词与词之间没有分隔符。不同于英文,英文每个单词之间有空格进行分隔,因此中文处理的很多地方都会用到分词。
比如,“佛陀是彻底的觉悟者”这句话,人很容易就进行了分词,佛陀/是/彻底/的/觉悟者/,但程序做不到。因为人的大脑在阅读了大量的书箱后,已经在潜移默化中积累了很多的词语,比如“佛陀”,“彻底”成词,“的”单独成词。
要想让程序识别其中的词语“佛陀”和“彻底”,最开始大家找了很多语言学家,企图让计算机能像人一样理解其中的意思,然后再来进行分词。但经过大量的尝试后,却发现效果并不理想。于是基于统计学的方式开始流行。想法就是:只简单的喂给计算机大量的文本资料,按一定的算法,让其进行统计,从中发现出哪些是可能组成词语,哪些是单字。而整个过程中,计算机并不需要理解其中的意思。
通常,如果不做深入的自然语言处理(NLP),可以不用太关心分词使用的具体的算法。直接使用现有的库即可。Python最有名的中文分词库,应该算是Jieba(结巴)了,这个名字很形象,结巴说话是一个词一个词的说,中间有停顿,停顿的地方便是单词的分隔。
结巴支持几个模式,精确模式、全模式、搜索引擎模式,各个模式有不同的适合场景。还支持自定义词库,比如:“彻底觉悟的人便是觉者”这句话,正确的分词为:彻底/觉悟/的/人/便是/觉者,其中的“觉者”,就是佛。假设你喂给程序的文档里面不包含这个词,Jieba分词也能通过新词识别算法识别出来。
假设算法也没有识别出来,那么可能会把“觉者”这个词分成“觉”和“者”,这是不合情理的。这种情况下,可以用Jieba的自定义词库功能,将“觉者”写入文本文件,在调用结巴之前加载这个自定义词典即可。其它的一些网络新词汇,如“然并卵”,或者领域专用词汇,或者人名等都可以进行自定义。
03 词袋向量化
一段文本,究竟用一个什么样的向量来表示,才能完整的表达其中的含义,这是自然语言处理的一大核心问题。比较简单的有词袋模型和主题模型。计算文本相似性的,可以使用最简单的词袋模型。
假定一篇文档中包含的信息,可以只由其中包含的词语来描述,并且与词语在文档中的位置没有关系,这便是词袋模型,英文为bag of words,意为单词的袋子。例如,一篇文档包含大量佛陀,菩萨等词,和一篇包含大量的学校,班级的文档,只由他们包含的词语便可以知道,他们描述了两个不同的主题,因此相似性很低。
抽取文档中出现的所有词汇,放入一个袋子里面,再对袋子里的词进行一些处理,便可以完成向量化,也即使用词袋模型进行向量化。对袋子进行处理的方法中,最简单的便是统计袋子里面各个词在各文档中出现的频度数,下一节的CountVectorizer便专门做这个事情。
与对每个词进行单纯计数不一样的,还有一个方法,TF-IDF,词频和逆文档词频,这个主要用于设置文档中一些词语的权重。其原理是:文档之间的区别,通常是由在两个文档中都出现得少的词来区别,因此这些词语权重增加,那些公共出现的词的权重降低,从而达到理有效区分文档的目的。
向量化需要注意的是,要保证在两个文档在相同的向量空间里面,也即使用的词袋相同。 训练数据与测试数据,必须在同一向量空间进行向量化,以保证两个向量的维度一样。这样对于后续的相似性比较,才有意义。
04 词频向量化
使用scikit-learn中的CountVectorizer来进行说明,这个方法把词袋模型中的概念基本都介绍清楚。CountVectorizer位于sklearn.feature_extraction.text中,从包名中也可以看出,这个方法用于提取文本的特征。
其中的一个参数,analyzer:使用字符还是单词对文本进行切分。在中文状态下,假定已经预先使用结巴分词对文本进行了分词,词之间用空格分开。那么使用"word"的切分方式。"char"的方式即对单字进行切分,在某些情况下会用到。假设下面句子,则可以使用char的方式:(这5个字,是写在茶壶外面一圈的5个字,从任何一个字开始的5种读法,都是可以读通,从中体会中文表达意思的强大):
可以清心也
以清心也可
清心也可以
心也可以清
也可以清心
如果你需要处理2元词,3元词,即认为词与词的顺序是有一定关系的,每个词的出现会与前面1个或者2个词有关系,那么就可以使用n-gram(n元词),常用的有bi-gram(2元词), tri-gram(3元词)。
依然以上面的5句话为例子,使用每个字为一个词(char的方式 ),且使用2元分割,则第一句话的分割为: 可以-以清-清心-心也,第二句话的分割为:以清-清心-心也-也可,其它类似。参数ngram_range即用来指定最小的元数和最大的元数。
回到CountVectorizer这个方法上来,Count即为计数的意思,假定要向量化上面“可以清心也”的前两句,使用char的分割,ngram_range使用(2,2),即只使用2元组合,则词袋为两个句子中的全部词语。词袋为:“可以,以清,清心,心也,也可”,共6个词。对照这个词袋,第一句的向量为:1,1, 1,1,1,0,第二名为:0,1,1,1,1,1。这里全为1和0,是因为我们句子很短,词都只出现1次或者不出现,实际应用中可以大于1,这便是Count的意义。当然,如果只关心词语是否出现,而不关心词出现的次数,可以加一个参数: binary=True,这个参数在一些实际问题上比较有用。
上面用了分词和分割两个描述,分词是专门针对中文的,而分割是针对CountVectorizer这个方法的。处理中文时,将中文进行分词后,使用空格进行分隔,上面方法可以直接处理。如果词中有自定义的词,而自定义的词中有特殊特号,默认的token_pattern可能不能满足,此时需要自定义这个正则表达式。token_pattern的默认的正则为:(?u)\b\w\w+\b, 要求单词最少两个字符,以单词的分界进行判断。
在Scikit-learn的源码中,是这样的两条语句:
token_pattern =re.compile(self.token_pattern)
return lambda doc: token_pattern.findall(doc)
如果修改了正则表达式,可以使用re.findall(string, pattern)来测试在分词的基础上的分割,看是否满足需求。
05 向量相似性
将文字转换为向量后,计算向量的相似性相对而言就比较简单了。根据具体的问题,选择一种合适的相似性度量即可。在选择相似性的时候,也可以尝试多种,然后选择一种最合适的。
相似性度量(similarity)的方式有很多种,最常用的当然是空间中的距离度量。剩下的还有常见的余弦相似性,街区距离,杰卡德相似系数等等。Scikit-learn中两个主要地方描述了相似性度量,一个是近邻方法中的sklearn.neighbors.DistanceMetric,另一个是度量相关的sklearn.metrics.pairwise.pairwise_distances。
在选择相似性度量的时候,需要参考向量的类型,在scikit-learn中,按向量的数据类型,区分三种类型:实数型、整数型、真假二值型。比如杰卡德相似系数,就只适用于真假二值型的数据。
另外,如果要自己实现相似性的方法,通常而言,需要满足以下四点:
- 非负性:相似性不可以为负数;
- 相等为零:当且仅当两个向量相等时,相似性为0;
- 对称性:A与B的相似性等于B与A的相似性;
- 三角不等式:d(x, y) + d(y, z) >= d(x, z),类似于三角形两边之和大于(等于)第三边;
在一般性的分类、聚类中,计算相似性度量之前,还需要考虑数据的量纲,尽量在相同的值域内。比如一个特征的取值范围为1000到2000,另外一个特征的取值范围为5到10,那么在计算相似性距离的时候,第一个特征会明显作为主导,第二个特征起到的作用就非常小。此时对数据进行归一化处理,将两个特征的范围都缩放到0到1或者-1到1,再进行相似性计算,就很有必要了。