机器学习算法常见距离计算
引言
想象一下,你要判断一个新来的同学和谁最相似:
- 方法1:看直线距离有多远(欧式距离)
- 方法2:看需要走多少条街才能到达(曼哈顿距离)
- 方法3:看最远的那一维差距有多大(切比雪夫距离)
- 方法4:根据情况灵活选择距离计算方式(闵可夫斯基距离)
在 KNN(K-Nearest Neighbors)算法中,距离计算是核心。不同的距离度量方式会影响算法的结果,就像用不同的"尺子"测量,会得到不同的"远近"判断。
本文将详细讲解 KNN 算法中常用的距离计算方式:
基础距离度量:
- 欧式距离(Euclidean Distance)
- 曼哈顿距离(Manhattan Distance)
- 切比雪夫距离(Chebyshev Distance)
- 闵可夫斯基距离(Minkowski Distance)
进阶距离度量:
5. 标准化欧式距离(Standardized Euclidean Distance)
6. 余弦距离(Cosine Distance)
7. 汉明距离(Hamming Distance)
8. 杰卡德距离(Jaccard Distance)
9. 马氏距离(Mahalanobis Distance)
每种距离都会包含:
- 数学定义和计算公式
- 为什么这样计算
- 直观理解
- 可视化图示
- 代码实现
距离的基本概念
什么是距离?
在数学和机器学习中,**距离(Distance)**是衡量两个点(样本)之间相似程度的度量。
类比理解:
- 就像地图上两个城市的距离
- 就像两个人之间的相似度
- 就像两个样本之间的差异
距离的性质
一个有效的距离函数 $d(x, y)$ 必须满足以下性质:
- 非负性:$d(x, y) \geq 0$,距离不能为负
- 同一性:$d(x, y) = 0$ 当且仅当 $x = y$
- 对称性:$d(x, y) = d(y, x)$,A到B的距离等于B到A的距离
- 三角不等式:$d(x, z) \leq d(x, y) + d(y, z)$,两点间直线最短
类比理解:
- 就像现实中的距离:不能为负,自己到自己的距离是0,往返距离相等,绕路总比直走远
为什么需要不同的距离度量?
不同的距离度量适用于不同的场景:
- 欧式距离:适用于连续特征,考虑所有维度的综合差异
- 曼哈顿距离:适用于离散特征,或者需要考虑"路径"的场景
- 切比雪夫距离:适用于只关心最大差异的场景
- 闵可夫斯基距离:通用的距离族,可以灵活调整
欧式距离(Euclidean Distance)
什么是欧式距离?
欧式距离是最直观、最常用的距离度量方式,就是我们常说的"直线距离"。
类比理解:
- 就像地图上两点之间的直线距离
- 就像用直尺测量的距离
- 就像勾股定理计算的距离
数学定义
对于二维空间中的两个点 $P_1(x_1, y_1)$ 和 $P_2(x_2, y_2)$:
$$
d_{euclidean}(P_1, P_2) = \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2}
$$
对于 $n$ 维空间中的两个向量 $\mathbf{x} = (x_1, x_2, ..., x_n)$ 和 $\mathbf{y} = (y_1, y_2, ..., y_n)$:
$$
d_{euclidean}(\mathbf{x}, \mathbf{y}) = \sqrt{\sum_{i=1}^{n} (x_i - y_i)^2} = |\mathbf{x} - \mathbf{y}|_2
$$
其中 $|\cdot|_2$ 表示 $L_2$ 范数(欧式范数)。

为什么这样计算?
欧式距离基于勾股定理:
- 二维情况:两点之间的直线距离就是直角三角形的斜边
- 多维情况:将每一维的差异平方后求和,再开平方根
直观理解:
- 就像在平面直角坐标系中,两点之间的最短路径
- 就像考虑所有维度的综合差异
可视化示例

让我们用 matplotlib 可视化欧式距离:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import FancyBboxPatch, FancyArrowPatch
import matplotlib.patches as mpatches
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei']
plt.rcParams['axes.unicode_minus'] = False
# 定义两个点
P1 = np.array([1, 2])
P2 = np.array([5, 6])
# 计算欧式距离
euclidean_dist = np.sqrt(np.sum((P2 - P1)**2))
# 创建图形
fig, ax = plt.subplots(1, 1, figsize=(10, 8))
# 绘制坐标轴
ax.axhline(y=0, color='k', linewidth=0.5, alpha=0.3)
ax.axvline(x=0, color='k', linewidth=0.5, alpha=0.3)
ax.grid(True, alpha=0.3)
# 绘制两点
ax.plot(P1[0], P1[1], 'ro', markersize=15, label=f'点 P1({P1[0]}, {P1[1]})', zorder=5)
ax.plot(P2[0], P2[1], 'bo', markersize=15, label=f'点 P2({P2[0]}, {P2[1]})', zorder=5)
# 绘制直线(欧式距离)
ax.plot([P1[0], P2[0]], [P1[1], P2[1]], 'g-', linewidth=3,
label=f'欧式距离 = {euclidean_dist:.2f}', zorder=3)
# 绘制辅助线(形成直角三角形)
ax.plot([P1[0], P2[0]], [P1[1], P1[1]], 'r--', linewidth=1, alpha=0.5)
ax.plot([P2[0], P2[0]], [P1[1], P2[1]], 'b--', linewidth=1, alpha=0.5)
# 标注距离
mid_x, mid_y = (P1[0] + P2[0]) / 2, (P1[1] + P2[1]) / 2
ax.text(mid_x + 0.3, mid_y + 0.3, f'd = {euclidean_dist:.2f}',
fontsize=14, color='green', weight='bold',
bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))
# 标注坐标
ax.text(P1[0] + 0.1, P1[1] + 0.2, f'({P1[0]}, {P1[1]})', fontsize=12)
ax.text(P2[0] + 0.1, P2[1] + 0.2, f'({P2[0]}, {P2[1]})', fontsize=12)
# 标注各边的长度
dx = P2[0] - P1[0]
dy = P2[1] - P1[1]
ax.text((P1[0] + P2[0]) / 2, P1[1] - 0.3, f'Δx = {dx}',
fontsize=10, ha='center', color='red')
ax.text(P2[0] + 0.2, (P1[1] + P2[1]) / 2, f'Δy = {dy}',
fontsize=10, va='center', color='blue')
# 设置坐标轴
ax.set_xlim(-1, 7)
ax.set_ylim(-1, 8)
ax.set_xlabel('X 轴', fontsize=12)
ax.set_ylabel('Y 轴', fontsize=12)
ax.set_title('欧式距离(Euclidean Distance)可视化', fontsize=16, weight='bold')
ax.legend(loc='best', fontsize=11)
# 添加公式说明
formula_text = r'$d = \sqrt{(x_2-x_1)^2 + (y_2-y_1)^2}$' + f'\n= √({dx}² + {dy}²) = {euclidean_dist:.2f}'
ax.text(0.02, 0.98, formula_text, transform=ax.transAxes,
fontsize=11, verticalalignment='top',
bbox=dict(boxstyle='round', facecolor='lightblue', alpha=0.7))
plt.tight_layout()
plt.savefig('euclidean_distance.png', dpi=300, bbox_inches='tight')
plt.show()
print(f"欧式距离: {euclidean_dist:.4f}")
print(f"计算过程: √(({P2[0]}-{P1[0]})² + ({P2[1]}-{P1[1]})²) = √({dx}² + {dy}²) = {euclidean_dist:.4f}")
代码实现
import numpy as np
def euclidean_distance(x, y):
"""
计算两个向量的欧式距离
参数:
x: 第一个向量(numpy数组)
y: 第二个向量(numpy数组)
返回:
欧式距离
"""
return np.sqrt(np.sum((x - y) ** 2))
# 示例
x = np.array([1, 2, 3])
y = np.array([4, 5, 6])
distance = euclidean_distance(x, y)
print(f"欧式距离: {distance:.4f}")
# 使用 NumPy 的 linalg.norm
distance_numpy = np.linalg.norm(x - y)
print(f"使用 NumPy: {distance_numpy:.4f}")
特点和应用
优点:
- 直观易懂,符合日常经验
- 对旋转和平移不变
- 适用于连续特征
缺点:
- 对异常值敏感
- 高维空间中效果可能不佳(维度灾难)
适用场景:
- 连续数值特征
- 低维或中等维度数据
- 需要综合考虑所有维度差异的场景
曼哈顿距离(Manhattan Distance)
什么是曼哈顿距离?
曼哈顿距离也称为城市街区距离(City Block Distance)或L1距离,得名于曼哈顿的街道布局——只能沿着街道走,不能斜穿。
类比理解:
- 就像在城市中,只能沿着街道走,不能穿墙
- 就像棋盘上,车(Rook)的移动方式
- 就像出租车在城市中行驶的距离

