Kmean算法:
一、原理简介
二、流程
2.1 Kmeans算法的流程:
1. 随机确定k个初始点作为作为k个簇的质心,即均值向量初始化;
2. 对数据集中的每个点,计算到每个簇质心的距离,将每个点分配到距其最近的质心,并将其分配给该质心所对应的簇;
3.更新每个簇的质心为该簇所包含点的平均值。
为避免运行时间多长,通常会设置一个最大运行轮数或最小调整幅度阈值,二者满足其一,则停止运行。
2.2 伪代码
三、代码实例
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