写在前面:笔者最近在梳理自己的文本挖掘知识结构,借助gensim、sklearn、keras等库的文档做了些扩充,会陆陆续续介绍文本向量化、tfidf、主题模型、word2vec,既会涉及理论,也会有详细的代码和案例进行讲解,希望在梳理自身知识体系的同时也能对想学习文本挖掘的朋友有一点帮助,这是笔者写该系列的初衷。
特别推荐|【文本挖掘系列教程】:
文本挖掘从小白到精通(一)---语料、向量空间和模型的概念
文本挖掘从小白到精通(三)---主题模型和文本数据转换
文本挖掘从小白到精通(五)---主题模型的主题数确定和可视化
文本挖掘从小白到精通(六)---word2vec的训练、使用和可视化
文本挖掘从小白到精通(七)--- Word2vec的增量学习
文本挖掘从小白到精通(八)--- 从海量文章中挖掘主要观点
文本分类算法集锦,从小白到大牛,附代码注释和训练语料
---------------------分--------------隔-----------------线---------------------
说到文本相似度和文本距离,它们其实是亲兄弟,是一体两面的关系:
相似度侧重于文本之间的相似性,量化了文本之间相同特征的多寡,而距离更侧重文本之间的差异性,量化了文本之间不同特征的多寡
将相似度或者距离的度量指标压缩到[0,1]这个区间,则有这样的关系:相似度 = 1 -距离
通常,在文本挖掘的过程中,我们会经常涉及到以下3个文本相似度度量的场景:
两个文本之间,比如比较“我是北京人”和“我是帝都人”之间的语义相似度;
两个文本集合之间,比如两组词汇分布['国家','民族','政治制度','政党'],['少数民族','旅游','美食','客栈']的相似度,通常用在度量主题模型中各主题间的相似度上;
文本对象与文本集合,比如给定一条语句,在另一个语句库中找出若干条与其语义最为接近的语句来,笔者本教程系列(四)中的基于LSI的相似文档检索。
我们之前有提到过,文本数据是一种高维的语义空间,对语句进行抽象分解,在保留语义和句法关系的情况下,最大限度的利用数学模型去量化其相似性。
有了文本之间相似性的度量方法,我们便可以利用基于划分法的K-means、基于密度的DBSCAN或者是基于主题模型的概率方法对文本进行聚类分析;另一方面,我们也可以利用文本之间的相似性对大规模语料进行去重预处理,或者找寻某一实体名称的相关名称(模糊匹配)。
衡量两个文本的相似性有很多种方法,如最直接的利用Hashcode,以及经典的欧氏距离、余弦距离等。
本文会着重介绍第二种文本相似度场景下的距离度量指标 --- 在比较两个文本集合(主要是基于主题模型的概率分布)的相似程度时,我们需要一些跟欧氏距离、余弦距离不同的相似度/距离度量方案。
在本文中,笔者还将使用教程五的主题模型语料库作为示例,对LDA下的主题分布相似度进行比较。
1、载入主要的库和语料
from gensim.corpora import Dictionary
from gensim.models import ldamodel
from gensim.matutils import kullback_leibler, jaccard, hellinger, sparse2full
import numpy
载入教程五中的示例语料库,该语料库已经经过分词和去停用词处理,围绕着“苹果”一词展开:一个是关于“苹果公司”,另一个是关于“苹果这种水果”。
texts = [
['苹果','叶子','椭圆形','树上'],
['植物','叶子','绿色','落叶乔木'],
['水果','苹果','红彤彤','味道'],
['苹果','落叶乔木','树上','水果'],
['植物','营养','维生素'],
['营养','维生素','苹果','成分'],
['互联网','电脑','智能手机','高科技'],
['苹果','公司','互联网','品质'],
['乔布斯','苹果','硅谷'],
['电脑','智能手机','苹果','乔布斯'],
['苹果','电脑','品质','生意'],
['电脑','品质','乔布斯'],
['苹果','公司','生意','硅谷']]
2、训练主题模型
构建词汇映射表(词汇:词频)和语料库(词汇ID:词频):
dictionary = Dictionary(texts)
corpus = [dictionary.doc2bow(text) for text in texts]
训练主题模型:
numpy.random.seed(2019) # 设置随机种子以获得每次相同的结果。
model = ldamodel.LdaModel(corpus, id2word=dictionary, iterations=100, num_topics=2)
model.show_topics(num_words=10) #展示主题模型,显示每个主题下的TOP10主题词
[(0,
'0.170*"苹果" + 0.087*"电脑" + 0.064*"品质" + 0.063*"乔布斯" + 0.053*"智能手机" + 0.052*"水果" + 0.051*"互联网" + 0.049*"树上" + 0.049*"公司" + 0.042*"生意"'),
(1,
'0.093*"苹果" + 0.082*"植物" + 0.075*"维生素" + 0.074*"营养" + 0.060*"叶子" + 0.058*"落叶乔木" + 0.051*"成分" + 0.049*"绿色" + 0.048*"硅谷" + 0.045*"乔布斯"')]
看看该主题模型中总计多少个词汇:
model.num_terms
22
3、基于训练好的主题模型来比较文档之间的相似度
笔者在这里人为设定3个文档,基于上面训练的主题模型,用以比较它们之间的两两相似度。注意,它们已经过分词、去停用词处理,可直接用于后续的词袋表示和主题模型分布表示。
注意:这些都是距离指标(Distance Metrics),这意味着返回值介于0到1之间,其中接近0的值表示较小的“距离”,相文档之间的相似度会更高,反之亦然。
3.1 将示例文档向量化表示(词袋表示和主题分布表示)
doc1 = ['树上', '叶子', '植物']
doc2 = ['乔布斯', '智能手机', '互联网']
doc3 = [ '营养', '苹果','维生素']
现将上述3个文档转化为词袋表示:
bow1 = model.id2word.doc2bow(doc1)
bow2 = model.id2word.doc2bow(doc2)
bow3 = model.id2word.doc2bow(doc3)
现在可以获得这些(词袋表示)语句的LDA主题分布:
lda_bow1 = model[bow1]
lda_bow2 = model[bow2]
lda_bow3 = model[bow3]
让我们先从流行的Hellinger距离开始吧。
Hellinger Distance 又称 Bhattacharyya Distance,因为作者的姓氏叫 Anil Kumar Bhattacharya。在概率和统计学中,Hellinger Distance 被用来衡量两个概率分布之间的相似性,属于 f-divergence 的一种。而 f-divergence 又是什么呢?一个 f-divergence 是一个函数 Df(P||Q) 用来衡量两个概率分布P and Q 之间的不同。具体数学原理太过复杂,在这里笔者就不多做阐述了,感兴趣的小伙伴可以去Google下。
对于两个概率分布,Hellinger距离度量给出了[0,1]范围内的输出,其值接近0意味着它们更相似。
hellinger(lda_bow1, lda_bow3)
0.04644444423361319
hellinger(lda_bow1, lda_bow2)
0.4615086926015422
hellinger(lda_bow2, lda_bow3)
0.5051615386187103
从上面的结果可以得知,文档1(['树上', '叶子', '植物'])和文档3([ '营养', '苹果','维生素'])之间的相似度最高,hellinger度量的返回值约为0.05,因为它们同属一个主题(“苹果是水果”这一主题)的缘故,从小试牛刀的结果来看,hellinger度量主题相似性的效果还行。
再来看看Kullback-Leibler。
Kullback-Leibler散度(Kullback-Leibler divergence),也叫相对熵(relative entropy),或信息散度(information divergence),是两个概率分布(probability distribution)间差异的非对称性度量 。在信息理论中,相对熵等价于两个概率分布的信息熵(Shannon entropy)的差值。
下面让我们再尝试使用Kullback Leibler来度量上面的语句相似度,它的距离度量值也在[0,1]之间,其值接近0意味着它们更相似。
kullback_leibler(lda_bow1, lda_bow3)
0.008893982
kullback_leibler(lda_bow2, lda_bow3)
1.0357668
从结果可知,Kullback Leibler也能较好的度量相似语句之间的距离。
*********************************注意!******************************************
Kullback-Leibler在数学意义上不是距离度量,它不是对称的。这意味着kullback_leibler(lda_bow2,lda_bow3)
不等于kullback_leibler(lda_bow3,lda_bow2)
。
现在让我检验一下:
# 如您所见,两者的值不相等。我们稍后会介绍其中的细节
kullback_leibler(lda_bow3, lda_bow2)
1.1175618
是吧,互换位置的结果分别是1.0357668和1.1175618,差异还是蛮明显的,待会笔者会谈到这是为何。
前面由LDA拟合出的2个主题:
0 代表主题 '0.170"苹果" + 0.087"电脑" + 0.064"品质" + 0.063"乔布斯" + 0.053*"智能手机"
1 代表主题 '0.093"苹果" + 0.082"植物" + 0.075"维生素" + 0.074"营养" + 0.060*"叶子"
在我们之前的例子中,我们看到文档1和文档3的之间的距离值比文档2和文档3之间的距离值要低,但它们之间的差异并不是很明显。这是为什么呢?
这是由于文档3中的“苹果”在语境(上下文为'营养'和 '维生素')里更接近主题1,而文档2更接近于主题0,文档1接近于主题1。
由此可知,文档1和文档3的从属主题一致,在语义上更为接近,所以它们之间的相似度较高,hellinger和kullback_leibler度量值较小。
接下来,我们验证一下这3个测试文档的主题分布,看是否跟笔者的判断一致:
print(model.get_document_topics(bow1))print(model.get_document_topics(bow2))print(model.get_document_topics(bow3))
[(0, 0.24727859), (1, 0.7527214)]
[(0, 0.8584863), (1, 0.14151369)]
[(0, 0.19274446), (1, 0.80725557)]
结果显示,笔者的判断是正确的!
关于上述提及的两种距离度量(Distance Metrics)/相似性度量(Similarity metrics)方法,你可以在在维基百科上找到很好的资料,【Kullback-Leibler_divergence】https://en.wikipedia.org/wiki/Kullback-Leibler_divergence)和【Hellinger】(https://en.wikipedia.org/wiki/Hellinger_distance) 。
Jaccard一般是指Jaccard相似系数(Jaccard similarity coefficient),其大致原理是:两个集合A和B的交集元素在A、B的并集中所占的比例,称为两个集合的杰卡德相似系数,用符号J(A,B)表示。它用于比较有限样本集之间的相似性与差异性,Jaccard系数值越大,样本相似度越低。
它的典型应用场景有:
比较文本相似度,用于文本查重与去重
计算对象间距离,用于数据聚类等
现在让我们看一下Jaccard Distance
这个度量标准,以获得词袋表示文档间的相似性。
jaccard(bow1, bow2)
1.0
jaccard(doc1, doc2)
1.0
jaccard(['苹果','大树','营养'], ['苹果','大树','营养'])
0.0
上面的3个示例涉及2种不同的输入方法。
在第一种情况下,笔者用Jaccard来度量文档词袋表示的距离。此时,文档之间的相似度可以被定义为1减去两个文档向量交集的大小,即1 - jaccard(bow1, bow2)。
我们可以看到(在人工检查中),距离值可能很高 --- 而且确实如此。
第二个例子说明,Jaccard也能直接接受文档词汇列表(甚至是未经处理的文档)作为输入,产生的距离值可能很高 。
在最后一种情况下,因为它们是相同的向量,返回的值是0 - 这意味着这两个文档间的距离为0,并且非常相似。
虽然已有标准方法来识别文档的相似性,但我们的距离度量还有一个非常有趣的应用场景--- 主题分布(Topic Distributions)。
前面已由LDA拟合出了2个主题:
0 代表主题 '0.170"苹果" + 0.087"电脑" + 0.064"品质" + 0.063"乔布斯" + 0.053*"智能手机"
1 代表主题 '0.093"苹果" + 0.082"植物" + 0.075"维生素" + 0.074"营养" + 0.060*"叶子"
这里,为了简便起见,且根据主题下的主题词分布情况,我们将主题0命名为“公司”,主题1命名为“水果”。
假若,我们想知道这两个主题在语句距离上是多么的相近,那么该如何做呢?
请仔细往下看。
4.1 度量两个主题间的相似度
编写抽取主题词汇分布的函数:
topic_公司, topic_水果 = model.show_topics()
#一些预处理,以使距离度量以可接受的数据形式来获得主题
def make_topics_bow(topic):
# 获取由model.show_topics()返回的字符串,分割字符串以便分别获取主题和概率
topic = topic.split('+')#用于存储主题bows的list
topic_bow = []
for word in topic:#分隔概率和词汇
prob, word = word.split('*')#去掉空格
word = word.replace(" ","")
word = word.replace('"','')#词汇表示转换
#print(word)
word = model.id2word.doc2bow([word])[0][0]
topic_bow.append((word, float(prob)))
return topic_bow
抽取“公司”、“水果”这两个主题的词汇分布,并展示“水果”这一主题的词汇分布情况:
公司_distribution = make_topics_bow(topic_公司[1])
水果_distribution = make_topics_bow(topic_水果[1])
# 以主题词汇加权表示的“水果”主题
水果_distribution
[(3, 0.093),
(4, 0.082),
(10, 0.075),
(11, 0.074),
(0, 0.06),
(6, 0.058),
(12, 0.051),
(5, 0.049),
(20, 0.048),
(19, 0.045)]
经过上述函数的转换,hellinger可以接受这种输入格式,让我们使用距离度量来查看上述两个主题中的词汇分布的相似程度如何。
hellinger(公司_distribution, 水果_distribution)
0.6917495641343796
返回值约为0.69,这意味着这2个主题在词汇分布方面存在一定差距,深层次的原因是 ---这两个主题所反映的语义存在差异。
在之前的例子中,我们没有使用Kullback Leibler来测试相似性,原因是KL在技术意义上不是距离度量指标。它在数学上的性质也意味着我们在使用它的时候必须谨慎 --- 因为它涉及到对数函数(Log Function),出现0的话,可能会使这个函数失效。例如
kullback_leibler(公司_distribution,水果_distribution)
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
<ipython-input-19-995e1961d972> in <module>()
----> 1 kullback_leibler(公司_distribution,水果_distribution)
D:\anaconda20190415\lib\site-packages\gensim\matutils.py in kullback_leibler(vec1, vec2, num_features)
943
944 """
--> 945 vec1, vec2 = _convert_vec(vec1, vec2, num_features=num_features)
946 return entropy(vec1, vec2)
947
D:\anaconda20190415\lib\site-packages\gensim\matutils.py in _convert_vec(vec1, vec2, num_features)
911 else:
912 max_len = max(len(vec1), len(vec2))
--> 913 dense1 = sparse2full(vec1, max_len)
914 dense2 = sparse2full(vec2, max_len)
915 return dense1, dense2
D:\anaconda20190415\lib\site-packages\gensim\matutils.py in sparse2full(doc, length)
398 doc = dict(doc)
399 # overwrite some of the zeroes with explicit values
--> 400 result[list(doc)] = list(itervalues(doc))
401 return result
402
IndexError: index 15 is out of bounds for axis 0 with size 10
看来kullback_leibler的使用不太方便。。。
这只意味着我们必须对输入有点小心。我们的旧例子没有成功,因为这两个主题中的某些词汇存在缺失(因为show_topics()方法默认
只返回了前10个主题)。
但这有办法解决,即返回主题模型的词汇表中用于描述“主题 - 词汇”分布的所有词汇。
topic1, topic2 = model.show_topics(num_words=len(model.id2word))
#再次进行词袋表示转换
公司_distribution = make_topics_bow(topic1[1])
水果_distribution = make_topics_bow(topic2[1])
# 返回kullback_leibler值
kullback_leibler(水果_distribution, 公司_distribution,22)
0.28363195
你可能会注意到它们之间的距离比较小,表明相似性很高。这可能会和实际情况有所偏离,因为语料库实在太小了,其中所蕴含的若干主题的主题词会存在大量重叠的情况,对于我们去解读主题的含义会有干扰。对于更大的语料库,你可能会获得更合理的结果。
所以,请记住,如果您打算使用KL作为度量来测量两个分布之间的相似性或距离,请通过返回整个分布(Entire Distribution)来避免零值的出现。因为任何特征/单词都不可能有绝对值为0的概率分布,所以像上面的情况一样返回所有值将获得更为合理的结果。
5、距离度量的4大“铁律”
看过这些距离/相似度测量的实际用法之后,我们可以从中归纳出它们的共性特征。让我们先了解一下距离度量和度量的确切含义。
笔者在上一节中提到,KL不是距离度量,因为它不是对称的。
那么,问题来了,到底什么样的度量可以算作是距离度量呢?
答案是,距离测量必须符合4个“硬性条件”,只要某种度量方法不满足其中之一,就说明它不是真正意义上的距离度量指标:
d(x,y) >= 0,非负性
d(x,y) = 0 <=> x = y,相等性
d(x,y) = d(y,x) ,对称性
d(x,z) <= d(x,y) + d(y,z),三角不等式
那就是:它必须是非负的; 如果x和y相同,则距离必须为零; 它必须是对称的; 它必须服从三角不等式定律。
很简单吧?让我们测试一下这些度量方法。
# 常规的Hellinger
hellinger(水果_distribution, 公司_distribution)
0.25049110183049994
我们交换一下这两个主题的位置,结果得到了相同的数值,即它确实是对称的!
hellinger(公司_distribution, 水果_distribution)
0.25049110183049994
如果我们传入相同的值,结果就为零:
hellinger(水果_distribution, 水果_distribution)
0.0
对于三角不等式,让我们使用LDA文档分布:
hellinger(lda_bow1, lda_bow2)
0.4615086926015422
三角不等式也符合:
hellinger(lda_bow1, lda_bow2) + hellinger(lda_bow1, lda_bow3)
0.5079531368351554
所以Hellinger确实是一个距离度量方法。
让我们来看看KL(kullback_leibler)吧。
kullback_leibler(水果_distribution, 公司_distribution,22)
0.28363195
kullback_leibler(公司_distribution, 水果_distribution,22)
0.23145032
我们立即注意到,当我们交换参数的位置时,它们返回的数值并不相等!四个硬性条件只要不满足其中之一,就足以判定它不是一个距离度量方法。
然而,仅仅因为它不是一个度量,(严格来说,在数学意义上)并不意味着它对于度量两个概率分布之间的距离没有用武之地。KL Divergence被广泛用于两个概率分布的差异度量,并且可能是信息理论(Information Theory)等领域中最知名的距离测量。
为了更好地回顾Hellinger和KL之间的数学差异,可参看http://stats.stackexchange.com/questions/130432/differences-between-bhattacharyya-distance-and-kl-divergence 。
在这一篇文章中,我们弄明白了什么距离度量指标的几个“硬性条件”,KL Divergence在严格意义上并不是距离度量指标,但它可以用于度量主题分布的差异度。基于今天所谈到的距离指标,有余力的小伙伴可以尝试用在教程八中的文本聚类上,试试不同于欧式距离和余弦距离的相似度量方法,以求得更好的聚类结果。
同时,也欢迎大家在下方评论处踊跃留言,大家的支持是我不断更新的动力!
推荐阅读
征稿启示| 200元稿费+5000DBC(价值20个小时GPU算力)
完结撒花!李宏毅老师深度学习与人类语言处理课程视频及课件(附下载)
模型压缩实践系列之——bert-of-theseus,一个非常亲民的bert压缩方法
文本自动摘要任务的“不完全”心得总结番外篇——submodular函数优化
斯坦福大学NLP组Python深度学习自然语言处理工具Stanza试用
关于AINLP
AINLP 是一个有趣有AI的自然语言处理社区,专注于 AI、NLP、机器学习、深度学习、推荐算法等相关技术的分享,主题包括文本摘要、智能问答、聊天机器人、机器翻译、自动生成、知识图谱、预训练模型、推荐系统、计算广告、招聘信息、求职经验分享等,欢迎关注!加技术交流群请添加AINLPer(id:ainlper),备注工作/研究方向+加群目的。
阅读至此了,分享、点赞、在看三选一吧🙏