05 - KNN 与 K-Means 聚类:从入门到实践

3 阅读15分钟

本文面向零基础小白,用最通俗的语言和生活化的例子,带你理解机器学习中两个带 "K" 的经典算法。


目录

  1. 开篇:两个带 "K" 的算法有什么不同?
  2. KNN(K-近邻算法)
    • 2.1 核心思想:近朱者赤,近墨者黑
    • 2.2 距离度量:如何衡量"近"
    • 2.3 K 值的选择
    • 2.4 KNN 做分类
    • 2.5 KNN 做回归
    • 2.6 实战:电影类型推荐
  3. K-Means(K-均值聚类)
    • 3.1 核心思想:物以类聚
    • 3.2 质心初始化
    • 3.3 迭代过程详解
    • 3.4 肘部法则选 K
    • 3.5 实战:客户分群
  4. KNN vs K-Means 全面对比
  5. 常见问题与总结

1. 开篇:两个带 "K" 的算法有什么不同?

很多初学者容易把 KNN 和 K-Means 搞混,因为它们名字里都有一个 "K"。 但它们本质上是完全不同的两类算法:

┌─────────────────────────────────────────────────────────┐
│                  KNN vs K-Means 速览                     │
├────────────────┬────────────────────────────────────────┤
│                │  KNN (K-近邻)     │  K-Means (K-均值)   │
├────────────────┼────────────────────────────────────────┤
│  类型          │  监督学习          │  无监督学习          │
│  目的          │  预测/分类         │  聚类/分群           │
│  需要标签吗?   │  需要 ✓           │  不需要 ✗           │
│  K 的含义      │  看 K 个邻居       │  分成 K 个簇         │
│  有训练过程吗? │  没有(懒惰学习)   │  有(迭代优化)      │
│  生活比喻      │  看你周围的人       │  把相似的东西归堆     │
└────────────────┴────────────────────────────────────────┘

一句话总结:

  • KNN:告诉我你的邻居是谁,我就知道你是谁。
  • K-Means:我来帮你把这堆东西分成 K 组。

2. KNN(K-近邻算法)

2.1 核心思想:近朱者赤,近墨者黑

KNN 的思想简单到令人感动——

一个样本的类别,由离它最近的 K 个邻居投票决定。

生活中的例子:

  • 你搬到一个新小区,不知道该去哪个菜市场买菜。
  • 你问了离你最近的 5 户邻居(K=5),其中 4 户说去东边的市场,1 户说去西边。
  • 那你大概率也会去东边的市场

这就是 KNN 的全部核心思想!

        KNN 分类示意图(K=3)

        判断 "?" 属于 ▲ 还是 ●?

                ▲
            ▲       ●
          ▲   ┌─────────┐
              │  ●   ?  │    ← 找最近的 3 个邻居
          ●   │    ●    │
              └─────────┘
            ▲       ●
                ●

        最近的 3 个邻居:● ● ●
        投票结果:● 获胜(3:0)
        所以 ? → ●

2.2 距离度量:如何衡量"近"

要找"最近的邻居",首先得定义什么叫"近"。 常用的距离度量有两种:

(1)欧氏距离(Euclidean Distance)—— 直线距离

就是两点之间的直线距离,也就是初中学的勾股定理。

公式:d = √[(x₁-x₂)² + (y₁-y₂)²]

示意图:

    B(4,4)
    ╱|
   ╱ |  欧氏距离 = √[(4-1)² + (4-1)²]
  ╱  |            = √[9 + 9]
 ╱   |            = √184.24
A────+
(1,1)  (4,1)

走斜线(直线距离)= 4.24

(2)曼哈顿距离(Manhattan Distance)—— 街区距离

想象你在纽约曼哈顿的街区里走路,只能横着走或竖着走,不能穿墙斜着走。

公式:d = |x₁-x₂| + |y₁-y₂|

示意图:

    B(4,4)
    │
    │  曼哈顿距离 = |4-1| + |4-1|
    │             = 3 + 3
    │             = 6A───┘
(1,1)

先向右走 3 步 + 再向上走 3 步 = 6

两种距离的直观对比

    ┌────────────────────────────┐
    │ B                          │
    │ ╲                          │
    │  ╲  ← 欧氏(直线)≈ 4.24   │
    │   ╲                        │
    │    ╲   ┌──→──→──┐          │
    │     ╲  ↑        ↓          │
    │      A ← 曼哈顿 = 6        │
    │     (横+竖,沿街区走)      │
    └────────────────────────────┘

    欧氏距离 ≤ 曼哈顿距离(永远成立)

