关于文本的Brich层次聚类

1.文本聚类的一般性过程:

一般生成文档向量矩阵的格式是,每一行代表一个文档,每一列是一个维度代表该文档这个词的权重,没出现这个词就是0,几千个文件维度在10多w左右(看文档的大小),这么大的维度人脑想也想到了,矩阵将是及其稀疏的,也就是说,在一个高维空间中,几千个点几乎都聚在了一起,虽说彼此之间有距离,但是距离非常之小,很明显这样聚类效果肯定非常差,实测过,跟抛硬币的概率一样。于是将矩阵稠密一点就想到了pca降维,pca是主成分分析的缩写,大致意思就是取这个高维向量中方差最大的方向经过一些数学变换将有用的部分保留,没用的部分舍弃。
 

2.Brich概述

BIRCH算法利用了一个树结构来帮助我们快速的聚类,这个数结构类似于平衡B+树,一般将它称之为聚类特征树(Clustering Feature Tree,简称CF Tree)。这颗树的每一个节点是由若干个聚类特征(Clustering Feature,简称CF)组成。从下图我们可以看看聚类特征树是什么样子的:每个节点包括叶子节点都有若干个CF,而内部节点的CF有指向孩子节点的指针,所有的叶子节点用一个双向链表链接起来。

3.聚类特征CF与聚类特征树CF Tree

 

在聚类特征树中,一个聚类特征CF是这样定义的:每一个CF是一个三元组,可以用(N,LS,SS)表示。其中N代表了这个CF中拥有的样本点的数量,这个好理解;LS代表了这个CF中拥有的样本点各特征维度的和向量,SS代表了这个CF中拥有的样本点各特征维度的平方和。举个例子如下图,在CF Tree中的某一个节点的某一个CF中,有下面5个样本(3,4), (2,6), (4,5), (4,7), (3,8)。则它对应的N=5, LS=(3+2+4+4+3,4+6+5+7+8)=(16,30)(3+2+4+4+3,4+6+5+7+8)=(16,30), SS =(32+22+42+42+32+42+62+52+72+82)=(54+190)=244

 

对于CF Tree,我们一般有几个重要参数,第一个参数是每个内部节点的最大CF数B,第二个参数是每个叶子节点的最大CF数L,第三个参数是针对叶子节点中某个CF中的样本点来说的,它是叶节点每个CF的最大样本半径阈值T,也就是说,在这个CF中的所有样本点一定要在半径小于T的一个超球体内。对于上图中的CF Tree,限定了B=7, L=5, 也就是说内部节点最多有7个CF,而叶子节点最多有5个CF。

 

