简介:个人学习分享,如有错误,欢迎批评指正。
一、什么是K-means算法?
K-means算法是一种无监督的聚类算法,用于将一组数据点分为K个簇(cluster)。其核心目标是将数据点划分到K个不同的簇中,使得每个簇内的数据点之间的相似性最大化,而不同簇之间的相似性最小化
。
具体而言,K-means算法通过以下方式实现聚类:
- 簇中心(质心):每个簇都有一个中心点,称为质心(centroid)。质心通常是该簇中所有数据点的平均值。
- 距离度量:数据点与质心之间的距离通常使用欧几里得距离度量。每个数据点被分配到与其最近的质心所在的簇中。
- 目标函数:K-means算法的目标是最小化所有数据点到其所属簇质心的距离平方和,这个值被称为簇内平方和误差(Within-Cluster Sum of Squares, WCSS)。
通过反复更新数据点的分配和质心的位置,K-means算法不断优化簇的划分,最终得到K个簇,使得簇内数据点之间更加相似,簇间差异更大。
牧师-村民模型
- 有四个牧师去郊区布道,一开始牧师们随意选了几个布道点,并且把这几个布道点的情况公告给了郊区所有的村民,于是每个村民到离自己家最近的布道点去听课。
听课之后,大家觉得距离太远了,于是每个牧师统计了一下自己的课上所有的村民的地址,搬到了所有地址的中心地带,并且在海报上更新了自己的布道点的位置。
牧师每一次移动不可能离所有人都更近,有的人发现A牧师移动以后自己还不如去B牧师处听课更近,于是每个村民又去了离自己最近的布道点……
就这样,牧师每个礼拜更新自己的位置,村民根据自己的情况选择布道点,最终稳定了下来。
我们可以看到该牧师的目的是为了让每个村民到其最近中心点的距离和最小。
二、K-近邻算法流程
1.初始化
- 确定K值:设定簇的数量K,这个值通常需要根据数据的特性或通过经验确定。
- 随机选择初始质心:从数据集中随机选择K个点作为初始质心,也可以通过如K-means++等方法选择更好的初始质心,以提高聚类效果。
2.分配数据点到最近的质心
- 计算距离:对于数据集中每个数据点,计算它与K个质心之间的距离。通常使用欧几里得距离计算,即:
d ( x i , μ j ) = ∑ m = 1 M ( x i m − μ j m ) 2 d(x_i, \mu_j) = \sqrt{\sum_{m=1}^{M}(x_{im} - \mu_{jm})^2} d(xi,μj)=m=1∑M(xim−μjm)2
其中, x i x_i xi 是数据点, μ j \mu_j μj 是第 j j j 个质心, M M M 是数据点的维度。
- 分配数据点:将每个数据点分配到距离其最近的质心所属的簇。
3.更新质心
- 计算新的质心:对于每一个簇,计算其所有数据点的平均值,并将该平均值作为新的质心。计算公式为:
μ j = 1 ∣ C j ∣ ∑ x i ∈ C j x i \mu_j = \frac{1}{|C_j|} \sum_{x_i \in C_j} x_i μj=∣Cj∣1xi∈Cj∑xi
其中, C j C_j Cj 是第 j j j 个簇中的所有数据点, μ j \mu_j μj 是该簇的质心。
4. 迭代
重复步骤2和3:继续将数据点分配到最近的质心,然后更新质心的位置。这个过程不断迭代,直到质心的位置不再发生显著变化,或者达到预定的迭代次数。
5. 收敛
收敛判断:当质心的位置在两次迭代间变化非常小时,或者所有数据点的分配不再变化,算法停止迭代,认为已经收敛。
6. 输出结果
输出簇和质心:算法最终输出每个数据点的簇标签,以及每个簇的质心位置。
伪代码
获取数据 n 个 m 维的数据
随机生成 K 个 m 维的点
while(t)
for(int i=0;i < n;i++)
for(int j=0;j < k;j++)
计算点 i 到类 j 的距离
for(int i=0;i < k;i++)
1. 找出所有属于自己这一类的所有数据点
2. 把自己的坐标修改为这些数据点的中心点坐标
end
时间复杂度:
O
(
t
k
n
m
)
O(tknm)
O(tknm) ,其中,t 为迭代次数,k 为簇的数目,n 为样本点数,m 为样本点维度。
空间复杂度:
O
(
m
(
n
+
k
)
O(m(n+k)
O(m(n+k) ,其中,k 为簇的数目,m 为样本点维度,n 为样本点数。
三、K-means算法的损失函数和收敛问题
1. 损失函数
K-means算法的目标是将数据点划分为K个簇,使得每个簇内的数据点到该簇中心(质心)的距离之和最小。这个优化目标可以通过一个损失函数来表示,该损失函数通常称为“簇内平方和误差
”(Within-Cluster Sum of Squares, WCSS),也可以称为“惯性”(Inertia)。
损失函数的定义
对于给定的K个簇 { C 1 , C 2 , … , C K } \{C_1, C_2, \ldots, C_K\} {C1,C2,…,CK},每个簇的质心为 μ k \mu_k μk,K-means算法的损失函数 J J J 定义为:
J = ∑ k = 1 K ∑ x i ∈ C k ∥ x i − μ k ∥ 2 J = \sum_{k=1}^{K} \sum_{x_i \in C_k} \|x_i - \mu_k\|^2 J=k=1∑Kxi∈Ck∑∥xi−μk∥2
其中:
- x i x_i xi 是数据集中的第 i i i 个数据点。
- μ k \mu_k μk 是簇 C k C_k Ck 的质心,等于该簇中所有数据点的均值。
- ∥ x i − μ k ∥ \|x_i - \mu_k\| ∥xi−μk∥ 是数据点 x i x_i xi 到簇中心 μ k \mu_k μk 的欧几里得距离。
损失函数的意义
这个损失函数衡量了数据点到其所属簇中心的距离的平方和,目标是最小化这个值,使得每个簇内的点尽可能地接近其簇中心
。损失函数的最小化意味着簇内数据点的紧密性最大化,从而实现有效的聚类。
2. 收敛问题
K-means算法通过迭代来最小化损失函数,收敛性是指算法最终会停止在一个局部最优点,即簇中心和数据点的分配不再发生显著变化。K-means算法的收敛性是保证其能在有限次迭代后终止的关键。
收敛的过程
K-means算法的迭代过程可以总结为以下步骤:
- 初始化:随机选择K个点作为初始质心。
- 分配数据点:将每个数据点分配到最近的质心所属的簇。
- 更新质心:计算每个簇的新的质心(即所有簇内数据点的均值)。
- 检查收敛:如果质心的位置不再发生变化,或者变化小于设定的阈值,则认为算法收敛。
K-means算法的收敛性主要体现在以下几个方面:
- 有限次迭代收敛:由于每次迭代都会减少或保持损失函数的值,且可能的簇分配组合有限,K-means算法在有限次迭代后必定会收敛。
- 局部最优解:由于K-means是基于贪心策略的算法,它可能会收敛到局部最优解,而非全局最优解。最终的簇分配可能依赖于初始质心的选择。
- 初始值敏感性:K-means对初始质心的位置非常敏感。不同的初始化可能导致算法收敛到不同的局部最优点。因此,常常使用如K-means++的方法来改善初始质心选择,增加算法收敛到全局最优解的可能性。
- 停机条件:K-means通常设置两种停机条件:质心的变化小于某个阈值或达到最大迭代次数。两者之一满足时,算法停止。
数学解释收敛性
从数学角度看,K-means算法每次迭代都是在最小化一个分段线性但非连续的损失函数。由于每次迭代都会减少或保持损失函数的值,并且损失函数的取值是有限的,因此K-means算法在有限次迭代后必定会收敛
。每次迭代后,新的簇分配和质心计算结果会维持或减小损失函数的值,直到算法停止变化,达成局部收敛。
四、k-means算法的调优与改进
K-means算法虽然简单有效,但也存在一些局限性。为了提高其性能,研究者和实践者提出了多种调优与改进方法。以下是常见的K-means算法的调优与改进策略:
1. 初始质心选择的改进
1.1.K-means++
K-means++是对传统K-means算法的一种改进,旨在解决K-means对初始质心选择敏感的问题。传统的K-means算法随机选择初始质心,这可能导致糟糕的聚类效果,尤其是当初始质心选择不佳时,可能会陷入局部最优解。K-means++通过更合理的初始质心选择策略,显著提高了K-means算法的收敛速度和聚类效果。
K-means++的核心思想是通过一种概率方法选择初始质心,使得质心能够更好地覆盖数据分布
。它通过以下步骤来选择初始质心:
-
随机选择第一个质心:
- 从数据集中随机选择一个点作为第一个质心。
-
为后续质心选择分配概率:
- 对于数据集中每个剩余的点,计算其到最近已选择质心的距离平方,记为 D ( x ) 2 D(x)^2 D(x)2。
- 通过距离平方 D ( x ) 2 D(x)^2 D(x)2 的值来决定点被选为下一个质心的概率。距离较远的点(与已选择的质心)有更大的概率被选为下一个质心。
-
选择下一个质心:
- 根据步骤2计算出的概率分布,随机选择一个点作为下一个质心。
-
重复:
- 重复步骤2和3,直到选出 K K K 个质心为止。
-
执行标准K-means算法:
- 使用K-means++选出的初始质心,执行标准的K-means算法,进行数据点的分配和质心的更新,直到算法收敛。
K-means++的Python代码实现
- 调包实现(使用scikit-learn库)
import numpy as np
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt
# 生成随机数据
X = np.random.rand(100, 2)
# 使用K-means++初始化的K-means模型
kmeans = KMeans(n_clusters=3, init='k-means++', random_state=0)
# 进行聚类
kmeans.fit(X)
# 获取聚类结果
labels = kmeans.labels_
centers = kmeans.cluster_centers_
# 可视化聚类结果
plt.scatter(X[:, 0], X[:, 1], c=labels, cmap='viridis')
plt.scatter(centers[:, 0], centers[:, 1], c='red', marker='x')
plt.title('K-means Clustering with K-means++ Initialization')
plt.show()
- 不调包实现
以下是K-means++算法的手动实现版本:
import numpy as np
import matplotlib.pyplot as plt
def initialize_centroids_kmeans_plus_plus(X, k):
# 随机选择第一个质心
centroids = [X[np.random.choice(X.shape[0])]]
for _ in range(1, k):
# 计算每个点到最近质心的距离平方
distances = np.array([min(np.sum((x - c) ** 2) for c in centroids) for x in X])
# 选择概率与距离平方成正比的点作为下一个质心
probabilities = distances / distances.sum()
cumulative_probabilities = probabilities.cumsum()
r = np.random.rand()
for i, p in enumerate(cumulative_probabilities):
if r < p:
centroids.append(X[i])
break
return np.array(centroids)
def assign_clusters(X, centroids):
distances = np.sqrt(((X - centroids[:, np.newaxis]) ** 2).sum(axis=2))
return np.argmin(distances, axis=0)
def update_centroids(X, labels, k):
return np.array([X[labels == i].mean(axis=0) for i in range(k)])
def kmeans(X, k, max_iters=100):
centroids = initialize_centroids_kmeans_plus_plus(X, k)
for _ in range(max_iters):
labels = assign_clusters(X, centroids)
new_centroids = update_centroids(X, labels, k)
if np.all(centroids == new_centroids):
break
centroids = new_centroids
return labels, centroids
# 生成随机数据
X = np.random.rand(100, 2)
# 调用自定义的k-means++算法
k = 3
labels, centroids = kmeans(X, k)
# 可视化聚类结果
plt.scatter(X[:, 0], X[:, 1], c=labels, cmap='viridis')
plt.scatter(centroids[:, 0], centroids[:, 1], c='red', marker='x')
plt.title('K-means Clustering with K-means++ Initialization (Without Using Libraries)')
plt.show()
K-means++的优势
- 更好的初始质心:通过使用距离平方的概率分布,K-means++能够选择更好的初始质心,这些质心更有可能位于数据的密集区域,从而避免选择过于接近或不合理的质心。
- 提高收敛速度:由于初始质心选择得更好,K-means++通常在更少的迭代中就能收敛,从而减少计算时间。
- 提高聚类质量:K-means++通常能够找到更接近全局最优解的聚类结果,从而提高了聚类的质量。
2.簇的数量K的确定
2.1. 肘部法则(Elbow Method)
核心思想
肘部法则是一种直观的、常用的确定K值的方法。它通过观察不同K值下簇内平方和误差(Within-Cluster Sum of Squares, WCSS)的变化趋势,选择一个使得WCSS明显减少的K值
。WCSS表示每个簇中数据点到其质心的距离平方和之和,WCSS越小,聚类效果越好。
操作步骤
- 对于不同的K值,计算对应的WCSS。
- 将K值与WCSS绘制成曲线图。
- 观察曲线,选择K值对应的“肘部”位置,即曲线开始趋于平缓的点作为合适的K值。
调包实现
import numpy as np
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt
# 生成随机数据
X = np.random.rand(100, 2)
# 计算不同K值的WCSS
wcss = []
for k in range(1, 11):
kmeans = KMeans(n_clusters=k, random_state=0).fit(X)
wcss.append(kmeans.inertia_)
# 绘制肘部法则图
plt.plot(range(1, 11), wcss, 'bx-')
plt.xlabel('Number of clusters (K)')
plt.ylabel('WCSS')
plt.title('Elbow Method for Optimal K')
plt.show()
不调包实现
import numpy as np
import matplotlib.pyplot as plt
def calculate_wcss(X, centroids, labels, k):
wcss = 0
for i in range(k):
cluster_points = X[labels == i]
centroid = centroids[i]
wcss += np.sum((cluster_points - centroid) ** 2)
return wcss
def initialize_centroids(X, k):
np.random.seed(0)
indices = np.random.choice(X.shape[0], k, replace=False)
return X[indices]
def assign_clusters(X, centroids):
distances = np.sqrt(((X - centroids[:, np.newaxis]) ** 2).sum(axis=2))
return np.argmin(distances, axis=0)
def update_centroids(X, labels, k):
return np.array([X[labels == i].mean(axis=0) for i in range(k)])
def kmeans_custom(X, k, max_iters=100):
centroids = initialize_centroids(X, k)
for _ in range(max_iters):
labels = assign_clusters(X, centroids)
new_centroids = update_centroids(X, labels, k)
if np.all(centroids == new_centroids):
break
centroids = new_centroids
return labels, centroids
# 生成随机数据
X = np.random.rand(100, 2)
# 计算不同K值的WCSS
wcss = []
for k in range(1, 11):
labels, centroids = kmeans_custom(X, k)
wcss.append(calculate_wcss(X, centroids, labels, k))
# 绘制肘部法则图
plt.plot(range(1, 11), wcss, 'bx-')
plt.xlabel('Number of clusters (K)')
plt.ylabel('WCSS')
plt.title('Elbow Method for Optimal K (Without Using Libraries)')
plt.show()
优点
- 简单直观,容易理解和操作。
- 适用于大多数数据集。
缺点
- 有时“肘部”不明显,导致K值难以确定。
- 在某些情况下,该方法可能会对数据的噪声敏感。
2.2. 轮廓系数(Silhouette Coefficient)
核心思想
轮廓系数是一种衡量聚类效果的指标,考虑了簇内紧密度(即数据点与同簇其他点的距离)和簇间分离度(即数据点与最近的其他簇的距离)
。轮廓系数的取值范围是[-1, 1],值越大,表示聚类效果越好。
操作步骤
-
对于每个数据点,计算它的轮廓系数,公式为:
s ( i ) = b ( i ) − a ( i ) max ( a ( i ) , b ( i ) ) s(i) = \frac{b(i) - a(i)}{\max(a(i), b(i))} s(i)=max(a(i),b(i))b(i)−a(i)
其中:
- a ( i ) a(i) a(i) 是数据点 i i i 到同簇内其他数据点的平均距离。
- b ( i ) b(i) b(i) 是数据点 i i i 到最近的其他簇的平均距离。
-
计算所有数据点的平均轮廓系数作为整体聚类效果的指标。
-
选择轮廓系数最大的K值作为最佳K值。
调包实现
from sklearn.metrics import silhouette_score
# 生成随机数据
X = np.random.rand(100, 2)
# 计算不同K值的轮廓系数
silhouette_scores = []
for k in range(2, 11):
kmeans = KMeans(n_clusters=k, random_state=0).fit(X)
score = silhouette_score(X, kmeans.labels_)
silhouette_scores.append(score)
# 绘制轮廓系数图
plt.plot(range(2, 11), silhouette_scores, 'bx-')
plt.xlabel('Number of clusters (K)')
plt.ylabel('Silhouette Score')
plt.title('Silhouette Method for Optimal K')
plt.show()
不调包实现
import numpy as np
def calculate_silhouette_score(X, labels, k):
silhouette_scores = []
for i, point in enumerate(X):
same_cluster = X[labels == labels[i]]
other_cluster = X[labels != labels[i]]
a = np.mean([np.linalg.norm(point - other) for other in same_cluster if not np.array_equal(point, other)])
b = np.min([np.mean([np.linalg.norm(point - other) for other in X[labels == label]]) for label in range(k) if label != labels[i]])
silhouette_score = (b - a) / max(a, b)
silhouette_scores.append(silhouette_score)
return np.mean(silhouette_scores)
# 生成随机数据
X = np.random.rand(100, 2)
# 计算不同K值的轮廓系数
silhouette_scores = []
for k in range(2, 11):
labels, centroids = kmeans_custom(X, k)
score = calculate_silhouette_score(X, labels, k)
silhouette_scores.append(score)
# 绘制轮廓系数图
plt.plot(range(2, 11), silhouette_scores, 'bx-')
plt.xlabel('Number of clusters (K)')
plt.ylabel('Silhouette Score')
plt.title('Silhouette Method for Optimal K (Without Using Libraries)')
plt.show()
优点
- 提供了聚类效果的直接量化评估。
- 对于不同形状的簇也能有效处理。
缺点
- 计算复杂度较高,尤其是在处理大规模数据时。
- 对数据的噪声和异常值敏感。
2.3. 信息准则(如AIC/BIC)
核心思想
信息准则(如AIC,Akaike Information Criterion;BIC,Bayesian Information Criterion)是一种基于模型复杂度和拟合优度的评估方法。信息准则考虑了模型的拟合优度(通常使用对数似然估计)和模型的复杂度(自由参数的数量)
。AIC和BIC的值越小,表示模型平衡了复杂度和拟合优度,选择的K值越合适。
操作步骤
计算不同K值下的AIC或BIC值。
选择使得AIC或BIC值最小的K值。
调包实现
from sklearn.mixture import GaussianMixture
# 生成随机数据
X = np.random.rand(100, 2)
# 计算不同K值的BIC
bic_scores = []
for k in range(1, 11):
gmm = GaussianMixture(n_components=k, random_state=0).fit(X)
bic_scores.append(gmm.bic(X))
# 绘制BIC图
plt.plot(range(1, 11), bic_scores, 'bx-')
plt.xlabel('Number of clusters (K)')
plt.ylabel('BIC')
plt.title('BIC for Optimal K')
plt.show()
不调包实现
import numpy as np
def calculate_bic(X, labels, centroids, k):
n, d = X.shape
m = k * d
wcss = calculate_wcss(X, centroids, labels, k)
bic = n * np.log(wcss / n) + m * np.log(n)
return bic
# 生成随机数据
X = np.random.rand(100, 2)
# 计算不同K值的BIC
bic_scores = []
for k in range(1, 11):
labels, centroids = kmeans_custom(X, k)
bic = calculate_bic(X, labels, centroids, k)
bic_scores.append(bic)
# 绘制BIC图
plt.plot(range(1, 11), bic_scores, 'bx-')
plt.xlabel('Number of clusters (K)')
plt.ylabel('BIC')
plt.title('BIC for Optimal K (Without Using Libraries)')
plt.show()
优点
- 同时考虑了模型的拟合优度和复杂度,具有理论上的优越性。
- 对复杂数据和大数据集适用性强。
缺点
- 计算过程相对复杂。
- 依赖于数据的具体分布假设,对于一些分布不明确的数据集,结果可能不准确。
3.算法的加速与优化
-
Mini-Batch K-means:在大型数据集上,标准K-means可能需要较长的计算时间。Mini-Batch K-means通过使用小批量的数据点来更新质心,显著减少了计算时间,同时保持了较好的聚类效果。
-
并行化与分布式计算:在多核处理器或分布式系统上,并行化K-means算法可以加速计算,特别适用于大数据场景。Hadoop、Spark等框架提供了K-means的并行实现。
-
球状簇的检测:标准K-means假设簇是球形的,因此在处理复杂形状的簇时效果较差。可以结合层次聚类或DBSCAN等其他算法,预处理数据或分割复杂簇。
4.对异常值的处理
4.1. 均值漂移聚类(Mean Shift Clustering)
均值漂移聚类(Mean Shift Clustering)是一种非参数化的聚类方法,它通过检测数据的密度峰值来确定簇的中心。与K-means不同,均值漂移聚类不需要预设簇的数量K,而是通过算法自动找到簇的数量和簇中心。
核心思想
均值漂移聚类的核心思想是通过滑动窗口(也称为核函数)来不断更新数据点的质心,质心会沿着数据密度的梯度移动,最终收敛到数据密度最大的区域
。每个数据点最终会聚集到一个密度峰值,密度峰值所在的位置就是簇的中心。
主要步骤
- 初始化:将所有数据点视为可能的簇中心。
- 计算窗口内的数据点:为每个点设置一个半径(称为带宽,bandwidth),并找到该半径范围内的所有数据点。
- 计算新的质心:通过这些邻近的数据点的均值来更新当前点的质心位置。质心位置向数据密集的区域移动。
- 重复移动质心:反复计算质心,直到所有质心收敛(即质心位置的变化小于设定的阈值)。
- 聚类:当所有质心收敛后,最终的质心就代表数据中的簇。那些收敛到相同质心的点被归为同一个簇。
带宽(Bandwidth)的作用
带宽参数控制滑动窗口的大小,是均值漂移聚类中的关键参数。它决定了算法搜索数据密集区域的半径。如果带宽过小,算法会产生过多的簇;如果带宽过大,则会产生较少的簇。
带宽可以通过交叉验证或手动调整来选择。
公式
假设我们有一个数据点 x i x_i xi,带宽为 h h h,则在该点周围的邻居点集合 N ( x i ) N(x_i) N(xi) 的质心更新公式为:
m ( x i ) = ∑ x j ∈ N ( x i ) K ( x j − x i h ) x j ∑ x j ∈ N ( x i ) K ( x j − x i h ) m(x_i) = \frac{\sum_{x_j \in N(x_i)} K\left(\frac{x_j - x_i}{h}\right) x_j}{\sum_{x_j \in N(x_i)} K\left(\frac{x_j - x_i}{h}\right)} m(xi)=∑xj∈N(xi)K(hxj−xi)∑xj∈N(xi)K(hxj−xi)xj
其中:
-
K ( ⋅ ) K(\cdot) K(⋅) 是核函数,通常使用高斯核函数:
K ( x ) = e − x 2 2 K(x) = e^{-\frac{x^2}{2}} K(x)=e−2x2
-
x j x_j xj 是数据集中属于点 x i x_i xi 的邻居点,带宽 h h h 控制着核函数的范围。
优点
- 不需要预设簇的数量:与K-means不同,均值漂移聚类不需要预设簇数,簇的数量是由算法自动确定的。
- 能够处理非球形簇:均值漂移聚类能够处理不规则形状的簇,不像K-means只能处理球形簇。
- 对异常值的鲁棒性强:因为质心会被数据密集区域吸引,异常值对均值漂移聚类的影响较小。
缺点
- 计算复杂度高:由于需要对每个点反复进行质心更新,均值漂移聚类的计算复杂度较高,特别是在大数据集上,计算成本会很大。
- 对带宽敏感:带宽的选择对结果影响较大,带宽过大或过小都会导致聚类效果不佳。如何选取合适的带宽是一个挑战。
- 可能产生空簇:如果某些区域的密度较低,均值漂移可能在这些区域不产生簇。
Python代码实现
调包实现
import numpy as np
from sklearn.cluster import MeanShift
import matplotlib.pyplot as plt
# 生成随机数据
X = np.random.rand(100, 2)
# 均值漂移聚类
ms = MeanShift(bandwidth=0.1)
ms.fit(X)
labels = ms.labels_
cluster_centers = ms.cluster_centers_
# 可视化聚类结果
plt.scatter(X[:, 0], X[:, 1], c=labels, cmap='viridis')
plt.scatter(cluster_centers[:, 0], cluster_centers[:, 1], c='red', marker='x')
plt.title('Mean Shift Clustering (Using scikit-learn)')
plt.show()
不调包实现
import numpy as np
import matplotlib.pyplot as plt
def gaussian_kernel(distance, bandwidth):
return np.exp(-0.5 * (distance / bandwidth) ** 2)
def mean_shift(X, bandwidth=0.1, max_iter=300):
# 初始化
points = X.copy()
for _ in range(max_iter):
for i in range(points.shape[0]):
# 计算每个点到其他所有点的距离
distances = np.linalg.norm(points - points[i], axis=1)
# 应用高斯核函数
weights = gaussian_kernel(distances, bandwidth)
# 更新质心位置
points[i] = np.sum(points.T * weights, axis=1) / np.sum(weights)
# 将点聚合为簇
cluster_centers = []
labels = np.zeros(points.shape[0])
for i, point in enumerate(points):
# 如果点接近已有的簇中心,则将其归入该簇
for center in cluster_centers:
if np.linalg.norm(point - center) < bandwidth:
labels[i] = cluster_centers.index(center)
break
else:
cluster_centers.append(point)
labels[i] = len(cluster_centers) - 1
return np.array(cluster_centers), labels
# 生成随机数据
X = np.random.rand(100, 2)
# 调用自定义的均值漂移聚类算法
cluster_centers, labels = mean_shift(X, bandwidth=0.1)
# 可视化聚类结果
plt.scatter(X[:, 0], X[:, 1], c=labels, cmap='viridis')
plt.scatter(cluster_centers[:, 0], cluster_centers[:, 1], c='red', marker='x')
plt.title('Mean Shift Clustering (Without Using Libraries)')
plt.show()
4.2. 内存占用优化
对于高维数据,可以通过主成分分析(PCA)或其他降维技术来减少维度,从而减小内存占用和计算复杂度。
5.混合模型
5.1. Soft K-means
Soft K-means(也称为Fuzzy K-means或模糊K-means)是K-means算法的一种变体,它通过引入模糊聚类的概念,使得每个数据点可以部分地属于多个簇,而不是像传统K-means那样硬性地分配到一个簇中
。Soft K-means在处理数据点处于多个簇的边界时尤为有效,能够更好地捕捉数据的模糊性和不确定性。
核心思想
Soft K-means的核心思想是通过模糊隶属度函数(membership function)来表示每个数据点属于不同簇的概率或隶属度。这个隶属度的值介于0和1之间,表示数据点与每个簇中心的关联程度。最终的簇中心更新会考虑所有数据点的隶属度,而不是仅仅考虑距离最近的点
。
主要步骤
-
初始化簇中心:与 K-means 类似,首先随机初始化 K 个簇中心。
-
计算隶属度:对于每个数据点,计算它属于每个簇的隶属度。隶属度通常与数据点到各簇中心的距离成反比,距离越近,隶属度越高。
隶属度 u i j u_{ij} uij 的计算公式为:
u i j = exp ( − β ⋅ ∥ x i − μ j ∥ 2 ) ∑ k = 1 K exp ( − β ⋅ ∥ x i − μ k ∥ 2 ) u_{ij} = \frac{\exp\left(-\beta \cdot \|x_i - \mu_j\|^2\right)}{\sum_{k=1}^{K} \exp\left(-\beta \cdot \|x_i - \mu_k\|^2\right)} uij=∑k=1Kexp(−β⋅∥xi−μk∥2)exp(−β⋅∥xi−μj∥2)
其中:
- u i j u_{ij} uij 表示数据点 x i x_i xi 对簇 j j j 的隶属度。
- μ j \mu_j μj 是簇 j j j 的质心。
- β \beta β 是控制隶属度分配的参数(通常与逆温度成比例)。
-
更新簇中心:根据所有数据点的隶属度加权更新每个簇的质心。更新公式为:
μ j = ∑ i = 1 n u i j ⋅ x i ∑ i = 1 n u i j \mu_j = \frac{\sum_{i=1}^{n} u_{ij} \cdot x_i}{\sum_{i=1}^{n} u_{ij}} μj=∑i=1nuij∑i=1nuij⋅xi
其中, n n n 是数据点的总数。
-
迭代:重复计算隶属度和更新簇中心的步骤,直到簇中心的变化小于预设的阈值或达到最大迭代次数。
优点
- 处理模糊边界:Soft K-means能够处理数据点同时属于多个簇的情况,更适合那些簇之间没有明显边界的数据集。
- 更灵活的簇划分:通过隶属度,Soft K-means能够更灵活地划分数据,尤其是在处理复杂的、多模态的数据分布时表现更好。
- 降低局部最优的风险:由于考虑了所有簇的隶属度,Soft K-means在一定程度上降低了陷入局部最优解的风险。
缺点
- 计算复杂度高:由于需要计算每个数据点对每个簇的隶属度,Soft K-means的计算复杂度高于传统K-means。
- 对参数敏感:Soft K-means对隶属度函数中的参数(如 β )较为敏感,参数选择不当可能影响聚类效果。
- 难以解释:由于每个点可以部分地属于多个簇,结果的解释性比硬性聚类更复杂。
Soft K-means的Python代码实现
import numpy as np
import matplotlib.pyplot as plt
def soft_kmeans(X, k, beta=1.0, max_iters=100):
# 初始化簇中心
n, d = X.shape
centers = X[np.random.choice(n, k, replace=False)]
for _ in range(max_iters):
# 计算隶属度
distances = np.array([[np.linalg.norm(x - center) for center in centers] for x in X])
membership = np.exp(-beta * distances)
membership = membership / membership.sum(axis=1, keepdims=True)
# 更新簇中心
new_centers = np.dot(membership.T, X) / membership.sum(axis=0)[:, np.newaxis]
# 检查收敛性
if np.allclose(centers, new_centers):
break
centers = new_centers
return centers, membership
# 生成随机数据
X = np.random.rand(100, 2)
# 调用自定义的Soft K-means算法
k = 3
centers, membership = soft_kmeans(X, k, beta=1.0)
# 根据最大隶属度分配标签
labels = np.argmax(membership, axis=1)
# 可视化聚类结果
plt.scatter(X[:, 0], X[:, 1], c=labels, cmap='viridis')
plt.scatter(centers[:, 0], centers[:, 1], c='red', marker='x')
plt.title('Soft K-means Clustering (Without Using Libraries)')
plt.show()
5.2. 高斯混合模型(GMM)
高斯混合模型(Gaussian Mixture Model,简称GMM)是一种概率模型,用于表示具有多个高斯分布的数据集。与K-means算法不同,高斯混合模型通过假设数据来自于多个高斯分布(即簇),并将数据点的生成过程视为从这些高斯分布中采样,从而实现对数据的聚类。
核心思想
高斯混合模型认为数据集中的每个数据点都可以由多个高斯分布生成,并且每个数据点属于某个高斯分布的概率是未知的。通过最大化数据点在这些高斯分布下的概率,GMM可以找到最符合数据分布的参数,即每个高斯分布的均值、协方差和混合系数
。
主要组成部分
-
混合系数:描述每个高斯分布在总体中的权重,即每个高斯分布生成数据点的概率。所有混合系数之和为1。
∑ k = 1 K π k = 1 \sum_{k=1}^{K} \pi_k = 1 k=1∑Kπk=1
其中, π k \pi_k πk 是第 k k k 个高斯分布的混合系数, K K K 是高斯分布的数量。
-
高斯分布:每个簇由一个高斯分布表示,具有自己的均值向量 μ k \mu_k μk 和协方差矩阵 Σ k \Sigma_k Σk。数据点的生成假设遵循这些高斯分布。
多维高斯分布的概率密度函数为:
N ( x ∣ μ k , Σ k ) = 1 ( 2 π ) d / 2 ∣ Σ k ∣ 1 / 2 exp ( − 1 2 ( x − μ k ) ⊤ Σ k − 1 ( x − μ k ) ) N(x|\mu_k, \Sigma_k) = \frac{1}{(2\pi)^{d/2} |\Sigma_k|^{1/2}} \exp \left( -\frac{1}{2} (x - \mu_k)^\top \Sigma_k^{-1} (x - \mu_k) \right) N(x∣μk,Σk)=(2π)d/2∣Σk∣1/21exp(−21(x−μk)⊤Σk−1(x−μk))
其中, d d d 是数据的维度, ∣ Σ k ∣ |\Sigma_k| ∣Σk∣ 是协方差矩阵的行列式。
-
潜在变量:表示每个数据点属于某个高斯分布的概率,这些概率值用于估计模型参数。
模型估计与期望最大化算法(EM算法)
GMM的参数估计通常使用期望最大化算法(EM算法)进行。EM算法通过迭代计算来找到最可能的参数,使得给定模型下的概率最大化。
-
初始化:初始化每个高斯分布的均值、协方差矩阵和混合系数,或者通过K-means算法进行初始聚类。
-
E步(期望步):计算每个数据点属于各个高斯分布的后验概率,即责任度 γ i k \gamma_{ik} γik:
γ i k = π k N ( x i ∣ μ k , Σ k ) ∑ j = 1 K π j N ( x i ∣ μ j , Σ j ) \gamma_{ik} = \frac{\pi_k N(x_i|\mu_k, \Sigma_k)}{\sum_{j=1}^{K} \pi_j N(x_i|\mu_j, \Sigma_j)} γik=∑j=1KπjN(xi∣μj,Σj)πkN(xi∣μk,Σk)
这里, γ i k \gamma_{ik} γik 表示数据点 x i x_i xi 属于簇 k k k 的概率。
-
M步(最大化步):利用E步计算得到的责任度更新模型参数(均值 μ k \mu_k μk,协方差 Σ k \Sigma_k Σk,混合系数 π k \pi_k πk):
-
更新均值:
μ k = ∑ i = 1 n γ i k x i ∑ i = 1 n γ i k \mu_k = \frac{\sum_{i=1}^{n} \gamma_{ik} x_i}{\sum_{i=1}^{n} \gamma_{ik}} μk=∑i=1nγik∑i=1nγikxi
-
更新协方差矩阵:
Σ k = ∑ i = 1 n γ i k ( x i − μ k ) ( x i − μ k ) ⊤ ∑ i = 1 n γ i k \Sigma_k = \frac{\sum_{i=1}^{n} \gamma_{ik} (x_i - \mu_k)(x_i - \mu_k)^\top}{\sum_{i=1}^{n} \gamma_{ik}} Σk=∑i=1nγik∑i=1nγik(xi−μk)(xi−μk)⊤
-
更新混合系数:
π k = 1 n ∑ i = 1 n γ i k \pi_k = \frac{1}{n} \sum_{i=1}^{n} \gamma_{ik} πk=n1i=1∑nγik
-
-
迭代:重复E步和M步,直到模型参数的变化小于设定的阈值或达到最大迭代次数。
优点
- 柔性更强:GMM可以捕捉数据的复杂分布,适用于具有不同形状和大小的簇。
- 概率模型:GMM是基于概率的模型,可以提供每个数据点属于某个簇的概率,这对于后续的决策和分析非常有用。
- 处理多模态数据:GMM适用于具有多模态分布的数据集,能够有效地分离不同的分布模式。
缺点
- 计算复杂度较高:由于涉及多次迭代和复杂的矩阵运算,GMM的计算复杂度较高。
- 对初始值敏感:GMM对初始参数值较为敏感,可能会陷入局部最优解。
- 假设高斯分布:GMM假设数据服从高斯分布,因此在非高斯分布的数据上表现可能不佳。
Python代码实现
调包实现
import numpy as np
from sklearn.mixture import GaussianMixture
import matplotlib.pyplot as plt
# 生成随机数据
X = np.random.rand(100, 2)
# 定义并训练GMM模型
gmm = GaussianMixture(n_components=3, random_state=0)
gmm.fit(X)
# 获取聚类结果
labels = gmm.predict(X)
centers = gmm.means_
# 可视化聚类结果
plt.scatter(X[:, 0], X[:, 1], c=labels, cmap='viridis')
plt.scatter(centers[:, 0], centers[:, 1], c='red', marker='x')
plt.title('Gaussian Mixture Model Clustering')
plt.show()
不调包实现
以下是一个简化的GMM手动实现,使用EM算法进行参数估计:
import numpy as np
def gaussian_pdf(x, mean, cov):
d = len(x)
cov_inv = np.linalg.inv(cov)
norm_const = 1.0 / (np.sqrt((2 * np.pi) ** d * np.linalg.det(cov)))
x_mu = np.matrix(x - mean)
result = np.exp(-0.5 * (x_mu * cov_inv * x_mu.T))
return norm_const * result
def gmm_em(X, k, max_iters=100):
n, d = X.shape
# 初始化参数
np.random.seed(0)
means = X[np.random.choice(n, k, replace=False)]
covariances = np.array([np.eye(d)] * k)
weights = np.ones(k) / k
responsibilities = np.zeros((n, k))
for _ in range(max_iters):
# E步:计算责任度
for i in range(n):
for j in range(k):
responsibilities[i, j] = weights[j] * gaussian_pdf(X[i], means[j], covariances[j])
responsibilities[i] /= np.sum(responsibilities[i])
# M步:更新参数
Nk = np.sum(responsibilities, axis=0)
for j in range(k):
means[j] = np.sum(responsibilities[:, j][:, np.newaxis] * X, axis=0) / Nk[j]
diff = X - means[j]
covariances[j] = np.dot((responsibilities[:, j][:, np.newaxis] * diff).T, diff) / Nk[j]
weights[j] = Nk[j] / n
return means, covariances, weights
# 生成随机数据
X = np.random.rand(100, 2)
# 调用自定义的GMM算法
k = 3
means, covariances, weights = gmm_em(X, k)
# 可视化聚类结果
plt.scatter(X[:, 0], X[:, 1], c=np.argmax(responsibilities, axis=1), cmap='viridis')
plt.scatter(means[:, 0], means[:, 1], c='red', marker='x')
plt.title('Gaussian Mixture Model Clustering (Without Using Libraries)')
plt.show()
6.距离度量的改进
- 使用其他距离度量:在某些情况下,欧几里得距离并不是最好的选择。可以根据数据特点选择其他的距离度量,如曼哈顿距离、余弦相似度、马氏距离等,以改善聚类效果。
7.自动化调参
- 自动化调参工具:可以使用诸如GridSearchCV或随机搜索(Random Search)等方法,自动寻找最优的K值和其他超参数组合,提高模型性能。
这些改进和调优方法能够有效地提升K-means算法的性能,使其更好地适应复杂的数据场景。然而,在选择具体的改进策略时,需要根据实际问题的特点进行综合考虑,以达到最优的聚类效果。
五、python代码实现
1. 调包实现(使用scikit-learn库)
import numpy as np
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt
# 生成随机数据
X = np.random.rand(100, 2)
# 定义k-means模型,设置簇的数量为3
kmeans = KMeans(n_clusters=3, random_state=0)
# 进行聚类
kmeans.fit(X)
# 获取聚类结果
labels = kmeans.labels_
centers = kmeans.cluster_centers_
# 可视化聚类结果
plt.scatter(X[:, 0], X[:, 1], c=labels, cmap='viridis')
plt.scatter(centers[:, 0], centers[:, 1], c='red', marker='x')
plt.title('K-means Clustering (Using scikit-learn)')
plt.show()
2. 不调包实现(手动编写K-means算法)
import numpy as np
import matplotlib.pyplot as plt
def initialize_centroids(X, k):
# 从数据集中随机选择k个点作为初始质心
np.random.seed(0)
indices = np.random.choice(X.shape[0], k, replace=False)
return X[indices]
def assign_clusters(X, centroids):
# 分配每个点到最近的质心
distances = np.sqrt(((X - centroids[:, np.newaxis])**2).sum(axis=2))
return np.argmin(distances, axis=0)
def update_centroids(X, labels, k):
# 更新质心
return np.array([X[labels == i].mean(axis=0) for i in range(k)])
def kmeans(X, k, max_iters=100):
centroids = initialize_centroids(X, k)
for _ in range(max_iters):
labels = assign_clusters(X, centroids)
new_centroids = update_centroids(X, labels, k)
# 检查质心是否收敛
if np.all(centroids == new_centroids):
break
centroids = new_centroids
return labels, centroids
# 生成随机数据
X = np.random.rand(100, 2)
# 调用自定义的k-means算法
k = 3
labels, centroids = kmeans(X, k)
# 可视化聚类结果
plt.scatter(X[:, 0], X[:, 1], c=labels, cmap='viridis')
plt.scatter(centroids[:, 0], centroids[:, 1], c='red', marker='x')
plt.title('K-means Clustering (Without Using Libraries)')
plt.show()
六、总结
优点
- 简单易懂:K-means算法相对简单,容易理解和实现。它基于直观的概念,即通过反复调整簇中心和数据点的分配来进行聚类。
- 计算效率高:对于大规模数据集,K-means的计算速度较快,特别是在K值较小的情况下。其时间复杂度为 O ( n ∗ k ∗ d ∗ t ) O(n * k * d *t) O(n∗k∗d∗t),其中n为数据点数量,k为簇数,d为数据维度,t为迭代次数。
- 适用于球形簇:K-means在处理形状相对规则、大小相近的球形簇时效果较好。因为算法假设簇是由相似分布的数据点组成的。
- 易于解释和应用:由于K-means产生的每个簇都有明确的中心点,且数据点归属于具体的簇,因此其结果容易解释,适用于许多实际应用场景,如客户分群、图像分割等。
缺点
-
需要预设K值:K-means算法要求事先设定簇的数量K,这在某些情况下并不容易确定。选择不当的K值可能会导致聚类效果不佳。
-
对初始值敏感:K-means算法对初始质心的选择非常敏感,不同的初始质心可能导致不同的聚类结果。初始质心选择不当可能导致算法陷入局部最优解,而非全局最优解。
-
仅适用于球形簇:K-means假设簇是球形且具有相似大小,这限制了它处理复杂形状的簇或不均匀大小簇的能力。在面对非球形、非均匀密度的簇时,K-means表现不佳。
-
对异常值敏感:K-means算法对异常值和噪声较为敏感,因为异常值会拉动簇中心的位置,影响聚类效果。在数据中存在异常值时,算法的聚类效果可能会大幅下降。
-
簇的形状受限:K-means算法假设簇是线性可分的、圆形的,这使得它不适合处理那些形状复杂、互相嵌套的簇。
-
收敛速度依赖于数据分布:在某些情况下,K-means算法可能需要较多的迭代才能收敛,尤其是在数据分布较为复杂或簇之间边界模糊时。
总结
K-means算法作为一种经典的聚类算法,在处理球形簇和大规模数据时表现优秀,且计算效率高,容易实现。然而,其局限性也较为明显,特别是在处理非球形簇、异常值和初始值选择上。因此,在实际应用中,通常需要结合数据的特性,以及使用如K-means++、层次聚类等改进或辅助方法,以获得更好的聚类效果。
结~~~