基于k-means++和brich算法的文本聚类

2022-07-06 11:38:15 浏览数 (1)

文本聚类流程如下:

处理流程处理流程

分词和过滤停用词,这里分词有两步,第一步是对停用词进行分词,第二步是切分训练数据。

  1. 停用词是一些不包含什么信息的词语,以及一些特别高频的词,比如the,to,the,a,an,and等,这些在句子中没什么存在的意义可以去掉,这里我通过切分将中文停用词保存下来,在后续的处理中需要用到。
  2. 在构建训练词空间词袋的时候将每一个句子中的关键词语提取出来最后最为特征,这里提取时使用jieba库把文本切分成为短句,然后再次切分(去掉)中文停用词表中存在的短语,将最后切分的结果保存下来,在使用tf-idf进行特征赋值的时候需要用。

构建词袋空间: 

  1. 将所有文档读入到程序中,再将每个文档切词。 
  2. 去除每个文档中的停用词。 
  3. 统计所有文档的词集合(sk-learn有相关函数,但是我知道能对中文也使用)。 
  4. 对每个文档,都将构建一个向量,向量的值是词语在本文档中出现的次数。 

举个例子,假设有两个文本,1,我爱上海,我爱中国2。中国伟大,上海漂亮

那么切词之后就有以下词语:我,爱,上海,中国,伟大,漂亮,,(逗号也可能被切词)。 

再假设停用词是我,,那么去除停用词后,剩余的词语就是 

爱,上海,中国,伟大,漂亮。

构建的空间向量许下:

文本

上海

中国

伟大

漂亮

文本1

2

1

1

0

0

文本2

0

1

1

1

1

主要代码:

代码语言:javascript复制
def get_all_vector(path, stop_words_set):
    #读取文件
    file = codecs.open(path, 'r')
    lines = [line.strip() for line in file]
    str = ''.join(lines)
    #print(len(lines))
    #print('over')
    docs = []
    #构建文本空间并删除停用词
    word_set = set()
    for each in lines:
        doc = del_stop_words(each, stop_words_set)
        docs.append(doc)
        word_set |= set(doc)
    word_set = list(word_set)
    #print(word_set)
    docs_vsm = []
    for doc in docs:
        a=0
        #print(len(doc))
        temp_vector = []
        #将文本特征转化为数字特征
        for word in word_set:
            if word in doc:
                a =1
            temp_vector.append(doc.count(word) * 1.0)
            #print(doc.count(word) * 1.0)
        # print temp_vector[-30:-1]
        docs_vsm.append(temp_vector)
        #print(docs_vsm)
    docs_matrix = np.array(docs_vsm)

将单词出现的次数转化为tf-idf权重

tf-idf(term frequency–inverse document frequency,词频-逆向文件频率)是一种用于信息检索(information retrieval)与文本挖掘(text mining)的常用加权技术。

  1. tf-idf是一种统计方法,用以评估一字词对于一个文件集或一个语料库中的其中一份文件的重要程度。字词的重要性随着它在文件中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降。
  2. tf-idf的主要思想是:如果某个单词在一篇文章中出现的频率TF高,并且在其他文章中很少出现,则认为此词或者短语具有很好的类别区分能力,适合用来分类。

(1)TF是词频(Term Frequency)计算公式日下,即文本中词条出现的次数/文本总词条数。

tf计算公式tf计算公式

(2) IDF是逆向文件频率(Inverse Document Frequency)计算公式如下,某一特定词语的IDF,可以由总文件数目除以包含该词语的文件的数目,再将得到的商取对数得到。如果包含词条t的文档越少, IDF越大,则说明词条具有很好的类别区分能力。

idf计算公式idf计算公式

(3)TF-IDF实际上是:TF * IDF

最后的代码如下:代码中先求出tf也就是column_sum,然后使用np.diag()函数将所有文本转化为对角矩阵(对角线为数值)其他地方为0,再求出idf,这里idf也是矩阵,两者相乘就构成了整个文本空间的权值矩阵。