4.聚类特征树CF Tree的生成

        下面我们看看怎么生成CF Tree。我们先定义好CF Tree的参数: 即内部节点的最大CF数B, 叶子节点的最大CF数L, 叶节点每个CF的最大样本半径阈值T

  在最开始的时候,CF Tree是空的,没有任何样本,我们从训练集读入第一个样本点,将它放入一个新的CF三元组A,这个三元组的N=1,将这个新的CF放入根节点,此时的CF Tree如下图:

 

 现在我们继续读入第二个样本点,我们发现这个样本点和第一个样本点A,在半径为T的超球体范围内,也就是说,他们属于一个CF,我们将第二个点也加入CF A,此时需要更新A的三元组的值。此时A的三元组中N=2。此时的CF Tree如下图:

    此时来了第三个节点,结果我们发现这个节点不能融入刚才前面的节点形成的超球体内,也就是说,我们需要一个新的CF三元组B,来容纳这个新的值。此时根节点有两个CF三元组A和B,此时的CF Tree如下图:

    当来到第四个样本点的时候,我们发现和B在半径小于T的超球体,这样更新后的CF Tree如下图:

    那个什么时候CF Tree的节点需要分裂呢?假设我们现在的CF Tree 如下图, 叶子节点LN1有三个CF, LN2和LN3各有两个CF。我们的叶子节点的最大CF数L=3。此时一个新的样本点来了,我们发现它离LN1节点最近,因此开始判断它是否在sc1,sc2,sc3这3个CF对应的超球体之内,但是很不幸,它不在,因此它需要建立一个新的CF,即sc8来容纳它。问题是我们的L=3,也就是说LN1的CF个数已经达到最大值了,不能再创建新的CF了,怎么办?此时就要将LN1叶子节点一分为二了。

    我们将LN1里所有CF元组中,找到两个最远的CF做这两个新叶子节点的种子CF,然后将LN1节点里所有CF sc1, sc2, sc3,以及新样本点的新元组sc8划分到两个新的叶子节点上。将LN1节点划分后的CF Tree如下图:

    如果我们的内部节点的最大CF数B=3,则此时叶子节点一分为二会导致根节点的最大CF数超了,也就是说,我们的根节点现在也要分裂,分裂的方法和叶子节点分裂一样,分裂后的CF Tree如下图:

    有了上面这一系列的图,相信大家对于CF Tree的插入就没有什么问题了,总结下CF Tree的插入:

    1. 从根节点向下寻找和新样本距离最近的叶子节点和叶子节点里最近的CF节点

    2. 如果新样本加入后,这个CF节点对应的超球体半径仍然满足小于阈值T,则更新路径上所有的CF三元组,插入结束。否则转入3.

    3. 如果当前叶子节点的CF节点个数小于阈值L,则创建一个新的CF节点,放入新样本,将新的CF节点放入这个叶子节点,更新路径上所有的CF三元组,插入结束。否则转入4。

    4.将当前叶子节点划分为两个新叶子节点,选择旧叶子节点中所有CF元组里超球体距离最远的两个CF元组,分布作为两个新叶子节点的第一个CF节点。将其他元组和新样本元组按照距离远近原则放入对应的叶子节点。依次向上检查父节点是否也要分裂,如果需要按和叶子节点分裂方式相同。

5.Brich聚类算法介绍

上面讲了半天的CF Tree,终于我们可以步入正题BIRCH算法,其实将所有的训练集样本建立了CF Tree,一个基本的BIRCH算法就完成了,对应的输出就是若干个CF节点,每个节点里的样本点就是一个聚类的簇。也就是说BIRCH算法的主要过程,就是建立CF Tree的过程。

    当然,真实的BIRCH算法除了建立CF Tree来聚类,其实还有一些可选的算法步骤的,现在我们就来看看 BIRCH算法的流程。

    1) 将所有的样本依次读入,在内存中建立一颗CF Tree, 建立的方法参考上一节。

    2)(可选)将第一步建立的CF Tree进行筛选,去除一些异常CF节点,这些节点一般里面的样本点很少。对于一些超球体距离非常近的元组进行合并

    3)(可选)利用其它的一些聚类算法比如K-Means对所有的CF元组进行聚类,得到一颗比较好的CF Tree.这一步的主要目的是消除由于样本读入顺序导致的不合理的树结构,以及一些由于节点CF个数限制导致的树结构分裂。

    4)(可选)利用第三步生成的CF Tree的所有CF节点的质心,作为初始质心点,对所有的样本点按距离远近进行聚类。这样进一步减少了由于CF Tree的一些限制导致的聚类不合理的情况。

    从上面可以看出,BIRCH算法的关键就是步骤1,也就是CF Tree的生成,其他步骤都是为了优化最后的聚类结果。

6.Brich算法的实现:

# coding:utf-8
 
# 2.0 使用jieba进行分词,彻底放弃低效的NLPIR,用TextRank算法赋值权重(实测textrank效果更好)
# 2.1 用gensim搞tfidf
# 2.2 sklearn做tfidf和kmeans
# 2.3 将kmeans改成BIRCH,使用传统tfidf
 
import logging
import time
import os
import jieba
import glob
import random
import copy
import chardet
import gensim
from gensim import corpora,similarities, models
from pprint import pprint
import jieba.analyse
from sklearn import feature_extraction
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.feature_extraction.text import CountVectorizer
import os
from sklearn.decomposition import PCA
 
 
 
# logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
 
start = time.clock()
 
print '#----------------------------------------#'
print '#                                        #'
print '#              载入语料库                #'
print '#                                        #'
print '#----------------------------------------#\n'
def PreprocessDoc(root):
 
    allDirPath = [] # 存放语料库数据集文件夹下面左右的文件夹路径,string,[1:]为所需
    fileNumList = []
 
    def processDirectory(args, dirname, filenames, fileNum=0):
        allDirPath.append(dirname)
        for filename in filenames:
            fileNum += 1
        fileNumList.append(fileNum)
    os.path.walk(root, processDirectory, None)
    totalFileNum = sum(fileNumList)
    print '总文件数为: ' + str(totalFileNum)
 
    return allDirPath
 
 
print '#----------------------------------------#'
print '#                                        #'
print '#              合成语料文档                #'
print '#                                        #'
print '#----------------------------------------#\n'
 
# 每个文档一行,第一个词是这个文档的类别
 
def SaveDoc(allDirPath, docPath, stopWords):
 
    print '开始合成语料文档:'
 
    category = 1 # 文档的类别
    f = open(docPath,'w') # 把所有的文本都集合在这个文档里
 
    for dirParh in allDirPath[1:]:
 
        for filePath in glob.glob(dirParh + '/*.txt'):
 
            data = open(filePath, 'r').read()
            texts = DeleteStopWords(data, stopWords)
            line = '' # 把这些词缩成一行,第一个位置是文档类别,用空格分开
            for word in texts:
                if word.encode('utf-8') == '\n' or word.encode('utf-8') == 'nbsp' or word.encode('utf-8') == '\r\n':
                    continue
                line += word.encode('utf-8')
                line += ' '
            f.write(line + '\n') # 把这行写进文件
        category += 1 # 扫完一个文件夹,类别+1
 
    return 0 # 生成文档,不用返回值
 
 
print '#----------------------------------------#'
print '#                                        #'
print '#             分词+去停用词               #'
print '#                                        #'
print '#----------------------------------------#\n'
def DeleteStopWords(data, stopWords):
 
    wordList = []
 
    # 先分一下词
    cutWords = jieba.cut(data)
    for item in cutWords:
        if item.encode('utf-8') not in stopWords: # 分词编码要和停用词编码一致
            wordList.append(item)
 
    return wordList
 
 
print '#----------------------------------------#'
print '#                                        #'
print '#                 tf-idf                 #'
print '#                                        #'
print '#----------------------------------------#\n'
def TFIDF(docPath):
 
    print '开始tfidf:'
 
    corpus = [] # 文档语料
 
    # 读取语料,一行语料为一个文档
    lines = open(docPath,'r').readlines()
    for line in lines:
        corpus.append(line.strip()) # strip()前后空格都没了,但是中间空格还保留
 
    # 将文本中的词语转换成词频矩阵,矩阵元素 a[i][j] 表示j词在i类文本下的词频
    vectorizer = CountVectorizer()
 
    # 该类会统计每个词语tfidf权值
    transformer = TfidfTransformer()
 
    # 第一个fit_transform是计算tf-idf 第二个fit_transform是将文本转为词频矩阵
    tfidf = transformer.fit_transform(vectorizer.fit_transform(corpus))
 
    # 获取词袋模型中的所有词语
    word = vectorizer.get_feature_names()
 
    # 将tf-idf矩阵抽取出来,元素w[i][j]表示j词在i类文本中的tf-idf权重
    weight = tfidf.toarray()
    print weight
 
    # # 输出所有词
    # result = open(docPath, 'w')
    # for j in range(len(word)):
    #     result.write(word[j].encode('utf-8') + ' ')
    # result.write('\r\n\r\n')
    #
    # # 输出所有权重
    # for i in range(len(weight)):
    #     for j in range(len(word)):
    #         result.write(str(weight[i][j]) + ' ')
    #     result.write('\r\n\r\n')
    #
    # result.close()
 
    return weight
 
 