数学定义
对于二维空间中的两个点 $P_1(x_1, y_1)$ 和 $P_2(x_2, y_2)$:
$$
d_{manhattan}(P_1, P_2) = |x_2 - x_1| + |y_2 - y_1|
$$
对于 $n$ 维空间中的两个向量 $\mathbf{x} = (x_1, x_2, ..., x_n)$ 和 $\mathbf{y} = (y_1, y_2, ..., y_n)$:
$$
d_{manhattan}(\mathbf{x}, \mathbf{y}) = \sum_{i=1}^{n} |x_i - y_i| = |\mathbf{x} - \mathbf{y}|_1
$$
其中 $|\cdot|_1$ 表示 $L_1$ 范数(曼哈顿范数)。

为什么这样计算?
曼哈顿距离基于只能沿坐标轴移动的假设:
- 二维情况:只能先水平移动,再垂直移动(或相反)
- 多维情况:每一维的差异绝对值之和
直观理解:
- 就像在城市中,只能沿着街道走,不能斜穿建筑物
- 就像考虑"路径"而不是"直线"
可视化示例

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei']
plt.rcParams['axes.unicode_minus'] = False
# 定义两个点
P1 = np.array([1, 2])
P2 = np.array([5, 6])
# 计算曼哈顿距离
manhattan_dist = np.sum(np.abs(P2 - P1))
dx = abs(P2[0] - P1[0])
dy = abs(P2[1] - P1[1])
# 创建图形
fig, ax = plt.subplots(1, 1, figsize=(12, 10))
# 绘制坐标轴
ax.axhline(y=0, color='k', linewidth=0.5, alpha=0.3)
ax.axvline(x=0, color='k', linewidth=0.5, alpha=0.3)
ax.grid(True, alpha=0.3)
# 绘制两点
ax.plot(P1[0], P1[1], 'ro', markersize=18, label=f'起点 P1({P1[0]}, {P1[1]})', zorder=10)
ax.plot(P2[0], P2[1], 'bo', markersize=18, label=f'终点 P2({P2[0]}, {P2[1]})', zorder=10)
# 定义多条曼哈顿路径
paths = []
# 路径1:先水平,后垂直(最简单)
paths.append({
'points': [(P1[0], P1[1]), (P2[0], P1[1]), (P2[0], P2[1])],
'label': '路径1: 先水平后垂直',
'color': 'green',
'style': '-',
'width': 2.5
})
# 路径2:先垂直,后水平
paths.append({
'points': [(P1[0], P1[1]), (P1[0], P2[1]), (P2[0], P2[1])],
'label': '路径2: 先垂直后水平',
'color': 'orange',
'style': '--',
'width': 2.5
})
# 路径3:先水平一部分,再垂直,再水平(中间点1)
mid_x1 = P1[0] + dx * 0.3
paths.append({
'points': [(P1[0], P1[1]), (mid_x1, P1[1]), (mid_x1, P2[1]), (P2[0], P2[1])],
'label': '路径3: 水平→垂直→水平',
'color': 'purple',
'style': '-.',
'width': 2
})
# 路径4:先垂直一部分,再水平,再垂直(中间点2)
mid_y1 = P1[1] + dy * 0.4
paths.append({
'points': [(P1[0], P1[1]), (P1[0], mid_y1), (P2[0], mid_y1), (P2[0], P2[1])],
'label': '路径4: 垂直→水平→垂直',
'color': 'brown',
'style': ':',
'width': 2
})
# 路径5:先水平大部分,再垂直,再水平一小段(中间点3)
mid_x2 = P1[0] + dx * 0.7
paths.append({
'points': [(P1[0], P1[1]), (mid_x2, P1[1]), (mid_x2, P2[1]), (P2[0], P2[1])],
'label': '路径5: 水平→垂直→水平',
'color': 'teal',
'style': '-',
'width': 1.8
})
# 路径6:先垂直大部分,再水平,再垂直一小段(中间点4)
mid_y2 = P1[1] + dy * 0.6
paths.append({
'points': [(P1[0], P1[1]), (P1[0], mid_y2), (P2[0], mid_y2), (P2[0], P2[1])],
'label': '路径6: 垂直→水平→垂直',
'color': 'crimson',
'style': '--',
'width': 1.8
})
# 路径7:多段路径(先水平→垂直→水平→垂直)
mid_x3 = P1[0] + dx * 0.5
mid_y3 = P1[1] + dy * 0.5
paths.append({
'points': [(P1[0], P1[1]), (mid_x3, P1[1]), (mid_x3, mid_y3), (P2[0], mid_y3), (P2[0], P2[1])],
'label': '路径7: 多段折线',
'color': 'darkblue',
'style': '-.',
'width': 1.5
})
# 路径8:先水平→垂直→水平→垂直(另一条多段路径)
mid_x4 = P1[0] + dx * 0.25
mid_y4 = P1[1] + dy * 0.75
paths.append({
'points': [(P1[0], P1[1]), (mid_x4, P1[1]), (mid_x4, mid_y4), (P2[0], mid_y4), (P2[0], P2[1])],
'label': '路径8: 多段折线',
'color': 'magenta',
'style': ':',
'width': 1.5
})
# 绘制所有路径
for i, path in enumerate(paths):
x_coords = [p[0] for p in path['points']]
y_coords = [p[1] for p in path['points']]
ax.plot(x_coords, y_coords,
color=path['color'],
linestyle=path['style'],
linewidth=path['width'],
alpha=0.7,
label=path['label'],
zorder=3 + i)
# 绘制欧式距离(直线)作为对比
ax.plot([P1[0], P2[0]], [P1[1], P2[1]], 'r--', linewidth=2.5,
alpha=0.6, label='欧式距离(直线对比)', zorder=2)
# 标注距离
mid_x, mid_y = (P1[0] + P2[0]) / 2, (P1[1] + P2[1]) / 2
ax.text(mid_x, P1[1] - 0.6, f'曼哈顿距离 = {manhattan_dist}(所有路径长度相同)',
fontsize=14, color='green', weight='bold', ha='center',
bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.7))
# 标注坐标
ax.text(P1[0] + 0.15, P1[1] + 0.25, f'({P1[0]}, {P1[1]})', fontsize=12, weight='bold')
ax.text(P2[0] + 0.15, P2[1] + 0.25, f'({P2[0]}, {P2[1]})', fontsize=12, weight='bold')
# 标注各段的长度
ax.text((P1[0] + P2[0]) / 2, P1[1] - 0.3, f'|Δx| = {dx}',
fontsize=11, ha='center', color='blue', weight='bold',
bbox=dict(boxstyle='round', facecolor='lightblue', alpha=0.5))
ax.text(P2[0] + 0.3, (P1[1] + P2[1]) / 2, f'|Δy| = {dy}',
fontsize=11, va='center', color='blue', weight='bold',
bbox=dict(boxstyle='round', facecolor='lightblue', alpha=0.5))
# 设置坐标轴
ax.set_xlim(-0.5, 7.5)
ax.set_ylim(-1, 8.5)
ax.set_xlabel('X 轴', fontsize=13)
ax.set_ylabel('Y 轴', fontsize=13)
ax.set_title('曼哈顿距离(Manhattan Distance)可视化 - 多条路径', fontsize=16, weight='bold')
# 调整图例位置和大小
ax.legend(loc='best', fontsize=9, framealpha=0.9, ncol=1)
# 添加公式说明
formula_text = r'$d = |x_2-x_1| + |y_2-y_1|$' + f'\n= |{P2[0]-P1[0]}| + |{P2[1]-P1[1]}| = {dx} + {dy} = {manhattan_dist}'
ax.text(0.02, 0.98, formula_text, transform=ax.transAxes,
fontsize=11, verticalalignment='top',
bbox=dict(boxstyle='round', facecolor='lightgreen', alpha=0.8))
# 添加说明文字
note_text = '所有路径长度相同\n曼哈顿距离 = 水平距离 + 垂直距离'
ax.text(0.98, 0.02, note_text, transform=ax.transAxes,
fontsize=10, verticalalignment='bottom', horizontalalignment='right',
bbox=dict(boxstyle='round', facecolor='yellow', alpha=0.6))
plt.tight_layout()
plt.savefig('manhattan_distance.png', dpi=300, bbox_inches='tight')
plt.show()
print(f"曼哈顿距离: {manhattan_dist}")
print(f"计算过程: |{P2[0]}-{P1[0]}| + |{P2[1]}-{P1[1]}| = {dx} + {dy} = {manhattan_dist}")
print(f"\n共绘制了 {len(paths)} 条不同的曼哈顿路径,所有路径长度都是 {manhattan_dist}")
代码实现
import numpy as np
def manhattan_distance(x, y):
"""
计算两个向量的曼哈顿距离
参数:
x: 第一个向量(numpy数组)
y: 第二个向量(numpy数组)
返回:
曼哈顿距离
"""
return np.sum(np.abs(x - y))
# 示例
x = np.array([1, 2, 3])
y = np.array([4, 5, 6])
distance = manhattan_distance(x, y)
print(f"曼哈顿距离: {distance}")
# 使用 NumPy 的 linalg.norm
distance_numpy = np.linalg.norm(x - y, ord=1)
print(f"使用 NumPy: {distance_numpy}")
特点和应用
优点:
- 对异常值不敏感(使用绝对值)
- 计算简单快速
- 适用于稀疏特征
缺点:
- 不考虑对角线移动
- 在某些场景下可能不够精确
适用场景:
- 离散特征
- 高维稀疏数据
- 需要快速计算的场景
- 对异常值敏感的数据
切比雪夫距离(Chebyshev Distance)
什么是切比雪夫距离?
切比雪夫距离也称为棋盘距离(Chessboard Distance)或L∞距离,得名于国际象棋中王的移动方式——可以朝8个方向移动,每次移动一格。
类比理解:
- 就像国际象棋中,王移动到目标位置所需的步数
- 就像只关心最大差异的场景
- 就像"最坏情况"的距离