代码语言:javascript复制
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
    #print(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)
   # np.delete(tfidf,[0],axis=1)
    tfidf_mean=[]
    dele_axis = []
    for j in range(len(tfidf_mean)):
        means = np.mean(tfidf_mean)
        if tfidf_mean[j]<means:
            dele_axis.append([j])
        tfidf_mean.remove

    # #新增top k
    # tfidf_max = []
    # dele_axis_top=[]
    # tfidf1 = sorted(tfidf_mean)
    # tfidf_max=tfidf1[len(tfidf_mean)-1000-1:]
    # for j in range(len(tfidf_mean)):
    #     if tfidf_mean[j] not in tfidf_max:
    #         dele_axis_top.append([j])
    #
    # # 新增top k
    # tfidf=np.delete(tfidf,dele_axis_top,axis=1)

   # 数据小的时候
    tfidf = np.delete(tfidf, dele_axis, axis=1)

使用k-means算法进行聚类,并调整参数,主要是聚类中心的数量的调整和迭代次数的调整

这里由于自己写的k-means算法很水导致数据大的时候特别容易出bug所以调用了sklearn的k-means算法直接进行聚类,并保存聚类模型。

代码如下:

代码语言:javascript复制
def kmeans(X, k):  # X=weight
    clusterer = KMeans(n_clusters=k, init='k-means  ',max_iter=1000)  # 设置聚类模型
    y1=clusterer.fit(X)
    y1.fit_predict(X)
    y = clusterer.fit_predict(X)  # 把weight矩阵扔进去fit一下,输出label
    joblib.dump(y1,'C:\Users\ruanlu\Desktop\km.model')

    print('ss')
    print(type(y))
    print(y.shape)
    return y

聚类结果如下:

这里设置了迭代次数大概800次之后就没什么变化了,所以为了保险起见就把迭代次数调成1000,下面就调整聚类中心k的个数,这里的数据是使用3000个文本的结果,下面这些图是根据聚类结果的轮廓系数画出来的图,由于无监督模型无法估计结果,这里使用轮廓系数来描述。轮廓系数(Silhouette Coefficient),是聚类效果好坏的一种评价方式。最早由 Peter J. Rousseeuw 在 1986 提出。它结合内聚度和分离度两种因素。可以用来在相同原始数据的基础上用来评价不同算法、或者算法不同运行方式对聚类结果所产生的影响。

这里是从k=2一直调整到k=20,最终发现k=8的时候效果是最好的,下面只以中间几个k值进行描述:

判断:

  1. si接近1,则说明样本i聚类合理;
  2. si接近-1,则说明样本i更应该分类到另外的簇;
  3. 若si 近似为0,则说明样本i在两个簇的边界上。

k=5时虽然看起来还不错,但是有一部分不分到第1类中去了,而且大于0的部分数据也不是很大,所以结果还有一定的争议,效果不是最好。

k=6时,第0类虽然和k=5时比起来面积变小了,但是其他的有几个类出现了很多小于0的部分。

k=7的时候,虽然没有k=6的时候的其他情况,但是第1类的小于0的面积还是比较大。

k=8的时候,这个时候,第0类的小于0的部分的面积比较小,其他的类没有小于0的部分并且数值比较大比较合理

k=9的时候,第2类的小于0的部分有变大了,并且第7类还出现了部分小于0的情况。

k=10的时候,第1类有很多小于0的部分,并且第7类和第0类还出现了部分小于0的情况。

k=11的时候,很多类都出现了小于0的情况。

这里的轮廓系数的代码是调用sklearn库中的二代吗实现的,这里包括求轮廓系数和通过轮廓系数绘图两步,代码如下:

求轮廓系数:

代码语言:javascript复制
def Silhouette(X, y):
    print('计算轮廓系数:')
    silhouette_avg = silhouette_score(X, y)  # 平均轮廓系数
    sample_silhouette_values = silhouette_samples(X, y)  # 每个点的轮廓系数
    #print(silhouette_avg)
    return silhouette_avg, sample_silhouette_values

根据轮廓系数画图:

