8种距离度量Python实战:从欧式到马氏,5行代码实现KNN分类
在机器学习领域,距离度量是许多算法的核心基础。无论是KNN分类、聚类分析还是推荐系统,选择合适的距离度量方式直接影响模型性能。本文将带你深入理解8种经典距离度量方法,并通过Python实战演示它们在KNN分类中的应用效果。
1. 距离度量的基础概念
距离度量是衡量数据点之间相似性或差异性的数学工具。在机器学习中,我们经常需要计算样本之间的距离,这直接影响模型的预测结果。不同的距离度量方法适用于不同的数据特征和应用场景。
为什么距离度量如此重要?
想象一下,如果你要判断一个新样本属于哪个类别,最直观的方法就是看它与哪些已知样本最"接近"。这里的"接近"就需要通过距离度量来量化。选择不当的距离度量可能导致模型性能下降,甚至得出完全错误的结论。
常见应用场景包括:
- 分类算法(如KNN)
- 聚类分析(如K-Means)
- 异常检测
- 推荐系统
- 图像识别
在开始具体介绍前,我们先准备好实验环境。确保已安装以下Python库:
import numpy as np from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.neighbors import KNeighborsClassifier from sklearn.metrics import accuracy_score from scipy.spatial.distance import mahalanobis2. 欧式距离与曼哈顿距离
2.1 欧式距离(Euclidean Distance)
欧式距离是最直观的距离度量方式,源于我们日常的几何空间概念。它计算的是两点之间的直线距离,公式为:
$$ d(x,y) = \sqrt{\sum_{i=1}^n (x_i - y_i)^2} $$
Python实现:
def euclidean_distance(x, y): return np.sqrt(np.sum((x - y)**2)) # 示例 point1 = np.array([1, 2, 3]) point2 = np.array([4, 5, 6]) print(f"欧式距离: {euclidean_distance(point1, point2):.2f}")特点:
- 对数据尺度敏感,使用时通常需要标准化
- 对异常值敏感
- 适用于连续型数据
2.2 曼哈顿距离(Manhattan Distance)
曼哈顿距离又称城市街区距离,得名于在曼哈顿街区行走的路径。它计算的是各维度绝对差之和:
$$ d(x,y) = \sum_{i=1}^n |x_i - y_i| $$
Python实现:
def manhattan_distance(x, y): return np.sum(np.abs(x - y)) print(f"曼哈顿距离: {manhattan_distance(point1, point2):.2f}")应用场景对比:
| 指标 | 欧式距离 | 曼哈顿距离 |
|---|---|---|
| 计算方式 | 平方和开方 | 绝对值和 |
| 敏感性 | 对异常值敏感 | 对异常值相对稳健 |
| 适用场景 | 物理空间距离 | 网格状路径规划 |
| 计算效率 | 中等(涉及平方运算) | 高(仅绝对值运算) |
提示:在高维数据中,曼哈顿距离往往比欧式距离表现更稳定,因为各维度差异的累加效应更明显。
3. 切比雪夫距离与闵可夫斯基距离
3.1 切比雪夫距离(Chebyshev Distance)
切比雪夫距离源于国际象棋中国王的移动方式,计算的是各维度绝对差的最大值:
$$ d(x,y) = \max_i |x_i - y_i| $$
Python实现:
def chebyshev_distance(x, y): return np.max(np.abs(x - y)) print(f"切比雪夫距离: {chebyshev_distance(point1, point2):.2f}")典型应用:
- 棋盘游戏AI
- 图像处理中的像素比较
- 工业质量控制中的公差检测
3.2 闵可夫斯基距离(Minkowski Distance)
闵可夫斯基距离是欧式距离和曼哈顿距离的推广,通过参数p控制距离类型:
$$ d(x,y) = \left( \sum_{i=1}^n |x_i - y_i|^p \right)^{1/p} $$
Python实现:
def minkowski_distance(x, y, p): return np.sum(np.abs(x - y)**p)**(1/p) # p=2时等同于欧式距离 print(f"闵可夫斯基距离(p=2): {minkowski_distance(point1, point2, 2):.2f}") # p=1时等同于曼哈顿距离 print(f"闵可夫斯基距离(p=1): {minkowski_distance(point1, point2, 1):.2f}")参数p的影响:
- p=1:曼哈顿距离
- p=2:欧式距离
- p→∞:趋近于切比雪夫距离
注意:闵可夫斯基距离虽然灵活,但选择合适的p值需要基于具体问题和交叉验证。
4. 余弦相似度与汉明距离
4.1 余弦相似度(Cosine Similarity)
余弦相似度衡量的是两个向量的夹角余弦值,关注方向而非大小:
$$ \text{similarity} = \frac{x \cdot y}{|x| |y|} $$
Python实现:
def cosine_similarity(x, y): dot_product = np.dot(x, y) norm_x = np.linalg.norm(x) norm_y = np.linalg.norm(y) return dot_product / (norm_x * norm_y) # 余弦距离=1-余弦相似度 print(f"余弦相似度: {cosine_similarity(point1, point2):.2f}")适用场景:
- 文本相似度计算(TF-IDF向量)
- 推荐系统中的用户偏好比较
- 高维稀疏数据
4.2 汉明距离(Hamming Distance)
汉明距离用于比较两个等长字符串在相同位置上不同字符的数量:
def hamming_distance(str1, str2): if len(str1) != len(str2): raise ValueError("字符串长度必须相同") return sum(c1 != c2 for c1, c2 in zip(str1, str2)) print(f"汉明距离: {hamming_distance('1011101', '1001001')}")典型应用:
- 错误检测与纠正编码
- DNA序列比对
- 密码学中的差异分析
5. 马氏距离及其特性
马氏距离(Mahalanobis Distance)是一种考虑数据分布特性的距离度量,能够解决特征间相关性和尺度不一致的问题:
$$ D_M(x,y) = \sqrt{(x-y)^T S^{-1} (x-y)} $$
其中S是协方差矩阵。
Python实现:
# 计算马氏距离需要先计算协方差矩阵的逆 iris = load_iris() X = iris.data cov_matrix = np.cov(X, rowvar=False) inv_cov_matrix = np.linalg.inv(cov_matrix) def mahalanobis_distance(x, y, inv_cov): diff = x - y return np.sqrt(diff.T @ inv_cov @ diff) # 示例计算两个样本间的马氏距离 sample1 = X[0] sample2 = X[1] print(f"马氏距离: {mahalanobis_distance(sample1, sample2, inv_cov_matrix):.2f}")马氏距离的独特优势:
- 自动处理特征间的相关性
- 不受特征尺度影响
- 考虑数据分布形状
注意:当协方差矩阵为单位矩阵时,马氏距离退化为欧式距离。
6. KNN分类实战比较
现在我们将这8种距离度量应用于Iris数据集的KNN分类,比较它们的性能差异。
6.1 数据准备
# 加载数据 iris = load_iris() X, y = iris.data, iris.target # 划分训练测试集 X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42) # 定义距离度量列表 distance_metrics = [ ('euclidean', '欧式距离'), ('manhattan', '曼哈顿距离'), ('chebyshev', '切比雪夫距离'), ('minkowski', '闵可夫斯基距离(p=3)'), ('cosine', '余弦距离'), ('hamming', '汉明距离'), ('mahalanobis', '马氏距离') ]6.2 模型训练与评估
results = [] for metric, name in distance_metrics: if metric == 'mahalanobis': # 马氏距离需要特殊处理 cov_matrix = np.cov(X_train, rowvar=False) inv_cov_matrix = np.linalg.inv(cov_matrix) knn = KNeighborsClassifier(n_neighbors=5, metric=lambda x, y: mahalanobis(x, y, inv_cov_matrix)) else: knn = KNeighborsClassifier(n_neighbors=5, metric=metric) knn.fit(X_train, y_train) y_pred = knn.predict(X_test) acc = accuracy_score(y_test, y_pred) results.append((name, acc))6.3 结果对比
print("不同距离度量在KNN中的表现:") for name, acc in sorted(results, key=lambda x: x[1], reverse=True): print(f"{name:<20}: 准确率 {acc:.4f}")典型输出结果可能如下(具体值可能因数据划分不同而变化):
马氏距离 : 准确率 0.9778 欧式距离 : 准确率 0.9556 曼哈顿距离 : 准确率 0.9556 闵可夫斯基距离(p=3): 准确率 0.9556 切比雪夫距离 : 准确率 0.9333 余弦距离 : 准确率 0.9111 汉明距离 : 准确率 0.31116.4 结果分析
从实验结果可以看出:
- 马氏距离表现最佳,因为它考虑了特征间的相关性
- 欧式、曼哈顿和闵可夫斯基距离表现相当
- 汉明距离表现最差,因为它不适合连续型数据
实际应用中,距离度量的选择应该基于数据特性和业务需求,而不是盲目选择准确率最高的方法。
7. 距离度量的选择指南
如何为你的项目选择合适距离度量?以下是一些实用建议:
数据类型考虑:
- 连续型数据:欧式、曼哈顿、马氏距离
- 离散型数据:汉明距离
- 文本数据:余弦相似度
问题特性考虑:
- 特征尺度差异大:马氏距离或标准化后使用欧式距离
- 高维数据:余弦相似度或曼哈顿距离
- 网格状结构:曼哈顿或切比雪夫距离
实用选择流程:
- 分析数据特征(尺度、相关性、维度)
- 初步筛选几种候选距离度量
- 通过交叉验证比较性能
- 考虑计算效率(特别是大规模数据时)
8. 高级应用与优化技巧
8.1 自定义距离度量
Scikit-learn允许自定义距离度量。例如,实现加权欧式距离:
def weighted_euclidean(x, y, weights=[1,1,1,1]): return np.sqrt(np.sum(weights * (x - y)**2)) # 使用自定义距离的KNN knn_custom = KNeighborsClassifier( n_neighbors=5, metric=lambda x, y: weighted_euclidean(x, y, weights=[0.1, 0.3, 0.3, 0.3]) )8.2 距离度量预处理技巧
标准化处理:对欧式距离等尺度敏感的度量尤为重要
from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test)特征选择:去除不相关特征可以提高距离度量的有效性
维度缩减:对高维数据使用PCA等降维技术
8.3 混合距离度量
对于包含不同类型特征的数据集,可以考虑为不同类型特征使用不同距离度量,然后组合结果。例如:
def mixed_distance(x, y): # 前两个特征使用曼哈顿距离 num_part = np.sum(np.abs(x[:2] - y[:2])) # 后两个特征使用欧式距离 cat_part = np.sqrt(np.sum((x[2:] - y[2:])**2)) return num_part + cat_part9. 性能优化与加速
对于大规模数据集,距离计算可能成为性能瓶颈。以下是一些优化策略:
使用KD树或Ball树:
knn = KNeighborsClassifier(algorithm='kd_tree') # 或 'ball_tree'近似最近邻:使用近似算法如LSH(Locality-Sensitive Hashing)
并行计算:利用多核CPU或GPU加速
距离矩阵缓存:对于静态数据,预计算并存储距离矩阵
from sklearn.neighbors import DistanceMetric dist = DistanceMetric.get_metric('euclidean') distance_matrix = dist.pairwise(X_train)