小贴士:大多数场景下用欧氏距离就够了。 如果特征维度很高且有很多稀疏特征(如文本分析),可以尝试曼哈顿距离。

2.3 K 值的选择

K 的选择至关重要,选不好会导致完全不同的结果:

K 值选择的影响:

K 太小(如 K=1):            K 太大(如 K=N):
┌──────────────────┐         ┌──────────────────┐
│ ● ▲              │         │ ● ▲              │
│   ?←只看最近1个   │         │   ?←看所有样本    │
│ ▲   ●            │         │ ▲   ●            │
│                  │         │ ● ●   ▲          │
│ 容易受噪声干扰    │         │ 永远预测多数类     │
│ 过拟合!          │         │ 欠拟合!           │
└──────────────────┘         └──────────────────┘

K 适中(如 K=5):
┌──────────────────┐
│   ▲ ●            │
│ ●  ?  ●          │
│   ▲ ●            │
│ 平衡了噪声和信号   │
│ 刚刚好! ✓        │
└──────────────────┘

选择 K 的经验法则:

  1. K 一般取奇数(避免投票平局)
  2. 常见起点:K = √n(n 为训练样本数)
  3. 通过交叉验证找到最佳 K
  4. K 通常在 3~10 之间效果不错

2.4 KNN 做分类

分类就是"投票制"——少数服从多数。

例子:判断一个水果是苹果还是橙子

已知数据:
        甜度
    高 │  🍎 🍎   🍊
       │    🍎  🍊 🍊
       │  🍎    ?
       │     🍊  🍊
    低 │  🍎      🍊
       └──────────────
        小    重量    大

新水果 "?" 的 K=5 个最近邻居:
  🍎 🍎 🍊 🍎 🍊

投票:🍎 = 3 票, 🍊 = 2 票
结果:? → 苹果 🍎

2.5 KNN 做回归

回归不是投票,而是求平均值

例子:预测一套房子的价格

已知数据(面积 → 价格):
  80㎡ → 200万
  90㎡ → 230万
  85㎡ → 220万
  100㎡ → 280万
  95㎡ → 250万

新房子:88㎡,K=3
最近的 3 个邻居:
  90㎡(230万), 85㎡(220万), 80㎡(200万)

预测价格 = (230 + 220 + 200) / 3 = 216.7

2.6 实战:电影类型推荐

假设我们要根据电影的"打斗镜头数"和"接吻镜头数"来判断电影是动作片还是爱情片。

# ============================================
# KNN 实战:电影类型分类
# ============================================
from sklearn.neighbors import KNeighborsClassifier
import numpy as np

# 训练数据:[打斗镜头数, 接吻镜头数]
X_train = np.array([
    [3, 104],   # 爱情片
    [2, 100],   # 爱情片
    [1, 81],    # 爱情片
    [101, 10],  # 动作片
    [99, 5],    # 动作片
    [98, 2],    # 动作片
])
# 标签:0 = 爱情片, 1 = 动作片
y_train = np.array([0, 0, 0, 1, 1, 1])

# 创建 KNN 模型,K=3
knn = KNeighborsClassifier(n_neighbors=3)
knn.fit(X_train, y_train)

# 预测新电影:打斗 18 次,接吻 90 次
new_movie = np.array([[18, 90]])
prediction = knn.predict(new_movie)

print(f"预测结果:{'动作片' if prediction[0] == 1 else '爱情片'}")
# 输出:预测结果:爱情片
# ============================================
# KNN 回归:预测电影评分
# ============================================
from sklearn.neighbors import KNeighborsRegressor

# 训练数据:[电影时长(分钟), 特效预算(百万)]
X_train = np.array([
    [120, 50],
    [90, 20],
    [150, 80],
    [100, 30],
    [130, 60],
])
# 评分(1~10)
y_train = np.array([7.5, 6.0, 8.5, 6.5, 8.0])

# 创建 KNN 回归模型
knn_reg = KNeighborsRegressor(n_neighbors=3)
knn_reg.fit(X_train, y_train)

# 预测:时长 110 分钟,特效预算 40 百万
new_movie = np.array([[110, 40]])
predicted_score = knn_reg.predict(new_movie)