数学定义
对于二维空间中的两个点 $P_1(x_1, y_1)$ 和 $P_2(x_2, y_2)$:
$$
d_{chebyshev}(P_1, P_2) = \max(|x_2 - x_1|, |y_2 - y_1|)
$$
对于 $n$ 维空间中的两个向量 $\mathbf{x} = (x_1, x_2, ..., x_n)$ 和 $\mathbf{y} = (y_1, y_2, ..., y_n)$:
$$
d_{chebyshev}(\mathbf{x}, \mathbf{y}) = \max_{i=1}^{n} |x_i - y_i| = |\mathbf{x} - \mathbf{y}|_\infty
$$
其中 $|\cdot|\infty$ 表示 $L\infty$ 范数(切比雪夫范数)。

为什么这样计算?
切比雪夫距离基于只关心最大差异的假设:
- 二维情况:取水平和垂直方向差异的最大值
- 多维情况:取所有维度差异绝对值的最大值
直观理解:
- 就像国际象棋中,王可以朝任意方向移动,移动到目标位置所需的步数就是最大坐标差
- 就像只关心"最坏情况",即最大差异
可视化示例

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei']
plt.rcParams['axes.unicode_minus'] = False
# 定义两个点
P1 = np.array([1, 2])
P2 = np.array([5, 6])
# 计算切比雪夫距离
chebyshev_dist = np.max(np.abs(P2 - P1))
# 创建图形
fig, ax = plt.subplots(1, 1, figsize=(10, 8))
# 绘制坐标轴
ax.axhline(y=0, color='k', linewidth=0.5, alpha=0.3)
ax.axvline(x=0, color='k', linewidth=0.5, alpha=0.3)
ax.grid(True, alpha=0.3)
# 绘制两点
ax.plot(P1[0], P1[1], 'ro', markersize=15, label=f'点 P1({P1[0]}, {P1[1]})', zorder=5)
ax.plot(P2[0], P2[1], 'bo', markersize=15, label=f'点 P2({P2[0]}, {P2[1]})', zorder=5)
# 绘制切比雪夫距离的"正方形"(王的移动范围)
# 以P1为中心,边长为2*chebyshev_dist的正方形
square_size = chebyshev_dist
square = Rectangle((P1[0] - square_size, P1[1] - square_size),
2 * square_size, 2 * square_size,
linewidth=2, edgecolor='purple', facecolor='purple',
alpha=0.2, label=f'切比雪夫距离范围', zorder=1)
ax.add_patch(square)
# 绘制从P1到P2的路径(王的移动方式:先对角线,再直线)
dx = P2[0] - P1[0]
dy = P2[1] - P1[1]
min_diff = min(abs(dx), abs(dy))
max_diff = max(abs(dx), abs(dy))
# 绘制路径
if abs(dx) >= abs(dy):
# 先对角线移动,再水平移动
ax.plot([P1[0], P1[0] + np.sign(dx) * min_diff, P2[0]],
[P1[1], P1[1] + np.sign(dy) * min_diff, P2[1]],
'purple', linewidth=3, label='切比雪夫路径', zorder=3)
else:
# 先对角线移动,再垂直移动
ax.plot([P1[0], P1[0] + np.sign(dx) * min_diff, P2[0]],
[P1[1], P1[1] + np.sign(dy) * min_diff, P2[1]],
'purple', linewidth=3, label='切比雪夫路径', zorder=3)
# 绘制欧式距离(直线)作为对比
ax.plot([P1[0], P2[0]], [P1[1], P2[1]], 'r--', linewidth=2,
alpha=0.5, label='欧式距离(对比)', zorder=2)
# 标注距离
mid_x, mid_y = (P1[0] + P2[0]) / 2, (P1[1] + P2[1]) / 2
ax.text(mid_x, mid_y + 0.5, f'切比雪夫距离 = {chebyshev_dist}',
fontsize=14, color='purple', weight='bold', ha='center',
bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))
# 标注坐标
ax.text(P1[0] + 0.1, P1[1] + 0.2, f'({P1[0]}, {P1[1]})', fontsize=12)
ax.text(P2[0] + 0.1, P2[1] + 0.2, f'({P2[0]}, {P2[1]})', fontsize=12)
# 标注各维度的差异
dx_abs = abs(dx)
dy_abs = abs(dy)
ax.text((P1[0] + P2[0]) / 2, P1[1] - 0.3, f'|Δx| = {dx_abs}',
fontsize=10, ha='center', color='blue')
ax.text(P2[0] + 0.2, (P1[1] + P2[1]) / 2, f'|Δy| = {dy_abs}',
fontsize=10, va='center', color='blue')
ax.text(P2[0] + 0.5, P2[1] + 0.3, f'max(|Δx|, |Δy|) = {chebyshev_dist}',
fontsize=11, color='purple', weight='bold',
bbox=dict(boxstyle='round', facecolor='lavender', alpha=0.7))
# 设置坐标轴
ax.set_xlim(-1, 7)
ax.set_ylim(-1, 8)
ax.set_xlabel('X 轴', fontsize=12)
ax.set_ylabel('Y 轴', fontsize=12)
ax.set_title('切比雪夫距离(Chebyshev Distance)可视化', fontsize=16, weight='bold')
ax.legend(loc='best', fontsize=11)
# 添加公式说明
formula_text = r'$d = \max(|x_2-x_1|, |y_2-y_1|)$' + f'\n= max(|{dx}|, |{dy}|) = max({dx_abs}, {dy_abs}) = {chebyshev_dist}'
ax.text(0.02, 0.98, formula_text, transform=ax.transAxes,
fontsize=11, verticalalignment='top',
bbox=dict(boxstyle='round', facecolor='lavender', alpha=0.7))
plt.tight_layout()
plt.savefig('chebyshev_distance.png', dpi=300, bbox_inches='tight')
plt.show()
print(f"切比雪夫距离: {chebyshev_dist}")
print(f"计算过程: max(|{P2[0]}-{P1[0]}|, |{P2[1]}-{P1[1]}|) = max({dx_abs}, {dy_abs}) = {chebyshev_dist}")
代码实现
import numpy as np
def chebyshev_distance(x, y):
"""
计算两个向量的切比雪夫距离
参数:
x: 第一个向量(numpy数组)
y: 第二个向量(numpy数组)
返回:
切比雪夫距离
"""
return np.max(np.abs(x - y))
# 示例
x = np.array([1, 2, 3])
y = np.array([4, 5, 6])
distance = chebyshev_distance(x, y)
print(f"切比雪夫距离: {distance}")
# 使用 NumPy 的 linalg.norm
distance_numpy = np.linalg.norm(x - y, ord=np.inf)
print(f"使用 NumPy: {distance_numpy}")
特点和应用
优点:
- 只关心最大差异,计算简单
- 对异常值不敏感(只取最大值)
- 适用于只关心最坏情况的场景
缺点:
- 忽略了其他维度的信息
- 可能不够精确
适用场景:
- 只关心最大差异的场景
- 高维数据中某些维度特别重要
- 需要快速判断"最坏情况"
闵可夫斯基距离(Minkowski Distance)
什么是闵可夫斯基距离?
闵可夫斯基距离是前面三种距离的通用形式,通过参数 $p$ 可以控制距离的计算方式。
类比理解:
- 就像一把"万能尺子",可以调节测量方式
- 就像前面三种距离的"统一公式"
- 就像根据 $p$ 值选择不同的距离度量
数学定义
对于 $n$ 维空间中的两个向量 $\mathbf{x} = (x_1, x_2, ..., x_n)$ 和 $\mathbf{y} = (y_1, y_2, ..., y_n)$:
$$
d_{minkowski}(\mathbf{x}, \mathbf{y}; p) = \left(\sum_{i=1}^{n} |x_i - y_i|^p\right)^{1/p} = |\mathbf{x} - \mathbf{y}|_p
$$
其中 $p \geq 1$ 是参数,$|\cdot|_p$ 表示 $L_p$ 范数。