print '#----------------------------------------#'
print '#                                        #'
print '#                   PCA                  #'
print '#                                        #'
print '#----------------------------------------#\n'
def PCA(weight, dimension):
 
    from sklearn.decomposition import PCA
 
    print '原有维度: ', len(weight[0])
    print '开始降维:'
 
    pca = PCA(n_components=dimension) # 初始化PCA
    X = pca.fit_transform(weight) # 返回降维后的数据
    print '降维后维度: ', len(X[0])
    print X
 
    return X
 
 
print '#----------------------------------------#'
print '#                                        #'
print '#                 k-means                #'
print '#                                        #'
print '#----------------------------------------#\n'
def kmeans(X, k): # X=weight
 
    from sklearn.cluster import KMeans
 
    print '开始聚类:'
 
    clusterer = KMeans(n_clusters=k, init='k-means++') # 设置聚类模型
 
    # X = clusterer.fit(weight) # 根据文本向量fit
    # print X
    # print clf.cluster_centers_
 
    # 每个样本所属的簇
    y = clusterer.fit_predict(X) # 把weight矩阵扔进去fit一下,输出label
    print y
 
    # i = 1
    # while i <= len(y):
    #     i += 1
 
    # 用来评估簇的个数是否合适,距离约小说明簇分得越好,选取临界点的簇的个数
    # print clf.inertia_
 
    return y
 
 
print '#----------------------------------------#'
print '#                                        #'
print '#                 BIRCH                 #'
print '#                                        #'
print '#----------------------------------------#\n'
def birch(X, k): # 待聚类点阵,聚类个数
 
    from sklearn.cluster import Birch
 
    print '开始聚类:'
 
    clusterer = Birch(n_clusters=k)
 
    y = clusterer.fit_predict(X)
    print '输出聚类结果:'
    print y
 
    return y
 
 
print '#----------------------------------------#'
print '#                                        #'
print '#                轮廓系数                 #'
print '#                                        #'
print '#----------------------------------------#\n'
def Silhouette(X, y):
 
    from sklearn.metrics import silhouette_samples, silhouette_score
 
    print '计算轮廓系数:'
 
    silhouette_avg = silhouette_score(X, y) # 平均轮廓系数
    sample_silhouette_values = silhouette_samples(X, y) # 每个点的轮廓系数
 
    pprint(silhouette_avg)
 
    return silhouette_avg, sample_silhouette_values
 
 
print '#----------------------------------------#'
print '#                                        #'
print '#                  画图                  #'
print '#                                        #'
print '#----------------------------------------#\n'
def Draw(silhouette_avg, sample_silhouette_values, y, k):
 
    import matplotlib.pyplot as plt
    import matplotlib.cm as cm
    import numpy as np
 
    # 创建一个 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
 
        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.show()
 
 
 
 
 
 
if __name__ == "__main__":
 
    root = '/Users/John/Desktop/test'
    stopWords = open('/Users/John/Documents/NLPStudy/stopwords-utf8', 'r').read()
    docPath = '/Users/John/Desktop/test/doc.txt'
    k = 3
 
    allDirPath = PreprocessDoc(root)
    SaveDoc(allDirPath, docPath, stopWords)
 
    weight = TFIDF(docPath)
    X = PCA(weight, dimension=800) # 将原始权重数据降维
    # y = kmeans(X, k) # y=聚类后的类标签
    y = birch(X, k)
    silhouette_avg, sample_silhouette_values = Silhouette(X, y) # 轮廓系数
    Draw(silhouette_avg, sample_silhouette_values, y, k)
 
 
end = time.clock()
print '运行时间: ' + str(end - start)
 
 
 
 
 

 本文结合了以下两篇:Brich聚类算法原理

                                     文本聚类教程

现阶段我刚开始熟悉Brich算法,我接下来会做相应的实现,数据文本为新闻类短文本,实践过后我会对短文做出修改和添加 !

  • 1
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值