用Python代码实战拆解KNN与K-Means:5分钟掌握核心差异

第一次接触机器学习时,看到KNN和K-Means这两个缩写,我的大脑自动把它们归为了"名字相似的兄弟算法"。直到在一次数据聚类项目中误把KNN当聚类算法使用,才意识到这种认知有多危险——结果就像用菜刀削苹果,虽然都是刀具,但效果截然不同。本文将用最直观的代码对比,带你穿透这两大经典算法的本质差异。

1. 核心概念:监督学习与无监督学习的本质分野

在机器学习领域,KNN(K-Nearest Neighbors)和K-Means最根本的区别在于它们所属的学习范式。这就像医生诊断疾病的两种思路:一种需要参考大量已知病例(监督学习),另一种则通过观察症状自动分组(无监督学习)。

KNN的关键特征

  • 监督学习算法,依赖已标注数据
  • 用于分类(预测类别)和回归(预测数值)
  • 没有显式的训练过程,直接保存全部训练数据
  • 预测阶段计算新数据点与训练数据的距离
# KNN分类示例代码骨架
from sklearn.neighbors import KNeighborsClassifier

# 训练数据需包含特征和标签
X_train = [[1, 2], [3, 4], [5, 6]]
y_train = [0, 1, 0]  

knn = KNeighborsClassifier(n_neighbors=3)
knn.fit(X_train, y_train)  # 实际只是存储数据
print(knn.predict([[2, 3]]))  # 输出预测类别

K-Means的核心机制

  • 无监督学习算法,只需输入特征数据
  • 用于聚类分析(发现数据内在分组)
  • 有明确的训练过程,迭代优化聚类中心
  • 输出每个数据点的所属簇标签
# K-Means聚类示例代码骨架
from sklearn.cluster import KMeans

# 只需特征数据,无需标签
X = [[1, 2], [3, 4], [5, 6], [10, 12]]

kmeans = KMeans(n_clusters=2)
kmeans.fit(X)  # 实际进行聚类计算
print(kmeans.predict([[4, 5]]))  # 输出所属簇编号

两者参数中的"K"也代表不同含义:

  • KNN中的K:投票邻居的数量
  • K-Means中的K:要形成的簇的数量

2. 算法流程对比:从代码看本质差异

2.1 KNN的工作流程

KNN的预测过程可以概括为"近朱者赤"——通过查找最近的K个已知样本进行投票决策。下面通过完整代码示例展示其工作方式:

import numpy as np
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

# 加载鸢尾花数据集
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)

# 创建KNN分类器
knn = KNeighborsClassifier(
    n_neighbors=5,      # K值
    weights='uniform',  # 投票权重
    metric='euclidean'  # 距离度量
)

# "训练"过程(实际只是存储数据)
knn.fit(X_train, y_train)

# 预测新样本
sample = [[5.1, 3.5, 1.4, 0.2]]
print(f"预测类别: {knn.predict(sample)}")
print(f"最近3个邻居的索引: {knn.kneighbors(sample, n_neighbors=3)[1]}")

关键特点:

  • 没有模型参数需要学习
  • 预测时需要存储全部训练数据
  • 计算复杂度随数据量线性增长
  • 对特征尺度敏感,通常需要归一化

2.2 K-Means的迭代过程

K-Means则通过不断优化聚类中心来实现数据分组,下面代码展示了其迭代过程:

import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs

# 生成模拟聚类数据
X, _ = make_blobs(n_samples=300, centers=4, random_state=42)

# 可视化初始数据
plt.scatter(X[:, 0], X[:, 1], s=10)
plt.title("原始数据分布")
plt.show()

# 执行K-Means聚类
kmeans = KMeans(
    n_clusters=4,       # 预设簇数量
    init='k-means++',   # 初始化方法
    max_iter=300,       # 最大迭代次数
    tol=1e-4            # 收敛阈值
)
kmeans.fit(X)

# 可视化聚类结果
plt.scatter(X[:, 0], X[:, 1], c=kmeans.labels_, s=10)
plt.scatter(kmeans.cluster_centers_[:, 0], 
           kmeans.cluster_centers_[:, 1],
           marker='x', s=100, c='red')
plt.title("K-Means聚类结果")
plt.show()

典型特征:

  • 需要预先指定簇数量K
  • 对初始中心点选择敏感
  • 可能收敛到局部最优解
  • 假设簇呈凸形分布

3. 实战对比:同一数据集上的不同表现

为了更直观展示差异,我们使用相同的鸢尾花数据集对两种算法进行对比:

from sklearn.preprocessing import StandardScaler
from sklearn.metrics import classification_report, silhouette_score

# 数据预处理
scaler = StandardScaler()
X_scaled = scaler.fit_transform(iris.data)

# KNN分类表现
knn = KNeighborsClassifier(n_neighbors=3)
knn.fit(X_scaled, iris.target)
print("KNN分类报告:")
print(classification_report(iris.target, knn.predict(X_scaled)))

# K-Means聚类表现
kmeans = KMeans(n_clusters=3)
kmeans.fit(X_scaled)
print("\nK-Means轮廓系数:", silhouette_score(X_scaled, kmeans.labels_))

# 比较标签分配(注意聚类标签与真实类别无对应关系)
print("\n真实标签:", iris.target[:10])
print("KNN预测:", knn.predict(X_scaled[:10]))
print("K-Means标签:", kmeans.labels_[:10])

输出结果对比:

指标 KNN (分类) K-Means (聚类)
评估方式 准确率、召回率 轮廓系数、惯性
需要标签
输出意义 预测已知类别 发现未知分组
典型应用 垃圾邮件识别 客户细分分析

4. 选择指南:何时使用哪种算法

4.1 选择KNN的场景

  • 有标注数据可用 :当您拥有带标签的训练数据时
  • 需要解释预测 :KNN的决策过程直观易懂
  • 数据分布复杂 :不对数据分布做特定假设
  • 小到中型数据集 :计算成本随数据量增长
# KNN优势案例:手写数字识别
from sklearn.datasets import load_digits

digits = load_digits()
X_train, X_test, y_train, y_test = train_test_split(digits.data, digits.target)

knn = KNeighborsClassifier(n_neighbors=5)
knn.fit(X_train, y_train)
print(f"KNN数字识别准确率: {knn.score(X_test, y_test):.2f}")

4.2 选择K-Means的场景

  • 探索数据结构 :当您想发现数据中的自然分组时
  • 数据无标签 :只有特征数据可用
  • 预处理步骤 :作为特征工程的一部分
  • 大规模数据 :可使用MiniBatch K-Means变种
# K-Means优势案例:图像颜色压缩
from sklearn.datasets import load_sample_image

china = load_sample_image("china.jpg")
X = china.reshape(-1, 3) / 255  # 归一化像素值

kmeans = KMeans(n_clusters=16)
kmeans.fit(X)
compressed_colors = kmeans.cluster_centers_[kmeans.labels_]
compressed_image = compressed_colors.reshape(china.shape)

plt.figure(figsize=(10, 5))
plt.subplot(121); plt.imshow(china)
plt.subplot(122); plt.imshow(compressed_image)
plt.show()

4.3 常见误用与避免方法

  1. 误将KNN用于聚类

    • 症状:试图对无标签数据使用KNN
    • 解决:明确是否需要发现新分组(用K-Means)还是预测已知类别(用KNN)
  2. 忽视数据预处理

    • KNN对特征尺度敏感,必须进行标准化
    from sklearn.pipeline import make_pipeline
    
    # 正确的KNN流程:包含标准化
    knn_pipe = make_pipeline(
        StandardScaler(),
        KNeighborsClassifier()
    )
    
  3. 随意选择K值

    • 对KNN:使用交叉验证选择最佳K
    from sklearn.model_selection import GridSearchCV
    
    param_grid = {'n_neighbors': range(1, 15)}
    grid = GridSearchCV(KNeighborsClassifier(), param_grid)
    grid.fit(X_train, y_train)
    print(f"最优K值: {grid.best_params_['n_neighbors']}")
    
    • 对K-Means:使用肘部法则或轮廓系数
    silhouette_scores = []
    for k in range(2, 10):
        kmeans = KMeans(n_clusters=k)
        kmeans.fit(X_scaled)
        score = silhouette_score(X_scaled, kmeans.labels_)
        silhouette_scores.append(score)
    
    plt.plot(range(2, 10), silhouette_scores, 'bo-')
    plt.xlabel('Number of clusters')
    plt.ylabel('Silhouette Score')
    plt.show()
    