特殊情况的统一
闵可夫斯基距离包含了前面三种距离作为特殊情况:
-
$p = 1$:曼哈顿距离
$$
d_{minkowski}(\mathbf{x}, \mathbf{y}; 1) = \sum_{i=1}^{n} |x_i - y_i| = d_{manhattan}(\mathbf{x}, \mathbf{y})
$$ -
$p = 2$:欧式距离
$$
d_{minkowski}(\mathbf{x}, \mathbf{y}; 2) = \sqrt{\sum_{i=1}^{n} (x_i - y_i)^2} = d_{euclidean}(\mathbf{x}, \mathbf{y})
$$ -
$p \to \infty$:切比雪夫距离
$$
\lim_{p \to \infty} d_{minkowski}(\mathbf{x}, \mathbf{y}; p) = \max_{i=1}^{n} |x_i - y_i| = d_{chebyshev}(\mathbf{x}, \mathbf{y})
$$
为什么这样计算?
闵可夫斯基距离基于**$L_p$ 范数**的概念:
- $p$ 值越小:越关注所有维度的差异
- $p$ 值越大:越关注最大差异
- $p = 1$:平均差异(曼哈顿)
- $p = 2$:综合差异(欧式)
- $p \to \infty$:最大差异(切比雪夫)
直观理解:
- 就像调节"权重",决定如何综合各维度的差异
- 就像根据场景选择合适的距离度量
可视化示例:不同p值的影响

import numpy as np
import matplotlib.pyplot as plt
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei']
plt.rcParams['axes.unicode_minus'] = False
# 定义两个点
P1 = np.array([1, 2])
P2 = np.array([5, 6])
# 计算不同p值的闵可夫斯基距离
p_values = [0.5, 1, 1.5, 2, 3, 5, 10, 50, 100]
distances = []
for p in p_values:
if p == np.inf:
dist = np.max(np.abs(P2 - P1))
else:
dist = np.power(np.sum(np.power(np.abs(P2 - P1), p)), 1/p)
distances.append(dist)
# 添加切比雪夫距离(p=∞)
p_values.append(np.inf)
distances.append(np.max(np.abs(P2 - P1)))
# 创建图形
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))
# 左图:不同p值的距离值
ax1.plot(p_values[:-1], distances[:-1], 'bo-', linewidth=2, markersize=8, label='闵可夫斯基距离')
ax1.axhline(y=distances[-1], color='r', linestyle='--', linewidth=2, label='切比雪夫距离 (p=∞)')
ax1.axvline(x=1, color='g', linestyle=':', alpha=0.5, label='p=1 (曼哈顿)')
ax1.axvline(x=2, color='orange', linestyle=':', alpha=0.5, label='p=2 (欧式)')
ax1.set_xlabel('p 值', fontsize=12)
ax1.set_ylabel('距离值', fontsize=12)
ax1.set_title('不同 p 值的闵可夫斯基距离', fontsize=14, weight='bold')
ax1.grid(True, alpha=0.3)
ax1.legend(fontsize=10)
ax1.set_xscale('log')
# 标注特殊点
ax1.annotate(f'p=1 (曼哈顿)\n距离={distances[1]:.2f}',
xy=(1, distances[1]), xytext=(1.5, distances[1] + 0.5),
arrowprops=dict(arrowstyle='->', color='green'),
fontsize=10, color='green', weight='bold')
ax1.annotate(f'p=2 (欧式)\n距离={distances[3]:.2f}',
xy=(2, distances[3]), xytext=(3, distances[3] + 0.5),
arrowprops=dict(arrowstyle='->', color='orange'),
fontsize=10, color='orange', weight='bold')
ax1.annotate(f'p=∞ (切比雪夫)\n距离={distances[-1]:.2f}',
xy=(100, distances[-1]), xytext=(50, distances[-1] + 0.5),
arrowprops=dict(arrowstyle='->', color='red'),
fontsize=10, color='red', weight='bold')
# 右图:可视化不同p值的"等距离线"
ax2.axhline(y=0, color='k', linewidth=0.5, alpha=0.3)
ax2.axvline(x=0, color='k', linewidth=0.5, alpha=0.3)
ax2.grid(True, alpha=0.3)
# 绘制中心点
ax2.plot(P1[0], P1[1], 'ro', markersize=15, label=f'中心点 P1({P1[0]}, {P1[1]})', zorder=5)
# 绘制不同p值的等距离线
from matplotlib.patches import Circle
import matplotlib.patches as mpatches
# 生成角度
theta = np.linspace(0, 2*np.pi, 1000)
# 选择几个代表性的p值
selected_p = [1, 2, np.inf]
colors = ['green', 'orange', 'red']
labels_p = ['p=1 (曼哈顿)', 'p=2 (欧式)', 'p=∞ (切比雪夫)']
for idx, p in enumerate(selected_p):
if p == np.inf:
# 切比雪夫:正方形
dist = distances[-1]
square = Rectangle((P1[0] - dist, P1[1] - dist),
2 * dist, 2 * dist,
linewidth=2, edgecolor=colors[idx],
facecolor='none', linestyle='--',
label=labels_p[idx], zorder=3)
ax2.add_patch(square)
else:
# 其他p值:使用参数方程绘制
dist = distances[p_values.index(p)]
# 对于闵可夫斯基距离,等距离线的形状取决于p值
# 这里用近似方法绘制
if p == 1:
# 曼哈顿:菱形
diamond_x = P1[0] + dist * np.sign(np.cos(theta)) * np.abs(np.cos(theta))
diamond_y = P1[1] + dist * np.sign(np.sin(theta)) * np.abs(np.sin(theta))
ax2.plot(diamond_x, diamond_y, colors[idx], linewidth=2,
linestyle='--', label=labels_p[idx], zorder=3)
elif p == 2:
# 欧式:圆形
circle = Circle((P1[0], P1[1]), dist,
linewidth=2, edgecolor=colors[idx],
facecolor='none', linestyle='--',
label=labels_p[idx], zorder=3)
ax2.add_patch(circle)
ax2.set_xlim(-2, 8)
ax2.set_ylim(-2, 8)
ax2.set_xlabel('X 轴', fontsize=12)
ax2.set_ylabel('Y 轴', fontsize=12)
ax2.set_title('不同 p 值的等距离线形状', fontsize=14, weight='bold')
ax2.legend(loc='upper left', fontsize=10)
ax2.set_aspect('equal')
plt.tight_layout()
plt.savefig('minkowski_distance.png', dpi=300, bbox_inches='tight')
plt.show()
# 打印结果
print("不同 p 值的闵可夫斯基距离:")
for i, p in enumerate(p_values):
if p == np.inf:
print(f"p = ∞ (切比雪夫): {distances[i]:.4f}")
else:
print(f"p = {p:6.2f}: {distances[i]:.4f}")
代码实现
import numpy as np
def minkowski_distance(x, y, p=2):
"""
计算两个向量的闵可夫斯基距离
参数:
x: 第一个向量(numpy数组)
y: 第二个向量(numpy数组)
p: 距离参数
p=1: 曼哈顿距离
p=2: 欧式距离
p=∞: 切比雪夫距离
返回:
闵可夫斯基距离
"""
if p == np.inf:
return np.max(np.abs(x - y))
elif p == 1:
return np.sum(np.abs(x - y))
elif p == 2:
return np.sqrt(np.sum((x - y) ** 2))
else:
return np.power(np.sum(np.power(np.abs(x - y), p)), 1/p)
# 示例
x = np.array([1, 2, 3])
y = np.array([4, 5, 6])
print(f"p=1 (曼哈顿): {minkowski_distance(x, y, p=1):.4f}")
print(f"p=2 (欧式): {minkowski_distance(x, y, p=2):.4f}")
print(f"p=∞ (切比雪夫): {minkowski_distance(x, y, p=np.inf):.4f}")
print(f"p=3: {minkowski_distance(x, y, p=3):.4f}")
# 使用 NumPy 的 linalg.norm
print(f"\n使用 NumPy:")
print(f"p=1: {np.linalg.norm(x - y, ord=1):.4f}")
print(f"p=2: {np.linalg.norm(x - y, ord=2):.4f}")
print(f"p=∞: {np.linalg.norm(x - y, ord=np.inf):.4f}")
标准化欧式距离(Standardized Euclidean Distance)
什么是标准化欧式距离?
标准化欧式距离是对欧式距离的改进,通过标准化每个特征,消除不同特征尺度的影响。
类比理解:
- 就像比较身高和体重时,需要先统一单位
- 就像不同科目的成绩需要标准化后才能比较
- 就像消除"单位"的影响,只关注"相对差异"
为什么需要标准化?
**问题:**不同特征的量纲和尺度不同,直接计算欧式距离会被大尺度特征主导。
示例:
- 特征1:身高(单位:厘米,范围:150-200)
- 特征2:体重(单位:千克,范围:50-100)
如果不标准化,身高的差异(50厘米)会远大于体重的差异(50千克),导致距离计算被身高主导。
数学定义
对于 $n$ 维空间中的两个向量 $\mathbf{x} = (x_1, x_2, ..., x_n)$ 和 $\mathbf{y} = (y_1, y_2, ..., y_n)$:
$$
d_{standardized}(\mathbf{x}, \mathbf{y}) = \sqrt{\sum_{i=1}^{n} \left(\frac{x_i - y_i}{\sigma_i}\right)^2}
$$
其中 $\sigma_i$ 是第 $i$ 个特征的标准差。
标准化过程:
- 计算每个特征的均值和标准差
- 对每个特征进行标准化:$z_i = \frac{x_i - \mu_i}{\sigma_i}$
- 在标准化后的空间计算欧式距离