代码语言:javascript复制
def Draw(silhouette_avg, sample_silhouette_values, y, k,X):
    # 创建一个 subplot with 1-row 2-column
    fig, ax1 = plt.subplots(1)
    fig.set_size_inches(18, 7)
    # 第一个 subplot 放轮廓系数点
    # 范围是[-1, 1]
    ax1.set_xlim([-0.2, 0.5])
    # 后面的 (k   1) * 10 是为了能更明确的展现这些点
    ax1.set_ylim([0, len(X)   (k   1) * 10])
    y_lower = 10
    for i in range(k):  # 分别遍历这几个聚类
        ith_cluster_silhouette_values = sample_silhouette_values[y == i]
        ith_cluster_silhouette_values.sort()
        size_cluster_i = ith_cluster_silhouette_values.shape[0]
        y_upper = y_lower   size_cluster_i
        cmap = cm.get_cmap("Spectral")
        color = cmap(float(i)/ k)
       # color = cm.spectral(float(i) / k)  # 搞一款颜色
        ax1.fill_betweenx(np.arange(y_lower, y_upper),
                          0,
                          ith_cluster_silhouette_values,
                          facecolor=color,
                          edgecolor=color,
                          alpha=0.7)  # 这个系数不知道干什么的
        # 在轮廓系数点这里加上聚类的类别号
        ax1.text(-0.05, y_lower   0.5 * size_cluster_i, str(i))

        # 计算下一个点的 y_lower y轴位置
        y_lower = y_upper   10

        # 在图里搞一条垂直的评论轮廓系数虚线
    ax1.axvline(x=silhouette_avg, color='red', linestyle="--")
    #plt.savefig('')
    plt.show()

保存训练集的分类结果:

每一个文本最后训练出来都会打一个标签,这个标签表面数据属于哪一类,最后根据标签将标签相同的类别保存到一个txt的文档里面。部分结果如下:

标签5的文档标签5的文档

可以看出来这个标签里面的数据效果都比较好。

标签6的文档标签6的文档

这个也不错。

标签0的文档标签0的文档

标签0的大部分都是与赌博相关的,但是其中也夹杂着一点点其他的正常文本。

代码如下:

代码语言:javascript复制
def sav_ressult(y,path2,k,lines):
    dics={}
    line=lines
    lists=[]
    for each1 in range(k):
        each1=[]
        lists.append(each1)
    for each in range(len(y)):
        #print(each)
        for r in range(k):
            #print(r)
            if y[each]==r:
                dics[each]=r
                lists[r].append(line[each])
    for result in range(k):
        #path=''
        path=path2 result.__str__() '.txt'
        with open(path,encoding='utf-8',mode='w') as file:
            for each in lists[result]:
                file.write(each 'n')
            file.close()

保存模型和数据预测

在对数据聚类的时候,每次将训练的模型保存下来然后使用测试数据去预测并打上标签,这里使用的数据是100000行的文本,参数调整为k=15,迭代次数为2000次,由于数据比较大,程序是在服务器上面跑的,结果如下:

预测数据1预测数据1

从图中可知预测数据被打上标签1的数据大致一致,都是形如:"上如何买 ?V徽.X信?【】【Q-Q?】全.国.货.到.付.款】【诚.信.第.一】【顺.丰.快.递】【诚.信.保.密】√√ 1个80后,24个90后,2个00后…救火英雄最后的朋"之类的,这一类是属于在通过微信购买一些违禁品的广告类似的。

预测数据2预测数据2

从图中可知预测数据被打上标签8的数据大致一致,都是形如“3d攻略技巧苹果app捕鱼下分捕鱼达人破解版在线捕鱼现金app送钱捕鱼游戏下载真人版赢现金app真人斗地主免费版彩虹乐园棋牌638棋牌最火的手机现金棋牌真人赢钱捕鱼可提现大发棋牌app正规可提现的斗地主途 8”,属于网上捕鱼类似的文本。

预测数据3预测数据3

从图中可知预测数据被打上标签3的数据大致一致,都是形如“团官网环亚集团手机版ag环亚88ag环亚集团环亚电游下载环亚集团agag登录环亚电游娱乐官网ag环亚备用网址环亚电游agag环亚集团娱乐环亚电游下载环亚88ag环亚手机登录ag游艇会官网ag环亚登录ag环亚”,

