花了好几天时间学习了文本聚类,以下记录一下这次的学习,也整理了一些这方面的资料,和大家分享一下,一起交流学习,进步在于不断总结和分享以及相互交流。
文本聚类就是把相似的文档聚集成一簇,通过把文本转换成数值进行聚类,主要分为两个部分,第一个是构建词袋,也就是TF-IDF矩阵,也叫作文档词频矩阵,这个矩阵的每行是一篇文档,每列是一个词,矩阵的某个值代表该词在某篇文档的TF-IDF权重,就是把文档中的每个词转成它在这篇文档的重要性,用TF-IDF来表达这个词的重要程度,从而用数值来替换文本。这一步非常重要,可以说它占整个文本聚类过程的五分之四。关于TF-IDF的介绍。我认为解释的比较易懂的是这篇文档:http://mp.weixin.qq.com/s?__biz=MzA3MDg0MjgxNQ==&mid=2652390921&idx=1&sn=c89606ab9a3d55387305f54a05d26fd5&chksm=84da46d9b3adcfcff0c500c82ea01d1ba3a3d4beb50354cad084863c24b3ab5978bb0d015354&mpshare=1&scene=1&srcid=03255LjRoyvBkfIFoMPxmsBt#rd,这里解释得非常简单易懂了,大家可以阅读参考,(附:这篇文章是我在一个叫 数据挖掘入门与实战的微信公众号看到的,这个公众号经常推送一些数据挖掘的文章,非常实用,感兴趣的伙伴可以关注学习。)我总结一下构建词袋的几个关键步骤,首先是分词,分词是否准确影响聚类效果,关于jieba分词可以参考这篇文章http://www.oschina.net/p/jieba/,然后是计算TF值和IDF值,最后计算TF-IDF值,构建矩阵,这就是构建词袋的过程。
我们已经知道TF-IDF的含义了,下面具体说说代码实现。一开始我不了解已经有现成的库可以直接调用函数就可以计算TF-IDF值,直接可以得到词袋,所以我一开始是自己参考一些资料,根据TF-IDF的含义,一步步编代码计算出来。当我花了很多时间不断地调代码,终于运行正确的时候,突然找到关于scikit-learn库有相应的模块直接调用函数就可以计算。所以我两种方法都试了,以下我把两个方法都介绍一下。
1、直接计算法
import jieba
import os
import pandas as pd
import numpy as np
stopw = [line.strip().decode(‘utf-8’) for line in open(u’F:/自然语言处理/stops.txt’).readlines()]#读取停用词
def del_stop_words(words,stop_words):#定义一个将分词结果过滤掉停用词的函数
result=jieba.cut(words)#分词
new_words = []
for i in result:#对分词结果进行遍历
if i not in stop_words:#如果词语不在停用词表,是我们需要的词
new_words.append(i)#将保留下来的词追加到一个新的list中
return new_words
names = [ os.path.join(u’F:/自然语言处理/document’,h) for h in os.listdir(u’F:/自然语言处理/document’) ]#主要是读取document文件夹下所有的文本文件的路径,其中os.listdir是读取文本文件的名称如‘1.txt’、‘2.txt’
posts = [ open(name).read() for name in names ]#循环读取每个文本文件的内容
docs = []
word_set = set()
以下这个循环的主要作用是获取每个文档的词表,然后把 每个文档的词表取并集,形成word_set。词表就是指该篇文档由哪些词组成。用set函数可以得到每篇文档包含的词语。
for post in posts:
doc = del_stop_words(post,stopw)
docs.append(doc)
word_set |= set(doc)
word_set = list(word_set)#将word_set转为一个list数据类型
docs_vsm = []
以下这两个循环的主要作用是计算每一篇文档的词频,并把所有文档的词频全部放在doc_vsm中,doc_vsm转换成矩阵docs_matrix,这就是一个词频矩阵,每行是一篇文档,列是相应文档里的词,矩阵的某个数值表示该词在某篇文档的频数。
for i in range(13):#我的实验数据一共13篇文档,所以遍历13次,或许有同学会问为什么不是for doc in docs,我参考的博客资料确实这么写,但是我运行测试以后发现有问题,就是最后有一个函数是计算矩阵docs_matrix每一行的和,出现为0的情况,这显然不对,不可能一篇文档的词总数为0,当我写成for i in range(13):的时候,就没有这种问题,13这个数字可以根据您测试的文档数进行更换。
temp_vector = []
for word in word_set:#遍历词表,该词表包含组成文档的所有词
temp_vector.append(docs[i].count(word) * 1.0)#计算每篇文档每个词的词频, temp_vector临时保存一篇文档的词频
docs_vsm.append(temp_vector)#将每篇文档的词频依次追加到docs_vsm数据表中
docs_matrix = np.mat(docs_vsm)
column_sum = [ float(len(np.nonzero(docs_matrix[:,i])[0])) for i in range(docs_matrix.shape[1]) ]#计算包含该词的文档数
column_sum = np.array(column_sum)#转换为数组,因为数组可以方便后面的批量除法计算
column_sum = docs_matrix.shape[0] / column_sum#用文档总数除以包含某个词的文档总数(根据idf的概念)
idf = np.log(column_sum)#取对数
idf = np.diag(idf)#将数组转换为n*n的对角矩阵
以下这个循环主要是根据前面的词频矩阵docs_matrix计算tf值,tf值是词频除以该篇文档的总词数。
for doc_v in docs_matrix:
if doc_v.sum() == 0:
doc_v = doc_v / 1
else:
doc_v = doc_v / (doc_v.sum())
tfidf = np.dot(docs_matrix,idf)#tf*idf
tfidf就是一个词袋了
以上是直接计算的方法,为了说明详细,所以写的注释有点多,为了大家看得更清晰一些,我把代码重新复制一次。
import jieba
import os
import pandas as pd
import numpy as np
stopw = [line.strip().decode(‘utf-8’) for line in open(u’F:/自然语言处理/stops.txt’).readlines()]
def del_stop_words(words,stop_words):
result=jieba.cut(words)
new_words = []
for i in result:
if i not in stop_words:
new_words.append(i)
return new_words
names = [ os.path.join(u’F:/自然语言处理/document’,h) for h in os.listdir(u’F:/自然语言处理/document’) ]
posts = [ open(name).read() for name in names ]
docs = []
word_set = set()
for post in posts:
doc = del_stop_words(post,stopw)
docs.append(doc)
word_set |= set(doc)
word_set = list(word_set)
docs_vsm = []
#print word.encode(“utf-8”)
for i in range(13):
temp_vector = []
for word in word_set:
temp_vector.append(docs[i].count(word) * 1.0)
docs_vsm.append(temp_vector)
docs_matrix = np.mat(docs_vsm)
column_sum = [ float(len(np.nonzero(docs_matrix[:,i])[0])) for i in range(docs_matrix.shape[1]) ]
column_sum = np.array(column_sum)
column_sum = docs_matrix.shape[0] / column_sum
idf = np.log(column_sum)
idf = np.diag(idf)
for doc_v in docs_matrix:
if doc_v.sum() == 0:
doc_v = doc_v / 1
else:
doc_v = doc_v / (doc_v.sum())
tfidf = np.dot(docs_matrix,idf)
2、使用scikit-learn库的feature_extraction.textTrans文本特征抽取模块中的former 和CountVectorizer 相关函数进行计算,比上面的方法简单十倍都不止,但是数据类型要必须满足它的要求,所以在使用这个方法之前要做一些特别的处理,最后形成一个词列表list,list的每个元素是一篇文档的所有词,即每篇文档的所有词构成一个向量,所有的向量构成list。以下是相关代码:
2.1计算TFIDF值之前的前期处理
import re
import os
import sys
import codecs
import jieba
stopw = [line.strip().decode(‘utf-8’) for line in open(u’F:/自然语言处理/stops.txt’).readlines()]
path=u’F:/file/’
ph=u’F:/自然语言处理/document/’
n= 1
while n<=13: #遍历每篇文档
name = “%d” % n
filename = ph + str(name) + “.txt”
resname=path+str(name)+”.txt”
source = open(filename, ‘r’)
result = codecs.open(resname, ‘w’, ‘utf-8’)
line = source.readline() #按行读取
line = line.rstrip(‘\n’)
while line!=”“: #对读取的每行数据进行分词
seglist = jieba.cut(line,cut_all=False)
newword=[]
for i in seglist:
if i not in stopw:
newword.append(i)#去停后的分词结果是以逗号分隔
output = ’ ‘.join(list(newword)) #所以要用join替换成空格拼接
result.write(output + ‘\r\n’) #将该篇文档的某一行文本分词后的结果写入新文档
line = source.readline() #循环读取该篇文档的每行文本
source.close()
result.close()
n = n + 1
result = codecs.open(u’F:/自然语言处理/document/all.txt’, ‘w’, ‘utf-8’)
num = 1
while num <= 13:
name = “%d” % num
fileName = path + str(name) + “.txt”
source = open(fileName, ‘r’)
line = source.readline()
line = line.strip(‘\n’) #去除回车换行符,让所有的词形成一行
line = line.strip(‘\r’)
while line!=”“:
line = line.replace(‘\n’,’ ‘)
line = line.replace(‘\r’,’ ‘) #去除回车换行符,让所有的词形成一行
result.write(line+ ’ ‘) #将每一行写入一个文档并用空格分隔
line = source.readline()
source.close()
num = num + 1
result.close()
主要思路是读取每篇文档进行分词,分词以后将分词结果分别写到新的文本文件中去,我原始文档有13个,最后也会有相应的13个分词结果文件,接着再读取每个分词结果文件到python中,再依次写到一个文件中,这个文件就包含了所有文档的分词结果,这个文件每一行就是一篇文档的分词结果,我这里测试文档是13篇,那么这个文件就只有13行数据,必须这样,否则后面使用函数计算TFIDF会出错。
2.2正式进行计算TF-IDF值
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.feature_extraction.text import CountVectorizer
corpus = []
for line in open(u’F:/自然语言处理/document/all.txt’, ‘r’).readlines():
#print line
corpus.append(line.strip())#all文件就是存放所有文档分词结果的文件,一共13行,每行代表一篇文本,把它读取到corpus中
vectorizer = CountVectorizer()#将文本中的词语转换为词频矩阵 矩阵元素a[i][j] 表示j词在i类文本下的词频
transformer = TfidfTransformer() #该类会统计每个词语的tf-idf权值
tfidf = transformer.fit_transform(vectorizer.fit_transform(corpus))#第一个fit_transform是计算tf-idf 第二个fit_transform是将文本转为词频矩阵
word = vectorizer.get_feature_names() #获取词袋模型中的所有词语
weight = tfidf.toarray() #将tf-idf矩阵抽取出来,元素w[i][j]表示j词在i类文本中的tf-idf权重
weight就是tfidf矩阵
3、聚类
根据前面计算的tfidf值,我们已经把文本转换成了数值,可以直接进行聚类了。实际上为了测试效果,我使用的文本是已经分好类的,是搜狗实验室的文本数据,我分别从财经、IT、汽车三个类抽取出13篇文章,所以我可以很容易了解到聚类效果。我尝试了所有的聚类方法,聚类效果都不是很好,在其他一些作者写的关于文本聚类的文章里面,也有提到效果不是很好。根据我以前看过的文献资料,应该是最后的TFIDF矩阵是一个高维稀疏矩阵,导致聚类效果很差,解决方法就是降维,目前我还没有继续往下优化,希望对这方面有研究的朋友跟我交流,多多指导。
以下是聚类的有关代码:
from sklearn.cluster import KMeans
kmeans=KMeans(n_clusters=3)
kmeans.fit(weight)
kmeans.labels_#输出k-means聚类的结果
from sklearn import cluster
ms = cluster.MeanShift()
ms.fit_predict(weight)
two_means = cluster.MiniBatchKMeans(n_clusters=3)
two_means.fit_predict(weight)
ward = cluster.AgglomerativeClustering(n_clusters=3, linkage=’ward’)
ward.fit_predict(weight)
spectral = cluster.SpectralClustering(n_clusters=3,eigen_solver=’arpack’,affinity=”nearest_neighbors”)
spectral.fit_predict(weight)
dbscan = cluster.DBSCAN(eps=.2)
dbscan.fit_predict(weight)
affinity_propagation = cluster.AffinityPropagation()
affinity_propagation.fit_predict(weight)
从所有的运行结果看,虽然结果都不太好,总的来说k-means稍微好一些,但是我了解到谱聚类是比较适合高维数据的聚类的,实际运行效果也不好,可能我调参不对,感兴趣的同学都可以尝试一下。
以下给出我参考的几篇博客资料:
http://blog.csdn.net/eastmount/article/details/50473675
http://blog.csdn.net/yyxyyx10/article/details/63685382
这两篇博客写得很好,尤其是第一篇,里面还总结了很多文本挖掘的资料,大家可以参考。