可视化示例

import numpy as np
import matplotlib.pyplot as plt
from sklearn.preprocessing import StandardScaler
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei']
plt.rcParams['axes.unicode_minus'] = False
# 创建示例数据(不同尺度的特征)
np.random.seed(42)
X = np.random.randn(100, 2)
X[:, 0] = X[:, 0] * 100 + 170 # 身高:均值170,标准差100
X[:, 1] = X[:, 1] * 20 + 70 # 体重:均值70,标准差20
# 选择两个点
P1_idx, P2_idx = 0, 1
P1 = X[P1_idx]
P2 = X[P2_idx]
# 计算标准化前后的距离
euclidean_dist = np.sqrt(np.sum((P2 - P1)**2))
# 标准化
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
P1_scaled = X_scaled[P1_idx]
P2_scaled = X_scaled[P2_idx]
standardized_dist = np.sqrt(np.sum((P2_scaled - P1_scaled)**2))
# 创建图形
fig, axes = plt.subplots(1, 2, figsize=(16, 7))
# 左图:标准化前
ax1 = axes[0]
ax1.scatter(X[:, 0], X[:, 1], alpha=0.3, s=30, c='gray', label='其他点')
ax1.plot(P1[0], P1[1], 'ro', markersize=15, label=f'点 P1({P1[0]:.1f}, {P1[1]:.1f})', zorder=5)
ax1.plot(P2[0], P2[1], 'bo', markersize=15, label=f'点 P2({P2[0]:.1f}, {P2[1]:.1f})', zorder=5)
ax1.plot([P1[0], P2[0]], [P1[1], P2[1]], 'g-', linewidth=2,
label=f'欧式距离 = {euclidean_dist:.2f}', zorder=3)
ax1.set_xlabel('身高 (cm)', fontsize=12)
ax1.set_ylabel('体重 (kg)', fontsize=12)
ax1.set_title('标准化前:不同尺度特征', fontsize=14, weight='bold')
ax1.legend(fontsize=10)
ax1.grid(True, alpha=0.3)
# 右图:标准化后
ax2 = axes[1]
ax2.scatter(X_scaled[:, 0], X_scaled[:, 1], alpha=0.3, s=30, c='gray', label='其他点')
ax2.plot(P1_scaled[0], P1_scaled[1], 'ro', markersize=15,
label=f'点 P1({P1_scaled[0]:.2f}, {P1_scaled[1]:.2f})', zorder=5)
ax2.plot(P2_scaled[0], P2_scaled[1], 'bo', markersize=15,
label=f'点 P2({P2_scaled[0]:.2f}, {P2_scaled[1]:.2f})', zorder=5)
ax2.plot([P1_scaled[0], P2_scaled[0]], [P1_scaled[1], P2_scaled[1]],
'purple', linewidth=2, label=f'标准化欧式距离 = {standardized_dist:.2f}', zorder=3)
ax2.set_xlabel('标准化身高', fontsize=12)
ax2.set_ylabel('标准化体重', fontsize=12)
ax2.set_title('标准化后:统一尺度', fontsize=14, weight='bold')
ax2.legend(fontsize=10)
ax2.grid(True, alpha=0.3)
ax2.set_aspect('equal')
plt.suptitle('标准化欧式距离对比', fontsize=16, weight='bold')
plt.tight_layout()
plt.savefig('standardized_euclidean_distance.png', dpi=300, bbox_inches='tight')
plt.show()
print(f"标准化前欧式距离: {euclidean_dist:.4f}")
print(f"标准化后欧式距离: {standardized_dist:.4f}")
代码实现
import numpy as np
from sklearn.preprocessing import StandardScaler
def standardized_euclidean_distance(x, y, X=None):
"""
计算标准化欧式距离
参数:
x: 第一个向量
y: 第二个向量
X: 用于计算标准差的参考数据集(可选)
返回:
标准化欧式距离
"""
if X is not None:
# 使用参考数据集计算标准差
scaler = StandardScaler()
scaler.fit(X)
x_scaled = scaler.transform(x.reshape(1, -1))[0]
y_scaled = scaler.transform(y.reshape(1, -1))[0]
else:
# 使用两个向量的标准差
std = np.std([x, y], axis=0)
std[std == 0] = 1 # 避免除零
x_scaled = (x - np.mean([x, y], axis=0)) / std
y_scaled = (y - np.mean([x, y], axis=0)) / std
return np.sqrt(np.sum((x_scaled - y_scaled)**2))
# 示例
x = np.array([170, 70])
y = np.array([180, 75])
X_ref = np.random.randn(100, 2)
X_ref[:, 0] = X_ref[:, 0] * 100 + 170
X_ref[:, 1] = X_ref[:, 1] * 20 + 70
distance = standardized_euclidean_distance(x, y, X_ref)
print(f"标准化欧式距离: {distance:.4f}")
特点和应用
优点:
- 消除不同特征尺度的影响
- 更公平地衡量各维度的差异
- 适用于特征尺度差异大的数据
缺点:
- 需要额外的标准化步骤
- 需要参考数据集计算标准差
适用场景:
- 特征尺度差异大的数据
- 需要公平对待所有特征的场景
- 多特征综合比较
余弦距离(Cosine Distance)
什么是余弦距离?
余弦距离基于向量之间的夹角,衡量的是方向相似性而不是距离大小。
类比理解:
- 就像比较两个箭头的方向,而不是长度
- 就像比较两个文档的主题相似性
- 就像只关心"方向"而不关心"大小"
数学定义
对于两个向量 $\mathbf{x}$ 和 $\mathbf{y}$:
余弦相似度(Cosine Similarity):
$$
\text{cosine_similarity}(\mathbf{x}, \mathbf{y}) = \frac{\mathbf{x} \cdot \mathbf{y}}{|\mathbf{x}| |\mathbf{y}|} = \frac{\sum_{i=1}^{n} x_i y_i}{\sqrt{\sum_{i=1}^{n} x_i^2} \sqrt{\sum_{i=1}^{n} y_i^2}}
$$
余弦距离(Cosine Distance):
$$
d_{cosine}(\mathbf{x}, \mathbf{y}) = 1 - \text{cosine_similarity}(\mathbf{x}, \mathbf{y}) = 1 - \frac{\mathbf{x} \cdot \mathbf{y}}{|\mathbf{x}| |\mathbf{y}|}
$$
二维空间中向量A(x1,y1)与向量B(x2,y2)的夹角余弦公式:

两个n维样本点a(x11,x12,…,x1n)和b(x21,x22,…,x2n)的夹角余弦为:

即:

夹角余弦取值范围为[-1,1]。余弦越大表示两个向量的夹角越小,余弦越小表示两向量的夹角越大。当两个向量的方向重合时余弦取最大值1,当两个向量的方向完全相反余弦取最小值-1。
为什么这样计算?
余弦距离关注的是方向而不是大小:
- 夹角为 0°:向量方向相同,余弦相似度为 1,距离为 0
- 夹角为 90°:向量垂直,余弦相似度为 0,距离为 1
- 夹角为 180°:向量方向相反,余弦相似度为 -1,距离为 2
直观理解:
- 就像比较两个文档的主题,不关心文档长度
- 就像比较两个用户的兴趣方向,不关心活跃度
可视化示例

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Arc
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei']
plt.rcParams['axes.unicode_minus'] = False
# 定义两个向量
v1 = np.array([3, 4])
v2 = np.array([4, 3])
# 计算余弦相似度和距离
cosine_sim = np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))
cosine_dist = 1 - cosine_sim
angle = np.arccos(cosine_sim) * 180 / np.pi
# 创建图形
fig, ax = plt.subplots(1, 1, figsize=(10, 10))
# 绘制坐标轴
ax.axhline(y=0, color='k', linewidth=0.5, alpha=0.3)
ax.axvline(x=0, color='k', linewidth=0.5, alpha=0.3)
ax.grid(True, alpha=0.3)
# 绘制向量
ax.arrow(0, 0, v1[0], v1[1], head_width=0.3, head_length=0.3,
fc='red', ec='red', linewidth=3, label=f'向量 v1({v1[0]}, {v1[1]})', zorder=5)
ax.arrow(0, 0, v2[0], v2[1], head_width=0.3, head_length=0.3,
fc='blue', ec='blue', linewidth=3, label=f'向量 v2({v2[0]}, {v2[1]})', zorder=5)
# 绘制角度弧
arc_radius = 1.5
arc = Arc((0, 0), arc_radius*2, arc_radius*2, angle=0,
theta1=0, theta2=angle, color='green', linewidth=2, zorder=3)
ax.add_patch(arc)
# 标注角度
angle_text_x = arc_radius * 0.7 * np.cos(np.radians(angle/2))
angle_text_y = arc_radius * 0.7 * np.sin(np.radians(angle/2))
ax.text(angle_text_x, angle_text_y, f'{angle:.1f}°', fontsize=12,
color='green', weight='bold', ha='center',
bbox=dict(boxstyle='round', facecolor='lightgreen', alpha=0.7))
# 标注向量长度
ax.text(v1[0]/2 + 0.2, v1[1]/2 + 0.2, f'|v1|={np.linalg.norm(v1):.2f}',
fontsize=10, color='red')
ax.text(v2[0]/2 + 0.2, v2[1]/2 - 0.3, f'|v2|={np.linalg.norm(v2):.2f}',
fontsize=10, color='blue')
# 标注余弦距离
ax.text(2, 5, f'余弦相似度 = {cosine_sim:.4f}\n余弦距离 = {cosine_dist:.4f}\n夹角 = {angle:.2f}°',
fontsize=12, weight='bold',
bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.7))
# 设置坐标轴
ax.set_xlim(-1, 6)
ax.set_ylim(-1, 6)
ax.set_xlabel('X 轴', fontsize=12)
ax.set_ylabel('Y 轴', fontsize=12)
ax.set_title('余弦距离可视化', fontsize=16, weight='bold')
ax.legend(loc='upper left', fontsize=11)
ax.set_aspect('equal')
plt.tight_layout()
plt.savefig('cosine_distance.png', dpi=300, bbox_inches='tight')
plt.show()
print(f"余弦相似度: {cosine_sim:.4f}")
print(f"余弦距离: {cosine_dist:.4f}")
print(f"夹角: {angle:.2f}°")
代码实现
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
def cosine_distance(x, y):
"""
计算余弦距离
参数:
x: 第一个向量
y: 第二个向量
返回:
余弦距离
"""
cosine_sim = np.dot(x, y) / (np.linalg.norm(x) * np.linalg.norm(y))
return 1 - cosine_sim
# 示例
x = np.array([1, 2, 3])
y = np.array([4, 5, 6])
distance = cosine_distance(x, y)
print(f"余弦距离: {distance:.4f}")
# 使用 sklearn
distance_sklearn = 1 - cosine_similarity([x], [y])[0][0]
print(f"使用 sklearn: {distance_sklearn:.4f}")
特点和应用
优点:
- 不受向量长度影响,只关注方向
- 适用于文本相似度计算
- 对稀疏数据友好
缺点:
- 不考虑向量的大小
- 在某些场景下可能不够精确
适用场景:
- 文本相似度计算
- 推荐系统(用户兴趣方向)
- 高维稀疏数据
- 只关心方向不关心大小的场景
汉明距离(Hamming Distance)
什么是汉明距离?
汉明距离用于衡量两个等长字符串或二进制向量之间的差异,表示需要改变多少位才能从一个变成另一个。
类比理解:
- 就像比较两个密码,看有多少位不同
- 就像比较两个二进制编码,看有多少位不同
- 就像比较两个字符串,看有多少字符不同
数学定义
对于两个等长的字符串或向量 $\mathbf{x} = (x_1, x_2, ..., x_n)$ 和 $\mathbf{y} = (y_1, y_2, ..., y_n)$:
$$
d_{hamming}(\mathbf{x}, \mathbf{y}) = \sum_{i=1}^{n} \mathbb{1}(x_i \neq y_i)
$$
其中 $\mathbb{1}(x_i \neq y_i)$ 是指示函数,如果 $x_i \neq y_i$ 则为 1,否则为 0。
归一化汉明距离:
$$
d_{hamming_normalized}(\mathbf{x}, \mathbf{y}) = \frac{1}{n} \sum_{i=1}^{n} \mathbb{1}(x_i \neq y_i)
$$
为什么这样计算?
汉明距离基于逐位比较:
- 二进制向量:比较每一位是否相同
- 字符串:比较每个字符是否相同
- 分类特征:比较每个特征值是否相同
直观理解:
- 就像数两个密码有多少位不同
- 就像数两个二进制编码有多少位不同
可视化示例

