0. 前言
上一篇博客中,通过调用机器学习库sklearn实现了K-means算法;由于都被封装好了,虽然对算法思想清楚,但是内部实现的过程还需要拆开来自己实现以下可能才比较清楚,所以下面不通过调用本来的库来实现该算法。
1. python实现K-means
- 加载数据集
首先,我们需要准备一个数据集。这里我们从文件加载数据集,此处附上该文件的网盘下载地址:testSet数据集 提取码:4pg1
load_data_set()函数,实现将文本文件导入到一个列表中。
def load_data_set(fileName):
"""加载数据集"""
dataSet = [] # 初始化一个空列表
fr = open(fileName)
for line in fr.readlines():
# 按tab分割字段,将每行元素分割为list的元素
curLine = line.strip().split('\t')
# 其中map(float, curLine)表示把列表的每个值用float函数转成float型,并返回迭代器
fltLine = map(float, curLine)
dataSet.append(fltLine)
return dataSet
文本文件的每一行为tab分隔的浮点数,每一个列表会被添加到dataSet中,最后返回dataSet。这个返回值是一个包含许多其他列表的列表。
- 计算距离
此处实现计算两个向量的欧氏距离
def distance_euclidean(vector1, vector2):
"""计算欧氏距离"""
return sqrt(sum(power(vector1-vector2, 2))) # 返回两个向量的距离
- 构建簇质心
为给定的数据集构建一个包含k个随机质心的集合。随机质心要在整个数据集的边界内,可以通过找到数据集每一维的最小和最大值来完成。然后生成0到1.0之间的随机数并通过取值范围和最小值,确保随机点在数据的边界之内。
def rand_center(dataSet, k):
"""构建一个包含K个随机质心的集合"""
n = shape(dataSet)[1] # 获取样本特征值
# 初始化质心,创建(k,n)个以0填充的矩阵
centroids = mat(zeros((k, n))) # 每个质心有n个坐标值,总共要k个质心
# 遍历特征值
for j in range(n):
# 计算每一列的最小值
minJ = min(dataSet[:, j])
# 计算每一列的范围值
rangeJ = float(max(dataSet[:, j]) - minJ)
# 计算每一列的质心,并将其赋给centroids
centroids[:, j] = minJ + rangeJ * random.rand(k, 1)
return centroids # 返回质心
先验证一下rand_center()函数能否正常运行,结合以上3部分代码:
在验证过程中,出现了一个错误:
TypeError: unsupported operand type(s) for -: ‘map’ and ‘map’
解决办法:将第一个加载数据集函数的fltLine = map(float, curLine)更改为fltLine = list(map(float, curLine))。因为在rangeJ = float(max(dataSet[:, j]) - minJ)
是2个map类型的数据在相减,fltLine = map(float, curLine)
在python2返回的是list型数据,在python3返回map型数据。
更改后的3部分总的代码:
from numpy import *
def load_data_set(fileName):
"""加载数据集"""
dataSet = [] # 初始化一个空列表
fr = open(fileName)
for line in fr.readlines():
# 按tab分割字段,将每行元素分割为list的元素
curLine = line.strip().split('\t')
# 用list函数把map函数返回的迭代器遍历展开成一个列表
# 其中map(float, curLine)表示把列表的每个值用float函数转成float型,并返回迭代器
fltLine = list(map(float, curLine))
dataSet.append(fltLine)
return dataSet
def distance_euclidean(vector1, vector2):
"""计算欧氏距离"""
return sqrt(sum(power(vector1-vector2, 2))) # 返回两个向量的距离
def rand_center(dataSet, k):
"""构建一个包含K个随机质心的集合"""
n = shape(dataSet)[1] # 获取样本特征值
# 初始化质心,创建(k,n)个以0填充的矩阵
centroids = mat(zeros((k, n))) # 每个质心有n个坐标值,总共要k个质心
# 遍历特征值
for j in range(n):
# 计算每一列的最小值
minJ = min(dataSet[:, j])
# 计算每一列的范围值
rangeJ = float(max(dataSet[:, j]) - minJ)
# 计算每一列的质心,并将其赋给centroids
centroids[:, j] = minJ + rangeJ * random.rand(k, 1)
return centroids # 返回质心
datMat = mat(load_data_set('testSet.txt'))
print(datMat.shape) # 80个样本,二维数据集
print("第一列最小:", min(datMat[:, 0])) # 取所有集合第0个数据中的最小,即第一维中的最小
print("第二列最小:", min(datMat[:, 1]))
print("第一列最大:", max(datMat[:, 0]))
print("第二列最大:", max(datMat[:, 1]))
# 随机生成4个min-max之间的值
print(rand_center(datMat, 4))
- K-means聚类算法
k_means()函数接收4个输入参数,其中数据集和簇的数目是必选参数。该函数最开始要确定数据集中数据点的总数,接着创建一个矩阵来存储每个点的簇分配结果。簇分配结果矩阵clusterAssment包含两列:一列记录簇索引值,一列存储误差;即第一列存放该数据所属中心点,第二列是该数据到中心点的距离。再“计算质心-分配-重新计算”反复迭代,直至所有数据点的簇分配结果不再改变。
def k_means(dataSet,k,distMeas = distance_euclidean,creatCent = rand_center):
"""K-means聚类算法"""
m = shape(dataSet)[0] # 行数
# 建立簇分配结果矩阵,第一列存放该数据所属中心点,第二列是该数据到中心点的距离
clusterAssment = mat(zeros((m, 2)))
centroids = creatCent(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:
# 如果第i个数据点到第j中心点更近,则将i归属为j
minDist = distJI
minIndex = j
# 如果分配发生变化,则需要继续迭代
if clusterAssment[i,0] != minIndex:
clusterChanged = True
# 并将第i个数据点的分配情况存入字典
clusterAssment[i,:] = minIndex,minDist**2
print(centroids)
for cent in range(k): # 重新计算中心点
# 去第一列等于cent的所有列
ptsInClust = dataSet[nonzero(clusterAssment[:, 0].A == cent)[0]]
# 算出这些数据的中心点
centroids[cent, :] = mean(ptsInClust, axis=0)
return centroids, clusterAssment
- 完整代码
from numpy import *
def load_data_set(fileName):
"""加载数据集"""
dataSet = [] # 初始化一个空列表
fr = open(fileName)
for line in fr.readlines():
# 按tab分割字段,将每行元素分割为list的元素
curLine = line.strip().split('\t')
# 用list函数把map函数返回的迭代器遍历展开成一个列表
# 其中map(float, curLine)表示把列表的每个值用float函数转成float型,并返回迭代器
fltLine = list(map(float, curLine))
dataSet.append(fltLine)
return dataSet
def distance_euclidean(vector1, vector2):
"""计算欧氏距离"""
return sqrt(sum(power(vector1-vector2, 2))) # 返回两个向量的距离
def rand_center(dataSet, k):
"""构建一个包含K个随机质心的集合"""
n = shape(dataSet)[1] # 获取样本特征值
# 初始化质心,创建(k,n)个以0填充的矩阵
centroids = mat(zeros((k, n))) # 每个质心有n个坐标值,总共要k个质心
# 遍历特征值
for j in range(n):
# 计算每一列的最小值
minJ = min(dataSet[:, j])
# 计算每一列的范围值
rangeJ = float(max(dataSet[:, j]) - minJ)
# 计算每一列的质心,并将其赋给centroids
centroids[:, j] = minJ + rangeJ * random.rand(k, 1)
return centroids # 返回质心
def k_means(dataSet,k,distMeas = distance_euclidean,creatCent = rand_center):
"""K-means聚类算法"""
m = shape(dataSet)[0] # 行数
# 建立簇分配结果矩阵,第一列存放该数据所属中心点,第二列是该数据到中心点的距离
clusterAssment = mat(zeros((m, 2)))
centroids = creatCent(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:
# 如果第i个数据点到第j中心点更近,则将i归属为j
minDist = distJI
minIndex = j
# 如果分配发生变化,则需要继续迭代
if clusterAssment[i,0] != minIndex:
clusterChanged = True
# 并将第i个数据点的分配情况存入字典
clusterAssment[i,:] = minIndex,minDist**2
print(centroids)
for cent in range(k): # 重新计算中心点
# 去第一列等于cent的所有列
ptsInClust = dataSet[nonzero(clusterAssment[:, 0].A == cent)[0]]
# 算出这些数据的中心点
centroids[cent, :] = mean(ptsInClust, axis=0)
return centroids, clusterAssment
datMat = mat(load_data_set('testSet.txt'))
myCentroids, clusterAssing = k_means(datMat, 4)
print(myCentroids)
print(clusterAssing)
运行结果:
2. 图形化展示
from numpy import *
from matplotlib import pyplot as plt
def load_data_set(fileName):
"""加载数据集"""
dataSet = [] # 初始化一个空列表
fr = open(fileName)
for line in fr.readlines():
# 按tab分割字段,将每行元素分割为list的元素
curLine = line.strip().split('\t')
# 用list函数把map函数返回的迭代器遍历展开成一个列表
# 其中map(float, curLine)表示把列表的每个值用float函数转成float型,并返回迭代器
fltLine = list(map(float, curLine))
dataSet.append(fltLine)
return dataSet
def distance_euclidean(vector1, vector2):
"""计算欧氏距离"""
return sqrt(sum(power(vector1-vector2, 2))) # 返回两个向量的距离
def rand_center(dataSet, k):
"""构建一个包含K个随机质心的集合"""
n = shape(dataSet)[1] # 获取样本特征值
# 初始化质心,创建(k,n)个以0填充的矩阵
centroids = mat(zeros((k, n))) # 每个质心有n个坐标值,总共要k个质心
# 遍历特征值
for j in range(n):
# 计算每一列的最小值
minJ = min(dataSet[:, j])
# 计算每一列的范围值
rangeJ = float(max(dataSet[:, j]) - minJ)
# 计算每一列的质心,并将其赋给centroids
centroids[:, j] = minJ + rangeJ * random.rand(k, 1)
return centroids # 返回质心
def k_means(dataSet,k,distMeas = distance_euclidean,creatCent = rand_center):
"""K-means聚类算法"""
m = shape(dataSet)[0] # 行数
# 建立簇分配结果矩阵,第一列存放该数据所属中心点,第二列是该数据到中心点的距离
clusterAssment = mat(zeros((m, 2)))
centroids = creatCent(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:
# 如果第i个数据点到第j中心点更近,则将i归属为j
minDist = distJI
minIndex = j
# 如果分配发生变化,则需要继续迭代
if clusterAssment[i,0] != minIndex:
clusterChanged = True
# 并将第i个数据点的分配情况存入字典
clusterAssment[i,:] = minIndex,minDist**2
print(centroids)
for cent in range(k): # 重新计算中心点
# 去第一列等于cent的所有列
ptsInClust = dataSet[nonzero(clusterAssment[:, 0].A == cent)[0]]
# 算出这些数据的中心点
centroids[cent, :] = mean(ptsInClust, axis=0)
return centroids, clusterAssment
# datMat = mat(load_data_set('testSet.txt'))
# myCentroids, clusterAssing = k_means(datMat, 4)
# print(myCentroids)
# print(clusterAssing)
datMat = mat(load_data_set('testSet.txt'))
myCentroids, clusterAssing = k_means(datMat, 4)
plt.scatter(array(datMat)[:, 0], array(datMat)[:, 1], c=array(clusterAssing)[:, 0].T)
plt.scatter(myCentroids[:, 0].tolist(), myCentroids[:, 1].tolist(), c="r")
plt.show()
效果图: