kmeans设置中心_kmeans聚类

Kmean算法:

一、原理简介

d2464c57bfc85a2c2fdc3fd5a2211f25.png
kmeans原理

二、流程

2.1 Kmeans算法的流程:

1. 随机确定k个初始点作为作为k个簇的质心,即均值向量初始化;

2. 对数据集中的每个点,计算到每个簇质心的距离,将每个点分配到距其最近的质心,并将其分配给该质心所对应的簇;

3.更新每个簇的质心为该簇所包含点的平均值。

为避免运行时间多长,通常会设置一个最大运行轮数或最小调整幅度阈值,二者满足其一,则停止运行。

2.2 伪代码

14029af588683ac7d04b748e3cb3f6c9.png
kmean伪代码

三、代码实例

1. 数据

链接:https://pan.baidu.com/s/1X5FtrhhhCzlYC1-Y1jIPfQ

提取码:a9oh

新闻数据的一部分,只为测试代码用。

2. python实现

import os
import math
import random
import operator

# kmean做新闻聚类
K = 5              # 设定类别数量(簇)
iter_num_max = 20  # 最大迭代次数
threshold = 1e-6   # 认为不变动的阈值
wcss = 0.0         # 初始化
new_wcss = 1

# 新闻数据
file_path = './all_NewsData'
label_dict = {'business': 0, 'yule': 1, 'it': 2, 'sports': 3, 'auto': 4}

# word编码字典 {单词:单词编码}
word_encoding = dict()

def load_data(tf_denominator_type="sum"):
    """
    加载新闻数据,并做词频统计(word count)
    :return: doc_list [文档名列表]
             doc_dict 每篇文档对应每个单词编码的词频
                      {文档名:word_freq} => {文档名:{单词编码:单词计数}}
    """
    doc_list = []
    doc_word_freq_dict = dict()
    i = 0  # 循环初始变量
    for doc_names in os.listdir(file_path):
        doc_name = doc_names.split('.')[0]
        doc_list.append(doc_name)
    
        # 用i来展示当前文件读取到哪里了,类似于每读取第10整数倍的文章数时,进行打印输出(进度条的感觉)
        if i % 25 == 0:
            print("The {0} file had been loaded!".format(i), 'file loaded ...')
    
        # 以可读模式逐个打开目录下的文件,并逐行读取文件中的word转化成列表,以每篇文章为单位
        with open(file_path + '/' + doc_names, 'r', encoding='utf-8') as f:
            # word_freq 记录每篇文章中的单词编码及出现次数 {word编码: word计数}
            word_freq = dict()
            words_count_sum = 0  # 文档中单词总数统计
            words_count_max = 0  # 文档中单词出现次数最大值
            # 每行读取并去除空格
            for line in f.readlines():
                words = line.strip().split(' ')
                # print('The line corresponds to words: {0}'.format(words))
            
                for word in words:
                    # 空字符串,此处也可以跳过停用词(stop_list为停用词集合,可百度得到,亦可自行增加)
                    # if len(word.strip()) < 1 or word in stop_list:
                    if len(word.strip()) < 1:
                        continue
                    
                    # word编码的逻辑:
                    # 1. 若word未曾出现在word_encoding编码表中,则刚好以word_encoding长度向上增加,从0开始;
                    # 2. 否则直接进行下一步词频统计
                    if word_encoding.get(word, -1) == -1:
                        word_encoding[word] = len(word_encoding)
                    # word_encoding_id 记录word对应的编码id
                    word_encoding_id = word_encoding[word]
                    # print('doc_name: {2}, word: {0}, word_encoding_id: {1} '.format(word, word_encoding_id, doc_name))
                
                    # 统计每篇文章中的word编码所对应的词频
                    # 第一次出现的word编码计1次,否则计数就+1
                    if word_freq.get(word_encoding_id, -1) == -1:
                        word_freq[word_encoding_id] = 1
                    else:
                        word_freq[word_encoding_id] += 1
                    # print('doc_name: {0}, word_freq: {1}'.format(doc_name, word_freq))
                
                    # 文档中单词出现次数的最大值
                    if word_freq[word_encoding_id] > words_count_max:
                        words_count_max = word_freq[word_encoding_id]
                    # 文档中单词出现次数加和
                    words_count_sum += 1
        
            # 计算tf值(计算占比: 1. 分母为总单词数;2、分母为单词最大词频)
            for word in word_freq.keys():
                if tf_denominator_type == "sum":
                    word_freq[word] /= words_count_sum
                if tf_denominator_type == "max":
                    word_freq[word] /= words_count_max
        
            doc_word_freq_dict[doc_name] = word_freq
        i += 1
    return doc_word_freq_dict, doc_list

def idf(doc_word_freq_dict):
    """
    idf: 统计每个单词的逆文档词频,
         计算idf值(idf = log(文档集的总数量/(文档出现次数+1)))
         (当新单词未出现在word编码时,拉普拉斯平滑消除异常报错)
    :param: doc_word_freq_dict {文档名:{word编码:word计数}}
    :return: word_idf  {word编码: idf值}
    """
    word_idf = {}
    docs_num = len(doc_word_freq_dict)  # 文档集总数量
    # step1: 统计对应每个单词在文档集中出现的文档数量
    for doc in doc_word_freq_dict.keys():
        # print("doc: {0}, doc_dict: {1}".format(doc, doc_dict[doc]))
        for word_id in doc_word_freq_dict[doc].keys():
            # 统计当前文档是否出现word编码,第一次出现计1,否则+1
            if word_idf.get(word_id, -1) == -1:
                word_idf[word_id] = 1
            else:
                word_idf[word_id] += 1
    
    # step2: 计算idf
    for word_id in word_idf.keys():
        word_idf[word_id] = math.log(docs_num/(word_idf[word_id] + 1))
    return word_idf

def doc_tf_idf():
    """
    实现tf*idf,计算每篇文章中对应每个单词的tf-idf值
    :return: doc_word_freq_dict {文档名:{单词: tf-idf值}}
             doc_list 文档名列表
    """
    doc_word_freq_dict, doc_list = load_data()
    word_idf = idf(doc_word_freq_dict)
    
    doc_word_tfidf = dict()
    for doc in doc_list:
        word_tfidf = dict()
        for word_id in doc_word_freq_dict[doc].keys():
            word_tfidf[word_id] = doc_word_freq_dict[doc][word_id] * word_idf[word_id]
            print('doc: {0}, word: {1}, tf: {2}, idf: {3}, tfidf: {4} '.format(doc,
                                                                               word_id,
                                                                               doc_word_freq_dict[doc][word_id],
                                                                               word_idf[word_id],
                                                                               word_tfidf[word_id]))
        # print(word_tfidf)
        doc_word_tfidf[doc] = word_tfidf
        # print(len(doc_word_tfidf))
    print('-----------------------------n我是优雅的分割线!n----------------------------')
    print('doc_list: {0}'.format(doc_list))
    print('word_encoding: {0}'.format(word_encoding))
    print('word_idf: {0}'.format(word_idf))
    print('doc_word_freq_dict: {0}'.format(doc_word_freq_dict))
    print('doc_word_tfidf: {0}'.format(doc_word_tfidf))
    return doc_word_tfidf, doc_list

def init_k(doc_word_tfidf, doc_list, K=K):
    """
    初始化K个中心点:随机选择K个样本点为中心点,每个doc是一条样本
    :param doc_word_tfidf: 样本数据集 {文档名:{word编码:tfidf值}}
    :param doc_list: [文档名列表]
    :param K: 默认簇的数量是10
    :return: center_dict: {中心点的文档索引:{word编码:tfidf值}}
    """
    center_dict = dict()
    k_doc_list = random.sample(doc_list, K)
    i = 0
    for doc_name in k_doc_list:
        center_dict[i] = doc_word_tfidf[doc_name]
        i += 1
    return center_dict

def compute_distance(doc1_dict, doc2_dict):
    """
    计算样本与样本之间的欧式距离(相同word编码对应tfidf差值的平方)
    :param doc1_dict: {word编码:tfidf值}
    :param doc2_dict: {word编码:tfidf值}
    :return: sum: 两个样本之间的欧氏距离
                  (相同word编码对应tfidf差值的平方)
    """
    sum = 0.0
    # 两个文档总共的去重单词数
    words = set(doc1_dict).union(set(doc2_dict))
    for word_id in words:
        d = doc1_dict.get(word_id, 0.0) - doc2_dict.get(word_id, 0.0)
        sum = d * d
    return sum


def compute_center(k_docs_dict, doc_word_tfidf):
    """
    计算第k簇的中心点
    :param k_docs_dict: 属于第K簇的所有docs {第K簇:[所属第k簇的所有文档列表]}
    :param doc_word_tfidf: 样本字典 {文档名:{word编码:tfidf值}}
    :return: tmp_center 中心点(坐标),维度比较多存储到dict
                        {word编码: tfidf值}
    """
    tmp_center = dict()
    
    for doc in k_docs_dict:
        for word_id in doc_word_tfidf[doc].keys():
            if tmp_center.get(word_id, -1) == -1:
                tmp_center[word_id] = doc_word_tfidf[doc][word_id]
            else:
                tmp_center[word_id] += 1
    for wid in tmp_center.keys():
        tmp_center[wid] /= len(k_docs_dict)
    return tmp_center