import numpy as np
import matplotlib.pyplot as plt
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei']
plt.rcParams['axes.unicode_minus'] = False
# 定义两个二进制向量
v1 = np.array([1, 0, 1, 1, 0, 1, 0, 1])
v2 = np.array([1, 1, 0, 1, 0, 0, 1, 1])
# 计算汉明距离
hamming_dist = np.sum(v1 != v2)
normalized_dist = hamming_dist / len(v1)
# 创建图形
fig, ax = plt.subplots(1, 1, figsize=(12, 6))
# 绘制两个向量
x_pos = np.arange(len(v1))
width = 0.35
bars1 = ax.bar(x_pos - width/2, v1, width, label=f'向量 v1: {v1}',
color='red', alpha=0.7, edgecolor='black')
bars2 = ax.bar(x_pos + width/2, v2, width, label=f'向量 v2: {v2}',
color='blue', alpha=0.7, edgecolor='black')
# 标注不同的位
for i in range(len(v1)):
if v1[i] != v2[i]:
ax.text(i, max(v1[i], v2[i]) + 0.1, '×', fontsize=16,
color='red', weight='bold', ha='center')
# 设置坐标轴
ax.set_xlabel('位置索引', fontsize=12)
ax.set_ylabel('值', fontsize=12)
ax.set_title(f'汉明距离可视化(距离 = {hamming_dist},归一化 = {normalized_dist:.2f})',
fontsize=14, weight='bold')
ax.set_xticks(x_pos)
ax.set_xticklabels([f'{i}' for i in range(len(v1))])
ax.set_ylim(-0.2, 1.5)
ax.legend(fontsize=11)
ax.grid(axis='y', alpha=0.3)
# 添加说明
ax.text(0.02, 0.98, f'汉明距离 = {hamming_dist}\n归一化距离 = {normalized_dist:.2f}\n不同位数: {hamming_dist}/{len(v1)}',
transform=ax.transAxes, fontsize=11, verticalalignment='top',
bbox=dict(boxstyle='round', facecolor='lightyellow', alpha=0.8))
plt.tight_layout()
plt.savefig('hamming_distance.png', dpi=300, bbox_inches='tight')
plt.show()
print(f"向量 v1: {v1}")
print(f"向量 v2: {v2}")
print(f"汉明距离: {hamming_dist}")
print(f"归一化汉明距离: {normalized_dist:.4f}")
代码实现
import numpy as np
from scipy.spatial.distance import hamming
def hamming_distance(x, y):
"""
计算汉明距离
参数:
x: 第一个向量(可以是二进制向量或字符串)
y: 第二个向量(与x等长)
返回:
汉明距离
"""
if isinstance(x, str) and isinstance(y, str):
return sum(c1 != c2 for c1, c2 in zip(x, y))
else:
return np.sum(x != y)
# 示例1:二进制向量
x = np.array([1, 0, 1, 1, 0])
y = np.array([1, 1, 0, 1, 0])
distance = hamming_distance(x, y)
print(f"二进制向量汉明距离: {distance}")
# 示例2:字符串
str1 = "hello"
str2 = "hallo"
distance_str = hamming_distance(str1, str2)
print(f"字符串汉明距离: {distance_str}")
# 使用 scipy
distance_scipy = hamming(x, y) * len(x) # scipy返回归一化距离
print(f"使用 scipy: {distance_scipy}")
特点和应用
优点:
- 计算简单快速
- 适用于二进制数据和分类特征
- 直观易懂
缺点:
- 只适用于等长向量
- 不考虑位置的重要性
适用场景:
- 二进制编码比较
- 分类特征比较
- 错误检测和纠正
- 字符串相似度(等长)
杰卡德距离(Jaccard Distance)
什么是杰卡德距离?
杰卡德距离基于杰卡德系数(Jaccard Coefficient),用于衡量两个集合的相似性。
类比理解:
- 就像比较两个购物清单,看有多少商品相同
- 就像比较两个用户的兴趣集合,看有多少共同兴趣
- 就像比较两个文档的词汇集合,看有多少共同词汇
数学定义
对于两个集合 $A$ 和 $B$:
杰卡德系数(Jaccard Similarity):
$$
J(A, B) = \frac{|A \cap B|}{|A \cup B|} = \frac{|A \cap B|}{|A| + |B| - |A \cap B|}
$$
杰卡德距离(Jaccard Distance):
$$
d_{jaccard}(A, B) = 1 - J(A, B) = 1 - \frac{|A \cap B|}{|A \cup B|}
$$
对于两个二进制向量 $\mathbf{x}$ 和 $\mathbf{y}$:
- $a$ = 两个向量都为 1 的位置数
- $b$ = 向量 x 为 1,y 为 0 的位置数
- $c$ = 向量 x 为 0,y 为 1 的位置数
- $d$ = 两个向量都为 0 的位置数
$$
d_{jaccard}(\mathbf{x}, \mathbf{y}) = 1 - \frac{a}{a + b + c}
$$
(注意:$d$ 不参与计算,因为只关心"存在"的特征)
为什么这样计算?
杰卡德距离关注的是集合的交集与并集的比例:
- 交集大,并集小:集合相似度高,距离小
- 交集小,并集大:集合相似度低,距离大
- 忽略共同缺失:两个集合都没有的元素不参与计算
直观理解:
- 就像比较两个购物清单,只关心买了什么,不关心没买什么
- 就像比较两个用户的兴趣,只关心共同兴趣的比例
可视化示例

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Circle, Rectangle
import matplotlib.patches as mpatches
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei']
plt.rcParams['axes.unicode_minus'] = False
# 定义两个集合
set_A = {1, 2, 3, 4, 5}
set_B = {4, 5, 6, 7, 8}
# 计算杰卡德系数和距离
intersection = set_A & set_B
union = set_A | set_B
jaccard_sim = len(intersection) / len(union)
jaccard_dist = 1 - jaccard_sim
# 创建图形
fig, ax = plt.subplots(1, 1, figsize=(12, 8))
# 绘制韦恩图
circle_A = Circle((0.3, 0.5), 0.25, color='red', alpha=0.3, label='集合 A')
circle_B = Circle((0.7, 0.5), 0.25, color='blue', alpha=0.3, label='集合 B')
ax.add_patch(circle_A)
ax.add_patch(circle_B)
# 标注元素
elements_A_only = set_A - set_B
elements_B_only = set_B - set_A
elements_both = intersection
# 标注交集
ax.text(0.5, 0.5, f'交集\n{intersection}\n({len(intersection)}个)',
fontsize=11, ha='center', va='center', weight='bold',
bbox=dict(boxstyle='round', facecolor='yellow', alpha=0.7))
# 标注A独有
ax.text(0.15, 0.5, f'A独有\n{elements_A_only}\n({len(elements_A_only)}个)',
fontsize=10, ha='center', va='center',
bbox=dict(boxstyle='round', facecolor='lightcoral', alpha=0.5))
# 标注B独有
ax.text(0.85, 0.5, f'B独有\n{elements_B_only}\n({len(elements_B_only)}个)',
fontsize=10, ha='center', va='center',
bbox=dict(boxstyle='round', facecolor='lightblue', alpha=0.5))
# 设置坐标轴
ax.set_xlim(-0.1, 1.1)
ax.set_ylim(0, 1)
ax.set_aspect('equal')
ax.axis('off')
ax.set_title('杰卡德距离可视化 - 韦恩图', fontsize=16, weight='bold')
# 添加图例和统计信息
legend_elements = [
mpatches.Patch(facecolor='red', alpha=0.3, label=f'集合 A: {set_A}'),
mpatches.Patch(facecolor='blue', alpha=0.3, label=f'集合 B: {set_B}'),
mpatches.Patch(facecolor='yellow', alpha=0.7, label=f'交集: {intersection}'),
]
ax.legend(handles=legend_elements, loc='upper left', fontsize=10)
# 添加公式和结果
info_text = f'集合 A: {set_A} (|A| = {len(set_A)})\n'
info_text += f'集合 B: {set_B} (|B| = {len(set_B)})\n'
info_text += f'交集: {intersection} (|A∩B| = {len(intersection)})\n'
info_text += f'并集: {union} (|A∪B| = {len(union)})\n\n'
info_text += f'杰卡德系数 = |A∩B| / |A∪B| = {len(intersection)} / {len(union)} = {jaccard_sim:.4f}\n'
info_text += f'杰卡德距离 = 1 - 杰卡德系数 = {jaccard_dist:.4f}'
ax.text(0.5, 0.1, info_text, fontsize=11, ha='center', va='bottom',
bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8))
plt.tight_layout()
plt.savefig('jaccard_distance.png', dpi=300, bbox_inches='tight')
plt.show()
print(f"集合 A: {set_A}")
print(f"集合 B: {set_B}")
print(f"交集: {intersection}")
print(f"并集: {union}")
print(f"杰卡德系数: {jaccard_sim:.4f}")
print(f"杰卡德距离: {jaccard_dist:.4f}")
代码实现
import numpy as np
from sklearn.metrics import jaccard_score
def jaccard_distance(set_A, set_B):
"""
计算两个集合的杰卡德距离
参数:
set_A: 第一个集合
set_B: 第二个集合
返回:
杰卡德距离
"""
intersection = len(set_A & set_B)
union = len(set_A | set_B)
if union == 0:
return 1.0 # 两个空集
jaccard_sim = intersection / union
return 1 - jaccard_sim
def jaccard_distance_binary(x, y):
"""
计算两个二进制向量的杰卡德距离
参数:
x: 第一个二进制向量
y: 第二个二进制向量
返回:
杰卡德距离
"""
intersection = np.sum((x == 1) & (y == 1))
union = np.sum((x == 1) | (y == 1))
if union == 0:
return 1.0
jaccard_sim = intersection / union
return 1 - jaccard_sim
# 示例1:集合
set_A = {1, 2, 3, 4, 5}
set_B = {4, 5, 6, 7, 8}
distance_set = jaccard_distance(set_A, set_B)
print(f"集合杰卡德距离: {distance_set:.4f}")
# 示例2:二进制向量
x = np.array([1, 0, 1, 1, 0, 1])
y = np.array([1, 1, 0, 1, 0, 0])
distance_binary = jaccard_distance_binary(x, y)
print(f"二进制向量杰卡德距离: {distance_binary:.4f}")
# 使用 sklearn
distance_sklearn = 1 - jaccard_score(x, y, average='binary')
print(f"使用 sklearn: {distance_sklearn:.4f}")
特点和应用
优点:
- 忽略共同缺失的特征
- 适用于集合和稀疏二进制数据
- 对集合大小不敏感
缺点:
- 只考虑"存在"的特征
- 不考虑特征的重要性
适用场景:
- 集合相似度计算
- 推荐系统(用户-物品集合)
- 文本分析(词汇集合)
- 稀疏二进制数据
马氏距离(Mahalanobis Distance)
什么是马氏距离?
马氏距离考虑了数据的协方差结构,是一种考虑数据分布的距离度量。
类比理解:
- 就像考虑数据的"形状"和"方向"的距离
- 就像在考虑数据相关性的基础上计算距离
- 就像"标准化"的距离,但考虑了特征之间的相关性
为什么需要马氏距离?
**问题:**欧式距离假设各维度独立且方差相同,但实际数据往往存在:
- 不同维度方差不同
- 维度之间存在相关性
示例:
- 身高和体重相关,不能独立考虑
- 不同特征的方差差异很大
数学定义
对于两个向量 $\mathbf{x}$ 和 $\mathbf{y}$,以及协方差矩阵 $\mathbf{S}$:
$$
d_{mahalanobis}(\mathbf{x}, \mathbf{y}) = \sqrt{(\mathbf{x} - \mathbf{y})^T \mathbf{S}^{-1} (\mathbf{x} - \mathbf{y})}
$$
其中 $\mathbf{S}^{-1}$ 是协方差矩阵的逆矩阵。
特殊情况下:
- 如果 $\mathbf{S}$ 是对角矩阵(各维度独立),马氏距离退化为标准化欧式距离
- 如果 $\mathbf{S}$ 是单位矩阵,马氏距离退化为欧式距离
可视化示例

