写在前面:笔者最近在梳理自己的文本挖掘知识结构,借助gensim、sklearn、keras等库的文档做了些扩充,会陆陆续续介绍文本向量化、tfidf、主题模型、word2vec,既会涉及理论,也会有详细的代码和案例进行讲解,希望在梳理自身知识体系的同时也能对想学习文本挖掘的朋友有一点帮助,这是笔者写该系列的初衷。
特别推荐|【文本挖掘系列教程】:
文本挖掘从小白到精通(一)---语料、向量空间和模型的概念
文本挖掘从小白到精通(三)---主题模型和文本数据转换
文本挖掘从小白到精通(五)---主题模型的主题数确定和可视化
文本挖掘从小白到精通(六)---word2vec的训练、使用和可视化
文本挖掘从小白到精通(七)--- Word2vec的增量学习
文本挖掘从小白到精通(八)--- 从海量文章中挖掘主要观点
文本挖掘从小白到精通(九)--- 文本相似性度量
【特辑】文本分类算法集锦,从小白到大牛,附代码注释和训练语料
很抱歉,最近没怎么写文章了,因为最近事情较多...
最近有不少小伙伴在问折耳喵关于文本聚类的话题,比如在《文本挖掘从小白到精通(八)--- 从海量文章中挖掘主要观点》中提到的文本聚类,用的是K-means聚类算法,需要事先设定聚类数,但很多时候我们不知道到底能聚出来多少主题,这个时候该咋办呢?能不能有一种聚类算法不需要预设聚类数,简单、高效、快捷?
别说。
还真有~
---------------------分--------------隔-----------------线---------------------
Single-pass clustering,中文名一般译作“单遍聚类”,它是一种简洁且高效的文本聚类算法。在文本主题聚类中,Single-pass聚类算法比K-means来的更为有效。Single-pass聚类算法不需要指定类目数量,可以通过设定相似度阈值来限定聚类数量。
Single-pass聚类算法同时是一种增量聚类算法(Incremental Clustering Algorithm),每个文档只需要流过算法一次,所以被称为single-pass,效率远高于K-means或KNN等算法。它可以很好的应用于话题监测与追踪、在线事件监测等社交媒体大数据领域,特别适合流式数据(Streaming Data),比如微博的帖子信息,因此适合对实时性要求较高的文本聚类场景。
接下来再说说它的处理步骤。
Single-pass算法顺序处理文本,以第一篇文档为种子,建立一个新主题。之后再进行新进入文档与已有主题的相似度,将该文档加入到与它相似度最大的且大于一定阈值的主题中。如果与所有已有话题相似度都小于阈值,则以该文档为聚类种子,建立新的主题类别。其算法流程如下:
以第一篇文档为种子,建立一个主题;
基于词袋模型将文档X向量化;
将文档X与已有的所有话题均做相似度计算,可采用欧氏距离、余弦距离,或者上篇文章中提到的kullback_leibler, jaccard, hellinger;
找出与文档X具有最大相似度的已有主题;
若相似度值大于阈值theta,则把文档X加入到有最大相似度的主题中,跳转至 7;
若相似度值小于阈值theta, 则文档X不属于任一已有主题, 需创建新的主题类别,同时将当前文本归属到新创建的主题类别中;
聚类结束,等待下一篇文档进入
...
废话不多说,下面是code,折耳喵在必要的部分加入了注释,这次算是写的比较规整的代码了,以类的形式呈献给大家,嘿嘿~
1import numpy as np
2import math
3import jieba
4import json
5from gensim import corpora, models, similarities, matutils
6from smart_open import smart_open
7import pandas as pd
8from pyltp import SentenceSplitter #按标点切分语句
9from textrank4zh import TextRank4Keyword,TextRank4Sentence #关键词和关键句提取
10from tkinter import _flatten #用于将嵌套列表压成一层
11
12class Single_Pass_Cluster(object):
13 def __init__(self,
14 filename,
15 stop_words_file= '停用词汇总.txt',
16 theta = 0.5):
17
18 self.filename = filename
19 self.stop_words_file = stop_words_file
20 self.theta = theta
21
22 def loadData(self,filename):
23 '''以列表的形式读取文档'''
24 Data = []
25 i = 0
26 with smart_open(self.filename,encoding='utf-8') as f:
27 #鉴于有些文档较长,包含多个语义中心,因此按语句结束标点进行切割获取表意单一的句子产生的聚类效果会更好
28 texts = [list(SentenceSplitter.split(i.strip().strip('\ufeff'))) for i in f.readlines()]
29 print('未切割前的语句总数有{}条...'.format(len(texts)))
30 print ("............................................................................................")
31 texts = [i.strip() for i in list(_flatten(texts)) if len(i)>5]
32 print('切割后的语句总数有{}条...'.format(len(texts)))
33 for line in texts:
34 i += 1
35 Data.append(line )
36 return Data
37
38 def word_segment(self,texts):
39 '''对语句进行分词,并去掉常见无意义的高频词(停用词)'''
40 stopwords = [line.strip() for line in open( self.stop_words_file,encoding='utf-8').readlines()]
41 word_segmentation = []
42 words = jieba.cut(texts)
43 for word in words:
44 if word == ' ':
45 continue
46 if word not in stopwords:
47 word_segmentation.append(word)
48 return word_segmentation
49
50 def get_Tfidf_vector_representation(self,word_segmentation,pivot= 10, slope = 0.1):
51 '''采用VSM(vector space model)得到文档的空间向量表示,也可以doc2vec等算法直接获取句向量'''
52
53 dictionary = corpora.Dictionary(word_segmentation) #获取分词后词汇和词汇id的映射关系,形成字典
54 corpus = [dictionary.doc2bow(text) for text in word_segmentation] #得到语句的向量表示
55 tfidf = models.TfidfModel(corpus,pivot=pivot, slope =slope) #进一步获取语句的TF-IDF向量表示
56 corpus_tfidf = tfidf[corpus]
57 return corpus_tfidf
58
59 def getMaxSimilarity(self,dictTopic, vector):
60 '''计算新进入文档和已有文档的文本相似度,这里的相似度采用的是cosine余弦相似度,根据上一篇文章的提示,大家还可以试试
61 kullback_leibler, jaccard, hellinger等相似度计算方法'''
62
63 maxValue = 0
64 maxIndex = -1
65 for k,cluster in dictTopic.items():
66 oneSimilarity = np.mean([matutils.cossim(vector, v) for v in cluster])
67 if oneSimilarity > maxValue:
68 maxValue = oneSimilarity
69 maxIndex = k
70 return maxIndex, maxValue
71
72 def single_pass(self,corpus,texts,theta):
73 dictTopic = {}
74 clusterTopic = {}
75 numTopic = 0
76 cnt = 0
77 for vector,text in zip(corpus,texts):
78 if numTopic == 0:
79 dictTopic[numTopic] = []
80 dictTopic[numTopic].append(vector)
81 clusterTopic[numTopic] = []
82 clusterTopic[numTopic].append(text)
83 numTopic += 1
84 else:
85 maxIndex, maxValue = self.getMaxSimilarity(dictTopic, vector)
86 # 以第一篇文档为种子,建立一个主题,将给定语句分配到现有的、最相似的主题中
87 if maxValue > theta:
88 dictTopic[maxIndex].append(vector)
89 clusterTopic[maxIndex].append(text)
90
91 # 或者创建一个新的主题
92 else:
93 dictTopic[numTopic] = []
94 dictTopic[numTopic].append(vector)
95 clusterTopic[numTopic] = []
96 clusterTopic[numTopic].append(text)
97 numTopic += 1
98 cnt += 1
99 if cnt % 1000 == 0:
100 print ("processing {}...".format(cnt))
101 return dictTopic, clusterTopic
102
103 def fit_transform(self,theta=0.5):
104
105 '''综合上述的函数,得出最终的聚类结果:包括聚类的标号、每个聚类的数量、关键主题词和关键语句'''
106 datMat = self.loadData(self.filename)
107 word_segmentation = []
108 for i in range(len(datMat)):
109 word_segmentation.append(self.word_segment(datMat[i]))
110 print ("............................................................................................")
111 print('文本已经分词完毕 !')
112
113 #得到文本数据的空间向量表示
114 corpus_tfidf = self.get_Tfidf_vector_representation(word_segmentation)
115 dictTopic, clusterTopic = self.single_pass(corpus_tfidf, datMat, theta)
116 print ("............................................................................................")
117 print( "得到的主题数量有: {} 个 ...".format(len(dictTopic)))
118 print ("............................................................................................\n")
119 #按聚类语句数量对聚类结果进行降序排列,找到重要的聚类群
120 clusterTopic_list = sorted(clusterTopic.items(),key=lambda x: len(x[1]),reverse=True)
121 for k in clusterTopic_list:
122 cluster_title = '\n'.join(k[1])
123 # 得到每个聚类中的主题关键词
124 word = TextRank4Keyword()
125 word.analyze(''.join(self.word_segment(''.join(cluster_title))),window = 5,lower = True)
126 w_list = word.get_keywords(num = 10,word_min_len = 2)
127 # 得到每个聚类中的关键主题句TOP3
128 sentence = TextRank4Sentence()
129 sentence.analyze(' '.join(k[1]) ,lower = True)
130 s_list = sentence.get_key_sentences(num = 3,sentence_min_len = 3)
131 print ("【主题索引】:{} \n【主题语量】:{} \n【主题关键词】:{} \n【主题中心句】 :\n{}".format(k[0],len(k[1]),','.join([i.word for i in w_list]),'\n'.join([i.sentence for i in s_list])))
132 print ("-------------------------------------------------------------------------")
写完这个类,接下来用实际数据测试一下效果,数据集来自http://docs.bosonnlp.com/_downloads/text_comments.txt
1single_pass_cluster = Single_Pass_Cluster('car_text_comments.txt',stop_words_file= '停用词汇总.txt')
2single_pass_cluster.fit_transform(theta = 0.25)
以下是运行结果,篇幅有限,仅展示部分:
未切割前的语句总数有500条...
............................................................................................
切割后的语句总数有875条...
............................................................................................
文本已经分词完毕 !
............................................................................................
得到的主题数量有: 583 个 ...
............................................................................................
【主题索引】:32
【主题语量】:10
【主题关键词】:满意,小福,私家车,打分,朋友,参考,空间,异味,消除,寒碜
【主题中心句】 :
要说“最”不满意没有
最不满意的就是空间,之前没觉得
其实我没有任何不满意的地方,非要说一个的话那就是原车的灯不太给力,不过我已经直接给换了,嘿嘿
-------------------------------------------------------------------------
【主题索引】:130
【主题语量】:8
【主题关键词】:缝隙,小福,做工,接近,车辆,细节,不是太好,发动机盖,进灰,得分
【主题中心句】 :
缝隙结合不缝隙太大,比较容易进灰,细节不是太好
远看还不觉得,近看真心太大缝隙了
:车身缝隙大,特别是尾箱盖缝隙太大了
-------------------------------------------------------------------------
【主题索引】:0
【主题语量】:7
【主题关键词】:空间,拥挤,马三宽,仪表,腿部,妹妹,过长,后排,长时间,乘车
【主题中心句】 :
最不满意的地方就是空间了,尤其是后排空间,确实不够用,座三个人就觉得有点太拥挤了,腿部空间不足,人长时间乘车会感到不舒服
和大多数人一样,感觉空间是一个短肋,毕竟十来万的车了,和朋友在一起,都说空间小,高点的后面都感觉挤,都不知道该怎么和他解释空间小的原因
空间啦,和同级的车比起来空间的确有点紧张,不过横向空间还是宽的,至少比我妹妹的马三宽多了,仪表台过长确实有点占空间,不过做工新颖
-------------------------------------------------------------------------
【主题索引】:15
【主题语量】:7
【主题关键词】:味道,新车,发动机盖,驾驶,大院,天天,包买,甲醛,右腿,劲儿
【主题中心句】 :
目前没发现什么令我太不满意的,新车味道有点大,开发动机盖开启不方便,驾驶时,右腿有点别劲儿(个人感觉)
新车味道大了些,放了碳包买了去甲醛的3M,天天在公司大院开着窗,希望味道早些散掉,我们家小宝都不敢让他座哦
新车味道太大
-------------------------------------------------------------------------
【主题索引】:28
【主题语量】:6
【主题关键词】:工艺,装配,长安,水平,福特,质量,无力,控制,差不多,拖拉机
【主题中心句】 :
装配工艺实在是太差了,和拖拉机的水平差不多,长安福特质量控制水平极低,很好的车型都被糟蹋了
最不满意的就是长安福特的装配工艺和橡胶的质量问题
长安坑爹的装配工艺,无力吐槽,但是用料比起其他真的很厚道
-------------------------------------------------------------------------
...
怎么样?效果还行吧?
但还有提升的空间,比如以下两点:
(1)文本的表示可以采用Doc2vec或Skip-thoughts等算法直接获取文档的向量表示;
(2)文本相似度计算可以试试欧氏距离、kullback_leibler(需要将互换位置的两个结果加总平均)、 jaccard或者hellinger等。
根据机器学习界的“没有免费晚餐”的定律,single pass聚类也是存在弱点的,主要是:
(1)依赖数据读入的顺序;
(2)阈值设定较为困难;
(3)单独效果使用较差。
不过,大家也不必过分纠结这几个“小毛病”,常言道“具体问题,具体分析”,没有最好的算法,使用场景适合的算法才是好算法~
推荐阅读
征稿启示| 200元稿费+5000DBC(价值20个小时GPU算力)
完结撒花!李宏毅老师深度学习与人类语言处理课程视频及课件(附下载)
模型压缩实践系列之——bert-of-theseus,一个非常亲民的bert压缩方法
文本自动摘要任务的“不完全”心得总结番外篇——submodular函数优化
斯坦福大学NLP组Python深度学习自然语言处理工具Stanza试用
关于AINLP
AINLP 是一个有趣有AI的自然语言处理社区,专注于 AI、NLP、机器学习、深度学习、推荐算法等相关技术的分享,主题包括文本摘要、智能问答、聊天机器人、机器翻译、自动生成、知识图谱、预训练模型、推荐系统、计算广告、招聘信息、求职经验分享等,欢迎关注!加技术交流群请添加AINLPer(id:ainlper),备注工作/研究方向+加群目的。
阅读至此了,分享、点赞、在看三选一吧🙏