赞
踩
对于中文短文本而言,其有着单个文本词量少,文本多等特点,并且在不同的领域中中文短文本有着不同的特点。本文以已获取的微博语料出发,使用DBSCAN密度聚类,并对其进行简单可视化。
#说明:
1-本文所有程序都已实现跑通,可直接复制调试,输入的文档为文本文档.txt,编码格式为utf-8(可以在另存为之中修改编码格式,默认为ANSI),注意每一行为一个短文本样本即可。
2-本文以python3.7为语言环境,使用到的jieba,sklearn等扩展包需要提前安装好。
对于原始文本,总会有很多东西是我们不需要的,比如标点、网址来源、表情转换符([西瓜]、[大笑])等,如下图所示。
因此我们首先读取记事本中的内容,并按照每行的格式保存到python列表list中,代码如下所示:
import codecs
corpus = []
file = codecs.open("微博短文本.txt","r","utf-8")
for line in file.readlines():
corpus.append(line.strip())
接着,我们就可以对于储存到列表corpus中的每个预料字符串进行去噪了,在这里使用的是re库中的sub函数,为了方便阅读和修改,所以以罗列的方式展现,代码如下所示:
stripcorpus = corpus.copy()
for i in range(len(corpus)):
stripcorpus[i] = re.sub("@([\s\S]*?):","",corpus[i]) # 去除@ ...:
stripcorpus[i] = re.sub("\[([\S\s]*?)\]","",stripcorpus[i]) # [...]:
stripcorpus[i] = re.sub("@([\s\S]*?)","",stripcorpus[i]) # 去除@...
stripcorpus[i] = re.sub("[\s+\.\!\/_,$%^*(+\"\']+|[+——!,。?、~@#¥%……&*()]+","",stripcorpus[i]) # 去除标点及特殊符号
stripcorpus[i] = re.sub("[^\u4e00-\u9fa5]","",stripcorpus[i]) # 去除所有非汉字内容(英文数字)
stripcorpus[i] = re.sub("客户端","",stripcorpus[i])
stripcorpus[i] = re.sub("回复","",stripcorpus[i])
接着,我们在只剩下汉语的stripcorpus列表中,将字符串长度小于5的去除,并使用jieba进行分词,代码如下所示:
import jieba.posseg as pseg onlycorpus = [] for string in stripcorpus: if(string == ''): continue else: if(len(string)<5): continue else: onlycorpus.append(string) cutcorpusiter = onlycorpus.copy() cutcorpus = onlycorpus.copy() cixingofword = [] # 储存分词后的词语对应的词性 wordtocixing = [] # 储存分词后的词语 for i in range(len(onlycorpus)): cutcorpusiter[i] = pseg.cut(onlycorpus[i]) cutcorpus[i] = "" for every in cutcorpusiter[i]: cutcorpus[i] = (cutcorpus[i] + " " + str(every.word)).strip() cixingofword.append(every.flag) wordtocixing.append(every.word) # 自己造一个{“词语”:“词性”}的字典,方便后续使用词性 word2flagdict = {wordtocixing[i]:cixingofword[i] for i in range(len(wordtocixing))}
到此,我们完成了对于原始语料的基本处理,最终得到的处理后的语料列表和词性字典如下图所示。
可以看到上面的cutcorpus语料库中,有一些词语是我们不需要的(如“我”,“了”等),因为后续在进行特征提取时会按照词性进行权重赋值,所以在这里并没有按照专用词和停用词词典进行筛选。
说到短文本的特征提取,最著名也是目前用的最多普适性最强的就是TF-IDF的词频特征提取了,当然对于不同类型的短文本TF-IDF方法有着不一样的缺点,因此在短文本特征提取这一块有着很多各式各样的小论文,改参数,改进公式,就可以写一篇小论文了,但是这些论文中真正具有普适性的较少。
在这里,我们首先使用标准的python sklearn库中的TfidfTransformer和CountVectorizer来获取每个短文本的特征向量,从而组成整个样本特征X,代码如下所示:
from sklearn import feature_extraction
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.feature_extraction.text import CountVectorizer
vectorizer = CountVectorizer()
transformer = TfidfTransformer()#该类会统计每个词语的tf-idf权值
#第一个fit_transform是计算tf-idf 第二个fit_transform是将文本转为词频矩阵
tfidf = transformer.fit_transform(vectorizer.fit_transform(cutcorpus))
#获取词袋模型中的所有词语
word = vectorizer.get_feature_names()
#将tf-idf矩阵抽取出来,元素w[i][j]表示j词在i类文本中的tf-idf权重
weight = tfidf.toarray()
其中weight就是特征矩阵X,word是X中每一维度特征对应的词。weight的大小在这里是(20097205),word的大小(72051)。其实到这里,已经可以用weight去聚类了,但是要知道在短文本中TF-IDF方法本质是提取词语的形状,和词语出现的顺序是无关的,所以从特征提取的角度而言是缺失了部分特征。
所以在这里,我们将word中每个词语的词性,通过自定义的方式赋给它们不同的权重,并乘到weight上的每一行样本中,进而改变它们的特征矩阵。这样做的目的其实是想让特征矩阵的区分能力增强一点,代码如下所示:
wordflagweight = [1 for i in range(len(word))] #这个是词性系数,需要调整系数来看效果 for i in range(len(word)): if(word2flagdict[word[i]]=="n"): # 这里只是举个例子,名词重要一点,我们就给它1.1 wordflagweight[i] = 1.2 elif(word2flagdict[word[i]]=="vn"): wordflagweight[i] = 1.1 elif(word2flagdict[word[i]]=="m"): # 只是举个例子,这种量词什么的直接去掉,省了一步停用词词典去除 wordflagweight[i] = 0 else: # 权重数值还要根据实际情况确定,更多类型还请自己添加 continue import numpy as np wordflagweight = np.array(wordflagweight) newweight = weight.copy() for i in range(len(weight)): for j in range(len(word)): newweight[i][j] = weight[i][j]*wordflagweight[j]
为什么使用DNSCAN聚类不使用其他聚类方法呢?其实其他聚类方法也可以,只不过这个聚类算法口碑比较好,它也有它自己的优缺点,具体可以Google一下。
其实有了特征之后,聚类是一件很简单的事情(除了调参以外)。或者换句话说聚类很简单,聚出能用的类就很难了,这需要一定的耐心和方法,话不多说,代码如下:
from sklearn.cluster import DBSCAN
DBS_clf = DBSCAN(eps=1, min_samples=6)
DBS_clf.fit(newweight)
没错,就是sklearn三步走:调包、实例化、训练。值得注意的是,DBSCAN有两个主要的参数需要调整,一个是eps(我们可以将它理解为聚类那个圈的半径),一个是min_samples(最少多少个样本可以称之为一类)。关于聚类效果的可视化,我是先将多维数据通过PCA压缩到聚类种类那么多维的数据,再通过TSNE压缩到2维来描点作图,这样效果好点,代码如下所示:
import matplotlib.pyplot as plt from sklearn.decomposition import PCA from sklearn.manifold import TSNE from sklearn.cluster import DBSCAN def DBS_Visualization(epsnumber,min_samplesnumber,X_weight): DBS_clf = DBSCAN(eps=epsnumber,min_samples=min_samplesnumber) DBS_clf.fit(X_weight) labels_ = DBS_clf.labels_ X_reduction = PCA(n_components=(max(labels_)+1)).fit_transform(X_weight) #这个weight是不需要改变的 X_reduction = TSNE(2).fit_transform(X_reduction) #每次压缩的结果都是不一样的,因为n_components在变 signal = 0 noise = 0 xyclfweight = [[[],[]] for k in range(max(labels_)+2)] for i in range(len(labels_)): if(labels_[i]==-1): noise += 1 xyclfweight[-1][0].append(X_reduction[i][0]) xyclfweight[-1][1].append(X_reduction[i][1]) else: for j in range(max(labels_)+1): if(labels_[i]==j): signal += 1 xyclfweight[j][0].append(X_reduction[i][0]) xyclfweight[j][1].append(X_reduction[i][1]) colors = ['red','blue','green','yellow','black','magenta'] * 3 for i in range(len(xyclfweight)-1): plt.plot(xyclfweight[i][0],xyclfweight[i][1],color=colors[i]) plt.plot(xyclfweight[-1][0],xyclfweight[-1][1],color='#FFB6C1') # 自适应坐标轴 plt.axis([min(X_reduction[:,0]),max(X_reduction[:,0]),min(X_reduction[:,1]),max(X_reduction[:,1])]) plt.xlabel("x1") plt.ylabel("x2") plt.show() print("分类数量(含噪声-1,粉色)= "+str(max(labels_)+2)," " + "信噪比 = "+str(signal/noise)) #包括噪声一共有多少类 print("eps = "+str(epsnumber)+" ", "min_sample = "+str(min_samplesnumber))
上面是一个简单的绘制效果的函数,下面调用一下这个函数,我们随意取一对参数值eps = 0.95,min_samples = 6,使用我们的新特征newweight进行训练,代码和得到的结果如下所示:
DBS_Visualization(0.95,6,newweight)
是的,这个参数聚类的效果并不好,所以还需要不断地尝试与调整参数。总的来说聚类效果主要有三个原因决定:一是文本特征的提取,二是聚类方法和参数的选择,三是语料本身的特点。本文只是做了一个初步的聚类,对于区别比较明显的大量文本数据还比较好使,但是对于不明显特征的短文本效果还有待提升,还望读者们留言批评指正。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。