5. 高级话题与实用技巧

5.1 距离度量的选择艺术

虽然欧氏距离是默认选择,但不同场景可能需要不同距离度量:

KNN距离选择指南

  • 连续特征:欧氏距离(L2)
  • 分类特征:汉明距离
  • 文本数据:余弦相似度
  • 混合数据:自定义距离函数
# 自定义距离函数示例
def manhattan_weighted(a, b, weights=[1, 0.5, 1, 1]):
    return np.sum(weights * np.abs(a - b))

knn_custom = KNeighborsClassifier(
    n_neighbors=5,
    metric=manhattan_weighted
)

K-Means距离注意事项

  • 理论上应使用与质心计算一致的距离
  • 欧氏距离隐含球形簇假设
  • 对于非凸簇可能表现不佳

5.2 算法变种与性能优化

KNN的加速技巧

  • KD树或球树数据结构
  • 近似最近邻算法(ANN)
  • 降维预处理
# 使用KD树加速
knn_fast = KNeighborsClassifier(
    algorithm='kd_tree',  # 或'ball_tree'
    leaf_size=30
)

K-Means的改进版本

  • K-Means++:更好的初始中心选择
  • MiniBatch K-Means:大数据集处理
  • 谱聚类:解决非凸簇问题
# MiniBatch K-Means示例
from sklearn.cluster import MiniBatchKMeans

mbkmeans = MiniBatchKMeans(
    n_clusters=4,
    batch_size=100,
    max_iter=100
)
mbkmeans.fit(X_large)

5.3 结果可视化技巧

清晰的图表能极大帮助理解算法行为:

# KNN决策边界可视化
from matplotlib.colors import ListedColormap

def plot_decision_boundary(model, X, y):
    h = .02  # 网格步长
    cmap_light = ListedColormap(['#FFAAAA', '#AAFFAA', '#AAAAFF'])
    
    x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
    y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
    xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
                         np.arange(y_min, y_max, h))
    
    Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
    Z = Z.reshape(xx.shape)
    
    plt.figure()
    plt.pcolormesh(xx, yy, Z, cmap=cmap_light)
    plt.scatter(X[:, 0], X[:, 1], c=y, edgecolor='k', s=20)
    plt.title("KNN决策边界 (k=3)")

# 只使用前两个特征便于可视化
X_vis = iris.data[:, :2]
knn_vis = KNeighborsClassifier(n_neighbors=3)
knn_vis.fit(X_vis, iris.target)
plot_decision_boundary(knn_vis, X_vis, iris.target)

对于K-Means,除了基本的簇可视化,还可以绘制SSE(误差平方和)随K值变化的肘部图:

# K-Means肘部法则可视化
sse = []
for k in range(1, 10):
    kmeans = KMeans(n_clusters=k)
    kmeans.fit(X_scaled)
    sse.append(kmeans.inertia_)

plt.plot(range(1, 10), sse, 'bo-')
plt.xlabel('Number of clusters')
plt.ylabel('SSE')
plt.title('Elbow Method For Optimal k')
plt.show()

在实际项目中,我发现当数据维度较高时,KNN的性能下降非常明显。有一次处理超过50个特征的数据集时,即使使用KD树优化,查询时间仍然难以接受。这时通过PCA降维保留95%的方差后,不仅速度提升了10倍,准确率还略有提高——这提醒我们,算法选择不仅要看理论特性,还要考虑实际工程约束。

更多推荐