整体上看,预测效果都没有出错,预测的样本打上同一个标签的样本都属于同一类。

代码如下:

代码语言:javascript复制
def predict():
    paths = 'C:\Users\ruanlu\Desktop\test1.txt'
    i=0
    file = codecs.open(paths, 'r','utf-8')
    line1 = [line.strip() for line in file]
    tfidf_mat1 = get_all_vector(paths, stop_words)
    tv=joblib.load('C:\Users\ruanlu\Desktop\km.model')
    y=tv.fit_predict(tfidf_mat1)
    #paths = 'C:\Users\ruanlu\Desktop\datas\result\'
    with open('C:\Users\ruanlu\Desktop\datas\predict\res.txt','w') as f:

        for each in y:
            #print(each.__str__())
            f.write(line1[i] 't' 'n')
            i =1

算法比较

这里主要使用k-means算法和birch算法分别聚类并进行比较,birch算法是通过集成层次聚类其他聚类算法来对大量数值数据进行聚类,其中层次聚类用于初始的微聚类阶段,而其他方法如迭代划分(在最后的宏聚类阶段)。birch算法适用于数据表较大的,类别数特别多的情况,并且运行速度还很快,这里由于在数据比较大的时候比如有上百万行的文本,这个时候数据构成的向量矩阵可以有上千万或者上亿的维数,这个时候用brich算法就很有优势。

数据量小的时候:

由于数据比较小,这个时候使用brich算法很多类别明显有很多小于0的部分,因为brich认为很多类别都还可以细分,但是k-means算法就不会有很多小于0的部分。这个时候使用k-means算法虽然可能会有错但是分类的效果还算不错。

数据量比较大的时候:

可以看到,当数据比较大并且,类别特别多的时候,这个时候birch算法的每个类别即使有小于0的部分,但是这个时候小于0的面积也不是很大,所以这个时候,birch算法效果还是比较好。

PCA降维:

在数据量比较大导致数据的向量矩阵比较大的时候可以使用PCA来对数据降维,PCA降维主要是用来减小维数比较高的矩阵的维数,他通过将将矩阵中一定数量的主要特征提取出来形成一个新的矩阵,然后以这个新的矩阵来代替之前的高维的矩阵以达到减少运算的目的。

降维后运行时间降维后运行时间
降维前运行时间降维前运行时间

可以看到降维后运行时间明显缩短了许多,这是由于降维后矩阵由原来的上万维变成了50维这个时候运算量就很小了,但是这个时候数据聚类的想过就会有误差。不仅降维后标签2(对应降维前标签0)的小于0的部分面积变大了,这个时候标签1也出现了部分小于0的部分,效果反而变差了。

代码如下:

代码语言:javascript复制
def PCA(weight, dimension):  
    print('原有维度: ', len(weight[0]))
    print('开始降维:')
    pca = PCA(n_components=dimension)  # 初始化PCA
    X = pca.fit_transform(weight)  # 返回降维后的数据
    print('降维后维度: ', len(X[0]))
    print(X)
    return X

总结:

本次对文本聚类是自己的第一个机器学习相关的练手小项目,其中涉及到许多和机器学习相关的算法和概念,比如,k-means,birch,tf-idf,PCA降维等等,本次小项目中,从文本聚类流程的理解,文本本身需要如何去构建特征才有意义到如何提取特征,以及最后的构建特征向量到算法里面的这一整个过程加深了我对样本特征这个词语的理解,之后就是对算法调整参数和如何评估算法的效果这一块也收获很多,比如在k-means算法中,由于需要调整的参数有两个,聚类中心数量k和算法的迭代次数n,由于这两个参数的变化最终都会印象到最终的结果,所以调整参数这一块还需要多去理解算法本身中这些参数的原理的意义何在,以及这些这些算法是如何影响到结果的,只有对参数足够了解才能调整参数的时候不那么费力。最后,由于本次项目中使用的是聚类算法,属于无监督学习,而无监督学习本身就很难评判结果的好坏,这里使用轮廓系数来描述效果的好坏也是一个不错的进步。

0 人点赞