行莫
行莫
发布于 2025-11-24 / 17 阅读
0
0

机器学习算法常见距离计算

机器学习算法常见距离计算

引言

想象一下,你要判断一个新来的同学和谁最相似:

  • 方法1:看直线距离有多远(欧式距离)
  • 方法2:看需要走多少条街才能到达(曼哈顿距离)
  • 方法3:看最远的那一维差距有多大(切比雪夫距离)
  • 方法4:根据情况灵活选择距离计算方式(闵可夫斯基距离)

KNN(K-Nearest Neighbors)算法中,距离计算是核心。不同的距离度量方式会影响算法的结果,就像用不同的"尺子"测量,会得到不同的"远近"判断。

本文将详细讲解 KNN 算法中常用的距离计算方式:

基础距离度量:

  1. 欧式距离(Euclidean Distance)
  2. 曼哈顿距离(Manhattan Distance)
  3. 切比雪夫距离(Chebyshev Distance)
  4. 闵可夫斯基距离(Minkowski Distance)

进阶距离度量:
5. 标准化欧式距离(Standardized Euclidean Distance)
6. 余弦距离(Cosine Distance)
7. 汉明距离(Hamming Distance)
8. 杰卡德距离(Jaccard Distance)
9. 马氏距离(Mahalanobis Distance)

每种距离都会包含:

  • 数学定义和计算公式
  • 为什么这样计算
  • 直观理解
  • 可视化图示
  • 代码实现

距离的基本概念

什么是距离?

在数学和机器学习中,**距离(Distance)**是衡量两个点(样本)之间相似程度的度量。

类比理解:

  • 就像地图上两个城市的距离
  • 就像两个人之间的相似度
  • 就像两个样本之间的差异

距离的性质

一个有效的距离函数 $d(x, y)$ 必须满足以下性质:

  1. 非负性:$d(x, y) \geq 0$,距离不能为负
  2. 同一性:$d(x, y) = 0$ 当且仅当 $x = y$
  3. 对称性:$d(x, y) = d(y, x)$,A到B的距离等于B到A的距离
  4. 三角不等式:$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$ 范数(欧式范数)。

为什么这样计算?

欧式距离基于勾股定理

  1. 二维情况:两点之间的直线距离就是直角三角形的斜边
  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$ 范数(曼哈顿范数)。

为什么这样计算?

曼哈顿距离基于只能沿坐标轴移动的假设:

  1. 二维情况:只能先水平移动,再垂直移动(或相反)
  2. 多维情况:每一维的差异绝对值之和

直观理解:

  • 就像在城市中,只能沿着街道走,不能斜穿建筑物
  • 就像考虑"路径"而不是"直线"

可视化示例

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$ 范数(切比雪夫范数)。

为什么这样计算?

切比雪夫距离基于只关心最大差异的假设:

  1. 二维情况:取水平和垂直方向差异的最大值
  2. 多维情况:取所有维度差异绝对值的最大值

直观理解:

  • 就像国际象棋中,王可以朝任意方向移动,移动到目标位置所需的步数就是最大坐标差
  • 就像只关心"最坏情况",即最大差异

可视化示例

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$ 范数。

特殊情况的统一

闵可夫斯基距离包含了前面三种距离作为特殊情况:

  1. $p = 1$:曼哈顿距离
    $$
    d_{minkowski}(\mathbf{x}, \mathbf{y}; 1) = \sum_{i=1}^{n} |x_i - y_i| = d_{manhattan}(\mathbf{x}, \mathbf{y})
    $$

  2. $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})
    $$

  3. $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$ 个特征的标准差。

标准化过程:

  1. 计算每个特征的均值和标准差
  2. 对每个特征进行标准化:$z_i = \frac{x_i - \mu_i}{\sigma_i}$
  3. 在标准化后的空间计算欧式距离

可视化示例

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。

为什么这样计算?

余弦距离关注的是方向而不是大小

  1. 夹角为 0°:向量方向相同,余弦相似度为 1,距离为 0
  2. 夹角为 90°:向量垂直,余弦相似度为 0,距离为 1
  3. 夹角为 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)
$$

为什么这样计算?

汉明距离基于逐位比较

  1. 二进制向量:比较每一位是否相同
  2. 字符串:比较每个字符是否相同
  3. 分类特征:比较每个特征值是否相同

直观理解:

  • 就像数两个密码有多少位不同
  • 就像数两个二进制编码有多少位不同

可视化示例

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$ 不参与计算,因为只关心"存在"的特征)

为什么这样计算?

杰卡德距离关注的是集合的交集与并集的比例

  1. 交集大,并集小:集合相似度高,距离小
  2. 交集小,并集大:集合相似度低,距离大
  3. 忽略共同缺失:两个集合都没有的元素不参与计算

直观理解:

  • 就像比较两个购物清单,只关心买了什么,不关心没买什么
  • 就像比较两个用户的兴趣,只关心共同兴趣的比例

可视化示例

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)

什么是马氏距离?

马氏距离考虑了数据的协方差结构,是一种考虑数据分布的距离度量。

类比理解:

  • 就像考虑数据的"形状"和"方向"的距离
  • 就像在考虑数据相关性的基础上计算距离
  • 就像"标准化"的距离,但考虑了特征之间的相关性

为什么需要马氏距离?

**问题:**欧式距离假设各维度独立且方差相同,但实际数据往往存在:

  1. 不同维度方差不同
  2. 维度之间存在相关性

示例:

  • 身高和体重相关,不能独立考虑
  • 不同特征的方差差异很大

数学定义

对于两个向量 $\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²)

实际应用建议

  1. 默认选择:欧式距离或标准化欧式距离
  2. 文本数据:余弦距离
  3. 二进制数据:汉明距离或杰卡德距离
  4. 高维稀疏数据:余弦距离或曼哈顿距离
  5. 特征相关数据:马氏距离
  6. 需要精确度量:马氏距离

在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算法的核心,不同的距离度量方式会影响算法的结果:

基础距离度量:

  • 欧式距离:最常用,适用于大多数场景
  • 曼哈顿距离:适合离散特征和稀疏数据
  • 切比雪夫距离:只关心最大差异
  • 闵可夫斯基距离:统一的距离框架

进阶距离度量:

  • 标准化欧式距离:消除特征尺度影响
  • 余弦距离:关注方向而非大小
  • 汉明距离:适用于二进制和分类特征
  • 杰卡德距离:适用于集合和稀疏数据
  • 马氏距离:考虑数据协方差结构

通过本文的讲解和可视化,希望你能:

  1. 深入理解各种距离的计算方式
  2. 理解为什么这样计算
  3. 知道如何选择合适的距离度量
  4. 在实际项目中灵活运用

记住:没有最好的距离度量,只有最适合的距离度量。根据你的数据和问题,选择最合适的距离计算方式!


参考文献

  1. Scikit-learn 官方文档: https://scikit-learn.org/stable/modules/neighbors.html
  2. Hastie, T., Tibshirani, R., & Friedman, J. (2009). The Elements of Statistical Learning
  3. James, G., et al. (2013). An Introduction to Statistical Learning

评论