K-Means和Fuzzy C-Means聚类算法原理以及python代码实现
1.K-Means聚类
1、原理
K-Means算法原理
网上有很多关于K-Means算法的原理,当然通过阅读《统计学习方法》也可以知道K-Means聚类的原理,我这个比较懒,在这里就不打出详细的原理了,具体的可以参考上面的链接,写的很详细。
简单来说就是类似EM迭代的过程:
(1)对于每个样本,将每个样本分配到最近的簇中;
(2)对于每个簇,重新求该簇的中心;
重复迭代步骤(1)和(2),直到损失函数收敛。
值得一提的是,我在代码实现的环节有采用K-Means算法的改进:K-Means++算法,该算法与K-Means算法最大的不同就是在初始聚类中心矩阵的构造上面,其他完全相同,详细原理见上面的链接。
2、python实现
在项目中需要用java实现聚类算法,因此不能直接简单的在sklearn中调个包然后就完事了,因此我用python写了K-Means聚类算法,方便阅读并转成java。搜索发现网上大部分的代码都是以《机器学习实战》这本书的代码为框架,但是使用了太多的for循环导致程序运行的效率低下,python的numpy库给矩阵运算提供了很大的帮助,通过合理利用numpy库可以使代码简洁且运行效率高。
下面的代码主要参考sklearn库下的K-Means框架完成,并进行了一些修改,容易读懂。
import numpy as np
def _EuclideanDistances(sample, centroids):
"""
计算样本和聚类中心的欧几里得距离,通过公式(a-b)^2 = a^2 + b^2 - 2ab
参数
----------
sample : array, size(n, p)
用户样本矩阵
centroids : array, size(k, p)
聚类中心矩阵(如果输入的是1d array在输入前用reshape转换成二维数组)
返回
----------
ED : array, size(n, k)
样本与聚类中心的距离矩阵
"""
if centroids.ndim == 1:
centroids = centroids.reshape(1, len(centroids))
vecProd = np.dot(sample, centroids.T)
SqA = sample**2
sumSqA = np.sum(SqA, axis=1, keepdims=True)
sumSqAEx = np.tile(sumSqA, (1, vecProd.shape[1])) # 将(n, 1)的向量sumSqA拓展成(n, k)的矩阵
SqB = centroids**2
sumSqB = np.sum(SqB, axis=1).reshape(1, -1)
sumSqBEx = np.tile(sumSqB, (vecProd.shape[0], 1)) # 将(1, k)的向量sumSqB扩展成(n, k)的矩阵
SqED = sumSqBEx + sumSqAEx - 2*vecProd
SqED[SqED<0]=0.0
ED = np.sqrt(SqED)
return ED
def _initialize_center(X, k, init_state='K_Means++'):
"""
初始化聚类中心矩阵
参数
----------
X : array, size(n, p)
用户样本矩阵
k : int
簇个数
init_state : string ['K_Means++', 'random']
选择初始化聚类中心矩阵的方式
返回
----------
C : 2d array, size(k, p)
初始化后的聚类中心矩阵
label : 1d array, size(n, )
初始化矩阵下的样本标签
D: 2d array, size(n, k)
初始化后的样本点与聚类中心的欧几里得距离矩阵
"""
n, p = X.shape
if init_state == 'random':
index = random.sample([i for i in range(n)], k)
C = X[index]
D = _EuclideanDistances(X, C)
label = np.argmin(D, axis=1)
elif init_state == 'K_Means++':
# 初始化聚类中心矩阵
C = np.zeros((k, p))
# index向量
index_vec = [i for i in range(n)]
# 随机选一个点作为第一个聚类中心
index0 = np.random.randint(0, n)
C[0] = X[index0]
# 求出欧几里得距离矩阵
D = _EuclideanDistances(X, C[0])
for i in range(2, k+1):
d_min = np.min(D, axis=1)
prob = d_min / np.sum(d_min)
index = np.random.choice(index_vec, size=1, p=prob)[0]
C[i-1] = X[index]
if i == k:
D = _EuclideanDistances(X, C)
break
# 求出欧几里得距离矩阵
D = _EuclideanDistances(X, C[:i])
label = np.argmin(D, axis=1)
return C, label, D
def K_Means(X, k=2, error=0.001, T=100, random_state=None, init_state='K_Means++'):
"""
模糊C均值聚类算法
参数
----------
X : 2d array, size (n, p)
用户样本矩阵. n是用户样本个数; p是特征维度
k : int
簇的个数
error : float
停止条件; J - J_old <= error
T : int
迭代次数
random_state : int
随机观测种子,如果提供了则观测结果可固定,否则随机
init_state : string ['K_Means++', 'random']
选择初始化聚类中心矩阵的方式
返回
-------
C : 2d array, size (k, p)
聚类中心矩阵
label : 1d array, size(n, )
样本标签向量
"""
if random_state:
np.random.seed(seed=random_state)
n, p = X.shape
C, label, D = _initialize_center(X, k, init_state)
J = np.sum(np.min(D**2, axis=1))
for t in range(T):
J_old = J
# 重新计算簇中心
for i in range(k):
index = np.where(label==i)[0]
C[i] = X[index].mean(axis=0)
D = _EuclideanDistances(X, C)
J = np.sum(np.min(D**2, axis=1))
if np.abs(J - J_old) < error:
print('迭代次数为:', t+1)
break
label = np.argmin(D, axis=1)
label = np.argmin(D, axis=1)
return C, label
正常来说K-Means聚类是需要对数据预处理的,包括去除异常值和归一化等,由于不同人的数据集不同,因此我把预处理的代码删掉了,留下了K-Means聚类的核心代码。
本文测试的数据集来自于https://blog.csdn.net/weixin_41857483/article/details/109136568
感谢大佬无私提供数据集
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 = list(map(float, curLine))
dataSet.append(fltLine)
return dataSet
test = np.array(load_data_set('testSet.txt'))
C, label = K_Means(test, k=4, error=0.001, T=100, random_state=2020)
import matplotlib.pyplot as plt
label_map = {
0: 'r', 1:'b', 2:'g', 3:'y'}
label_color = list(map(lambda x: label_map[x], label))
plt.scatter(x=test[:, 0], y=test[:, 1], c=label_color)
2.Fuzzy C-Means聚类
1、原理
Fuzzy C-Means聚类原理
Fuzzy C-Means聚类原理
Fuzzy C-Means聚类原理主要参考这两篇博客,写的很详细了,补充一点就是,里面的m是模糊化因子,是一个超参数,基本取2,同样的,Fuzzy C-Means聚类也是一个迭代算法。
2、python实现
import numpy as np
def _initialize_U0(numbers, clusters):
"""
初始化隶属度矩阵U0
参数
----------
numbers : int
用户样本的个数
clusters : int
簇的个数
返回
----------
U0 : array, size(numbers, clusters)
初始化后的隶属矩阵
"""
U = np.random.rand(numbers, clusters)
U0 = U / np.sum(U, axis=1, keepdims=True)
return U0
def _EuclideanDistances(sample, centroids):
"""
计算样本和聚类中心的欧几里得距离,通过公式(a-b)^2 = a^2 + b^2 - 2ab
参数
----------
sample : 2d array, size(n, p)
用户样本矩阵
centroids : 2d array, size(k, p)
聚类中心矩阵(如果输入的是1d array在输入前用reshape转换成二维数组)
返回
----------
ED : array, size(n, k)
样本与聚类中心的距离矩阵
"""
if centroids.ndim == 1:
centroids = centroids.reshape(1, len(centroids))
vecProd = np.dot(sample, centroids.T)
SqA = sample**2
sumSqA = np.sum(SqA, axis=1, keepdims=True)
sumSqAEx = np.tile(sumSqA, (1, vecProd.shape[1])) # 将(n, 1)的向量sumSqA拓展成(n, k)的矩阵
SqB = centroids**2
sumSqB = np.sum(SqB, axis=1).reshape(1, -1)
sumSqBEx = np.tile(sumSqB, (vecProd.shape[0], 1)) # 将(1, k)的向量sumSqB扩展成(n, k)的矩阵
SqED = sumSqBEx + sumSqAEx - 2*vecProd
SqED[SqED<0]=0.0
ED = np.sqrt(SqED)
return ED
def C_Means(X, k=5, m=2, error=0.001, T=1000, random_state=False):
"""
模糊C均值聚类算法
参数
----------
X : 2d array, size (n, p)
用户样本矩阵. n是用户样本个数; p是特征维度
k : int
簇的个数
m : float
模糊化因子,是一个超参数
error : float
停止条件; np.max(np.abs(U - U_old)) < error
T : int
迭代次数
random_state : int
随机观测种子,如果提供了则观测结果可固定,否则随机
返回
-------
C : 2d array, size (k, p)
聚类中心矩阵
U : 2d array, (n, k)
隶属度矩阵
"""
if random_state:
np.random.seed(seed=random_state)
n = X.shape[0] # 用户个数
p = X.shape[1] # 特征维度
U = _initialize_U0(n, k)
for t in range(T):
# 拷贝U用于停止迭代的判断
U_old = U.copy()
# 计算聚类中心矩阵
C = np.dot((U**m).T, X) / np.sum(U**2, axis=0, keepdims=True).T
# 更新隶属度矩阵U
d = _EuclideanDistances(X, C) # 计算X与C的欧几里得距离矩阵
d = d ** (2/(m-1))
U = (d * (1/d).sum(axis=1).reshape(n, 1))
U = 1 / U
if np.max(np.abs(U - U_old)) <= error:
print('迭代次数为: ',t+1)
break
return C, U
由于输出的U是一个隶属度矩阵,只说明样本归属于第i个簇的概率有多大,因此验证时将概率最大的作为输出标签。
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 = list(map(float, curLine))
dataSet.append(fltLine)
return dataSet
test = np.array(load_data_set('testSet.txt'))
C, U = C_Means(test, k=4, error=0.001, T=100, random_state=2020)
label = np.argmax(U, axis=1)
import matplotlib.pyplot as plt
label_map = {
0: 'r', 1:'b', 2:'g', 3:'y'}
label_color = list(map(lambda x: label_map[x], label))
plt.scatter(x=test[:, 0], y=test[:, 1], c=label_color)
如果有什么错误欢迎各位大佬指出,也欢迎大家在评论区讨论。