import numpy as np
import matplotlib.pyplot as plt
from scipy.spatial.distance import mahalanobis
from scipy.linalg import inv
from matplotlib.patches import Ellipse
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei']
plt.rcParams['axes.unicode_minus'] = False
# 生成相关数据
np.random.seed(42)
mean = [0, 0]
cov = [[3, 2], [2, 3]] # 协方差矩阵
X = np.random.multivariate_normal(mean, cov, 100)
# 选择两个点
P1 = np.array([2, 2])
P2 = np.array([-1, -1])
# 计算协方差矩阵
S = np.cov(X.T)
S_inv = inv(S)
# 计算马氏距离
mahal_dist_P1 = mahalanobis(P1, mean, S_inv)
mahal_dist_P2 = mahalanobis(P2, mean, S_inv)
mahal_dist_between = mahalanobis(P1, P2, S_inv)
# 计算欧式距离作为对比
euclidean_dist = np.linalg.norm(P1 - P2)
# 创建图形
fig, ax = plt.subplots(1, 1, figsize=(12, 10))
# 绘制数据点
ax.scatter(X[:, 0], X[:, 1], alpha=0.3, s=30, c='gray', label='数据点')
# 绘制两个点
ax.plot(P1[0], P1[1], 'ro', markersize=15, label=f'点 P1({P1[0]}, {P1[1]})', zorder=5)
ax.plot(P2[0], P2[1], 'bo', markersize=15, label=f'点 P2({P2[0]}, {P2[1]})', zorder=5)
ax.plot(mean[0], mean[1], 'g*', markersize=20, label=f'均值点({mean[0]}, {mean[1]})', zorder=5)
# 绘制等马氏距离椭圆
eigenvals, eigenvecs = np.linalg.eigh(S)
order = eigenvals.argsort()[::-1]
eigenvals = eigenvals[order]
eigenvecs = eigenvecs[:, order]
# 绘制几个等距离椭圆
for dist in [1, 2, 3]:
width = 2 * dist * np.sqrt(eigenvals[0])
height = 2 * dist * np.sqrt(eigenvals[1])
angle = np.degrees(np.arctan2(eigenvecs[1, 0], eigenvecs[0, 0]))
ellipse = Ellipse(mean, width, height, angle=angle,
fill=False, linestyle='--', linewidth=1.5,
color='green', alpha=0.5, label=f'等马氏距离={dist}' if dist == 1 else '')
ax.add_patch(ellipse)
# 绘制欧式距离(直线)
ax.plot([P1[0], P2[0]], [P1[1], P2[1]], 'r--', linewidth=2,
alpha=0.5, label=f'欧式距离 = {euclidean_dist:.2f}', zorder=2)
# 设置坐标轴
ax.set_xlim(-6, 6)
ax.set_ylim(-6, 6)
ax.set_xlabel('X 轴', fontsize=12)
ax.set_ylabel('Y 轴', fontsize=12)
ax.set_title('马氏距离可视化', fontsize=16, weight='bold')
ax.legend(loc='upper left', fontsize=10)
ax.grid(True, alpha=0.3)
ax.set_aspect('equal')
# 添加说明
info_text = f'协方差矩阵:\n{S}\n\n'
info_text += f'P1到均值的马氏距离: {mahal_dist_P1:.2f}\n'
info_text += f'P2到均值的马氏距离: {mahal_dist_P2:.2f}\n'
info_text += f'P1到P2的马氏距离: {mahal_dist_between:.2f}\n'
info_text += f'P1到P2的欧式距离: {euclidean_dist:.2f}'
ax.text(0.02, 0.98, info_text, transform=ax.transAxes,
fontsize=10, verticalalignment='top',
bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8))
plt.tight_layout()
plt.savefig('mahalanobis_distance.png', dpi=300, bbox_inches='tight')
plt.show()
print(f"P1到均值的马氏距离: {mahal_dist_P1:.4f}")
print(f"P2到均值的马氏距离: {mahal_dist_P2:.4f}")
print(f"P1到P2的马氏距离: {mahal_dist_between:.4f}")
print(f"P1到P2的欧式距离: {euclidean_dist:.4f}")
代码实现
import numpy as np
from scipy.spatial.distance import mahalanobis
from scipy.linalg import inv
def mahalanobis_distance(x, y, cov_matrix):
"""
计算马氏距离
参数:
x: 第一个向量
y: 第二个向量
cov_matrix: 协方差矩阵
返回:
马氏距离
"""
diff = x - y
cov_inv = inv(cov_matrix)
return np.sqrt(diff.T @ cov_inv @ diff)
# 示例
x = np.array([2, 2])
y = np.array([-1, -1])
cov = np.array([[3, 2], [2, 3]])
distance = mahalanobis_distance(x, y, cov)
print(f"马氏距离: {distance:.4f}")
# 使用 scipy
distance_scipy = mahalanobis(x, y, inv(cov))
print(f"使用 scipy: {distance_scipy:.4f}")
特点和应用
优点:
- 考虑数据的协方差结构
- 不受特征尺度影响
- 考虑特征之间的相关性
缺点:
- 需要计算协方差矩阵及其逆矩阵
- 计算复杂度高
- 需要足够的数据估计协方差矩阵
适用场景:
- 特征之间存在相关性的数据
- 需要精确距离度量的场景
- 异常检测
- 多元统计分析
所有距离度量的综合对比
距离度量选择指南
| 距离类型 | 适用场景 | 优点 | 缺点 | 计算复杂度 |
|---|---|---|---|---|
| 欧式距离 | 连续特征、低维数据 | 直观、常用 | 对异常值敏感 | O(n) |
| 曼哈顿距离 | 离散特征、稀疏数据 | 对异常值不敏感 | 不考虑对角线 | O(n) |
| 切比雪夫距离 | 只关心最大差异 | 计算简单 | 忽略其他维度 | O(n) |
| 标准化欧式距离 | 特征尺度差异大 | 消除尺度影响 | 需要标准化 | O(n) |
| 余弦距离 | 文本、稀疏数据 | 不受长度影响 | 不考虑大小 | O(n) |
| 汉明距离 | 二进制、分类特征 | 计算简单 | 只适用于等长 | O(n) |
| 杰卡德距离 | 集合、稀疏二进制 | 忽略共同缺失 | 不考虑重要性 | O(n) |
| 马氏距离 | 特征相关、精确度量 | 考虑协方差 | 计算复杂 | O(n²) |
实际应用建议
- 默认选择:欧式距离或标准化欧式距离
- 文本数据:余弦距离
- 二进制数据:汉明距离或杰卡德距离
- 高维稀疏数据:余弦距离或曼哈顿距离
- 特征相关数据:马氏距离
- 需要精确度量:马氏距离
在Scikit-learn中使用
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics.pairwise import (
euclidean_distances, manhattan_distances,
cosine_distances, pairwise_distances
)
from scipy.spatial.distance import hamming, jaccard, mahalanobis
# 不同距离度量的使用
distances = {
'euclidean': euclidean_distances,
'manhattan': manhattan_distances,
'cosine': cosine_distances,
'hamming': lambda X, Y: pairwise_distances(X, Y, metric='hamming'),
'jaccard': lambda X, Y: pairwise_distances(X, Y, metric='jaccard'),
}
# 在KNN中使用
for name, dist_func in distances.items():
knn = KNeighborsClassifier(n_neighbors=5, metric=name)
# ... 训练和评估
结语
距离计算是KNN算法的核心,不同的距离度量方式会影响算法的结果:
基础距离度量:
- 欧式距离:最常用,适用于大多数场景
- 曼哈顿距离:适合离散特征和稀疏数据
- 切比雪夫距离:只关心最大差异
- 闵可夫斯基距离:统一的距离框架
进阶距离度量:
- 标准化欧式距离:消除特征尺度影响
- 余弦距离:关注方向而非大小
- 汉明距离:适用于二进制和分类特征
- 杰卡德距离:适用于集合和稀疏数据
- 马氏距离:考虑数据协方差结构
通过本文的讲解和可视化,希望你能:
- 深入理解各种距离的计算方式
- 理解为什么这样计算
- 知道如何选择合适的距离度量
- 在实际项目中灵活运用
记住:没有最好的距离度量,只有最适合的距离度量。根据你的数据和问题,选择最合适的距离计算方式!
参考文献
- Scikit-learn 官方文档: https://scikit-learn.org/stable/modules/neighbors.html
- Hastie, T., Tibshirani, R., & Friedman, J. (2009). The Elements of Statistical Learning
- James, G., et al. (2013). An Introduction to Statistical Learning