
- 个人首页: 永远都不秃头的程序员(互关)
- C语言专栏:从零开始学习C语言
- C++专栏:C++的学习之路
- 本文章所属专栏:K-Means深度探索系列
文章目录
引言:K-Means的"心灵之窗"------距离度量
亲爱的读者朋友们,欢迎回到我们的"K-Means深度探索"系列!在前几篇文章中,我们从零手撕了 K-Means 算法的核心,学会了 K 值选择、K-Means++ 初始化优化,以及 MiniBatch K-Means 在大数据场景下的应用。你已经掌握了 K-Means 的"骨架"和"肌肉"!
然而,K-Means 算法之所以能够"物以类聚",其核心就在于它能衡量数据点之间的"相似性"或"不相似性"。而这种相似性,正是通过**距离度量(Distance Metric)**来定义的。当我们说"某个数据点离质心最近"时,这个"最近"就是由我们选择的距离度量标准来判定的。
在之前的实践中,我们默认使用的都是最常见的欧氏距离(Euclidean Distance)。它直观、易懂,但在真实世界中,数据往往复杂多变,欧氏距离并非总是最佳选择。不同的数据类型、不同的业务场景,可能需要 K-Means 戴上不同的"心灵之窗",才能准确地识别数据点之间的真实关系。
今天,我们就来一场"距离度量"的深度之旅,探索除了欧氏距离之外,还有哪些强大的距离度量方式,它们各自的特点、适用场景,以及它们如何影响你的 K-Means 聚类结果。准备好了吗?让我们一起拓宽 K-Means 的"视野"!
欧氏距离:最常见的"直线距离"
理论回顾
欧氏距离,也被称为 L2 范数,是我们日常生活中最直观的距离概念------两点之间直线最短。在二维或三维空间中,它就是两点之间线段的长度。
对于两个 n n n 维向量 x = ( x 1 , x 2 , . . . , x n ) x = (x_1, x_2, ..., x_n) x=(x1,x2,...,xn) 和 y = ( y 1 , y 2 , . . . , y n ) y = (y_1, y_2, ..., y_n) y=(y1,y2,...,yn),欧氏距离计算公式为:
d ( x , y ) = ∑ i = 1 n ( x i − y i ) 2 d(x, y) = \sqrt{\sum_{i=1}^{n} (x_i - y_i)^2} d(x,y)=∑i=1n(xi−yi)2
适用场景与特点
-
适用: 数据特征具有连续性、数值型且在几何空间中具有实际意义的场景,例如地理坐标、物理测量值等。它倾向于形成球形或椭球形的簇。
-
优点: 直观、易于理解和计算。
-
缺点:
- 对量纲敏感: 如果不同特征的取值范围差异很大,量纲大的特征会主导距离计算,因此**特征缩放(Feature Scaling)**在应用欧氏距离前至关重要。
- 对异常值敏感: 距离是平方和的形式,异常值会对距离产生较大影响。
- "维度灾难": 在高维空间中,所有点之间的距离会趋于相等,欧氏距离的区分能力会下降。
曼哈顿距离:街区中的"出租车距离"
理论解读
曼哈顿距离,又称 L1 范数或"城市街区距离"(Taxicab Distance),指的是在网格状的城市中,从一个交叉口到另一个交叉口,只能沿着街道(平行于坐标轴)行走的距离。
对于两个 n n n 维向量 x x x 和 y y y,曼哈顿距离计算公式为:
d ( x , y ) = ∑ i = 1 n ∣ x i − y i ∣ d(x, y) = \sum_{i=1}^{n} |x_i - y_i| d(x,y)=∑i=1n∣xi−yi∣
适用场景与特点
-
适用:
- 当特征维度较高时,曼哈顿距离在高维空间中的表现可能比欧氏距离更稳定。
- 当数据存在较多离群点时,由于曼哈顿距离使用绝对值而不是平方,它对异常值的敏感度相对较低。
- 当特征表示不同"类型"的成本或差异,并且这些成本是线性累加时,曼哈顿距离可能更合适。例如,评估两个房屋在"卧室数量差异"和"卫生间数量差异"上的总和。
-
优点: 相对欧氏距离,对异常值不那么敏感;在高维空间中,其区分度可能更好。
-
缺点: 在某些几何直观的场景中,可能不如欧氏距离符合人类的直觉。
其他距离度量(简要提及)
除了欧氏距离和曼哈顿距离,还有许多其他的距离度量方式,它们根据数据的不同特性而设计:
-
闵可夫斯基距离 (Minkowski Distance): 它是欧氏距离和曼哈顿距离的泛化。
d ( x , y ) = ( ∑ i = 1 n ∣ x i − y i ∣ p ) 1 / p d(x, y) = (\sum_{i=1}^{n} |x_i - y_i|^p)^{1/p} d(x,y)=(∑i=1n∣xi−yi∣p)1/p当 p = 1 p=1 p=1 时是曼哈顿距离,当 p = 2 p=2 p=2 时是欧氏距离。
-
余弦相似度 (Cosine Similarity) / 余弦距离:
- 适用: 主要用于衡量两个向量在方向上的相似性,而非绝对值大小。在文本挖掘(文档相似度)、推荐系统(用户偏好相似度)等高维稀疏数据场景中非常有效。
- 原理: 计算两个向量夹角的余弦值。余弦值越接近 1,表示夹角越小,方向越相似。
- 余弦距离 = 1 - 余弦相似度。
-
汉明距离 (Hamming Distance):
- 适用: 用于比较两个等长字符串或二进制向量之间不同位的数量。
- 原理: 统计两个二进制串中对应位置不同的位数。
手把手代码实践:度量不同,感受各异
sklearn.cluster.KMeans 默认且只支持欧氏距离来计算点到质心的距离并更新质心。这是因为 K-Means 的质心更新(求均值)在数学上是最小化欧氏距离的平方和的,这种匹配性使得算法在理论上非常优雅。
如果我们需要使用其他距离度量(如曼哈顿距离)来运行 K-Means 风格的聚类,通常需要自己实现 K-Means 的迭代过程 ,或者使用支持自定义距离度量的其他聚类算法(例如 sklearn.cluster.AgglomerativeClustering 可以通过 metric 参数指定多种距离)。
为了让大家直观感受不同距离度量的差异,我们来写一个简单的函数,计算这两个距离,并观察它们在同一组点上的结果。
python
import numpy as np
import matplotlib.pyplot as plt
from scipy.spatial.distance import euclidean, cityblock # 从scipy导入欧氏和曼哈顿距离
# 1. 模拟数据点
# 两个点在二维空间中
point_a = np.array([1, 1])
point_b = np.array([4, 5])
point_c = np.array([2, 4]) # 引入第三个点用于比较
# 2. 手动实现欧氏距离和曼哈顿距离函数(为了加深理解,与scipy对比)
def custom_euclidean_distance(p1, p2):
return np.sqrt(np.sum((p1 - p2)**2))
def custom_manhattan_distance(p1, p2):
return np.sum(np.abs(p1 - p2))
# 3. 计算并对比距离
print(f"点 A: {point_a}, 点 B: {point_b}, 点 C: {point_c}")
# A 到 B 的距离
dist_ab_euclidean = custom_euclidean_distance(point_a, point_b)
dist_ab_manhattan = custom_manhattan_distance(point_a, point_b)
print(f"\n点 A 到点 B 的欧氏距离: {dist_ab_euclidean:.2f}")
print(f"点 A 到点 B 的曼哈顿距离: {dist_ab_manhattan:.2f}") # (4-1) + (5-1) = 3 + 4 = 7
# A 到 C 的距离
dist_ac_euclidean = custom_euclidean_distance(point_a, point_c)
dist_ac_manhattan = custom_manhattan_distance(point_a, point_c)
print(f"\n点 A 到点 C 的欧氏距离: {dist_ac_euclidean:.2f}")
print(f"点 A 到点 C 的曼哈顿距离: {dist_ac_manhattan:.2f}") # (2-1) + (4-1) = 1 + 3 = 4
# B 到 C 的距离
dist_bc_euclidean = custom_euclidean_distance(point_b, point_c)
dist_bc_manhattan = custom_manhattan_distance(point_b, point_c)
print(f"\n点 B 到点 C 的欧氏距离: {dist_bc_euclidean:.2f}")
print(f"点 B 到点 C 的曼哈顿距离: {dist_bc_manhattan:.2f}") # (4-2) + (5-4) = 2 + 1 = 3
# 使用 scipy 验证 (仅作为参考,实际手撕KMeans时会用numpy实现)
print("\n--- Scipy 验证 ---")
print(f"Scipy 欧氏距离 (A到B): {euclidean(point_a, point_b):.2f}")
print(f"Scipy 曼哈顿距离 (A到B): {cityblock(point_a, point_b):.2f}")
# 4. 可视化直观理解
plt.figure(figsize=(8, 6))
plt.plot([point_a[0], point_b[0]], [point_a[1], point_b[1]], 'k--', alpha=0.6, label='欧氏距离路径') # 欧氏距离是直线
plt.plot([point_a[0], point_b[0]], [point_a[1], point_a[1]], 'r:', alpha=0.6) # 曼哈顿距离横向路径
plt.plot([point_b[0], point_b[0]], [point_a[1], point_b[1]], 'r:', alpha=0.6, label='曼哈顿距离路径') # 曼哈顿距离纵向路径
plt.scatter(point_a[0], point_a[1], s=100, color='blue', label='点 A')
plt.scatter(point_b[0], point_b[1], s=100, color='green', label='点 B')
plt.scatter(point_c[0], point_c[1], s=100, color='purple', label='点 C')
plt.xlim(0, 6)
plt.ylim(0, 6)
plt.title('欧氏距离与曼哈顿距离的几何对比')
plt.xlabel('X轴')
plt.ylabel('Y轴')
plt.grid(True, linestyle='--', alpha=0.6)
plt.legend()
plt.show()
从输出结果和图表中可以看到,欧氏距离和曼哈顿距离对于同一对点,其计算结果是不同的,并且几何含义也不同。在我们的例子中,A 到 C 的曼哈顿距离(4.00)比 A 到 B 的曼哈顿距离(7.00)小得多,这可能意味着在曼哈顿距离的视角下,C 更接近 A。而欧氏距离也呈现出类似的趋势,但具体数值和相对大小可能不同。
实践的深度思考:
-
选择的艺术: 没有"万能"的距离度量。正确的选择取决于你对数据本质的理解,以及你希望 K-Means 找到什么类型的"相似性"。
- 如果簇是大致球形且特征数值意义明确, 欧氏距离通常是首选。
- 如果数据点有很多离群值,或者特征维度非常高,且希望减少极端值影响, 曼哈顿距离可能更合适。
- 如果关注的是数据点"方向"上的相似(如文档主题、用户偏好), 那么余弦相似度(或余弦距离)才是你的利器,但这时需要使用支持自定义距离的聚类算法或自己实现 K-Means。
-
特征缩放再强调: 无论选择哪种距离度量,如果特征的尺度差异很大,**特征缩放(如标准化 Standard Scaling 或归一化 Min-Max Scaling)**几乎总是必不可少的步骤。否则,范围大的特征将主导距离计算,掩盖其他特征的重要性。
-
K-Means与质心更新的内在联系: K-Means 算法的质心更新过程是计算簇内点的均值。在数学上,这个"均值"操作是最小化簇内欧氏距离平方和 的最佳质心。如果使用曼哈顿距离,理论上最佳的质心应该是簇内所有点的中位数 (而非均值),以最小化簇内曼哈顿距离和。因此,如果需要用曼哈顿距离运行 K-Means,不仅仅是距离函数要变,质心更新的逻辑也应该从求均值变为求中位数 ,这才是真正的深度定制。
sklearn的KMeans由于内部优化,无法直接更换距离和对应的更新逻辑,这也是我们理解其工作原理的重要性所在。
小结与展望:精准度量,洞察数据真谛
理解距离度量的选择,是 K-Means 实践中非常重要的一环。它要求我们不仅会使用算法,更要能理解其底层机制,并根据实际需求进行灵活调整。
在接下来的文章中,我们将继续深入 K-Means 的局限性,并探索它在图像压缩和市场细分中的神奇应用。