print(f"预测评分:{predicted_score[0]:.1f}")
# 输出:预测评分:6.7(附近3部电影评分的平均值)

3. K-Means(K-均值聚类)

3.1 核心思想:物以类聚

K-Means 是一种无监督学习算法——也就是说,数据没有标签, 算法自动把相似的数据归为一组。

生活比喻:你面前有一桌子水果(苹果、橙子、香蕉混在一起), 你不知道它们的名字,但你可以按颜色/形状/大小把它们分成几堆。 这就是聚类!

K-Means 的意思是:把数据分成 K 个簇(cluster),每个簇有一个中心点(质心)。

    K-Means 聚类前 vs 聚类后(K=3)

    聚类前(一片混沌):          聚类后(清晰分组):

    · ·   · ·                   ○ ○   △ △
      ·     · ·                   ○     △ △
    · ·   ·                     ○ ○   △
            · ·                         □ □
      ·   ·   ·                   ○   □   □
        ·       ·                   ○       □

    全部是同一种符号              ○ = 簇A  △ = 簇B  □ = 簇C
    看不出任何结构                每个簇内部的点彼此相似

3.2 质心初始化

K-Means 的第一步是随机选择 K 个初始质心

初始化示意图(K=3):

    步骤 1:随机选 3 个点作为初始质心

    ·  ·     ·  ·
      ★         ·      ★ = 初始质心A
    ·    ·   ·
         ·      ★      ★ = 初始质心B
      ·     ·
    ·    ·              ★ = 初始质心C

    注意:初始位置不同,最终结果可能不同!
    这就是为什么 sklearn 默认会运行多次取最优。

改进方法K-Means++ 是一种更聪明的初始化方法。 它让初始质心尽量彼此远离,避免都挤在一起。 sklearn 默认就是用 K-Means++

3.3 迭代过程详解

K-Means 的核心就是反复执行两个步骤,直到收敛:

步骤 A:分配 —— 每个点归入离它最近的质心所在的簇
步骤 B:更新 —— 每个簇重新计算质心(簇内所有点的平均值)

让我们用一个完整的例子来看迭代过程:

═══════════════════════════════════════════════════
  K-Means 迭代过程演示(K=2, 二维数据)
═══════════════════════════════════════════════════

数据点:A(1,1) B(1,2) C(2,1) D(6,6) E(7,6) F(6,7)

─── 第 0 轮:随机初始化 ───

  7 │            F
  6 │         D     E
  5432B
  1A  C
    └──────────────────
     1  2  3  4  5  6  7

  随机选择初始质心:★1 = A(1,1)  ★2 = D(6,6)

─── 第 1 轮:分配 + 更新 ───

  【分配】每个点离哪个质心更近?
  A(1,1) → ★1 距离 0.0   vs ★2 距离 7.1  → 簇1B(1,2) → ★1 距离 1.0   vs ★2 距离 6.4  → 簇1C(2,1) → ★1 距离 1.0   vs ★2 距离 6.4  → 簇1D(6,6) → ★1 距离 7.1   vs ★2 距离 0.0  → 簇2E(7,6) → ★1 距离 7.8   vs ★2 距离 1.0  → 簇2F(6,7) → ★1 距离 7.8   vs ★2 距离 1.0  → 簇2 □

  【更新】重新计算质心
  ★1 新位置 = ((1+1+2)/3, (1+2+1)/3) = (1.33, 1.33)
  ★2 新位置 = ((6+7+6)/3, (6+6+7)/3) = (6.33, 6.33)

  7 │            □
  6 │         □     □
  5 │            ★2(6.33,6.33)
  432 │  ○
  1 │  ○  ○  ★1(1.33,1.33)
    └──────────────────
     1  2  3  4  5  6  7

─── 第 2 轮:分配 + 更新 ───

  【分配】用新质心重新分配 → 结果和上一轮完全一样
  【更新】质心没有变化

  ✓ 收敛!算法结束!

  最终结果:
  簇1(○):A, B, C — 左下角的一群点
  簇2(□):D, E, F — 右上角的一群点
═══════════════════════════════════════════════════

3.4 肘部法则(Elbow Method)选 K

K-Means 需要你事先指定 K 值。那怎么知道该分几组呢?

肘部法则的思路:尝试不同的 K 值,看**组内误差(SSE)**的变化趋势。 SSE = 每个点到其所属簇质心的距离之和。

    肘部法则示意图

    SSE(组内误差)
    │
800 │ ×
    │  ╲
600 │   ╲
    │    ╲
400 │     ×
    │      ╲
200 │       ╲
    │        × ── × ── × ── × ── ×
100 │            ↑
    │         这里是"肘部"!
  0 │         最佳 K = 3
    └──────────────────────────────
      1    2    3    4    5    6    7
                K 的取值

解读:
- K=1 到 K=3:SSE 急剧下降(信息增益大)
- K=3 到 K=7:SSE 下降变缓(增加K已无太大意义)
- 拐点处(肘部)的 K 就是最佳选择

类比:就像你搬家,从 1 间房换到 3 间房,幸福感大增; 但从 3 间换到 7 间,幸福感增加就很有限了。 那 3 间就是最佳"性价比"。

# ============================================
# 肘部法则:确定最佳 K 值
# ============================================
from sklearn.cluster import KMeans
import numpy as np

# 示例数据
np.random.seed(42)
X = np.vstack([
    np.random.randn(50, 2) + [2, 2],     # 簇 1
    np.random.randn(50, 2) + [8, 8],     # 簇 2
    np.random.randn(50, 2) + [2, 8],     # 簇 3
])

# 尝试不同的 K 值
sse_list = []
K_range = range(1, 10)

for k in K_range:
    kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
    kmeans.fit(X)
    sse_list.append(kmeans.inertia_)  # inertia_ 就是 SSE

# 打印结果
print("K 值  |  SSE(组内误差)")
print("─" * 30)
for k, sse in zip(K_range, sse_list):
    bar = "█" * int(sse / 50)
    print(f"  {k}   |  {sse:8.1f}  {bar}")

# 输出类似:
# K 值  |  SSE
# ──────────────────────────────
#   1   |   3200.5  ████████████████████████████
#   2   |   1500.2  ██████████████
#   3   |    280.1  ██            ← 这里明显下降变缓,选 K=3
#   4   |    260.3  ██
#   5   |    240.5  ██

3.5 实战:客户分群

这是 K-Means 最经典的应用之一:根据客户的消费行为把他们分群, 从而进行精准营销。

# ============================================
# K-Means 实战:客户分群
# ============================================
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
import numpy as np

# 客户数据:[年收入(万元), 年消费金额(万元)]
customers = np.array([
    # 低收入低消费群体
    [15, 5],  [18, 8],  [20, 10], [12, 3],  [16, 7],
    # 高收入高消费群体
    [80, 70], [85, 75], [90, 80], [78, 65], [88, 78],
    # 高收入低消费群体(节俭型)
    [75, 10], [82, 12], [70, 8],  [78, 15], [85, 11],
])

# 数据标准化(非常重要!消除量纲影响)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(customers)

# K-Means 聚类,分成 3 组
kmeans = KMeans(n_clusters=3, random_state=42, n_init=10)
labels = kmeans.fit_predict(X_scaled)

# 查看每个客户的分组
group_names = {0: "?", 1: "?", 2: "?"}  # 先看结果再命名

for i, (customer, label) in enumerate(zip(customers, labels)):
    print(f"客户{i+1:2d}: 年收入={customer[0]:3.0f}万, "
          f"年消费={customer[1]:3.0f}万 → 群组 {label}")

# 查看每个簇的质心(还原到原始尺度)
centers = scaler.inverse_transform(kmeans.cluster_centers_)
print("\n各群组质心(平均特征):")
for i, center in enumerate(centers):
    print(f"  群组{i}: 平均年收入={center[0]:.0f}万, "
          f"平均年消费={center[1]:.0f}万")
预期输出分析:

┌──────────────────────────────────────────────────────┐
│  群组 0:低收入低消费群体(经济型客户)                   │
│  → 营销策略:推送高性价比产品、优惠券                    │
│                                                      │
│  群组 1:高收入高消费群体(VIP 客户)                    │
│  → 营销策略:推送高端产品、专属会员服务                   │
│                                                      │
│  群组 2:高收入低消费群体(潜力客户)                     │
│  → 营销策略:重点培养!推送个性化推荐,挖掘消费潜力        │
└──────────────────────────────────────────────────────┘
# ============================================
# 可视化客户分群结果(可选)
# ============================================
# 如果你安装了 matplotlib,可以画出漂亮的散点图

# import matplotlib.pyplot as plt
#
# colors = ['red', 'blue', 'green']
# for i in range(3):
#     mask = labels == i
#     plt.scatter(customers[mask, 0], customers[mask, 1],
#                 c=colors[i], label=f'群组{i}', s=100)
#
# # 画出质心
# plt.scatter(centers[:, 0], centers[:, 1],
#             c='black', marker='X', s=200, label='质心')
#
# plt.xlabel('年收入(万元)')
# plt.ylabel('年消费金额(万元)')
# plt.title('客户分群结果')
# plt.legend()
# plt.show()

4. KNN vs K-Means 全面对比

╔══════════════════╦═══════════════════════╦═══════════════════════╗
║     对比维度      ║     KNN(K-近邻)      ║    K-Means(K-均值)   ║
╠══════════════════╬═══════════════════════╬═══════════════════════╣
║  学习类型         ║  监督学习              ║  无监督学习            ║
║  是否需要标签     ║  需要                  ║  不需要                ║
║  任务类型         ║  分类 / 回归           ║  聚类                  ║
║  K 的含义         ║  邻居个数              ║  簇的个数              ║
║  是否有训练过程   ║  无(懒惰学习)         ║  有(迭代优化)         ║
║  预测速度         ║  慢(每次都要算距离)    ║  快(只需找最近质心)   ║
║  对异常值敏感     ║  K 较大时不太敏感       ║  敏感(会拉偏质心)     ║
║  数据需标准化     ║  是                    ║  是                    ║
║  典型应用         ║  推荐系统、手写识别      ║  客户分群、图像压缩     ║
║  算法复杂度       ║  O(n·d) 每次预测       ║  O(n·K·d·i) 总计      ║
╚══════════════════╩═══════════════════════╩═══════════════════════╝

注:n=样本数, d=特征维度, i=迭代次数

形象记忆法

    KNN 像"找朋友":                K-Means 像"分组活动":

    你是新来的同学 (?)                老师说:"大家分成3组!"
    看看身边坐的是谁                  同学们自己找相似的人
    跟谁最近就和谁一组                站到一起就是一组

    ┌─────────┐                     ┌─────────────────┐
    │ A A ? B  │                     │ ○○○  △△△  □□□  │
    │ A   B B  │                     │ ○○   △△   □□   │
    │ → ? 跟 B │                     │ → 自动形成3个组  │
    └─────────┘                     └─────────────────┘

什么时候用哪个?

你有标签数据吗?
    │
    ├── 有 → 你想做什么?
    │         │
    │         ├── 预测类别 → KNN 分类
    │         └── 预测数值 → KNN 回归
    │
    └── 没有 → 你想发现数据中的隐藏分组吗?
               │
               └── 是 → K-Means 聚类

5. 常见问题与总结

Q1:KNN 为什么叫"懒惰学习"?

因为 KNN 没有显式的训练过程。它只是把训练数据存起来,等到有新数据 需要预测时,才临时去计算距离、找邻居。所以它"训练"很快(什么都不做), 但"预测"很慢(每次都要算一遍距离)。

Q2:为什么要做数据标准化?

反面例子(不标准化):

特征A:身高(cm)  范围 150~190
特征B:年龄(岁)  范围 20~60

距离 = √[(190-150)² + (60-20)²]
     = √[1600 + 1600]

看起来两个特征贡献一样?不对!
如果身高单位换成毫米(1500~1900),距离就被身高完全主导了。

标准化后:所有特征都在同一尺度(均值0,标准差1),公平竞争。
# 标准化代码
from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# 现在每个特征的均值=0,标准差=1

Q3:K-Means 一定能找到最优解吗?

不一定! K-Means 只能找到局部最优解,结果取决于初始质心的位置。

初始化不同 → 结果可能不同:

  初始化 A:              初始化 B:
  ★ 在左边两个簇里        ★ 分布在三个区域

    ★  ·  · ·            ★  ·  · ·
  · ★  ·   ·           ·    ·   ·
       · ·    ★              · · ★  ·
       ·  ·                  · ★·
  → 可能得到错误分组       → 得到正确分组

解决办法

  • 使用 K-Means++ 初始化(sklearn 默认使用)
  • 多次运行取最优(sklearn 的 n_init=10 参数,默认运行 10 次)

Q4:K-Means 能处理非球形簇吗?

K-Means 擅长:              K-Means 不擅长:

  ○○○    □□□               ○○○○○○○○
  ○○○    □□□                 □□□□
  ○○○    □□□               ○○○○○○○○

  球形/团状簇 ✓            环形/月牙形 ✗

对于非球形簇,可以考虑 DBSCAN 等其他聚类算法。

核心要点回顾

┌────────────────────────────────────────────────────┐
│                    KNN 核心要点                      │
├────────────────────────────────────────────────────┤
│  1. 近朱者赤:看最近的 K 个邻居来做决策               │
│  2. 距离度量:欧氏距离最常用                          │
│  3. K 值选择:交叉验证,一般取奇数                    │
│  4. 数据预处理:一定要标准化!                        │
│  5. 优点:简单直观,无需训练                          │
│  6. 缺点:预测慢,高维数据效果差(维度灾难)           │
└────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────┐
│                  K-Means 核心要点                    │
├────────────────────────────────────────────────────┤
│  1. 物以类聚:自动将数据分成 K 组                     │
│  2. 迭代过程:分配 → 更新 → 分配 → 更新 ... 收敛     │
│  3. K 值选择:肘部法则(Elbow Method)                │
│  4. 初始化:优先使用 K-Means++                       │
│  5. 优点:简单高效,适合大数据集                      │
│  6. 缺点:需要指定 K,对异常值和非球形簇敏感           │
└────────────────────────────────────────────────────┘

附录:完整可运行代码汇总

# ============================================
# 完整示例:KNN 分类 + K-Means 聚类
# 安装依赖:pip install scikit-learn numpy
# ============================================
import numpy as np
from sklearn.neighbors import KNeighborsClassifier
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import cross_val_score

# ------------------------------------------
# 第一部分:KNN 分类 — 鸢尾花数据集
# ------------------------------------------
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, random_state=42
)

# 标准化
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# 用交叉验证选择最佳 K
print("=" * 50)
print("KNN:通过交叉验证选择最佳 K")
print("=" * 50)

best_k = 1
best_score = 0

for k in range(1, 20, 2):  # 只尝试奇数
    knn = KNeighborsClassifier(n_neighbors=k)
    scores = cross_val_score(knn, X_train_scaled, y_train, cv=5)
    avg_score = scores.mean()
    bar = "█" * int(avg_score * 30)
    print(f"  K={k:2d}  准确率={avg_score:.4f}  {bar}")
    if avg_score > best_score:
        best_score = avg_score
        best_k = k

print(f"\n最佳 K = {best_k},交叉验证准确率 = {best_score:.4f}")

# 用最佳 K 训练并测试
knn_best = KNeighborsClassifier(n_neighbors=best_k)
knn_best.fit(X_train_scaled, y_train)
test_score = knn_best.score(X_test_scaled, y_test)
print(f"测试集准确率 = {test_score:.4f}")

# ------------------------------------------
# 第二部分:K-Means 聚类 + 肘部法则
# ------------------------------------------
print("\n" + "=" * 50)
print("K-Means:肘部法则确定最佳 K")
print("=" * 50)

# 生成模拟数据(3个簇)
np.random.seed(42)
X_cluster = np.vstack([
    np.random.randn(100, 2) * 0.8 + [0, 0],
    np.random.randn(100, 2) * 0.8 + [5, 5],
    np.random.randn(100, 2) * 0.8 + [10, 0],
])

# 肘部法则
sse_list = []
for k in range(1, 11):
    km = KMeans(n_clusters=k, random_state=42, n_init=10)
    km.fit(X_cluster)
    sse_list.append(km.inertia_)

print("\n  K   |   SSE")
print("  " + "─" * 30)
for k, sse in zip(range(1, 11), sse_list):
    bar_len = int(sse / max(sse_list) * 40)
    bar = "█" * bar_len
    print(f"  {k:2d}  | {sse:8.1f}  {bar}")

# 用 K=3 做最终聚类
km_final = KMeans(n_clusters=3, random_state=42, n_init=10)
labels = km_final.fit_predict(X_cluster)

print(f"\n最终聚类结果(K=3):")
for i in range(3):
    count = np.sum(labels == i)
    center = km_final.cluster_centers_[i]
    print(f"  簇 {i}: {count} 个样本, "
          f"质心 = ({center[0]:.2f}, {center[1]:.2f})")

print("\n完成!")

最后的话:KNN 和 K-Means 虽然名字相似,但一个是"看邻居做决定", 一个是"自动分组"。掌握了它们,你就已经入门了监督学习和无监督学习 这两大机器学习范式。继续加油!