机器学习算法原理实现——kmeans聚类算法
kmeans算法原理和步骤
K-means是一种常用的聚类方法,它将数据划分为K个相似的簇,其中每个簇的中心为该簇内所有数据点的均值。以下是K-means的基本原理和步骤:
原理: K-means基于一个简单的想法:相似的数据点应该在空间中彼此靠近,并且可以通过计算每个点到各个簇中心的距离来找到这些点的簇标签。
步骤:
-
初始化:首先选择K个数据点作为初始的簇中心。这可以是随机选择,也可以是使用某种启发式方法。
-
分配数据点:对于数据集中的每一个数据点,计算其到K个中心的距离,并将其分配到距离最近的中心所在的簇。
-
更新簇中心:对于每一个簇,计算簇中所有数据点的均值,将均值作为新的簇中心。
-
收敛判断:比较新的簇中心与上一次迭代的簇中心,如果簇中心没有(或只有微小的)变化,算法结束。否则,返回第2步。
-
结束:当簇中心不再变化或达到预定的迭代次数时,算法结束。
需要注意的是,K-means的结果可能会受到初始中心的影响,导致局部最优。为了获得更好的聚类结果,通常会多次运行算法,每次使用不同的初始中心,然后选择最好的结果。
优点:
- 实现简单
- 对于大数据集,算法是高效的
缺点:
- 结果可能会受到初始中心选择的影响,导致局部最优
- 对于簇的形状和大小敏感(例如,对于非凸形状的簇,K-means可能不会很好地工作)
- 需要预先指定K值,这在实际应用中可能不容易确定
为了解决部分缺点,有很多变种和改进的方法,例如K-means++(用于更好的初始化中心)、二分K-means、DBSCAN(不需要预先指定簇的数量,并且可以发现任意形状的簇)等。
因为这个算法比较直观,所以代码如下:
# 导入numpy库 import numpy as np ### 定义欧式距离 def euclidean_distance(x, y): ''' 输入: x:向量x y:向量y 输出: np.sqrt(distance):欧式距离 ''' # 初始化距离 distance = 0 # 遍历并对距离的平方进行累加 for i in range(len(x)): distance += pow((x[i] - y[i]), 2) return np.sqrt(distance) ### 定义质心初始化函数 def centroids_init(X, k): ''' 输入: X:训练样本,NumPy数组 k:质心个数,也是聚类个数 输出: centroids:质心矩阵 ''' # 样本数和特征数 m, n = X.shape # 初始化质心矩阵,大小为质心个数×特征数 centroids = np.zeros((k, n)) # 遍历 for i in range(k): # 每一次循环随机选择一个类中心作为质心向量 centroid = X[np.random.choice(range(m))] # 将质心向量分配给质心矩阵 centroids[i] = centroid return centroids ### 定义样本所属最近质心的索引 def closest_centroid(x, centroids): ''' 输入: x:单个样本实例 centroids:质心矩阵 输出: closest_i: ''' # 初始化最近索引和最近距离 closest_i, closest_dist = 0, float('inf') # 遍历质心矩阵 for i, centroid in enumerate(centroids): # 计算欧式距离 distance = euclidean_distance(x, centroid) # 根据欧式距离判断并选择最近质心的索引 if distance < closest_dist: closest_i = i closest_dist = distance return closest_i ### 分配样本与构建簇 def build_clusters(centroids, k, X): ''' 输入: centroids:质心矩阵 k:质心个数,也是聚类个数 X:训练样本,NumPy数组 输出: clusters:聚类簇 ''' # 初始化簇列表 clusters = [[] for _ in range(k)] # 遍历训练样本 for x_i, x in enumerate(X): # 获取样本所属最近质心的索引 centroid_i = closest_centroid(x, centroids) # 将当前样本添加到所属类簇中 clusters[centroid_i].append(x_i) return clusters ### 计算质心 def calculate_centroids(clusters, k, X): ''' 输入: clusters:上一步的聚类簇 k:质心个数,也是聚类个数 X:训练样本,NumPy数组 输出: centroids:更新后的质心矩阵 ''' # 特征数 n = X.shape[1] # 初始化质心矩阵,大小为质心个数×特征数 centroids = np.zeros((k, n)) # 遍历当前簇 for i, cluster in enumerate(clusters): # 计算每个簇的均值作为新的质心 centroid = np.mean(X[cluster], axis=0) # 将质心向量分配给质心矩阵 centroids[i] = centroid return centroids ### 获取每个样本所属的聚类类别 def get_cluster_labels(clusters, X): ''' 输入: clusters:当前的聚类簇 X:训练样本,NumPy数组 输出: y_pred:预测类别 ''' # 预测结果初始化 y_pred = np.zeros(X.shape[0]) # 遍历聚类簇 for cluster_i, cluster in enumerate(clusters): # 遍历当前簇 for sample_i in cluster: # 为每个样本分配类别簇 y_pred[sample_i] = cluster_i return y_pred ### k均值聚类算法流程封装 def kmeans(X, k, max_iterations): ''' 输入: X:训练样本,NumPy数组 k:质心个数,也是聚类个数 max_iterations:最大迭代次数 输出: 预测类别列表 ''' # 1.初始化质心 centroids = centroids_init(X, k) # 遍历迭代求解 for _ in range(max_iterations): # 2.根据当前质心进行聚类 clusters = build_clusters(centroids, k, X) # 保存当前质心 cur_centroids = centroids # 3.根据聚类结果计算新的质心 centroids = calculate_centroids(clusters, k, X) # 4.设定收敛条件为质心是否发生变化 diff = centroids - cur_centroids # 检查差异diff中是否有任何非零元素。如果所有元素都是0,那么diff.any()将返回False if not diff.any(): break # 返回最终的聚类标签 return get_cluster_labels(clusters, X) # 创建测试数据 # X = np.array([[0,2],[0,0],[1,0],[5,0],[5,2]]) from sklearn.datasets import make_blobs # 生成3类数据,每类50个样本,每个样本2个特征 X, y = make_blobs(n_samples=150, centers=3, n_features=2, random_state=0) # 设定聚类类别为2个,最大迭代次数为10 labels = kmeans(X, 3, 10) # 打印每个样本所属的类别标签 print(labels) import matplotlib.pyplot as plt # 使用k-means聚类的结果绘制散点图 plt.subplot(131) plt.scatter(X[:, 0], X[:, 1], c=labels) plt.title('K-means Clustering Result') # 使用真实的类别标签绘制散点图 plt.subplot(132) plt.scatter(X[:, 0], X[:, 1], c=y) plt.title('True Labels') # 导入KMeans模块 from sklearn.cluster import KMeans # 创建k均值聚类实例并进行数据拟合 kmeans = KMeans(n_clusters=3, random_state=0).fit(X) # 打印拟合标签 print(kmeans.labels_) plt.subplot(133) plt.scatter(X[:, 0], X[:, 1], c=kmeans.labels_) plt.title('Sklearn kmeans Labels') plt.show()
最后绘图如下:
可以看到和sklearn的结果几乎没区别!
还可以优化下:
### 定义欧式距离,不使用numpy def euclidean_distance2(x, y): ''' 输入: x:向量x y:向量y 输出: np.sqrt(distance):欧式距离 ''' # 初始化距离 distance = 0 # 遍历并对距离的平方进行累加 for i in range(len(x)): distance += pow((x[i] - y[i]), 2) return np.sqrt(distance) ### 定义欧式距离,使用numpy def euclidean_distance(x, y): return np.sqrt(np.sum(np.square(x - y))) ### 定义质心初始化函数 def centroids_init(X, k): ''' 输入: X:训练样本,NumPy数组 k:质心个数,也是聚类个数 输出: centroids:质心矩阵 ''' # 样本数和特征数 m, n = X.shape # 初始化质心矩阵,大小为质心个数×特征数 centroids = np.zeros((k, n)) # 遍历 for i in range(k): # 每一次循环随机选择一个类中心作为质心向量 centroid = X[np.random.randint(m)] # 将质心向量分配给质心矩阵 centroids[i] = centroid return centroids ### 定义样本所属最近质心的索引,不用numpy def closest_centroid2(x, centroids): ''' 输入: x:单个样本实例 centroids:质心矩阵 输出: closest_i: ''' # 初始化最近索引和最近距离 closest_i, closest_dist = 0, float('inf') # 遍历质心矩阵 for i, centroid in enumerate(centroids): # 计算欧式距离 distance = euclidean_distance(x, centroid) # 根据欧式距离判断并选择最近质心的索引 if distance < closest_dist: closest_i = i closest_dist = distance return closest_i ### 定义样本所属最近质心的索引,使用numpy def closest_centroid(x, centroids): ''' 输入: x:单个样本实例 centroids:质心矩阵 输出: closest_i: ''' # 计算x与所有质心的欧式距离, axis=1 计算每一行 distances = np.sqrt(np.sum((centroids - x)**2, axis=1)) # 返回最近质心的索引 return np.argmin(distances)