def all_k_distance(doc_list, doc_word_tfidf, k_center_dict):
    """
    计算所有样本与某一簇样本中心点之间的距离
    :param doc_list:
    :param doc_word_tfidf:
    :param k_center_dict:
    :return:
    """
    sum = 0
    for doc in doc_list:
        sum += compute_distance(doc_word_tfidf[doc], k_center_dict)
    return sum


if __name__ == '__main__':
    # 读取数据,获得每篇文章对应每个单词的tf-idf值
    doc_word_tfidf, doc_list = doc_tf_idf()
    # 初始化K个中心点到样本上
    k_center_dict = init_k(doc_word_tfidf, doc_list, K=K)
    print("original center_dict: {0}".format(k_center_dict))
    # doc_k: 初始化所有doc所属类别,都在k=0的中心点上 {文档名:所属哪个K}
    doc_k = dict(zip(doc_list, [0 for i in range(len(doc_list))]))
    print("doc_k: {0}".format(doc_k))
    
    iter_num = 0    # 迭代次数
    center_mv = 1
    print('Start train!!!')
    # 算法循环终止条件,只要有其中一个条件不满足即跳出循环
    while new_wcss - wcss > threshold and iter_num < iter_num_max and center_mv > threshold:
        # k_doc_dict:{k簇:所属第k簇的文章列表}
        k_docs_dict = dict()
        
        # 对每一个样本计算到所有K中心点的距离
        for doc in doc_list:
            tmp_select_k = dict()
            for k in k_center_dict.keys():
                tmp_select_k[k] = compute_distance(doc_word_tfidf[doc], k_center_dict[k])
            # (k, val) = sorted(tmp_select_k.items(), key=operator.itemgetter(1))[0]
            # doc_k: {文档名: 所属哪个K},根据最小欧氏距离对每一个doc进行更新
            (k, value) = min(tmp_select_k.items(), key=operator.itemgetter(1))
            doc_k[doc] = k
            if k_docs_dict.get(k, -1) == -1:
                k_docs_dict[k] = [doc]
            else:
                k_docs_dict[k].append(doc)
            print("k_docs_dict: {0}".format(k_docs_dict))
            
        # step2:
        center_mv = 0
        wcss = new_wcss
        new_wcss = 0
        for k in k_docs_dict.keys():
            # 重新计算中心点
            tmp_k_center = compute_center(k_docs_dict[k], doc_word_tfidf)
            # 更新中心点坐标
            # tmp_new_k_center = {k: tmp_k_center}
            # 所有中心点移动距离
            center_mv += compute_distance(k_center_dict[k], tmp_k_center)
            # 计算wcss公式
            new_wcss += all_k_distance(doc_list, doc_word_tfidf, tmp_k_center)
            # 更新K簇中心点信息
            k_center_dict[k] = tmp_k_center
        print("iter_num: {0}, k_center_dict: {1}".format(iter_num, k_center_dict))
        iter_num += 1

        # sorted(doc_word_tfidf['1000business'].items(), key=lambda x: x[1], reverse=True)[0:10]
        # sorted(doc_word_tfidf['1000business'].items(), key=operator.itemgetter(1), reverse=True)[0:10]

四、优缺点

优点:算法简单高效

缺点:1. 簇的个数k需要人为确定;

2. 算法受异常值影响较大;

3. kmeans算法容易收敛到局部最小值,而非全局最小值。

五、补充说明

1. 针对k值需要事先确定的情况,可以利用kmeans++算法进行改进。

2. 聚类的目标是在保持簇数目不变的情况下提高簇的质量。有2种可以量化的办法:

1) 合并最近的质心。

计算所有质心之间的距离,然后合并距离最近的两个点来实现。

2)合并两个使得SSE(误差平方和)增幅最小的质心。

合并两个簇然后计算总SSE值,必须在所有可能的两个簇上面重复上述处理过程,直到找到合并最佳的两个簇为止。具有代表的是二分K-均值算法。

二分K-均值算法的伪代码:

将所有点看成一个簇
当簇数目小于k时
        对于每一个簇
                计算总误差
                在给定的簇上面进行k-均值聚类(k=2)
                计算将该簇一分为二的总误差
        选择使得误差最小的那个簇进行划分操作 

另一种做法是选择SSE最大的簇进行划分,直到簇数目达到用户指定的数目为止。

3)二分均值聚类算法(《机器学习实战 Chapter10》)

3.1) kmeans支持函数

from numpy import *

fileName = './testSet.txt'
def loadDataSet(fileName):
    """
    将文本文件导入到一个列表中,文本文件每一行为tab分割的浮点数
    """
    dataMat = []
    fr = open(fileName)
    for line in fr.readlines():
        curLine = line.strip().split('t')
        fltLine = map(float, curLine)  # map all elements to float()
        dataMat.append(fltLine)
    return dataMat

def distEclud(vecA, vecB):
    """
    距离计算函数
    """
    return sqrt(sum(power(vecA - vecB, 2)))  # la.norm(vecA-vecB)

def randCent(dataSet, k):
    """
    构建一个包含k个随机质心的集合。随机质心必须在整个数据集的边界之内,
    通过找到数据集的每一维的最小和最大值来完成。
    """
    n = shape(dataSet)[1]
    centroids = mat(zeros((k, n)))  # create centroid mat
    for j in range(n):  # create random cluster centers, within bounds of each dimension
        minJ = min(dataSet[:, j])
        rangeJ = float(max(dataSet[:, j]) - minJ)
        centroids[:, j] = mat(minJ + rangeJ * random.rand(k, 1))
    return centroids

3.2) kmean函数

def kMeans(dataSet, k, distMeas=distEclud, createCent=randCent):
    m = shape(dataSet)[0]
    clusterAssment = mat(zeros((m, 2)))  # [簇索引值,平方和误差]

    centroids = createCent(dataSet, k)
    clusterChanged = True
    while clusterChanged:
        clusterChanged = False
        
        # 对每个点遍历所有质心,并计算点到质心的距离,寻找最近的质心
        for i in range(m):
            minDist = inf
            minIndex = -1
            for j in range(k):
                distJI = distMeas(centroids[j, :], dataSet[i, :])
                if distJI < minDist:
                    minDist = distJI
                    minIndex = j
            if clusterAssment[i, 0] != minIndex:
                clusterChanged = True
                
            clusterAssment[i, :] = minIndex, minDist ** 2
        print(centroids)
        # 遍历所有质心,并更新质心取值
        for cent in range(k):  # recalculate centroids
            ptsInClust = dataSet[nonzero(clusterAssment[:, 0].A == cent)[0]]
            centroids[cent, :] = mean(ptsInClust, axis=0)
    return centroids, clusterAssment

运行:

if __name__ == '__main__':     
    datMat = mat(loadDataSet(fileName))    
    myCentroids, clusterAssment = kMeans(datMat, 4)

3.3) 二分K均值聚类

def biKmeans(dataSet, k, distMeas=distEclud):

    m = shape(dataSet)[0]
    clusterassment = mat(zeros((m, 2)))
    centroid0 = mean(dataSet, axis=0).tolist()[0]
    centList = [centroid0]  # 质心列表
    
    # 计算初始误差
    for j in range(m):
        clusterassment[j, 1] = distMeas(mat(centroid0), dataSet[j, :]) ** 2
    # 当前簇数目小于k时,遍历所有簇来决定最佳簇划分
    while (len(centList) < k):
        lowestSSE = inf
        
        for i in range(len(centList)):
            # 对每个簇,将该簇中所有点看成一个小的数据集ptsInCurrCluster,输入到函数kMeans中进行处理(k=2)
            ptsInCurrCluster = dataSet[nonzero(clusterassment[:, 0].A == i)[0], :]
            # kMeans会生成2个质心簇,并记录每个簇的误差值
            centroidMat, splitClustAss = kMeans(ptsInCurrCluster, 2, distMeas)
            sseSplit = sum(splitClustAss[:, 1])
            sseNotSplit = sum(clusterassment[nonzero(clusterassment[:, 0].A != i)[0], 1])
            print("sseSplit, and notSplit: ", sseSplit, sseNotSplit)
            
            if (sseSplit + sseNotSplit) < lowestSSE:
                bestCentToSplit = i
                bestNewCents = centroidMat
                bestClustAss = splitClustAss.copy()
                lowestSSE = sseSplit + sseNotSplit
                
        # 更新簇及簇编号
        bestClustAss[nonzero(bestClustAss[:, 0].A == 1)[0], 0] = len(centList)
        bestClustAss[nonzero(bestClustAss[:, 0].A == 0)[0], 0] = bestCentToSplit
        print('the bestCentToSplit is: ', bestCentToSplit)
        print('the len of bestClustAss is: ', len(bestClustAss))
        
        centList[bestCentToSplit] = bestNewCents[0, :].tolist()[0]
        centList.append(bestNewCents[1, :].tolist()[0])
        clusterassment[nonzero(clusterassment[:, 0].A == bestCentToSplit)[0], :] = bestClustAss
    return mat(centList), clusterassment

运行:

if __name__ == '__main__':
    datMat2 = mat(loadDataSet('testSet2.txt'))
    centList, NewclusterAssment = biKmeans(datMat2, 3)
    print(centList)

附赠学习大礼包

朱倩:学习大礼包​zhuanlan.zhihu.com
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值